org.danann.cernunnos.runtime.XmlGrammar.java Source code

Java tutorial

Introduction

Here is the source code for org.danann.cernunnos.runtime.XmlGrammar.java

Source

/*
 * Copyright 2007 Andrew Wills
 *
 * 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.danann.cernunnos.runtime;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.danann.cernunnos.Attributes;
import org.danann.cernunnos.EntityConfig;
import org.danann.cernunnos.Formula;
import org.danann.cernunnos.Grammar;
import org.danann.cernunnos.LiteralPhrase;
import org.danann.cernunnos.Phrase;
import org.danann.cernunnos.Reagent;
import org.danann.cernunnos.ReturnValueImpl;
import org.danann.cernunnos.Task;
import org.dom4j.Document;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;

/**
 * Represents a "task language" or syntax in Cernunnos.  <code>XmlGrammar</code>
 * instances are responsible for bootstrapping <code>Task</code> objects from
 * XML.
 */
public final class XmlGrammar implements Grammar {

    // Static Members.
    public static final String DEFAULT_GRAMMAR_NAME = "Unnamed";
    private static final DocumentFactory fac = new DocumentFactory();
    private static final String MAIN_GRAMMAR_LOCATION = "main.grammar";
    private static Grammar mainGrammar = null;

    // Instance Members.
    private final String name;
    private final String origin;
    private final Grammar parent;
    private final ClassLoader loader;
    private ConcurrentMap<String, List<Entry>> entries;
    private final Log log = LogFactory.getLog(XmlGrammar.class); // Don't declare as static in general libraries

    /*
     * Public API.
     */

    public static synchronized Grammar getMainGrammar() {

        if (mainGrammar == null) {
            // Create it...
            try {
                final Grammar root = new XmlGrammar("ROOT", null, null, XmlGrammar.class.getClassLoader());
                final InputStream inpt = XmlGrammar.class.getResourceAsStream(MAIN_GRAMMAR_LOCATION);
                final Document doc = new SAXReader().read(inpt);
                final Task k = new ScriptRunner(root).compileTask(doc.getRootElement());
                final RuntimeRequestResponse req = new RuntimeRequestResponse();
                final ReturnValueImpl rslt = new ReturnValueImpl();
                req.setAttribute(Attributes.RETURN_VALUE, rslt);
                k.perform(req, new RuntimeRequestResponse());
                mainGrammar = (Grammar) rslt.getValue();
            } catch (Throwable t) {
                String msg = "Error parsing Main Grammar.";
                throw new RuntimeException(msg, t);
            }
        }

        return mainGrammar;

    }

    public String getName() {
        return name;
    }

    public String getOrigin() {
        return origin;
    }

