Package com.google.gxp.compiler.js

Source Code of com.google.gxp.compiler.js.JavaScriptCodeGenerator$TemplateWorker$ToEscapableExpressionVisitor

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

import static com.google.gxp.compiler.base.OutputLanguage.JAVASCRIPT;

import com.google.common.base.CharEscapers;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.gxp.compiler.alerts.AlertSink;
import com.google.gxp.compiler.alerts.SourcePosition;
import com.google.gxp.compiler.alerts.common.NothingToCompileError;
import com.google.gxp.compiler.base.AbbrExpression;
import com.google.gxp.compiler.base.AttrBundleParam;
import com.google.gxp.compiler.base.AttrBundleReference;
import com.google.gxp.compiler.base.BooleanConstant;
import com.google.gxp.compiler.base.BoundCall;
import com.google.gxp.compiler.base.Call;
import com.google.gxp.compiler.base.Callable;
import com.google.gxp.compiler.base.CallableVisitor;
import com.google.gxp.compiler.base.CallVisitor;
import com.google.gxp.compiler.base.Concatenation;
import com.google.gxp.compiler.base.Conditional;
import com.google.gxp.compiler.base.ConstructedConstant;
import com.google.gxp.compiler.base.ConvertibleToContent;
import com.google.gxp.compiler.base.DefaultingExpressionVisitor;
import com.google.gxp.compiler.base.EscapeExpression;
import com.google.gxp.compiler.base.ExampleExpression;
import com.google.gxp.compiler.base.ExceptionExpression;
import com.google.gxp.compiler.base.Expression;
import com.google.gxp.compiler.base.ExpressionVisitor;
import com.google.gxp.compiler.base.ExtractedMessage;
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.InstanceCallable;
import com.google.gxp.compiler.base.Interface;
import com.google.gxp.compiler.base.IsXmlExpression;
import com.google.gxp.compiler.base.LoopExpression;
import com.google.gxp.compiler.base.NativeExpression;
import com.google.gxp.compiler.base.NullRoot;
import com.google.gxp.compiler.base.ObjectConstant;
import com.google.gxp.compiler.base.Parameter;
import com.google.gxp.compiler.base.Root;
import com.google.gxp.compiler.base.RootVisitor;
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.UnboundCall;
import com.google.gxp.compiler.base.UnexpectedNodeException;
import com.google.gxp.compiler.base.ValidatedCall;
import com.google.gxp.compiler.codegen.BracesCodeGenerator;
import com.google.gxp.compiler.msgextract.MessageExtractedTree;
import com.google.gxp.compiler.reparent.Attribute;
import com.google.gxp.compiler.schema.AttributeValidator;
import com.google.gxp.compiler.schema.ContentFamilyVisitor;
import com.google.gxp.compiler.schema.Schema;
import com.google.transconsole.common.messages.MessageFragment;
import com.google.transconsole.common.messages.Placeholder;

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

/**
* {@code CodeGenerator} which generates JavaScript code.
*/
public class JavaScriptCodeGenerator extends BracesCodeGenerator<MessageExtractedTree> {
  public JavaScriptCodeGenerator(MessageExtractedTree tree) {
    super(tree);
  }

  public void generateCode(final Appendable appendable, final AlertSink alertSink) {
    alertSink.addAll(tree.getAlerts());

    root.acceptVisitor(new RootVisitor<Void>() {
      public Void visitInterface(Interface iface) {
        // TODO: what should we even do with interfaces in javascript?
        //       should we add an alert?
        return null;
      }

      public Void visitNullRoot(NullRoot nullRoot) {
        alertSink.add(new NothingToCompileError(nullRoot.getSourcePosition()));
        return null;
      }

      public Void visitTemplate(Template template) {
        validateFormalTypeParameters(alertSink, template.getFormalTypeParameters());
        createTemplateWorker(appendable, alertSink, template).run();
        return null;
      }
    });
  }

  private void validateFormalTypeParameters(AlertSink alertSink,
                                            List<FormalTypeParameter> formalTypeParameters) {
    for (FormalTypeParameter formalTypeParameter : formalTypeParameters) {
      JAVASCRIPT.validateName(alertSink, formalTypeParameter, formalTypeParameter.getName());
    }
  }

  private TemplateWorker createTemplateWorker(Appendable appendable,
                                              AlertSink alertSink,
                                              Template template) {
    return new TemplateWorker(appendable, alertSink, template);
  }

  private static class TemplateWorker extends BracesCodeGenerator.Worker {
    private final Template template;
    private int varCounter = 0;

    public TemplateWorker(Appendable appendable, AlertSink alertSink, Template template) {
      super(appendable, alertSink);
      this.template = Preconditions.checkNotNull(template);
    }

    public TemplateWorker createSubWorker(Appendable newAppendable) {
      return new TemplateWorker(newAppendable, alertSink, template);
    }

