/**
* 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.util.EngineeringUtil;
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.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;
/**
* This scanner scans the context class loader for lifecycle steps and resolves their dependencies. After the scan() method has been invoked, a call to
* getLifecycleStepList() will return all of the lifecycles steps in their proper order.
*
* @author Christian Trimble
* @author Devon Tackett
* @author John Trimble
* @author Josh Kennedy
*/
public class LifecycleStepScanner
extends AbstractScanner
{
public static Logger log = LoggerFactory.getLogger(LifecycleStepScanner.class);
protected static final Pattern NAMESPACE_DEFINITION_MAPPING_PATTERN = Pattern.compile("xmlns[:]([\\w-.]+)=['\"]([^'\"]+)['\"]");
protected static final String VALID_STEP_METHOD_SIGNATURES = "Lifecycle step methods must have one of the following parameter signatures: (), (LifecycleContext), (ConfigDocumentContext), (LifecycleContext, ConfigDocumentContext).";
protected List<LifecycleStep> lifecycleStepList = null;
protected Map<QName, LifecycleStep> lifecycleStepMap = null;
protected LifecycleContext context = null;
protected ClassPool classPool = null;
protected CtClass lifecycleStepCtClass = null;
protected DependencySorter<QName> dependencySorter = null;
public LifecycleStepScanner( 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 getLifecycleStepList
* 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());
lifecycleStepList = new ArrayList<LifecycleStep>();
lifecycleStepMap = new HashMap<QName, LifecycleStep>();
classPool = EngineeringUtil.createClassPool(classLoader);
lifecycleStepCtClass = classPool.get("org.xchain.framework.lifecycle.LifecycleStep");
super.scan();
try {
List<QName> lifecycleStepQNameList = dependencySorter.sort();
for( QName lifecycleStepQName : lifecycleStepQNameList ) {
LifecycleStep lifecycleStep = lifecycleStepMap.get(lifecycleStepQName);
if( lifecycleStep != null ) {
lifecycleStepList.add(lifecycleStep);
//try {
//lifecycleStepList.add(lifecycleStepClass.newInstance());
//}
//catch( Exception e ) {
//throw new ScanException("Could not create instance of lifecycle step '"+lifecycleStepClass.getName()+"'.", e);
//}
}
}
}
catch( DependencyCycleException dce ) {
throw new ScanException("There is at least one cycle in the lifecycle step dependencies.", dce);
}
}
catch( NotFoundException nfe ) {
throw new ScanException("The definition of a required class could not be found.", nfe);
}
finally {
dependencySorter = null;
classPool = null;
lifecycleStepCtClass = null;
}
}
/**
* @return A list of all loaded LifecycleSteps. Only valid after scan() has run.
*
* @see #scan()
*/
public List<LifecycleStep> getLifecycleStepList()
{
return lifecycleStepList;
}
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, StartStep.class);
boolean isStopStep = hasAnnotation(method, StopStep.class);
boolean isStatic = Modifier.isStatic(method.getModifiers());
boolean isConfigStep = isDocumentConfigStep(method);
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.
StartStep startStep = method.getAnnotation(StartStep.class);
localName = startStep.localName();
qName = new QName(namespaceUri, localName);
beforeSet = toQNameSet(startStep.before(), namespaceUri);
afterSet = toQNameSet(startStep.after(), namespaceUri);
}
else if( isStopStep ) {
StopStep stopStep = method.getAnnotation(StopStep.class);
localName = stopStep.localName();
qName = new QName(namespaceUri, localName);
afterSet = toQNameSet(stopStep.before(), namespaceUri);
beforeSet = toQNameSet(stopStep.after(), namespaceUri);
}
// add implicit dependency for steps that need configuration information--this will insure those steps
// run only after the step that creates the configuration information they need.
if( isStartStep && isConfigStep )
afterSet.add(QName.valueOf("{http://www.xchain.org/framework/lifecycle}create-config-document-context"));
// ASSERT: the qName, beforeSet, and afterSet have all been set from the perspective of a start step.
Map<String, String> prefixMappings = null;
if( isStartStep ) {
// get the start step annotation.
StartStep startStep = method.getAnnotation(StartStep.class);
prefixMappings = buildPrefixMappings(startStep.xmlns(), new HashMap<String, String>());
}
// ASSERT: prefix mappings set for the start step.
LifecycleStep step = lifecycleStepMap.get(qName);
if( step == null ) {
step = new AnnotationLifecycleStep(qName);
}
AnnotationLifecycleStep annotationStep = (AnnotationLifecycleStep)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);
annotationStep.setStartMethodPrefixMappings(prefixMappings);
}
else if (isStopStep) {
annotationStep.setStopMethod(method);
}
// add the lifecycle step to the map.
lifecycleStepMap.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 Map<String, String> buildPrefixMappings(String[] prefixDefinitions, Map<String, String> prefixUriMap) throws ScanException {
for( String prefixDefinition : prefixDefinitions ) {
Matcher matcher = NAMESPACE_DEFINITION_MAPPING_PATTERN.matcher(prefixDefinition);
if( !matcher.matches() ) {
throw new ScanException("Invalid prefix definition: "+prefixDefinition);
}
String prefix = matcher.group(1);
String uri = matcher.group(2);
prefixUriMap.put(prefix, uri);
}
return prefixUriMap;
}
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 && (parameterTypes[0] != LifecycleContext.class && parameterTypes[0] != ConfigDocumentContext.class) ) {
throw new ScanException("The class '"+lifecycleClass+"' has a lifecycle method '"+method.getName()+"' that has an illegal signature."+
" "+VALID_STEP_METHOD_SIGNATURES);
}
if( parameterTypes.length == 2 && (parameterTypes[0] != LifecycleContext.class || parameterTypes[1] != ConfigDocumentContext.class) ) {
throw new ScanException("The class '"+lifecycleClass+"' has a lifecycle method '"+method.getName()+"' that has an illegal signature."+
" "+VALID_STEP_METHOD_SIGNATURES);
}
if( parameterTypes.length > 2 ) {
throw new ScanException("The class '"+lifecycleClass+"' has a lifecycle method '"+method.getName()+"' that has an illegal signature."+
" "+VALID_STEP_METHOD_SIGNATURES);
}
}
private static boolean isDocumentConfigStep(Method stepMethod) {
for( Class<?> type : stepMethod.getParameterTypes() ) {
if( type.equals(ConfigDocumentContext.class) )
return true;
}
return false;
}
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;
}
}