    public Task newTask(Element e, Task parent) {

        // Assertions...
        if (e == null) {
            String msg = "Argument 'e [Element]' cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        // NB:  parent may be null...

        // Elements that define tasks *must* be normalized...
        e.normalize();

        String name = e.getName();
        Entry n = getEntry(name, Entry.Type.TASK);

        Task rslt = null;
        EntityConfig config = null;
        try {

            // Create & bootstrap the result...
            config = prepareEntryConfig(n, e);
            rslt = (Task) n.getFormula().getImplementationClass().newInstance();
            rslt.init(config);

        } catch (Throwable t) {
            String msg = "Unable to create the specified task:  " + name;
            throw new RuntimeException(msg, t);
        }

        return new RuntimeTaskDecorator(rslt, config);

    }

    public Phrase newPhrase(String inpt) {
        return newPhrase(fac.createText(inpt));
    }

    public Phrase newPhrase(Node n) {

        // Assertions...
        if (n == null) {
            String msg = "Argument 'n [Node]' cannot be null.";
            throw new IllegalArgumentException(msg);
        }

        List<String> chunks = new LinkedList<String>();
        String chunkMe = n.getText();
        while (chunkMe.length() != 0) {
            if (chunkMe.startsWith(Phrase.OPEN_PHRASE_DELIMITER)) {
                chunks.add(Phrase.OPEN_PHRASE_DELIMITER);
                chunkMe = chunkMe.substring(2);
            } else {
                chunks.add(chunkMe.substring(0, 1));
                chunkMe = chunkMe.substring(1);
            }
        }

        List<Phrase> children = new LinkedList<Phrase>();
        StringBuffer buffer = new StringBuffer();
        int openCount = 0;
        for (String chunk : chunks) {
            switch (openCount) {
            case 0:
                if (chunk.equals(Phrase.OPEN_PHRASE_DELIMITER)) {
                    if (buffer.length() > 0) {
                        children.add(new LiteralPhrase(buffer.toString()));
                        buffer.setLength(0);
                    }
                    ++openCount;
                } else {
                    buffer.append(chunk);
                }
                break;
            default:
                if (chunk.equals(Phrase.OPEN_PHRASE_DELIMITER)) {
                    ++openCount;
                    buffer.append(chunk);
                } else if (chunk.equals(Phrase.CLOSE_PHRASE_DELIMITER)) {
                    --openCount;
                    if (openCount == 0) {

                        // Time to create a dynamic component...
                        String expression = buffer.toString();
                        String name = null; // Name of the phrase to use...
                        String nested = null; // Content passed to the phrase

                        // Determine if a Phrase impl was specified or if we should use the default...
                        int openParenIndex = expression.indexOf("(");
                        if (openParenIndex != -1 && expression.endsWith(")")) {
                            // A phrase impl was specified -- use it!
                            try {
                                name = expression.substring(0, openParenIndex);
                                nested = expression.substring(expression.indexOf("(") + 1, expression.length() - 1);
                            } catch (Throwable t) {
                                String msg = "The specified expression is not well formed:  " + expression;
                                throw new RuntimeException(msg, t);
                            }
                        } else {
                            // Use the default phrase impl...
                            name = Grammar.DEFAULT_PHRASE_IMPL.getName();
                            nested = expression;
                        }

                        Entry y = getEntry(name, Entry.Type.PHRASE);
                        Phrase p = null;
                        try {

                            // Create & bootstrap the phrase...
                            EntityConfig config = prepareEntryConfig(y, fac.createText(nested), n.getUniquePath());
                            Phrase enclosed = (Phrase) y.getFormula().getImplementationClass().newInstance();
                            enclosed.init(config);
                            p = new RuntimePhraseDecorator(enclosed, config);

                        } catch (Throwable t) {
                            String msg = "Unable to create the specified phrase:  " + name;
                            throw new RuntimeException(msg, t);
                        }

                        children.add(p);
                        buffer.setLength(0);
                    } else {
                        buffer.append(chunk);
                    }
                } else {
                    buffer.append(chunk);
                }
                break;
            }
        }
        if (buffer.length() > 0) {
            // Add anything that's left...
            children.add(new LiteralPhrase(buffer.toString()));
        }

        return new ConcatenatingPhrase(children);

    }

    /*
     * Package API.
     */

    /**
     * @deprecated
     */
    XmlGrammar(Grammar parent) {
        this(XmlGrammar.DEFAULT_GRAMMAR_NAME, null, parent, ((XmlGrammar) parent).getClassLoader());
    }

    XmlGrammar(String name, String origin, Grammar parent) {
        this(name, origin, parent, ((XmlGrammar) parent).getClassLoader());
    }

    ClassLoader getClassLoader() {
        return loader;
    }

    void addEntry(Entry e) {

        List<Entry> list = entries.get(e.getName());
        if (list == null) {
            list = new CopyOnWriteArrayList<Entry>();
            final List<Entry> oldList = entries.putIfAbsent(e.getName(), list);
            if (oldList != null) {
                list = oldList;
            }
        }
        list.add(e);

    }

    Set<Entry> getEntries() {
        return getEntries(true);
    }

    Set<Entry> getEntries(boolean recursive) {

        Set<Entry> rslt = null;
        if (recursive && parent != null && parent instanceof XmlGrammar) {
            rslt = ((XmlGrammar) parent).getEntries(true);
        } else {
            rslt = new HashSet<Entry>();
        }

        for (List<Entry> list : entries.values()) {
            for (Entry y : list) {
                rslt.add(y);
            }
        }

        return rslt;

    }

    /*
     * Implementation.
     */

    private XmlGrammar(String name, String origin, Grammar parent, ClassLoader loader) {

        // Assertions...
        if (name == null) {
            String msg = "Argument 'name' cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        // NB:  origin & parent may be null.
        if (loader == null) {
            String msg = "Argument 'loader' cannot be null.";
            throw new IllegalArgumentException(msg);
        }

        // Instance Members.
        this.name = name;
        this.origin = origin;
        this.parent = parent;
        this.loader = loader;

        // NB:  Tasks & phrases are added after creation...
        this.entries = new ConcurrentHashMap<String, List<Entry>>();

    }

    private Entry getEntry(String name, Entry.Type type) {

        Entry rslt = null;

        // If there's a matching entry w/in this Grammar it trumps all...
        List<Entry> list = entries.get(name);
        if (list != null) {
            for (Entry y : list) {
                // Be sure we have an entry of the correct type...
                if (y.getType().equals(type)) {
                    rslt = y;
                    break;
                }
            }
        }

        if (rslt == null) {
            if (parent != null && parent instanceof XmlGrammar) {
                // This is a little hokey... perhaps Entry should be a first-class type?
                rslt = ((XmlGrammar) parent).getEntry(name, type);
            } else {
                // Assume the name is a class that implements Bootstrappable...
                rslt = new Entry(name, (Element) null, name, (Element) null, this, new LinkedList<Node>());
            }
        }

        return rslt;

    }

    private EntityConfig prepareEntryConfig(Entry n, Node d) {
        return prepareEntryConfig(n, d, d.getUniquePath());
    }

    private EntityConfig prepareEntryConfig(Entry n, Node d, String source) {

        // Assertions...
        if (n == null) {
            String msg = "Argument 'n [Entry]' cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        if (d == null) {
            String msg = "Argument 'd [Element]' cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        if (source == null) {
            String msg = "Argument 'source' cannot be null.";
            throw new IllegalArgumentException(msg);
        }

        // Report any use of deprecated entries...
        if (n.isDeprecated()) {
            StringBuilder msg = new StringBuilder();
            msg.append("USE OF DEPRECATED API:  A deprecated ENTRY was referenced.").append("\n\t\tEntry Name:  ")
                    .append(n.getName()).append("\n\t\tDeprecated Since:  ").append(n.getDeprecation().getVersion())
                    .append("\n\t\tEntry Type:  ").append(n.getType()).append("\n\t\tSource:  ").append(source)
                    .append("\n");
            log.warn(msg.toString());
        }

        try {

            Formula f = n.getFormula();

            Map<Reagent, Object> mappings = new HashMap<Reagent, Object>();
            List<Reagent> needed = new ArrayList<Reagent>(f.getReagents());
            needed.removeAll(n.getMappings().keySet());
            for (Reagent r : needed) {
                Object value = r.getReagentType().evaluate(this, d, r.getXpath());

                // Report any use of deprecated reagents...
                if (r.isDeprecated() && value != null) {
                    StringBuilder msg = new StringBuilder();
                    msg.append("USE OF DEPRECATED API:  A value was specified for a deprecated REAGENT.")
                            .append("\n\t\tReagent Name:  ").append(r.getName()).append("\n\t\tDeprecated Since:  ")
                            .append(r.getDeprecation().getVersion()).append("\n\t\tEntry Name:  ")
                            .append(n.getName()).append("\n\t\tEntry Type:  ").append(n.getType())
                            .append("\n\t\tSource:  ").append(d.getUniquePath() + "/" + r.getXpath()).append("\n");
                    log.warn(msg.toString());
                }

                if (value == null) {
                    // First see if there's a default...
                    if (r.hasDefault()) {
                        value = r.getDefault();
                    } else {
                        String msg = "The required expression '" + r.getXpath()
                                + "' is missing from the following node:  " + d.asXML();
                        throw new RuntimeException(msg);
                    }
                }
                mappings.put(r, value);
            }
            mappings.putAll(n.getMappings());

            String entryName = null;
            if (n.getType().equals(Entry.Type.TASK)) {
                entryName = "<" + n.getName() + ">";
            } else if (n.getType().equals(Entry.Type.PHRASE)) {
                entryName = "${" + n.getName() + "}";
            } else {
                throw new RuntimeException("Unsupported Entry Type:  " + n.getType());
            }

            return new SimpleEntityConfig(this, entryName, source, n.getFormula(), mappings);

        } catch (Throwable t) {
            StringBuilder msg = new StringBuilder();
            msg.append("Unable to prepare an EntityConfig based on the specified information:")
                    .append("\n\t\tEntity Name:  ").append(n.getName()).append("\n\t\tSource:  ").append(source);
            throw new RuntimeException(msg.toString(), t);
        }

    }
}