Package com.google.sitebricks.compiler

Source Code of com.google.sitebricks.compiler.HtmlTemplateCompiler$PageCompilingContext

package com.google.sitebricks.compiler;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import com.google.sitebricks.Renderable;
import com.google.sitebricks.Template;
import com.google.sitebricks.conversion.generics.Generics;
import com.google.sitebricks.rendering.Strings;
import com.google.sitebricks.rendering.control.Chains;
import com.google.sitebricks.rendering.control.WidgetChain;
import com.google.sitebricks.rendering.control.WidgetRegistry;
import com.google.sitebricks.routing.PageBook;
import com.google.sitebricks.routing.SystemMetrics;
import org.jetbrains.annotations.NotNull;
import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Attributes;
import org.jsoup.nodes.Comment;
import org.jsoup.nodes.DataNode;
import org.jsoup.nodes.Element;
import org.jsoup.nodes.Node;
import org.jsoup.nodes.TextNode;
import org.jsoup.nodes.XmlDeclaration;

import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import static com.google.sitebricks.compiler.AnnotationNode.ANNOTATION;
import static com.google.sitebricks.compiler.AnnotationNode.ANNOTATION_CONTENT;
import static com.google.sitebricks.compiler.AnnotationNode.ANNOTATION_KEY;
import static com.google.sitebricks.compiler.HtmlParser.LINE_NUMBER_ATTRIBUTE;
import static com.google.sitebricks.compiler.HtmlParser.SKIP_ATTR;

/**
* @author Shawn based on XMLTemplateCompiler by Dhanji R. Prasanna (dhanji@gmail.com)
*/
@Singleton
public class HtmlTemplateCompiler implements TemplateCompiler {
   
  private final WidgetRegistry registry;
    private final PageBook pageBook;
    private final SystemMetrics metrics;

    //special widget types (built-in symbol table)
    private static final String REQUIRE_WIDGET = "@require";
    private static final String REPEAT_WIDGET = "repeat";
    private static final String CHOOSE_WIDGET = "choose";

    @Inject
    public HtmlTemplateCompiler(WidgetRegistry registry, PageBook pageBook, SystemMetrics metrics) {
        this.registry = registry;
        this.pageBook = pageBook;
        this.metrics = metrics;
    }
   
    //
    // compiler state
    //
    class PageCompilingContext {
      Class<?> page;
      Template template;
      List<CompileError> errors = Lists.newArrayList();
      List<CompileError> warnings = Lists.newArrayList();
      Element form;
      Stack<EvaluatorCompiler> lexicalScopes = new Stack<EvaluatorCompiler>();     
    }
   
    public Renderable compile(Class<?> page, Template template) {
     
        PageCompilingContext pc = new PageCompilingContext();
        pc.page = page;
        pc.template = template;
        pc.lexicalScopes.push(new MvelEvaluatorCompiler(page));
     
        WidgetChain widgetChain;
        widgetChain = walk(pc, HtmlParser.parse(template.getText()));

        // TODO - get the errors when !(isValid)
        if (!pc.errors.isEmpty() || !pc.warnings.isEmpty()) {
            // If there were any errors we must track them.
            metrics.logErrorsAndWarnings(page, pc.errors, pc.warnings);

            // Only explode if there are errors.
            if (!pc.errors.isEmpty())
                throw new TemplateCompileException(page, template.getText(), pc.errors, pc.warnings);
        }

      return widgetChain;
    }

    private WidgetChain walk(PageCompilingContext pc, List<Node> nodes) {
        WidgetChain chain = Chains.proceeding();

        for (Node n: nodes)
            chain.addWidget(widgetize(pc, n, walk(pc, n)));

        return chain;
    }

