8

The following is a simple use case of <f:viewAction>.

<f:metadata>
    <f:viewParam name="id" value="#{testManagedBean.id}" maxlength="20"/>
    <f:viewAction action="#{testManagedBean.viewAction}"/>
</f:metadata>

The managed bean involved.

@ManagedBean
@ViewScoped
public final class TestManagedBean implements Serializable {

    private static final long serialVersionUID = 1L;
    private Long id; //Getter and setter.

    public void viewAction() {
        System.out.println("viewAction() called : " + id);
    }
}

The parameter id is passed through a URL. There is a conversion error, when a non-numeric value like xxx is passed through the URL in question and the viewAction() method associated with the listener of <f:viewAction> is not invoked.

The value of id is null in this case. I would like to redirect to another page, when id is not convertible to a desired target type (like in this case) or id is not validated against the specified validation criteria to avoid potential exceptions which are likely to be thrown in the LazyDataModel#load() method of PrimeFaces or somewhere else in the associated managed bean whenever access to these parameters is attempted in the corresponding managed bean. For this to be so, the viewAction() method should be invoked.

How to proceed with this? Should I use

<f:event type="preRenderView">

in conjunction with <f:viewAction>?

Tiny
  • 27,221
  • 105
  • 339
  • 599
  • At the moment, if `id` is `null`, the `viewAction` is not called? – Mr.J4mes Apr 14 '14 at 16:54
  • No then it is called. It is invoked, for example if the URL looks like this, `www.example.com/abc.jsf?id=` (`id` is given no value here). It is not invoked, when the value of `id` supplied through a URL cannot be converted to `java.lang.Long` like so, `www.example.com/abc.jsf?id=xxx`. – Tiny Apr 14 '14 at 16:57

2 Answers2

11

This is specified behavior. When PROCESS_VALIDATIONS phase ends with a validation failure, both the UPDATE_MODEL_VALUES and INVOKE_APPLICATION phases are skipped. Exactly like as in "regular" forms with <h:form>. Think of <f:viewParam> as a <h:inputText> and a <f:viewAction> as a <h:commandButton> and it will become more clear.

