/** * Copyright 2011 meltmedia * * 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.xchain.framework.lifecycle; import static org.xchain.framework.util.AnnotationUtil.hasAnnotation; import org.xchain.framework.scanner.AbstractScanner; import org.xchain.framework.scanner.MarkerResourceLocator; import org.xchain.framework.scanner.ScanException; import org.xchain.framework.scanner.ScanNode; import org.xchain.framework.util.EngineeringUtil; import org.xchain.framework.util.DependencySorter; import org.xchain.framework.util.DependencyCycleException; import org.xchain.framework.util.LexicographicQNameComparator; import javassist.ClassPool; import javassist.CtClass; import javassist.NotFoundException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.xml.namespace.QName; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author Christian Trimble * @author Devon Tackett * @author John Trimble * @author Josh Kennedy */ public class ThreadStepScanner extends AbstractScanner { public static Logger log = LoggerFactory.getLogger(ThreadStepScanner.class); protected static final Pattern NAMESPACE_DEFINITION_MAPPING_PATTERN = Pattern.compile("xmlns[:]([\\w-.]+)=['\"]([^'\"]+)['\"]"); protected static final String VALID_STEP_METHOD_SIGNATURES = "Thread step methods must have one of the following parameter signatures: (), (LifecycleContext)."; protected List<ThreadStep> threadStepList = null; protected Map<QName, ThreadStep> threadStepMap = null; protected LifecycleContext context = null; protected ClassPool classPool = null; protected CtClass threadStepCtClass = null; protected DependencySorter<QName> dependencySorter = null; public ThreadStepScanner( LifecycleContext context ) { super( new MarkerResourceLocator("META-INF/xchain.xml"), Thread.currentThread().getContextClassLoader() ); this.context = context; } /** * Scans the context class loader for lifecycle steps and resolves their dependencies. After this method is called, a call to getThreadStepList * will return all of the lifecycle steps in their proper order. * * @throws ScanException this exception is thrown if there is a cyclic dependency among the lifecycle steps or if there is a problem creating an instance * of one of the lifecycle steps. */ public void scan() throws ScanException { try { dependencySorter = new DependencySorter<QName>(new LexicographicQNameComparator()); threadStepList = new ArrayList<ThreadStep>(); threadStepMap = new HashMap<QName, ThreadStep>(); classPool = EngineeringUtil.createClassPool(classLoader); threadStepCtClass = classPool.get("org.xchain.framework.lifecycle.ThreadStep"); super.scan(); try { List<QName> threadStepQNameList = dependencySorter.sort(); for( QName threadStepQName : threadStepQNameList ) { ThreadStep threadStep = threadStepMap.get(threadStepQName); if( threadStep != null ) { threadStepList.add(threadStep); } } } catch( DependencyCycleException dce ) { throw new ScanException("There is at least one cycle in the thread step dependencies.", dce); } } catch( NotFoundException nfe ) { throw new ScanException("The definition of a required class could not be found.", nfe); } catch( Exception e ) { throw new ScanException("An unknown exception was thrown from the thread step scanner.", e); } finally { dependencySorter = null; classPool = null; threadStepCtClass = null; } } /** * @return A list of all loaded ThreadSteps. Only valid after scan() has run. * * @see #scan() */ public List<ThreadStep> getThreadStepList() { return threadStepList; } public void scanNode( ScanNode node ) throws ScanException { try { if( isLoadableClassFile(node.getResourceName()) ) { String className = toClassName(node); CtClass scannedCtClass = null; boolean requiresAnnotationScanning = false; try { scannedCtClass = classPool.get(className); requiresAnnotationScanning = hasAnnotation(scannedCtClass, LifecycleClass.class); } catch( Exception e ) { if( log.isDebugEnabled() ) { log.debug("Could not scan file '"+node.getResourceName()+"' due to an exception.", e); } return; } if( requiresAnnotationScanning ) { // get the actual class. Class lifecycleClass = context.getClassLoader().loadClass(className); // get the lifecycle annotation. LifecycleClass lifecycleAnnotation = (LifecycleClass)lifecycleClass.getAnnotation(LifecycleClass.class); // get the uri for the annotations found in the class. String namespaceUri = lifecycleAnnotation.uri(); // scan the lifecycle class for a lifecycle accessor method. Method accessorMethod = null; int accessorMethodCount = 0; for( Method method : lifecycleClass.getDeclaredMethods() ) { if( Modifier.isStatic(method.getModifiers()) && hasAnnotation(method, LifecycleAccessor.class) ) { accessorMethod = method; accessorMethodCount++; } } if( accessorMethodCount > 1 ) { throw new ScanException("The class '"+lifecycleClass+"' has "+accessorMethodCount+" static methods that are annotated with the LifecycleAccessor annotation."+ " Lifecycle classes should have at most one accessor method."); } // scan the class for for start and stop steps. for( Method method : lifecycleClass.getMethods() ) { boolean isStartStep = hasAnnotation(method, StartThreadStep.class); boolean isStopStep = hasAnnotation(method, StopThreadStep.class); boolean isStatic = Modifier.isStatic(method.getModifiers()); if( isStartStep && isStopStep ) { throw new ScanException("The class '"+lifecycleClass+"' has a lifecycle method '"+method.getName()+"' that has both a StartStep and a StopStep annotations."); } // if this is not a start or stop step, then move on. if( !isStartStep && !isStopStep ) { continue; } // ASSERT: The method has a single step annotation. assertProperStepMethodSignature(lifecycleClass, method); // ASSERT: The method has the proper signature. String localName = null; QName qName = null; Set<QName> beforeSet = null; Set<QName> afterSet = null; if( isStartStep ) { // get the start step annotation. StartThreadStep startStep = method.getAnnotation(StartThreadStep.class); localName = startStep.localName(); qName = new QName(namespaceUri, localName); beforeSet = toQNameSet(startStep.before(), namespaceUri); afterSet = toQNameSet(startStep.after(), namespaceUri); } else if( isStopStep ) { StopThreadStep stopStep = method.getAnnotation(StopThreadStep.class); localName = stopStep.localName(); qName = new QName(namespaceUri, localName); afterSet = toQNameSet(stopStep.before(), namespaceUri); beforeSet = toQNameSet(stopStep.after(), namespaceUri); } // ASSERT: the qName, beforeSet, and afterSet have all been set from the perspective of a start step. if( isStartStep ) { // get the start step annotation. StartThreadStep startStep = method.getAnnotation(StartThreadStep.class); } // ASSERT: prefix mappings set for the start step. ThreadStep step = threadStepMap.get(qName); if( step == null ) { step = new AnnotationThreadStep(qName); } AnnotationThreadStep annotationStep = (AnnotationThreadStep)step; // set the accessor method on the annotation step. annotationStep.setLifecycleAccessor(accessorMethod); // make sure that the annotation step does not already have a step defined. if( isStartStep && annotationStep.getStartMethod() != null ) { throw new ScanException("The class '"+lifecycleClass+"' has more than one start step for QName '"+qName+"'."); } else if( isStopStep && annotationStep.getStopMethod() != null ) { throw new ScanException("The class '"+lifecycleClass+"' has more than one stop step for QName '"+qName+"'."); } // ASSERT: There is not a duplicate start or stop step for this qName. if( isStartStep ) { annotationStep.setStartMethod(method); } else if (isStopStep) { annotationStep.setStopMethod(method); } // add the lifecycle step to the map. threadStepMap.put(qName, annotationStep); // add information about the step to the sorter. dependencySorter.add(qName); for( QName before : beforeSet ) { dependencySorter.addDependency(qName, before); } for( QName after : afterSet ) { dependencySorter.addDependency(after, qName); } } } } } catch( Exception e ) { throw new ScanException("Failed to scan file '"+node.getResourceName()+"' due to an exception.", e); } } private static void assertProperStepMethodSignature(Class<?> lifecycleClass, Method method) throws ScanException { // check the parameters on the method, to make sure it is a valid method signature. Class<?>[] parameterTypes = method.getParameterTypes(); // make sure that the parameter list is valid. If it isn't, tell the user about the problem. if( parameterTypes.length == 1 && (LifecycleContext.class.isAssignableFrom(parameterTypes[0])) ) { throw new ScanException("The class '"+lifecycleClass+"' has a thread lifecycle method '"+method.getName()+"' that has an illegal signature."+ " "+VALID_STEP_METHOD_SIGNATURES); } if( parameterTypes.length > 1 ) { throw new ScanException("The class '"+lifecycleClass+"' has a thread lifecycle method '"+method.getName()+"' that has an illegal signature."+ " "+VALID_STEP_METHOD_SIGNATURES); } } private static Set<QName> toQNameSet( String[] qNameArray, String defaultUri ) throws ScanException { Set<QName> qNameSet = new HashSet<QName>(); for( String qNameString : qNameArray ) { QName qName = null; if( qNameString.startsWith("{") ) { qName = QName.valueOf(qNameString); } else if( qNameString.matches("[A-Za-z][-A-Za-z0-9._]*") ) { qName = new QName( defaultUri, qNameString ); } else { throw new ScanException("The qname string '"+qNameString+"' does not appear to be a valid qname or local name."); } qNameSet.add(qName); } return qNameSet; } }