Monday, 2 January 2012

JSF: managed beans without JSF dependencies

A previous post discussed how to inject the FacesContext into managed beans using a broker. This post demonstrates how to build on that approach with greater levels of abstraction.

It is generally possible to remove direct JSF dependencies from managed bean code. You might want to do this to reduce coupling, improve cohesion or in the interests of writing testable code.

The code was written against Java 6, JSF 2 and JUnit 4 but the approach could be adapted to earlier versions of all of these.

  1. The Managed Bean
  2. Injecting the Scope
  3. Using Proxy Scopes
  4. The Abstract Collection Proxies

The Managed Bean

The managed bean increments a session-scoped integer when incrementFoo() is invoked:

package foo;

import java.io.Serializable;
import java.util.Map;

public class DeltaBean implements Serializable {
  private static final long serialVersionUID = 1L;

  private Map<String, Object> dataStore;

  public Map<String, Object> getDataStore() {
    return dataStore;
  }

  public void setDataStore(Map<String, Object> dataStore) {
    this.dataStore = dataStore;
  }

  public String incrementFoo() {
    Integer foo = (IntegerdataStore.get("foo");
    foo = (foo == null: foo + 1;
    dataStore.put("foo", foo);
    return null;
  }
}

This is configured as a session scope variable in faces-config.xml:

  <managed-bean>
    <managed-bean-name>deltaBean</managed-bean-name>
    <managed-bean-class>foo.DeltaBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
  </managed-bean>

Using the faces-config.xml instead of annotations means that there are no compile-time dependencies on the JSF API.

The design of the bean makes it easy to test:

package foo.test;

import static org.junit.Assert.assertEquals;

import java.util.HashMap;
import java.util.Map;

import org.junit.Test;

import foo.DeltaBean;

public class DeltaBeanTest {
  @Test
  public void testIncrementFoo() {
    Map<String, Object> scope = new HashMap<String, Object>();
    DeltaBean bean = new DeltaBean();
    bean.setDataStore(scope);
    bean.incrementFoo();
    assertEquals(1, scope.get("foo"));
    bean.incrementFoo();
    assertEquals(2, scope.get("foo"));
  }
}

All that is missing is injecting the session scope into the bean.

Injecting the Scope

The JSF 2 specification mandates a number of implicit objects:

Data accessed via an implicit object is also defined to be in a scope. The following implicit objects are considered to be in request scope:

  • cookie
  • facesContext
  • header
  • headerValues
  • param
  • paramValues
  • request
  • requestScope
  • view

The only implicit objects in session scope are session and sessionScope

The following implicit objects are considered to be in application scope:

  • application
  • applicationScope
  • initParam

So, it is possible to inject the sessionScope map into the bean:

  <!-- Do not use this code! -->
  <managed-bean>
    <managed-bean-name>deltaBean</managed-bean-name>
    <managed-bean-class>foo.DeltaBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
    <managed-property>
      <property-name>dataStore</property-name>
      <property-class>java.util.Map</property-class>
      <value>#{sessionScope}</value>
    </managed-property>
  </managed-bean>

However, this is a bug. The Map instance belongs to the FacesContext which is request scoped. Keeping a reference to it in the session causes the object to leak out of scope.

This is a contrived example; having this sort of circular reference where an object in the session has a reference to the session would probably indicate a defective design.

To prevent scope leaks, the managed bean specification also prohibits injecting a narrowly scoped object into a broader scope.

Using Proxy Scopes

Scope proxies can be used to work round the limitations of implicit objects. These dynamically look up the underlying scope on every call:

public abstract class MapProxy<K, V> implements Map<K, V> {

  /** Returns the underlying instance. */
  protected abstract Map<K, V> map();

  @Override
  public V get(Object key) {
    //implemented methods defer to the context object
    return map().get(key);
  }

Here is an implementation of a session scope proxy:

//headers elided

public final class SessionMapProxy extends MapProxy<String, Object> implements
    Serializable {
  private static final long serialVersionUID = 1L;

  private static final SessionMapProxy MAP = new SessionMapProxy();
  private static final Values VALUES = new Values();
  private static final EntrySet ENTRY_SET = new EntrySet();
  private static final KeySet KEY_SET = new KeySet();

  private Object readResolve() throws ObjectStreamException {
    return MAP;
  }

  @Override protected Map<String, Object> map() {
    return FacesContext.getCurrentInstance()
        .getExternalContext()
        .getSessionMap();
  }

  @Override public Set<String> keySet() {
    return KEY_SET;
  }

  @Override public Collection<Object> values() {
    return VALUES;
  }

  @Override public Set<java.util.Map.Entry<String, Object>> entrySet() {
    return ENTRY_SET;
  }

  private static final class Values extends CollectionProxy<Object> implements
      Serializable {
    private static final long serialVersionUID = 1L;

    @Override protected Collection<Object> collection() {
      return MAP.map().values();
    }

    private Object readResolve() throws ObjectStreamException {
      return VALUES;
    }
  }

  private static final class EntrySet extends SetProxy<Entry<String, Object>>
      implements Serializable {
    private static final long serialVersionUID = 1L;

    @Override protected Set<Entry<String, Object>> set() {
      return MAP.map().entrySet();
    }

    private Object readResolve() throws ObjectStreamException {
      return ENTRY_SET;
    }
  }

  private static final class KeySet extends SetProxy<String> implements
      Serializable {
    private static final long serialVersionUID = 1L;

    @Override protected Set<String> set() {
      return MAP.map().keySet();
    }

    private Object readResolve() throws ObjectStreamException {
      return KEY_SET;
    }
  }
}

Alter the map() implementation for alternative scopes.

If the amount of code for each scope seems excessive it is due to the serialization support. readResolve() is used to limit the number of object instances in the JVM at any one time. If this were not a concern the implementation could be rationalized significantly.

This type is configured as application scope:

  <managed-bean>
    <managed-bean-name>sessionMapProxy</managed-bean-name>
    <managed-bean-class>faces.proxies.SessionMapProxy</managed-bean-class>
    <managed-bean-scope>application</managed-bean-scope>
  </managed-bean>

The proxy can be safely injected into objects in any scope:

  <managed-bean>
    <managed-bean-name>deltaBean</managed-bean-name>
    <managed-bean-class>foo.DeltaBean</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
    <managed-property>
      <property-name>dataStore</property-name>
      <property-class>java.util.Map</property-class>
      <value>#{sessionMapProxy}</value>
    </managed-property>
  </managed-bean>

The Abstract Collection Proxies

For completeness, here are the abstract types used in the sample code. These help eliminate boiler plate.

Map:

//headers elided

public abstract class MapProxy<K, V> implements Map<K, V> {

  /** Returns the underlying instance. */
  protected abstract Map<K, V> map();

  @Override public int size() {
    return map().size();
  }

  @Override public boolean isEmpty() {
    return map().isEmpty();
  }

  @Override public boolean containsKey(Object key) {
    return map().containsKey(key);
  }

  @Override public boolean containsValue(Object value) {
    return map().containsValue(value);
  }

  @Override public V get(Object key) {
    return map().get(key);
  }

  @Override public V put(K key, V value) {
    return map().put(key, value);
  }

  @Override public V remove(Object key) {
    return map().remove(key);
  }

  @Override public void putAll(Map<? extends K, ? extends V> m) {
    map().putAll(m);
  }

  @Override public void clear() {
    map().clear();
  }

  @Override public boolean equals(Object obj) {
    return map().equals(obj);
  }

  @Override public int hashCode() {
    return map().hashCode();
  }

  @Override public String toString() {
    return map().toString();
  }
}

Set:

//headers elided

public abstract class SetProxy<E> implements Set<E> {

  /** Returns the underlying instance. */
  protected abstract Set<E> set();

  @Override public int size() {
    return set().size();
  }

  @Override public boolean isEmpty() {
    return set().isEmpty();
  }

  @Override public boolean contains(Object o) {
    return set().contains(o);
  }

  /** Warning: not proxied. */
  @Override public Iterator<E> iterator() {
    return set().iterator();
  }

  @Override public Object[] toArray() {
    return set().toArray();
  }

  @Override public <T> T[] toArray(T[] a) {
    return set().toArray(a);
  }

  @Override public boolean add(E e) {
    return set().add(e);
  }

  @Override public boolean remove(Object o) {
    return set().remove(o);
  }

  @Override public boolean containsAll(Collection<?> c) {
    return set().containsAll(c);
  }

  @Override public boolean addAll(Collection<? extends E> c) {
    return set().addAll(c);
  }

  @Override public boolean retainAll(Collection<?> c) {
    return set().retainAll(c);
  }

  @Override public boolean removeAll(Collection<?> c) {
    return set().removeAll(c);
  }

  @Override public void clear() {
    set().clear();
  }

  @Override public boolean equals(Object obj) {
    return set().equals(obj);
  }

  @Override public int hashCode() {
    return set().hashCode();
  }

  @Override public String toString() {
    return set().toString();
  }
}

Collection:

//headers elided

public abstract class CollectionProxy<E> implements Collection<E> {

  /** Returns the underlying instance. */
  protected abstract Collection<E> collection();

  @Override public int size() {
    return collection().size();
  }

  @Override public boolean isEmpty() {
    return collection().isEmpty();
  }

  @Override public boolean contains(Object o) {
    return collection().contains(o);
  }

  /** Warning: not proxied. */
  @Override public Iterator<E> iterator() {
    return collection().iterator();
  }

  @Override public Object[] toArray() {
    return collection().toArray();
  }

  @Override public <T> T[] toArray(T[] a) {
    return collection().toArray(a);
  }

  @Override public boolean add(E e) {
    return collection().add(e);
  }

  @Override public boolean remove(Object o) {
    return collection().remove(o);
  }

  @Override public boolean containsAll(Collection<?> c) {
    return collection().containsAll(c);
  }

  @Override public boolean addAll(Collection<? extends E> c) {
    return collection().addAll(c);
  }

  @Override public boolean removeAll(Collection<?> c) {
    return collection().removeAll(c);
  }

  @Override public boolean retainAll(Collection<?> c) {
    return collection().retainAll(c);
  }

  @Override public void clear() {
    collection().clear();
  }

  @Override public String toString() {
    return collection().toString();
  }
}

No comments:

Post a Comment

All comments are moderated