My JSF Books/Videos My JSF Tutorials OmniFaces/JSF PPTs
JSF 2.3 Tutorial
JSF Caching Tutorial
JSF Navigation Tutorial
JSF Scopes Tutorial
JSF Page Author Beginner's Guide
OmniFaces 2.3 Tutorial Examples
OmniFaces 2.2 Tutorial Examples
JSF Events Tutorial
OmniFaces Callbacks Usages
JSF State Tutorial
JSF and Design Patterns
JSF 2.3 New Features (2.3-m04)
Introduction to OmniFaces
25+ Reasons to use OmniFaces in JSF
OmniFaces Validators
OmniFaces Converters
JSF Design Patterns
Mastering OmniFaces
Reusable and less-verbose JSF code

My JSF Resources ...

Java EE Guardian
Member of JCG Program
Member MVB DZone
Blog curated on ZEEF
OmniFaces is an utility library for JSF, including PrimeFaces, RichFaces, ICEfaces ...

[OmniFaces Utilities] - Find the right JSF OmniFaces 2 utilities methods/functions

Search on blog

Petition by Java EE Guardians

Twitter

joi, 5 noiembrie 2015

OmniFaces 2.2 <o:viewAction>

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>

// 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!


This post was a sample of style and approach for revealing JSF hidden but valuable lessons. If you like it then you can find much more in Mastering OminFaces book.

Niciun comentariu :

Trimiteți un comentariu

JSF BOOKS COLLECTION

Postări populare

OmniFaces/JSF Fans

Follow by Email

Visitors Starting 4 September 2015

Locations of Site Visitors