Sunday 29 July 2012

JSF: CDI and EL

The Contexts and Dependency Injection (CDI) API introduced in Java EE 6 complements the JSF framework. There are good reasons to favour it over JavaServer Faces (JSF) managed bean mechanisms.

CDI doesn't do everything and there are edge cases where you might want to make use of Expression Language (EL) bindings during dependency injection. Fortunately, the gaps between these APIs are easy to fill.

Sample Beans

Consider a bean that counts the number of "foo" query parameters on a URI:

http://host/path?foo&foo&foo

The sample beans that follow implement this requirement in a number of different ways. The resultant EL expressions in a sample JSF view look like this:

 #{alfa.fooParamCount}
 #{bravo.fooParamCount}
 #{charlie.fooParamCount}
 #{delta.fooParamCount}

JSF 2 Dependency Injection

Here is a request scoped bean that counts the number of parameters:

package beans;

import javax.faces.bean.*;

@ManagedBean
@RequestScoped
public class Alfa {
  @ManagedProperty("#{paramValues.foo}")
  private String[] foo;

  public String[] getFoo() {
    return foo;
  }

  public void setFoo(String[] foo) {
    this.foo = foo;
  }

  public int getFooParamCount() {
    return foo == null ? 0 : foo.length;
  }

}

The parameters are injected using a managed property.

Here is another managed bean implementation that consumes the parameters from a broader scope:

package beans;

import javax.faces.bean.*;
import beans.faces.FacesBroker;

@ManagedBean
@ApplicationScoped
public class Bravo {
  @ManagedProperty("#{facesBroker}")
  private FacesBroker broker;

  public FacesBroker getBroker() {
    return broker;
  }

  public void setBroker(FacesBroker broker) {
    this.broker = broker;
  }

  public int getFooParamCount() {
    String[] foo = broker.getContext()
        .getExternalContext()
        .getRequestParameterValuesMap()
        .get("foo");
    return foo == null ? 0 : foo.length;
  }
}

The Bravo bean utilizes a context broker to circumvent scope issues. The downside of this approach is that unit tests need to mock the broker, the JSF context, the external context and the parameter map.

JSF Scopes and CDI

The managed properties in Alfa and Bravo use runtime evaluation and type casting. Conversely, CDI provides a lot of deploy-time dependency checking and type safety.

Here is the JSF request parameter map injected into a bean:

package beans;

import java.util.Map;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.*;
import beans.providers.Params;

@ApplicationScoped
@Named
public class Charlie {
  @Inject
  @Params
  private Map<String, String[]> params;

  public int getFooParamCount() {
    System.out.println(params.getClass());
    String[] foo = params.get("foo");
    return (foo == null) ? 0 : foo.length;
  }
}

This code relies on a producer to provide the map:

package beans.providers;

import java.util.Map;
import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.faces.context.FacesContext;

public class ParamsProducer {
  @Produces
  @Params
  @RequestScoped
  public Map<String, String[]> paramValues() {
    return FacesContext.getCurrentInstance()
        .getExternalContext()
        .getRequestParameterValuesMap();
  }
}

CDI uses the type to resolve the property, so a qualifier must be used to distinguish it from every other Map implementation:

package beans.providers;

import static java.lang.annotation.ElementType.*;
import java.lang.annotation.*;
import javax.inject.Qualifier;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ FIELD, PARAMETER, METHOD, TYPE })
public @interface Params {}

It is safe to inject this request-scoped artefact into an application-scoped bean because the CDI framework will proxy the Map instance.

CDI with EL

It is possible to utilize CDI to enhance managed property style expressions:

package demo.beans;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.*;
import demo.faces.defer.Property;
import demo.faces.defer.el.EL;

@ApplicationScoped
@Named
public class Delta {
  @Inject
  @EL("#{paramValues.foo}")
  private Property fooRef;

  public int getFooParamCount() {
    String[] foo = fooRef.get();
    return (foo == null) ? 0 : foo.length;
  }
}

This bean avoids the need for the FacesBroker type while still avoiding scope leaks.

The Delta bean relies on a small JSF/CDI library provided below. The library sacrifices type and dependency checking but removes the need for a qualifier and producer for every edge-case dependency like the parameter map. A Property facade type is used to hide details of the container API.

Testing

The unit test for Delta looks like this:

package demo.beans.test;

import static demo.beans.test.ElFields.forExpressionsIn;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;
import java.util.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import demo.beans.Delta;
import demo.faces.defer.Property;

@RunWith(Parameterized.class)
public class DeltaTest {
  private String[] params;

  public DeltaTest(String[] params) {
    this.params = params;
  }

  @Parameters
  public static Collection<Object[]> data() {
    String[] nothing = null;
    String[] zero = {};
    String[] n = { "bar", "baz" };
    Object[][] data = { { nothing }, { zero }, { n } };
    return Arrays.asList(data);
  }

  @Test
  public void delta() {
    // mock the property
    Property foo = mock(Property.class);
    when(foo.get()).thenReturn(params);
    // instantiate the test bean and set mocks
    Delta delta = forExpressionsIn(new Delta()).set("#{paramValues.foo}", foo)
        .done();
    // test the class
    int expectedCount = (params == null) ? 0 : params.length;
    assertEquals(expectedCount, delta.getFooParamCount());
  }
}

This test uses JUnit with Mockito for mocking and reflection for setting fields with no setter method.

Sample Code

References

  • JSR 316: Java Platform, Enterprise Edition (Java EE) Specification, v6
  • JSR 314: JavaServer Faces 2.0
  • JSR 299: Contexts and Dependency Injection for the Java EE platform

2 comments:

  1. The link to your sample code is broken.

    ReplyDelete
    Replies
    1. Try:
      https://github.com/mcdiae/iae/tree/master/code/java/el-defer
      https://github.com/mcdiae/iae/tree/master/code/java/el-defer-sample

      Delete

All comments are moderated