Package org.apache.shindig.gadgets.templates

Source Code of org.apache.shindig.gadgets.templates.DefaultTemplateProcessor

/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements.  See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership.  The ASF licenses this file
* to you 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.apache.shindig.gadgets.templates;

import org.apache.shindig.expressions.Expressions;
import org.apache.shindig.gadgets.GadgetELResolver;
import org.apache.shindig.gadgets.parse.HtmlSerialization;
import org.apache.shindig.gadgets.templates.tags.RepeatTagHandler;
import org.apache.shindig.gadgets.templates.tags.TagHandler;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.el.ELContext;
import javax.el.ELException;
import javax.el.ELResolver;
import javax.el.ValueExpression;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;

/**
* Implements a DOM-based OS templates compiler.
* Supports:
*   - ${...} style expressions in content and attributes
*   - @if attribute
*   - @repeat attribute
* TODO:
*   - Handle built-in/custom tags
*/
public class DefaultTemplateProcessor implements TemplateProcessor {
 
  private static final Logger logger = Logger.getLogger(DefaultTemplateProcessor.class.getName());
 
  public static final String PROPERTY_INDEX = "Index";
  public static final String PROPERTY_COUNT = "Count";
 
  public static final String ATTRIBUTE_IF = "if";
  public static final String ATTRIBUTE_INDEX = "index";
  public static final String ATTRIBUTE_REPEAT = "repeat";
  public static final String ATTRIBUTE_VAR = "var";
  public static final String ATTRIBUTE_CUR = "cur";
 
  /**
   * Set of attributes in HTML 4 that are boolean, and may only be set
   * to that value, and should be omitted to indicate "false".
   */
  private static final Set<String> HTML4_BOOLEAN_ATTRIBUTES =
    ImmutableSet.of("checked", "compact", "declare", "defer", "disabled", "ismap",
        "multiple", "nohref", "noresize", "noshade", "nowrap", "readonly", "selected");
 
  private static final Set<String> ONCREATE_ATTRIBUTES =
    ImmutableSet.of("oncreate", "x-oncreate");
 
  private final Expressions expressions;
  // Reused buffer for creating template output
  private final StringBuilder outputBuffer;

  private TagRegistry registry;
  private TemplateContext templateContext;
  private ELContext elContext;
 
  private int uniqueIdCounter = 0;
 
  @Inject
  public DefaultTemplateProcessor(Expressions expressions) { 
    this.expressions = expressions;
    outputBuffer = new StringBuilder();
  }

  /**
   * Process an entire template.
   *
   * @param template the DOM template, typically a script element
   * @param templateContext a template context providing top-level
   *     variables
   * @param globals ELResolver providing global variables other
   *     than those in the templateContext
   * @return a document fragment with the resolved content
   */
  public DocumentFragment processTemplate(Element template,
      TemplateContext templateContext, ELResolver globals, TagRegistry registry) {

    this.registry = registry;
    this.templateContext = templateContext;
    this.elContext = expressions.newELContext(globals,
        new GadgetELResolver(templateContext.getGadget().getContext()),
        new TemplateELResolver(templateContext),
        new ElementELResolver());

    DocumentFragment result = template.getOwnerDocument().createDocumentFragment();
    processChildNodes(result, template);
    return result;
  }
 
  /** Process the children of an element or document. */
  public void processChildNodes(Node result, Node source) {
    NodeList nodes = source.getChildNodes();
    for (int i = 0; i < nodes.getLength(); i++) {
      processNode(result, nodes.item(i));
    }
  }
 
  public TemplateContext getTemplateContext() {
    return templateContext;
  }
 
  /**
   * Process a node.
   *
   * @param result the target node where results should be inserted
   * @param source the source node of the template being processed
   */
  private void processNode(Node result, Node source) {
    switch (source.getNodeType()) {
      case Node.TEXT_NODE:
        processText(result, source.getTextContent());
        break;
      case Node.ELEMENT_NODE:
        processElement(result, (Element) source);
        break;
      case Node.DOCUMENT_NODE:
        processChildNodes(result, source);
        break;
    }
  }

  /**
   * Process text content by including non-expression content verbatim and
   * escaping expression content.

   * @param result the target node where results should be inserted
   * @param textContent the text content being processed
   */
  private void processText(Node result, String textContent) {
    Document ownerDocument = result.getOwnerDocument();
   
    int start = 0;
    int current = 0;
    while (current < textContent.length()) {
      current = textContent.indexOf("${", current);
      // No expressions, we're done
      if (current < 0) {
        break;
      }
     
      // An escaped expression "\${"
      if (current > 0 && textContent.charAt(current - 1) == '\\') {
        // Drop the \ by outputting everything before it, and moving past
        // the ${
        if (current - 1 > start) {
          String staticText = textContent.substring(start, current - 1);
          result.appendChild(ownerDocument.createTextNode(staticText));
        }
       
        start = current;
        current = current + 2;
        continue;
      }
     
      // Not a real expression, we're done
      int expressionEnd = textContent.indexOf('}', current + 2);
      if (expressionEnd < 0) {
        break;
      }
 
      // Append the existing static text, if any
      if (current > start) {
        result.appendChild(ownerDocument.createTextNode(textContent.substring(start, current)));
      }
     
      // Isolate the expression, parse and evaluate
      String expression = textContent.substring(current, expressionEnd + 1);
      String value = evaluate(expression, String.class, "");
     
      if (!"".equals(value)) {
        // And now escape
        outputBuffer.setLength(0);
        try {
          HtmlSerialization.printEscapedText(value, outputBuffer);
        } catch (IOException e) {
          // Can't happen writing to StringBuilder
          throw new RuntimeException(e);
        }
       
        result.appendChild(ownerDocument.createTextNode(outputBuffer.toString()));
      }
     
      // And continue with the next expression
      current = start = expressionEnd + 1;
    }
   
    // Add any static text left over
    if (start < textContent.length()) {
      result.appendChild(ownerDocument.createTextNode(textContent.substring(start)));
    }
  }

