com.squarespace.template.Context.java Source code

Java tutorial

Introduction

Here is the source code for com.squarespace.template.Context.java

Source

/**
 * Copyright (c) 2014 SQUARESPACE, Inc.
 *
 * 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 com.squarespace.template;

import static com.squarespace.template.ExecuteErrorType.APPLY_PARTIAL_RECURSION_DEPTH;

import static com.squarespace.template.ExecuteErrorType.APPLY_PARTIAL_SELF_RECURSION;
import static com.squarespace.template.ExecuteErrorType.UNEXPECTED_ERROR;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.LongNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.fasterxml.jackson.databind.node.TextNode;

/**
 * Tracks all of the state needed for executing a template against a given JSON tree.
 *
 * Compilation converts the raw text into an instruction tree. The instruction tree
 * is stateless and can be reused across multiple executions.
 *
 * The Context is used to carry out a single execution of the template instruction tree.
 * Each execution of a template requires a fresh context object.
 */
public class Context {

    private static final JsonNode DEFAULT_UNDEFINED = MissingNode.getInstance();

    private static final String META_LEFT = "{";

    private static final String META_RIGHT = "}";

    private final Locale locale;

    private Compiler compiler;

    private Frame currentFrame;

    private JsonNode undefined = DEFAULT_UNDEFINED;

    private boolean safeExecution = false;

    private int maxPartialDepth = Constants.DEFAULT_MAX_PARTIAL_DEPTH;

    private List<ErrorInfo> errors;

    /**
     * Reference to the currently-executing instruction. All instruction execution
     * must pass control via the Context, for proper error handling.
     */
    private Instruction currentInstruction;

    private JsonNode rawPartials;

    private Map<String, Instruction> compiledPartials;

    private Set<String> partialsExecuting;

    private int partialDepth;

    private LoggingHook loggingHook;

    private CodeLimiter codeLimiter = new NoopCodeLimiter();

    /* Holds the final output of the template execution */
    private StringBuilder buf;

    public Context(JsonNode node) {
        this(node, new StringBuilder(), Locale.getDefault());
    }

    public Context(JsonNode node, StringBuilder buf, Locale locale) {
        this.currentFrame = new Frame(null, node == null ? MissingNode.getInstance() : node);
        this.buf = buf == null ? new StringBuilder() : buf;
        this.locale = locale == null ? Locale.getDefault() : locale;
    }

    public boolean safeExecutionEnabled() {
        return safeExecution;
    }

    public List<ErrorInfo> getErrors() {
        return (errors == null) ? Collections.<ErrorInfo>emptyList() : errors;
    }

    public Locale locale() {
        return locale;
    }

    /**
     * Set mode where no exceptions will be thrown; instead
     */
    public void setSafeExecution() {
        this.safeExecution = true;
    }

    public void setMaxPartialDepth(int depth) {
        this.maxPartialDepth = Math.max(0, depth);
    }

    public CharSequence getMetaLeft() {
        return META_LEFT;
    }

    public CharSequence getMetaRight() {
        return META_RIGHT;
    }

    /**
     * Swap the buffer for the current formatter.
     */
    public StringBuilder swapBuffer(StringBuilder newBuffer) {
        StringBuilder tmp = buf;
        buf = newBuffer;
        return tmp;
    }

    /**
     * Sets a compiler to be used for compiling partials. If no compiler is set,
     * partials cannot be compiled and will raise errors.
     */
    public void setCompiler(Compiler compiler) {
        this.compiler = compiler;
    }

    public Compiler getCompiler() {
        return compiler;
    }

    public void setLoggingHook(LoggingHook hook) {
        this.loggingHook = hook;
    }

    public CodeLimiter getCodeLimiter() {
        return codeLimiter;
    }

    public void setCodeLimiter(CodeLimiter limiter) {
        this.codeLimiter = limiter;
    }

