Theoretical Aspects
Starring: Calling actions via
<f:viewAction/>
Starting with JSF 2.2, we can deal with calling actions on GET/POST requests
by using the new generic view action feature (well-known in Seam 2 and 3). This
new feature is materialized in the <f:viewAction> tag, which is declared as a child of
the metadata facet, <f:metadata>.
This allows the view action to be part of the JSF life cycle for faces/non-faces
requests. The main attribute is named action, and its value is a MethodExpression. The expression must evaluate to a public
method that takes no parameters, and returns void or an outcome.
<f:metadata>
...
<f:viewAction action="#{fooBean.fooAction()}"/>
...
</f:metadata>
We can also place an outcome directly in action:
<f:metadata>
...
<f:viewAction action="page"/>
...
</f:metadata>
Problem/Issue
The problem identified and fixed by OmniFaces is related to the if attribute of the <f:viewAction>
tag. Practically, via this attribute we can decide when the specified action
(indicated via the action
attribute) should be executed or not.
<f:metadata>
...
<f:viewAction if="#{foo_condition}" action="#{fooBean.fooAction()}"/>
...
</f:metadata>
The value of the if
attribute is evaluated in the Apply Request Values phase, which means that this
value wasn't converted, validated and, obviously, set in the model yet (if
there is a converter/validator specified and/or a proper model). Conversion and
validation will take place in the Process Validation phase and the model will
be updated in the Update Model Values phase; both of these phases are after the
Apply Request Values phase. Let's suppose that the if value is evaluated to true against a non-null data check, and a
custom converter will convert the checked data to null without causing any exception. This
means that the pointed action (method) will be invoked (by default, in Invoke Application phase), but the data in the
model is actually null (JSF doesn't check this again before calling the action(method)).
This is at least a "strange" behavior, because we may think that the checked data is not null
and try to use it for further tasks. Moreover, the invoked action (method) may
causes unexpected behaviors.
Brainstorming JSF
Let's begin with an example. Check out the below code (do not conclude
that <f:viewAction>
cannot be used without <f:viewParam>):
// index.xhtml
- starting page
<h:link
outcome="ping?numberParam=0721-9348334">Ping 0721-9348334</h:link>
<h:link outcome="ping?numberParam=0743-9348334">Ping 0743-9348334</h:link>
<h:link outcome="ping?numberParam=0743-9348334">Ping 0743-9348334</h:link>
// ping.xhtml
<f:metadata>
<f:viewParam name="numberParam"
value="#{pingBean.number}"
converter="phoneNumberConverter"/>
<f:viewAction if="#{pingBean.number ne
null}" action="#{pingBean.ping()}"/>
</f:metadata>
// custom
converter
@FacesConverter(value
= "phoneNumberConverter")
public class
PhoneNumberConverter implements Converter {
@Override
public Object getAsObject(FacesContext
context, UIComponent component, String value) {
if
(value.startsWith("0721")) {
return "343-" + value;
}
return null;
}
@Override
public String getAsString(FacesContext
context, UIComponent component, Object value) {
return (String) value;
}
}
//
PingBean.java
@Named
@RequestScoped
public class
PingBean {
private String number;
private String ping;
public PingBean() {
number
= "0000-000000";
ping = "Nothing to ping!";
}
public void ping() {
ping = "Ping number: " + number + "!";
}
public String getNumber() {
return
number;
}
public void setNumber(String number) {
this.number = number;
}
public String getPing() {
return
ping;
}
}
The code is very simple to understand - just check it line by line and pay
attention to the highlighted parts. Basically, our converter
"accepts" only phone numbers starting with 0721 prefix. For those numbers it adds
one more local prefix, 343.
For numbers that starts with other prefixes (e.g. 0743) our converter returns null (!it doesn't
throw an exception).
Test 1: If you press on the first link, Ping 0721-9348334, you will see the expected
result of calling ping()
method:
Ping number:
343-0721-9348334!
Test 2: If you press on the second link, Ping 0743-9348334, you will see an
un-expected result (you may be surprised by the fact that ping() was invoked, and
you don't see on screen, Nothing
to ping!). This is happening because the if doesn't evaluate the value returned by the
PhoneNumberConverter,
which is null (!the converter was not even called). Flow is in the Apply Request Values phase and it evaluates the #{pingBean.number} against the state, which doesn't confirm the null value!
Ping number:
null!
A simple approach to solve this issue is to add an explicit null check condition
in the ping(),
like this:
public void
ping() {
if (number != null) {
ping = "Ping number: " + number
+ "!";
}
}
This will solve the issue, but it has at least two drawbacks:
- the ping()
method is still invoked
- this works only for this specific case
Is time to see how OmniFaces solves this issue!
Omnify Brainstorming
The OmniFaces solution is named ViewAction
and it is focused on postponing the moment of if value evaluation. Practically, the
OmniFaces implementation postpone the if evaluation until Invoke Application phase. Since the if attribute's value will
be evaluated during Invoke Application phase instead of the Apply Request
Values phase, the evaluated value was converted/validated (if this was
required) and set in the model (if there is the case). In order to use the
OmniFaces implementation just replace the f with o, and add OmniFaces namespace, http://omnifaces.org/ui,
as below:
<f:metadata>
<f:viewParam name="numberParam"
value="#{pingBean.number}"
converter="phoneNumberConverter"/>
<o:viewAction if="#{pingBean.number ne
null}" action="#{pingBean.ping()}"/>
</f:metadata>
Test 1: If you press on the first link, Ping 0721-9348334, you will see the expected
result of calling ping()
method:
Ping number:
343-0721-9348334!
Test 2: If you press on the second link, Ping 0743-9348334, you will see the expected
result also:
Nothing to ping!
Note
If you set immediate="true"
for <o:viewAction>,
then it will behave the same as the standard <f:viewAction>. If you are not familiar
with this attribute then more details are available here.
The complete application is available here.
How it Works
If you are not familiar with JSF events is a good start to read here.
Especially the part referring the ActionEvent (action events) creation, queue and broadcasting.
In order to understand the OmniFaces implementation, we have to focus
on a few aspects of the JSF default implementation of the UIViewAction
component. More exactly, we need to focus on the UIViewAction#decode() method, which is called
in the Apply Request Scoped phase (more details about decode() method role can be found here):
// Mojarra
2.2.9 source code of UIViewAction#decode() method
@Override
public void
decode(final FacesContext context) {
if (context == null) {
throw new NullPointerException();
}
if ((context.isPostback() &&
!isOnPostback()) || !isRendered()) {
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);
}
incrementEventCount(context);
queueEvent(e);
}
In this code, JSF creates an instance of the ActionEvent, 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. There is nothing unusual
in this approach, but, what's really important here is the !isRendered()
part. This event is created and queued
only if:
- this is not a postback request (this can be altered via the onPostback flag
attribute, default false)
- the !isRendered() part
return true
Actually is nothing hard to understand here if we check out what is
this isRendered(). The relevant
snippets are below (follow the "chain" of highlights parts):
// Mojarra
2.2.9 source code of UIViewAction snippets
enum
PropertyKeys {
onPostback, actionExpression, immediate,
phase, renderedAttr("if");
...
}
...
public boolean isRendered()
{
return (Boolean)
getStateHelper().eval(PropertyKeys.renderedAttr, true);
}
public void
setRendered(final boolean condition) {
getStateHelper().put(PropertyKeys.renderedAttr,
condition);
}
...
public void
decode(final FacesContext context) {
...
if ((context.isPostback() &&
!isOnPostback()) || !isRendered()) {
return;
}
// create and queue the ActionEvent
}
So, the mystery was resolved! Behind isRendered()
we have the evaluation of the if
attribute's value. We are in the Apply Request Values phase and we know that
for a non-postback request:
- if the value of the if
attribute is evaluated to false
then the ActionEvent
is not
created and queued.
- if the value of the if
attribute is evaluated to true
then the ActionEvent
is created and queued.
Practically, this is exactly what we said in the Problem/Issue subsection. The if attribute is evaluated in the Apply
Request Values phase, before conversion, validation and update model. Further,
even if is less important for our dissertation, let's have a quick look over
the below code from decode()
method:
...
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);
}
incrementEventCount(context);
queueEvent(e);
...
In words, we can say that the ActionEvent is created and queued as follows:
- if this component instance has been configured with a specific lifecycle phase via the phase attribute, then
use that phase
- if the value of the immediate
is true then use Apply Request Values phase
- otherwise, use Invoke Application phase
Well, until now the problem is still under control because a created
and queued event wasn't broadcasted yet. The problem is that JSF will always
broadcast this event via UIActionEvent#broadcast()
method (this is a pretty large method, not listed here).
And, now we finally reach the moment when OmniFaces implementation
comes into equation. Notice the code:
@FacesComponent(ViewAction.COMPONENT_TYPE)
public class
ViewAction extends UIViewAction {
public static final String COMPONENT_TYPE =
"org.omnifaces.component.input.ViewAction";
@Override
public void broadcast(FacesEvent event) throws
AbortProcessingException {
if (super.isRendered()) {
super.broadcast(event);
}
}
@Override
public boolean isRendered() {
return !isImmediate() || super.isRendered();
}
}
Basically, org.omnifaces.component.input.ViewAction
is an extension of JSF UIViewAction
that controls the broadcasting of the ActionEvent by overriding the broadcast() method. By controlling the broadcasting,
we understand that OmniFaces only broadcast the action event when UIViewAction#isRendered()
returns true (with
other words the if
condition was evaluated to true).
Normally (by default, when no phase
was explicitly specified and immediate
remains false), this
evaluation takes place is in Invoke Application phase, since that is the moment
for broadcasting. So, the OmniFaces implementation only "cancel" the
broadcasting if the if
is evaluated to false!
This is a great lesson of understanding the JSF phases!
In order to maintain the original behavior of immediate="true", OmniFaces
provides a straightforward overriding of isRendered():
@Override
public boolean
isRendered() {
return !isImmediate() || super.isRendered();
}
Done!
Niciun comentariu :
Trimiteți un comentariu