    /**
     * Walks the DOM recursively, and converts elements into corresponding sitebricks widgets.
     */
    @NotNull
    private <N extends Node> WidgetChain walk(PageCompilingContext pc, N node) {
        WidgetChain widgetChain = Chains.proceeding();
        for (Node n: node.childNodes()) {
            if (n instanceof Element) {
                final Element child = (Element) n;

                //push form if this is a form tag
                if (child.tagName().equals("form"))
                    pc.form = (Element) n;

                //setup a lexical scope if we're going into a repeat widget (by reading the previous node)
                final boolean shouldPopScope = lexicalClimb(pc, child);

                //continue recursing down, perform a post-order, depth-first traversal of the DOM
                WidgetChain childsChildren;
                try {
                    childsChildren = walk(pc, child);

                    //process the widget itself into a Renderable with child tree
                    widgetChain.addWidget(widgetize(pc, child, childsChildren));
                } finally {
                    lexicalDescend(pc, child, shouldPopScope);
                }

            } else if (n instanceof TextNode) {
              TextNode child = (TextNode)n;
              Renderable textWidget;
             
                //setup a lexical scope if we're going into a repeat widget (by reading the previous node)
                final boolean shouldPopScope = lexicalClimb(pc, child);

                // construct the text widget
                try {
                  textWidget = registry.textWidget(cleanHtml(n), pc.lexicalScopes.peek());
                 
                  // if there are no annotations, add the text widget to the chain
                  if (!child.hasAttr(ANNOTATION_KEY))  {
                    widgetChain.addWidget(textWidget);
                  }
                  else  {
                    // construct a new widget chain for this text node
                    WidgetChain childsChildren = Chains.proceeding().addWidget(textWidget);
                   
                    // make a new widget for the annotation, making the text chain the child
                    String widgetName = child.attr(ANNOTATION_KEY).toLowerCase();
                    Renderable annotationWidget = registry.newWidget(widgetName, child.attr(ANNOTATION_CONTENT), childsChildren, pc.lexicalScopes.peek());
                    widgetChain.addWidget(annotationWidget);
                  }
                 
                } catch (ExpressionCompileException e) {
                    pc.errors.add(
                            CompileError.in(node.outerHtml())
                            .near(line(n))
                            .causedBy(e)
                    );
                }

                if (shouldPopScope)
                  pc.lexicalScopes.pop();
             
            } else if ((n instanceof Comment) || (n instanceof DataNode)) {
                //process as raw text widget
                try {
                    widgetChain.addWidget(registry.textWidget(cleanHtml(n), pc.lexicalScopes.peek()));
                } catch (ExpressionCompileException e) {

                    pc.errors.add(
                            CompileError.in(node.outerHtml())
                            .near(line(node))
                            .causedBy(e)
                    );
                }
            } else if (n instanceof XmlDeclaration) {
                try {
                    widgetChain.addWidget(registry
                        .xmlDirectiveWidget(((XmlDeclaration)n).getWholeDeclaration(),
                        pc.lexicalScopes.peek()));
                } catch (ExpressionCompileException e) {
                    pc.errors.add(
                            CompileError.in(node.outerHtml())
                            .near(line(node))
                            .causedBy(e)
                  );

                }
            }
        }

        //return computed chain, or a terminal
        return widgetChain;
    }


    /**
     * Complement of HtmlTemplateCompiler#lexicalClimb().
     *  This method pops off the stack of lexical scopes when
     *  we're done processing a sitebricks widget.
     */
    private void lexicalDescend(PageCompilingContext pc, Element element, boolean shouldPopScope) {

        //pop form
        if ("form".equals(element.tagName()))
            pc.form = null;

        //pop compiler if the scope ends
        if (shouldPopScope) {
            pc.lexicalScopes.pop();
        }
    }


    /**
     * Called to push a new lexical scope onto the stack.
     */
    private boolean lexicalClimb(PageCompilingContext pc, Node node) {
        if (node.attr(ANNOTATION).length()>1) {

            // Setup a new lexical scope (symbol table changes on each scope encountered).
            if (REPEAT_WIDGET.equalsIgnoreCase(node.attr(ANNOTATION_KEY))
                || CHOOSE_WIDGET.equalsIgnoreCase(node.attr(ANNOTATION_KEY))) {

                String[] keyAndContent = {node.attr(ANNOTATION_KEY), node.attr(ANNOTATION_CONTENT)};
                pc.lexicalScopes.push(new MvelEvaluatorCompiler(parseRepeatScope(pc, keyAndContent, node)));
                return true;
            }

            // Setup a new lexical scope for compiling against embedded pages (closures).
            final PageBook.Page embed = pageBook.forName(node.attr(ANNOTATION_KEY));
            if (null != embed) {
                final Class<?> embedClass = embed.pageClass();
                MvelEvaluatorCompiler compiler = new MvelEvaluatorCompiler(embedClass);
                checkEmbedAgainst(pc, compiler, Parsing.toBindMap(node.attr(ANNOTATION_CONTENT)),
                    embedClass, node);

              pc.lexicalScopes.push(compiler);
              return true;
            }
        }

        return false;
    }

