All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.squarespace.template.Context Maven / Gradle / Ivy

/**
 * 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.UNEXPECTED_ERROR;

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

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.IntNode;
import com.fasterxml.jackson.databind.node.MissingNode;
import com.squarespace.cldrengine.CLDR;
import com.squarespace.cldrengine.api.CLocale;
import com.squarespace.template.expr.ExprOptions;


/**
 * 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 Locale javaLocale;

  private CLDR cldrengine;

  private MessageFormats messageformats;

  private Compiler compiler;

  private Frame currentFrame;

  private JsonNode undefined = DEFAULT_UNDEFINED;

  private boolean safeExecution = false;

  private boolean preprocess = false;

  private int maxPartialDepth = Constants.DEFAULT_MAX_PARTIAL_DEPTH;

  private List 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 compiledPartials;

  private JsonNode rawInjectables;

  private Map parsedInjectables;

  private int partialDepth;

  private Long now = null;

  private LoggingHook loggingHook;

  private CodeLimiter codeLimiter = new NoopCodeLimiter();

  private boolean enableExpr = false;
  private ExprOptions exprOptions = null;
  private boolean enableInclude = false;

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

  public Context(JsonNode node) {
    this(node, new StringBuilder(), null);
  }

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

  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.javaLocale = locale == null ? Locale.US : locale;
  }

  public boolean safeExecutionEnabled() {
    return safeExecution;
  }

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

  /**
   * Return the expression evaluation options.
   */
  public ExprOptions exprOptions() {
    return exprOptions;
  }

  /**
   * Set the options for expression evaluation.
   */
  public void exprOptions(ExprOptions options) {
    this.exprOptions = options;
  }

  public Locale javaLocale() {
    return javaLocale;
  }

  public void javaLocale(Locale locale) {
    this.javaLocale = locale;
  }

  public void now(Long value) {
    this.now = value;
  }

  public Long now() {
    return now;
  }

//  public void cldrLocale(CLDR.Locale locale) {
//    this.cldrLocale = locale;
//  }
//
//  public CLDR.Locale cldrLocale() {
//    return cldrLocale;
//  }

  public MessageFormats messageFormatter() {
    if (this.messageformats == null) {
      this.messageformats = new MessageFormats(this.cldr());
    }
    return this.messageformats;
  }

  public CLDR cldr() {
    if (cldrengine == null) {
      String tag = javaLocale != null ? this.javaLocale.toLanguageTag() : "en-US";
      this.cldrengine = com.squarespace.cldrengine.CLDR.get(tag);
    }
    return cldrengine;
  }

  public void cldrengine(CLDR cldr) {
    this.cldrengine = cldr;
  }

  public void cldrengine(String id) {
    this.cldrengine = CLDR.get(id);
  }

  public void cldrengine(CLocale locale) {
    this.cldrengine = CLDR.get(locale);
  }

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

  /**
   * Enable the expression evaluation instruction EVAL.
   */
  public void setEnableExpr(boolean flag) {
    this.enableExpr = flag;
  }

  public boolean getEnableExpr() {
    return this.enableExpr;
  }

  public void setEnableInclude(boolean flag) {
    this.enableInclude = flag;
  }

  public boolean getEnableInclude() {
    return this.enableInclude;
  }

  /**
   * Set the options governing expression evaluation.
   */
  public void setExprOptions(ExprOptions opts) {
    this.exprOptions = opts;
  }

  public ExprOptions getExprOptions() {
    return this.exprOptions;
  }

  /**
   * Enable pre-processor mode.
   */
  public void setPreprocess(boolean preprocess) {
    this.preprocess = preprocess;
  }

  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 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);
    if (currentInstruction != null) {
      info.line(currentInstruction.getLineNumber());
      info.offset(currentInstruction.getCharOffset());
    } else {
      info.line(0);
      info.offset(0);
    }
    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 {
    // Macros override partials, so if one is defined in the current scope, return it.
    Instruction inst = resolveMacro(name);
    if (inst != null) {
      return inst;
    }

    if (rawPartials == null) {
      // Template wants to use a partial but none are defined.
      return null;
    }

    // See if we've previously compiled this exact partial.
    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, preprocess);
      if (safeExecution) {
        List 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 {
    // 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) {
    partialDepth--;
  }

  /**
   * Lazily allocate the injectable JSON cache.
   */
  public void setInjectables(JsonNode node) {
    this.rawInjectables = node;
    this.parsedInjectables = new HashMap<>();
  }

  /**
   * Retrieve an injectable JSON object by name.
   */
  public JsonNode getInjectable(String name) {
    if (rawInjectables == null) {
      return Constants.MISSING_NODE;
    }

    // Check if we've already parsed and cached this injectable
    JsonNode result = parsedInjectables.get(name);
    if (result == null) {

      // Not cached, so parse the injectable if it exists.
      JsonNode rawNode = rawInjectables.get(name);
      if (rawNode != null) {
        String raw = rawNode.asText();
        result = JsonUtils.decode(raw, true);
      } else {
        result = Constants.MISSING_NODE;
      }

      // Update the mapping even if it is MISSING to save a lookup.
      parsedInjectables.put(name, result);
    }

    return result;
  }


  public StringBuilder buffer() {
    return buf;
  }

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

  public boolean initIteration() {
    JsonNode node = node();
    if (!node.isArray() || node.size() == 0) {
      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 void setMacro(String name, Instruction inst) {
    currentFrame.setMacro(name, inst);
  }

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

  public Instruction resolveMacro(String name) {
    Frame frame = currentFrame;
    while (frame != null) {
      Instruction inst = frame.getMacro(name);
      if (inst != null) {
        return inst;
      }
      frame = frame.parent();
    }
    return null;
  }

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

  /**
   * Lookup the JSON node referenced by the list of names, starting at
   * the given frame. This allows formatters to selectively skip their
   * stack frame when resolving a variable reference.
   */
  public JsonNode resolve(Object[] names, Frame startingFrame) {
    if (names == null) {
      return startingFrame.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.
   * Special variables:
   *
   *   @         current node
   *   @index0   0-based index of the current iteration context
   *   @index    1-based index of the current iteration context
   */
  private JsonNode resolve(Object name, Frame frame) {
    if (name instanceof String) {
      String strName = (String)name;

      if (strName.equals("@")) {
        return frame.node();
      } else if (strName.startsWith("@")) {
        boolean isIndex = name.equals("@index");
        if (isIndex || name.equals("@index0")) {
          if (frame.currentIndex != -1) {
            // @zindex is 0-based, @index is 1-based
            int index = isIndex ? frame.currentIndex + 1 : frame.currentIndex;
            return new IntNode(index);
          }
          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);
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy