/* * JBoss, Home of Professional Open Source * Copyright 2009 Red Hat Inc. and/or its affiliates and other contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0 * 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.jboss.arquillian.testenricher.ejb; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.List; import java.util.logging.Logger; import javax.ejb.EJB; import javax.naming.Context; import javax.naming.NamingException; import org.jboss.arquillian.core.api.Instance; import org.jboss.arquillian.core.api.annotation.Inject; import org.jboss.arquillian.core.spi.Validate; import org.jboss.arquillian.test.spi.TestEnricher; /** * Enricher that provide EJB class and setter method injection. * * @author <a href="mailto:aknutsen@redhat.com">Aslak Knutsen</a> * @version $Revision: $ */ public class EJBInjectionEnricher implements TestEnricher { private static final String ANNOTATION_NAME = "javax.ejb.EJB"; private static final Logger log = Logger.getLogger(TestEnricher.class.getName()); @Inject private Instance<Context> contextInst; /* * (non-Javadoc) * * @see org.jboss.arquillian.spi.TestEnricher#enrich(org.jboss.arquillian.spi .Context, java.lang.Object) */ public void enrich(Object testCase) { if (SecurityActions.isClassPresent(ANNOTATION_NAME)) { try { if (createContext() != null) { injectClass(testCase); } } catch (Exception e) { log.throwing(EJBInjectionEnricher.class.getName(), "enrich", e); } } } /* * (non-Javadoc) * * @see org.jboss.arquillian.spi.TestEnricher#resolve(org.jboss.arquillian.spi .Context, java.lang.reflect.Method) */ public Object[] resolve(Method method) { return new Object[method.getParameterTypes().length]; } /** * Obtains all field in the specified class which contain the specified annotation * * @throws IllegalArgumentException * If either argument is not specified */ // TODO Hack, this leaks out privileged operations outside the package. Extract out properly. protected List<Field> getFieldsWithAnnotation(final Class<?> clazz, final Class<? extends Annotation> annotation) throws IllegalArgumentException { // Precondition checks if (clazz == null) { throw new IllegalArgumentException("clazz must be specified"); } if (annotation == null) { throw new IllegalArgumentException("annotation must be specified"); } // Delegate to the privileged operations return SecurityActions.getFieldsWithAnnotation(clazz, annotation); } protected void injectClass(Object testCase) { try { @SuppressWarnings("unchecked") Class<? extends Annotation> ejbAnnotation = (Class<? extends Annotation>) SecurityActions.getThreadContextClassLoader().loadClass(ANNOTATION_NAME); List<Field> annotatedFields = SecurityActions.getFieldsWithAnnotation( testCase.getClass(), ejbAnnotation); for (Field field : annotatedFields) { if (field.get(testCase) == null) // only try to lookup fields that are not already set { EJB fieldAnnotation = (EJB) field.getAnnotation(ejbAnnotation); try { String mappedName = fieldAnnotation.mappedName(); ; String beanName = fieldAnnotation.beanName(); String lookup = attemptToGet31LookupField(fieldAnnotation); String[] jndiNames = resolveJNDINames(field.getType(), mappedName, beanName, lookup); Object ejb = lookupEJB(jndiNames); field.set(testCase, ejb); } catch (Exception e) { log.fine("Could not lookup " + fieldAnnotation + ", other Enrichers might, move on. Exception: " + e.getMessage()); } } } List<Method> methods = SecurityActions.getMethodsWithAnnotation(testCase.getClass(), ejbAnnotation); for (Method method : methods) { if (method.getParameterTypes().length != 1) { throw new RuntimeException("@EJB only allowed on single argument methods"); } if (!method.getName().startsWith("set")) { throw new RuntimeException("@EJB only allowed on 'set' methods"); } EJB parameterAnnotation = null; // method.getParameterAnnotations()[0] for (Annotation annotation : method.getParameterAnnotations()[0]) { if (EJB.class.isAssignableFrom(annotation.annotationType())) { parameterAnnotation = (EJB) annotation; } } // Default values of the annotation attributes. String mappedName = null; String beanName = null; String lookup = null; if (parameterAnnotation != null) { mappedName = parameterAnnotation.mappedName(); beanName = parameterAnnotation.beanName(); lookup = attemptToGet31LookupField(parameterAnnotation); } String[] jndiNames = resolveJNDINames(method.getParameterTypes()[0], mappedName, beanName, lookup); Object ejb = lookupEJB(jndiNames); method.invoke(testCase, ejb); } } catch (Exception e) { throw new RuntimeException("Could not inject members", e); } } protected String attemptToGet31LookupField(EJB annotation) throws IllegalAccessException, InvocationTargetException { String lookup = null; try { Method m = EJB.class.getMethod("lookup"); lookup = String.valueOf(m.invoke(annotation)); } catch (NoSuchMethodException e) { // No op, running on < 3.1 EJB lib } return lookup; } /** * Resolves the JNDI name of the given field. * <p> * If <tt>mappedName</tt>, <tt>lookup</tt> or <tt>beanName</tt> are specified, they're used to resolve JNDI name. * Otherwise, default policy * applies. * <p> * If more than one of the <tt>mappedName</tt>, <tt>lookup</tt> and <tt>beanName</tt> {@link EJB} annotation * attributes is specified at the same time, an {@link IllegalStateException} * will be thrown. * * @param fieldType * annotated field which JNDI name should be resolved. * @param mappedName * Value of {@link EJB}'s <tt>mappedName</tt> attribute. * @param beanName * Value of {@link EJB}'s <tt>beanName</tt> attribute. * @param lookup * Value of {@link EJB}'s <tt>lookup</tt> attribute. * * @return possible JNDI names which should be looked up to access the proper object. */ protected String[] resolveJNDINames(Class<?> fieldType, String mappedName, String beanName, String lookup) { MessageFormat msg = new MessageFormat( "Trying to resolve JNDI name for field \"{0}\" with mappedName=\"{1}\" and beanName=\"{2}\""); log.finer(msg.format(new Object[] {fieldType, mappedName, beanName})); Validate.notNull(fieldType, "EJB enriched field cannot to be null."); boolean isMappedNameSet = hasValue(mappedName); boolean isBeanNameSet = hasValue(beanName); boolean isLookupSet = hasValue(lookup); if (isMoreThanOneValueTrue(isMappedNameSet, isBeanNameSet, isLookupSet)) { throw new IllegalStateException( "Only one of the @EJB annotation attributes 'mappedName', 'lookup' and 'beanName' can be specified at the same time."); } String[] jndiNames; // If set, use only mapped name or bean name to lookup the EJB. if (isMappedNameSet) { jndiNames = new String[] {mappedName}; } else if (isLookupSet) { jndiNames = new String[] {lookup}; } else if (isBeanNameSet) { jndiNames = new String[] {"java:module/" + beanName + "!" + fieldType.getName()}; } else { jndiNames = getJndiNamesForAnonymousEJB(fieldType); } return jndiNames; } protected String[] getJndiNamesForAnonymousEJB(Class<?> fieldType) { String[] jndiNames; // TODO: These names are not spec compliant; fieldType needs to be a bean type here, but usually is just an interface of a bean. These seldom work. jndiNames = new String[] { "java:global/test.ear/test/" + fieldType.getSimpleName() + "Bean", "java:global/test.ear/test/" + fieldType.getSimpleName(), "java:global/test/" + fieldType.getSimpleName(), "java:global/test/" + fieldType.getSimpleName() + "Bean", "java:global/test/" + fieldType.getSimpleName() + "/no-interface", "test/" + fieldType.getSimpleName() + "Bean/local", "test/" + fieldType.getSimpleName() + "Bean/remote", "test/" + fieldType.getSimpleName() + "/no-interface", fieldType.getSimpleName() + "Bean/local", fieldType.getSimpleName() + "Bean/remote", fieldType.getSimpleName() + "/no-interface", // WebSphere Application Server Local EJB default binding "ejblocal:" + fieldType.getCanonicalName(), // WebSphere Application Server Remote EJB default binding fieldType.getCanonicalName()}; return jndiNames; } protected Object lookupEJB(String[] jndiNames) throws Exception { // TODO: figure out test context ? Context initcontext = createContext(); for (String jndiName : jndiNames) { try { return initcontext.lookup(jndiName); } catch (NamingException e) { // no-op, try next } } throw new NamingException("No EJB found in JNDI, tried the following names: " + joinJndiNames(jndiNames)); } protected Context createContext() throws Exception { return contextInst.get(); } // Simple helper for printing the jndi names private String joinJndiNames(String[] strings) { StringBuilder sb = new StringBuilder(); for (String string : strings) { sb.append(string).append(", "); } return sb.toString(); } /** * Helper method that checks if the given String has a non-empty value. * * @param string * String to be checked. * * @return true if <tt>string</tt> is not null and has non-empty value; false otherwise. */ private boolean hasValue(String string) { if (string != null && (!string.trim().equals(""))) { return true; } else { return false; } } private boolean isMoreThanOneValueTrue(boolean... values) { boolean trueFound = false; for (boolean value : values) { if (value) { if (trueFound) { return true; } trueFound = true; } } return false; } }