Wednesday, August 25, 2010

Converting a NetBeans-generated JSF2 project to use CDI

I started playing with Netbeans 6.9's JSF2 interface generation for database apps, and quickly noticed that (a) it uses JSF2 injection, not CDI and (b) it generates some verbose, ugly converters.

I decided to switch it over, as a bit of a learning experience. Here's what landed up being necessary.

Switching the controller beans to CDI injection is as simple as changing:

import javax.ejb.EJB;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.SessionScoped;

@ManagedBean
@SessionScoped
class AdController {
   @EJB SomeFacade ejbFacade;
}

to:

import javax.inject.Named;
import javax.inject.Inject;
import javax.enterprise.context.SessionScoped;

class AdController implements Serializable {
   // Use a uid generator to provide a suitable id here:
   private static final long serialVersionUID = 1L;

   @Inject AdFacade ejbFacade;

}

In other words, we've switched from JSF2 @EJB injection to CDI @Inject, from JSF2 @ManagedBean to CDI EL name annotation @Named, and from the JSF to the CDI scope annotation for @SessionScoped (they're in different packages but have the same name). Because CDI enforces the requirement that session beans be serializable, we implement serializable - though in practice this was necessary for correct session bean passivation in JSF2 even without CDI.

There's a catch in this conversion - the static inner converter classes in each of the controller classes.

Injection doesn't work in a @FacesConverter* so you can't @Inject the EJB facade you want directly into your converter. That's why the converter code generated by NetBeans uses such a roundabout method to get the ejbFacade, by looking up the controller bean in EL and getting it from there. However, CDI clears injection sites between phases. Because CDI has cleared the ejbFacade member by the time the converter runs, it'll be null by the time we need it in the converter. You're likely to land up with null pointer exceptions due to null ejbFacade members in the controller beans the converters look up up via FacesContext. (See this Mojarra bug)

To work around that, we'll use the fact that the Netbeans-generated facades share a common abstract base to write an abstract base for the converters. This base will know how to look up an EJB from CDI directly, without relying on injection. Here's the code, which is derived from Dominik Dorn's example:

package com.mycompany;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;

public abstract class AbstractFacesConverter<EntityType, FacadeType extends AbstractFacade<EntityType>> implements Converter {

    private final Class<FacadeType> facadeClass;
    private final Class<EntityType> entityClass;

    protected AbstractFacesConverter(Class<EntityType> entityClass, Class<FacadeType> facadeClass) {
        this.facadeClass = facadeClass;
        this.entityClass = entityClass;
    }

    @Override
    public Object getAsObject(FacesContext facesContext, UIComponent component, String value) {
        if (value == null || value.length() == 0) {
            return null;
        }
        FacadeType facade = (FacadeType) CDIHelper.getManagedBean(facesContext, facadeClass);
        return facade.find(getKeyFromString(value));
    }

    @Override
    public String getAsString(FacesContext facesContext, UIComponent component, Object object) {
        if (object == null) {
            return null;
        }
        if (entityClass.isInstance(object)) {
            return getKeyAsString( (EntityType)object);
        } else {
            throw new IllegalArgumentException("object " + object + " is of type " + object.getClass().getName() + "; expected type: "+AdController.class.getName());
        }
    }

    /**
     * Given the string-form of the primary key for the target object,
     * return a representation of the primary key in the proper type.
     *
     * @param value String representation of primary key
     * @return Primary key
     */
    protected abstract Object getKeyFromString(String value);

    /**
     * Given the an entity, return a string representation of the
     * primary key of that entity. The string must round-trip back
     * to the PK of the entity when passed to getKeyFromString().
     *
     * @param entity Entity to get the PK of
     * @return A string representation of the PK
     */
    protected abstract String getKeyAsString(EntityType entity);
    
}

... and here's a converter for an entity with an Integer ID that's been implemented using the abstract base converter:

    @FacesConverter(forClass=Ad.class)
    public static class AdControllerConverter extends AbstractFacesConverter<Ad,AdFacade> {

        public AdControllerConverter() {
            super(Ad.class, AdFacade.class);
        }

        @Override
        protected Object getKeyFromString(String value) {
            return Integer.valueOf(value);
        }

        @Override
        protected String getKeyAsString(Ad entity) {
            return entity.getId().toString();
        }

    }

Much nicer, eh?

Let's try running the code. It should work initially, but if you're on Glassfish 3.0.1 it'll fail with an exception like:

java.lang.IllegalStateException: Unable to convert ejbRef for ejb AdFacade to a business object of type class com.mycompany.AbstractFacade

... as soon as you try to do anything much. The cause is this Glassfish/Weld integration bug. As a workaround for the bug, you can override all the methods implemented in the AbstractFacade in each concrete facade EJB with trivial wrapper methods. NetBeans' "Insert Code" -> "Override Method" context menu option is very useful for this.

Once you've wrapped your inherited EJB methods, you should finally be back where you started, with an app that's now CDI-enabled and does exactly what the JSF2-only one did. Yay?


* The SeamFaces project - a part of Seam 3 - is intended to fix that, but isn't ready yet. There are also apparently plans to fix this limitation in JSF 2.1. Seam Solder offers tools for BeanManager lookup, etc ... but doesn't work on Glassfish 3.0.

3 comments:

  1. Nice article, Craig.
    Unfortunelly, Oracle yet hasn't fixed this GF 3.01 bug.
    Regards,
    Marcio Wesley Borges
    http://marciowb.info

    ReplyDelete
  2. They're not going to fix the failure of injection in FacesConverter etc, either. It's a JSF2 spec limitation. You can work around it by using Seam Solder (doesn't work on GF 3.0) or by using various custom methods to look up the BeanManager and resolve things directly.

    The weld integration bug is fixed in GF 3.1. Oracle don't appear to be putting any effort into GF 3.0 bug fixing and maintenance, they're focused on pushing 3.1 out the door. Fair enough with limited resources I guess, but it'd be nice if they'd get around to releasing 3.1 in that case.

    ReplyDelete
  3. Just an update on this: Seam 3 Faces provides injection into @FacesConverter, so that's a good workaround. The bugs referred to here are fixed in Weld 1.1.1, available in Glassfish 3.1.1 (to be released any day now) and 3.2 .

    It's been almost a full year since I wrote this. WTF.

    ReplyDelete

Captchas suck. Bots suck more. Sorry.