Java tutorial
/* * Copyright 2010-2015, Red Hat, Inc. and individual contributors as indicated by the * @author tags. See the copyright.txt file in the distribution for a full * listing of individual contributors. * * This is free software; you can redistribute it and/or modify it under the * terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This software is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this software; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF * site: http://www.fsf.org. */ package org.zanata.seam; import com.google.common.base.Optional; import com.google.common.collect.Iterables; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import javassist.CannotCompileException; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import javassist.NotFoundException; import javax.annotation.Resource; import javax.inject.Inject; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang.ArrayUtils; import org.apache.deltaspike.core.api.exclude.Exclude; import org.apache.deltaspike.core.api.projectstage.ProjectStage; import com.google.common.collect.Lists; import org.zanata.util.AutowireLocator; /** * Helps with Auto-wiring of beans for integrated tests without the * need for a full Seam environment. It's a singleton class that upon first use * will change the way the {@link org.zanata.util.ServiceLocator} class works by * returning its own auto-wired beans. * <p> * Note: If CDI-Unit is active, the modified ServiceLocator will attempt * to use real CDI beans first, otherwise falling back on Autowire * beans if available. * </p> * <p> * Supports beans injected using: {@link javax.inject.Inject}, * {@link org.zanata.util.ServiceLocator#getInstance(java.lang.Class, java.lang.annotation.Annotation...)} * and similar methods... and which have no-arg constructors. * </p> * <p> * Limitations: * <ul> * <li>Injection by name is only supported where use(String, Object) * has been called beforehand. Otherwise, injection will be done by * class/interface.</li> * <li>Injection by class or interface is only supported where * useImpl(Class) has been called beforehand.</li> * <li>Injection by class or interface creates a new instance of the * bean class at each injection point.</li> * <li>There is only one, global, scope. All scope annotations are * ignored.</li> * <li>Lifecycle methods are ignored.</li> * <li>Injection of unnamed beans may not work (Seam components * always have names).</li> * <li>CDI qualifiers are ignored with a warning.</li> * <li>Beans with the same name or interfaces will silently overwrite * each other in the autowire scope.</li> * </ul> * * @author Carlos Munoz <a * href="mailto:camunoz@redhat.com">camunoz@redhat.com</a> * @author Sean Flanigan <a href="mailto:sflaniga@redhat.com">sflaniga@redhat.com</a> * @author Patrick Huang * <a href="mailto:pahuang@redhat.com">pahuang@redhat.com</a> */ @Slf4j @Exclude(ifProjectStage = ProjectStage.IntegrationTest.class) public class SeamAutowire { private static final Object PLACEHOLDER = new Object(); private static SeamAutowire instance; public static boolean disableRealServiceLocator = false; public static boolean useRealServiceLocator = false; // key is String (name) or Class (bean type) // value is an autowired instance, or an implementation class private Map<Object, Object> namedComponents = new HashMap<>(); /** * key is an interface Class (or other type), values is the concrete bean * implementation Class (which should be assignable to the interface class) */ private Map<Class<?>, Class<?>> beanImpls = new HashMap<Class<?>, Class<?>>(); private boolean ignoreNonResolvable; private boolean allowCycles; static { rewireServiceLocatorClass(); } protected SeamAutowire() { } public SeamAutowire allowCycles() { allowCycles = true; return this; } /** * Initializes and returns the SeamAutowire instance. * * @return The Singleton instance of the SeamAutowire class. */ public static SeamAutowire instance() { if (instance == null) { instance = new SeamAutowire(); } return instance; } /** * Clears out any beans and returns to it's initial value. */ public SeamAutowire reset() { // TODO create a new instance instead, to be sure of clearing all state ignoreNonResolvable = false; namedComponents.clear(); beanImpls.clear(); allowCycles = false; AutowireContexts.simulateSessionContext(false); useImpl(AutowireLocator.class); return this; } /** * Indicates if the presence of a session context will be simulated. * By default contexts are not simulated. */ public SeamAutowire simulateSessionContext(boolean simulate) { AutowireContexts.simulateSessionContext(simulate); return this; } /** * Indicates if the presence of an event context will be simulated. * By default contexts are not simulated. */ public SeamAutowire simulateEventContext(boolean simulate) { AutowireContexts.simulateEventContext(simulate); return this; } /** * Indicates a specific instance of a bean to use, by name. * * @param name * The name of the bean. When another bean injects * using <code>@Inject(value = "name")</code> or * <code>@Inject varName</code>, the provided bean will be used. * @param beanInstance * The bean instance to use under the provided name. */ public SeamAutowire use(String name, Object beanInstance) { return use((Object) name, beanInstance); } public SeamAutowire useJndi(String jndiName, Object beanInstance) { return use((Object) jndiName, beanInstance); } /** * Indicates a specific instance of a bean to use, by bean type. * * @param beanType * The class of the bean. When another bean injects * using <code>@Inject BeanType</code>, the provided bean will be used. * @param beanInstance * The bean instance to use when the beanType is requested . */ public SeamAutowire use(Class beanType, Object beanInstance) { return use((Object) beanType, beanInstance); } /** * Indicates an implementation class to use for a given bean type. * * @param beanType * The class of the bean. When another bean injects * using <code>@Inject BeanType</code>, an autowired instance of * the specified implementation class will be used. * @param beanImplClass * The implementation class to use when the beanType is requested . */ public SeamAutowire use(Class beanType, Class beanImplClass) { return use((Object) beanType, beanImplClass); } private SeamAutowire use(Object key, Object bean) { if (namedComponents.containsKey(key)) { throw new AutowireException("The bean " + key + " was already created. You should register it before " + "it is resolved."); } namedComponents.put(key, bean); return this; } /** * Registers an implementation to use for beans. This method is * provided for beans which are injected by interface rather than name. * * @param cls * The class to register. */ public SeamAutowire useImpl(Class<?> cls) { if (Modifier.isAbstract(cls.getModifiers())) { throw new AutowireException("Class " + cls.getName() + " is abstract."); } this.registerInterfaces(cls); return this; } /** * Indicates that a warning should be logged if for some reason a bean * cannot be resolved. Otherwise, an exception will be thrown. */ public SeamAutowire ignoreNonResolvable() { this.ignoreNonResolvable = true; return this; } /** * Returns a bean by name. * * @param name * The bean's name. * @return The bean registered under the provided name, or null if such * a bean has not been auto wired or cannot be resolved * otherwise. */ public Object getComponent(String name) { Object o = namedComponents.get(name); if (o instanceof Class) { Class clazz = (Class) o; try { Object instance = autowire(clazz.newInstance()); namedComponents.put(name, instance); return instance; } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } return o; } public <T> T getComponent(Class<T> beanClass, Annotation... qualifiers) { if (qualifiers.length != 0) { log.warn( "Qualifiers currently not supported by SeamAutowire. Try CDI-Unit and CdiUnitRunner. Class:{}, Qualifiers:{}", beanClass, Lists.newArrayList(qualifiers)); } return autowire(beanClass); } /** * Creates (but does not autowire) the bean instance for the provided * class. * * @param fieldClass * The bean class to create - may be an interface if useImpl * was called, otherwise must have a no-arg constructor per Seam * spec. * @return The bean. */ private <T> T create(Class<T> fieldClass, String beanPath) { // field might be an interface, but we need to find the // implementation class Class<T> beanClass = getImplClass(fieldClass); // The bean class might be an interface if (beanClass.isInterface()) { throw new AutowireException("" + "Could not auto-wire bean with path " + beanPath + " of type " + beanClass.getName() + ". The bean is defined as an interface, but no " + "implementations have been defined for it."); } try { // No-arg constructor Constructor<T> constructor = beanClass.getDeclaredConstructor(); constructor.setAccessible(true); return constructor.newInstance(); } catch (NoSuchMethodException e) { throw new AutowireException("" + "Could not auto-wire bean with path " + beanPath + " of type " + beanClass.getName() + ". No no-args constructor.", e); } catch (InvocationTargetException e) { throw new AutowireException("" + "Could not auto-wire bean with path " + beanPath + " of type " + beanClass.getName() + ". Exception thrown from constructor.", e); } catch (Exception e) { throw new AutowireException( "" + "Could not auto-wire bean with path " + beanPath + " of type " + beanClass.getName(), e); } } private <T> Class<T> getImplClass(Class<T> fieldClass) { // If the bean type is an interface, try to find a declared // implementation // TODO field class might a concrete superclass // of the impl class if (Modifier.isAbstract(fieldClass.getModifiers()) && this.beanImpls.containsKey(fieldClass)) { fieldClass = (Class<T>) this.beanImpls.get(fieldClass); } return (Class<T>) fieldClass; } /** * Autowires and returns the bean instance for the provided class. * * @param beanClass * The bean class to create - may be an interface if useImpl * was called, otherwise must have a no-arg constructor per Seam * spec. * @return The autowired bean. */ public <T> T autowire(Class<T> beanClass) { // We could use getComponentName(Class) to simulate Seam's lookup // (by the @Named annotation on the injection point's class), but // this would just move us further away from CDI semantics. // TODO don't create multiple instances of a class // TODO abort if multiple matches Optional<?> instanceOrClass = Iterables.tryFind(namedComponents.values(), o -> beanClass.isInstance(o) || o instanceof Class && beanClass.isAssignableFrom((Class<?>) o)); if (instanceOrClass.isPresent()) { Object val = instanceOrClass.get(); if (val instanceof Class) { try { T autowired = ((Class<T>) val).newInstance(); autowire(autowired); // store it for future lookups namedComponents.put(beanClass, autowired); return autowired; } catch (InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } else { return (T) val; } } // FIXME beanImpls.values are Classes, not instances of beanClass! // try Predicates.assignableFrom // TODO what if there's more than one match?! // Optional<Class<?>> implOptional = // Iterables.tryFind(beanImpls.values(), predicate); // if (implOptional.isPresent()) { // return (T) implOptional.get(); // } String beanPath = beanClass.getSimpleName(); T autowired = create(beanClass, beanPath); autowire(autowired, beanPath); // store it for future lookups namedComponents.put(beanClass, autowired); return autowired; } /** * Autowires a bean instance. The provided instance of the bean * will be autowired instead of creating a new one. * * @param bean * The bean instance to autowire. * @param <T> * @return Returns bean. */ public <T> T autowire(T bean) { return autowire(bean, bean.getClass().getSimpleName()); } private <T> T autowire(T bean, String beanPath) { Class<T> beanClass = (Class<T>) bean.getClass(); // Register all interfaces for this class this.registerInterfaces(beanClass); // Resolve injected Components for (ComponentAccessor accessor : getAllComponentAccessors(bean)) { // Another annotated bean Annotation inAnnotation = accessor.getAnnotation(Inject.class); if (inAnnotation == null) { inAnnotation = accessor.getAnnotation(Resource.class); } if (inAnnotation != null) { Object fieldVal = null; String beanName = accessor.getComponentName(); Class<?> beanType = accessor.getComponentType(); Set<Annotation> qualifiers = accessor.getQualifiers(); Class<?> implType = getImplClass(beanType); // TODO stateless beans should not / need not be cached // autowire the bean if not done yet if (!namedComponents.containsKey(beanName)) { String newComponentPath = beanPath + "." + beanName; Object newComponent = null; try { newComponent = create(beanType, newComponentPath); } catch (AutowireException e) { if (ignoreNonResolvable) { log.warn("Could not build bean with name '" + beanName + "' of type: " + beanType + ".", e); } else { throw e; } } if (allowCycles) { namedComponents.put(beanName, newComponent); } else { // to detect mutual injection namedComponents.put(beanName, PLACEHOLDER); } try { if (newComponent != null) { autowire(newComponent, newComponentPath); } } catch (AutowireException e) { if (ignoreNonResolvable) { log.warn("Could not autowire bean of type: " + beanType + ".", e); } else { throw e; } } if (!allowCycles) { // replace placeholder with the injected object namedComponents.put(beanName, newComponent); } } fieldVal = getComponent(beanName); if (fieldVal == PLACEHOLDER) { throw new AutowireException("Recursive dependency: unable to inject " + beanName + " into bean of type " + bean.getClass().getName()); } try { accessor.setValue(bean, fieldVal); } catch (AutowireException e) { if (ignoreNonResolvable) { log.warn("Could not set autowire field " + accessor.getComponentName() + " on bean of type " + bean.getClass().getName() + " to value of type " + fieldVal.getClass().getName(), e); } else { throw e; } } } } // call post constructor invokePostConstructMethod(bean, beanPath); return bean; } // TODO why are we rewiring classes we control? private static void rewireServiceLocatorClass() { try { ClassPool pool = ClassPool.getDefault(); CtClass locatorCls = pool.get("org.zanata.util.ServiceLocator"); // Commonly used CtClasses final CtClass stringCls = pool.get("java.lang.String"); final CtClass objectCls = pool.get("java.lang.Object"); final CtClass classCls = pool.get("java.lang.Class"); // Replace ServiceLocator's method bodies with the ones in // AutowireComponent CtClass[] emptyArgs = {}; CtMethod methodToReplace = locatorCls.getDeclaredMethod("instance", emptyArgs); methodToReplace.setBody("{return org.zanata.util.AutowireLocator.instance(); }"); locatorCls.toClass(); } catch (NotFoundException | CannotCompileException e) { throw new AutowireException("Problem rewiring ServiceLocator class", e); } } /** * Replaces Component.getInstance(params) method body with that of * AutowireComponent.getInstance(params). * * @param pool * Class pool to get class instances. * @param componentCls * Class that represents the jboss Component class. * @param params * Parameters for the getComponent method that will be replaced * @throws javassist.NotFoundException * @throws javassist.CannotCompileException */ private static void replaceGetInstance(ClassPool pool, CtClass componentCls, CtClass... params) throws NotFoundException, CannotCompileException { CtMethod methodToReplace = componentCls.getDeclaredMethod("getInstance", params); methodToReplace.setBody(pool.get(AutowireLocator.class.getName()).getDeclaredMethod("getInstance", params), null); } // TODO why are we rewiring classes we control? private static void replaceGetDependent(ClassPool pool, CtClass locatorCls, CtClass... params) throws NotFoundException, CannotCompileException { CtMethod methodToReplace = locatorCls.getDeclaredMethod("getDependent", params); methodToReplace.setBody(pool.get(AutowireLocator.class.getName()).getDeclaredMethod("getDependent", params), null); } private static ComponentAccessor[] getAllComponentAccessors(Object bean) { Collection<ComponentAccessor> props = new ArrayList<ComponentAccessor>(); for (Field f : getAllComponentFields(bean)) { if (f.getAnnotation(Inject.class) != null || f.getAnnotation(Resource.class) != null) { props.add(ComponentAccessor.newInstance(f)); } } for (Method m : getAllComponentMethods(bean)) { if (m.getAnnotation(Inject.class) != null || m.getAnnotation(Resource.class) != null) { props.add(ComponentAccessor.newInstance(m)); } } return props.toArray(new ComponentAccessor[props.size()]); } private static Field[] getAllComponentFields(Object bean) { Field[] fields = bean.getClass().getDeclaredFields(); Class<?> superClass = bean.getClass().getSuperclass(); while (superClass != null) { fields = (Field[]) ArrayUtils.addAll(fields, superClass.getDeclaredFields()); superClass = superClass.getSuperclass(); } return fields; } private static Method[] getAllComponentMethods(Object bean) { Method[] methods = bean.getClass().getDeclaredMethods(); Class<?> superClass = bean.getClass().getSuperclass(); while (superClass != null) { methods = (Method[]) ArrayUtils.addAll(methods, superClass.getDeclaredMethods()); superClass = superClass.getSuperclass(); } return methods; } private void registerInterfaces(Class<?> cls) { assert !Modifier.isAbstract(cls.getModifiers()); // register all interfaces registered by this bean for (Class<?> iface : getAllInterfaces(cls)) { this.beanImpls.put(iface, cls); } } private static Set<Class<?>> getAllInterfaces(Class<?> cls) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); for (Class<?> superClass : cls.getInterfaces()) { interfaces.add(superClass); interfaces.addAll(getAllInterfaces(superClass)); } return interfaces; } // @SuppressWarnings("unchecked") // private static List<Class<?>> getAllTypes(Class<?> cls) { // List<Class<?>> classes = ClassUtils.getAllInterfaces(cls); // classes.addAll(ClassUtils.getAllSuperclasses(cls)); // classes.add(cls); // return classes; // } /** * Invokes a single method (the first found) annotated with * {@link javax.annotation.PostConstruct}, */ private static void invokePostConstructMethod(Object bean, String beanPath) { Class<?> compClass = bean.getClass(); boolean postConstructAlreadyFound = false; for (Method m : compClass.getDeclaredMethods()) { // Per the spec, there should be only one PostConstruct method if (m.getAnnotation(javax.annotation.PostConstruct.class) != null) { if (postConstructAlreadyFound) { throw new AutowireException( "More than one PostConstruct method found for class " + compClass.getName()); } try { m.setAccessible(true); m.invoke(bean); // there should be no params postConstructAlreadyFound = true; } catch (IllegalAccessException | InvocationTargetException e) { throw new AutowireException("Error invoking PostConstruct method in bean '" + beanPath + "' of class " + compClass.getName(), e); } } } } // public static String getComponentName(Class<?> clazz) { // Named named = clazz.getAnnotation(Named.class); // if (named == null) { // return null; // } // if (named.value().isEmpty()) { // return StringUtils.uncapitalize(clazz.getSimpleName()); // } // return named.value(); // } }