Package com.google.gxp.compiler.reparent

Source Code of com.google.gxp.compiler.reparent.Reparenter$ElementVisitor

/*
* Copyright (C) 2008 Google 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.google.gxp.compiler.reparent;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gxp.compiler.alerts.AlertSetBuilder;
import com.google.gxp.compiler.alerts.AlertSink;
import com.google.gxp.compiler.alerts.common.ContentTypeExpectedAlert;
import com.google.gxp.compiler.alerts.common.InvalidAttributeValueError;
import com.google.gxp.compiler.alerts.common.MissingAttributeError;
import com.google.gxp.compiler.alerts.common.UnknownAttributeError;
import com.google.gxp.compiler.base.AbbrExpression;
import com.google.gxp.compiler.base.BooleanConstant;
import com.google.gxp.compiler.base.BooleanType;
import com.google.gxp.compiler.base.BundleType;
import com.google.gxp.compiler.base.ClassImport;
import com.google.gxp.compiler.base.CollapseExpression;
import com.google.gxp.compiler.base.Conditional;
import com.google.gxp.compiler.base.Constructor;
import com.google.gxp.compiler.base.ContentType;
import com.google.gxp.compiler.base.ConvertibleToContent;
import com.google.gxp.compiler.base.CppFileImport;
import com.google.gxp.compiler.base.CppLibraryImport;
import com.google.gxp.compiler.base.Expression;
import com.google.gxp.compiler.base.FormalParameter;
import com.google.gxp.compiler.base.FormalTypeParameter;
import com.google.gxp.compiler.base.Implementable;
import com.google.gxp.compiler.base.ImplementsDeclaration;
import com.google.gxp.compiler.base.Import;
import com.google.gxp.compiler.base.Interface;
import com.google.gxp.compiler.base.JavaAnnotation;
import com.google.gxp.compiler.base.LoopExpression;
import com.google.gxp.compiler.base.MultiLanguageAttrValue;
import com.google.gxp.compiler.base.NativeExpression;
import com.google.gxp.compiler.base.NativeImplementsDeclaration;
import com.google.gxp.compiler.base.NativeType;
import com.google.gxp.compiler.base.Node;
import com.google.gxp.compiler.base.NoMessage;
import com.google.gxp.compiler.base.NullRoot;
import com.google.gxp.compiler.base.ObjectConstant;
import com.google.gxp.compiler.base.OutputElement;
import com.google.gxp.compiler.base.PackageImport;
import com.google.gxp.compiler.base.Parameter;
import com.google.gxp.compiler.base.PlaceholderEnd;
import com.google.gxp.compiler.base.PlaceholderStart;
import com.google.gxp.compiler.base.Root;
import com.google.gxp.compiler.base.SpaceOperator;
import com.google.gxp.compiler.base.SpaceOperatorSet;
import com.google.gxp.compiler.base.StringConstant;
import com.google.gxp.compiler.base.Template;
import com.google.gxp.compiler.base.TemplateName;
import com.google.gxp.compiler.base.TemplateType;
import com.google.gxp.compiler.base.ThrowsDeclaration;
import com.google.gxp.compiler.base.Type;
import com.google.gxp.compiler.base.UnboundCall;
import com.google.gxp.compiler.base.UnboundImplementsDeclaration;
import com.google.gxp.compiler.base.UnexpectedNodeException;
import com.google.gxp.compiler.base.UnextractedMessage;
import com.google.gxp.compiler.ifexpand.IfExpandedTree;
import com.google.gxp.compiler.parser.CallNamespace;
import com.google.gxp.compiler.parser.CppNamespace;
import com.google.gxp.compiler.parser.DefaultingParsedElementVisitor;
import com.google.gxp.compiler.parser.ExprNamespace;
import com.google.gxp.compiler.parser.GxpNamespace;
import com.google.gxp.compiler.parser.JavaNamespace;
import com.google.gxp.compiler.parser.JavaScriptNamespace;
import com.google.gxp.compiler.parser.MsgNamespace;
import com.google.gxp.compiler.parser.Namespace;
import com.google.gxp.compiler.parser.NamespaceVisitor;
import com.google.gxp.compiler.parser.NoMsgNamespace;
import com.google.gxp.compiler.parser.NullElement;
import com.google.gxp.compiler.parser.NullNamespace;
import com.google.gxp.compiler.parser.OutputLanguageNamespace;
import com.google.gxp.compiler.parser.OutputNamespace;
import com.google.gxp.compiler.parser.ParsedAttribute;
import com.google.gxp.compiler.parser.ParsedElement;
import com.google.gxp.compiler.parser.ParsedElementVisitor;
import com.google.gxp.compiler.parser.TextElement;
import com.google.gxp.compiler.schema.AttributeValidator;
import com.google.gxp.compiler.schema.DocType;
import com.google.gxp.compiler.schema.ElementValidator;
import com.google.gxp.compiler.schema.Schema;
import com.google.gxp.compiler.schema.SchemaFactory;

import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/**
* Moves <code>gxp:attr</code>, <code>gxp:param</code>, <code>gxp:import</code>
* and <code>gxp:throws</code> into their parent elements.  Also converts
* attributes into some type of "attribute node" that shares a superclass with
* whatever <code>gxp:attr</code>s turn into, at least for elements that can
* contain <code>gxp:attr</code>s.
*/
public class Reparenter implements Function<IfExpandedTree, ReparentedTree> {
  private static final String DEFAULT_CONTENT_TYPE = "text/html";

  private final SchemaFactory schemaFactory;
  private final String className;
  private Schema rootSchema = null;