    /**
     * Execute a single instruction.
     */
    public void execute(Instruction instruction) throws CodeExecuteException {
        if (instruction == null) {
            return;
        }
        currentInstruction = instruction;
        try {
            codeLimiter.check();
            instruction.invoke(this);

        } catch (CodeExecuteException e) {
            // This is thrown explicitly when an instruction / plugin needs to
            // abort execution. Instructions and plugins must first check if
            // safe execution mode is enabled before throwing. This gives us
            // the flexibility to abort execution even when safe mode is enabled
            // for severe errors, or when a hard resource limit is reached.
            throw e;

        } catch (Exception e) {
            String repr = ReprEmitter.get(instruction, false);
            ErrorInfo error = error(UNEXPECTED_ERROR).name(e.getClass().getSimpleName()).data(e.getMessage())
                    .repr(repr);

            // In safe mode we don't raise exceptions; just append the error.
            if (safeExecution) {
                addError(error);
            } else {
                throw new CodeExecuteException(error, e);
            }

            // If a logging hook exists, always log the unexpected exception.
            log(e);
        }
    }

    /**
     * Execute a list of instructions.
     */
    public void execute(List<Instruction> instructions) throws CodeExecuteException {
        if (instructions != null) {
            int size = instructions.size();
            for (int i = 0; i < size; i++) {
                execute(instructions.get(i));
            }
        }
    }

    public ErrorInfo error(ExecuteErrorType code) {
        ErrorInfo info = new ErrorInfo(code);
        info.code(code);
        info.line(currentInstruction.getLineNumber());
        info.offset(currentInstruction.getCharOffset());
        return info;
    }

    /**
     * Lazily allocate the compiled partials cache.
     */
    public void setPartials(JsonNode node) {
        this.rawPartials = node;
        this.compiledPartials = new HashMap<>();
    }

    /**
     * Returns the root instruction for a compiled partial, assuming the partial exists
     * in the partials map. Compiled partials are cached for reuse within the same
     * context, since a partial may be applied multiple times within a template, or
     * inside a loop.
     */
    public Instruction getPartial(String name) throws CodeSyntaxException {
        if (rawPartials == null) {
            // Template wants to use a partial but none are defined.
            return null;
        }

        // See if we've previously compiled this exact partial.
        Instruction inst = compiledPartials.get(name);
        if (inst == null) {
            JsonNode partialNode = rawPartials.get(name);
            if (partialNode == null) {
                // Indicate partial is missing.
                return null;
            }
            if (!partialNode.isTextual()) {
                // Should we bother worrying about this, or just cast the node to text?
                return null;
            }

            // Compile the partial.  This can throw a syntax exception, which the formatter
            // will catch and nest inside a runtime exception.
            String source = partialNode.asText();
            CompiledTemplate template = compiler.compile(source, safeExecution);
            if (safeExecution) {
                List<ErrorInfo> errors = template.errors();
                if (!errors.isEmpty()) {
                    ErrorInfo parent = error(ExecuteErrorType.COMPILE_PARTIAL_SYNTAX).name(name);
                    parent.child(errors);
                    addError(parent);
                }
            }

            // Cache the compiled template in case it is used more than once.
            inst = template.code();
            compiledPartials.put(name, inst);
        }
        return inst;
    }

    /**
     * Check if we're about to recurse through a partial we're already evaluating.
     * This code currently prevents all reentrant evaluation of partials.
     *
     * NOTE: The template team will need to weigh in on whether we currently have
     * partials which recurse but properly terminate recursion. For now this code treats
     * all recursion as an error.
     */
    public boolean enterPartial(String name) throws CodeExecuteException {
        if (partialsExecuting == null) {
            partialsExecuting = new HashSet<>();
        }

        // Prevent partials from recursing into themselves
        if (!partialsExecuting.add(name)) {
            ErrorInfo error = error(APPLY_PARTIAL_SELF_RECURSION).name(name);
            if (safeExecution) {
                addError(error);
                return false;
            } else {
                throw new CodeExecuteException(error);
            }
        }

        // Limit maximum partial recursion depth
        partialDepth++;
        if (partialDepth > maxPartialDepth) {
            ErrorInfo error = error(APPLY_PARTIAL_RECURSION_DEPTH).name(name).data(maxPartialDepth);
            if (safeExecution) {
                addError(error);
                return false;
            } else {
                throw new CodeExecuteException(error);
            }
        }
        return true;
    }

    /**
     * Clears flag indicating we're executing inside a partial template.
     */
    public void exitPartial(String name) {
        partialsExecuting.remove(name);
        partialDepth--;
    }

    public StringBuilder buffer() {
        return buf;
    }

    public JsonNode node() {
        return currentFrame.node();
    }

    public JsonNode buildNode(String value) {
        return new TextNode(value);
    }

    public JsonNode buildNode(int value) {
        return new IntNode(value);
    }

