Package galoot.interpret

Source Code of galoot.interpret.Interpreter

package galoot.interpret;

import galoot.ContextStack;
import galoot.Filter;
import galoot.PluginRegistry;
import galoot.Template;
import galoot.TemplateUtils;
import galoot.analysis.DepthFirstAdapter;
import galoot.lexer.LexerException;
import galoot.node.AAndBooleanOp;
import galoot.node.ABlock;
import galoot.node.ABooleanExpr;
import galoot.node.ACharEntity;
import galoot.node.AExtends;
import galoot.node.AFilter;
import galoot.node.AFilterBlock;
import galoot.node.AFirstOfEntity;
import galoot.node.AForBlock;
import galoot.node.AIfBlock;
import galoot.node.AIfequalBlock;
import galoot.node.ANowEntity;
import galoot.node.AOrBooleanOp;
import galoot.node.AQuotedFilterArg;
import galoot.node.ASetEntity;
import galoot.node.AStringArgument;
import galoot.node.AStringAsPlugin;
import galoot.node.AStringInclude;
import galoot.node.AStringPlugin;
import galoot.node.ATemplatetagEntity;
import galoot.node.AUnquotedFilterArg;
import galoot.node.AVarAsPlugin;
import galoot.node.AVarExpression;
import galoot.node.AVarPlugin;
import galoot.node.AVariableEntity;
import galoot.node.AVariableInclude;
import galoot.node.AWithBlock;
import galoot.node.PArgument;
import galoot.node.PEntity;
import galoot.node.Start;
import galoot.node.TMember;
import galoot.parser.ParserException;
import galoot.types.BlockFragment;
import galoot.types.Document;
import galoot.types.Pair;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;