  public Reparenter(SchemaFactory schemaFactory, String className) {
    this.schemaFactory = Preconditions.checkNotNull(schemaFactory);
    this.className = Preconditions.checkNotNull(className);
  }

  public ReparentedTree apply(IfExpandedTree parseTree) {
    AlertSetBuilder alertSetBuilder = new AlertSetBuilder(parseTree.getAlerts());

    // make sure root element is a template
    List<ParsedElement> children = parseTree.getChildren();
    if (!children.isEmpty()) {
      ParsedElement first = children.get(0);
      if (!first.canBeRoot()) {
        alertSetBuilder.add(new InvalidRootError(first));
      } else {
        // determine the schema of this gxp
        String contentType = DEFAULT_CONTENT_TYPE;
        for (ParsedAttribute attr : first.getAttributes()) {
          if (attr.getName().equals("content-type")
              && attr.getNamespace() == NullNamespace.INSTANCE) {
            contentType = attr.getValue();
          }
        }
        rootSchema = schemaFactory.fromContentTypeName(contentType);
      }
    }

    Iterable<ParsedAttribute> noAttrs = Collections.emptyList();
    Parts parts = groupParts(alertSetBuilder, parseTree, noAttrs, children);

    // Throw out all but the first Root. The parse phase should have
    // already complained about there being too many roots.
    List<Root> roots = parts.getRoots();
    Root root = roots.isEmpty()
        ? new NullRoot(parseTree, TemplateName.parseFullyQualifiedDottedName(className))
        : roots.get(0);
    return new ReparentedTree(parseTree.getSourcePosition(),
                              alertSetBuilder.buildAndClear(),
                              root);
  }

  /**
   * Converts a {@code ParsedAttribute} into an {@code Attribute}.
   */
  private static Attribute convertAttribute(final AlertSink alertSink,
                                            final ParsedAttribute parsedAttr) {
    Namespace namespace = parsedAttr.getNamespace();
    return namespace.acceptVisitor(
        new NamespaceVisitor<Attribute>() {
          private Attribute defaultVisitNamespace(Namespace ns) {
            return new Attribute(parsedAttr, ns, parsedAttr.getName(),
                                 new StringConstant(parsedAttr, null, parsedAttr.getValue()));
          }

          public Attribute visitCallNamespace(CallNamespace ns) {
            throw new Error("TODO(laurence): implement");
          }

          public Attribute visitCppNamespace(CppNamespace ns) {
            return defaultVisitNamespace(ns);
          }

          public Attribute visitExprNamespace(ExprNamespace ns) {
            MultiLanguageAttrValue value = new MultiLanguageAttrValue(parsedAttr.getValue());
            return new Attribute(parsedAttr, NullNamespace.INSTANCE,
                                 parsedAttr.getName(),
                                 new NativeExpression(parsedAttr, value));
          }

          public Attribute visitGxpNamespace(GxpNamespace ns) {
            return defaultVisitNamespace(ns);
          }

          public Attribute visitJavaNamespace(JavaNamespace ns) {
            return defaultVisitNamespace(ns);
          }

          public Attribute visitJavaScriptNamespace(JavaScriptNamespace ns) {
            return defaultVisitNamespace(ns);
          }

          public Attribute visitMsgNamespace(MsgNamespace ns) {
            Expression str = new StringConstant(parsedAttr, null, parsedAttr.getValue());
            Expression msg = new UnextractedMessage(
                parsedAttr, null, new MultiLanguageAttrValue(""), null, null, false, str);
            return new Attribute(parsedAttr, NullNamespace.INSTANCE, parsedAttr.getName(),
                                 new ConvertibleToContent(msg));
          }

          public Attribute visitNoMsgNamespace(NoMsgNamespace ns) {
            Expression str = new StringConstant(parsedAttr, null, parsedAttr.getValue());
            Expression nomsg = new NoMessage(parsedAttr.getSourcePosition(),
                                             String.format("%s namespace on %s attribute",
                                                           ns.getUri(), parsedAttr.getName()),
                                             str);
            return new Attribute(parsedAttr, NullNamespace.INSTANCE, parsedAttr.getName(),
                                 new ConvertibleToContent(nomsg));
          }

          public Attribute visitNullNamespace(NullNamespace ns) {
            return defaultVisitNamespace(ns);
          }

          public Attribute visitOutputNamespace(OutputNamespace ns) {
            throw new Error("TODO(laurence): implement");
          }
        });
  }

  /**
   * Converts a list of {@code ParsedAttribute}s and a list of {@code
   * ParsedNode}s into a {@code Parts} structure. It does this by converting
   * each element that it comes across into a higher-level type (Attribute,
   * Import, etc.) and then placing each converted element into the appropriate
   * part bucket.
   */
  private Parts groupParts(final AlertSink alertSink,
                           final Node forNode,
                           Iterable<ParsedAttribute> parsedAttrs,
                           Iterable<ParsedElement> children) {
    final EditableParts result = new EditableParts(alertSink, forNode);
    ParsedElementVisitor<Void> childVisitor = new DefaultingParsedElementVisitor<Void>() {
      public Void defaultVisitElement(ParsedElement element) {
        Parts nodeParts = groupParts(alertSink,
                                     element,
                                     element.getAttributes(),
                                     element.getChildren());
        element.acceptVisitor(new ElementVisitor(result, nodeParts, alertSink));
        nodeParts.reportUnused();
        return null;
      }
    };
    for (ParsedAttribute parsedAttr : parsedAttrs) {
      result.accumulate(convertAttribute(alertSink, parsedAttr));
    }
    for (ParsedElement child : children) {
      child.acceptVisitor(childVisitor);
    }
    return result;
  }