    public void run() {
      for (Parameter param : template.getAllParameters()) {
        JAVASCRIPT.validateName(alertSink, param, param.getPrimaryName());
      }
      appendHeader(template);
      appendLine();
      formatLine("goog.provide('%s');", getClassName(template.getName()));
      appendLine();
      appendImports(template);
      appendLine();
      appendConstructor();
      appendLine();
      appendWriteMethod();
      appendLine();
      appendWriteWithObjMethod(false);
      appendLine();
      appendGetGxpClosureMethod(false);
      appendLine();
      appendStaticWriteMethod();
      appendLine();
      appendWriteWithObjMethod(true);
      appendLine();
      appendGetGxpClosureMethod(true);
      appendGetDefaultMethods();
      appendConstructorMethods();
      appendLine();
      appendFooter();
    }

    private String getWriteMethodSignature(boolean isStatic) {
      Iterable<Parameter> params = isStatic
          ? template.getAllParameters()
          : template.getParameters();

      StringBuilder sb = new StringBuilder();
      sb.append(getClassName(template.getName()));
      if (!isStatic) {
        sb.append(".prototype");
      }
      sb.append(".write = function(");
      COMMA_JOINER.appendTo(sb, Iterables.concat(ImmutableSet.of(GXP_SIG),
                                                 Iterables.transform(params, parameterToName)));
      sb.append(") {");
      return sb.toString();
    }

    private String getGetGxpClosureMethodSignature(boolean isStatic) {
      Iterable<Parameter> params = isStatic
          ? template.getAllParameters()
          : template.getParameters();

      StringBuilder sb = new StringBuilder();
      sb.append(getClassName(template.getName()));
      if (!isStatic) {
        sb.append(".prototype");
      }
      sb.append(".getGxpClosure = function(");
      COMMA_JOINER.appendTo(sb, Iterables.transform(params, parameterToName));
      sb.append(") {");
      return sb.toString();
    }

    private void appendImports(Root root) {
      for (String imp : root.getSchema().getJavaScriptImports()) {
        formatLine("goog.require('%s');", imp);
      }
    }

    private void appendConstructor() {
      Iterable<Parameter> params = template.getConstructor().getParameters();

      appendLine("/**");
      appendLine(" * @constructor");
      appendLine(" */");
      StringBuilder sb = new StringBuilder();
      sb.append(getClassName(template.getName()));
      sb.append(" = function(");
      COMMA_JOINER.appendTo(sb, Iterables.transform(params, parameterToName));
      sb.append(") {");
      appendLine(sb);
      for (Parameter param : params) {
        formatLine("this.%s = %s;", param.getPrimaryName(), param.getPrimaryName());
      }
      appendLine("};");
    }

    private void appendWriteMethod() {
      Iterable<Parameter> ctorParams = template.getConstructor().getParameters();
      Iterable<Parameter> params = template.getParameters();

      // varargs + generics = spurious unchecked warning.  OK to suppress.
      @SuppressWarnings("unchecked")
      Iterable<String> methodParameters = Iterables.concat(
          ImmutableSet.of(GXP_SIG),
          Iterables.transform(ctorParams, parameterToMemberName),
          Iterables.transform(params, parameterToName));

      appendLine(getWriteMethodSignature(false));
      StringBuilder sb = new StringBuilder();
      sb.append(getClassName(template.getName()));
      sb.append(".write(");
      COMMA_JOINER.appendTo(sb, methodParameters);
      sb.append(");");
      appendLine(sb);
      appendLine("};");
    }

    private void appendGetGxpClosureMethod(boolean isStatic) {
      Iterable<Parameter> params = isStatic
          ? template.getAllParameters()
          : template.getParameters();

      appendLine(getGetGxpClosureMethodSignature(isStatic));
      if (!isStatic) {
        for (Parameter param : template.getConstructor().getParameters()) {
          formatLine("var %s = this.%s;", param.getPrimaryName(), param.getPrimaryName());
        }
      }
      formatLine("return new %s(function(%s) {",
                 template.getSchema().getJavaScriptType(), GXP_SIG);

      StringBuilder sb = new StringBuilder();
      sb.append(getClassName(template.getName()));
      sb.append(".write(");
      COMMA_JOINER.appendTo(sb, Iterables.concat(ImmutableSet.of(GXP_SIG),
                                                 Iterables.transform(template.getAllParameters(),
                                                                     parameterToName)));
      sb.append(");");
      appendLine(sb);
      formatLine("return %s;", GXP_OUT_VAR);
      appendLine("});");
      appendLine("};");
    }

    private void appendStaticWriteMethod() {
      appendLine(getWriteMethodSignature(true));
      template.getContent().acceptVisitor(statementVisitor);
      appendLine("};");
    }

