Is obvious
that JSF knows how to react when we click a button/link, make a selection in a
drop down, change a value in a field, etc., but how it does that ? If you don't
know how to answer to this question, then you better read this post. Beside the
answer to this question, you will see an example of how to write a custom FacesEvent
wrapper, and how to use it. This example is provided by OmniFaces.
Let's start
with the JSF event model.
So, whenever
we interact with the application by clicking a button/link, make selections in
a list, etc, we trigger an event and
JSF must handle it - this is known as event
handling (event handler).
Programmatically speaking, each event is an instance of a class, which is known
as event class. JSF recognize an
event class if it extends the javax.faces.event.FacesEvent class, which is
the base class for user interface and application events that can be fired by UIComponents.
This means that an event class is always related to an UIComponent, and the UIComponent
represents the event source or event source object. The FacesEvent
class extends the standard Java event superclass java.util.EventObject, which
represents the root class from which all event state objects shall be derived (the
JSF event model is based on the event model defined by the JavaBeans
specification - also followed by Java Swing components). Among other things, the
FacesEvent
provides a constructor that takes the UIComponent event source object (UIComponent)
as an argument, and implements a type-safe method for returning the event
source object - they have been highlighted in red below:
// source
code from Mojarra 2.2.9
package
javax.faces.event;
import
java.util.EventObject;
import
javax.faces.component.UIComponent;
import
javax.faces.component.UIViewRoot;
public
abstract class FacesEvent extends EventObject {
public FacesEvent(UIComponent component) {
super(component);
}
public UIComponent getComponent() {
return ((UIComponent)
getSource());
}
private PhaseId phaseId = PhaseId.ANY_PHASE;
public PhaseId getPhaseId() {
return phaseId;
}
public void setPhaseId(PhaseId phaseId) {
if (null == phaseId) {
throw new IllegalArgumentException();
}
this.phaseId = phaseId;
}
public void queue() {
getComponent().queueEvent(this);
}
public
abstract boolean isAppropriateListener(FacesListener listener);
public abstract void processListener(FacesListener listener);
}
JSF comes by
default with three classes that extends the FacesEvent (ActionEvent,
ValueChangeEvent
and BehaviorEvent).
Beside these
"exposed" event classes, JSF uses some private event classes also,
like the IndexedEvent
defined in UIRepeat,
but these aren't in today topic. So, let's focus on these three!
ActionEvent - Represents the activation of a user interface
component (such as a UICommand). For example, if you click a button (or a
link), you will trigger an event represented by the javax.faces.event.ActionEvent
class:
// source
code from Mojarra 2.2.9
package
javax.faces.event;
import
javax.faces.component.UIComponent;
public class
ActionEvent extends FacesEvent {
public ActionEvent(UIComponent component) {
super(component);
}
public
boolean isAppropriateListener(FacesListener listener) {
return (listener instanceof
ActionListener);
}
public void processListener(FacesListener
listener) {
((ActionListener)
listener).processAction(this);
}
}
In this
case, the event is created in the Apply Request Values phase (second phase in
JSF lifecycle). When JSF fits the request parameters map values with the
components values, it finds a request parameter whose clientId correspond to the clicked button (link). See the below
helper figure:
At this moment,
JSF creates an instance of the ActionEvent (in Mojarra, for a button, this
is happening in ButtonRenderer#decode(),
while for a link, in CommandLinkRenderer#decode() method), but it doesn't
process it immediately. Actually, it will queue the event, because is possible
that not all components in the tree have their values attached yet.
// source
code from Mojarra 2.2.9 - method ButtonRenderer#decode()
@Override
public void
decode(FacesContext context, UIComponent component) {
rendererParamsNotNull(context, component);
if (!shouldDecode(component)) {
return;
}
String clientId = decodeBehaviors(context,
component);
if (wasClicked(context, component, clientId)
&& !isReset(component)) {
component.queueEvent(new
ActionEvent(component));
...
}
}
The queueEvent()
invoked above belongs to the UICommand. Below, you can see the
relevant code from the UICommand class:
// source
code from Mojarra 2.2.9 - method UIICommand#queueEvent()
public void
queueEvent(FacesEvent e) {
UIComponent
c = e.getComponent();
if (e instanceof ActionEvent && c instanceof ActionSource) {
if (((ActionSource) c).isImmediate()) {
e.setPhaseId(PhaseId.APPLY_REQUEST_VALUES);
} else {
e.setPhaseId(PhaseId.INVOKE_APPLICATION);
}
}
super.queueEvent(e);
}
The above
method intercepts the invocation of UICommandBase#queueEvent() (see super.queueEvent(e); which refers to UICommandBase#queueEvent()
method, and indirectly, to the UIViewRoot#queueEvent() method), and decide,
based on the immediate
attribute value, if the event will be processed in the Apply Request Values
(current phase) or it must wait until Invoke Application phase (fifth phase in
JSF lifecycle). The idea is to wait until the data model properties are updated
- after the Update Model Values phase. Setting the phase when this event will
be processed is accomplished via FacesEvent#setPhaseId() - see the blue
highlighted methods in FacesEvent class listed earlier. Moreover,
notice that, at this point, only events of type ActionEvent whose sources
are of type ActionSource
benefit from this treatment. The ActionSource (and ActionSource2) are interface
that may be implemented by any concrete UIComponent that wishes to
be a source of ActionEvents
(e.g. UICommand).
At the end
of Apply Request Values phase, JSF will scan the queue to process events that
have been "prepared" for this phase (if exist such events). This
takes place in, UIViewRoot#processDecodes()
method. The events "prepared" for Invoke Application phase, will be
process later, via UIViewRoot#processApplication() method. Both of these
methods uses the UIViewRoot#broadcastEvents().
BehaviorEvent - abstract class that represents event that can
be generated from component Behavior. For example, the AjaxBehaviorEvent
represents the component behavior specific to Ajax (you should know that <f:ajax>
register an AjaxBehavior instance on one or more UIComponents
implementing the ClientBehaviorHolder interface). The source code for BehaviorEvent
is:
// source
code from Mojarra 2.2.9
package
javax.faces.event;
import
javax.faces.component.UIComponent;
import
javax.faces.component.behavior.Behavior;
public
abstract class BehaviorEvent extends FacesEvent {
private
final Behavior behavior;
public BehaviorEvent(UIComponent component,
Behavior behavior) {
super(component);
if (null == behavior) {
throw new
IllegalArgumentException("Behavior agrument cannot be null");
}
this.behavior = behavior;
}
public Behavior getBehavior() {
return behavior;
}
}
And, the AjaxBehaviorEvent
source code is:
// source
code from Mojarra 2.2.9
package
javax.faces.event;
import
javax.faces.component.UIComponent;
import
javax.faces.component.behavior.Behavior;
public class
AjaxBehaviorEvent extends BehaviorEvent {
public AjaxBehaviorEvent(UIComponent
component, Behavior behavior) {
super(component,
behavior);
}
public
boolean isAppropriateListener(FacesListener listener) {
return (listener instanceof
AjaxBehaviorListener);
}
public void processListener(FacesListener
listener) {
((AjaxBehaviorListener)
listener).processAjaxBehavior(this);
}
}
In this
case, the event is created in the Apply Request Values phase (second phase in
JSF lifecycle). JSF creates an instance
of AjaxBehaviorEvent
in AjaxBehaviorRenderer#decode()
method (as a quick tip, not related to events: this class is responsible for
generating the client side script for AJAX behavior, mojarra.ab(...);). The
decode()
method is helped by two private methods, createEvent() and isImmediate().
Note AjaxBehaviorRenderer
renders Ajax behavior for a component.
All three
are listed below:
// source
code from Mojarra 2.2.9 - method AjaxBehaviorRenderer#decode()
@Override
public void
decode(FacesContext context, UIComponent component, ClientBehavior behavior) {
if (null == context || null == component ||
null == behavior) {
throw new NullPointerException();
}
if (!(behavior instanceof AjaxBehavior)) {
throw new IllegalArgumentException(
"Instance of
javax.faces.component.behavior.AjaxBehavior required: " + behavior);
}
AjaxBehavior ajaxBehavior = (AjaxBehavior)behavior;
if (ajaxBehavior.isDisabled()) {
return;
}
component.queueEvent(createEvent(component, ajaxBehavior));
if (logger.isLoggable(Level.FINE)) {
logger.fine("This command resulted
in form submission " + " AjaxBehaviorEvent queued.");
logger.log(Level.FINE, "End decoding
component {0}", component.getId());
}
}
// source
code from Mojarra 2.2.9 - method AjaxBehaviorRenderer#createEvent()
private
static AjaxBehaviorEvent createEvent(UIComponent component,
AjaxBehavior ajaxBehavior) {
AjaxBehaviorEvent event = new AjaxBehaviorEvent(component,
ajaxBehavior);
PhaseId phaseId = isImmediate(component, ajaxBehavior) ?
PhaseId.APPLY_REQUEST_VALUES :
PhaseId.INVOKE_APPLICATION;
event.setPhaseId(phaseId);
return event;
}
// source
code from Mojarra 2.2.9 - method AjaxBehaviorRenderer#isImmediate()
private
static boolean isImmediate(UIComponent component,
AjaxBehavior ajaxBehavior) {
boolean immediate = false;
if (ajaxBehavior.isImmediateSet()) {
immediate = ajaxBehavior.isImmediate();
} else if (component instanceof EditableValueHolder) {
immediate =
((EditableValueHolder)component).isImmediate();
} else if (component instanceof ActionSource) {
immediate =
((ActionSource)component).isImmediate();
}
return immediate;
}
An AJAX
behavior is attached to an UIComponent that implements the ClientBehaviorHolder
interface, represented in AjaxBehaviorRenderer#decode() as the UIComponent component
argument. For example, if we attach an AJAX behavior (via <f:ajax>)
to an UICommand
then, when an AJAX request is fired (submit behavior), the clientId of this component (UICommand) is the value of
the request parameter, javax.faces.source. This component is the
event source.
The event is
queued with respect to the value of the immediate attribute. If the immediate
attribute is not specified on the behavior then it is inherited from its parent
(the event source, which can be an instance of EditableValueHolder (e.g. UIInput)
or an instance of ActionSource (e.g. UICommand)). Depending on immediate
value, the queued event will be process in Apply Request Values phase or in
Invoke Application phase (default phase, because immediate is false
by default).
Note JSF
distinguish between an AJAX submitting behavior (<f:ajax> - behavior
action event) and a jsf.ajax.request() (not a Behavior-related request) by
inspecting the presence of javax.faces.behavior.event request parameter
in the request parameters map. This is not present in case of jsf.ajax.request().
So, when <f:ajax>
is nested in <h:commandButton>/<h:commandLink>,
JSF will queue two events for that button/link, an AjaxBehaviorEvent instance
and an ActionEvent
instance. In Apply Request Values phase, before queuing any ActionEvent,
the ButtonRenderer#decode()/CommandLinkRenderer#decode()
is responsible to decode Behaviors, if any match the behavior
source/event. For this, in Mojarra, it invokes the HtmlBasicRenderer#decodeBehaviors()
method:
// source
code from Mojarra 2.2.9 - method ButtonRenderer#decode()
@Override
public void
decode(FacesContext context, UIComponent component) {
...
String clientId = decodeBehaviors(context, component);
if (wasClicked(context, component, clientId)
&& !isReset(component)) {
component.queueEvent(new
ActionEvent(component));
...
}
}
Further,
from HtmlBasicRenderer#decodeBehaviors()
method, the flow reaches the AjaxBehaviorRenderer#decode() method, and the
AjaxBehaviorEvent
is created and queued for the proper phase (after it determines the phase, it
simply calls UICommand#queueEvent()
with an event of type AjaxBehaviorEvent - this will finally reach UIComponentBase#queueEvent()
and UIViewRoot#queueEvent()).
When the flow comes back in ButtonRenderer#decode()/CommandLinkRenderer#decode(),
the corresponding ActionEvent is created and queued, via the same UICommand#queueEvent()/UIViewRoot#queueEvent().
Summary
figure (pretty obvious, but good to point is the fact that when the AJAX
behavior is attached to an EditableValueHolder, the UICommand#queueEvent()
is not invoked!):
When the
AJAX behavior is attached to an EditableValueHolder, we usually use an
explicit listener. That listener will be invoked in Apply Request Values phase
or Invoke Application phase, depending for which phase was the event queued.
ValueChangeEvent - Signals a value change. The source code of ValueChangeEvent
is:
package
javax.faces.event;
import
javax.faces.component.UIComponent;
public class
ValueChangeEvent extends FacesEvent {
public ValueChangeEvent(UIComponent
component,
Object oldValue,
Object newValue) {
super(component);
this.oldValue = oldValue;
this.newValue = newValue;
}
private Object oldValue = null;
public Object getOldValue() {
return (this.oldValue);
}
private Object newValue = null;
public Object getNewValue() {
return (this.newValue);
}
public boolean
isAppropriateListener(FacesListener listener) {
return (listener instanceof ValueChangeListener);
}
public void processListener(FacesListener
listener) {
((ValueChangeListener)
listener).processValueChange(this);
}
}
In this
case, the event is created in the Process Validations phase. If the previous
value is different from the current new valid value, then a ValueChangeEvent
is queued, via queueEvent()
method, which gets the event class instance as argument. For example, the UIInput
reveals this code in the validate() method:
// source
code from Mojarra 2.2.9 - method UIInput#validate()
public void
validate(FacesContext context) {
...
if (isValid()) {
Object previous = getValue();
setValue(newValue);
setSubmittedValue(null);
if (compareValues(previous, newValue)) {
queueEvent(new ValueChangeEvent(this,
previous, newValue));
}
}
}
Most
probably that more than one ValueChangeEvent is queued
in this phase, because the user may have changed multiple values. The UIInput class doesn't have a
queueEvent()
method; the one invoked above is UIComponentBase#queueEvent()
method.
At the end of the Process
Validations phase, after all ValueChangeEvents
have been queued, JSF will try to process the ValueChangeEvents by scanning the queue. This
is happening in UIViewRoot#processValidators()/UIViewRoot#broadcastEvents()
methods.
But, if the event source (e.g.
UIInput)
has the immediate
attribute set to true,
then the event will be created in the Apply Request Values phase, and it will
be process at the end of this phase, via UIViewRoot#processDecodes()/UIViewRoot#broadcastEvents()
methods.
But, why the
event handling implies event classes instances to be queued ? Why they are not
processed immediately ? Well, let's suppose that we click on a button and we
submit a bulk of data. Further, in the Apply Request Values phase, JSF will
create the needed ActionEvent for our button, and will process it
immediately. If this event affects only the user interface, then this is
perfect, but let's suppose further that our event was meant for saving that
bulk of data into a database. Now, we have a problem, because this event should
be processed when all model properties have been updated with the values from
our bulk of data, otherwise we will not save the latest submitted values. We
are in Apply Request Values phase, while the model is updated in Update Model
Values phase. So, the earliest moment when the action should be processed is in
the Invoke Application phase, after Update Model phase was executed.
Until now,
we discussed only about creating and queuing events. Further, we will discuss
about listening events, which implies special interfaces, named listeners, that declares the methods
that the event source should invoke for notifying listeners of the event. JSF
provides a generic such interface, named FacesListener (extension of java.util.EventListener).
For each type of event, JSF provides a sub-interface of FacesListener
interface. In the context of our discussion, we are especially interested in ActionListener
(listener interface for receiving ActionEvents), BehaviorListener (generic
base interface for event listeners for various types of BehaviorEvents), AjaxBehaviorListener
(listener for one or more kinds of BehaviorEvents) and ValueChangeListener
(listener interface for receiving ValueChangeEvents).
Note ActionListener
is the most "famous" of these interfaces, and starting with JSF 2.2,
it even has a wrapper, named ActionListenerWrapper (simple implementation
of ActionListener
that can be extended).
In the below
source codes, you can identify the methods that should be implemented for each
of these interfaces:
// source
code from Mojarra 2.2.9 - FacesListener
package
javax.faces.event;
import
java.util.EventListener;
public
interface FacesListener extends EventListener {}
// source
code from Mojarra 2.2.9 - BehaviorListener
package
javax.faces.event;
public
interface BehaviorListener extends FacesListener {}
// source
code from Mojarra 2.2.9 - ActionListener
package
javax.faces.event;
import
javax.faces.component.UIComponent;
public interface
ActionListener extends FacesListener {
public static final String
TO_FLOW_DOCUMENT_ID_ATTR_NAME = "to-flow-document-id";
public void processAction(ActionEvent event)
throws AbortProcessingException;
}
// source
code from Mojarra 2.2.9 - ValueChangeListener
package
javax.faces.event;
import
javax.faces.component.UIComponent;
public
interface ValueChangeListener extends FacesListener {
public void
processValueChange(ValueChangeEvent event)
throws AbortProcessingException;
}
// source
code from Mojarra 2.2.9 - AjaxBehaviorListener
package
javax.faces.event;
public
interface AjaxBehaviorListener extends BehaviorListener {
public void
processAjaxBehavior(AjaxBehaviorEvent event)
throws AbortProcessingException;
}
Methods defined
in these interfaces are further implemented by classes that wants to be
informed about specific events. These classes are called event listeners.
They declare
which events they are interested in by implementing the corresponding listener
interfaces. For example, an event listener that wants to deal with the ActionEvent
will implement ActionListener,
while an event listener that want to deal with ValueChangeEvent will
implement ValueChangeListener.
For example:
import
javax.faces.event.ActionListener;
public class
MyHandler implements ActionListener {
...
public void processAction(ActionEvent e)
throws AbortProcessingException {
...
}
}
Typically,
you will use <f:actionListener>, and indicate the MyHandler
class via the type
attribute. Or, you may be more familiar with the case when the listener is
indicated as a MethodExpression via
the actionListener
attribute (for UICommands
- it supports method bindings for two
types of methods: action methods and action listener methods,
and either of it can be used to process an ActionEvent, but the action method type is
recommended for business logic and navigation) or listener
attribute (for <f:ajax>).
Further, in managed bean, you declare a public method that takes an XxxEvent parameter, with a
return type of void,
or to a public
method that takes no arguments with a return type of void.
Note The
AbortProcessingException
represents an exception that may be
thrown by event listeners to terminate the processing of the current event.
Among others,
one of the event source classes responsibility consist in indicating the type
of event they can produce (emit). For this, event source classes defines, based
on JavaBeans conventions, special methods for register/unregister event
listeners. For example, UICommand defines the following two methods
for register/unregister event of type, ActionEvent:
public void
addActionListener(ActionListener listener) {
addFacesListener(listener);
}
public void
removeActionListener(ActionListener listener) {
removeFacesListener(listener);
}
For example,
if you use the <f:actionListener> with <h:commandButton> then
the class indicated via type attribute will be instantiated, and the
instance will be passed to the above addActionListener() method.
Obviously, that class will implement ActionListener.
So, now we
have the event source and the event listener. At the right moment (phase) event
source may process the event by notifying listeners of the event. The process
of notification is known as broadcasting
(we can say that the component (event source) fires the event), and takes
places in UIViewRoot#broadcastEvents()
method. After each JSF phase, this method determines the events that
should be broadcasted, and invoke the proper event source broadcast()
method. This is a massive method, that begins with the events with phaseId
set to PhaseId.ANY_PHASE,
and continue with the events queued for the current phase. The process keep
going until all the events with these phaseIds have been broadcasted. Sometimes,
processing one events may cause other events to occur. In this case, if the
occurred event matches a phaseId, it is also broadcasted.
// source
code from Mojarra 2.2.9 - UIViewRoot
public void
broadcastEvents(FacesContext context, PhaseId phaseId) {
...
List<FacesEvent> eventsForPhaseId =
events.get(PhaseId.ANY_PHASE.getOrdinal());
...
// broadcast the ANY_PHASE events first
if (null != eventsForPhaseId) {
while (!eventsForPhaseId.isEmpty()) {
...
UIComponent source = event.getComponent();
...
source.broadcast(event);
...
}
}
// then
broadcast the events for this phase.
if (null != (eventsForPhaseId =
events.get(phaseId.getOrdinal()))) {
while
(!eventsForPhaseId.isEmpty()) {
...
UIComponent source =
event.getComponent();
...
source.broadcast(event);
...
}
}
// true if we have any more ANY_PHASE events
...
}
For example,
the UICommand#broadcast()
method is responsible to broadcast the ActionEvents:
// source
code from Mojarra 2.2.9 - UICommand
public void
broadcast(FacesEvent event) throws AbortProcessingException {
// Perform standard superclass processing (including
calling our ActionListeners)
super.broadcast(event);
if (event instanceof ActionEvent) {
FacesContext context = getFacesContext();
// Notify the specified action listener
method (if any)
MethodBinding mb = getActionListener();
if (mb != null) {
mb.invoke(context, new Object[] {
event });
}
// Invoke the default ActionListener
ActionListener listener =
context.getApplication().getActionListener();
if (listener != null) {
listener.processAction((ActionEvent) event);
}
}
}
Check the above
code and notice the comment, // Invoke the default ActionListener. What is
the default ActionListener
? Well, the default ActionListener is an important wheel in the JSF event
handling mechanism. When an UICommand must fire an ActionEvent,
if follows two main steps:
·
Notify the listeners attached to this component
(of course, if there are any).
·
Invoke the default ActionListener and delegate
the event handling to it. The default ActionListener (in Mojarra, com.sun.faces.application.ActionListenerImpl)
invokes the specified application action
method, and uses the logical outcome value to invoke the default navigation
handler mechanism to determine which view should be displayed next.
OmniFaces FacesEvent Considerations
Starting
with version 2.2, JSF comes with a consistent number of wrappers for its
artifacts (e.g. ActionListenerWrapper).
Basically, a wrapper class is a simple abstract implementation of an abstract
class/interface that can be sub-classed by developers wishing to provide
specialized behavior to an existing instance (the wrapped instance) without the
need to override/implement all the methods which do not necessarily need to be
implemented. Well, OmniFaces provides a wrapper class for the FacesEvent
class, named FacesEventWrapper.
So, whenever you need to write a FacesEvent extension (e.g. ActionEvent),
you can simply extend the OmniFaces, FacesEventWrapper, instead of FacesEvent.
The implementation is listed below, but is also available on GitHub:
public
abstract class FacesEventWrapper extends FacesEvent implements
FacesWrapper<FacesEvent> {
private static final long serialVersionUID =
-2455330585230983386L;
private FacesEvent wrapped;
public FacesEventWrapper(FacesEvent wrapped,
UIComponent component) {
super(component);
this.wrapped = wrapped;
}
@Override
public void queue() {
wrapped.queue();
}
@Override
public boolean
isAppropriateListener(FacesListener listener) {
return
wrapped.isAppropriateListener(listener);
}
@Override
public void processListener(FacesListener
listener) {
wrapped.processListener(listener);
}
@Override
public PhaseId getPhaseId() {
return wrapped.getPhaseId();
}
@Override
public void setPhaseId(PhaseId phaseId) {
wrapped.setPhaseId(phaseId);
}
@Override
public FacesEvent getWrapped() {
return wrapped;
}
}
For example,
OmniFaces uses this wrapper for defining a FacesEvent implementation in
Tree
component. This implementation is named, TreeFacesEvent and its main
goal is to remember the current model
node at the moment the faces event was queued (GitHub):
private
static class TreeFacesEvent extends FacesEventWrapper {
private static final long serialVersionUID =
-7751061713837227515L;
private TreeModel node;
public TreeFacesEvent(FacesEvent wrapped,
Tree tree, TreeModel node) {
super(wrapped, tree);
this.node = node;
}
public TreeModel getNode() {
return node;
}
}
Now, for the
Tree
component, OmniFaces will queue instances of TreeFacesEvent (this will
wrap the given faces event in a specific faces event which remembers the
current model node), as below:
@Override
public void
queueEvent(FacesEvent event) {
super.queueEvent(new TreeFacesEvent(event,
this, getCurrentModelNode()));
}
Moreover,
OmniFaces overrides the broadcast() method for broadcasting event
queued in the above queueEvent() method:
@Override
public void
broadcast(FacesEvent event) throws AbortProcessingException {
if (event instanceof TreeFacesEvent) {
FacesContext
context = FacesContext.getCurrentInstance();
TreeFacesEvent treeEvent =
(TreeFacesEvent) event;
final FacesEvent wrapped =
treeEvent.getWrapped();
// do tasks specific to Tree component
} else {
// rest of events are broadcasted by super
super.broadcast(event);
}
}
Done!
Niciun comentariu :
Trimiteți un comentariu