    /**
     * This method converts an XML element into a specific kind of widget.
     * Special cases are the XML widget, Header, @Require widget. Otherwise a standard
     * widget is created.
     */
    @SuppressWarnings({"JavaDoc"}) @NotNull
    private <N extends Node> Renderable widgetize(PageCompilingContext pc, N node, WidgetChain childsChildren) {
        if (node instanceof XmlDeclaration) {
            try {
              XmlDeclaration decl = (XmlDeclaration)node;
              return registry.xmlDirectiveWidget(decl.getWholeDeclaration(), pc.lexicalScopes.peek());
            } catch (ExpressionCompileException e) {
                pc.errors.add(
                        CompileError.in(node.outerHtml())
                        .near(line(node))
                        .causedBy(e)
              );
            }
        }
     
        // Header widget is a special case, where we match by the name of the tag =(
        if ("head".equals(node.nodeName())) {
          try {
            return registry.headWidget(childsChildren, parseAttribs(node.attributes()), pc.lexicalScopes.peek());
          } catch (ExpressionCompileException e) {
            pc.errors.add(
                CompileError.in(node.outerHtml())
                .near(line(node))
                .causedBy(e)
            );

          }
        }

        String annotation = node.attr(ANNOTATION);

        //if there is no annotation, treat as a raw xml-widget (i.e. tag)
        if ((null == annotation) || 0 == annotation.trim().length())
            try {
                checkUriConsistency(pc, node);
                checkFormFields(pc, node);

                return registry.xmlWidget(childsChildren, node.nodeName(), parseAttribs(node.attributes()),
                        pc.lexicalScopes.peek());
            } catch (ExpressionCompileException e) {
                pc.errors.add(
                    CompileError.in(node.outerHtml())
                    .near(line(node))
                    .causedBy(e)
                );

                return Chains.terminal();
            }

        // Special case: is this annotated with @Require
        //   if so, tags in head need to be promoted to head of enclosing page.
        if (REQUIRE_WIDGET.equalsIgnoreCase(annotation.trim()))
            try {
                return registry.requireWidget(registry.xmlWidget(childsChildren, node.nodeName(),
                    parseAttribs(node.attributes()), pc.lexicalScopes.peek()));
            } catch (ExpressionCompileException e) {
                pc.errors.add(
                    CompileError.in(node.outerHtml())
                    .near(line(node))
                    .causedBy(e)
                );

                return Chains.terminal();
            }

        // If this is NOT a self-rendering widget, give it a child.
        // final String widgetName = node.attr(ANNOTATION_KEY).trim().toLowerCase());
        final String widgetName = node.attr(ANNOTATION_KEY).toLowerCase();

        if (!registry.isSelfRendering(widgetName))
            try {
                childsChildren = Chains.singleton(registry.xmlWidget(childsChildren, node.nodeName(),
                        parseAttribs(node.attributes()), pc.lexicalScopes.peek()));
            } catch (ExpressionCompileException e) {
                pc.errors.add(
                    CompileError.in(node.outerHtml())
                    .near(line(node))
                    .causedBy(e)
                );
            }


        // Recursively build widget from [Key, expression, child widgets].
        try {
            return registry.newWidget(widgetName, node.attr(ANNOTATION_CONTENT), childsChildren, pc.lexicalScopes.peek());
        } catch (ExpressionCompileException e) {
            pc.errors.add(
                CompileError.in(node.outerHtml())
                .near(line(node))
                .causedBy(e)
            );

            // This should never be used.
            return Chains.terminal();
        }
    }