    private void extractMapParameters(boolean isStatic) {
      String baseName = isStatic ? getClassName(template.getName()) : "this";
      Iterable<Parameter> params = isStatic
          ? template.getAllParameters()
          : template.getParameters();

      // Create some stub declarations of all parameters, to tell jscompiler
      // that it's ok if these parameters are not defined anywhere.
      for (Parameter param : params) {
        formatLine(
            "/** @type {*} */ %s.%s;", GXP_PARAM_VAR, param.getPrimaryName());
      }

      for (Parameter param : params) {
        if (param.getDefaultValue() != null) {
          formatLine(param.getSourcePosition(),
                     "var %s = %s.%s || %s.%s();",
                     param.getPrimaryName(),
                     GXP_PARAM_VAR, param.getPrimaryName(),
                     baseName, getDefaultMethodName(param));
        } else {
          formatLine(param.getSourcePosition(), "var %s = %s.%s;",
                     param.getPrimaryName(), GXP_PARAM_VAR, param.getPrimaryName());
        }
      }
    }

    private void appendWriteWithObjMethod(boolean isStatic) {
      StringBuilder sb = new StringBuilder();
      sb.append(getClassName(template.getName()));
      if (!isStatic) {
        sb.append(".prototype");
      }
      sb.append(".writeWithObj = function(");
      sb.append(GXP_PARAM_SIG);
      sb.append(") {");
      appendLine(sb);

      extractMapParameters(isStatic);

      sb = new StringBuilder(getClassName(template.getName()));
      sb.append(".write(");
      if (isStatic) {
        COMMA_JOINER.appendTo(sb, Iterables.concat(ImmutableSet.of(GXP_SIG),
                                                   Iterables.transform(template.getAllParameters(),
                                                                       parameterToName)));
      } else {
        // varargs + generics = spurious unchecked warning.  OK to suppress.
        @SuppressWarnings("unchecked")
        Iterable<String> methodParameters = Iterables.concat(
            ImmutableSet.of(GXP_SIG),
            Iterables.transform(template.getConstructor().getParameters(), parameterToMemberName),
            Iterables.transform(template.getParameters(), parameterToName));

        COMMA_JOINER.appendTo(sb, methodParameters);
      }
      sb.append(");");

      appendLine(sb);
      appendLine("};");
    }

    private void appendGetDefaultMethods() {
      for (Parameter param : template.getAllParameters()) {
        Expression defaultValue = param.getDefaultValue();
        if (defaultValue != null) {
          appendLine();
          formatLine(param.getDefaultValue().getSourcePosition(),
                     "%s.GXP_DEFAULT$%s = %s;",
                     getClassName(template.getName()),
                     param.getPrimaryName(),
                     getJavaScriptExpression(param.getDefaultValue()));

          appendLine();
          String methodName = getDefaultMethodName(param);
          formatLine("%s.%s = function() {", getClassName(template.getName()), methodName);
          formatLine("return %s.GXP_DEFAULT$%s;",
                     getClassName(template.getName()), param.getPrimaryName());
          appendLine("};");
        }
      }

      // only non constuctor parameters get getDefault methods on an instantiated template
      for (Parameter param : template.getParameters()) {
        if (param.getDefaultValue() != null) {
          String methodName = getDefaultMethodName(param);
          appendLine();
          formatLine("%s.prototype.%s = function() {",
                     getClassName(template.getName()), methodName);
          formatLine("return %s.%s();", getClassName(template.getName()), methodName);
          appendLine("};");
        }
      }
    }

    private void appendConstructorMethods() {
      for (Parameter param : template.getAllParameters()) {
        if (param.getConstructor() != null) {
          String methodName = getConstructorMethodName(param);
          appendLine();
          formatLine("%s.%s = function(%s) {",
                     getClassName(template.getName()), methodName, param.getPrimaryName());
          formatLine(param.getSourcePosition(), "return %s;",
                     param.getConstructor().acceptVisitor(toExpressionVisitor));
          appendLine("};");
        }
      }

      // only non constuctor parameters get construct methods on an instantiated template
      for (Parameter param : template.getParameters()) {
        if (param.getConstructor() != null) {
          String methodName = getConstructorMethodName(param);
          appendLine();
          formatLine("%s.prototype.%s = function(%s) {",
                     getClassName(template.getName()), methodName, param.getPrimaryName());
          formatLine("return %s.%s(%s);",
                     getClassName(template.getName()), methodName, param.getPrimaryName());
          appendLine("};");
        }
      }
    }

    private static final String GXP_OUT_VAR = "gxp$out";
    private static final String GXP_CONTEXT_VAR = "gxp_context";
    private static final String GXP_PARAM_VAR = "gxp_param_map";

    private static final String GXP_SIG = COMMA_JOINER.join(GXP_OUT_VAR, GXP_CONTEXT_VAR);
    private static final String GXP_PARAM_SIG = COMMA_JOINER.join(GXP_OUT_VAR, GXP_CONTEXT_VAR,
                                                                  GXP_PARAM_VAR);