    public JsonNode buildNode(long value) {
        return new LongNode(value);
    }

    public JsonNode buildNode(double value) {
        return new DoubleNode(value);
    }

    public boolean initIteration() {
        JsonNode node = node();
        if (!node.isArray()) {
            return false;
        }
        currentFrame.currentIndex = 0;
        return true;
    }

    /**
     * Use this to find the index position in the current frame.
     */
    public int currentIndex() {
        return currentFrame.currentIndex;
    }

    public boolean hasNext() {
        return currentFrame.currentIndex < currentFrame.node().size();
    }

    /**
     * Return the current frame's array size.
     */
    public int arraySize() {
        return currentFrame.node().size();
    }

    /**
     * Increment the array element pointer for the current frame.
     */
    public void increment() {
        currentFrame.currentIndex++;
    }

    /**
     * Push the node referenced by names onto the stack.
     */
    public void push(Object[] names) {
        push(resolve(names));
    }

    /**
     * SECTION/REPEATED scope does not look up the stack.  It only resolves
     * names against the current frame's node downward.
     */
    public void pushSection(Object[] names) {
        JsonNode node;
        if (names == null) {
            node = currentFrame.node();
        } else {
            node = resolve(names[0], currentFrame);
            for (int i = 1, len = names.length; i < len; i++) {
                if (node.isMissingNode()) {
                    break;
                }
                node = nodePath(node, names[i]);
            }
        }
        push(node);
    }

    /**
     * Pushes the next element from the current array node onto the stack.
     */
    public void pushNext() {
        JsonNode node = currentFrame.node().path(currentFrame.currentIndex);
        if (node.isNull()) {
            node = undefined;
        }
        push(node);
    }

    public void setVar(String name, JsonNode node) {
        currentFrame.setVar(name, node);
    }

    public JsonNode resolve(Object name) {
        return lookupStack(name);
    }

    /**
     * Lookup the JSON node referenced by the list of names.
     */
    public JsonNode resolve(Object[] names) {
        if (names == null) {
            return currentFrame.node();
        }

        // Find the starting point.
        JsonNode node = lookupStack(names[0]);
        for (int i = 1, len = names.length; i < len; i++) {
            if (node.isMissingNode()) {
                return undefined;
            }
            if (node.isNull()) {
                // NOTE: Future warnings should be emitted as a side-effect not inline.
                return Constants.MISSING_NODE;
            }
            node = nodePath(node, names[i]);
        }
        return node;
    }

    private void log(Exception exc) {
        if (loggingHook != null) {
            loggingHook.log(exc);
        }
    }

    public Frame frame() {
        return currentFrame;
    }

    public void push(JsonNode node) {
        currentFrame = new Frame(currentFrame, node);
    }

    public void pop() {
        currentFrame = currentFrame.parent();
    }

    /**
     * Starting at the current frame, walk up the stack looking for the first
     * object node which contains 'name' and return that. If none match, return
     * undefined.
     */
    private JsonNode lookupStack(Object name) {
        JsonNode node = resolve(name, currentFrame);
        if (!node.isMissingNode()) {
            return node;
        }

        Frame frame = currentFrame;
        while (frame != null) {
            node = resolve(name, frame);
            if (!node.isMissingNode()) {
                return node;
            }
            if (frame.stopResolution) {
                break;
            }
            frame = frame.parent();
        }
        return undefined;
    }

    /**
     * Obtain the value for 'name' from the given stack frame's node.
     */
    private JsonNode resolve(Object name, Frame frame) {
        // Special internal variable @index points to the array index for a
        // given stack frame.
        if (name instanceof String) {
            String strName = (String) name;

            if (strName.startsWith("@")) {
                if (name.equals("@index")) {
                    if (frame.currentIndex != -1) {
                        // @index is 1-based
                        return new IntNode(frame.currentIndex + 1);
                    }
                    return Constants.MISSING_NODE;
                }
                JsonNode node = frame.getVar(strName);
                return (node == null) ? Constants.MISSING_NODE : node;
            }

            // Fall through
        }
        return nodePath(frame.node(), name);
    }

    private JsonNode nodePath(JsonNode node, Object key) {
        if (key instanceof Integer) {
            return node.path((int) key);
        }
        return node.path((String) key);
    }

    public void addError(ErrorInfo error) {
        if (errors == null) {
            errors = new ArrayList<>();
        }
        errors.add(error);
    }

}