For your particular requirement, performing a redirect when conversion/validation has failed, there are at least 3 solutions:

  1. As you found out, add a <f:event listener>. I'd rather hook on postValidate event instead for better self-documentability.

    <f:metadata>
        <f:viewParam name="id" value="#{bean.id}" maxlength="20" />
        <f:event type="postValidate" listener="#{bean.redirectIfNecessary}" />
        <f:viewAction action="#{bean.viewAction}" />
    </f:metadata>
    
    public void redirectIfNecessary() throws IOException {
        FacesContext context = FacesContext.getCurrentInstance();
    
        if (!context.isPostback() && context.isValidationFailed()) {
            context.getExternalContext().redirect("some.xhtml");
        }
    }
    

    The check on FacesContext#isPostback() prevents the redirect being performed on validation failures of "regular" forms in the same view (if any).


  2. Extend the builtin LongConverter whereby you perform the redirect in getAsObject() (a validator is insuitable as the default converter for Long would already fail on non-numeric inputs; if a converter fails, the validators are never fired). This is however poor design (tight-coupling).

    <f:metadata>
        <f:viewParam name="id" value="#{bean.id}" converter="idConverter" />
        <f:viewAction action="#{bean.viewAction}" />
    </f:metadata>
    
    @FacesConverter("idConverter")
    public class IdConverter extends LongConverter {
    
        @Override
        public Object getAsObject(FacesContext context, UIComponent component, String value) {
            if (value == null || !value.matches("[0-9]{1,20}")) {
                try {
                    context.getExternalContext().redirect("some.xhtml");
                    return null;
                }
                catch (IOException e) {
                    throw new FacesException(e);
                }
            }
            else {
                return super.getAsObject(context, component, value);
            }
        }
    
    }
    

    You could if necessary play around with <f:attribute> inside <f:viewParam> to "pass" parameters to the converter.

    <f:viewParam name="id" value="#{bean.id}" converter="idConverter">
        <f:attribute name="redirect" value="some.xhtml" />
    </f:viewParam>
    
    String redirect = (String) component.getAttributes().get("redirect");
    context.getExternalContext().redirect(redirect);
    

  3. Create a custom taghandler which does basically the same as <f:event listener> but without the need for an additional backing bean method.

    <html ... xmlns:my="http://example.com/ui">
    
    <f:metadata>
        <f:viewParam name="id" value="#{bean.id}" maxlength="20" />
        <my:viewParamValidationFailed redirect="some.xhtml" />
        <f:viewAction action="#{bean.viewAction}" />
    </f:metadata>
    

    com.example.taghandler.ViewParamValidationFailed

    public class ViewParamValidationFailed extends TagHandler implements ComponentSystemEventListener {
    
        private String redirect;
    
        public ViewParamValidationFailed(TagConfig config) {
            super(config);
            redirect = getRequiredAttribute("redirect").getValue();
        }
    
        @Override
        public void apply(FaceletContext context, UIComponent parent) throws IOException {
            if (parent instanceof UIViewRoot && !context.getFacesContext().isPostback()) {
                ((UIViewRoot) parent).subscribeToEvent(PostValidateEvent.class, this);
            }
        }
    
        @Override
        public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
            FacesContext context = FacesContext.getCurrentInstance();
    
            if (context.isValidationFailed()) {
                try {
                    context.getExternalContext().redirect(redirect);
                }
                catch (IOException e) {
                    throw new AbortProcessingException(e);
                }
            }
        }
    
    }
    

    /WEB-INF/my.taglib.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <facelet-taglib
        xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
        version="2.0"
    >
        <namespace>http://example.com/ui</namespace>
    
        <tag>
            <tag-name>viewParamValidationFailed</tag-name>
            <handler-class>com.example.taghandler.ViewParamValidationFailed</handler-class>
        </tag>  
    </facelet-taglib>
    

    /WEB-INF/web.xml

    <context-param>
        <param-name>javax.faces.FACELETS_LIBRARIES</param-name>
        <param-value>/WEB-INF/my.taglib.xml</param-value>
    </context-param>
    

    True, it's a bit of code, but it ends up in clean and reusable <my:viewParamValidationFailed> tag and is actually a good fit for a new OmniFaces feature.

BalusC
  • 1,082,665
  • 372
  • 3,610
  • 3,555
  • In the last example or in ``, is it possible to prevent a `@PostConstruct` method in a relevant view / request scoped managed bean from being executed, when conversion or validation fails? Sometimes, a `@PostConstruct` method is designed to invoke one or more business methods which execute expensive database calls quite unnecessarily even though conversion or validation fails. – Tiny Oct 29 '15 at 09:59
  • 1
    @Tiny: No. Either use `` or check `isValidationFailed()`. – BalusC Oct 29 '15 at 10:09
1

Why not simply validate id yourself?

@ManagedBean
@ViewScoped
public final class TestManagedBean implements Serializable
{
    private String id;     //Getter and setter.
    private Long validId;  //Getter and setter.

    public void viewAction() {
        try {
            validId = Long.parseLong(id);
        } catch (NumberFormatException ex) {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            String outcome = "redirect.xhtml";
            facesContext.getApplication().getNavigationHandler().handleNavigation(facesContext, null, outcome);
        }
    }
}
perissf
  • 15,979
  • 14
  • 80
  • 117
  • Sorry, the listener method, `viewAction()` itself is not invoked, when the value of `id` passed as a query-string parameter is not convertible to `java.lang.Long` like `xxx`. How could we then validate `id` in the `viewAction()` method? It would cause the `NullPointerException` in the `load()` method of PrimeFaces `LazyDataModel`. – Tiny Apr 14 '14 at 19:48
  • In my test case (Mojarra 2.2.6), `viewAction()` is always invoked. Note that I have changed the type of `id` to String. – perissf Apr 14 '14 at 19:52
  • Although I did not show the actual code to avoid code noise, `id` is actually converted to a JPA entity using a custom JSF converter. I'm currently using an additional `` tag inside `` (it may completely be unnecessary) to make a redirect in its listener method, whenever `id` is null. I will accept this answer a few days later, if no more answer(s) are added. – Tiny Apr 16 '14 at 15:46