    /**
     * Creates a unique (to this Worker) variable name.
     *
     * @param token string which is included in variable name. This can be used
     * to (slightly) increase readability of the generated code.
     */
    protected final String createVarName(String token) {
      return "gxp$" + token + "$" + varCounter++;
    }

    private String getClassName(TemplateName name) {
      return name.toString();
    }

    private String getWriteMethodName(Schema schema) {
      String name = schema.getName();
      return "gxp$write" + Character.toUpperCase(name.charAt(0)) + name.substring(1);
    }

    private Function<Parameter, String> parameterToName =
      new Function<Parameter, String>() {
        public String apply(Parameter param) {
          return param.getPrimaryName();
        }
      };

    private Function<Parameter, String> parameterToMemberName =
      new Function<Parameter, String>() {
        public String apply(Parameter param) {
          return "this." + param.getPrimaryName();
        }
      };

    ////////////////////////////////////////////////////////////////////////////////
    // Utility Functions used by Visitors
    ////////////////////////////////////////////////////////////////////////////////

    protected void writeExpression(SourcePosition pos, String expr) {
      formatLine(pos, "%s.append(%s);", GXP_OUT_VAR, expr);
    }

    // TODO(harryh): find out what this should be
    private static final int MAX_JAVASCRIPT_STRING_LENGTH = 65534;

    protected void writeString(SourcePosition pos, String s) {
      int length = s.length();
      if (length != 0) {
        int curPos = 0;
        while (length - curPos > MAX_JAVASCRIPT_STRING_LENGTH) {
          writeExpression(pos, JAVASCRIPT.toStringLiteral(
                              s.substring(curPos, curPos + MAX_JAVASCRIPT_STRING_LENGTH)));
          curPos += MAX_JAVASCRIPT_STRING_LENGTH;
        }
        writeExpression(pos, JAVASCRIPT.toStringLiteral(s.substring(curPos, length)));
      }
    }

    protected final Deque<String> instantiatedGxps = new ArrayDeque<String>();

    /**
     * @return either the fully qualified static name of the GXP if we're
     * doing a static call, or the variable name of the instantiated GXP
     * if we're doing an instance call.
     */
    protected String getCalleeName(Callable callee) {
      return callee.acceptCallableVisitor(new CallableVisitor<String>() {
        public String visitCallable(Callable callable) {
          return getClassName(callable.getName());
        }

        public String visitInstanceCallable(InstanceCallable callable) {
          return instantiatedGxps.peek();
        }
      });
    }

    private List<String> getCallArguments(Callable callee, Map<String, Attribute> callerAttrs) {
      List<String> fParams = Lists.newArrayList();
      String calleeName = getCalleeName(callee);
      for (FormalParameter parameter : callee.getParameters()) {
        String paramName = parameter.getPrimaryName();
        if (!Implementable.INSTANCE_PARAM_NAME.equals(paramName)) {
          Attribute value = callerAttrs.get(paramName);

          String defaultFetchString = (parameter.getType().getDefaultValue() == null)
              ? calleeName + "." + getDefaultMethodName(parameter) + "()"
              : getJavaScriptExpression(parameter.getType().getDefaultValue());

          if (value == null) {
            fParams.add(defaultFetchString);
          } else if (value.getCondition() != null) {
            String s = "(" +
                getJavaScriptExpression(value.getCondition()) +
                " ? " + getJavaScriptExpression(value.getValue()) + " : " +
                defaultFetchString + ")";
            fParams.add(s);
          } else {
            fParams.add(getJavaScriptExpression(value.getValue()));
          }
        }
      }
      return fParams;
    }

    ////////////////////////////////////////////////////////////////////////////////
    // Visitors
    ////////////////////////////////////////////////////////////////////////////////

    private final StatementVisitor statementVisitor = getStatementVisitor();

    private final ExpressionVisitor<String> toExpressionVisitor =
        new ToExpressionVisitor();

    private final ExpressionVisitor<String> toEscapableExpressionVisitor =
        getToEscapableExpressionVisitor();

    protected StatementVisitor getStatementVisitor() {
      return new StatementVisitor();
    }

    protected ToEscapableExpressionVisitor getToEscapableExpressionVisitor() {
      return new ToEscapableExpressionVisitor();
    }