    private Map<String,Type> parseRepeatScope(PageCompilingContext pc, String[] extract, Node node) {
        RepeatToken repeat = registry.parseRepeat(extract[1]);
        Map<String, Type> context = Maps.newHashMap();

        // Verify that @Repeat was parsed correctly.
        if (null == repeat.var()) {
            pc.errors.add(
                        CompileError.in(node.outerHtml())
                        .near(node.siblingIndex()) // TODO - line number
                        .causedBy(CompileErrors.MISSING_REPEAT_VAR)
                );
        }
        if (null == repeat.items()) {
            pc.errors.add(
                    CompileError.in(node.outerHtml())
                    .near(node.siblingIndex())  // TODO  - line number
                    .causedBy(CompileErrors.MISSING_REPEAT_ITEMS)
            );
        }

        try {
            Type egressType = pc.lexicalScopes.peek().resolveEgressType(repeat.items());
           
            // convert to collection if we need to
            Type elementType;
            Class<?> egressClass = Generics.erase(egressType);
      if (egressClass.isArray()) {
        elementType = Generics.getArrayComponentType(egressType);
            }
            else if (Collection.class.isAssignableFrom(egressClass)) {
              elementType = Generics.getTypeParameter(egressType, Collection.class.getTypeParameters()[0]);
            }
            else {
              pc.errors.add(
                  CompileError.in(node.outerHtml())
                  .near(node.siblingIndex()) // TODO - line number
                  .causedBy(CompileErrors.REPEAT_OVER_ATOM)
              );
              return Collections.emptyMap();
            }

            context.put(repeat.var(), elementType);
            context.put(repeat.pageVar(), pc.page);
            context.put("__page", pc.page);
            context.put("index", int.class);
            context.put("isLast", boolean.class);

        } catch (ExpressionCompileException e) {
                pc.errors.add(
                    CompileError.in(node.outerHtml())
                    .near(node.siblingIndex()) // TODO - line number
                    .causedBy(e)
                );
        }

        return context;
    }




    private void checkFormFields(PageCompilingContext pc, Node element) {
        if (null == pc.form)
            return;

        String action = pc.form.attr("action");

        // Only look at contextual uris (i.e. hosted by us).
        // TODO - relative, not starting with '/'
        if (null == action || (!action.startsWith("/")))
            return;

        final PageBook.Page page = pageBook.get(action);

        // Only look at pages we actually have registered.
        if (null == page) {
            pc.warnings.add(
                CompileError.in(element.outerHtml())
                .near(line(element))
                .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION)
            );

            return;
        }

