/*
* Copyright 2015 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.jbcsrc;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.template.soy.jbcsrc.BytecodeUtils.STRING_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constantNull;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.msgs.internal.MsgUtils.MsgPartsAndIds;
import com.google.template.soy.msgs.restricted.SoyMsgPart;
import com.google.template.soy.msgs.restricted.SoyMsgPart.Case;
import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart;
import com.google.template.soy.msgs.restricted.SoyMsgPluralCaseSpec;
import com.google.template.soy.msgs.restricted.SoyMsgPluralCaseSpec.Type;
import com.google.template.soy.msgs.restricted.SoyMsgPluralPart;
import com.google.template.soy.msgs.restricted.SoyMsgPluralRemainderPart;
import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart;
import com.google.template.soy.msgs.restricted.SoyMsgSelectPart;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.MsgHtmlTagNode;
import com.google.template.soy.soytree.MsgNode;
import com.google.template.soy.soytree.MsgPlaceholderNode;
import com.google.template.soy.soytree.MsgPluralNode;
import com.google.template.soy.soytree.MsgSelectNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.objectweb.asm.Label;
/** A helper for compiling {@link MsgNode messages} */
final class MsgCompiler {
private static final ConstructorRef SOY_MSG_PLACEHOLDER_PART =
ConstructorRef.create(SoyMsgPlaceholderPart.class, String.class);
private static final ConstructorRef SOY_MSG_PLURAL_REMAINDER_PART =
ConstructorRef.create(SoyMsgPluralRemainderPart.class, String.class);
private static final ConstructorRef SOY_MSG_PURAL_PART =
ConstructorRef.create(SoyMsgPluralPart.class, String.class, int.class, Iterable.class);
private static final ConstructorRef SOY_MSG_SELECT_PART =
ConstructorRef.create(SoyMsgSelectPart.class, String.class, Iterable.class);
private static final MethodRef SOY_MSG_RAW_TEXT_PART_OF =
MethodRef.create(SoyMsgRawTextPart.class, "of", String.class);
private static final MethodRef CASE_CREATE =
MethodRef.create(Case.class, "create", Object.class, Iterable.class);
private static final ConstructorRef SOY_MSG_PLURAL_CASE_SPEC_TYPE =
ConstructorRef.create(SoyMsgPluralCaseSpec.class, SoyMsgPluralCaseSpec.Type.class);
private static final ConstructorRef SOY_MSG_PLURAL_CASE_SPEC_INT =
ConstructorRef.create(SoyMsgPluralCaseSpec.class, int.class);
/**
* A helper interface that allows the MsgCompiler to interact with the SoyNodeCompiler in a
* limited way.
*/
interface SoyNodeToStringCompiler {
/**
* Compiles the expression to a {@link String} valued expression.
*
* <p>If the node requires detach logic, it should use the given label as the reattach point.
*/
Expression compileToString(ExprRootNode node, Label reattachPoint);
/**
* Compiles the expression to an {@code IntegerData} valued expression.
*
* <p>If the node requires detach logic, it should use the given label as the reattach point.
*/
Expression compileToInt(ExprRootNode node, Label reattachPoint);
/**
* Compiles the print node to a {@link String} valued expression.
*
* <p>If the node requires detach logic, it should use the given label as the reattach point.
*/
Expression compileToString(PrintNode node, Label reattachPoint);
/**
* Compiles the given CallNode to a statement that writes the result into the given appendable.
*
* <p>The statement is guaranteed to be written to a location with a stack depth of zero.
*/
Statement compileToBuffer(CallNode call, AppendableExpression appendable);
/**
* Compiles the given MsgHtmlTagNode to a statement that writes the result into the given
* appendable.
*
* <p>The statement is guaranteed to be written to a location with a stack depth of zero.
*/
Statement compileToBuffer(MsgHtmlTagNode htmlTagNode, AppendableExpression appendable);
}
private final Expression thisVar;
private final DetachState detachState;
private final TemplateVariableManager variables;
private final TemplateParameterLookup parameterLookup;
private final AppendableExpression appendableExpression;
private final SoyNodeToStringCompiler soyNodeCompiler;
MsgCompiler(
Expression thisVar,
DetachState detachState,
TemplateVariableManager variables,
TemplateParameterLookup parameterLookup,
AppendableExpression appendableExpression,
SoyNodeToStringCompiler soyNodeCompiler) {
this.thisVar = checkNotNull(thisVar);
this.detachState = checkNotNull(detachState);
this.variables = checkNotNull(variables);
this.parameterLookup = checkNotNull(parameterLookup);
this.appendableExpression = checkNotNull(appendableExpression);
this.soyNodeCompiler = checkNotNull(soyNodeCompiler);
}
/**
* Compiles the given {@link MsgNode} to a statement with the given escaping directives applied.
*
* <p>The returned statement must be written to a location with a stack depth of zero, since
* placeholder formatting may require detach logic.
*
* @param partsAndId The computed msg id
* @param msg The msg node
* @param escapingDirectives The set of escaping directives to apply.
*/
Statement compileMessage(
MsgPartsAndIds partsAndId, MsgNode msg, List<String> escapingDirectives) {
Expression soyMsgDefaultParts = compileDefaultMessagePartsConstant(partsAndId);
Expression soyMsgParts =
parameterLookup
.getRenderContext()
.invoke(
MethodRef.RENDER_CONTEXT_GET_SOY_MSG_PARTS,
constant(partsAndId.id),
soyMsgDefaultParts);
Statement printMsg;
if (msg.isRawTextMsg()) {
// Simplest case, just a static string translation
printMsg = handleBasicTranslation(escapingDirectives, soyMsgParts);
} else {
// String translation + placeholders
printMsg =
handleTranslationWithPlaceholders(
msg,
escapingDirectives,
soyMsgParts,
parameterLookup.getRenderContext().invoke(MethodRef.RENDER_CONTEXT_GET_LOCALE),
partsAndId.parts);
}
return Statement.concat(
printMsg.withSourceLocation(msg.getSourceLocation()),
detachState.detachLimited(appendableExpression));
}
/**
* Returns an expression that evaluates to a constant {@code ImmutableList<SoyMsgPart>} used as
* the default message for when translations don't exist.
*
* <p>For each msg we generate a static final field that holds an {@code
* ImmutableList<SoyMsgPart>} which means we have to go through the somewhat awkward process of
* generating code to construct objects we have at compile time. We could do something like use
* java serialization, but just invoking the SoyMsgPart constructors isn't too hard.
*/
private Expression compileDefaultMessagePartsConstant(MsgPartsAndIds partsAndId) {
return variables
.addStaticField("msg_parts_" + partsAndId.id, partsToPartsList(partsAndId.parts))
.accessor();
}
private Expression partsToPartsList(ImmutableList<SoyMsgPart> parts) throws AssertionError {
List<Expression> partsExprs = new ArrayList<>(parts.size());
for (SoyMsgPart part : parts) {
partsExprs.add(partToPartExpression(part));
}
// ensure that the runtime type is immutablelist, ensures monomorphism
return BytecodeUtils.asImmutableList(partsExprs);
}
/** Returns an {@link Expression} that evaluates to an equivalent SoyMsgPart as the argument. */
private Expression partToPartExpression(SoyMsgPart part) {
if (part instanceof SoyMsgPlaceholderPart) {
return SOY_MSG_PLACEHOLDER_PART.construct(
constant(((SoyMsgPlaceholderPart) part).getPlaceholderName()));
} else if (part instanceof SoyMsgPluralPart) {
SoyMsgPluralPart pluralPart = (SoyMsgPluralPart) part;
List<Expression> caseExprs = new ArrayList<>(pluralPart.getCases().size());
for (Case<SoyMsgPluralCaseSpec> item : pluralPart.getCases()) {
Expression spec;
if (item.spec().getType() == Type.EXPLICIT) {
spec = SOY_MSG_PLURAL_CASE_SPEC_INT.construct(constant(item.spec().getExplicitValue()));
} else {
spec =
SOY_MSG_PLURAL_CASE_SPEC_TYPE.construct(
FieldRef.enumReference(item.spec().getType()).accessor());
}
caseExprs.add(CASE_CREATE.invoke(spec, partsToPartsList(item.parts())));
}
return SOY_MSG_PURAL_PART.construct(
constant(pluralPart.getPluralVarName()),
constant(pluralPart.getOffset()),
BytecodeUtils.asList(caseExprs));
} else if (part instanceof SoyMsgPluralRemainderPart) {
return SOY_MSG_PLURAL_REMAINDER_PART.construct(
constant(((SoyMsgPluralRemainderPart) part).getPluralVarName()));
} else if (part instanceof SoyMsgRawTextPart) {
return SOY_MSG_RAW_TEXT_PART_OF.invoke(
constant(((SoyMsgRawTextPart) part).getRawText(), variables));
} else if (part instanceof SoyMsgSelectPart) {
SoyMsgSelectPart selectPart = (SoyMsgSelectPart) part;
List<Expression> caseExprs = new ArrayList<>(selectPart.getCases().size());
for (Case<String> item : selectPart.getCases()) {
caseExprs.add(
CASE_CREATE.invoke(
item.spec() == null ? constantNull(STRING_TYPE) : constant(item.spec()),
partsToPartsList(item.parts())));
}
return SOY_MSG_SELECT_PART.construct(
constant(selectPart.getSelectVarName()), BytecodeUtils.asList(caseExprs));
} else {
throw new AssertionError("unrecognized part: " + part);
}
}
/** Handles a translation consisting of a single raw text node. */
private Statement handleBasicTranslation(
List<String> escapingDirectives, Expression soyMsgParts) {
// optimize for simple constant translations (very common)
// this becomes: renderContext.getSoyMessge(<id>).getParts().get(o).getRawText()
SoyExpression text =
SoyExpression.forString(
soyMsgParts
.invoke(MethodRef.LIST_GET, constant(0))
.checkedCast(SoyMsgRawTextPart.class)
.invoke(MethodRef.SOY_MSG_RAW_TEXT_PART_GET_RAW_TEXT));
for (String directive : escapingDirectives) {
text = text.applyPrintDirective(parameterLookup.getRenderContext(), directive);
}
return appendableExpression.appendString(text.coerceToString()).toStatement();
}
/** Handles a complex message with placeholders. */
private Statement handleTranslationWithPlaceholders(
MsgNode msg,
List<String> escapingDirectives,
Expression soyMsgParts,
Expression locale,
ImmutableList<SoyMsgPart> parts) {
// We need to render placeholders into a buffer and then pack them into a map to pass to
// Runtime.renderSoyMsgWithPlaceholders.
Expression placeholderMap = variables.getMsgPlaceholderMapField().accessor(thisVar);
Map<String, Statement> placeholderNameToPutStatement = new LinkedHashMap<>();
putPlaceholdersIntoMap(placeholderMap, msg, parts, placeholderNameToPutStatement);
// sanity check
checkState(!placeholderNameToPutStatement.isEmpty());
variables.setMsgPlaceholderMapMinSize(placeholderNameToPutStatement.size());
Statement populateMap = Statement.concat(placeholderNameToPutStatement.values());
Statement clearMap = placeholderMap.invokeVoid(MethodRef.LINKED_HASH_MAP_CLEAR);
Statement render;
if (escapingDirectives.isEmpty()) {
render =
MethodRef.RUNTIME_RENDER_SOY_MSG_PARTS_WITH_PLACEHOLDERS.invokeVoid(
soyMsgParts, locale, placeholderMap, appendableExpression);
} else {
// render into the handy buffer we already have!
Statement renderToBuffer =
MethodRef.RUNTIME_RENDER_SOY_MSG_PARTS_WITH_PLACEHOLDERS.invokeVoid(
soyMsgParts, locale, placeholderMap, tempBuffer());
// N.B. the type here is always 'string'
SoyExpression value =
SoyExpression.forString(
tempBuffer().invoke(MethodRef.ADVISING_STRING_BUILDER_GET_AND_CLEAR));
for (String directive : escapingDirectives) {
value = value.applyPrintDirective(parameterLookup.getRenderContext(), directive);
}
render =
Statement.concat(
renderToBuffer,
appendableExpression.appendString(value.coerceToString()).toStatement());
}
Statement detach = detachState.detachLimited(appendableExpression);
return Statement.concat(populateMap, render, clearMap, detach)
.withSourceLocation(msg.getSourceLocation());
}
/**
* Adds a {@link Statement} to {@link Map#put} every msg placeholder, plural variable and select
* case value into {@code mapExpression}
*/
private void putPlaceholdersIntoMap(
Expression mapExpression,
MsgNode originalMsg,
Iterable<? extends SoyMsgPart> parts,
Map<String, Statement> placeholderNameToPutStatement) {
for (SoyMsgPart child : parts) {
if (child instanceof SoyMsgRawTextPart || child instanceof SoyMsgPluralRemainderPart) {
// raw text doesn't have placeholders and remainders use the same placeholder as plural they
// are a member of.
continue;
}
if (child instanceof SoyMsgPluralPart) {
putPluralPartIntoMap(
mapExpression, originalMsg, placeholderNameToPutStatement, (SoyMsgPluralPart) child);
} else if (child instanceof SoyMsgSelectPart) {
putSelectPartIntoMap(
mapExpression, originalMsg, placeholderNameToPutStatement, (SoyMsgSelectPart) child);
} else if (child instanceof SoyMsgPlaceholderPart) {
putPlaceholderIntoMap(
mapExpression,
originalMsg,
placeholderNameToPutStatement,
(SoyMsgPlaceholderPart) child);
} else {
throw new AssertionError("unexpected child: " + child);
}
}
}
private void putSelectPartIntoMap(
Expression mapExpression,
MsgNode originalMsg,
Map<String, Statement> placeholderNameToPutStatement,
SoyMsgSelectPart select) {
MsgSelectNode repSelectNode = originalMsg.getRepSelectNode(select.getSelectVarName());
if (!placeholderNameToPutStatement.containsKey(select.getSelectVarName())) {
Label reattachPoint = new Label();
Expression value = soyNodeCompiler.compileToString(repSelectNode.getExpr(), reattachPoint);
placeholderNameToPutStatement.put(
select.getSelectVarName(),
putToMap(mapExpression, select.getSelectVarName(), value).labelStart(reattachPoint));
}
// Recursively visit select cases
for (Case<String> caseOrDefault : select.getCases()) {
putPlaceholdersIntoMap(
mapExpression, originalMsg, caseOrDefault.parts(), placeholderNameToPutStatement);
}
}
private void putPluralPartIntoMap(
Expression mapExpression,
MsgNode originalMsg,
Map<String, Statement> placeholderNameToPutStatement,
SoyMsgPluralPart plural) {
MsgPluralNode repPluralNode = originalMsg.getRepPluralNode(plural.getPluralVarName());
if (!placeholderNameToPutStatement.containsKey(plural.getPluralVarName())) {
Label reattachPoint = new Label();
Expression value = soyNodeCompiler.compileToInt(repPluralNode.getExpr(), reattachPoint);
placeholderNameToPutStatement.put(
plural.getPluralVarName(),
putToMap(mapExpression, plural.getPluralVarName(), value)
.labelStart(reattachPoint)
.withSourceLocation(repPluralNode.getSourceLocation()));
}
// Recursively visit plural cases
for (Case<SoyMsgPluralCaseSpec> caseOrDefault : plural.getCases()) {
putPlaceholdersIntoMap(
mapExpression, originalMsg, caseOrDefault.parts(), placeholderNameToPutStatement);
}
}
private void putPlaceholderIntoMap(
Expression mapExpression,
MsgNode originalMsg,
Map<String, Statement> placeholderNameToPutStatement,
SoyMsgPlaceholderPart placeholder)
throws AssertionError {
MsgPlaceholderNode repPlaceholderNode =
originalMsg.getRepPlaceholderNode(placeholder.getPlaceholderName());
String placeholderName = placeholder.getPlaceholderName();
if (!placeholderNameToPutStatement.containsKey(placeholderName)) {
StandaloneNode initialNode = repPlaceholderNode.getChild(0);
Statement putEntyInMap;
if (initialNode instanceof MsgHtmlTagNode) {
putEntyInMap =
addHtmlTagNodeToPlaceholderMap(
mapExpression, placeholderName, (MsgHtmlTagNode) initialNode);
} else if (initialNode instanceof CallNode) {
putEntyInMap =
addCallNodeToPlaceholderMap(mapExpression, placeholderName, (CallNode) initialNode);
} else if (initialNode instanceof PrintNode) {
putEntyInMap =
addPrintNodeToPlaceholderMap(mapExpression, placeholderName, (PrintNode) initialNode);
} else if (initialNode instanceof RawTextNode) {
putEntyInMap =
addRawTextNodeToPlaceholderMap(
mapExpression, placeholderName, (RawTextNode) initialNode);
} else {
// the AST for MsgNodes guarantee that these are the only options
throw new AssertionError("Unexpected child: " + initialNode.getClass());
}
placeholderNameToPutStatement.put(
placeholder.getPlaceholderName(),
putEntyInMap.withSourceLocation(repPlaceholderNode.getSourceLocation()));
}
}
/**
* Returns a statement that adds the content of the raw text node to the map.
*
* @param mapExpression The map to put the new entry in
* @param mapKey The map key
* @param rawText The node
*/
private Statement addRawTextNodeToPlaceholderMap(
Expression mapExpression, String mapKey, RawTextNode rawText) {
return mapExpression
.invoke(
MethodRef.LINKED_HASH_MAP_PUT,
constant(mapKey),
constant(rawText.getRawText(), variables))
.toStatement()
.withSourceLocation(rawText.getSourceLocation());
}
/**
* Returns a statement that adds the content of the node to the map.
*
* @param mapExpression The map to put the new entry in
* @param mapKey The map key
* @param htmlTagNode The node
*/
private Statement addHtmlTagNodeToPlaceholderMap(
Expression mapExpression, String mapKey, MsgHtmlTagNode htmlTagNode) {
Optional<String> rawText = tryGetRawTextContent(htmlTagNode);
Statement putStatement;
if (rawText.isPresent()) {
putStatement =
mapExpression
.invoke(MethodRef.LINKED_HASH_MAP_PUT, constant(mapKey), constant(rawText.get()))
.toStatement();
} else {
Statement renderIntoBuffer = soyNodeCompiler.compileToBuffer(htmlTagNode, tempBuffer());
Statement putBuffer = putBufferIntoMapForPlaceholder(mapExpression, mapKey);
putStatement = Statement.concat(renderIntoBuffer, putBuffer);
}
return putStatement.withSourceLocation(htmlTagNode.getSourceLocation());
}
/**
* Returns a statement that adds the content rendered by the call to the map.
*
* @param mapExpression The map to put the new entry in
* @param mapKey The map key
* @param callNode The node
*/
private Statement addCallNodeToPlaceholderMap(
Expression mapExpression, String mapKey, CallNode callNode) {
Statement renderIntoBuffer = soyNodeCompiler.compileToBuffer(callNode, tempBuffer());
Statement putBuffer = putBufferIntoMapForPlaceholder(mapExpression, mapKey);
return Statement.concat(renderIntoBuffer, putBuffer)
.withSourceLocation(callNode.getSourceLocation());
}
/**
* Returns a statement that adds the content rendered by the call to the map.
*
* @param mapExpression The map to put the new entry in
* @param mapKey The map key
* @param printNode The node
*/
private Statement addPrintNodeToPlaceholderMap(
Expression mapExpression, String mapKey, PrintNode printNode) {
// This is much like the escaping path of visitPrintNode but somewhat simpler because our
// ultimate target is a string rather than putting bytes on the output stream.
Label reattachPoint = new Label();
Expression compileToString = soyNodeCompiler.compileToString(printNode, reattachPoint);
return putToMap(mapExpression, mapKey, compileToString)
.labelStart(reattachPoint)
.withSourceLocation(printNode.getSourceLocation());
}
private Statement putToMap(Expression mapExpression, String mapKey, Expression valueExpression) {
return mapExpression
.invoke(MethodRef.LINKED_HASH_MAP_PUT, constant(mapKey), valueExpression)
.toStatement();
}
private AppendableExpression tempBuffer() {
return AppendableExpression.forStringBuilder(variables.getTempBufferField().accessor(thisVar));
}
private Statement putBufferIntoMapForPlaceholder(Expression mapExpression, String mapKey) {
return mapExpression
.invoke(
MethodRef.LINKED_HASH_MAP_PUT,
constant(mapKey),
tempBuffer().invoke(MethodRef.ADVISING_STRING_BUILDER_GET_AND_CLEAR))
.toStatement();
}
private Optional<String> tryGetRawTextContent(ParentSoyNode<?> initialNode) {
if (initialNode.numChildren() == 1 && initialNode.getChild(0) instanceof RawTextNode) {
return Optional.of(((RawTextNode) initialNode.getChild(0)).getRawText());
}
return Optional.absent();
}
}