    /**
     * A visitor that outputs statements to {@code out} based on the nodes that
     * it visits.
     */
    protected class StatementVisitor extends DefaultingExpressionVisitor<Void>
        implements CallVisitor<Void> {

      @Override
      public Void defaultVisitExpression(Expression node) {
        throw new UnexpectedNodeException(node);
      }

      ////////////////////////////////////////////////////////////////////////////////
      // Expression Visitors
      ////////////////////////////////////////////////////////////////////////////////

      @Override
      public Void visitAbbrExpression(AbbrExpression abbr) {
        appendLine("(function() {");
        formatLine(abbr.getSourcePosition(),
                   "var %s = %s;",
                   JAVASCRIPT.validateName(alertSink, abbr, abbr.getName()),
                   getJavaScriptExpression(abbr.getValue()));
        abbr.getContent().acceptVisitor(this);
        appendLine("})();");
        return null;
      }

      @Override
      public Void visitConcatenation(Concatenation value) {
        for (Expression subValue : value.getValues()) {
          subValue.acceptVisitor(this);
        }
        return null;
      }

      @Override
      public Void visitConditional(Conditional value) {
        Iterator<Conditional.Clause> clauses = value.getClauses().iterator();
        if (clauses.hasNext()) {
          appendIf("if (", clauses.next());
          while (clauses.hasNext()) {
            appendIf("} else if (", clauses.next());
          }
          Expression elseExpression = value.getElseExpression();
          if (!elseExpression.alwaysEmpty()) {
            appendLine("} else {");
            elseExpression.acceptVisitor(this);
          }
          appendLine("}");
        } else {
          throw new AssertionError("No clauses in Conditional!");
        }
        return null;
      }

      private void appendIf(String prefix, Conditional.Clause clause) {
        Expression predicate = clause.getPredicate();
        appendLine(predicate.getSourcePosition(),
                   prefix + getJavaScriptExpression(predicate) + ") {");
        clause.getExpression().acceptVisitor(this);
      }

      @Override
      public Void visitConvertibleToContent(ConvertibleToContent value) {
        value.getSubexpression().acceptVisitor(this);
        return null;
      }

      @Override
      public Void visitEscapeExpression(EscapeExpression value) {
        formatLine(value.getSourcePosition(), "%s.%s(%s);",
                   getEscapableExpression(value.getSubexpression()),
                   getWriteMethodName(value.getSchema()), GXP_SIG);
        return null;
      }

      @Override
      public Void visitExampleExpression(ExampleExpression value) {
        return value.getSubexpression().acceptVisitor(this);
      }

      @Override
      public Void visitExceptionExpression(ExceptionExpression value) {
        String excClass;
        switch (value.getKind()) {
          case NOT_SUPPORTED_IN_SGML_MODE:
            excClass = "Error";
            break;
          default:
            throw new AssertionError("Unsupported ExceptionExpression.Kind: " + value.getKind());
        }
        appendLine(value.getSourcePosition(),
                   "throw new " + excClass + "("
                   + JAVASCRIPT.toStringLiteral(value.getMessage()) + ");");
        return null;
      }

      public String toLowerCamelCase(String s) {
        StringBuilder sb = new StringBuilder();
        boolean first = true;
        for (String part : s.split("_")) {
          part = part.toLowerCase();
          if (first) {
            first = false;
          } else {
            part = Character.toUpperCase(part.charAt(0)) + part.substring(1);
          }
          sb.append(part);
        }
        return sb.toString();
      }

      @Override
      public Void visitExtractedMessage(ExtractedMessage msg) {
        String paramVar = createVarName("params");
        if (!msg.getParameters().isEmpty()) {
          StringBuilder sb = new StringBuilder("var ");
          sb.append(paramVar);
          sb.append(" = [");
          COMMA_JOINER.appendTo(sb, Iterables.transform(msg.getParameters(),
                                                        expressionToEscapedString));
          sb.append("];");
          appendLine(sb);
        }
        Set<Placeholder> placeholders = Sets.newHashSet();
        String msgVar = "MSG_EXTERNAL_" + msg.getTcMessage().getId();
        StringBuilder sb = new StringBuilder("var ");
        sb.append(msgVar);
        sb.append(" = goog.getMsg(\"");
        for (MessageFragment fragment : msg.getTcMessage()) {
          if (fragment instanceof Placeholder) {
            placeholders.add((Placeholder) fragment);
            sb.append("{$");
            sb.append(toLowerCamelCase(fragment.getPresentation()));
            sb.append("}");
          } else {
            sb.append(CharEscapers.javascriptEscaper().escape(fragment.getPresentation()));
          }
        }
        sb.append('"');
        Iterator<Placeholder> phIter = placeholders.iterator();
        if (phIter.hasNext()) {
          sb.append(", {");
          appendLine(msg.getSourcePosition(), sb);
          sb = new StringBuilder();
          while(phIter.hasNext()) {
            Placeholder placeholder = phIter.next();
            sb.append(JAVASCRIPT.toStringLiteral(toLowerCamelCase(placeholder.getPresentation())));
            sb.append(": ");
            sb.append(evalPlaceholder(placeholder.getOriginal(), paramVar));
            if (phIter.hasNext()) {
              sb.append(",");
            }
            appendLine(msg.getSourcePosition(), sb);
            sb = new StringBuilder();
          }
          sb.append("}");
        }
        sb.append(");");
        appendLine(msg.getSourcePosition(), sb);
        writeExpression(msg.getSourcePosition(), msgVar);
        return null;
      }

      private final Pattern PARAM_PATTERN = Pattern.compile("%([1-9%])");

      private String evalPlaceholder(String original, String paramVar) {
        List<String> parts = Lists.newArrayList();
        Matcher m = PARAM_PATTERN.matcher(original);
        int cur = 0;
        while (m.find(cur)) {
          if (cur != m.start()) {
            parts.add(JAVASCRIPT.toStringLiteral(original.substring(cur, m.start())));
          }
          char ch = original.charAt(m.start() + 1);
          if (m.group(1).equals("%")) {
            parts.add("'%'");
          } else {
            parts.add(paramVar + "[" + (Integer.parseInt(m.group(1)) - 1) + "]");
          }
          cur = m.end();
        }
        if (cur != original.length()) {
          parts.add(JAVASCRIPT.toStringLiteral(original.substring(cur, original.length())));
        }

        return Joiner.on("+").join(parts);
      }

      @Override
      public Void visitLoopExpression(LoopExpression loop) {
        // JS Loops require an iterable
        if (loop.getIterable() == null) {
          // this is kind of wonky, but even though we don't support JS Loops with
          // iterators, if they do provide one we still do expression validation.  It
          // makes our unit tests a bit cleaner, and there's no real harm
          if (loop.getIterator() != null && loop.getIterator().canEvaluateAs(JAVASCRIPT)) {
            getJavaScriptExpression(loop.getIterator());
          }
          alertSink.add(new LoopRequiresIterableInJavaScriptError(loop));
          return null;
        }

        String keyVar = (loop.getKey() == null)
            ? createVarName("key")
            : JAVASCRIPT.validateName(alertSink, loop, loop.getKey());

        formatLine(loop.getSourcePosition(),
                   "goog.gxp.base.forEach(%s, function(%s, %s, gxp$isFirst) {",
                   getJavaScriptExpression(loop.getIterable()), keyVar,
                   JAVASCRIPT.validateName(alertSink, loop, loop.getVar()));
        if (!loop.getDelimiter().alwaysEmpty()) {
          appendLine("if (!gxp$isFirst) {");
          loop.getDelimiter().acceptVisitor(this);
          appendLine("}");
        }
        loop.getSubexpression().acceptVisitor(this);
        appendLine("});");
        return null;
      }

      @Override
      public Void visitStringConstant(StringConstant value) {
        if (value.getSchema() == null) {
          throw new AssertionError();
        }
        writeString(value.getSourcePosition(), value.evaluate());
        return null;
      }

      ////////////////////////////////////////////////////////////////////////////////
      // Call Visitors
      ////////////////////////////////////////////////////////////////////////////////

      @Override
      public Void visitCall(Call value) {
        return value.acceptCallVisitor(this);
      }

      public Void visitUnboundCall(UnboundCall call) {
        throw new UnexpectedNodeException(call);
      }

      public Void visitBoundCall(BoundCall call) {
        throw new UnexpectedNodeException(call);
      }

      public Void visitValidatedCall(final ValidatedCall call) {
        final Callable callee = call.getCallee();
        final Map<String, Attribute> params = call.getAttributes();
        final StringBuilder sb = new StringBuilder();

        boolean isInstance = callee.acceptCallableVisitor(new CallableVisitor<Boolean>() {
          public Boolean visitCallable(Callable callable) {
            return false;
          }

          public Boolean visitInstanceCallable(InstanceCallable callable) {
            instantiatedGxps.push(createVarName("inst"));
            Attribute thisAttr = params.get(Implementable.INSTANCE_PARAM_NAME);

            appendLine("{");
            if (thisAttr != null) {
              formatLine(call.getSourcePosition(), "var %s = %s;",
                         instantiatedGxps.peek(),
                         thisAttr.getValue().acceptVisitor(toExpressionVisitor));
            }
            return true;
          }
        });

        sb.append(getCalleeName(callee));
        sb.append(".write(");
        sb.append(GXP_SIG);
        for (String param : getCallArguments(callee, params)) {
          sb.append(", ");
          sb.append(param);
        }
        sb.append(");");
        appendLine(call.getSourcePosition(), sb);

        if (isInstance) {
          instantiatedGxps.pop();
          appendLine("}");
        }
        return null;
      }
    }

