/* * JBoss, Home of Professional Open Source * Copyright 2011, Red Hat, Inc., and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * Licensed 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.jboss.seam.faces.component; import javax.el.MethodExpression; import javax.faces.FacesException; import javax.faces.FactoryFinder; import javax.faces.application.NavigationHandler; import javax.faces.component.ActionSource2; import javax.faces.component.FacesComponent; import javax.faces.component.UICommand; import javax.faces.component.UIComponent; import javax.faces.component.UIComponentBase; import javax.faces.component.UIViewParameter; import javax.faces.component.UIViewRoot; import javax.faces.context.FacesContext; import javax.faces.context.FacesContextWrapper; import javax.faces.el.MethodBinding; import javax.faces.event.AbortProcessingException; import javax.faces.event.ActionEvent; import javax.faces.event.ActionListener; import javax.faces.event.FacesEvent; import javax.faces.event.PhaseId; import javax.faces.event.PreRenderViewEvent; import javax.faces.lifecycle.Lifecycle; import javax.faces.lifecycle.LifecycleFactory; import javax.faces.view.ViewMetadata; import javax.faces.webapp.FacesServlet; /** * <p> * <strong>UIViewAction</strong> is an {@link ActionSource2} {@link UIComponent} that specifies an application-specific command * (or action)--defined as an EL method expression--to be invoked during one of the JSF lifecycle phases that proceeds view * rendering. This component must be declared as a child of the {@link ViewMetadata} facet of the {@link UIViewRoot} so that it * gets incorporated into the JSF lifecycle on both non-faces (initial) requests and faces (postback) requests. * </p> * <p/> * <p> * The purpose of this component is to provide a light-weight front-controller solution for executing code upon the loading of a * JSF view to support the integration of system services, content retrieval, view management, and navigation. This * functionality is especially useful for non-faces (initial) requests. * </p> * <p/> * <p> * The {@link UIViewAction} component is closely tied to the {@link UIViewParameter} component. The {@link UIViewParameter} * component binds a request parameter to a model property. Most of the time, this binding is used to populate the model with * data that supports the method being invoked by a {@link UIViewAction} component, much like form inputs populate the model * with data to support the method being invoked by a {@link UICommand} component. * </p> * <p/> * <p> * When the <literal>decode()</literal> method of the {@link UIViewAction} is invoked, it will queue an {@link ActionEvent} to * be broadcast to all interested listeners when the <literal>broadcast()</literal> method is invoked. * </p> * <p/> * <p> * If the value of the component's <literal>immediate</literal> attribute is <literal>true</literal>, the action will be invoked * during the Apply Request Values JSF lifecycle phase. Otherwise, the action will be invoked during the Invoke Application * phase, the default behavior. The phase can be set explicitly in the <literal>phase</literal> attribute, which takes * precedence over the <literal>immediate</literal> attribute. * </p> * <p/> * <p> * The invocation of the action is normally suppressed (meaning the {@link ActionEvent} is not queued) on a faces request. It * can be enabled by setting the component's <literal>onPostback</literal> attribute to <literal>true</literal>. Execution of * the method can be subject to a required condition for all requests by assigning an EL value expression of expected type * boolean to the component's <literal>if</literal> attribute, which must evaluate to <literal>true</literal> for the action to * be invoked. * </p> * <p/> * <p> * The {@link NavigationHandler} is consulted after the action is invoked to carry out the navigation case that matches the * action signature and outcome. If a navigation case is matched, or the response is marked complete by the action, subsequent * {@link UIViewAction} components associated with the current view are short-circuited. The lifecycle then advances * appropriately. * </p> * <p/> * <p> * It's important to note that the full component tree is not built before the UIViewAction components are processed on an * non-faces (initial) request. Rather, the component tree only contains the {@link ViewMetadata}, an important part of the * optimization of this component and what sets it apart from a {@link PreRenderViewEvent} listener. * </p> * * @author Dan Allen * @author Andy Schwartz * @see UIViewParameter */ @FacesComponent( // tagName = "viewAction", // namespace = "http://jboss.org/seam/faces", // (see // https://javaserverfaces-spec-public.dev.java.net/issues/show_bug.cgi?id=594) value = UIViewAction.COMPONENT_TYPE) public class UIViewAction extends UIComponentBase implements ActionSource2 { // ------------------------------------------------------ Manifest Constants /** * <p> * The standard component type for this component. * </p> */ public static final String COMPONENT_TYPE = "org.jboss.seam.faces.ViewAction"; /** * <p> * The standard component family for this component. * </p> */ public static final String COMPONENT_FAMILY = "org.jboss.seam.faces.ViewAction"; /** * Properties that are tracked by state saving. */ enum PropertyKeys { onPostback, actionExpression, immediate, phase, ifAttr("if"); private String name; PropertyKeys() { } PropertyKeys(final String name) { this.name = name; } @Override public String toString() { return name != null ? name : super.toString(); } } // ------------------------------------------------------------ Constructors /** * <p> * Create a new {@link UIViewAction} instance with default property values. * </p> */ public UIViewAction() { super(); setRendererType(null); } // -------------------------------------------------------------- Properties @Override public String getFamily() { return COMPONENT_FAMILY; } /** * {@inheritDoc} * * @deprecated This has been replaced by {@link #getActionExpression}. */ @Deprecated public MethodBinding getAction() { MethodBinding result = null; MethodExpression me; if (null != (me = getActionExpression())) { result = new MethodBindingMethodExpressionAdapter(me); } return result; } /** * {@inheritDoc} * * @throws UnsupportedOperationException if called * @deprecated This has been replaced by {@link #setActionExpression(javax.el.MethodExpression)}. */ @Deprecated public void setAction(final MethodBinding action) { throw new UnsupportedOperationException("Not supported."); } /** * Action listeners are not supported by the {@link UIViewAction} component. * * @throws UnsupportedOperationException if called */ @SuppressWarnings("deprecation") public MethodBinding getActionListener() { throw new UnsupportedOperationException("Not supported."); } /** * Action listeners are not supported by the {@link UIViewAction} component. * * @throws UnsupportedOperationException if called */ @SuppressWarnings("deprecation") public void setActionListener(final MethodBinding actionListener) { throw new UnsupportedOperationException("Not supported."); } /** * Returns the value which dictates the JSF lifecycle phase in which the action is invoked. If the value of this attribute * is <literal>true</literal>, the action will be invoked in the Apply Request Values phase. If the value of this attribute * is <literal>false</literal>, the default, the action will be invoked in the Invoke Application Phase. */ public boolean isImmediate() { return (Boolean) getStateHelper().eval(PropertyKeys.immediate, false); } /** * Sets the immediate flag, which controls the JSF lifecycle in which the action is invoked. */ public void setImmediate(final boolean immediate) { getStateHelper().put(PropertyKeys.immediate, immediate); } /** * <p> * Returns the name of the phase in which the action is to be queued. Only the following phases are supported (case does not * matter): * </p> * <ul> * <li>APPLY_REQUEST_VALUES</li> * <li>PROCESS_VALIDATIONS</li> * <li>UPDATE_MODEL_VALUES</li> * <li>INVOKE_APPLICATION</li> * </ul> * <p> * If the phase is set, it takes precedence over the immediate flag. * </p> */ public String getPhase() { String phase = (String) getStateHelper().eval(PropertyKeys.phase); if (phase != null) { phase = phase.toUpperCase(); } return phase; } /** * Set the name of the phase in which the action is to be queued. */ public void setPhase(final String phase) { getStateHelper().put(PropertyKeys.phase, phase); } public PhaseId getPhaseId() { String phase = getPhase(); if (phase == null) { return null; } if ("APPLY_REQUEST_VALUES".equals(phase)) { return PhaseId.APPLY_REQUEST_VALUES; } else if ("PROCESS_VALIDATIONS".equals(phase)) { return PhaseId.PROCESS_VALIDATIONS; } else if ("UPDATE_MODEL_VALUES".equals(phase)) { return PhaseId.UPDATE_MODEL_VALUES; } else if ("INVOKE_APPLICATION".equals(phase)) { return PhaseId.INVOKE_APPLICATION; } else if ("ANY_PHASE".equals(phase) || "RESTORE_VIEW".equals(phase) || "RENDER_RESPONSE".equals(phase)) { throw new FacesException("View actions cannot be executed in specified phase: [" + phase + "]"); } else { throw new FacesException("Not a valid phase [" + phase + "]"); } } /** * Action listeners are not supported by the {@link UIViewAction} component. * * @throws UnsupportedOperationException if called */ public void addActionListener(final ActionListener listener) { throw new UnsupportedOperationException("Not supported."); } /** * Action listeners are not supported by the {@link UIViewAction} component. */ public ActionListener[] getActionListeners() { return new ActionListener[0]; } /** * Action listeners are not supported by the {@link UIViewAction} component. * * @throws UnsupportedOperationException if called */ public void removeActionListener(final ActionListener listener) { throw new UnsupportedOperationException("Not supported."); } /** * Returns the action, represented as an EL method expression, to invoke. */ public MethodExpression getActionExpression() { return (MethodExpression) getStateHelper().get(PropertyKeys.actionExpression); } /** * Sets the action, represented as an EL method expression, to invoke. */ public void setActionExpression(final MethodExpression actionExpression) { getStateHelper().put(PropertyKeys.actionExpression, actionExpression); } /** * Returns a boolean value that controls whether the action is invoked during faces (postback) request. The default is * false. */ public boolean isOnPostback() { return (Boolean) getStateHelper().eval(PropertyKeys.onPostback, false); } /** * Set the bookean flag that controls whether the action is invoked during a faces (postback) request. */ public void setOnPostback(final boolean onPostback) { getStateHelper().put(PropertyKeys.onPostback, onPostback); } /** * Returns a condition, represented as an EL value expression, that must evaluate to true for the action to be invoked. */ public boolean isIf() { return (Boolean) getStateHelper().eval(PropertyKeys.ifAttr, true); } /** * Sets the condition, represented as an EL value expression, that must evaluate to true for the action to be invoked. */ public void setIf(final boolean condition) { getStateHelper().put(PropertyKeys.ifAttr, condition); } // ----------------------------------------------------- UIComponent Methods /** * <p> * In addition to to the default {@link UIComponent#broadcast} processing, pass the {@link ActionEvent} being broadcast to * the default {@link ActionListener} registered on the {@link javax.faces.application.Application}. * </p> * * @param event {@link FacesEvent} to be broadcast * @throws AbortProcessingException Signal the JavaServer Faces implementation that no further processing on the current * event should be performed * @throws IllegalArgumentException if the implementation class of this {@link FacesEvent} is not supported by this * component * @throws NullPointerException if <code>event</code> is <code>null</code> */ @Override public void broadcast(final FacesEvent event) throws AbortProcessingException { super.broadcast(event); FacesContext context = getFacesContext(); // OPEN QUESTION: should we consider a navigation to the same view as a // no-op navigation? // only proceed if the response has not been marked complete and // navigation to another view has not occurred if ((event instanceof ActionEvent) && !context.getResponseComplete() && (context.getViewRoot() == getViewRootOf(event))) { ActionListener listener = context.getApplication().getActionListener(); if (listener != null) { UIViewRoot viewRootBefore = context.getViewRoot(); InstrumentedFacesContext instrumentedContext = new InstrumentedFacesContext(context); // defer the call to renderResponse() that happens in // ActionListener#processAction(ActionEvent) instrumentedContext.disableRenderResponseControl().set(); listener.processAction((ActionEvent) event); instrumentedContext.restore(); // if the response is marked complete, the story is over if (!context.getResponseComplete()) { UIViewRoot viewRootAfter = context.getViewRoot(); // if the view id changed as a result of navigation, then execute // the JSF lifecycle for the new view id if (viewRootBefore != viewRootAfter) { /* * // execute the JSF lifecycle by dispatching a forward request // this approach is problematic because * it throws a wrench in the event broadcasting try { context.getExternalContext * ().dispatch(context.getApplication() .getViewHandler().getActionURL(context, * viewRootAfter.getViewId()) .substring(context.getExternalContext * ().getRequestContextPath().length())); // kill this lifecycle execution context.responseComplete(); } * catch (IOException e) { throw new FacesException("Dispatch to viewId failed: " + * viewRootAfter.getViewId(), e); } */ // manually execute the JSF lifecycle on the new view id // certain tweaks have to be made to the FacesContext to allow // us to reset the lifecycle Lifecycle lifecycle = getLifecycle(context); instrumentedContext = new InstrumentedFacesContext(context); instrumentedContext.pushViewIntoRequestMap().clearViewRoot().clearPostback().set(); lifecycle.execute(instrumentedContext); instrumentedContext.restore(); /* * Another approach would be to register a phase listener in the decode() method for the phase in which * the action is set to invoke. The phase listener would performs a servlet forward if a non-redirect * navigation occurs after the phase. */ } else { // apply the deferred call (relevant when immediate is true) context.renderResponse(); } } } } } /** * First, determine if the action should be invoked by evaluating this components pre-conditions. If this is a faces * (postback) request and the evaluated value of the postback attribute is false, take no action. If the evaluated value of * the if attribute is false, take no action. If both conditions pass, proceed with creating an {@link ActionEvent}. * <p/> * Set the phaseId in which the queued {@link ActionEvent} should be broadcast by assigning the appropriate value to the * phaseId property of the {@link ActionEvent} according to the evaluated value of the immediate attribute. If the value is * <literal>true</literal>, set the phaseId to {@link PhaseId#APPLY_REQUEST_VALUES}. Otherwise, set the phaseId to to * {@link PhaseId#INVOKE_APPLICATION}. * <p/> * Finally, queue the event by calling <literal>queueEvent()</literal> and passing the {@link ActionEvent} just created. */ @Override public void decode(final FacesContext context) { if (context == null) { throw new NullPointerException(); } if ((context.isPostback() && !isOnPostback()) || !isIf()) { return; } ActionEvent e = new ActionEvent(this); PhaseId phaseId = getPhaseId(); if (phaseId != null) { e.setPhaseId(phaseId); } else if (isImmediate()) { e.setPhaseId(PhaseId.APPLY_REQUEST_VALUES); } else { e.setPhaseId(PhaseId.INVOKE_APPLICATION); } queueEvent(e); } private UIViewRoot getViewRootOf(final FacesEvent e) { UIComponent c = e.getComponent(); do { if (c instanceof UIViewRoot) { return (UIViewRoot) c; } c = c.getParent(); } while (c != null); return null; } private Lifecycle getLifecycle(final FacesContext context) { LifecycleFactory lifecycleFactory = (LifecycleFactory) FactoryFinder.getFactory(FactoryFinder.LIFECYCLE_FACTORY); String lifecycleId = context.getExternalContext().getInitParameter(FacesServlet.LIFECYCLE_ID_ATTR); if (lifecycleId == null) { lifecycleId = LifecycleFactory.DEFAULT_LIFECYCLE; } return lifecycleFactory.getLifecycle(lifecycleId); } /** * A FacesContext delegator that gives us the necessary controls over the FacesContext to allow the execution of the * lifecycle to accomodate the UIViewAction sequence. */ private class InstrumentedFacesContext extends FacesContextWrapper { private final FacesContext wrapped; private boolean viewRootCleared = false; private boolean renderedResponseControlDisabled = false; private Boolean postback = null; public InstrumentedFacesContext(final FacesContext wrapped) { this.wrapped = wrapped; } @Override public FacesContext getWrapped() { return wrapped; } @Override public UIViewRoot getViewRoot() { if (viewRootCleared) { return null; } return wrapped.getViewRoot(); } @Override public void setViewRoot(final UIViewRoot viewRoot) { viewRootCleared = false; wrapped.setViewRoot(viewRoot); } @Override public boolean isPostback() { return postback == null ? wrapped.isPostback() : postback; } @Override public void renderResponse() { if (!renderedResponseControlDisabled) { wrapped.renderResponse(); } } /** * Make it look like we have dispatched a request using the include method. */ public InstrumentedFacesContext pushViewIntoRequestMap() { getExternalContext().getRequestMap().put("javax.servlet.include.servlet_path", wrapped.getViewRoot().getViewId()); return this; } public InstrumentedFacesContext clearPostback() { postback = false; return this; } public InstrumentedFacesContext clearViewRoot() { viewRootCleared = true; return this; } public InstrumentedFacesContext disableRenderResponseControl() { renderedResponseControlDisabled = true; return this; } public void set() { setCurrentInstance(this); } public void restore() { setCurrentInstance(wrapped); } } }