  private class ElementVisitor implements ParsedElementVisitor<Void> {
    private final EditableParts output;
    private final Parts nodeParts;
    private final AlertSink alertSink;

    /**
     * @param output where we should place the thing that we're building.
     * @param nodeParts the parts of the node we're visiting, which go into the
     * what we're building.
     */
    ElementVisitor(EditableParts output, Parts nodeParts, AlertSink alertSink) {
      this.output = Preconditions.checkNotNull(output);
      this.nodeParts = Preconditions.checkNotNull(nodeParts);
      this.alertSink = Preconditions.checkNotNull(alertSink);
    }

    private SpaceOperator parseSpaceOperator(AttributeMap attrMap,
                                             Namespace ns,
                                             String name) {
      String value = attrMap.getOptional(ns, name, null);
      if (value != null) {
        SpaceOperator result =
            SpaceOperator.valueOf(value.trim().toUpperCase());
        if (result == null) {
          alertSink.add(
              new InvalidAttributeValueError(attrMap.getAttribute(ns, name)));
        }
        return result;
      } else {
        return null;
      }
    }

    private SpaceOperatorSet getSpaceOperators(AttributeMap attrMap) {
      SpaceOperator interiorSpaceOperator = parseSpaceOperator(
          attrMap, GxpNamespace.INSTANCE, "ispace");
      SpaceOperator exteriorSpaceOperator = parseSpaceOperator(
          attrMap, GxpNamespace.INSTANCE, "espace");
      return new SpaceOperatorSet(interiorSpaceOperator, exteriorSpaceOperator);
    }

    /**
     * @param defaultElement the default {@code JavaAnnotation.Element} to use for
     * {@code JavaAnnotation}s that don't already have a
     * {@code JavaAnnotation.Element}.
     * @param rest other {@JavaAnnotation.Element}s that are allowed in this
     * context.
     */
    private List<JavaAnnotation> getJavaAnnotations(Node parent,
                                                    JavaAnnotation.Element defaultElement,
                                                    JavaAnnotation.Element... rest) {
      List<JavaAnnotation.Element> allowedElements = Lists.newArrayList(rest);
      allowedElements.add(defaultElement);

      List<JavaAnnotation> result = Lists.newArrayList();
      for (JavaAnnotation annotation : nodeParts.getJavaAnnotations()) {
        if (annotation.getElement() == null) {
          result.add(annotation.withElement(defaultElement));
        } else if (allowedElements.contains(annotation.getElement())) {
          result.add(annotation);
        } else {
          alertSink.add(new MisplacedJavaAnnotationError(annotation));
        }
      }

      // Check for java:annotate attribute
      AttributeMap attrMap = nodeParts.getAttributes();
      Attribute annotateAttr = attrMap.getAttribute(JavaNamespace.INSTANCE, "annotate");
      if (annotateAttr != null) {
        String annotateStr = annotateAttr.getValue().getStaticString(alertSink, "");
        result.add(new JavaAnnotation(annotateAttr, defaultElement, annotateStr));
      }

      return result;
    }

    private Expression getCollapsableContent(AttributeMap attrMap) {
      Expression content = nodeParts.getContent();
      return CollapseExpression.create(content, getSpaceOperators(attrMap));
    }

    public Void visitAttrElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String name = attrMap.get("name", null);
      if (name != null) {
        // TODO(laurence): make it possible to have attr elements for other
        // namespaces?
        Expression content =
            new ConvertibleToContent(getCollapsableContent(attrMap));
        Expression condition = attrMap.getOptionalExprValue("cond", null);
        output.accumulate(new Attribute(node, name, content, condition));
      }
      return null;
    }

