/******************************************************************************
* JBoss, a division of Red Hat *
* Copyright 2006, Red Hat Middleware, LLC, and individual *
* contributors as indicated by the @authors tag. See the *
* copyright.txt in the distribution for a full listing of *
* individual contributors. *
* *
* This is free software; you can redistribute it and/or modify it *
* under the terms of the GNU Lesser General Public License as *
* published by the Free Software Foundation; either version 2.1 of *
* the License, or (at your option) any later version. *
* *
* This software is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this software; if not, write to the Free *
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA *
* 02110-1301 USA, or see the FSF site: http://www.fsf.org. *
******************************************************************************/
package org.jboss.portletbridge.application;
import java.io.IOException;
import java.io.Writer;
import java.net.MalformedURLException;
import java.util.Locale;
import java.util.Map;
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.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.RenderResponse;
import javax.portlet.faces.Bridge;
import javax.portlet.faces.annotation.PortletNamingContainer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.portletbridge.AjaxPortletBridge;
import org.jboss.portletbridge.component.UIPortletViewRoot;
import org.jboss.portletbridge.context.PortalActionURL;
import org.jboss.portletbridge.richfaces.RichFacesHelper;
import org.jboss.portletbridge.util.FacesUtil;
/**
* @author asmirnov
*/
public class PortletViewHandler extends ViewHandlerWrapper {
private static final String FACELETS_VIEW_HANDLER_CLASS = "org.jboss.portletbridge.application.FaceletPortletViewHandler";
private static final Log _log = LogFactory.getLog(PortletViewHandler.class);
// 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 SAVESTATE_FIELD_MARKER = "~com.sun.faces.saveStateFieldMarker~";
ViewHandler parent;
private volatile Class<? extends UIViewRoot> viewRootClass;
/**
* @param parent
*/
public PortletViewHandler(ViewHandler parent) {
super();
try {
ClassLoader classLoader = Thread.currentThread()
.getContextClassLoader();
if (null == classLoader) {
classLoader = PortletViewHandler.class.getClassLoader();
}
Class<? extends ViewHandler> faceletsViewRootClass = classLoader
.loadClass(FACELETS_VIEW_HANDLER_CLASS).asSubclass(
ViewHandler.class);
parent = faceletsViewRootClass.getConstructor(ViewHandler.class)
.newInstance(parent);
} catch (Throwable e) {
_log.info("No Facelets library is presented");
}
this.parent = parent;
getViewRootClass();
}
private Class<? extends UIViewRoot> getViewRootClass() {
if (null == viewRootClass) {
// JDK-5 Double Checked Locking
synchronized (this) {
if (null == viewRootClass) {
try {
RichFacesHelper richFacesHelper = new RichFacesHelper();
viewRootClass = richFacesHelper.getViewRootClass();
} catch (NoClassDefFoundError e) {
viewRootClass = UIPortletViewRoot.class;
}
}
}
}
return viewRootClass;
}
@Override
public Locale calculateLocale(FacesContext context) {
Locale locale;
if (FacesUtil.isPortletRequest(context)) {
locale = context.getExternalContext().getRequestLocale();
if (null == locale) {
locale = super.calculateLocale(context);
}
} else {
locale = super.calculateLocale(context);
}
return locale;
}
public UIViewRoot createView(FacesContext facesContext, String viewId) {
boolean portletRequest = FacesUtil.isPortletRequest(facesContext);
if (portletRequest) {
viewId = evaluateUrl(facesContext, viewId);
try {
PortalActionURL viewIdUrl = new PortalActionURL(viewId);
viewId = viewIdUrl.getPath();
Map<String, String[]> viewIdParameters = viewIdUrl
.getParameters();
facesContext.getExternalContext().getRequestMap().put(
AjaxPortletBridge.VIEW_ID_PARAMETERS, viewIdParameters);
} catch (MalformedURLException e) {
// Do nothing, it is ordinary view Id
_log.warn("Mailformed ViewId url", e);
}
}
UIViewRoot root = super.createView(facesContext, viewId);
Class<? extends UIViewRoot> rootClass = root.getClass();
if (portletRequest
&& rootClass.getAnnotation(PortletNamingContainer.class) == null) {
try {
UIViewRoot portletRoot = getViewRootClass().newInstance();
portletRoot.setViewId(root.getViewId());
portletRoot.setLocale(root.getLocale());
portletRoot.setRenderKitId(root.getRenderKitId());
root = portletRoot;
} catch (Exception e) {
throw new FacesException(e);
}
}
return root;
}
@Override
public void writeState(FacesContext context) throws IOException {
StringBuilderWriter stringBuilderWriter = StringBuilderWriter
.getInstance();
if (null != stringBuilderWriter) {
stringBuilderWriter.stateWrited();
context.getResponseWriter().write(SAVESTATE_FIELD_MARKER);
} else {
super.writeState(context);
}
}
public String getActionURL(FacesContext context, String url) {
// 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. Only executed during portlet request or AJAX
// request
// from portlet page.
url = evaluateUrl(context, url);
return super.getActionURL(context, url);
}
protected String evaluateUrl(FacesContext context, String url) {
if (url.startsWith("#")) {
// evaluate this as an EL expression
url = (String) context.getApplication().evaluateExpressionGet(
context, url, String.class);
if (url == null) {
throw new FacesException("Evaluated view ID is null " + url);
}
}
return url;
}
/**
* @deprecated Use {@link FacesUtil#isPortletRequest(FacesContext)} instead
*/
public static boolean isPortletRequest(FacesContext context) {
return FacesUtil.isPortletRequest(context);
}
// TODO - create own implementation. Now, this code are copied from MyFaces
// implementation.
@Override
public void renderView(FacesContext context, UIViewRoot viewToRender)
throws IOException, FacesException {
// Get the renderPolicy from the init parameters
ExternalContext externalContext = context.getExternalContext();
String renderPolicyParam = externalContext
.getInitParameter(Bridge.BRIDGE_PACKAGE_PREFIX
+ Bridge.RENDER_POLICY);
Bridge.BridgeRenderPolicy renderPolicy;
if (renderPolicyParam == null) {
renderPolicy = Bridge.BridgeRenderPolicy.DEFAULT;
} else {
renderPolicy = Bridge.BridgeRenderPolicy.valueOf(renderPolicyParam);
}
if (null == externalContext.getRequestMap().get(
Bridge.PORTLET_LIFECYCLE_PHASE)
|| renderPolicy == Bridge.BridgeRenderPolicy.ALWAYS_DELEGATE) {
super.renderView(context, viewToRender);
return;
} else if (renderPolicy == Bridge.BridgeRenderPolicy.DEFAULT) {
try {
super.renderView(context, viewToRender);
return;
} catch (Throwable t) {
if (_log.isDebugEnabled()) {
_log
.debug(
"Error rendering view by parent ViewHandler, try to render as portletbridge JSP page",
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 = externalContext;
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 portletbridge
// extContext.dispatch(viewToRender.getViewId());
if (executePageToBuildView(context, viewToRender)) {
renderResponse.flushBuffer();
return;
}
} catch (IOException e) {
throw new FacesException(e);
}
// 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,
renderResponse.getWriter(), 4096);
try {
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.
strWriter.flushToWriter();
} finally {
strWriter.release();
}
if (null != oldWriter) {
context.setResponseWriter(oldWriter);
}
renderResponse.flushBuffer();
}
/**
* <p>
* This is a separate method to account for handling the content after the
* view tag.
* </p>
* <p/>
* <p>
* Create a new ResponseWriter around this response's Writer. Set it into
* the FacesContext, saving the old one aside.
* </p>
* <p/>
* <p>
* call encodeBegin(), encodeChildren(), encodeEnd() on the argument
* <code>UIViewRoot</code>.
* </p>
* <p/>
* <p>
* Restore the old ResponseWriter into the FacesContext.
* </p>
* <p/>
* <p>
* Write out the after view content to the response's writer.
* </p>
* <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 static final ThreadLocal<StringBuilderWriter> instance = new ThreadLocal<StringBuilderWriter>();
private final StringBuilder mBuilder;
private final FacesContext context;
private final Writer responseWriter;
private boolean stateWrited = false;
private static final int SAVESTATE_MARK_LEN = SAVESTATE_FIELD_MARKER
.length();
public StringBuilderWriter(FacesContext context, Writer responseWriter,
int initialCapacity) {
if (initialCapacity < 0) {
throw new IllegalArgumentException();
}
mBuilder = new StringBuilder(initialCapacity);
this.context = context;
this.responseWriter = responseWriter;
instance.set(this);
}
public void release() {
instance.remove();
}
public void stateWrited() {
this.stateWrited = true;
}
public static StringBuilderWriter getInstance() {
return instance.get();
}
@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;
}
if (stateWrited) {
mBuilder.append(cbuf, off, len);
} else {
responseWriter.write(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) throws IOException {
if (stateWrited) {
mBuilder.append(str);
} else {
responseWriter.write(str);
}
}
@Override
public void write(String str, int off, int len) throws IOException {
if (stateWrited) {
mBuilder.append(str, off, off + len);
} else {
responseWriter.write(str, off, len);
}
}
public StringBuilder getBuffer() {
return mBuilder;
}
@Override
public String toString() {
return mBuilder.toString();
}
public void flushToWriter() throws IOException {
// TODO: Buffer?
if (stateWrited) {
StateManager stateManager = context.getApplication()
.getStateManager();
ResponseWriter oldResponseWriter = context.getResponseWriter();
context.setResponseWriter(oldResponseWriter
.cloneWithWriter(responseWriter));
Object stateToWrite = stateManager.saveView(context);
int pos = 0;
int tildeIdx = mBuilder.indexOf(SAVESTATE_FIELD_MARKER);
while (tildeIdx >= 0) {
responseWriter.write(mBuilder.substring(pos, tildeIdx));
stateManager.writeState(context, stateToWrite);
pos = tildeIdx + SAVESTATE_MARK_LEN;
tildeIdx = mBuilder.indexOf(SAVESTATE_FIELD_MARKER, pos);
}
responseWriter.write(mBuilder.substring(pos));
context.setResponseWriter(oldResponseWriter);
}
}
}
/**
* Execute the target view. If the HTTP status code range is not 2xx, then
* return true to indicate the response should be immediately flushed by the
* caller so that conditions such as 404 are properly handled.
*
* @param context
* the <code>FacesContext</code> for the current request
* @param viewToExecute
* the view to build
* @return <code>true</code> if the response should be immediately flushed
* to the client, otherwise <code>false</code>
* @throws IOException
* if an error occurs executing the page
*/
private boolean executePageToBuildView(FacesContext context,
UIViewRoot viewToExecute) throws IOException {
String requestURI = viewToExecute.getViewId();
ExternalContext extContext = context.getExternalContext();
extContext.dispatch(requestURI);
return false;
}
@Override
protected ViewHandler getWrapped() {
return parent;
}
}