  /**
   * Process repeater state, if needed, on an element.
   */
  private void processElement(final Node result, final Element element) {
    Attr repeat = element.getAttributeNode(ATTRIBUTE_REPEAT);
    if (repeat != null) {
      Iterable<?> dataList = evaluate(repeat.getValue(), Iterable.class, null);
      processRepeat(result, element, dataList, new Runnable() {
        public void run() {
          processElementInner(result, element);
        }
      });
    } else {
      processElementInner(result, element);
    }
  }

  /**
   * @param result
   * @param element
   * @param dataList
   */
  public void processRepeat(Node result, Element element, Iterable<?> dataList,
      Runnable onEachLoop) {
    if (dataList == null) {
      return;
    }
   
    // Compute list size
    int size = Iterables.size(dataList);
   
    if (size > 0) {
      // Save the initial EL state
      Map<String, ? extends Object> oldContext = templateContext.getContext();
      Object oldCur = templateContext.getCur();
      ValueExpression oldVarExpression = null;
     
      // Set the new Context variable.  Copy the old context to preserve
      // any existing "index" variable
      Map<String, Object> loopData = Maps.newHashMap(oldContext);
      loopData.put(PROPERTY_COUNT, size);
      templateContext.setContext(loopData);

      // TODO: This means that any loop with @var doesn't make the loop
      // variable available in the default expression context.
      // Update the specification to make this explicit.
      Attr varAttr = element.getAttributeNode(ATTRIBUTE_VAR);
      if (varAttr == null) {
        oldCur = templateContext.getCur();
      } else {
        oldVarExpression = elContext.getVariableMapper().resolveVariable(varAttr.getValue());
      }

      Attr indexVarAttr = element.getAttributeNode(ATTRIBUTE_INDEX);
      String indexVar = indexVarAttr == null ? PROPERTY_INDEX : indexVarAttr.getValue();
       
      int index = 0;
      for (Object data : dataList) {
        loopData.put(indexVar, index++);
       
        // Set up context for rendering inner node
        templateContext.setCur(data);
        if (varAttr != null) {
          ValueExpression varExpression = expressions.constant(data, Object.class);
          elContext.getVariableMapper().setVariable(varAttr.getValue(), varExpression);
        }
       
        onEachLoop.run();

      }
     
      // Restore EL state       
      if (varAttr == null) {
        templateContext.setCur(oldCur);
      } else {
        elContext.getVariableMapper().setVariable(varAttr.getValue(), oldVarExpression);
      }
     
      templateContext.setContext(oldContext);
    }
  }
 
  /**
   * Process conditionals and non-repeat attributes on an element
   */
  private void processElementInner(Node result, Element element) {
    TagHandler handler = registry.getHandlerFor(element);
   
    // An ugly special-case:  <os:Repeat> will re-evaluate the "if" attribute
    // (as it should) for each loop of the repeat.  Don't evaluate it here.
    if (!(handler instanceof RepeatTagHandler)) {
      Attr ifAttribute = element.getAttributeNode(ATTRIBUTE_IF);
      if (ifAttribute != null) {
        if (!evaluate(ifAttribute.getValue(), Boolean.class, false)) {
          return;
        }
      }
    }

    // TODO: the spec is silent on order of evaluation of "cur" relative
    // to "if" and "repeat"
    Attr curAttribute = element.getAttributeNode(ATTRIBUTE_CUR);
    Object oldCur = templateContext.getCur();
    if (curAttribute != null) {
      templateContext.setCur(evaluate(curAttribute.getValue(), Object.class, null));
    }
   
    if (handler != null) {
      handler.process(result, element, this);
    } else {
      // Be careful cloning nodes! If a target node belongs to a different document than the
      // template node then use importNode rather than cloneNode as that avoids side-effects
      // in UserDataHandlers where the cloned template node would belong to its original
      // document before being adopted by the target document.
      Element resultNode;
      if (element.getOwnerDocument() != result.getOwnerDocument()) {
        resultNode = (Element)result.getOwnerDocument().importNode(element, false);
      } else {
        resultNode = (Element)element.cloneNode(false);
      }
     
      clearSpecialAttributes(resultNode);
      Node additionalNode = processAttributes(resultNode);
     
      processChildNodes(resultNode, element);
      result.appendChild(resultNode);     
     
      if (additionalNode != null) {
        result.appendChild(additionalNode);
      }
    }
   
    if (curAttribute != null) {
      templateContext.setCur(oldCur);
    }
  }