    public Void visitEvalElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      MultiLanguageAttrValue expr = attrMap.getMultiLanguageAttrValue("expr", true);
      String example = attrMap.getOptional("example", null);
      String phName = attrMap.getOptional(GxpNamespace.INSTANCE, "ph", null);
      if (expr.isEmpty()) {
        alertSink.add(new MissingAttributeError(node, "expr"));
      } else {
        output.accumulate(new NativeExpression(node, expr, example, phName));
      }
      return null;
    }

    public Void visitCondElement(GxpNamespace.GxpElement node) {
      List<Conditional.Clause> clauses = nodeParts.getClauses();
      Expression elseExpression = null;

      // add MissingAttributeError if cond isn't supplied for a clause
      // that isn't the last (or if it's the only) clause
      //
      // TODO(harryh): stricly speaking a single Clause shouldn't have to have a
      // clause, but we need to add an alert in the case of a <gxp:if> without
      // a cond and it's tricky to do that elsewhere
      Iterator<Conditional.Clause> iter = clauses.iterator();
      while (iter.hasNext()) {
        Conditional.Clause clause = iter.next();
        if (clause.getPredicate().alwaysEquals(true)
            && (clauses.size() == 1 || iter.hasNext() == true)) {
          alertSink.add(new MissingAttributeError(clause, "cond"));
        }
      }

      if (!clauses.isEmpty()) {
        Conditional.Clause lastClause = clauses.get(clauses.size() - 1);
        if (lastClause.getPredicate().alwaysEquals(true)) {
          elseExpression = lastClause.getExpression();
          clauses = clauses.subList(0, clauses.size() - 1);
        } else {
          elseExpression = new StringConstant(node, null, "");
        }
      }

      if (clauses.isEmpty()) {
        if (elseExpression == null) {
          alertSink.add(new NoClausesInCondError(node));
        }
      } else {
        output.accumulate(new Conditional(node, null, clauses,
                                          elseExpression));
      }

      return null;
    }

    public Void visitClauseElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();


      Expression predicate =
          attrMap.getOptionalExprValue("cond",
                                       new BooleanConstant(node, true));
      if (predicate != null) {
        output.accumulate(
            new Conditional.Clause(node, predicate,
                                   getCollapsableContent(attrMap)));
      }
      return null;
    }

    public Void visitIfElement(GxpNamespace.GxpElement node) {
      throw new UnexpectedNodeException(node);
    }

    public Void visitElifElement(GxpNamespace.GxpElement node) {
      throw new UnexpectedNodeException(node);
    }

    public Void visitElseElement(GxpNamespace.GxpElement node) {
      throw new UnexpectedNodeException(node);
    }

    public Void visitImportElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();

      String cls = attrMap.getOptional("class", null);
      String pkg = attrMap.getOptional("package", null);

      if (cls == null && pkg == null) {
        alertSink.add(new MissingAttributesError(node, "class", "package"));
        return null;
      }

      if (cls != null && pkg != null) {
        alertSink.add(new ConflictingAttributesError(
                          node,
                          attrMap.getAttribute(NullNamespace.INSTANCE, "class"),
                          attrMap.getAttribute(NullNamespace.INSTANCE, "package")));
        return null;
      }

      if (pkg != null) {
        // TODO(harryh): strictly speaking this will incorrectly reject
        //               package='foo', but this will prolly never matter
        //               in real life.
        TemplateName.FullyQualified packageName =
            TemplateName.parseFullyQualifiedDottedName(
                alertSink, node.getSourcePosition(), pkg);
        if (packageName != null) {
          output.accumulate(new PackageImport(node, pkg));
        }
      } else if (cls != null) {
        TemplateName.FullyQualified className =
            TemplateName.parseFullyQualifiedDottedName(
                alertSink, node.getSourcePosition(), cls);
        if (className != null) {
          output.accumulate(new ClassImport(node, className));
        }
      }
      return null;
    }

    private TemplateName.FullyQualified createRootName(Node node) {
      AttributeMap attrMap = nodeParts.getAttributes();

      Expression nameAttr = attrMap.getValue("name", null);
      if (nameAttr != null) {
        String s = nameAttr.getStaticString(alertSink, null);
        if (!className.equals(s)) {
          alertSink.add(new MismatchedTemplateNameError(
                            nameAttr.getSourcePosition(), s, className));
        }
      }

      return TemplateName.parseFullyQualifiedDottedName(
          alertSink, node.getSourcePosition(), className);
    }

    public Void visitInterfaceElement(GxpNamespace.GxpElement node) {
      TemplateName.FullyQualified name = createRootName(node);
      ContentType contentType = createContentType(node, DEFAULT_CONTENT_TYPE);

      // add a "this" parameter to the list of Parameters
      List<Parameter> parameters = Lists.newArrayList(nodeParts.getParameters());
      FormalParameter formal = new FormalParameter(node.getSourcePosition(),
                                                   Implementable.INSTANCE_PARAM_NAME,
                                                   Implementable.INSTANCE_PARAM_NAME,
                                                   new TemplateType(node.getSourcePosition(),
                                                                    name.toString(), name));
      parameters.add(new Parameter(formal));

      List<JavaAnnotation> javaAnnotations =
          getJavaAnnotations(node, JavaAnnotation.Element.INTERFACE);
      List<Import> imports = nodeParts.getImports();
      List<ThrowsDeclaration> throwsDeclarations = nodeParts.getThrowsDeclarations();
      List<FormalTypeParameter> formalTypeParameters = nodeParts.getFormalTypeParameters();

      if (contentType != null) {
        output.accumulate(new Interface(node,
                                        name,
                                        contentType.getSchema(),
                                        javaAnnotations,
                                        imports,
                                        throwsDeclarations,
                                        parameters,
                                        formalTypeParameters));
      }

      return null;
    }

    public Void visitAbbrElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      Type type = createType(node, attrMap, false, null);
      String name = getVariableName(attrMap, "name", false);
      Expression expr = attrMap.getExprValue("expr", null);
      Expression content = getCollapsableContent(attrMap);
      if (type != null && name != null && expr != null) {
        output.accumulate(new AbbrExpression(node, type, name, expr, content));
      }
      return null;
    }

    public Void visitLoopElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      Type type = createType(node, attrMap, false, null);
      String key = getVariableName(attrMap, "key", true);
      String var = getVariableName(attrMap, "var", false);
      Expression content = getCollapsableContent(attrMap);

      Expression delimiter = attrMap.getOptionalAttributeValue(
          "delimiter", new StringConstant(node, null, " "));

      Expression iterator = attrMap.getOptionalExprValue("iterator", null);
      Expression iterable = attrMap.getOptionalExprValue("iterable", null);

      if (iterable == null && iterator == null) {
        alertSink.add(new MissingAttributesError(node, "iterator", "iterable"));
        return null;
      }

      // check for all of the ways iteratpr and iterable attributes can
      // conflict with each other
      boolean foundConflict = checkForConflictingLoopAttributes(node, attrMap,
                                                                NullNamespace.INSTANCE,
                                                                NullNamespace.INSTANCE);
      for (OutputLanguageNamespace ns : AttributeMap.getOutputLanguageNamespaces()) {
        foundConflict |= checkForConflictingLoopAttributes(node, attrMap,
                                                           NullNamespace.INSTANCE, ns);
        foundConflict |= checkForConflictingLoopAttributes(node, attrMap, ns,
                                                           NullNamespace.INSTANCE);
        foundConflict |= checkForConflictingLoopAttributes(node, attrMap, ns, ns);
      }

      // special case to disallow JavaScript iterator attribute this
      // isn't in JavaScriptCodeGenerator, because we want to generate an
      // alert for this even if we aren't generating JS code
      Attribute jsIterator = attrMap.getAttribute(JavaScriptNamespace.INSTANCE, "iterator");
      if (jsIterator != null) {
        alertSink.add(new UnknownAttributeError(node, jsIterator));
        return null;
      }

      if ((type != null) && (var != null) && !foundConflict) {
        output.accumulate(new LoopExpression(node, type, key, var, iterable, iterator,
                                             content, delimiter));
      }
      return null;
    }

    public boolean checkForConflictingLoopAttributes(Node node, AttributeMap attrMap,
                                                     Namespace iterableNs, Namespace iteratorNs) {
      Attribute attrA = attrMap.getAttribute(iterableNs, "iterable");
      Attribute attrB = attrMap.getAttribute(iteratorNs, "iterator");
      if (attrA != null && attrB != null) {
        alertSink.add(new ConflictingAttributesError(node, attrA, attrB));
        return true;
      } else {
        return false;
      }
    }

    /**
     * @return a {@code Type} based on {@code gxpType}.  Optionally grab
     * additional attributes from the {@code AttributeMap}
     */
    private Type createGxpType(Node node, AttributeMap attrMap, Attribute gxpType) {
      String kind = gxpType.getValue().getStaticString(alertSink, null);

      GxpType type = GxpType.parse(kind);
      if (type == null) {
        alertSink.add(new InvalidAttributeValueError(gxpType));
        return null;
      }
      switch (type) {
        case BOOL:
          return new BooleanType(node);
        case BUNDLE:
          String from = attrMap.get("from-element", null);
          if (from == null || rootSchema == null) {
            return null;
          }

          Map<String, AttributeValidator> subAttrMap = Maps.newHashMap(
              rootSchema.getElementValidator(from).getAttributeValidatorMap());

          // remove items from the exclude list
          String exclude = attrMap.getOptional("exclude", null);
          if (exclude != null) {
            for (String eItem : exclude.split(",")) {
              // TODO(harryh): make sure items in exclude list
              //               actually remove elements
              subAttrMap.remove(eItem.trim());
            }
          }

          return new BundleType(node, rootSchema, subAttrMap);
        default:
          alertSink.add(new InvalidAttributeValueError(gxpType));
          return null;
      }
    }

    /**
     * @return a {@code Type} based on the attribures contained in the
     *         passed in {@code AttributeMap}.
     */
    private Type createType(Node node, AttributeMap attrMap, boolean forParam, Type defaultType) {
      Attribute gxpType = attrMap.getAttribute(GxpNamespace.INSTANCE, "type");
      Attribute contentType = attrMap.getAttribute(NullNamespace.INSTANCE, "content-type");
      MultiLanguageAttrValue nativeType = attrMap.getMultiLanguageAttrValue("type");

      // check for conflicting attributes
      if (gxpType != null && contentType != null) {
        alertSink.add(new ConflictingAttributesError(node, gxpType, contentType));
      }
      if ((gxpType != null || contentType != null) && !nativeType.isEmpty()) {
        Attribute conflict = (gxpType != null) ? gxpType : contentType;
        for (OutputLanguageNamespace ns : AttributeMap.getOutputLanguageNamespaces()) {
          Attribute nsAttr = attrMap.getAttribute(ns, "type");
          if (nsAttr != null) {
            alertSink.add(new ConflictingAttributesError(node, conflict, nsAttr));
          }
        }
        Attribute defaultTypeAttr = attrMap.getAttribute("type");
        if (defaultTypeAttr != null) {
          alertSink.add(new ConflictingAttributesError(node, conflict, defaultTypeAttr));
        }
      }

      // build an appropriate type
      Type type = (gxpType != null) ? createGxpType(node, attrMap, gxpType)
          : (contentType != null) ? createContentType(node, null)
          : (defaultType == null || !nativeType.isEmpty()) ? new NativeType(node, nativeType)
          : defaultType;

      if (type != null && type.onlyAllowedInParam() && !forParam) {
        alertSink.add(new InvalidTypeError(type));
        type = null;
      }

      return type;
    }

    /**
     * Parses an attribute to produce a ContentType.
     *
     * @param forNode Node ContentType is for
     * @param defaultValue default value in case the attribute is
     * unset/invalid, or null if there is no default
     */
    private ContentType createContentType(Node forNode, String defaultValue) {
      AttributeMap attrMap = nodeParts.getAttributes();
      Attribute attr = attrMap.getAttribute("content-type");

      Node node;
      String contentType;
      if (attr == null) {
        node = forNode;
        contentType = defaultValue;
      } else {
        node = attr;
        contentType = attr.getValue().getStaticString(alertSink, defaultValue);
      }

      Schema schema = (contentType == null)
          ? null
          : schemaFactory.fromContentTypeName(contentType);

      if (schema == null && contentType != null) {
        alertSink.add(new UnknownContentTypeError(node, contentType));
      }

      return (schema == null)
          ? null
          : new ContentType(node.getSourcePosition(),
                            node.getDisplayName(),
                            schema);
    }

    public Void visitParamElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String name = getVariableName(attrMap, "name", false);
      List<JavaAnnotation> javaAnnotations
          = getJavaAnnotations(node, JavaAnnotation.Element.PARAM);
      Expression defaultValue = null;
      boolean hasDefaultFlag = false;
      Pattern regex = null;
      Expression constructor = null;
      boolean hasConstructorFlag = false;

      String content = attrMap.getOptional("content", null);
      Type defaultType = null;
      boolean consumesContent = content != null;
      if (consumesContent) {
        if (!"*".equals(content)) {
          alertSink.add(new InvalidAttributeValueError(attrMap.getAttribute("content")));
        }
        if (rootSchema != null) {
          defaultType = new ContentType(node.getSourcePosition(),
                                        node.getDisplayName(),
                                        rootSchema);
        }
      }

      Type type = createType(node, attrMap, true, defaultType);
      if (type == null) {
        // Bail out without trying to construct Parameter.
        return null;
      }
      if (consumesContent && !type.isContent()) {
        alertSink.add(new ContentTypeExpectedAlert(node.getSourcePosition(),
                                                   node.getDisplayName(),
                                                   "when content='*' is set."));
        type = defaultType;
      }

      if (type.takesDefaultParam()) {
        defaultValue = attrMap.getOptionalExprValue("default", null);
        hasDefaultFlag = attrMap.getBooleanValue("has-default");
      }

      if (type.takesRegexParam()) {
        String regexStr = attrMap.getOptional("regex", null);
        if (regexStr != null) {
          try {
            regex = Pattern.compile(regexStr);
          } catch (PatternSyntaxException e) {
            alertSink.add(new BadRegexError(node, e.getPattern()));
          }
        }
      } else {
        regex = type.getPattern(name);
      }

      if (type.takesConstructorParam()) {
        constructor = attrMap.getOptionalExprValue("constructor", null);
        hasConstructorFlag = attrMap.getBooleanValue("has-constructor");
      }

      SpaceOperatorSet spaceOperators = getSpaceOperators(attrMap);

      // Note that we *don't* want to run the comment through space collapsing
      // as the space operators specified on a gxp:param actually apply to the
      // passed values.
      // TODO(laurence): should gxp:param even support nested content?
      Expression comment = nodeParts.getContent();
      if (!comment.hasStaticString()) {
        alertSink.add(new RequiresStaticContentError(node));
        comment = new StringConstant(node, null, "");
      }
      if (name != null) {
        FormalParameter formal = new FormalParameter(node, name, consumesContent, type,
                                                     defaultValue, hasDefaultFlag, regex,
                                                     constructor, hasConstructorFlag,
                                                     spaceOperators);
        output.accumulate(new Parameter(formal, javaAnnotations, defaultValue, hasDefaultFlag,
                                        constructor, hasConstructorFlag, comment));
      }
      return null;
    }

    public Void visitTemplateElement(GxpNamespace.GxpElement node) {
      TemplateName.FullyQualified name = createRootName(node);

      AttributeMap attrMap = nodeParts.getAttributes();

      ContentType contentType = createContentType(node, DEFAULT_CONTENT_TYPE);

      List<Constructor> constructors = nodeParts.getConstructors();
      Constructor constructor = constructors.isEmpty()
          ? Constructor.empty(node) : constructors.get(0);

      if (constructors.size() > 1) {
        alertSink.add(new MoreThanOneConstructorError(constructors.get(1)));
      }

      List<JavaAnnotation> javaAnnotations = getJavaAnnotations(node,
                                                                JavaAnnotation.Element.CLASS,
                                                                JavaAnnotation.Element.INSTANCE,
                                                                JavaAnnotation.Element.INTERFACE);

      List<Import> imports = nodeParts.getImports();
      List<ImplementsDeclaration> implementsDeclarations = nodeParts.getImplementsDeclarations();
      List<ThrowsDeclaration> throwsDeclarations = nodeParts.getThrowsDeclarations();
      List<Parameter> parameters = nodeParts.getParameters();
      List<FormalTypeParameter> formalTypeParameters = nodeParts.getFormalTypeParameters();
      Expression content = getCollapsableContent(attrMap);

      if (contentType != null) {
        output.accumulate(new Template(node,
                                       name,
                                       contentType.getSchema(),
                                       javaAnnotations,
                                       constructor,
                                       imports,
                                       implementsDeclarations,
                                       throwsDeclarations,
                                       parameters,
                                       formalTypeParameters,
                                       content));
      }

      return null;
    }

    public Void visitConstructorElement(GxpNamespace.GxpElement node) {
      output.accumulate(new Constructor(node,
                                        getJavaAnnotations(node,
                                                           JavaAnnotation.Element.CONSTRUCTOR),
                                        nodeParts.getParameters()));
      return null;
    }

    public Void visitImplementsElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String interfaceType = attrMap.getOptional(JavaNamespace.INSTANCE, "interface", null);
      if (interfaceType != null) {
        NativeType type = new NativeType(node, interfaceType);
        output.accumulate(new NativeImplementsDeclaration(node, type));
      } else {
        interfaceType = attrMap.get("interface", null);
        if (interfaceType != null) {
          TemplateName templateName = TemplateName.create(null, interfaceType);
          output.accumulate(new UnboundImplementsDeclaration(node, templateName));
        }
      }
      return null;
    }

    public Void visitThrowsElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String exceptionType = attrMap.get("exception", null);
      if (exceptionType != null) {
        TemplateName exception =
            TemplateName.parseDottedName(
                alertSink, node.getSourcePosition(), exceptionType);
        if (exception != null) {
          // TODO(harryh): exceptionType should be a NativeType, not just
          //               a String
          output.accumulate(new ThrowsDeclaration(node, exceptionType));
        }
      }
      return null;
    }

    public Void visitTypeParamElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String name = getVariableName(attrMap, "name", false);
      String extendsType = attrMap.getOptional("extends", null);
      if (name != null) {
        NativeType type = (extendsType == null)
            ? null : new NativeType(node, extendsType);
        output.accumulate(new FormalTypeParameter(node, name, type));
      }
      return null;
    }

    public Void visitMsgElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      MultiLanguageAttrValue name = attrMap.getMultiLanguageAttrValue("name");
      String meaning = attrMap.getOptional("meaning", null);
      String comment = attrMap.getOptional("comment", null);
      boolean hidden = attrMap.getBooleanValue("hidden");
      // TODO(laurence): coerce content to HTML
      Expression content = getCollapsableContent(attrMap);

      ContentType contentType = createContentType(node, null);
      Schema schema = (contentType == null) ? null : contentType.getSchema();

      output.accumulate(
          new UnextractedMessage(node, schema, name, meaning, comment, hidden, content));
      return null;
    }

    public Void visitNoMsgElement(GxpNamespace.GxpElement node) {
      // TODO(laurence) suppress i18n warnings (when they exist)
      AttributeMap attrMap = nodeParts.getAttributes();
      output.accumulate(new NoMessage(node, getCollapsableContent(attrMap)));
      return null;
    }

    public Void visitPHElement(GxpNamespace.GxpElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String name = attrMap.get("name", null);
      String example = attrMap.getOptional("example", null);
      if (example != null) {
        if (example.trim().length() == 0) {
          alertSink.add(
              new InvalidAttributeValueError(attrMap.getAttribute("example")));
          example = "<var>" + name + "</var>";
        }
      }
      if (name != null) {
        output.accumulate(new PlaceholderStart(node, null, name, example));
      }
      return null;
    }

    public Void visitEPHElement(GxpNamespace.GxpElement node) {
      output.accumulate(new PlaceholderEnd(node, null));
      return null;
    }

    /**
     * Examine the {@code gxp:bundles} attribute and return the list of
     * bundles it specifies.
     */
    private List<String> getBundles(AttributeMap attrMap) {
      List<String> bundles = Lists.newArrayList();
      String bundlesStr = attrMap.getOptional(GxpNamespace.INSTANCE, "bundles", null);
      if (bundlesStr != null) {
        for (String s : bundlesStr.split(",")) {
          bundles.add(s.trim());
        }
      }
      return bundles;
    }

    // CallNamespace elements
    public Void visitCallElement(CallNamespace.CallElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      ImmutableMap.Builder<String, Attribute> attrBuilder = ImmutableMap.builder();
      Expression content = new ConvertibleToContent(getCollapsableContent(attrMap));
      List<String> bundles = getBundles(attrMap);

      for (Attribute attr : attrMap.getUnusedAttributes()) {
        if (attr.getNamespace() instanceof NullNamespace) {
          Expression value = attr.getValue();
          if (value instanceof StringConstant) {
            attr = attr.withValue(new ObjectConstant((StringConstant) value));
          }
          attrBuilder.put(attr.getName(), attr);
        } else {
          alertSink.add(new UnknownAttributeError(node, attr));
        }
      }

      TemplateName callee = TemplateName.parseDottedName(alertSink,
                                                         node.getSourcePosition(),
                                                         node.getTagName());

      if (callee.isValid()) {
        // TODO(laurence): if callee == null then substitute good one
        output.accumulate(
            new UnboundCall(node.getSourcePosition(),
                            node.getDisplayName(),
                            callee,
                            attrBuilder.build(),
                            bundles,
                            content));
      }
      return null;
    }

    // OutputNamespace elements
    public Void visitParsedOutputElement(OutputNamespace.ParsedOutputElement node) {
      ElementValidator validator = node.getValidator();
      AttributeMap attrMap = nodeParts.getAttributes();

      String docTypeName = attrMap.getOptional(GxpNamespace.INSTANCE, "doctype", null);
      DocType docType;
      if (docTypeName == null) {
        docType = null;
      } else {
        docType = validator.getDocType(docTypeName);
        if (docType == null) {
          alertSink.add(new InvalidDoctypeError(node, docTypeName));
        }
      }

      // if an output element has a gxp:ph attribute we surrount the element
      // tags (but NOT the element content) with placeholders
      String phName = attrMap.getOptional(GxpNamespace.INSTANCE, "ph", null);

      // TODO(laurence): always collect content here, and check against
      // NOENDTAG in Validator phase.
      Expression content;
      if (validator.isFlagSet(ElementValidator.Flag.NOENDTAG)) {
        // Ignore supplied children. EditableParts will generate alerts about
        // them if necessary.
        content = new StringConstant(node, null, "");
      } else {
        content = getCollapsableContent(attrMap);
      }

      String innerContentTypeString = validator.getInnerContentType();
      Schema innerSchema = (innerContentTypeString == null)
          ? null
          : schemaFactory.fromContentTypeName(innerContentTypeString);

      List<String> bundles = getBundles(attrMap);

      List<Attribute> attrs = attrMap.getUnusedAttributes();
      output.accumulate(
          new OutputElement(node.getSourcePosition(),
                            node.getDisplayName(),
                            node.getSchema(),
                            innerSchema,
                            validator.getTagName(),
                            validator,
                            docType,
                            checkAttributes(node, validator, attrs),
                            bundles,
                            phName,
                            content));
      return null;
    }

    // CppNamespace elements
    public Void visitCppIncludeElement(CppNamespace.CppElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();

      String libraryName = attrMap.getOptional("library", null);
      String fileName = attrMap.getOptional("file", null);

      if (libraryName == null && fileName == null) {
        alertSink.add(new MissingAttributesError(node, "library", "file"));
        return null;
      }

      if (libraryName != null && fileName != null) {
        alertSink.add(new ConflictingAttributesError(
                          node,
                          attrMap.getAttribute(NullNamespace.INSTANCE, "library"),
                          attrMap.getAttribute(NullNamespace.INSTANCE, "file")));
        return null;
      }

      if (libraryName != null) {
        output.accumulate(new CppLibraryImport(node, libraryName));
      } else if (fileName != null) {
        output.accumulate(new CppFileImport(node, fileName));
      }

      return null;
    }

    // JavaNamespace elements
    public Void visitJavaAnnotateElement(JavaNamespace.JavaElement node) {
      AttributeMap attrMap = nodeParts.getAttributes();
      String with = attrMap.get("with", null);
      JavaAnnotation.Element element = null;
      String elementStr = attrMap.getOptional("element", null);
      if (elementStr != null) {
        try {
          element = JavaAnnotation.Element.valueOf(elementStr.toUpperCase());
        } catch (IllegalArgumentException e) {
          alertSink.add(new InvalidAttributeValueError(attrMap.getValue("element", null)));
          with = null;
        }
      }

      if (with != null) {
        output.accumulate(new JavaAnnotation(node.getSourcePosition(),
                                             node.getDisplayName(),
                                             element,
                                             with));
      }
      return null;
    }

    public Void visitTextElement(TextElement node) {
      output.accumulate(new StringConstant(node, null, node.getText()));
      return null;
    }

    // ignore NullElements (created by IfExpander in error cases)
    public Void visitNullElement(NullElement node) {
      return null;
    }

    /**
     * Checks attributes based on element validator. This involves
     * removing unknown attributes, and reporting these and any other
     * attribute problems as {@code Alert}s.
     *
     * The main purpose of this function is actually reporting these issues,
     * but they are also "corrected" to some  degree in order to allow
     * processing to continue without tripping over these issues again
     * later on in the pipeline.
     *
     * In addition, Attributes that have an inner content-type are modified
     * to indicate this fact.
     *
     * @param elementValidator ElementValidator to be used in checking
     * all attributes
     * @param attrs the attributes to filter
     * @return a copy of {@code attrs} with "corrections"
     */
    private List<Attribute> checkAttributes(Node forNode, ElementValidator elementValidator,
                                            List<Attribute> attrs) {
      if (attrs.isEmpty()) {
        return Collections.emptyList();
      } else {
        List<Attribute> result = Lists.newArrayList();
        for (Attribute attr : attrs) {
          AttributeValidator attrValidator =
              elementValidator.getAttributeValidator(attr.getName());
          if (attrValidator == null) {
            alertSink.add(new UnknownAttributeError(forNode, attr));
          } else {
            Expression attrValue = attr.getValue();
            if (attrValue instanceof StringConstant) {
              String value = ((StringConstant) attrValue).evaluate();
              if (!attrValidator.isValidValue(value)) {
                alertSink.add(new InvalidAttributeValueError(attr));
              }
            }

            // if the attribute validator indicates an inner content type then
            // set this on the attribute
            String innerContentTypeString = attrValidator.getContentType();
            if (innerContentTypeString != null) {
              Schema innerSchema = schemaFactory.fromContentTypeName(innerContentTypeString);
              attr = attr.withInnerSchema(innerSchema);
            }

            result.add(attr);
          }
        }
        return result;
      }
    }

    private String getVariableName(AttributeMap attrMap, String attrName, boolean optional) {
      String result = optional
          ? attrMap.getOptional(attrName, null)
          : attrMap.get(attrName, null);
      if (result != null) {
        if (!VARIABLE_NAME_PATTERN.matcher(result).matches()) {
          alertSink.add(new IllegalVariableNameError(attrMap.getAttribute(attrName), result));
          return null;
        }

        if (ILLEGAL_NAME_PATTERN.matcher(result).matches()) {
          alertSink.add(new IllegalVariableNameError(attrMap.getAttribute(attrName), result));
          return null;
        }

        if (result.length() > 64) {
          alertSink.add(new IllegalVariableNameError(attrMap.getAttribute(attrName), result));
          return null;
        }
      }
      return result;
    }
  }

  // Starts with an ASCII letter, followed by (ASCII) letters, numbers and
  // underscores. Underscores may only appear between two non-underscores.
  private static final Pattern VARIABLE_NAME_PATTERN =
      Pattern.compile("[a-zA-Z](_?[a-zA-Z0-9])*");

  // "this" is reserved for use with gxp:interface
  // "gxp_" prefix is reserved for internal use (gxp_context, gxp_locale)
  private static final Pattern ILLEGAL_NAME_PATTERN =
      Pattern.compile("this|gxp_(.)*");

  private enum GxpType {
    BOOL, BUNDLE;

    private static final Map<String, GxpType> MAP =
      ImmutableMap.<String, GxpType>builder()
        .put("boolean", GxpType.BOOL)
        .put("bundle", GxpType.BUNDLE)
        .build();

    public static GxpType parse(String s) {
      return MAP.get(s);
    }
  }
}
TOP

Related Classes of com.google.gxp.compiler.reparent.Reparenter$ElementVisitor

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.