/**
* 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;
}
}
}