/*
* Copyright (C) 2014 Civilian Framework.
*
* Licensed under the Civilian License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.civilian-framework.org/license.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.civilian.controller.factory;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.civilian.Controller;
import org.civilian.application.AppConfig;
import org.civilian.controller.ControllerFactory;
import org.civilian.util.ArrayUtil;
import org.civilian.util.Check;
import org.civilian.util.ClassUtil;
/**
* An experimental ControllerFactory which injects CDI managed beans into Controller instances.
* <p>
* Right now we only support injection of simple beans (no collections or iterators)
* into fields of a controller, which are annotated with javax.inject.Inject.
* <p>
* To use CDI for controller instances install a CdiControllerFactory during
* application setup.
* @see AppConfig#setControllerFactory(ControllerFactory)
*/
public class CdiControllerFactory implements ControllerFactory
{
/**
* Creates a new CdiControllerFactory.
* @throws IllegalStateException if the CDI BeanManager cannot be obtained
*/
public CdiControllerFactory()
{
// JEE7 beanManager_ = CDI.current().getBeanManager();
try
{
InitialContext initialContext = new InitialContext();
beanManager_ = (BeanManager)initialContext.lookup("java:comp/BeanManager");
if (beanManager_ == null)
throw new IllegalStateException("cannot obtain CDI BeanManager");
}
catch (NamingException e)
{
throw new IllegalStateException("unable to lookup bean manager", e);
}
}
/**
* Creates a new CdiControllerFactory.
* @throws IllegalStateException if the CDI BeanManager cannot be obtained
*/
public CdiControllerFactory(BeanManager beanManager)
{
beanManager_ = Check.notNull(beanManager, "beanManager");
}
/**
* Creates a Controller and resolves the fields which are annotated with @Inject.
*/
@Override public Controller createController(Class<? extends Controller> controllerClass) throws Exception
{
Injector injector = getInjector(controllerClass, Controller.class);
Controller controller = controllerClass.newInstance();
injector.inject(beanManager_, controller);
return controller;
}
private Injector getInjector(Class<?> c, Class<?> stopClass)
{
Injector injector = injectorCache_.get(c.getName());
// we construct an Injector for the class if
// we look it up the first time or - due to class reloading -
// the controller class has changed
if ((injector == null) || (injector.targetClass != c))
{
injector = buildInjector(c, stopClass);
injectorCache_.put(c.getName(), injector);
}
return injector;
}
private Injector buildInjector(Class<?> c, Class<?> stopClass)
{
ArrayList<InjectorAction> actions = new ArrayList<>();
Class<?> next = c;
while(buildInjectorActions(next, stopClass, actions))
next = next.getSuperclass();
return new Injector(c, actions.toArray(new InjectorAction[actions.size()]));
}
private boolean buildInjectorActions(Class<?> c, Class<?> stopClass, ArrayList<InjectorAction> actions)
{
if ((c == null) || (c == stopClass))
return false;
// check for Inject annotation on fields
for (Field field : c.getDeclaredFields())
{
Annotation[] annotations = field.getAnnotations();
for (int i=0; i<annotations.length; i++)
{
if (ClassUtil.isA(annotations[i], Inject.class))
{
annotations = ArrayUtil.removeAt(annotations, i);
actions.add(buildFieldInjectorAction(field, annotations));
break;
}
}
}
return true;
}
private InjectorAction buildFieldInjectorAction(Field field, Annotation[] annotations)
{
Set<Bean<?>> beans = beanManager_.getBeans(field.getGenericType(), annotations);
if (beans.isEmpty())
throw new IllegalStateException("no bean available for field " + field.getDeclaringClass().getName() + '.' + field.getName());
return new FieldInjectorAction(field, beans.iterator().next());
}
private static class Injector
{
public Injector(Class<?> targetClass, InjectorAction[] actions)
{
this.targetClass = targetClass;
actions_ = actions;
}
public void inject(BeanManager beanManager, Object object) throws Exception
{
for (InjectorAction action : actions_)
action.run(beanManager, object);
}
public final Class<?> targetClass;
private InjectorAction[] actions_;
}
private static abstract class InjectorAction
{
public abstract void run(BeanManager beanManager, Object object) throws Exception;
}
private static class FieldInjectorAction extends InjectorAction
{
public FieldInjectorAction(Field field, Bean<?> bean)
{
field.setAccessible(true);
field_ = field;
bean_ = bean;
type_ = field.getGenericType();
}
@Override public void run(BeanManager beanManager, Object object) throws Exception
{
CreationalContext<?> context = beanManager.createCreationalContext(bean_);
Object reference = beanManager.getReference(bean_, type_, context);
field_.set(object, reference);
}
private Bean<?> bean_;
private Field field_;
private Type type_;
}
private BeanManager beanManager_;
private ConcurrentHashMap<String,Injector> injectorCache_ = new ConcurrentHashMap<>();
}