/*
* Copyright 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.template.soy.soytree;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.LinkedListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.basetree.CopyState;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.exprparse.ExpressionParser;
import com.google.template.soy.exprparse.SoyParsingContext;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.internal.base.Pair;
import com.google.template.soy.soytree.CommandTextAttributesParser.Attribute;
import com.google.template.soy.soytree.SoyNode.ExprHolderNode;
import com.google.template.soy.soytree.SoyNode.MsgBlockNode;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Node representing a 'msg' block. Every child must be a RawTextNode, MsgPlaceholderNode,
* MsgPluralNode, or MsgSelectNode.
*
* <p>The AST will be one of the following
*
* <ul>
* <li>A single {@link RawTextNode}
* <li>A mix of {@link RawTextNode} and {@link MsgPlaceholderNode}
* <li>A single {@link MsgPluralNode}
* <li>A single {@link MsgSelectNode}
* </ul>
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
*/
public final class MsgNode extends AbstractBlockCommandNode
implements ExprHolderNode, MsgBlockNode {
static final SoyErrorKind WRONG_NUMBER_OF_GENDER_EXPRS =
SoyErrorKind.of("Attribute ''genders'' does not contain 1-3 expressions");
/**
* Returns a new {@link Builder} representing a {@code msg} MsgNode.
*
* @param id The node's id.
* @param commandText The node's command text.
* @param sourceLocation The node's source location.
*/
public static Builder msg(int id, String commandText, SourceLocation sourceLocation) {
return new Builder(id, "msg", commandText, sourceLocation);
}
/**
* Returns a new {@link Builder} representing a {@code fallbackmsg} MsgNode.
*
* @param id The node's id.
* @param commandText The node's command text.
* @param sourceLocation The node's source location.
*/
public static Builder fallbackmsg(int id, String commandText, SourceLocation sourceLocation) {
return new Builder(id, "fallbackmsg", commandText, sourceLocation);
}
private static class SubstUnitInfo {
/**
* The generated map from substitution unit var name to representative node.
*
* <p>There is only one rep node for each var name. There may be multiple nodes with the same
* var name, but only one will be mapped here. If two nodes should not have the same var name,
* then their var names will be different, even if their base var names are the same.
*/
public final ImmutableMap<String, MsgSubstUnitNode> varNameToRepNodeMap;
/**
* The generated map from substitution unit node to var name.
*
* <p>There may be multiple nodes that map to the same var name.
*/
public final ImmutableMap<MsgSubstUnitNode, String> nodeToVarNameMap;
public SubstUnitInfo(
ImmutableMap<String, MsgSubstUnitNode> varNameToRepNodeMap,
ImmutableMap<MsgSubstUnitNode, String> nodeToVarNameMap) {
this.varNameToRepNodeMap = varNameToRepNodeMap;
this.nodeToVarNameMap = nodeToVarNameMap;
}
}
/** Parser for the command text. */
private static final CommandTextAttributesParser ATTRIBUTES_PARSER =
new CommandTextAttributesParser(
"msg",
new Attribute("genders", Attribute.ALLOW_ALL_VALUES, null),
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 use different content types. It may be a historical artifact in the TC. */
private static final String DEFAULT_CONTENT_TYPE = "text/html";
/** The list of expressions for gender values. Null after rewriting. */
@Nullable private List<ExprRootNode> genderExprs;
/** 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 substitution unit info (var name mappings, or null if not yet generated. */
private SubstUnitInfo substUnitInfo = null;
private MsgNode(
int id,
SourceLocation sourceLocation,
@Nullable List<ExprRootNode> genderExprs,
String commandName,
String commandText,
String meaning,
String desc,
boolean isHidden) {
super(id, sourceLocation, commandName, commandText);
this.genderExprs = genderExprs;
this.meaning = meaning;
this.desc = desc;
this.isHidden = isHidden;
}
/**
* Copy constructor.
*
* @param orig The node to copy.
*/
private MsgNode(MsgNode orig, CopyState copyState) {
super(orig, copyState);
if (orig.genderExprs != null) {
ImmutableList.Builder<ExprRootNode> builder = ImmutableList.builder();
for (ExprRootNode node : orig.genderExprs) {
builder.add(node.copy(copyState));
}
this.genderExprs = builder.build();
} else {
this.genderExprs = null;
}
this.meaning = orig.meaning;
this.desc = orig.desc;
this.isHidden = orig.isHidden;
// The only reason we don't run genSubstUnitInfo 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 genSubstUnitInfo now.
substUnitInfo = genSubstUnitInfo(this);
}
@Override
public Kind getKind() {
return Kind.MSG_NODE;
}
/**
* Returns the list of expressions for gender values and sets that field to null. Note that this
* node's command text will still contain the substring genders="...". We think this is okay since
* the command text is only used for reporting errors (in fact, it might be good as a reminder of
* how the msg was originally written).
*/
@Nullable
public List<ExprRootNode> getAndRemoveGenderExprs() {
List<ExprRootNode> genderExprs = this.genderExprs;
this.genderExprs = null;
return genderExprs;
}
@Override
public ImmutableList<ExprRootNode> getExprList() {
if (genderExprs != null) {
throw new AssertionError();
}
return ImmutableList.of();
}
/** Returns the meaning string if set, otherwise null (usually null). */
@Nullable
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 isSelectMsg() || isPluralMsg();
}
/** Returns whether this is a select message. */
public boolean isSelectMsg() {
return getChildren().size() == 1 && (getChild(0) instanceof MsgSelectNode);
}
/** Returns whether this is a plural message. */
public boolean isPluralMsg() {
return getChildren().size() == 1 && (getChild(0) instanceof MsgPluralNode);
}
/** Returns whether this is a raw text message. */
public boolean isRawTextMsg() {
return getChildren().size() == 1 && (getChild(0) instanceof RawTextNode);
}
/**
* 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 (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return (MsgPlaceholderNode) substUnitInfo.varNameToRepNodeMap.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 (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return substUnitInfo.nodeToVarNameMap.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 (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return (MsgPluralNode) substUnitInfo.varNameToRepNodeMap.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 (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return substUnitInfo.nodeToVarNameMap.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 (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return (MsgSelectNode) substUnitInfo.varNameToRepNodeMap.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 (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return substUnitInfo.nodeToVarNameMap.get(selectNode);
}
/** Getter for the generated map from substitution unit var name to representative node. */
public ImmutableMap<String, MsgSubstUnitNode> getVarNameToRepNodeMap() {
if (substUnitInfo == null) {
substUnitInfo = genSubstUnitInfo(this);
}
return substUnitInfo.varNameToRepNodeMap;
}
@Override
public String toSourceString() {
StringBuilder sb = new StringBuilder();
sb.append(getTagString());
appendSourceStringForChildren(sb);
// Note: No end tag.
return sb.toString();
}
@Override
public MsgNode copy(CopyState copyState) {
return new MsgNode(this, copyState);
}
// -----------------------------------------------------------------------------------------------
// Static helpers for building SubstUnitInfo.
/**
* Helper function to generate SubstUnitInfo, which contains mappings from/to substitution unit
* nodes (placeholders and plural/select nodes) to/from generated var names.
*
* <p>It is guaranteed that the same var name will never be shared by multiple nodes of different
* types (types are placeholder, plural, and select).
*
* @param msgNode The MsgNode to process.
* @return The generated SubstUnitInfo for the given MsgNode.
*/
private static SubstUnitInfo genSubstUnitInfo(MsgNode msgNode) {
return genFinalSubstUnitInfoMapsHelper(genPrelimSubstUnitInfoMapsHelper(msgNode));
}
/**
* Private helper for genSubstUnitInfo(). Determines representative nodes and builds preliminary
* maps.
*
* <p>If there are multiple nodes in the message that should share the same var name, then the
* first such node encountered becomes the "representative node" for the group ("repNode" in
* variable names). The rest of the nodes in the group are non-representative ("nonRepNode").
*
* <p>The baseNameToRepNodesMap is a multimap from each base name to its list of representative
* nodes (they all generate the same base var name, but should not have the same final var name).
* If we encounter a non-representative node, then we insert it into nonRepNodeToRepNodeMap,
* mapping it to its corresponding representative node.
*
* <p>The base var names are preliminary because some of the final var names will be the base
* names plus a unique suffix.
*
* @param msgNode The MsgNode being processed.
* @return A pair of (1) a multimap from each base name to its list of representative nodes, and
* (2) a map from each non-representative node to its respective representative node.
*/
@SuppressWarnings("unchecked")
private static Pair<
ListMultimap<String, MsgSubstUnitNode>, Map<MsgSubstUnitNode, MsgSubstUnitNode>>
genPrelimSubstUnitInfoMapsHelper(MsgNode msgNode) {
ListMultimap<String, MsgSubstUnitNode> baseNameToRepNodesMap = LinkedListMultimap.create();
Map<MsgSubstUnitNode, MsgSubstUnitNode> nonRepNodeToRepNodeMap = new HashMap<>();
Deque<MsgSubstUnitNode> traversalQueue = new ArrayDeque<>();
// Seed the traversal queue with the children of this MsgNode.
for (SoyNode child : msgNode.getChildren()) {
if (child instanceof MsgSubstUnitNode) {
traversalQueue.add((MsgSubstUnitNode) child);
}
}
while (!traversalQueue.isEmpty()) {
MsgSubstUnitNode node = traversalQueue.remove();
if ((node instanceof MsgSelectNode) || (node instanceof MsgPluralNode)) {
for (CaseOrDefaultNode child : ((ParentSoyNode<CaseOrDefaultNode>) node).getChildren()) {
for (SoyNode grandchild : child.getChildren()) {
if (grandchild instanceof MsgSubstUnitNode) {
traversalQueue.add((MsgSubstUnitNode) grandchild);
}
}
}
}
String baseName = node.getBaseVarName();
if (!baseNameToRepNodesMap.containsKey(baseName)) {
// Case 1: First occurrence of this base name.
baseNameToRepNodesMap.put(baseName, node);
} else {
boolean isNew = true;
for (MsgSubstUnitNode other : baseNameToRepNodesMap.get(baseName)) {
if (node.shouldUseSameVarNameAs(other)) {
// Case 2: Should use same var name 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 should not use the same var name.
baseNameToRepNodesMap.put(baseName, node);
}
}
}
return Pair.of(baseNameToRepNodesMap, nonRepNodeToRepNodeMap);
}
/**
* Private helper for genSubstUnitInfo(). Generates the final SubstUnitInfo given preliminary
* maps.
*
* @param prelimMaps A pair of (1) a multimap from each base name to its list of representative
* nodes, and (2) a map from each non-representative node to its respective representative
* node.
* @return The generated SubstUnitInfo.
*/
private static SubstUnitInfo genFinalSubstUnitInfoMapsHelper(
Pair<ListMultimap<String, MsgSubstUnitNode>, Map<MsgSubstUnitNode, MsgSubstUnitNode>>
prelimMaps) {
ListMultimap<String, MsgSubstUnitNode> baseNameToRepNodesMap = prelimMaps.first;
Map<MsgSubstUnitNode, MsgSubstUnitNode> nonRepNodeToRepNodeMap = prelimMaps.second;
// ------ Step 1: Build final map of var name to representative node. ------
//
// The final map substUnitVarNameToRepNodeMap 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.
Map<String, MsgSubstUnitNode> substUnitVarNameToRepNodeMap = new LinkedHashMap<>();
for (String baseName : baseNameToRepNodesMap.keys()) {
List<MsgSubstUnitNode> nodesWithSameBaseName = baseNameToRepNodesMap.get(baseName);
if (nodesWithSameBaseName.size() == 1) {
substUnitVarNameToRepNodeMap.put(baseName, nodesWithSameBaseName.get(0));
} else {
// Case 2: Multiple nodes generate this base name. Need number suffixes.
int nextSuffix = 1;
for (MsgSubstUnitNode repNode : nodesWithSameBaseName) {
String newName;
do {
newName = baseName + "_" + nextSuffix;
++nextSuffix;
} while (baseNameToRepNodesMap.containsKey(newName));
substUnitVarNameToRepNodeMap.put(newName, repNode);
}
}
}
// ------ Step 2: Create map of every node to its var name. ------
Map<MsgSubstUnitNode, String> substUnitNodeToVarNameMap = new LinkedHashMap<>();
// Reverse the map of names to representative nodes.
for (Map.Entry<String, MsgSubstUnitNode> entry : substUnitVarNameToRepNodeMap.entrySet()) {
substUnitNodeToVarNameMap.put(entry.getValue(), entry.getKey());
}
// Add mappings for the non-representative nodes.
for (Map.Entry<MsgSubstUnitNode, MsgSubstUnitNode> entry : nonRepNodeToRepNodeMap.entrySet()) {
MsgSubstUnitNode nonRepNode = entry.getKey();
MsgSubstUnitNode repNode = entry.getValue();
substUnitNodeToVarNameMap.put(nonRepNode, substUnitNodeToVarNameMap.get(repNode));
}
return new SubstUnitInfo(
ImmutableMap.copyOf(substUnitVarNameToRepNodeMap),
ImmutableMap.copyOf(substUnitNodeToVarNameMap));
}
/**
* Builder for {@link MsgNode}. Access through {@link MsgNode#msg} or {@link MsgNode#fallbackmsg}.
*/
public static final class Builder {
private final int id;
private final String commandName;
private final String commandText;
private final SourceLocation sourceLocation;
private Builder(int id, String commandName, String commandText, SourceLocation sourceLocation) {
this.id = id;
this.commandName = commandName;
this.commandText = commandText;
this.sourceLocation = sourceLocation;
}
/**
* Returns a new {@link MsgNode} from the state of this builder, reporting syntax errors to the
* given {@link ErrorReporter}.
*/
public MsgNode build(SoyParsingContext context) {
Map<String, String> attributes =
ATTRIBUTES_PARSER.parse(commandText, context, sourceLocation);
String gendersAttr = attributes.get("genders");
List<ExprRootNode> genderExprs = null;
if (gendersAttr != null) {
genderExprs =
ExprRootNode.wrap(
new ExpressionParser(gendersAttr, sourceLocation, context).parseExpressionList());
if (genderExprs.isEmpty() || genderExprs.size() > 3) {
context.report(sourceLocation, WRONG_NUMBER_OF_GENDER_EXPRS);
}
}
String meaning = attributes.get("meaning");
String desc = attributes.get("desc");
boolean isHidden = attributes.get("hidden").equals("true");
return new MsgNode(
id, sourceLocation, genderExprs, commandName, commandText, meaning, desc, isHidden);
}
}
}