    protected String getJavaScriptExpression(Expression value) {
      return value.acceptVisitor(toExpressionVisitor);
    }

    protected String getEscapedString(Expression value) {
      StringBuilder sb = new StringBuilder();
      sb.append(getJavaScriptExpression(value));
      sb.append(".");
      sb.append(getWriteMethodName(value.getSchema()));
      sb.append("(new goog.string.StringBuffer(), ");
      sb.append(GXP_CONTEXT_VAR);
      sb.append(").toString()");
      return sb.toString();
    }

    protected Function<Expression, String> expressionToEscapedString =
      new Function<Expression, String>() {
        public String apply(Expression value) {
          return getEscapedString(value);
        }
      };

    private String getEscapableExpression(Expression value) {
      return value.acceptVisitor(toEscapableExpressionVisitor);
    }

    /**
     * Converts an Expression into a JavaScript expression that evaluates to
     * a String containing that Expression's value. This differs from
     * ToExpressionVisitor in that ToExpressionVisitor will sometimes return
     * closure types (eg: HtmlClosure), rather than Strings. Note that this is
     * only ever called on Expressions that are the child of an
     * EscapeExpression, so only types that can appear as the child of an
     * EscapeExpression need to be handled.
     */
    protected class ToEscapableExpressionVisitor
        extends DefaultingExpressionVisitor<String>
        implements CallVisitor<String> {

      @Override
      public String defaultVisitExpression(Expression value) {
        throw new UnexpectedNodeException(value);
      }

      ////////////////////////////////////////////////////////////////////////////////
      // Expression Visitors
      ////////////////////////////////////////////////////////////////////////////////

      @Override
      public String visitAttrBundleReference(AttrBundleReference value) {
        return value.getName();
      }

      @Override
      public String visitEscapeExpression(EscapeExpression value) {
        StringBuilder sb = new StringBuilder();
        sb.append(getEscapableExpression(value.getSubexpression()));
        sb.append(".");
        sb.append(getWriteMethodName(value.getSchema()));
        sb.append("(new goog.string.StringBuffer(), ");
        sb.append(GXP_CONTEXT_VAR);
        sb.append(").toString()");

        return sb.toString();
      }

      @Override
      public String visitExtractedMessage(ExtractedMessage msg) {
        return getEscapedString(msg);
      }

      @Override
      public String visitNativeExpression(NativeExpression value) {
        StringBuilder sb = new StringBuilder();
        sb.append('(');
        sb.append(JAVASCRIPT.validateExpression(alertSink, value));
        sb.append(')');
        return sb.toString();
      }

      ////////////////////////////////////////////////////////////////////////////////
      // Call Visitors
      ////////////////////////////////////////////////////////////////////////////////

      @Override
      public String visitCall(Call call) {
        return call.acceptCallVisitor(this);
      }

      @Override
      public String visitBoundCall(BoundCall call) {
        throw new UnexpectedNodeException(call);
      }

      @Override
      public String visitUnboundCall(UnboundCall call) {
        throw new UnexpectedNodeException(call);
      }

      @Override
      public String visitValidatedCall(ValidatedCall call) {
        StringBuilder sb = new StringBuilder(GXP_CONTEXT_VAR);
        sb.append(".getString(");
        sb.append(getJavaScriptExpression(call));
        sb.append(".");
        sb.append(getWriteMethodName(call.getSchema()));
        sb.append(")");
        return sb.toString();
      }
    }

