/* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.myfaces.portlet.faces.application;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Locale;
import java.util.Map;
import javax.faces.FacesException;
import javax.faces.FactoryFinder;
import javax.faces.application.Application;
import javax.faces.application.FacesMessage;
import javax.faces.application.StateManager;
import javax.faces.application.ViewHandler;
import javax.faces.application.ViewHandlerWrapper;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.RenderKit;
import javax.faces.render.RenderKitFactory;
import javax.portlet.PortletContext;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.faces.Bridge;
import javax.portlet.faces.BridgeUtil;
import javax.portlet.faces.annotation.PortletNamingContainer;
import javax.portlet.faces.component.PortletNamingContainerUIViewRoot;
import org.apache.myfaces.portlet.faces.bridge.BridgeImpl;
import org.apache.myfaces.portlet.faces.util.QueryString;
/**
* View handler implementation for JSF portlet bridge.
*
* The only method we override here is getActionURL().
*
* TODO JSF 1.2 note: JSF 1.2 RI implements ViewHandler.renderView() differently in order to handle
* emitting non-JSF markup that follows the JSF tags after the JSF renders correctly. Unfortunately,
* the RI handles this by introducing several servlet dependencies. Currently, the bridge handles
* this by overriding the renderView() and ignoring (not interleafing) the non-JSF markup - see HACK
* below
*/
public class PortletViewHandlerImpl extends ViewHandlerWrapper
{
// the ViewHandler to delegate to
private ViewHandler mDelegate;
private Bridge.BridgeRenderPolicy mRenderPolicy = null;
public PortletViewHandlerImpl(ViewHandler handler)
{
mDelegate = handler;
}
protected ViewHandler getWrapped()
{
return mDelegate;
}
@Override
public String getActionURL(FacesContext context, String viewId)
{
// action URLs are processed by the bridge in encodeActionURL
// however the bridge extends Faces navigation rule support in that it
// allows a to-view-id element to contain an EL expression.
// We recognize this EL expresion here and evaluate to a viewid
// before delegating// Do nothing when not running in portlet request
if (BridgeUtil.isPortletRequest())
{
/* Note: later versions of glassfish jsf append a / if not in the nav rule
* -- remove it if blocking the expression.
*/
if (viewId.startsWith("/#"))
{
viewId = viewId.substring(1);
}
if (viewId.startsWith("#"))
{
// evaluate this as an EL expression
viewId = (String) context.getApplication().evaluateExpressionGet(context, viewId, String.class);
if (viewId == null)
{
//TODO: at least log an error.
}
}
}
// Faces can't do suffix mapping (extension mapping) properly if there is a query string
int qsLoc = viewId.indexOf('?');
if (qsLoc < 0) qsLoc = viewId.length();
String actionURL = super.getActionURL(context, viewId.substring(0, qsLoc));
if (qsLoc < viewId.length())
actionURL = actionURL.concat(viewId.substring(qsLoc));
return actionURL;
}
@Override
public UIViewRoot createView(FacesContext facesContext, String viewId)
{
// The Bridge extends Faces navigation rule support in that it
// allows a to-view-id element to contain an EL expression.
// We recognize this EL expresion here and evaluate to a viewid
// before delegating
// Do nothing when not running in portlet request
if (BridgeUtil.isPortletRequest())
{
/* Note: later versions of glassfish jsf append a / if not in the nav rule
* -- remove it if blocking the expression.
*/
if (viewId.startsWith("/#"))
{
viewId = viewId.substring(1);
}
if (viewId.startsWith("#"))
{
// evaluate this as an EL expression
viewId = (String) facesContext.getApplication().evaluateExpressionGet(facesContext, viewId, String.class);
if (viewId == null)
{
//TODO: at least log an error.
}
}
}
UIViewRoot viewRoot = null;
// The bridge allows viewIds to carry querystrings -- e.g. a qs that has the
// mode parameter. Unfortunately, underlying Faces impls don't expect any query
// string when mapping from the viewId to the view -- in particular they don't
// account for the query string when the Faces servlet is suffix mapped (extension
// mapped). So strip the query string here -- create the view and add back
int qsLoc = viewId.indexOf('?');
if (qsLoc < 0)
qsLoc = viewId.length();
viewRoot = super.createView(facesContext, viewId.substring(0, qsLoc));
// As we have provided an ApplicationImpl to override createComponent
// to detect when to swap in our NamingContainer the returned viewRoot
// from above should already be the one we want. Because its possible
// its still the native UIViewRoot -- i.e. createComponent wasn't called
// the following code checks/and does the right thing to satisfy the spec.
if (viewRoot.getClass() == UIViewRoot.class &&
UIViewRoot.class.getAnnotation(PortletNamingContainer.class) == null)
{
String prior = setUIViewRootComponent(facesContext);
try
{
viewRoot = super.createView(facesContext, viewId.substring(0, qsLoc));
}
finally
{
if (prior != null)
resetUIViewRootComponent(facesContext, prior);
}
}
if (qsLoc < viewId.length())
{
viewRoot.setViewId(viewRoot.getViewId().concat(viewId.substring(qsLoc)));
}
return viewRoot;
}
private String setUIViewRootComponent(FacesContext context)
{
Application app = context.getApplication();
UIComponent root = app.createComponent(UIViewRoot.COMPONENT_TYPE);
if (root == null || (root.getClass() == UIViewRoot.class &&
root.getClass().getAnnotation(PortletNamingContainer.class) == null))
{
app.addComponent(UIViewRoot.COMPONENT_TYPE, PortletNamingContainerUIViewRoot.class.getName());
return UIViewRoot.class.getName();
}
return null;
}
private void resetUIViewRootComponent(FacesContext context, String componentClassName)
{
if (componentClassName != null)
{
context.getApplication().addComponent(UIViewRoot.COMPONENT_TYPE, componentClassName);
}
}
@Override
public void renderView(FacesContext context, UIViewRoot viewToRender) throws IOException,
FacesException
{
// Do nothing when not running in portlet request
if (!BridgeUtil.isPortletRequest())
{
super.renderView(context, viewToRender);
return;
}
// If first time -- Get the renderPolicy from the context init parameter
if (mRenderPolicy == null)
{
PortletContext pCtx = (PortletContext) context.getExternalContext().getContext();
String policy = pCtx.getInitParameter(Bridge.RENDER_POLICY);
if (policy != null)
{
mRenderPolicy = Bridge.BridgeRenderPolicy.valueOf(policy);
}
else
{
mRenderPolicy = Bridge.BridgeRenderPolicy.DEFAULT;
}
}
if (mRenderPolicy == Bridge.BridgeRenderPolicy.ALWAYS_DELEGATE)
{
super.renderView(context, viewToRender);
return;
}
else if (mRenderPolicy == Bridge.BridgeRenderPolicy.DEFAULT)
{
try
{
super.renderView(context, viewToRender);
return;
}
catch (Throwable t)
{
// catch all throws and swallow -- falling through to our own
// render
}
}
// suppress rendering if "rendered" property on the component is
// false
if (!viewToRender.isRendered())
{
return;
}
ExternalContext extContext = context.getExternalContext();
RenderResponse renderResponse = (RenderResponse) extContext.getResponse();
try
{
// set request attribute indicating we can deal with content
// that is supposed to be delayed until after JSF tree is ouput.
extContext.getRequestMap().put(Bridge.RENDER_CONTENT_AFTER_VIEW, Boolean.TRUE);
// TODO JSF 1.2 - executePageToBuildView() creates
// ViewHandlerResponseWrapper
// to handle error page and text that exists after the <f:view> tag
// among other things which have lots of servlet dependencies -
// we're skipping this for now for portlet
// Bridge has had to set this attribute so Faces RI will skip servlet dependent
// code when mapping from request paths to viewIds -- however we need to remove it
// as it screws up the dispatch
extContext.getRequestMap().remove("javax.servlet.include.servlet_path");
extContext.dispatch(viewToRender.getViewId());
/*
* if (executePageToBuildView(context, viewToRender)) { response.flushBuffer(); return; }
*/
}
catch (IOException e)
{
throw new FacesException(e);
}
// If a redirect occurred -- merely return
// check here to see if a redirect occurred -- if so rerun doFacesRequest
// for this new view
QueryString redirectParams = (QueryString) context.getExternalContext()
.getRequestMap().get(BridgeImpl.REDIRECT_VIEWPARAMS);
if ((redirectParams != null))
{
return;
}
// set up the ResponseWriter
RenderKitFactory renderFactory = (RenderKitFactory) FactoryFinder
.getFactory(FactoryFinder.RENDER_KIT_FACTORY);
RenderKit renderKit = renderFactory.getRenderKit(context, viewToRender.getRenderKitId());
ResponseWriter oldWriter = context.getResponseWriter();
StringBuilderWriter strWriter = new StringBuilderWriter(context, 4096);
ResponseWriter newWriter;
if (null != oldWriter)
{
newWriter = oldWriter.cloneWithWriter(strWriter);
}
else
{
newWriter = renderKit.createResponseWriter(strWriter, null,
renderResponse.getCharacterEncoding());
}
context.setResponseWriter(newWriter);
newWriter.startDocument();
doRenderView(context, viewToRender);
newWriter.endDocument();
// replace markers in the body content and write it to response.
ResponseWriter responseWriter;
// Dispatch may have output to an OutputStream instead of a Writer
Writer renderResponseWriter = null;
try {
renderResponseWriter = renderResponse.getWriter();
}
catch (IllegalStateException ise) {
// got this exception because we've called getOutputStream() previously
renderResponseWriter = new BufferedWriter(
new OutputStreamWriter(
renderResponse.getPortletOutputStream(),
renderResponse.getCharacterEncoding()));
}
if (null != oldWriter)
{
responseWriter = oldWriter.cloneWithWriter(renderResponseWriter);
}
else
{
responseWriter = newWriter.cloneWithWriter(renderResponseWriter);
}
context.setResponseWriter(responseWriter);
strWriter.write(responseWriter);
renderResponseWriter.flush();
if (null != oldWriter)
{
context.setResponseWriter(oldWriter);
}
Object content = extContext.getRequestMap().get(Bridge.AFTER_VIEW_CONTENT);
if (content != null)
{
if (content instanceof char[])
{
renderResponse.getWriter().write(new String((char[]) content));
}
else if (content instanceof byte[])
{
renderResponse.getWriter().write(new String((byte[]) content));
}
else
{
throw new IOException("PortletViewHandlerImpl: invalid" + "AFTER_VIEW_CONTENT buffer type");
}
}
renderResponse.flushBuffer();
}
/**
* <p>
* This is a separate method to account for handling the content after the view tag.
* </p>
*
* <p>
* Create a new ResponseWriter around this response's Writer. Set it into the FacesContext, saving
* the old one aside.
* </p>
*
* <p>
* call encodeBegin(), encodeChildren(), encodeEnd() on the argument <code>UIViewRoot</code>.
* </p>
*
* <p>
* Restore the old ResponseWriter into the FacesContext.
* </p>
*
* <p>
* Write out the after view content to the response's writer.
* </p>
*
* <p>
* Flush the response buffer, and remove the after view content from the request scope.
* </p>
*
* @param context
* the <code>FacesContext</code> for the current request
* @param viewToRender
* the view to render
* @throws IOException
* if an error occurs rendering the view to the client
*/
private void doRenderView(FacesContext context, UIViewRoot viewToRender) throws IOException,
FacesException
{
viewToRender.encodeAll(context);
}
private static final class StringBuilderWriter extends Writer
{
private StringBuilder mBuilder;
private FacesContext mContext;
// TODO: These bridge needs to use it's own constants here. This will
// confine
// us to only work with the R.I.
private static final String RI_SAVESTATE_FIELD_MARKER = "~com.sun.faces.saveStateFieldMarker~";
private static final String MYFACES_SAVESTATE_FIELD_MARKER = "<!--@@JSF_FORM_STATE_MARKER@@-->";
private static String sSaveStateFieldMarker = null;
public StringBuilderWriter(FacesContext context, int initialCapacity)
{
if (initialCapacity < 0)
{
throw new IllegalArgumentException();
}
mBuilder = new StringBuilder(initialCapacity);
mContext = context;
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException
{
if (off < 0 || off > cbuf.length || len < 0 || off + len > cbuf.length || off + len < 0)
{
throw new IndexOutOfBoundsException();
}
else if (len == 0)
{
return;
}
mBuilder.append(cbuf, off, len);
}
@Override
public void flush() throws IOException
{
}
@Override
public void close() throws IOException
{
}
/**
* Write a string.
*
* @param str
* String to be written
*/
@Override
public void write(String str)
{
mBuilder.append(str);
}
@Override
public void write(String str, int off, int len)
{
write(str.substring(off, off + len));
}
public StringBuilder getBuffer()
{
return mBuilder;
}
@Override
public String toString()
{
return mBuilder.toString();
}
public void write(Writer writer) throws IOException
{
// See if we already have determined the SAVESTATE_FIELD_MARKER in use
// If not then determine it and set for future use
if (sSaveStateFieldMarker == null)
{
sSaveStateFieldMarker = determineSaveStateFieldMarker();
}
// TODO: Buffer?
int pos = 0;
// First we need to make sure we save the view
StateManager stateManager = mContext.getApplication().getStateManager();
Object stateToWrite = stateManager.saveView(mContext);
// If we didn't find a savestate_field_marker don't search to replace for one.
if (sSaveStateFieldMarker != null)
{
int markLen = sSaveStateFieldMarker.length();
int tildeIdx = mBuilder.indexOf(sSaveStateFieldMarker);
while (tildeIdx > 0)
{
writer.write(mBuilder.substring(pos, tildeIdx));
stateManager.writeState(mContext, stateToWrite);
pos = tildeIdx + markLen;
tildeIdx = mBuilder.indexOf(sSaveStateFieldMarker, pos);
}
}
writer.write(mBuilder.substring(pos));
}
private String determineSaveStateFieldMarker() throws IOException
{
// First check to see if there is one set in the configuration - if so test it first
String marker = ((PortletContext)FacesContext.getCurrentInstance().
getExternalContext().getContext()).getInitParameter(Bridge.SAVESTATE_FIELD_MARKER);
if (isMarker(marker))
{
return marker;
}
// wasn't that one so test the Faces RI marker
else if (isMarker(RI_SAVESTATE_FIELD_MARKER))
{
return RI_SAVESTATE_FIELD_MARKER;
}
// wasn't that one so test the MyFaces marker
else if (isMarker(MYFACES_SAVESTATE_FIELD_MARKER))
{
return MYFACES_SAVESTATE_FIELD_MARKER;
}
// log that we didn't find a marker
// However ignore this "exceptional" situation because its not so exceptional
// MyFaces actually directly writes the state into the response more commonly
// than it writes the Marker.
mContext.getExternalContext().log("Unable to locate a SAVESTATE_FIELD_MARKER in response. This could be because your Faces environment doesn't write such a marker or because the bridge doesn't know the marker in use. If the later, configure the appropriate application init parameter javax.portlet.faces.SAVESTATE_FIELD_MARKER.");
return null;
}
private boolean isMarker(String marker)
{
return marker != null && mBuilder.indexOf(marker) >= 0;
}
}
// END TODO HACK JSF 1.2
}