Package com.google.template.soy.soytree

Source Code of com.google.template.soy.soytree.AbstractMsgNode

/*
* Copyright 2011 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.template.soy.soytree;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.template.soy.base.BaseUtils;
import com.google.template.soy.base.SoySyntaxException;
import com.google.template.soy.exprtree.DataRefAccessKeyNode;
import com.google.template.soy.exprtree.DataRefNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.GlobalNode;
import com.google.template.soy.soytree.CommandTextAttributesParser.Attribute;
import com.google.template.soy.soytree.SoyNode.MsgBlockNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
import java.util.Map;


/**
* Abstract node representing a 'msg' block. Each child must be a RawTextNode, MsgPlaceholderNode,
* MsgPluralNode, or MsgSelectNode.
*
* <p> Important: Do not use outside of Soy code (treat as superpackage-private).
*
* @author Kai Huang
*/
public abstract class AbstractMsgNode extends AbstractBlockCommandNode
    implements StandaloneNode, MsgBlockNode {


  /** Parser for the command text. */
  private static final CommandTextAttributesParser ATTRIBUTES_PARSER =
      new CommandTextAttributesParser("msg",
          new Attribute("meaning", Attribute.ALLOW_ALL_VALUES, null),
          new Attribute("desc", Attribute.ALLOW_ALL_VALUES,
                        Attribute.NO_DEFAULT_VALUE_BECAUSE_REQUIRED),
          new Attribute("hidden", Attribute.BOOLEAN_VALUES, "false"));


  /** We don't support different content types. Always provide this value to the TC. */
  private static final String DEFAULT_CONTENT_TYPE = "text/html";


  /** The meaning string if set, otherwise null (usually null). */
  private final String meaning;

  /** The description string for translators. */
  private final String desc;

  /** Whether the message should be added as 'hidden' in the TC. */
  private final boolean isHidden;

  /** The generated map from placeholder name to representative node, or null if not generated. */
  private Map<String, MsgPlaceholderNode> phNameToRepNodeMap = null;

  /** The generated map from placeholder node to placeholder name, or null if not generated. */
  private Map<MsgPlaceholderNode, String> phNodeToNameMap = null;

  /** The generated map from plural var name to rep node, or null if not generated. */
  private Map<String, MsgPluralNode> pluralVarNameToRepNodeMap = null;

  /** The generated map from plural node to plural var name, or null if not generated. */
  private Map<MsgPluralNode, String> pluralNodeToVarNameMap = null;

  /** The generated map from select var name to rep node, or null if not generated. */
  private Map<String, MsgSelectNode> selectVarNameToRepNodeMap = null;

  /** The generated map from select node to select var name, or null if not generated. */
  private Map<MsgSelectNode, String> selectNodeToVarNameMap = null;


  /**
   * @param id The id for this node.
   * @param commandText The command text.
   * @throws SoySyntaxException If a syntax error is found.
   */
  public AbstractMsgNode(int id, String commandText) throws SoySyntaxException {
    super(id, "msg", commandText);

    Map<String, String> attributes = ATTRIBUTES_PARSER.parse(commandText);
    meaning = attributes.get("meaning");
    desc = attributes.get("desc");
    isHidden = attributes.get("hidden").equals("true");
  }


  /**
   * Constructor that copies most fields (except id). This constructor assumes children nodes will
   * not change, so we simply reuse the existing internal maps with pointers to the original
   * children. This constructor should be used when creating a GoogMsgNode from a MsgNode.
   * @param id The id for this node.
   * @param orig The original node from which to derive this node.
   * @see #AbstractMsgNode(AbstractMsgNode)
   */
  protected AbstractMsgNode(int id, AbstractMsgNode orig) {
    super(id, "msg", orig.getCommandText());
    this.meaning = orig.meaning;
    this.desc = orig.desc;
    this.isHidden = orig.isHidden;
    this.phNameToRepNodeMap =
        (orig.phNameToRepNodeMap != null) ? ImmutableMap.copyOf(orig.phNameToRepNodeMap) : null;
    this.phNodeToNameMap =
        (orig.phNodeToNameMap != null) ? ImmutableMap.copyOf(orig.phNodeToNameMap) : null;
    this.pluralVarNameToRepNodeMap =
        (orig.pluralVarNameToRepNodeMap != null) ?
            ImmutableMap.copyOf(orig.pluralVarNameToRepNodeMap) : null;
    this.pluralNodeToVarNameMap =
        (orig.pluralNodeToVarNameMap != null) ?
            ImmutableMap.copyOf(orig.pluralNodeToVarNameMap) : null;
    this.selectVarNameToRepNodeMap =
        (orig.selectVarNameToRepNodeMap != null) ?
            ImmutableMap.copyOf(orig.selectVarNameToRepNodeMap) : null;
    this.selectNodeToVarNameMap =
        (orig.selectNodeToVarNameMap != null) ?
            ImmutableMap.copyOf(orig.selectNodeToVarNameMap) : null;
  }


  /**
   * Copy constructor. This constructor clones the children nodes and regenerates all the internal
   * maps with pointers to the new children. This constructor should be used when cloning.
   * @param orig The node to copy.
   */
  protected AbstractMsgNode(AbstractMsgNode orig) {
    super(orig);
    this.meaning = orig.meaning;
    this.desc = orig.desc;
    this.isHidden = orig.isHidden;
    // The only reason we don't run genPhNamesAndSelectPluralVarsHelper from the other constructors
    // is because the children haven't been added yet. But for cloning, the children already exist,
    // so there's no reason not to run genPhNamesAndSelectPluralVarsHelper now.
    genPhNamesAndSelectPluralVarsHelper();
  }


  /** Returns the meaning string if set, otherwise null (usually null). */
  public String getMeaning() {
    return meaning;
  }

  /** Returns the description string for translators. */
  public String getDesc() {
    return desc;
  }

  /** Returns whether the message should be added as 'hidden' in the TC. */
  public boolean isHidden() {
    return isHidden;
  }

  /** Returns the content type for the TC. */
  public String getContentType() {
    return DEFAULT_CONTENT_TYPE;
  }


  /** Returns whether this is a plural or select message. */
  public boolean isPlrselMsg() {
    return getChildren().size() == 1 &&
        (getChild(0) instanceof MsgPluralNode || getChild(0) instanceof MsgSelectNode);
  }


  /**
   * Gets the representative placeholder node for a given placeholder name.
   * @param placeholderName The placeholder name.
   * @return The representative placeholder node for the given placeholder name.
   */
  public MsgPlaceholderNode getRepPlaceholderNode(String placeholderName) {
    if (phNameToRepNodeMap == null) {
      genPhNamesAndSelectPluralVarsHelper();
    }
    return phNameToRepNodeMap.get(placeholderName);
  }


  /**
   * Gets the placeholder name for a given placeholder node.
   * @param placeholderNode The placeholder node.
   * @return The placeholder name for the given placeholder node.
   */
  public String getPlaceholderName(MsgPlaceholderNode placeholderNode) {
    if (phNodeToNameMap == null) {
      genPhNamesAndSelectPluralVarsHelper();
    }
    return phNodeToNameMap.get(placeholderNode);
  }


  /**
   * Gets the representative plural node for a given plural variable name.
   * @param pluralVarName The plural variable name.
   * @return The representative plural node for the given plural variable name.
   */
  public MsgPluralNode getRepPluralNode(String pluralVarName) {
    if (pluralVarNameToRepNodeMap == null) {
      genPhNamesAndSelectPluralVarsHelper();
    }
    return pluralVarNameToRepNodeMap.get(pluralVarName);
  }


  /**
   * Gets the variable name associated with a given plural node.
   * @param pluralNode The plural node.
   * @return The plural variable name for the given plural node.
   */
  public String getPluralVarName(MsgPluralNode pluralNode) {
    if (pluralNodeToVarNameMap == null) {
      genPhNamesAndSelectPluralVarsHelper();
    }
    return pluralNodeToVarNameMap.get(pluralNode);
  }


  /**
   * Gets the representative select node for a given select variable name.
   * @param selectVarName The select variable name.
   * @return The representative select node for the given select variable name.
   */
  public MsgSelectNode getRepSelectNode(String selectVarName) {
    if (selectVarNameToRepNodeMap == null) {
      genPhNamesAndSelectPluralVarsHelper();
    }
    return selectVarNameToRepNodeMap.get(selectVarName);
  }


  /**
   * Gets the variable name associated with a given select node.
   * @param selectNode The select node.
   * @return The select variable name for the given select node.
   */
  public String getSelectVarName(MsgSelectNode selectNode) {
    if (selectNodeToVarNameMap == null) {
      genPhNamesAndSelectPluralVarsHelper();
    }
    return selectNodeToVarNameMap.get(selectNode);
  }


  /*
   * Helper function to generate internal maps with details of
   * placeholders, select variables and plural variables and the
   * corresponding nodes.  This should be called before retrieving
   * data from any of the internal maps.
   *
   * This builds the following maps for each of placeholders, plurals and selects.
   * <li>Map from name or var name to the representative node.  There is only
   *     one rep node corresponding to a given name.  There may be many nodes
   *     with exactly same var name and source string, but only one will be
   *     mapped here. If the source strings are different, the var names will be
   *     different.
   * <li>Map from a node to its name or var name.  All nodes are included in this
   *     map. There may be many nodes that map to the same name or var name.
   *
   * It is guaranteed that:
   * <li>A plural var name, a select var name and placeholder name will never
   *     be the same in the same message, even if they have identical source
   *     strings.
   * <li>The plural/select var name of a node will never be the same as the
   *     plural/select var name of another node with a different source string.
   *     If the source strings are identical, the var names also will be
   *     identical.
   * <li>The name of a placeholder will never be the same as another placeholder
   *     with a different source string.  If the source strings are the same,
   *     the placeholder names also will be the same.
   */
  @SuppressWarnings("SuspiciousMethodCalls")
  protected void genPhNamesAndSelectPluralVarsHelper() {

    // ------ Step 1: Determine representative nodes and build preliminary map ------
    //
    // Specifically, we are building (base name) -> (list of RepNodes) map for
    // placeholders, select nodes and plural nodes.
    //
    // If there are multiple nodes in the message that are exactly the same, then the
    // first such node encountered becomes the "representative node" for the group ("RepNode" in
    // variable names). The rest of the same nodes are non-representative ("nonRepNode").
    //
    // The (base name) -> (list of RepNodes) map is from base name to the list of
    // representative nodes (not exactly same) that all generate the same base name. If we
    // encounter a non-representative node, then we insert it into nonRepNodeToRepNodeMap, mapping
    // it to its corresponding representative node.
    //
    // The (base name) -> (list of RepNodes) map is preliminary because some of the final
    // names will be the base names plus some unique suffix.

    Map<String, List<SoyNode>> baseNameToRepNodesMap = Maps.newHashMap();
    Map<SoyNode, SoyNode> nonRepNodeToRepNodeMap = Maps.newHashMap();

    Deque<SoyNode> traversalQueue = new ArrayDeque<SoyNode>();
    // Seed the traversal queue with the children of this MsgNode.
    for (SoyNode child : this.getChildren()) {
      if (child instanceof MsgPlaceholderNode || child instanceof MsgPluralNode ||
          child instanceof MsgSelectNode) {
        traversalQueue.add(child);
      }
    }

    while (traversalQueue.size() > 0) {
      SoyNode node = traversalQueue.remove();
      String baseName;
      if (node instanceof MsgSelectNode)  {
        addGrandchildrenToQueue(traversalQueue, (MsgSelectNode) node);
        baseName = genBaseNameFromExpr(((MsgSelectNode) node).getExpr(), "STATUS");
      } else if (node instanceof MsgPluralNode) {
        addGrandchildrenToQueue(traversalQueue, (MsgPluralNode) node);
        baseName = genBaseNameFromExpr(((MsgPluralNode) node).getExpr(), "NUM");
      } else if (node instanceof MsgPlaceholderNode) {
        baseName = ((MsgPlaceholderNode) node).genBasePlaceholderName();
      } else {
        throw new AssertionError();
      }
      if (!baseNameToRepNodesMap.containsKey(baseName)) {
        // Case 1: First occurrence of this base name.
        baseNameToRepNodesMap.put(baseName, Lists.newArrayList(node));
      } else {
        List<SoyNode> nodesWithSameBaseName = baseNameToRepNodesMap.get(baseName);
        boolean isNew = true;
        for (SoyNode other : nodesWithSameBaseName) {
          if (isSameAs(node, other)) {
            // Case 2: Exactly same as another node we've seen.
            nonRepNodeToRepNodeMap.put(node, other);
            isNew = false;
            break;
          }
        }
        if (isNew) {
          // Case 3: New representative node that has the same base name as another
          // node we've seen, but is not the same node.
          nodesWithSameBaseName.add(node);
        }
      }
    }

    // ------ Step 2: Build final maps of name to representative node ------
    //
    // The final map *NameToRepNodeMap must be a one-to-one mapping. If a base name
    // only maps to one representative node, then we simply put that same mapping into the final
    // map. But if a base name maps to multiple nodes, we must append number suffixes
    // ("_1", "_2", etc) to make the names unique.
    //
    // Note: We must be careful that, while appending number suffixes, we don't generate a new
    // name that is the same as an existing base name.

    phNameToRepNodeMap = Maps.newHashMap();
    pluralVarNameToRepNodeMap = Maps.newHashMap();
    selectVarNameToRepNodeMap = Maps.newHashMap();

    for (Map.Entry<String, List<SoyNode>> entry : baseNameToRepNodesMap.entrySet()) {
      String baseName = entry.getKey();
      List<SoyNode> nodesWithSameBaseName = entry.getValue();
      if (nodesWithSameBaseName.size() == 1) {
        updateFinalMapsWithNode(baseName, nodesWithSameBaseName.get(0));
      } else {
        // Case 2: Multiple nodes generate this base name. Need number suffixes.
        int nextSuffix = 1;
        for (SoyNode repNode : nodesWithSameBaseName) {
          String newName;
          do {
            newName = baseName + "_" + nextSuffix;
            ++nextSuffix;
          } while (baseNameToRepNodesMap.containsKey(newName));
          updateFinalMapsWithNode(newName, repNode);
        }
      }
    }

    // ------ Step 3: Create maps of every node to its name ------

    // Reverse the maps of names to representative nodes.
    phNodeToNameMap = Maps.newHashMap();
    for (Map.Entry<String, MsgPlaceholderNode> entry : phNameToRepNodeMap.entrySet()) {
      phNodeToNameMap.put(entry.getValue(), entry.getKey());
    }

    selectNodeToVarNameMap = Maps.newHashMap();
    for (Map.Entry<String, MsgSelectNode> entry : selectVarNameToRepNodeMap.entrySet()) {
      selectNodeToVarNameMap.put(entry.getValue(), entry.getKey());
    }

    pluralNodeToVarNameMap = Maps.newHashMap();
    for (Map.Entry<String, MsgPluralNode> entry : pluralVarNameToRepNodeMap.entrySet()) {
      pluralNodeToVarNameMap.put(entry.getValue(), entry.getKey());
    }

    // Add mappings for the non-representative nodes.
    for (Map.Entry<SoyNode, SoyNode> entry : nonRepNodeToRepNodeMap.entrySet()) {
      SoyNode nonRepNode = entry.getKey();
      SoyNode repNode = entry.getValue();
      if (nonRepNode instanceof MsgPlaceholderNode) {
        phNodeToNameMap.put(
            (MsgPlaceholderNode) nonRepNode, phNodeToNameMap.get(repNode));
      } else if (nonRepNode instanceof MsgSelectNode) {
        selectNodeToVarNameMap.put(
            (MsgSelectNode) nonRepNode, selectNodeToVarNameMap.get(repNode));
      } else if (nonRepNode instanceof MsgPluralNode) {
        pluralNodeToVarNameMap.put(
            (MsgPluralNode) nonRepNode, pluralNodeToVarNameMap.get(repNode));
      }
    }
  }


  /**
   * Helper function to add a parent node and its children to the traversal queue.
   * @param traversalQueue The traversal queue.
   * @param selectOrPluralNode The parent node.
   */
  private static void addGrandchildrenToQueue(
      Deque<SoyNode> traversalQueue, ParentSoyNode<CaseOrDefaultNode> selectOrPluralNode) {
    for (CaseOrDefaultNode child : selectOrPluralNode.getChildren()) {
      for (SoyNode grandchild : child.getChildren()) {
        if (grandchild instanceof MsgPlaceholderNode || grandchild instanceof MsgPluralNode ||
            grandchild instanceof MsgSelectNode) {
          traversalQueue.add(grandchild);
        }
      }
    }
  }


  /**
   * Helper function to update the appropriate map with a (baseName, node) info.
   * @param baseName The base name.
   * @param node The node.
   */
  private void updateFinalMapsWithNode(String baseName, SoyNode node) {
    if (node instanceof MsgPlaceholderNode) {
      phNameToRepNodeMap.put(baseName, (MsgPlaceholderNode) node);
    } else if (node instanceof MsgSelectNode) {
      selectVarNameToRepNodeMap.put(baseName, (MsgSelectNode) node);
    } else if (node instanceof MsgPluralNode){
      pluralVarNameToRepNodeMap.put(baseName, (MsgPluralNode) node);
    }
  }


  /**
   * Helper function to determine whether two nodes are equivalent.
   * @param node The first node.
   * @param otherNode The second node.
   * @return true if they contain the same data, false otherwise.
   */
  private static boolean isSameAs(SoyNode node, SoyNode otherNode) {
    if ((node instanceof MsgPlaceholderNode) && (otherNode instanceof MsgPlaceholderNode)) {
      return ((MsgPlaceholderNode) node).isSamePlaceholderAs((MsgPlaceholderNode) otherNode);
    } else if ((node instanceof MsgPluralNode) && (otherNode instanceof MsgPluralNode)) {
      return (((MsgPluralNode) node).getCommandText().equals(
          ((MsgPluralNode) otherNode).getCommandText()));
    } else if ((node instanceof MsgSelectNode) && (otherNode instanceof MsgSelectNode)) {
      return (((MsgSelectNode) node).getCommandText().equals(
          ((MsgSelectNode) otherNode).getCommandText()));
    } else {
      return false;
    }
  }


  @Override public BlockNode getParent() {
    return (BlockNode) super.getParent();
  }


  // -----------------------------------------------------------------------------------------------
  // Static helpers that other nodes can use.


  /**
   * Helper function to get the base placeholder (or plural/select var) name from an expression.
   *
   * If the expression is a data ref or global, then the last key (if any) is used as the base
   * placeholder name. Otherwise, the fallback name is used.
   *
   * For example,
   *   $foo -> FOO
   *   $foo.0 -> fallback
   *   $foo[0] -> fallback
   *   $foo.0.bar -> BAR
   *   $fooBar -> FOO_BAR
   *   $foo_bar -> FOO_BAR
   *   $foo.barBaz -> BAR_BAZ
   *   foo.BAR_BAZ -> BAR_BAZ
   *   min($foo, 4) -> fallback
   *
   * @param exprRoot The root node for an expression.
   * @param fallbackBaseName The fallback base name.
   * @return The base placeholder (or plural/select var) name for the given expression.
   */
  static String genBaseNameFromExpr(ExprRootNode<?> exprRoot, String fallbackBaseName) {

    ExprNode exprNode = exprRoot.getChild(0);

    if (exprNode instanceof DataRefNode) {
      DataRefNode dataRefNode = (DataRefNode) exprNode;

      if (dataRefNode.numChildren() > 0) {
        ExprNode lastChild = dataRefNode.getChild(dataRefNode.numChildren() - 1);
        // Only handle if last child is a key. Else, fall through.
        if (lastChild instanceof DataRefAccessKeyNode) {
          return BaseUtils.convertToUpperUnderscore(((DataRefAccessKeyNode) lastChild).getKey());
        }
      } else {
        // No children, so the first key is the last key.
        return BaseUtils.convertToUpperUnderscore(dataRefNode.getFirstKey());
      }

    } else if (exprNode instanceof GlobalNode) {
      String globalName = ((GlobalNode) exprNode).getName();
      return BaseUtils.convertToUpperUnderscore(BaseUtils.extractPartAfterLastDot(globalName));
    }

    return fallbackBaseName;
  }

}
TOP

Related Classes of com.google.template.soy.soytree.AbstractMsgNode

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.