  private void clearSpecialAttributes(Element element) {
    element.removeAttribute(ATTRIBUTE_IF);
    element.removeAttribute(ATTRIBUTE_REPEAT);
    element.removeAttribute(ATTRIBUTE_INDEX);
    element.removeAttribute(ATTRIBUTE_VAR);
    element.removeAttribute(ATTRIBUTE_CUR);
  }
 
  /**
   * Process expressions on attributes.
   * @param element The Element to process attributes on
   * @return Node to attach after this Element, or null
   */
  private Node processAttributes(Element element) {
    NamedNodeMap attributes = element.getAttributes();
    Node additionalNode = null;
   
    // Mutations to perform after iterating (if needed)
    List<Attr> attrsToRemove = null;
    String newId = null;
   
    for (int i = 0; i < attributes.getLength(); i++) {
      boolean removeThisAttribute = false;
     
      Attr attribute = (Attr) attributes.item(i);
      // Boolean attributes: evaluate as a boolean.  If true, set the value to the
      // name of the attribute, e.g. selected="selected".  If false, remove the attribute
      // altogether.  The check here has some limitations for efficiency:  it assumes the
      // attribute is lowercase, and doesn't bother to check whether the boolean attribute
      // actually exists on the referred element (but HTML has no attrs that are sometimes
      // boolean and sometimes not)
      if (element.getNamespaceURI() == null &&
          HTML4_BOOLEAN_ATTRIBUTES.contains(attribute.getName())) {
        if (Boolean.TRUE.equals(evaluate(attribute.getValue(), Boolean.class, Boolean.FALSE))) {
          attribute.setNodeValue(attribute.getName());
        } else {
          removeThisAttribute = true;
        }
      } else if (ONCREATE_ATTRIBUTES.contains(attribute.getName())) {
        String id = element.getAttribute("id");
        if (id.length() == 0) {
          newId = id = getUniqueId();
        }
       
        additionalNode = buildOnCreateScript(
            evaluate(attribute.getValue(), String.class, null), id, element.getOwnerDocument());
        removeThisAttribute = true;
      } else {     
        attribute.setNodeValue(evaluate(attribute.getValue(), String.class, null));
      }
     
      // Because NamedNodeMaps are live, removing them interferes with iteration.
      // Remove the attributes in a later pass
      if (removeThisAttribute) {
        if (attrsToRemove == null) {
          attrsToRemove = Lists.newArrayListWithCapacity(attributes.getLength());
        }
       
        attrsToRemove.add(attribute);
      }
    }
   
    // Now that iteration is complete, perform mutations
    if (attrsToRemove != null) {
      for (Attr attr : attrsToRemove) {
        element.removeAttributeNode(attr);
      }
    }
   
    if (newId != null) {
      element.setAttribute("id", newId);
    }
   
    return additionalNode;
  }
 
  /**
   * Inserts an inline script element that executes a snippet of Javascript
   * code after the element is emitted.
   * <p>
   * The approach used involves using Javascript to find the previous sibling
   * node and apply the code to it - this avoids decorating nodes with IDs, an
   * approach that could potentially clash with existing element IDs that could
   * be non-unique.
   * <p>
   * The resulting script element is subject to sanitization.
   * <p>
   * @param code Javascript code to execute
   * @param id Element ID which should be used
   * @param document document for creating elements
   *
   * TODO: Move boilerplate code for finding the right node out to a function
   * to reduce code size.
   */
  private Node buildOnCreateScript(String code, String id, Document document) {
    Element script = document.createElement("script");
    script.setAttribute("type", "text/javascript");
    StringBuilder builder = new StringBuilder();
    builder.append("(function(){");
    builder.append(code);
    builder.append("}).apply(document.getElementById('");
    builder.append(id);
    builder.append("'));");
    script.setTextContent(builder.toString());
    return script;
  }
 
  /**
   *  Evaluates an expression within the scope of this processor's context.
   *  @param expression The String expression
   *  @param type Expected result type
   *  @param defaultValue Default value to return in case of error
   */
  public <T> T evaluate(String expression, Class<T> type, T defaultValue) {
    try {
      ValueExpression expr = expressions.parse(expression, type);
      Object result = expr.getValue(elContext);
      return type.cast(result);
    } catch (ELException e) {
      logger.log(Level.WARNING, "EL failure for gadget {0}: {1}",
          new Object[]{getTemplateContext().getGadget().getContext().getUrl(),
              e.getMessage()});
      return defaultValue;
    }
  }

  private String getUniqueId() {
    return "ostid" + (uniqueIdCounter++);
  }
}
TOP

Related Classes of org.apache.shindig.gadgets.templates.DefaultTemplateProcessor

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.