        // If we're inside a form do a throw-away compile against the target page.
        if ("input".equals(element.nodeName()) || "textarea".equals(element.nodeName())) {
            String name = element.attr("name");

            // Skip submits and buttons.
            if (skippable(element.attr("type")))
                return;

            //TODO Skip empty?
            if (null == name) {
                pc.warnings.add(
                        CompileError.in(element.outerHtml())
                        .near(line(element))
                        .causedBy(CompileErrors.FORM_MISSING_NAME)
                );

                return;
            }

            // Compile expression path.
          try {
                new MvelEvaluatorCompiler(page.pageClass())
                        .compile(name);

            } catch (ExpressionCompileException e) {
                //TODO Very hacky, needed to strip out xmlns attribution.
                pc.warnings.add(
                    CompileError.in(element.outerHtml())
                    .near(element.siblingIndex()) // TODO - line number
                    .causedBy(CompileErrors.UNRESOLVABLE_FORM_BINDING, e)
                );
            }

        }

    }

    private void checkUriConsistency(PageCompilingContext pc, Node element) {
        String uriAttrib = element.attr("action");
        if (null == uriAttrib)
            uriAttrib = element.attr("src");
        if (null == uriAttrib)
            uriAttrib = element.attr("href");

        if (null != uriAttrib) {

            // Verify that such a uri exists in the page book,
            // only if it is contextual--ignore abs & relative URIs.
          if (uriAttrib.startsWith("/"))
                if (null == pageBook.nonCompilingGet(uriAttrib))
                    pc.warnings.add(
                        CompileError.in(element.outerHtml())
                        .near(element.siblingIndex()) // TODO - line number
                        .causedBy(CompileErrors.UNRESOLVABLE_FORM_ACTION, uriAttrib)
                );
        }
    }




  /**
   * @param attributes A list of attribs
   * @return Returns a mutable map parsed out of the attribute list
   */
  public static Map<String, String> parseAttribs(Attributes attributes) {

      Map<String, String> attrs = new LinkedHashMap<String, String>(attributes.size() + 4);

      for (Attribute a : attributes.asList())
          if (!SKIP_ATTR.contains(a.getKey()))
              attrs.put(a.getKey(), a.getValue());

      return attrs;
  }

  // Ensures that embed bound properties are writable
  private void checkEmbedAgainst(PageCompilingContext pc, EvaluatorCompiler compiler, Map<String, String> properties, Class<?> embedClass, Node node) {

    // TODO also type check them against expressions
    for (String property : properties.keySet()) {
        try {
            if (!compiler.isWritable(property)) {
                pc.errors.add(
                    CompileError.in(node.outerHtml())
                      //TODO we need better line number detection if there is whitespace between the annotation and tag.
                      .near(node.siblingIndex()-1) // TODO -  line number of the annotation
                      .causedBy(CompileErrors.PROPERTY_NOT_WRITEABLE,
                          String.format("Property %s#%s was not writable. Did you forget to create "
                              + "a setter or @Visible annotation?", embedClass.getSimpleName(), property))
                );
            }
        } catch (ExpressionCompileException ece) {
            pc.errors.add(
                CompileError.in(node.outerHtml())
                    .near(node.siblingIndex()) // TODO - line number
                    .causedBy(CompileErrors.ERROR_COMPILING_PROPERTY)
            );
        }
     }
  }


  static boolean skippable(String kind) {
    return null != kind && ("submit".equals(kind)
            || "button".equals(kind)
            || "reset".equals(kind)
            || "file".equals(kind));
  }





  // TESTING jsoup.nodes.Node

  public List<Node> findSiblings(Node node) {
      Preconditions.checkNotNull(node);
   
      Node parent = node.parent();
      if (null == parent) return null;

      return parent.childNodes();              
  }

  protected static <N extends Node> Integer indexInList(N search, List<N> nodes) {
        Preconditions.checkNotNull(search);
        Preconditions.checkNotNull(nodes);

        for (int i = 0; i < nodes.size(); i++) {
            N node = nodes.get(i);
            if (node.equals(search))
                return i;
        }
        return null;
    }

    private static int line(Node node) {
        return Integer.valueOf(node.attr(LINE_NUMBER_ATTRIBUTE));
    }

    // outerHtml from jsoup.Node, Element with suppressed _attribs

    private static String cleanHtml(final Node node) {
        if (node instanceof Element) {
            Element element = ((Element) node);
            StringBuilder accum = new StringBuilder();
            accum.append("<").append(element.tagName());
            for (Attribute attribute: element.attributes()) {
                if (!(attribute.getKey().startsWith("_"))) {
                    accum.append(" ");
                    accum.append(attribute.getKey());
                    accum.append("=\"");
                    accum.append(attribute.getValue());
                    accum.append('"');
                }
            }

            if (element.childNodes().isEmpty() && element.tag().isEmpty()) {
                accum.append(" />");
            } else {
                accum.append(">");
                for (Node child : element.childNodes())
                    accum.append(cleanHtml(child));

                accum.append("</").append(element.tagName()).append(">");
            }
            return accum.toString();
        } else if (node instanceof TextNode) {
            return ((TextNode) node).getWholeText();
        } else if (node instanceof XmlDeclaration) {

          // HACK
          if (node.childNodes().isEmpty()) {
            return "";
          }
            return node.outerHtml();
        } else if (node instanceof Comment) {
          // HACK: elide comments for now.
          return "";
        } else if (node instanceof DataNode && node.childNodes().isEmpty()) {
          // No child nodes are defined but we have to handle content if such exists, example
            // <script language="JavaScript">var a =  { name: "${user.name}"}</script> 

            String content = node.attr("data");
            if (Strings.empty(content)) {
                return "";
            }

            return content;
        } else {
            return node.outerHtml();
        }
    }
}
TOP

Related Classes of com.google.sitebricks.compiler.HtmlTemplateCompiler$PageCompilingContext

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.