import java.util.Formatter;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Pattern;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class Interpreter extends DepthFirstAdapter
{
    private static Log log = LogFactory.getLog(Interpreter.class);

    // ! The document that contains all of the fragments
    private Document document = null;

    // ! If the document is an extension of another, the parent document
    private Document parentDocument = null;

    // ! the context stack
    private ContextStack context;

    // ! stack used to keep track of variable expressions as they evaluated
    private Stack<Object> variableStack;

    // ! stack used to keep track of filter expressions, as they are evaluated
    private LinkedList<Pair<String, String>> filterStack;

    /**
     * stack of actual {% filter %} blocks, which needs to do post-processing on
     * the block of data
     */
    private Stack<StringBuilder> filterBlockData;

    // ! keeps track of for loops vars, so we can refer to parent loop
    // counter(s)
    private Stack<Map<String, Object>> forLoopStack;

    /**
     * Creates a new Interpreter, using the given context stack as the initial
     * values available to any processed templates
     *
     * @param context
     */
    public Interpreter(ContextStack context)
    {
        this.context = context;
    }

    /**
     * Initialize the instance vars used in processing
     */
    private void init()
    {
        document = new Document();
        variableStack = new Stack<Object>();
        filterStack = new LinkedList<Pair<String, String>>();
        filterBlockData = new Stack<StringBuilder>();
        forLoopStack = new Stack<Map<String, Object>>();
    }

    @Override
    public void inStart(Start node)
    {
        // when in the start production, we initialize everything
        init();
        super.inStart(node);
    }

    /**
     * Call finishString when the evaluated string is completely finished being
     * processed.
     *
     * @param s
     */
    private void finishString(String s)
    {
        // if we are in a filterBlock, we need to "write" the data to it
        if (!filterBlockData.isEmpty())
        {
            filterBlockData.peek().append(s);
        }
        else
        {
            // add the text to the document
            document.addContent(s.toString());
        }
    }

    @Override
    public void outAVarExpression(AVarExpression node)
    {
        // whenever we come across a variable expression, we evaluate it, then
        // push it onto an expression stack. This way, the operations that use
        // the expressions can just pop off the stack

        String referent = node.getReferent().getText();

        // turn the members into a String list
        LinkedList<TMember> members = node.getMembers();
        List<String> stringMembers = new ArrayList<String>(members.size());
        String fullDotExpression = referent; // construct a full expression
        for (TMember member : members)
        {
            String memberText = member.getText();
            stringMembers.add(memberText);
            fullDotExpression += "." + memberText;
        }

        // first things first, see if the full "dot" expression is in the map
        Object object = context.get(fullDotExpression);
        if (object == null)
        {
            // try to get just the base object then
            object = context.get(referent);

            // evaluate the object/methods
            object = TemplateUtils.evaluateObject(object, stringMembers);
        }

        // apply the filters
        for (int i = 0, size = node.getFilters().size(); i < size; ++i)
        {
            Pair<String, String> nextFilter = filterStack.getLast();
            Filter filter = context.getFilterMap().getFilter(
                    nextFilter.getFirst());
            if (filter == null)
                object = null;
            else
                object = filter.filter(object, nextFilter.getSecond());
        }

        variableStack.push(object);
    }

    @Override
    public void outAQuotedFilterArg(AQuotedFilterArg node)
    {
        // strip the quotes and push it on the stack
        variableStack.push(TemplateUtils.stripEncasedString(node.getArg()
                .getText(), '"'));
    }

    @Override
    public void outAUnquotedFilterArg(AUnquotedFilterArg node)
    {
        variableStack.push(node.getArg().getText());
    }

    @Override
    public void outAFilter(AFilter node)
    {
        String name = node.getFilter().getText();
        // pop the filter arg off of the var stack, if it exists
        String arg = node.getArg() != null ? variableStack.pop().toString()
                : "";
        filterStack.add(0, new Pair<String, String>(name, arg));
    }

    @Override
    public void outAVariableEntity(AVariableEntity node)
    {
        Object object = variableStack.pop();
        if (object != null)
        {
            finishString(object.toString());
        }
    }

    @Override
    public void outABooleanExpr(ABooleanExpr node)
    {
        // pop the variable (evaluated expression) off the stack
        Object object = variableStack.pop();
        boolean negate = node.getNot() != null;

        boolean boolObj = TemplateUtils.evaluateAsBoolean(object);
        boolObj = negate ? !boolObj : boolObj;
        variableStack.push(boolObj);
    }

    @Override
    public void outAAndBooleanOp(AAndBooleanOp node)
    {
        // pop the top two items off the variable stack
        boolean result = TemplateUtils.evaluateAsBoolean(variableStack.pop())
                && TemplateUtils.evaluateAsBoolean(variableStack.pop());
        variableStack.push(result);
    }

    @Override
    public void outAOrBooleanOp(AOrBooleanOp node)
    {
        // pop the top two items off the variable stack
        boolean result = TemplateUtils.evaluateAsBoolean(variableStack.pop())
                || TemplateUtils.evaluateAsBoolean(variableStack.pop());
        variableStack.push(result);
    }

    @Override
    public void outAStringArgument(AStringArgument node)
    {
        // remove the end-quotes and push the string argument onto the stack
        variableStack.push(TemplateUtils.stripEncasedString(node.getString()
                .getText(), '"'));
    }

    private void loadFilterPlugin(String pluginName, String alias)
    {
        if (alias == null)
            alias = pluginName;
        Filter filter = PluginRegistry.getInstance().getFilter(pluginName);
        if (filter == null)
        {
            // TODO log here
        }
        else
        {
            // add the filter to the context's filter map
            context.getFilterMap().addFilter(filter, alias);
        }
    }

    @Override
    public void outAStringPlugin(AStringPlugin node)
    {
        // strip the quotes
        loadFilterPlugin(TemplateUtils.stripEncasedString(node.getString()
                .getText(), '"'), null);
    }

    @Override
    public void outAStringAsPlugin(AStringAsPlugin node)
    {
        // strip the quotes
        loadFilterPlugin(TemplateUtils.stripEncasedString(node.getString()
                .getText(), '"'), node.getAlias().getText());
    }

    @Override
    public void outAVarPlugin(AVarPlugin node)
    {
        // pop the var off the stack
        Object var = variableStack.pop();
        if (var != null)
            loadFilterPlugin(var.toString(), null);
        else
            log
                    .warn("plug-in expression evaluated to null: "
                            + node.toString());
    }

    @Override
    public void outAVarAsPlugin(AVarAsPlugin node)
    {
        // pop the var off the stack
        Object var = variableStack.pop();
        loadFilterPlugin(var.toString(), node.getAlias().getText());
    }

    @Override
    public void outAStringInclude(AStringInclude node)
    {
        String filename = TemplateUtils.stripEncasedString(node.getString()
                .getText(), '"');

        processIncludedFile(filename);
    }

    @Override
    public void outAVariableInclude(AVariableInclude node)
    {
        Object pop = variableStack.pop();
        if (pop != null)
            processIncludedFile(pop.toString());
        else
            log.warn("include expression evaluated to null: "
                    + node.getVariable().toString());
    }

    /**
     * Lookup and render the filename if it is found in the include directories
     *
     * @param filename
     */
    private void processIncludedFile(String filename)
    {
        context.push();
        try
        {
            Document doc = loadDocument(filename);
            if (doc == null)
                throw new Exception();
            finishString(doc.evaluateAsString());
        }
        catch (Throwable e)
        {
            log.warn("File could not be included: " + filename);
            // ignore the problem for now
        }
        finally
        {
            context.pop();
        }
    }

    @Override
    public void caseAForBlock(AForBlock node)
    {
        // figure out how many times we will have to iterate
        // for each iteration, push, then pop a new context onto the stack
        // the context will contain some extra variables, including the
        // loop variable, and some other "special" counter variables

        inAForBlock(node);
        if (node.getIterVar() != null)
        {
            node.getIterVar().apply(this);
        }
        if (node.getVariable() != null)
        {
            node.getVariable().apply(this);
        }

        // this is the loop variable, which will get updated each iteration
        String loopVar = node.getIterVar().getText();

        // pop the loop expression off the stack
        Object loopObj = variableStack.pop();

        // we will only support Iterable types
        Integer objectSize = TemplateUtils.getObjectLength(loopObj);

        // first, see if we can convert the type to an iterable type
        if (TemplateUtils.isArrayType(loopObj))
            loopObj = TemplateUtils.objectToCollection(loopObj);

        if (loopObj instanceof Iterable)
        {
            Iterable iter = (Iterable) loopObj;

            int iterCount = 0;
            for (Iterator it = iter.iterator(); it.hasNext(); ++iterCount)
            {
                Object object = (Object) it.next();
                List<PEntity> copy = new ArrayList<PEntity>(node.getEntities());
                processForLoopIteration(loopVar, object, iterCount, copy, !it
                        .hasNext(), objectSize);
            }
        }

        outAForBlock(node);
    }

    /**
     * Process one iteration of a forloop.
     *
     * @param loopVar
     *            the loop variable name
     * @param loopObj
     *            the loop object, already fully evaluated
     * @param iterCount
     *            the current count of the iteration, zero based
     * @param entities
     *            the entities to process
     * @param isLast
     *            true if this iteration is the last in the loop
     */
    private void processForLoopIteration(String loopVar, Object loopObj,
            int iterCount, Iterable<PEntity> entities, boolean isLast,
            int totalLoops)
    {
        // push a new context
        context.push();

        Map<String, Object> extraLoopVars = new HashMap<String, Object>();

        // add the vars
        context.put(loopVar, loopObj);
        extraLoopVars.put("counter0", iterCount);
        extraLoopVars.put("counter1", iterCount + 1);
        extraLoopVars.put("first", iterCount == 0);
        extraLoopVars.put("last", isLast);
        int revCounter = totalLoops - iterCount;
        extraLoopVars.put("revcounter", revCounter);
        extraLoopVars.put("revcounter0", revCounter - 1);
        if (!forLoopStack.isEmpty())
            extraLoopVars.put("parent", forLoopStack.peek());
        context.put("forloop", extraLoopVars);

        // push the loop vars onto the stack, so sub-for loops can access it
        forLoopStack.push(extraLoopVars);

        // apply to the entities
        for (PEntity e : entities)
        {
            e.apply(this);
        }

        // pop the context
        context.pop();

        // pop the extra vars off the forloop stack
        forLoopStack.pop();
    }

    @Override
    public void caseAIfBlock(AIfBlock node)
    {
        // if true, execute the entities
        // if false, and an else block exists, execute the else entities

        inAIfBlock(node);
        if (node.getExpr1() != null)
        {
            node.getExpr1().apply(this);
        }
        if (node.getExpr2() != null)
        {
            node.getExpr2().apply(this);
        }

        // at this point, the boolean expression result is on the var. stack
        boolean evaluateIf = TemplateUtils.evaluateAsBoolean(variableStack
                .pop());

        // evalute the if-clause
        if (evaluateIf)
        {
            List<PEntity> copy = new ArrayList<PEntity>(node.getIf());
            for (PEntity e : copy)
            {
                e.apply(this);
            }
        }
        // otherwise, evaluate the else-clause
        else
        {
            List<PEntity> copy = new ArrayList<PEntity>(node.getElse());
            for (PEntity e : copy)
            {
                e.apply(this);
            }
        }
        outAIfBlock(node);
    }

    @Override
    public void caseAIfequalBlock(AIfequalBlock node)
    {
        inAIfequalBlock(node);
        {
            List<PArgument> copy = new ArrayList<PArgument>(node.getArguments());
            for (PArgument e : copy)
            {
                e.apply(this);
            }
        }

        // this assumes that each of the arguments has already been evaluated
        // and added to the stack
        // get the objects from the stack and check to see if they are equal
        boolean equals = true;
        Object object = variableStack.pop();
        int numArgs = node.getArguments().size();
        for (int i = 1; i < numArgs && equals && object != null; i++)
        {
            Object nextObj = variableStack.pop();
            equals &= (nextObj != null && object.equals(nextObj));
        }

        // if they are equal, we evaluate the if-clause
        if (equals)
        {
            List<PEntity> copy = new ArrayList<PEntity>(node.getIfequal());
            for (PEntity e : copy)
            {
                e.apply(this);
            }
        }
        // otherwise, evaluate the else clause
        else
        {
            List<PEntity> copy = new ArrayList<PEntity>(node.getElse());
            for (PEntity e : copy)
            {
                e.apply(this);
            }
        }
        outAIfequalBlock(node);
    }

    @Override
    public void inAFilterBlock(AFilterBlock node)
    {
        // push a new StringBuilder, for this block
        filterBlockData.push(new StringBuilder());
    }

    @Override
    public void outAFilterBlock(AFilterBlock node)
    {
        StringBuilder filteredData = filterBlockData.pop();
        Object output = filteredData.toString();

        // apply the filters
        for (int i = 0, size = node.getFilters().size(); i < size
                && output != null; ++i)
        {
            Pair<String, String> nextFilter = filterStack.getLast();
            Filter filter = context.getFilterMap().getFilter(
                    nextFilter.getFirst());

            // TODO decide on what we should do if a bad filter was given
            if (filter == null)
                // if a bad filter, nothing gets written
                output = null;
            else
            {
                output = filter.filter(output, nextFilter.getSecond());
            }
        }

        if (output != null)
            finishString(output.toString());
    }

    @Override
    public void caseAWithBlock(AWithBlock node)
    {
        inAWithBlock(node);
        if (node.getExpression() != null)
        {
            node.getExpression().apply(this);
        }
        if (node.getVar() != null)
        {
            node.getVar().apply(this);
        }

        // this is the loop variable, which will get updated each iteration
        String withVar = node.getVar().getText();

        // pop the loop expression off the stack
        Object withObj = variableStack.pop();

        // push a new context
        context.push();
        // add the vars
        context.put(withVar, withObj);
        {
            List<PEntity> copy = new ArrayList<PEntity>(node.getEntities());
            for (PEntity e : copy)
            {
                e.apply(this);
            }
        }
        // pop the context
        context.pop();
        outAWithBlock(node);
    }

    /**
     * This method probably should be propagated to a higher level somewhere. It
     * searches for the given filename within the template include paths
     * specified in the PluginRegistry.
     *
     * @param filename
     * @return
     * @throws ParserException
     * @throws LexerException
     * @throws IOException
     */
    private Document loadDocument(String filename) throws ParserException,
            LexerException, IOException
    {
        // look in the registry for the paths where include files can be found.
        Iterable<String> includePaths = PluginRegistry.getInstance()
                .getTemplateIncludePaths();
        File templateFile = null;
        // loop over the paths to see if the file exists
        for (String path : includePaths)
        {
            File file = new File(path, filename);
            if (file.exists())
            {
                templateFile = file;
                break;
            }
        }
        if (templateFile != null)
            return new Template(templateFile).renderDocument(context);
        else
            log.warn("File could not be located in include path(s): " + filename);
        return null;
    }

    @Override
    public void inAExtends(AExtends node)
    {
        // if we are in here, then this document extends another one
        String parentDoc = TemplateUtils.stripEncasedString(node
                .getParentName().getText(), '"');

        try
        {
            // try to load the doc.
            parentDocument = loadDocument(parentDoc);
            if (parentDoc == null)
                throw new Exception("Unable to load parent document: "
                        + parentDoc);
        }
        catch (Throwable e)
        {
            log.warn(ExceptionUtils.getStackTrace(e));
            throw new RuntimeException(e);
        }
    }

    @Override
    public void outACharEntity(ACharEntity node)
    {
        finishString(node.getChar().getText());
    }

    @Override
    public void caseABlock(ABlock node)
    {
        boolean evaluate = false, existsInParent = false;
        int curBlockDepth = document.getBlockDepth();

        String blockName = node.getId() != null ? node.getId().getText() : null;

        /*
         * We only render the block if one of the following is true: (1) if the
         * document has a parent doc AND ((this block is a top-level block of
         * this document AND exists in the parent) OR (this block is NOT a
         * top-level block and does NOT exist in the parent)), or if (2) the
         * document does NOT have a parent doc. AND the block name has not
         * already been used in the current document.
         */

        if (parentDocument != null)
        {
            BlockFragment parentBlock = parentDocument.getDocumentBlock();
            existsInParent = blockName != null
                    && parentBlock.hasBlock(blockName);

            if ((curBlockDepth == 0 && existsInParent)
                    || (curBlockDepth > 0 && !existsInParent))
                evaluate = true;
        }
        else if (blockName != null && document.hasBlock(blockName))
            throw new RuntimeException("Block already exists with name: "
                    + blockName);
        else
            evaluate = true;

        if (evaluate)
        {
            // push on a new context
            context.push();

            // add a new BlockFragment to the document
            BlockFragment newBlock = new BlockFragment(blockName);
            document.addContent(newBlock);

            /*
             * If it existed in the parent, and top-level, we add a special
             * variable to the context, which is essentially a "super" lookup.
             * Note that the data in the super block has already been evaluated.
             */
            if (curBlockDepth == 0 && existsInParent)
            {
                String superBlock = parentDocument.getDocumentBlock().getBlock(
                        blockName).evaluateAsString();
                context.put("block.super", superBlock);
            }

            inABlock(node);
            if (node.getId() != null)
            {
                node.getId().apply(this);
            }

            {
                List<PEntity> copy = new ArrayList<PEntity>(node.getEntities());
                for (PEntity e : copy)
                {
                    e.apply(this);
                }
            }
            outABlock(node);

            // tell the document we are done with the block
            document.popBlock();

            // pop the context
            context.pop();

            // now, replace the "super" fragment, if one existed
            if (curBlockDepth == 0 && existsInParent)
            {
                parentDocument.replaceBlock(newBlock);
            }
        }
    }

    @Override
    public void outAFirstOfEntity(AFirstOfEntity node)
    {
        int numArgs = node.getArgs().size();

        /*
         * since the args are pushed on the expression stack in order, we have
         * to use reverse logic here
         */
        Object obj = null;
        for (int i = 0; i < numArgs; ++i)
        {
            Object var = variableStack.pop();
            if (TemplateUtils.evaluateAsBoolean(var))
                obj = var;
        }
        if (obj != null)
            finishString(obj.toString());
    }

    @Override
    public void outATemplatetagEntity(ATemplatetagEntity node)
    {
        String tag = node.getTag().getText().toLowerCase();
        if (tag.equals("openblock"))
            finishString("{%");
        else if (tag.equals("closeblock"))
            finishString("%}");
        else if (tag.equals("openvariable"))
            finishString("{{");
        else if (tag.equals("closevariable"))
            finishString("}}");
        else if (tag.equals("openbrace"))
            finishString("{");
        else if (tag.equals("closebrace"))
            finishString("}");
        else if (tag.equals("opencomment"))
            finishString("{#");
        else if (tag.equals("closecomment"))
            finishString("#}");
    }

    @Override
    public void outANowEntity(ANowEntity node)
    {
        Calendar cal = Calendar.getInstance();
        String format = node.getFormat() != null ? TemplateUtils
                .stripEncasedString(node.getFormat().getText(), '"') : null;

        try
        {
            String formatted = cal.getTime().toString();
            if (format != null)
            {
                // add a little hack here that lets you not have to specify the
                // 1$ positional chars in the format
                String[] parts = format.split("%[tT]");
                if (parts.length == 0)
                    formatted = new Formatter().format(format, cal).toString();
                else
                {
                    Calendar calArr[] = new Calendar[parts.length];
                    for (int i = 0, size = parts.length; i < size; ++i)
                        calArr[i] = cal;
                    formatted = new Formatter().format(format, (Object[])calArr)
                            .toString();
                }
            }

            if (formatted != null)
                finishString(formatted);
        }
        catch (Throwable e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
   
    @Override
    public void outASetEntity(ASetEntity node)
    {
        //pop off the stack
        Object value = variableStack.pop();
        String varName = node.getVar().getText();
       
        //add the variable to the current context
        context.put(varName, value);
    }

    /**
     * Get the fully qualified document which results after applying the
     * interpreter to an AST.
     *
     * @return
     */
    public Document getDocument()
    {
        /*
         * if we have a parent (via extends keyword) return it, since everything
         * was added to it anyway. Otherwise, return our own document.
         */
        return parentDocument != null ? parentDocument : document;
    }

}
TOP

Related Classes of galoot.interpret.Interpreter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.