/******************************************************************************
* 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.seam;
import static javax.faces.event.PhaseId.ANY_PHASE;
import static javax.faces.event.PhaseId.INVOKE_APPLICATION;
import static javax.faces.event.PhaseId.PROCESS_VALIDATIONS;
import static javax.faces.event.PhaseId.RENDER_RESPONSE;
import static javax.faces.event.PhaseId.RESTORE_VIEW;
import java.lang.reflect.Method;
import java.util.Map;
import javax.faces.application.FacesMessage;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.portlet.faces.BridgeUtil;
import org.jboss.seam.Seam;
import org.jboss.seam.contexts.Context;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.contexts.FacesLifecycle;
import org.jboss.seam.core.ConversationPropagation;
import org.jboss.seam.core.Events;
import org.jboss.seam.core.Init;
import org.jboss.seam.core.Manager;
import org.jboss.seam.exception.Exceptions;
import org.jboss.seam.faces.FacesManager;
import org.jboss.seam.faces.FacesMessages;
import org.jboss.seam.faces.FacesPage;
import org.jboss.seam.faces.Switcher;
import org.jboss.seam.faces.Validation;
import org.jboss.seam.log.LogProvider;
import org.jboss.seam.log.Logging;
import org.jboss.seam.navigation.Pages;
import org.jboss.seam.pageflow.Pageflow;
import org.jboss.seam.persistence.PersistenceContexts;
import org.jboss.seam.transaction.Transaction;
import org.jboss.seam.util.Reflections;
/**
* Manages the Seam contexts associated with a JSF request
* throughout the lifecycle of the request. Performs
* transaction demarcation when Seam transaction management
* is enabled. Hacks the JSF lifecyle to provide page
* actions and page parameters.
*
* @author Gavin King
*/
public class SeamPhaseListener implements PhaseListener
{
private static final long serialVersionUID = -91275557294550234L;
private static final LogProvider log = Logging.getLogProvider(SeamPhaseListener.class);
private static boolean exists = false;
static
{
}
public SeamPhaseListener()
{
if (exists)
{
log.warn("There should only be one Seam phase listener per application");
}
exists=true;
}
public PhaseId getPhaseId()
{
return ANY_PHASE;
}
public void beforePhase(PhaseEvent event)
{
log.trace( "before phase: " + event.getPhaseId() );
FacesLifecycle.setPhaseId( event.getPhaseId() );
try
{
if ( isPortletRequest(event) )
{
beforePortletPhase(event);
}
else
{
beforeServletPhase(event);
}
raiseEventsBeforePhase(event);
}
catch (Exception e)
{
log.error("uncaught exception", e);
try
{
Exceptions.instance().handle(e);
}
catch (Exception ehe)
{
log.error("swallowing exception", e);
}
}
}
private void beforeServletPhase(PhaseEvent event)
{
if ( event.getPhaseId() == RESTORE_VIEW )
{
beforeRestoreView( event.getFacesContext() );
}
handleTransactionsBeforePhase(event);
if ( event.getPhaseId() == RENDER_RESPONSE )
{
beforeRenderResponse( event.getFacesContext() );
}
}
private void beforePortletPhase(PhaseEvent event)
{
FacesContext facesContext = event.getFacesContext();
if ( event.getPhaseId() == RESTORE_VIEW || event.getPhaseId() == RENDER_RESPONSE )
{
beforeRestoreView(facesContext);
}
//delegate to subclass:
handleTransactionsBeforePhase(event);
if ( event.getPhaseId() == RENDER_RESPONSE )
{
afterRestoreView(facesContext);
beforeRenderResponse( event.getFacesContext() );
}
}
public void afterPhase(PhaseEvent event)
{
log.trace( "after phase: " + event.getPhaseId() );
try
{
raiseEventsAfterPhase(event);
if ( isPortletRequest(event) )
{
afterPortletPhase(event);
}
else
{
afterServletPhase(event);
}
}
catch (Exception e)
{
log.error("uncaught exception", e);
try
{
Exceptions.instance().handle(e);
// A redirect occurred inside the error handler, and we are in after
// phase, so we need to clean up now as there are no more after
// phases to be run
if ( event.getFacesContext().getResponseComplete() )
{
afterResponseComplete(event.getFacesContext());
}
}
catch (Exception ehe)
{
log.error("swallowing exception", e);
}
}
FacesLifecycle.clearPhaseId();
}
private void afterServletPhase(PhaseEvent event)
{
FacesContext facesContext = event.getFacesContext();
if ( event.getPhaseId() == RESTORE_VIEW )
{
afterRestoreView(facesContext);
}
else if ( event.getPhaseId() == INVOKE_APPLICATION )
{
afterInvokeApplication();
}
else if ( event.getPhaseId() == PROCESS_VALIDATIONS )
{
afterProcessValidations(facesContext);
}
//has to happen after, since restoreAnyConversationContext()
//can add messages
FacesMessages.afterPhase();
handleTransactionsAfterPhase(event);
if ( event.getPhaseId() == RENDER_RESPONSE )
{
afterRenderResponse(facesContext);
}
else if ( facesContext.getResponseComplete() )
{
afterResponseComplete(facesContext);
}
}
private void afterPortletPhase(PhaseEvent event)
{
FacesContext facesContext = event.getFacesContext();
if ( event.getPhaseId() == RESTORE_VIEW )
{
afterRestoreView(facesContext);
}
else if ( event.getPhaseId() == INVOKE_APPLICATION )
{
afterInvokeApplication();
}
else if ( event.getPhaseId() == PROCESS_VALIDATIONS )
{
afterProcessValidations(facesContext);
}
FacesMessages.afterPhase();
//delegate to subclass:
handleTransactionsAfterPhase(event);
if ( event.getPhaseId() == RENDER_RESPONSE )
{
//writeConversationIdToResponse( facesContext.getExternalContext().getResponse() );
afterRenderResponse(facesContext);
}
else if ( event.getPhaseId() == INVOKE_APPLICATION || facesContext.getRenderResponse() || facesContext.getResponseComplete() )
{
// Manager manager = Manager.instance();
// manager.beforeRedirect();
// if ( manager.isLongRunningConversation() )
// {
// setPortletRenderParameter(
// facesContext.getExternalContext().getResponse(),
// manager.getConversationIdParameter(),
// manager.getCurrentConversationId()
// );
// }
afterResponseComplete(facesContext);
}
}
// private static void setPortletRenderParameter(Object response, String conversationIdParameter, String conversationId)
// {
// if ( ACTION_RESPONSE.isInstance(response) )
// {
// Reflections.invokeAndWrap(SET_RENDER_PARAMETER, response, conversationIdParameter, conversationId);
// }
// }
private static boolean isPortletRequest(PhaseEvent event)
{
return BridgeUtil.isPortletRequest();
}
public void handleTransactionsBeforePhase(PhaseEvent event)
{
if ( Init.instance().isTransactionManagementEnabled() )
{
PhaseId phaseId = event.getPhaseId();
boolean beginTran = phaseId == PhaseId.RENDER_RESPONSE ||
phaseId == ( Transaction.instance().isConversationContextRequired() ? PhaseId.APPLY_REQUEST_VALUES : PhaseId.RESTORE_VIEW );
//( phaseId == PhaseId.RENDER_RESPONSE && !Init.instance().isClientSideConversations() );
if (beginTran)
{
begin(phaseId);
}
}
}
public void handleTransactionsAfterPhase(PhaseEvent event)
{
if ( Init.instance().isTransactionManagementEnabled() )
{
PhaseId phaseId = event.getPhaseId();
boolean commitTran = phaseId == PhaseId.INVOKE_APPLICATION ||
event.getFacesContext().getRenderResponse() || //TODO: no need to commit the tx if we failed to restore the view
event.getFacesContext().getResponseComplete() ||
phaseId == PhaseId.RENDER_RESPONSE;
//( phaseId == PhaseId.RENDER_RESPONSE && !Init.instance().isClientSideConversations() );
if (commitTran)
{
commitOrRollback(phaseId); //we commit before destroying contexts, cos the contexts have the PC in them
}
}
}
protected void handleTransactionsAfterPageActions(FacesContext facesContext)
{
if ( Init.instance().isTransactionManagementEnabled() )
{
commitOrRollback(PhaseId.INVOKE_APPLICATION);
if ( !facesContext.getResponseComplete() )
{
begin(PhaseId.INVOKE_APPLICATION);
}
}
}
protected void afterInvokeApplication()
{
if ( Init.instance().isTransactionManagementEnabled() )
{
addTransactionFailedMessage();
}
}
protected void afterProcessValidations(FacesContext facesContext)
{
Validation.instance().afterProcessValidations(facesContext);
}
/**
* Set up the Seam contexts, except for the conversation
* context
*/
protected void beforeRestoreView(FacesContext facesContext)
{
FacesLifecycle.beginRequest( facesContext.getExternalContext() );
}
/**
* Restore the page and conversation contexts during a JSF request
*/
protected void afterRestoreView(FacesContext facesContext)
{
FacesLifecycle.resumePage();
Map parameters = facesContext.getExternalContext().getRequestParameterMap();
ConversationPropagation.instance().restoreConversationId(parameters);
boolean conversationFound = Manager.instance().restoreConversation();
FacesLifecycle.resumeConversation( facesContext.getExternalContext() );
postRestorePage(facesContext, parameters, conversationFound);
}
public void raiseEventsBeforePhase(PhaseEvent event)
{
if ( Contexts.isApplicationContextActive() )
{
Events.instance().raiseEvent("org.jboss.seam.beforePhase", event);
}
/*if ( Contexts.isConversationContextActive() && Init.instance().isJbpmInstalled() && Pageflow.instance().isInProcess() )
{
String name;
PhaseId phaseId = event.getPhaseId();
if ( phaseId == PhaseId.PROCESS_VALIDATIONS )
{
name = "process-validations";
}
else if ( phaseId == PhaseId.UPDATE_MODEL_VALUES )
{
name = "update-model-values";
}
else if ( phaseId == PhaseId.INVOKE_APPLICATION )
{
name = "invoke-application";
}
else if ( phaseId == PhaseId.RENDER_RESPONSE )
{
name = "render-response";
}
else
{
return;
}
Pageflow.instance().processEvents(name);
}*/
}
public void raiseEventsAfterPhase(PhaseEvent event)
{
if ( Contexts.isApplicationContextActive() )
{
Events.instance().raiseEvent("org.jboss.seam.afterPhase", event);
}
}
/**
* Add a faces message when Seam-managed transactions fail.
*/
protected void addTransactionFailedMessage()
{
try
{
if ( Transaction.instance().isRolledBackOrMarkedRollback() )
{
FacesMessages.instance().addFromResourceBundleOrDefault(
FacesMessage.SEVERITY_WARN,
"org.jboss.seam.TransactionFailed",
"Transaction failed"
);
}
}
catch (Exception e) {} //swallow silently, not important
}
protected void beforeRenderResponse(FacesContext facesContext)
{
if ( Contexts.isPageContextActive() )
{
Context pageContext = Contexts.getPageContext();
//after every time that the view may have changed,
//we need to flush the page context, since the
//attribute map is being discarder
pageContext.flush();
//force refresh of the conversation lists (they are kept in PAGE context)
pageContext.remove(Seam.getComponentName(Switcher.class));
pageContext.remove("org.jboss.seam.core.conversationList");
pageContext.remove("org.jboss.seam.core.conversationStack");
}
preRenderPage(facesContext);
if ( facesContext.getResponseComplete() )
{
//workaround for a bug in MyFaces prior to 1.1.3
if ( Init.instance().isMyFacesLifecycleBug() )
{
FacesLifecycle.endRequest( facesContext.getExternalContext() );
}
}
else //if the page actions did not call responseComplete()
{
FacesMessages.instance().beforeRenderResponse();
//do this both before and after render, since conversations
//and pageflows can begin during render
FacesManager.instance().prepareBackswitch(facesContext);
}
FacesPage.instance().storeConversation();
FacesPage.instance().storePageflow();
PersistenceContexts persistenceContexts = PersistenceContexts.instance();
if (persistenceContexts != null)
{
persistenceContexts.beforeRender();
}
}
protected void afterRenderResponse(FacesContext facesContext)
{
//do this both before and after render, since conversations
//and pageflows can begin during render
FacesManager.instance().prepareBackswitch(facesContext);
PersistenceContexts persistenceContexts = PersistenceContexts.instance();
if (persistenceContexts != null)
{
persistenceContexts.afterRender();
}
ExternalContext externalContext = facesContext.getExternalContext();
Manager.instance().endRequest( externalContext.getSessionMap() );
FacesLifecycle.endRequest(externalContext);
}
protected void afterResponseComplete(FacesContext facesContext)
{
//responseComplete() was called by one of the other phases,
//so we will never get to the RENDER_RESPONSE phase
//Note: we can't call Manager.instance().beforeRedirect() here,
//since a redirect is not the only reason for a responseComplete
ExternalContext externalContext = facesContext.getExternalContext();
Manager.instance().endRequest( externalContext.getSessionMap() );
FacesLifecycle.endRequest( facesContext.getExternalContext() );
}
private void postRestorePage(FacesContext facesContext, Map parameters, boolean conversationFound)
{
if ( !Pages.isDebugPage() )
{
// Only redirect to no-conversation-view if a login redirect isn't required
if (!conversationFound && !Pages.instance().isLoginRedirectRequired(facesContext))
{
Pages.instance().redirectToNoConversationView();
}
Manager.instance().handleConversationPropagation(parameters);
if ( Init.instance().isJbpmInstalled() && !isExceptionHandlerRedirect() )
{
Pageflow.instance().validatePageflow(facesContext);
}
Pages.instance().postRestore(facesContext);
}
}
private boolean preRenderPage(FacesContext facesContext)
{
if ( Pages.isDebugPage() )
{
return false;
}
else
{
FacesLifecycle.setPhaseId(PhaseId.INVOKE_APPLICATION);
boolean actionsWereCalled = false;
try
{
actionsWereCalled = Pages.instance().preRender(facesContext);
return actionsWereCalled;
}
finally
{
FacesLifecycle.setPhaseId(PhaseId.RENDER_RESPONSE);
if (actionsWereCalled)
{
FacesMessages.afterPhase();
handleTransactionsAfterPageActions(facesContext); //TODO: does it really belong in the finally?
}
}
}
}
private boolean isExceptionHandlerRedirect()
{
return Contexts.getConversationContext().isSet("org.jboss.seam.handledException");
}
void begin(PhaseId phaseId)
{
try
{
if ( !Transaction.instance().isActiveOrMarkedRollback() )
{
log.debug("beginning transaction prior to phase: " + phaseId);
Transaction.instance().begin();
}
}
catch (Exception e)
{
throw new IllegalStateException("Could not start transaction", e);
}
}
void commitOrRollback(PhaseId phaseId)
{
try
{
if ( Transaction.instance().isActive() )
{
log.debug("committing transaction after phase: " + phaseId);
Transaction.instance().commit();
}
else if ( Transaction.instance().isRolledBackOrMarkedRollback() )
{
log.debug("rolling back transaction after phase: " + phaseId);
Transaction.instance().rollback();
}
}
catch (Exception e)
{
throw new IllegalStateException("Could not commit transaction", e);
}
}
}