    private class ToExpressionVisitor
        extends DefaultingExpressionVisitor<String>
        implements CallVisitor<String> {

      @Override
      public String defaultVisitExpression(Expression value) {
        throw new UnexpectedNodeException(value);
      }

      ////////////////////////////////////////////////////////////////////////////////
      // Expression Visitors
      ////////////////////////////////////////////////////////////////////////////////

      @Override
      public String visitAbbrExpression(AbbrExpression value) {
        return toAnonymousClosure(value);
      }

      @Override
      public String visitAttrBundleParam(AttrBundleParam bundle) {
        // optimization where a single bundle is being passed without
        // restrictions to another.  Just pass the bundle along without
        // using a GxpAttrBundle.Builder
        if (bundle.getIncludeAttrs().isEmpty() && bundle.getAttrs().isEmpty()
            && bundle.getSubBundles().size() == 1) {
          return bundle.getSubBundles().get(0);
        }

        StringBuilder sb = new StringBuilder("new goog.gxp.base.GxpAttrBundle.Builder(");
        sb.append('"');
        sb.append(getWriteMethodName(bundle.getSchema()));
        sb.append('"');
        for (String includeAttr : bundle.getIncludeAttrs()) {
          sb.append(", ");
          sb.append(JAVASCRIPT.toStringLiteral(includeAttr));
        }
        sb.append(")");
        for (Map.Entry<AttributeValidator, Attribute> entry : bundle.getAttrs().entrySet()) {
          AttributeValidator validator = entry.getKey();
          Expression condition = entry.getValue().getCondition();
          Expression value = entry.getValue().getValue();

          sb.append(".attr(");
          sb.append(JAVASCRIPT.toStringLiteral(validator.getName()));
          sb.append(", ");
          sb.append(validator.isFlagSet(AttributeValidator.Flag.BOOLEAN)
                      ? value.acceptVisitor(this)
                      : toAnonymousClosure(value));
          if (condition != null) {
            sb.append(", ");
            sb.append(condition.acceptVisitor(this));
          }
          sb.append(")");
        }
        for (String subBundle : bundle.getSubBundles()) {
          sb.append(".addBundle(");
          sb.append(subBundle);
          sb.append(")");
        }
        sb.append(".build()");
        return sb.toString();
      }

      @Override
      public String visitBooleanConstant(BooleanConstant value) {
        return value.getValue().toString();
      }

      @Override
      public String visitConcatenation(Concatenation value) {
        return toAnonymousClosure(value);
      }

      @Override
      public String visitConditional(Conditional value) {
        return toAnonymousClosure(value);
      }

      @Override
      public String visitConstructedConstant(ConstructedConstant node) {
        StringBuilder sb = new StringBuilder();
        sb.append(getCalleeName(node.getCallee()));
        sb.append(".");
        sb.append(getConstructorMethodName(node.getParam()));
        sb.append("(\"");
        sb.append(node.getValue());
        sb.append("\")");

        return sb.toString();
      }

      @Override
      public String visitConvertibleToContent(ConvertibleToContent value) {
        return toAnonymousClosure(value);
      }

      @Override
      public String visitEscapeExpression(EscapeExpression value) {
        return value.getSubexpression().acceptVisitor(this);
      }

      @Override
      public String visitExampleExpression(ExampleExpression value) {
        return value.getSubexpression().acceptVisitor(this);
      }

      @Override
      public String visitExtractedMessage(ExtractedMessage msg) {
        return toAnonymousClosure(msg);
      }

      @Override
      public String visitLoopExpression(LoopExpression value) {
        return toAnonymousClosure(value);
      }

      @Override
      public String visitIsXmlExpression(IsXmlExpression ixe) {
        return GXP_CONTEXT_VAR + ".isForcingXmlSyntax()";
      }

      @Override
      public String visitNativeExpression(NativeExpression value) {
        return JAVASCRIPT.validateExpression(alertSink, value);
      }

      @Override
      public String visitObjectConstant(ObjectConstant node) {
        return JAVASCRIPT.toStringLiteral(node.getValue());
      }

      @Override
      public String visitStringConstant(StringConstant value) {
        if (value.getSchema() == null) {
          throw new AssertionError();
        }
        return value.getSchema().getContentFamily().acceptVisitor(STRING_CONSTANT_VISITOR, value);
      }

      private ContentFamilyVisitor<StringConstant, String>
        STRING_CONSTANT_VISITOR =
        new ContentFamilyVisitor<StringConstant, String>() {
          public String visitCss(StringConstant value) {
            return toAnonymousClosure(value);
          }

          public String visitJavaScript(StringConstant value) {
            return toAnonymousClosure(value);
          }

          public String visitMarkup(StringConstant value) {
            return toAnonymousClosure(value);
          }

          public String visitPlaintext(StringConstant value) {
            return JAVASCRIPT.toStringLiteral(value.evaluate());
          }
        };

      ////////////////////////////////////////////////////////////////////////////////
      // Call Visitors
      ////////////////////////////////////////////////////////////////////////////////

      @Override
      public String visitCall(Call value) {
        return value.acceptCallVisitor(this);
      }

      @Override
      public String visitBoundCall(BoundCall call) {
        throw new UnexpectedNodeException(call);
      }

      @Override
      public String visitUnboundCall(UnboundCall call) {
        throw new UnexpectedNodeException(call);
      }

      @Override
      public String visitValidatedCall(final ValidatedCall call) {
        final Callable callee = call.getCallee();
        final StringBuilder sb = new StringBuilder();

        callee.acceptCallableVisitor(new CallableVisitor<Void>() {
          public Void visitCallable(Callable callable) {
            sb.append(callee.getName().toString());
            sb.append(".getGxpClosure(");
            COMMA_JOINER.appendTo(sb, getCallArguments(callee, call.getAttributes()));
            sb.append(")");
            return null;
          }

          public Void visitInstanceCallable(InstanceCallable callable) {
            // we can't use getGxpClosure in the instance call case, because we have to
            // return a single expression, so we can't set an inst variable like we
            // do in StatementVisitor#visitValidatedCall
            sb.append(toAnonymousClosure(call));
            return null;
          }
        });

        return sb.toString();
      }
    }

    private String toAnonymousClosure(Expression value) {
      StringBuilder sb = new StringBuilder();
      TemplateWorker subWorker = createSubWorker(sb);
      subWorker.toAnonymousClosureImpl(value);
      sb.append("})");
      return sb.toString();
    }

    private void toAnonymousClosureImpl(Expression value) {
      formatLine(value.getSourcePosition(), "new %s(function(%s) {",
                 value.getSchema().getJavaScriptType(), GXP_SIG);

      value.acceptVisitor(statementVisitor);
      formatLine("return %s;", GXP_OUT_VAR);
    }
  }
}
TOP

Related Classes of com.google.gxp.compiler.js.JavaScriptCodeGenerator$TemplateWorker$ToEscapableExpressionVisitor

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.