org.xchain.framework.digester.AnnotationRuleSet.java Source code

Java tutorial

Introduction

Here is the source code for org.xchain.framework.digester.AnnotationRuleSet.java

Source

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