/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.openejb.assembler.classic; import org.apache.openejb.BeanContext; import org.apache.openejb.OpenEJBException; import org.apache.openejb.core.interceptor.InterceptorData; import org.apache.openejb.util.LogCategory; import org.apache.openejb.util.Logger; import org.apache.openejb.util.SetAccessible; import javax.interceptor.InvocationContext; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * @version $Rev$ $Date$ */ public class InterceptorBindingBuilder { public static final Logger logger = Logger.getInstance(LogCategory.OPENEJB_STARTUP, InterceptorBindingBuilder.class.getPackage().getName()); private final List<InterceptorBindingInfo> packageAndClassBindings; public static enum Level { PACKAGE, ANNOTATION_CLASS, CLASS, ANNOTATION_METHOD, OVERLOADED_METHOD, EXACT_METHOD } public static enum Type { ADDITION_OR_LOWER_EXCLUSION, SAME_LEVEL_EXCLUSION, SAME_AND_LOWER_EXCLUSION, EXPLICIT_ORDERING } private final ArrayList<InterceptorBindingInfo> bindings; private final Map<String, InterceptorData> interceptors = new HashMap<String, InterceptorData>(); public InterceptorBindingBuilder(final ClassLoader cl, final EjbJarInfo ejbJarInfo) throws OpenEJBException { bindings = new ArrayList<InterceptorBindingInfo>(ejbJarInfo.interceptorBindings); Collections.sort(bindings, new IntercpetorBindingComparator()); Collections.reverse(bindings); packageAndClassBindings = new ArrayList<InterceptorBindingInfo>(); for (final InterceptorBindingInfo binding : bindings) { final Level level = level(binding); if (level == Level.PACKAGE || level == Level.CLASS || level == Level.ANNOTATION_CLASS) { packageAndClassBindings.add(binding); } } for (final InterceptorInfo info : ejbJarInfo.interceptors) { Class<?> clazz = null; try { clazz = Class.forName(info.clazz, true, cl); } catch (final ClassNotFoundException e) { throw new OpenEJBException("Interceptor class cannot be loaded: " + info.clazz); } final InterceptorData interceptor = new InterceptorData(clazz); toMethods(clazz, info.aroundInvoke, interceptor.getAroundInvoke()); toMethods(clazz, info.postActivate, interceptor.getPostActivate()); toMethods(clazz, info.prePassivate, interceptor.getPrePassivate()); toMethods(clazz, info.postConstruct, interceptor.getPostConstruct()); toMethods(clazz, info.preDestroy, interceptor.getPreDestroy()); toMethods(clazz, info.afterBegin, interceptor.getAfterBegin()); toMethods(clazz, info.beforeCompletion, interceptor.getBeforeCompletion()); toMethods(clazz, info.afterCompletion, interceptor.getAfterCompletion()); toMethods(clazz, info.aroundTimeout, interceptor.getAroundTimeout()); interceptors.put(info.clazz, interceptor); } } public void build(final BeanContext beanContext, final EnterpriseBeanInfo beanInfo) { Class<?> clazz = beanContext.getBeanClass(); final InterceptorData beanAsInterceptor = new InterceptorData(clazz); if (beanInfo instanceof StatelessBeanInfo || beanInfo instanceof MessageDrivenBeanInfo) { /* * 4.3.10.2 and 4.5.8 * If the stateless session bean or MDB instance has an ejbCreate method, * the container treats the ejbCreate method as the instance’s PostConstruct method, * and, in this case, the PostConstruct annotation (or deployment descriptor metadata) * can only be applied to the bean’s ejbCreate method. */ final NamedMethodInfo info = new NamedMethodInfo(); info.className = clazz.getName(); final Method createMethod = beanContext.getCreateMethod(); info.methodName = (createMethod != null) ? createMethod.getName(): "ejbCreate"; info.methodParams = new ArrayList<String>(); try { final Method ejbcreate = MethodInfoUtil.toMethod(clazz, info); if (ejbcreate != null) { final CallbackInfo ejbcreateAsPostConstruct = new CallbackInfo(); ejbcreateAsPostConstruct.className = ejbcreate.getDeclaringClass().getName(); ejbcreateAsPostConstruct.method = ejbcreate.getName(); beanInfo.postConstruct.add(ejbcreateAsPostConstruct); } } catch (final IllegalStateException e) { // there's no ejbCreate method in stateless bean. } } toMethods(clazz, beanInfo.aroundInvoke, beanAsInterceptor.getAroundInvoke()); toCallback(clazz, beanInfo.postConstruct, beanAsInterceptor.getPostConstruct()); toCallback(clazz, beanInfo.preDestroy, beanAsInterceptor.getPreDestroy()); if (beanInfo instanceof StatefulBeanInfo) { final StatefulBeanInfo stateful = (StatefulBeanInfo) beanInfo; toCallback(clazz, stateful.postActivate, beanAsInterceptor.getPostActivate()); toCallback(clazz, stateful.prePassivate, beanAsInterceptor.getPrePassivate()); toCallback(clazz, stateful.afterBegin, beanAsInterceptor.getAfterBegin()); toCallback(clazz, stateful.beforeCompletion, beanAsInterceptor.getBeforeCompletion()); toCallback(clazz, stateful.afterCompletion, beanAsInterceptor.getAfterCompletion(), boolean.class); } else { toMethods(clazz, beanInfo.aroundTimeout, beanAsInterceptor.getAroundTimeout()); } while (clazz != null && clazz != Object.class) { for (final Method method : clazz.getDeclaredMethods()) { final List<InterceptorData> methodInterceptors = createInterceptorDatas(method, beanInfo.ejbName, this.bindings); // The bean itself gets to intercept too and is always last. beanContext.setMethodInterceptors(method, methodInterceptors); beanContext.getMethodContext(method).setSelfInterception(beanAsInterceptor); } clazz = clazz.getSuperclass(); } final List<InterceptorData> callbackInterceptorDatas = createInterceptorDatas(null, beanInfo.ejbName, this.packageAndClassBindings); // The bean itself gets to intercept too and is always last. callbackInterceptorDatas.add(beanAsInterceptor); beanContext.setCallbackInterceptors(callbackInterceptorDatas); } private List<InterceptorData> createInterceptorDatas(final Method method, final String ejbName, final List<InterceptorBindingInfo> bindings) { final List<InterceptorBindingInfo> methodBindings = processBindings(method, ejbName, bindings); Collections.reverse(methodBindings); final List<InterceptorData> methodInterceptors = new ArrayList<InterceptorData>(); for (final InterceptorBindingInfo info : methodBindings) { final List<String> classes = info.interceptorOrder.size() > 0 ? info.interceptorOrder : info.interceptors; for (final String interceptorClassName : classes) { final InterceptorData interceptorData = interceptors.get(interceptorClassName); if (interceptorData == null) { logger.warning("InterceptorBinding references non-existent (undeclared) interceptor: " + interceptorClassName); continue; } methodInterceptors.add(interceptorData); } } return methodInterceptors; } private List<InterceptorBindingInfo> processBindings(final Method method, final String ejbName, final List<InterceptorBindingInfo> bindings) { final List<InterceptorBindingInfo> methodBindings = new ArrayList<InterceptorBindingInfo>(); // The only critical thing to understand in this loop is that // the bindings have already been sorted high to low (first to last) // in this order: // // Primary sort is "level": // (highest/first) // - Method-level bindings with params // - Method-level with no params // - Method-level bindings with params from annotations // - Class-level // - Class-level from annotation // - Package-level (aka. default level) // (lowest/last) // // They've been secondarily sorted *within* these levels by "type": // // (highest) // - Explicit order // - Exclude applying to current and lower levels // - Exclude applying to just current level // - Any addition for current level and/or exclusion for a lower level // (lowest) // final Set<Level> excludes = new HashSet<Level>(); for (final InterceptorBindingInfo info : bindings) { final Level level = level(info); if (!implies(method, ejbName, level, info)) { continue; } final Type type = type(level, info); if (type == Type.EXPLICIT_ORDERING && !excludes.contains(level)) { methodBindings.add(info); // An explicit ordering trumps all other bindings of the same level or below // (even other explicit order bindings). Any bindings that were higher than // this one still apply (and will have already been applied). // // So we keep what we have, add this last binding and we're done with this method. return methodBindings; } if (type == Type.SAME_AND_LOWER_EXCLUSION) { // We're done as the only things that will come after this will be // at the same or lower level and we've just been told to exclude them. // Nothing more to do for this method. return methodBindings; } if (type == Type.SAME_LEVEL_EXCLUSION) { excludes.add(level); } if (!excludes.contains(level)) { methodBindings.add(info); } if (info.excludeClassInterceptors) { excludes.add(Level.CLASS); excludes.add(Level.ANNOTATION_CLASS); } if (info.excludeDefaultInterceptors) { excludes.add(Level.PACKAGE); } } return methodBindings; } private boolean implies(final Method method, final String ejbName, final Level level, final InterceptorBindingInfo info) { if (level == Level.PACKAGE) { return true; } if (!ejbName.equals(info.ejbName)) { return false; } if (level == Level.CLASS || level == Level.ANNOTATION_CLASS) { return true; } final NamedMethodInfo methodInfo = info.method; return MethodInfoUtil.matches(method, methodInfo); } /** * Used for getting the java.lang.reflect.Method objects for the following callbacks: * <p/> * - @PostConstruct <any-scope> void <method-name>(InvocationContext) * - @PreDestroy <any-scope> void <method-name>(InvocationContext) * - @PrePassivate <any-scope> void <method-name>(InvocationContext) * - @PostActivate <any-scope> void <method-name>(InvocationContext) * - @AroundInvoke <any-scope> Object <method-name>(InvocationContext) throws Exception * - @AroundTimeout <any-scope> Object <method-name>(InvocationContext) throws Exception * * @param clazz * @param callbackInfos the raw CallbackInfo objects * @param callbacks the collection where the created methods will be placed */ private void toMethods(final Class<?> clazz, final List<CallbackInfo> callbackInfos, final Set<Method> callbacks) { final List<Method> methods = new ArrayList<Method>(); for (final CallbackInfo callbackInfo : callbackInfos) { try { Method method = getMethod(clazz, callbackInfo.method, InvocationContext.class); if (callbackInfo.className == null && method.getDeclaringClass().equals(clazz) && !methods.contains(method)) { methods.add(method); } if (method.getDeclaringClass().getName().equals(callbackInfo.className) && !methods.contains(method)) { methods.add(method); } else { // check for a private method on the declared class // find declared class Class<?> c = clazz; while (c != null && !c.getName().equals(callbackInfo.className)) { c = c.getSuperclass(); } // get callback method if (c != null) { try { method = getMethod(c, callbackInfo.method, InvocationContext.class); // make sure it is private if (Modifier.isPrivate(method.getModifiers()) && !methods.contains(method)) { SetAccessible.on(method); methods.add(method); } } catch (final NoSuchMethodException e) { // no-op } } } } catch (final NoSuchMethodException e) { logger.warning("Interceptor method not found (skipping): public Object " + callbackInfo.method + "(InvocationContext); in class " + clazz.getName()); } } Collections.sort(methods, new MethodCallbackComparator()); callbacks.addAll(methods); } /** * Used for getting the java.lang.reflect.Method objects for the following callbacks: * <p/> * - @PostConstruct <any-scope> void <method-name>() * - @PreDestroy <any-scope> void <method-name>() * - @PrePassivate <any-scope> void <method-name>() * - @PostActivate <any-scope> void <method-name>() * - @AfterBegin <any-scope> void <method-name>() * - @BeforeCompletion <any-scope> void <method-name>() * - @AfterCompletion <any-scope> void <method-name>(boolean) * <p/> * These apply to the bean class only, interceptor methods use InvocationContext as * a parameter. The toMethods method is used for those. * * @param clazz * @param callbackInfos * @param callbacks */ private void toCallback(final Class<?> clazz, final List<CallbackInfo> callbackInfos, final Set<Method> callbacks, final Class<?>... parameterTypes) { final List<Method> methods = new ArrayList<Method>(); for (final CallbackInfo callbackInfo : callbackInfos) { Class<?> usedClazz = clazz; if (clazz.isInterface() && !callbackInfo.className.equals(clazz.getName())) { // dynamic mbean for instance try { usedClazz = clazz.getClassLoader().loadClass(callbackInfo.className); } catch (final ClassNotFoundException e) { // ignored } } try { Method method = getMethod(usedClazz, callbackInfo.method, parameterTypes); if (callbackInfo.className == null && !methods.contains(method)) { methods.add(method); } else if (method.getDeclaringClass().getName().equals(callbackInfo.className) && !methods.contains(method)) { methods.add(method); } else { // check for a private method on the declared class // find declared class Class<?> c = clazz; while (c != null && !c.getName().equals(callbackInfo.className)) { c = c.getSuperclass(); } // get callback method if (c != null) { try { method = c.getDeclaredMethod(callbackInfo.method); // make sure it is private if (Modifier.isPrivate(method.getModifiers()) && !methods.contains(method)) { SetAccessible.on(method); methods.add(method); } } catch (final NoSuchMethodException e) { // no-op } } } } catch (final NoSuchMethodException e) { final String message = "Bean Callback method not found (skipping): public void " + callbackInfo.method + "(); in class " + clazz.getName(); logger.warning(message); throw new IllegalStateException(message, e); } } Collections.sort(methods, new MethodCallbackComparator()); callbacks.addAll(methods); } /** * Used by toMethods and toCallbacks to find the nearest java.lang.reflect.Method with the given * name and parameters. Callbacks can be private so class.getMethod() cannot be used. Searching * starts by looking in the specified class, if the method is not found searching continues with * the immediate parent and continues recurssively until the method is found or java.lang.Object * is reached. If the method is not found a NoSuchMethodException is thrown. * * @param clazz * @param methodName * @param parameterTypes * @return * @throws NoSuchMethodException if the method is not found in this class or any of its parent classes */ private Method getMethod(Class<?> clazz, final String methodName, final Class<?>... parameterTypes) throws NoSuchMethodException { NoSuchMethodException original = null; while (clazz != null) { try { final Method method = clazz.getDeclaredMethod(methodName, parameterTypes); return SetAccessible.on(method); } catch (final NoSuchMethodException e) { if (original == null) { original = e; } } clazz = clazz.getSuperclass(); } throw original; } // ------------------------------------------------------------------- // Methods for sorting the bindings and callbacks // ------------------------------------------------------------------- public static class IntercpetorBindingComparator implements Comparator<InterceptorBindingInfo> { public int compare(final InterceptorBindingInfo a, final InterceptorBindingInfo b) { final Level levelA = level(a); final Level levelB = level(b); if (levelA != levelB) { return levelA.ordinal() - levelB.ordinal(); } // Now resort to secondary sorting. return type(levelA, a).ordinal() - type(levelB, b).ordinal(); } } private static Level level(final InterceptorBindingInfo info) { if (info.ejbName.equals("*")) { return Level.PACKAGE; } if (info.method == null) { return info.className == null ? Level.CLASS : Level.ANNOTATION_CLASS; } if (info.method.methodParams == null) { return Level.OVERLOADED_METHOD; } return info.className == null ? Level.EXACT_METHOD : Level.ANNOTATION_METHOD; } private static Type type(final Level level, final InterceptorBindingInfo info) { if (info.interceptorOrder.size() > 0) { return Type.EXPLICIT_ORDERING; } if ((level == Level.CLASS || level == Level.ANNOTATION_CLASS) && info.excludeClassInterceptors && info.excludeDefaultInterceptors) { return Type.SAME_AND_LOWER_EXCLUSION; } if ((level == Level.CLASS || level == Level.ANNOTATION_CLASS) && info.excludeClassInterceptors) { return Type.SAME_LEVEL_EXCLUSION; } return Type.ADDITION_OR_LOWER_EXCLUSION; } public static class MethodCallbackComparator implements Comparator<Method> { public int compare(final Method m1, final Method m2) { final Class<?> c1 = m1.getDeclaringClass(); final Class<?> c2 = m2.getDeclaringClass(); if (c1.equals(c2)) { return 0; } if (c1.isAssignableFrom(c2)) { return -1; } return 1; } } }