Blog-Archiv

Donnerstag, 29. August 2019

JSF Lifecycle Phases

In my previous Blog I mentioned that setting a server-side chosen language should be done through a life-cycle listener (phase listener), better than using a <f:view locale="#{userSettings.language}"/> statement in each view. So let me prove that this actually works.

HTTP Request Processing Phases

Talking about JSF lifecycle phases means talking about how the server processes one HTTP request. There are several articles on the Internet that describe this more or less understandable. The best I found is the one of BalusC, a prominent JSF persona supporting OmniFaces and Stackoverflow. You should really read his article, it makes things technically very clear. Also the technique how you can make a JSF component accessible in a Java bean via the binding ="#{myBean.myTextField}" attribute is interesting.

Here are the the phases with my comments:

  1. Restore View:
    If there is a JSF component tree for the HTTP request (UiComponent derivates like HtmlInputText), get it from cache and set the request's locale into it. If not, all phases are skipped until Render Response.

  2. Apply Request Values:
    Retrieve submitted values from the HTTP request, "decode" these values to the components in tree.

  3. Process Validations:
    Presentation (view) logic, call optional converters, validators, event listeners.

  4. Update Model Values:
    Write values from components to the Java backing beans as implemented in the component's value expressions.

  5. Invoke Application:
    Business logic, call actions implemented in command-buttons. Actions return page identifiers, or identifiers that serve for navigation via faces-config.xml <navigation-rule>.

  6. Render Response:
    "Encode" the component tree of the current, new or delegate XHTML page into an HTML response page.

Any of these phases except Restore View can branch off into Render Response, when errors occur, or FacesContext.renderResponse() gets called. Furhter the immediate attribute on fields and commands can move things into Apply Request Values phase, and change how conversion and validation is done.

You can find Java sources in com.sun.faces.lifecycle.LifecycleImpl, you can try to understand their implementation, or debug it.

public class LifecycleImpl extends Lifecycle {
    ....

    private Phase response = new RenderResponsePhase();

    private Phase[] phases = {
        null, // ANY_PHASE placeholder, not a real Phase
        new RestoreViewPhase(),
        new ApplyRequestValuesPhase(),
        new ProcessValidationsPhase(),
        new UpdateModelValuesPhase(),
        new InvokeApplicationPhase(),
        response
    };
    ....
}

Internationalization via PhaseListener

Implementation base for this example is the "JSF Internationalization" source from my previous Blog.

Modify XHTML views

Take out the <f:view locale="#{userSettings.language}"/> statement from both

  • i18n.xhtml
  • i18n-response.xhtml
    <h:head>
        <title>JSF Internationalization</title>
        <!-- f:view locale="#{userSettings.language}"/-->
    </h:head>

Now switching the language should not work any more. Try it out. We will fix this by adding a Java PhaseListener.

Introduce PhaseListener

The class UserSettings from the previous example stays unchanged. We add a new session-scoped class, implementing the JSF interface PhaseListener, in same package, where we refer to the userSettings bean for getting the current language (locale). (I could not make dependency injection work with JSF 2.3, so excuse the ugly CDI.current() workaround!)

package fri.jsf;

import java.io.Serializable;
import java.util.*;
import javax.enterprise.context.SessionScoped;
import javax.enterprise.inject.spi.CDI;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;
import javax.inject.Inject;

@SessionScoped
public class I18nPhaseListener implements Serializable, PhaseListener
{
    @Override
    public PhaseId getPhaseId() {
        return PhaseId.ANY_PHASE;
    }
    @Override
    public void beforePhase(PhaseEvent event) {
        if (event.getFacesContext().getViewRoot() != null && 
                event.getPhaseId() == PhaseId.RENDER_RESPONSE)  // initial call doesn't have RESTORE_VIEW phase
            event.getFacesContext().getViewRoot().setLocale(getLocale());
    }
    @Override
    public void afterPhase(PhaseEvent event) {
        if (event.getFacesContext().getViewRoot() != null &&
                (event.getPhaseId() == PhaseId.RESTORE_VIEW ||  // prepare life-cycle with recent language
                 event.getPhaseId() == PhaseId.UPDATE_MODEL_VALUES))  // establish any new language
            event.getFacesContext().getViewRoot().setLocale(getLocale());
    }
    
    private Locale getLocale()  {
     return getUserSettings().getLocale();
    }
    
    // @Inject
    // private UserSettings userSettings;
    private UserSettings getUserSettings()  {
        //return userSettings;
        return CDI.current().select(UserSettings.class).get();
    }
}

To make this work we must register the class as phase listener in faces-config.xml:

<?xml version="1.0"?>
<faces-config
    xmlns="http://xmlns.jcp.org/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_3.xsd"
    version="2.3">

  <application>
    <resource-bundle>
      <base-name>messages</base-name> 
      <var>translations</var>
    </resource-bundle>
    
    <locale-config>
      <default-locale>de</default-locale>
      <supported-locale>de</supported-locale>
      <supported-locale>fr</supported-locale>
      <supported-locale>en</supported-locale>
    </locale-config>
  </application>
  
  <lifecycle>
    <!-- Register i18n phase listener: -->
    <phase-listener>fri.jsf.I18nPhaseListener</phase-listener>
  </lifecycle>

</faces-config>

Now launch the application and check that it works:

Conclusion

The question rises whether we need to know about JSF life cycle phases. Generally I think no, but global special cases may need it. Read about the POST-Redirect-GET pattern implemented generically by BalusC.




Keine Kommentare: