/* 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.ByteArrayOutputStream;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import javax.faces.application.Application;
import javax.faces.FacesException;
import javax.faces.FactoryFinder;
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.MimeResponse;
import javax.portlet.PortletContext;
import javax.portlet.RenderResponse;
import javax.portlet.ResourceResponse;
import javax.portlet.faces.Bridge;
import javax.portlet.faces.BridgeUtil;
import javax.portlet.faces.BridgeWriteBehindResponse;
import javax.portlet.faces.annotation.PortletNamingContainer;
import javax.portlet.faces.component.PortletNamingContainerUIViewRoot;
import org.apache.myfaces.portlet.faces.application.BridgeViewHandlerRenderResponseWrapper;
import org.apache.myfaces.portlet.faces.application.BridgeViewHandlerResourceResponseWrapper;
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())
{
return super.getActionURL(context, viewId);
}
// else its a portlet request so processing accoring to bridge rules
/* 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));
// Now add the parameters back on (if needed)
if (qsLoc < viewId.length())
{
int aLoc = actionURL.indexOf('?');
if (aLoc < 0)
{
actionURL = actionURL.concat(viewId.substring(qsLoc));
}
else if (viewId.length() > (qsLoc + 1))
{
actionURL = actionURL + "&" + viewId.substring(qsLoc + 1);
}
}
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())
{
return super.createView(facesContext, viewId);
}
// else its a portlet request so processing accoring to bridge rules
/* 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())
{
// Add the QueryString as an additional attribute to the UIViewRoot.
// Note: only expected situation is when a NavigationHandler returns a viewId with a QS
// as we support this in the faces-config.xml to introduce modes, etc.
// The Bridge (ActionHandler) will pull this when it constructs the target
// of the actionResponse based on this current ViewId.
viewRoot.getAttributes().put(BridgeImpl.VIEWID_QUERYSTRING_ATTRIBUTE, 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;
}
ExternalContext extContext = context.getExternalContext();
MimeResponse mimeResponse = (MimeResponse) extContext.getResponse();
// 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 -- Note because delegate may have set the Response object and
// not cleaned it up on the exception -- reset to ensure a good object
if (!mimeResponse.equals(extContext.getResponse()))
{
extContext.setResponse(mimeResponse);
}
}
}
// suppress rendering if "rendered" property on the component is
// false
if (!viewToRender.isRendered())
{
return;
}
try
{
extContext.getRequestMap().remove("javax.servlet.include.servlet_path");
dispatchPageToBuildView(context, extContext, viewToRender);
}
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))
{
// redirect must have occurred during dispatch
// set a flag indicating we are in a renderRedirect so the ViewHandler will know to dispatch.include instead of forward
// mark it as such so the next dispatch will use an include
extContext.getRequestMap().put(BridgeImpl.HAS_RENDER_REDIRECTED_AFTER_FORWARD, Boolean.TRUE);
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,
mimeResponse.getCharacterEncoding());
}
context.setResponseWriter(newWriter);
newWriter.startDocument();
doRenderView(context, viewToRender);
newWriter.endDocument();
// Check again to see if the processing of the Faces view triggered a redirect
redirectParams = (QueryString) context.getExternalContext()
.getRequestMap().get(BridgeImpl.REDIRECT_VIEWPARAMS);
if ((redirectParams != null))
{
// set a flag indicating we are in a renderRedirect so the ViewHandler will know to dispatch.include instead of forward
// mark it as such so the next dispatch will use an include
extContext.getRequestMap().put(BridgeImpl.HAS_RENDER_REDIRECTED_AFTER_FORWARD, Boolean.TRUE);
return;
}
// 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 = mimeResponse.getWriter();
}
catch (IllegalStateException ise) {
// got this exception because we've called getOutputStream() previously
renderResponseWriter = new BufferedWriter(
new OutputStreamWriter(
mimeResponse.getPortletOutputStream(),
mimeResponse.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)
{
try
{
if (content instanceof char[])
{
mimeResponse.getWriter().write(new String((char[]) content));
}
else if (content instanceof byte[])
{
mimeResponse.getWriter().write(new String((byte[]) content));
}
else
{
throw new IOException("PortletViewHandlerImpl: invalid" + "AFTER_VIEW_CONTENT buffer type");
}
}
finally
{
// So it doesn't geet reused accidentaly
extContext.getRequestMap().remove(Bridge.AFTER_VIEW_CONTENT);
}
}
mimeResponse.flushBuffer();
}
private void dispatchPageToBuildView(FacesContext context,
ExternalContext extContext,
UIViewRoot viewToRender)
throws IOException
{
String viewURI = viewToRender.getViewId();
// update the JSTL locale attribute in request scope so that JSTL
// picks up the locale from viewRoot. This attribute must be updated
// before the JSTL setBundle tag is called because that is when the
// new LocalizationContext object is created based on the locale.
extContext.getRequestMap().put("javax.servlet.jsp.jstl.fmt.locale", context.getViewRoot().getLocale());
// save the original response
MimeResponse originalResponse = (MimeResponse) extContext.getResponse();
// see whether the portlet wants to use a writeBehindResponse(Wrapper)
Class<? extends BridgeWriteBehindResponse> writeBehindResponseWrapperClass = (Class<? extends BridgeWriteBehindResponse>) extContext.getRequestMap().get(BridgeImpl.WRITE_BEHIND_RESPONSE);
BridgeWriteBehindResponse wrapped = null;
if (writeBehindResponseWrapperClass != null)
{
try
{
wrapped = writeBehindResponseWrapperClass.newInstance();
if ((BridgeUtil.getPortletRequestPhase() == Bridge.PortletPhase.RENDER_PHASE && wrapped instanceof RenderResponse) ||
(BridgeUtil.getPortletRequestPhase() == Bridge.PortletPhase.RESOURCE_PHASE && wrapped instanceof ResourceResponse))
{
extContext.setResponse(wrapped);
}
else
{
// warn and don't use
wrapped = null;
extContext.log("Unable to use configured BridgeWriteBehindResponse instance because its not the appropriate PortletResponseWrapper for the associated phase.");
}
} catch (Exception e)
{
// Just warn and don't use;
extContext.log("Unable to instantiate configured BridgeWriteBehindResponse instance: ", e);
}
}
else
{
// Use the generic (non-writeBehindResponse) wrapper
if (BridgeUtil.getPortletRequestPhase() == Bridge.PortletPhase.RENDER_PHASE)
{
wrapped = new BridgeViewHandlerRenderResponseWrapper();
}
else
{
wrapped = new BridgeViewHandlerResourceResponseWrapper();
}
extContext.setResponse(wrapped);
// don't set write behind as this one doesn't support the Faces specific implementation dependent interfaces
}
// 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);
// build the view by executing the page
extContext.dispatch(viewURI);
if (wrapped != null)
{
// replace the original response
extContext.setResponse(originalResponse);
// Check whether our wrapper participated in the the writeBehind mechanism and/or
// something else already did (servlet filter)
if (wrapped.hasFacesWriteBehindMarkup() && extContext.getRequestMap().get(Bridge.AFTER_VIEW_CONTENT) == null)
{
// Put the AFTER_VIEW_CONTENT into request scope
// temporarily
Object o = (wrapped.isChars()) ? (Object) wrapped.getChars() : (Object) wrapped.getBytes();
if (o != null)
{
extContext.getRequestMap().put(Bridge.AFTER_VIEW_CONTENT, o);
}
}
else
{
// write ahead -- just write this to the restored response
wrapped.flushMarkupToWrappedResponse();
}
}
}
/**
* <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
}