/** * 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.digester; import org.apache.commons.digester.RuleSetBase; import org.apache.commons.digester.Rule; import org.apache.commons.digester.Digester; import org.apache.commons.digester.WithDefaultsRulesWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xml.sax.Attributes; import org.xchain.AttributeDetail; import org.xchain.Catalog; import org.xchain.Chain; import org.xchain.Command; import org.xchain.EngineeredCatalog; import org.xchain.EngineeredCommand; import org.xchain.Locatable; import org.xchain.Registerable; import org.xchain.annotations.Element; import org.xchain.annotations.ParentElement; import javax.xml.namespace.QName; import javax.xml.XMLConstants; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Set; import static org.xchain.framework.util.AnnotationUtil.*; import org.xchain.framework.lifecycle.Lifecycle; import org.xchain.framework.lifecycle.LifecycleContext; import org.xchain.framework.lifecycle.LifecycleClassLoader; import org.xchain.framework.lifecycle.NamespaceContext; import org.xml.sax.Locator; import org.xml.sax.helpers.LocatorImpl; import org.xml.sax.ext.Locator2; import org.xml.sax.ext.Locator2Impl; /** * @author Christian Trimble * @author Devon Tackett * @author Josh Kennedy */ public class AnnotationRuleSet extends RuleSetBase { /** The log for this class. */ public static final Logger log = LoggerFactory.getLogger( AnnotationRuleSet.class ); /** The namespace for XChain command and catalog elements - {@value}.*/ public static final String NAMESPACE_URI = "http://www.xchain.org/core/1.0"; /** The local name of the name attribute - {@value}. */ public static final String NAME_ATTRIBUTE = "name"; public static final QName NAME_QNAME = new QName(NAMESPACE_URI, NAME_ATTRIBUTE); public static String getAttribute( Attributes attributes, String namespace, String localName, String defaultValue ) { String value = defaultValue; int index = attributes.getIndex(namespace, localName); if( index != -1 ) { value = attributes.getValue(index); } return value; } private String systemId = null; public AnnotationRuleSet(String systemId) { this.systemId = systemId; } public void addRuleInstances(Digester digester) { // log what we are attempting to do. // set up the namespace in the digester. digester.setNamespaceAware(true); // add the place holder rule used to pass mappings when there is not an element to pass them on... // TODO: add code for the place holder command. LifecycleContext lifecycleContext = Lifecycle.getLifecycleContext(); for( NamespaceContext namespaceContext : lifecycleContext.getNamespaceContextMap().values() ) { digester.setRuleNamespaceURI(namespaceContext.getNamespaceUri()); for( Class classObject : namespaceContext.getCatalogList() ) { // for catalogs that we find, we need to add a create rule, a properties rule, a set next rule, and a registration rule. addRulesForCatalog(digester, classObject, lifecycleContext); } for( Class classObject : namespaceContext.getCommandList() ) { // for commands that we find, we need to add a create rule, a properties rule, a set next rule, and a registration rule. addRulesForCommand(digester, lifecycleContext, systemId, classObject); } } WithDefaultsRulesWrapper defaults = new WithDefaultsRulesWrapper(digester.getRules()); defaults.addDefault(new UnknownElementRule()); digester.setRules(defaults); } public static void addRulesForCatalog( Digester digester, Class classObject, LifecycleContext lifecycleContext ) { String localName = ((Element)classObject.getAnnotation(Element.class)).localName(); digester.addRule( localName, new ClassObjectCreateRule( classObject ) ); // set the class loader. digester.addRule( localName, new SetCatalogClassLoader( lifecycleContext ) ); } public static void addRulesForCommand( Digester digester, LifecycleContext lifecycleContext, String systemId, Class classObject ) { Element element = (Element)classObject.getAnnotation(Element.class); List<String> commandPathList = null; try { // build a list of all the possible mappings for this class. commandPathList = getCommandPathList(lifecycleContext, classObject); } catch( Throwable t ) { t.printStackTrace(); } Rule createRule = new ClassObjectCreateRule( classObject ); Rule prefixMappingRule = new CommandPrefixMappingRule(); Rule setAttributeRule = new SetCommandAttributeRule(); Rule nextCommandRule = new CommandSetNextRule(); Rule commandRegistrationRule = new CommandRegistrationRule(systemId); for( String commandPath : commandPathList ) { digester.addRule( "*"+commandPath, createRule ); digester.addRule( "*"+commandPath, prefixMappingRule ); digester.addRule( "*"+commandPath, setAttributeRule ); digester.addRule( "*"+commandPath, nextCommandRule ); if( element.parentElements().length == 0 ) { digester.addRule( "*"+commandPath, commandRegistrationRule ); } } } public static List<String> getCommandPathList( LifecycleContext lifecycleContext, Class classObject ) { // get the current element. Element element = (Element)classObject.getAnnotation(Element.class); List<String> parentPaths = new ArrayList<String>(); // if the element has parent elements, then create a path for each parent element. for( ParentElement parentElement : element.parentElements() ) { // get the element namespace for the parent element. NamespaceContext namespaceContext = lifecycleContext.getNamespaceContextMap().get(parentElement.namespaceUri()); if( namespaceContext == null ) { break; } // get the class for the command. for( Class commandClass : namespaceContext.getCommandList() ) { Element namespaceCommandElement = (Element)commandClass.getAnnotation(Element.class); if( namespaceCommandElement.localName().equals(parentElement.localName()) ) { parentPaths.addAll(getCommandPathList( lifecycleContext, commandClass )); break; } } } // get this elements local name. String localName = element.localName(); List<String> pathList = new ArrayList<String>(); if( parentPaths.isEmpty() ) { pathList.add("/"+localName); } // if the element has parent elements, then create a path for each parent element. for( String parentPath : parentPaths ) { pathList.add(parentPath+"/"+localName); } return pathList; } public static class UnknownElementRule extends Rule { public void begin( String namespaceURI, String name, Attributes attributes ) throws Exception { QName elementQName = new QName(namespaceURI, name); StringBuffer sb = new StringBuffer(); sb.append("Could not find command for ").append(elementQName).append(".\n"); // if this is a namespace that is defined, then display a message about the namespace. NamespaceContext namespaceContext = Lifecycle.getLifecycleContext().getNamespaceContextMap().get(namespaceURI); if( namespaceContext != null ) { sb.append("The following commands are defined for this namespace '"+namespaceContext.getNamespaceUri()+"'.\n"); for( Class<?> commandClass : namespaceContext.getCommandList() ) { QName commandQName = new QName(namespaceContext.getNamespaceUri(), commandClass.getAnnotation(org.xchain.annotations.Element.class).localName()); sb.append(" ").append(commandQName).append("\n"); } } else { sb.append("There are no commands defined in the namespace '").append(namespaceURI).append("'. If you are tring to create an output element, "); sb.append("make sure it is wrapped in an '{http://www.xchain.org/jsl/1.0}template' element.\n"); } throw new XChainParseException(getDigester().getDocumentLocator(), sb.toString()); } } /** * A create rule that takes the class of the object to create. This class was selected over ObjectCreateRule, since it allows the * Class object to come from a class loader other than the digesters class loader. */ public static class ClassObjectCreateRule extends Rule { protected Class classObject; public ClassObjectCreateRule( Class classObject ) { this.classObject = classObject; } /** * Creates a new instance of the class and places it on the top of the stack. */ public void begin( String namespaceURI, String name, Attributes attributes ) throws Exception { // get the class from the class loader. Object instance = classObject.newInstance(); if( instance instanceof Locatable ) { Locatable locatable = (Locatable)instance; Locator locator = digester.getDocumentLocator(); if( locator == null ) { locatable.setLocator(new LocatorImpl()); } else if( locator instanceof Locator2 ) { locatable.setLocator(new Locator2Impl((Locator2)locator)); } else { locatable.setLocator(new LocatorImpl(locator)); } } // push the class onto the stack. digester.push( instance ); } /** * Removes the instance of the class from the top of the stack. */ public void end() throws Exception { digester.pop(); } } public static class CommandPrefixMappingRule extends Rule { public void begin( String namespaceURI, String name, Attributes attributes ) throws Exception { Object top = digester.peek(); if( top instanceof EngineeredCommand ) { ((EngineeredCommand)top).getPrefixMap().putAll(digester.getCurrentNamespaces()); } } } public static class SetCommandAttributeRule extends Rule { public void begin( String namespaceUri, String name, Attributes attributes ) throws Exception { Object top = digester.peek(); if( top instanceof EngineeredCommand ) { Map<QName, AttributeDetail> attributeDetailMap = ((EngineeredCommand)top).getAttributeDetailMap(); Map<QName, String> attributeMap = ((EngineeredCommand)top).getAttributeMap(); QName elementQName = new QName(namespaceUri, name); for( int i = 0; i < attributes.getLength(); i++ ) { QName attribute = new QName(attributes.getURI(i), attributes.getLocalName(i)); // if the attribute is not defined, then we have an error. if ( !attributeDetailMap.containsKey(attribute) && !attribute.equals(NAME_QNAME)) { StringBuffer sb = new StringBuffer(); sb.append("Unknown attribute '").append(attribute).append("' found on element '").append(elementQName).append("'.\n"); sb.append("Valid attributes are:\n"); for( QName validAttribute : attributeDetailMap.keySet() ) { sb.append(" ").append(validAttribute).append("\n"); } throw new XChainParseException(getDigester().getDocumentLocator(), sb.toString()); } else if (attribute.equals(NAME_QNAME)) { // SyntaxUtil.validateQName(attributes.getValue(), getDigester().getDocumentLocator() ); attributeMap.put(attribute, attributes.getValue(i)); } else { // we need to validate the attribute here. // we need to know the attrubute type and there needs to be a generic way to pass that attribute name and type into a // a utility method that does the validating, or the attribute type needs to know how to do the validating. try { attributeDetailMap.get(attribute).getType().validate(attributes.getValue(i), new DigesterNamespaceContext(getDigester())); } catch( Exception e ) { throw new XChainParseException(getDigester().getDocumentLocator(), "Invalid attribute value for "+attribute+":"+e.getMessage()); } attributeMap.put(attribute, attributes.getValue(i)); } } // check for required attributes that are not defined. for( Map.Entry<QName, AttributeDetail> entry : attributeDetailMap.entrySet() ) { if( entry.getValue().getRequired() && !attributeMap.containsKey(entry.getKey()) ) { throw new XChainParseException(getDigester().getDocumentLocator(), "The attribute "+entry.getKey()+" is required for element "+elementQName+"."); } } } } } /** * If the object at the top of the stack is an instance of Command and the object above it is an instance of Chain, then this rule adds the command * to the chain. */ public static class CommandSetNextRule extends Rule { public void end( String namespace, String name ) { Object top = digester.peek(); Object nextToTop = digester.peek(1); if( top != null && top instanceof Command && nextToTop != null && nextToTop instanceof Chain ) { ((Chain)nextToTop).addCommand((Command)top); } } } /** * Registers a command with the inner most catalog tag in the input document. */ public static class CommandRegistrationRule extends Rule { protected int depth = 0; protected QName qName = null; protected String systemId = null; public CommandRegistrationRule( String systemId ) { this.systemId = systemId; } public void begin( String namespaceURI, String name, Attributes attributes ) throws Exception { // record the name of the command. String commandName = null; int commandNameIndex = attributes.getIndex(NAMESPACE_URI, NAME_ATTRIBUTE); if( commandNameIndex != -1 ) { commandName = attributes.getValue(commandNameIndex); } if( commandName != null && depth == 0 ) { if( commandName.matches(".*:.*") ) { String[] splitName = commandName.split(":", 2); String namespaceUri = (String)getDigester().getCurrentNamespaces().get(splitName[0]); if( namespaceUri == null ) { throw new XChainParseException(getDigester().getDocumentLocator(), "Could not find a namespace for the prefix '"+splitName[0]+"'."); } qName = new QName(namespaceUri, splitName[1]); } else { qName = new QName(XMLConstants.NULL_NS_URI, commandName); } } else if( commandName != null ) { throw new XChainParseException(getDigester().getDocumentLocator(), "Named commands may not be nested in other commands."); } depth++; } public void end( String namespaceURI, String name ) throws Exception { depth--; if( qName != null && depth == 0 ) { // get the command that is being constructed. Catalog catalog = findCatalogInStack(); if( catalog != null ) { // get the command on the top of the stack. Command command = (Command)digester.peek(); if( command instanceof Registerable ) { Registerable registerable = (Registerable)command; registerable.setQName(qName); registerable.setSystemId(systemId); } // register the command with the catalog. catalog.addCommand(qName, command); } else { throw new XChainParseException(getDigester().getDocumentLocator(), "Command name '"+qName+"' defined outside the scope of a catalog."); } } if( depth == 0 ) { qName = null; } } /** * Finds the catalog that is closest to the top of the digesters stack. */ protected Catalog findCatalogInStack() { Catalog catalog = null; if( log.isDebugEnabled() ) { log.debug("digester.getCount() = "+digester.getCount()); } for( int i = 0; i < digester.getCount() && catalog == null; i++ ) { Object peeked = digester.peek(i); if( log.isDebugEnabled() ) { log.debug("Looking for catalog in digester stack at index "+i+". Found object of type '"+peeked.getClass().getName()+"'."); } if( peeked instanceof Catalog ) { catalog = (Catalog)peeked; } } return catalog; } } /** */ public static class SetCatalogClassLoader extends Rule { protected LifecycleContext lifecycleContext = null; public SetCatalogClassLoader( LifecycleContext lifecycleContext ) { this.lifecycleContext = lifecycleContext; } public void begin( String namespaceURI, String name, Attributes attributes ) throws Exception { Catalog catalog = findCatalogInStack(); if( catalog instanceof EngineeredCatalog ) { ((EngineeredCatalog)catalog).setClassLoader(new LifecycleClassLoader(lifecycleContext.getClassLoader())); } } /** * Finds the catalog that is closest to the top of the digesters stack. */ protected Catalog findCatalogInStack() { Catalog catalog = null; if( log.isDebugEnabled() ) { log.debug("digester.getCount() = "+digester.getCount()); } for( int i = 0; i < digester.getCount() && catalog == null; i++ ) { Object peeked = digester.peek(i); if( log.isDebugEnabled() ) { log.debug("Looking for catalog in digester stack at index "+i+". Found object of type '"+peeked.getClass().getName()+"'."); } if( peeked instanceof Catalog ) { catalog = (Catalog)peeked; } } return catalog; } } }