Monday, 13 July 2009

JSF - Injecting Managed Beans into Managed Beans

Using Google Guice and JSF with something like GuiceSF to inject the JSF managed beans (backing beans) gives you a lot of power over the injection. However, by doing so JSF no longer resolves the EL expressions from faces-config.xml and injects them into the managed beans. An easy way around this, and somewhat cleaner, is to create an annotation that is 'bound' to an EL expression. To do so requires the use of GuiceyFruit's extensions to Guice.

First create the annotation:
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import com.google.inject.BindingAnnotation;

/** 
 * Injection by expression language.
 * @see com.google.inject.name.Named
 */
@BindingAnnotation
@Target( { FIELD, PARAMETER, METHOD })
@Retention(RUNTIME)
public @interface NamedEL
{
   /** expression language (without the #{}). */
   String value();
}

Next you configure the annotation and the provider inside your module:
public final class GuiceModule extends GuiceyFruitModule
{
   @Override
   protected void configure()
   {
      bindAnnotationInjector(NamedEL.class, ELProvider.class);
   }
}

And finally the provider:
public class ELProvider extends AnnotationMemberProviderSupport<NamedEL>
{
   /** Logger for this class */
   private static final Logger log = LoggerFactory.getLogger(ELProvider.class);

   private Injector injector;
   private FacesContext context;

   @Inject
   public ELProvider(Injector injector)
   {
      this.injector = injector;
   }

   public FacesContext getContext()
   {
      return context;
   }

   @Inject(optional = true)
   public void setContext(FacesContext context)
   {
      this.context = context;
   }

   public boolean isNullParameterAllowed(NamedEL annotation, Method method, Class<?> parameterType, int parameterIndex)
   {
      return false;
   }

   protected Object provide(NamedEL resource, Member member, TypeLiteral<?> requiredType, Class<?> memberType, Annotation[] annotations)
   {
      String name = getName(resource, member);

      Binding<?> binding = Injectors.getBinding(injector, Key.get(requiredType, Names.named(name)));
      if (binding != null)
      {
         return binding.getProvider().get();
      }

      try
      {
         if (context == null)
         {
            context = FacesContext.getCurrentInstance();
         }
         return getBean(name, requiredType.getRawType());
      }
      catch (ProvisionException e)
      {
         throw e;
      }
      catch (Exception e)
      {
         throw new ProvisionException("Failed to find name '" + name + "' ExpressionResolver. Cause: " + e, e);
      }
   }

   String getName(NamedEL resource, Member member)
   {
      String answer = resource.value();
      if (answer == null || answer.length() == 0)
      {
         answer = member.getName();
      }
      if (answer == null || answer.length() == 0)
      {
         throw new IllegalArgumentException("No name defined");
      }
      return answer;
   }
   
   private <T> T getBean(String expr, Class<T> expected)
   {
      try
      {
         Application app = context.getApplication();
         ValueExpression ve = app.getExpressionFactory().createValueExpression(context.getELContext(), "#{" + expr + "}", expected);
         return (T) ve.getValue(context.getELContext());
      }
      catch (RuntimeException e)
      {
         log.error("getBean caught RuntimeException", e);
         throw e;
      }
   }
}
It uses a custom FacesContext provider bound to ServletScopes.REQUEST but because its optional it defaults to looking it up the standard JSF way. This allows startup time initialisation of singletons, which occurs outside the request scope.

Usage

To use it is pretty simple. Say you had a bean called mybean, then the following would inject it during the normal creation time (e.g. when first referencing a session scoped bean).
/** managed bean 'mybean' */
@NamedEL("mybean")
private String mybean;

/** request parameter 'key' */
@NamedEL("param.key")
private String paramKey;
The second reference is a nifty trick which injects the value of the request parameter 'key'.

Resources

Guicesf seems to be down so here are my copies of them (slightly modified). binary source
Source for this example: download source

No comments:

Post a Comment