![JAR search and dependency download from the Maven repository](/logo.png)
org.jboss.seam.faces.component.UIViewAction Maven / Gradle / Ivy
/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat, Inc., and individual contributors
* 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.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;
/**
*
* UIViewAction 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.
*
*
*
* 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.
*
*
*
* 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.
*
*
*
* When the decode() method of the {@link UIViewAction} is
* invoked, it will queue an {@link ActionEvent} to be broadcast to all
* interested listeners when the broadcast() method is
* invoked.
*
*
*
* If the value of the component's immediate attribute is
* true , 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 cannot be set
* explicitly in the phase attribute, which takes precedence
* over the immediate attribute.
*
*
*
* 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 onPostback attribute to
* true . 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 if attribute, which must
* evaluate to true for the action to be invoked.
*
*
*
* 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.
*
*
*
* 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.
*
*
* @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
/**
*
* The standard component type for this component.
*
*/
public static final String COMPONENT_TYPE = "org.jboss.seam.faces.ViewAction";
/**
*
* The standard component family for this component.
*
*/
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
/**
*
* Create a new {@link UIViewAction} instance with default property values.
*
*/
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}
*
* @deprecated This has been replaced by
* {@link #setActionExpression(javax.el.MethodExpression)}.
* @throws UnsupportedOperationException if called
*/
@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
* true , the action will be invoked in the Apply Request
* Values phase. If the value of this attribute is false ,
* 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);
}
/**
*
* Returns the name of the phase in which the action is to be queued. Only
* the following phases are supported (case does not matter):
*
*
* - APPLY_REQUEST_VALUES
* - PROCESS_VALIDATIONS
* - UPDATE_MODEL_VALUES
* - INVOKE_APPLICATION
*
*
* If the phase is set, it takes precedence over the immediate flag.
*
*/
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_REPONSE".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
/**
*
* 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}.
*
*
* @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 event
is null
*/
@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}.
*
* 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 true , set the phaseId to
* {@link PhaseId#APPLY_REQUEST_VALUES}. Otherwise, set the phaseId to to
* {@link PhaseId#INVOKE_APPLICATION}.
*
* Finally, queue the event by calling queueEvent() 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);
}
}
}