com.google.template.soy.jbcsrc.MsgCompiler.java Source code

Java tutorial

Introduction

Here is the source code for com.google.template.soy.jbcsrc.MsgCompiler.java

Source

/*
 * 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.SoyMsg;
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 org.objectweb.asm.Label;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * A helper for compiling {@link MsgNode messages}
 */
final class MsgCompiler {
    private static final ConstructorRef SOY_MSG = ConstructorRef.create(SoyMsg.class, long.class, String.class,
            boolean.class, Iterable.class);
    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.forMethod(SoyMsgRawTextPart.class, "of",
            String.class);
    private static final MethodRef CASE_CREATE = MethodRef.forMethod(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 VariableSet variables;
    private final VariableLookup variableLookup;
    private final AppendableExpression appendableExpression;
    private final SoyNodeToStringCompiler soyNodeCompiler;

    MsgCompiler(Expression thisVar, DetachState detachState, VariableSet variables, VariableLookup variableLookup,
            AppendableExpression appendableExpression, SoyNodeToStringCompiler soyNodeCompiler) {
        this.thisVar = checkNotNull(thisVar);
        this.detachState = checkNotNull(detachState);
        this.variables = checkNotNull(variables);
        this.variableLookup = checkNotNull(variableLookup);
        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.
     *
     * @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 soyMsgDefault = compileDefaultMessageConstant(partsAndId, msg);
        Expression soyMsg = variableLookup.getRenderContext().invoke(MethodRef.RENDER_CONTEXT_GET_SOY_MSG,
                constant(partsAndId.id), soyMsgDefault);
        Statement printMsg;
        if (msg.isRawTextMsg()) {
            // Simplest case, just a static string translation
            printMsg = handleBasicTranslation(escapingDirectives, soyMsg);
        } else {
            // String translation + placeholders
            printMsg = handleTranslationWithPlaceholders(msg, escapingDirectives, soyMsg, partsAndId.parts);
        }
        return Statement.concat(printMsg.withSourceLocation(msg.getSourceLocation()),
                detachState.detachLimited(appendableExpression));
    }

    /**
     * Returns an expression the evaluates to a constant SoyMsg object used as the default message for
     * when translations don't exist.
     *
     * <p>For each msg we generate a static final field that holds a SoyMsg object 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
     * constructors isn't too hard.
     */
    private Expression compileDefaultMessageConstant(MsgPartsAndIds partsAndId, MsgNode msgNode) {
        Expression constructSoyMsg = SOY_MSG.construct(constant(partsAndId.id), // id
                // locale, technically uknown so pass null
                BytecodeUtils.constantNull(BytecodeUtils.STRING_TYPE),
                BytecodeUtils.constant(msgNode.isPlrselMsg()), partsToPartsList(partsAndId.parts));
        return variables.addStaticField("msg_" + partsAndId.id, constructSoyMsg).accessor();
    }

    private Expression partsToPartsList(ImmutableList<SoyMsgPart> parts) throws AssertionError {
        List<Expression> partsExprs = new ArrayList<>(parts.size());
        for (SoyMsgPart part : parts) {
            partsExprs.add(partToPartExpression(part));
        }
        return BytecodeUtils.asList(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()));
        } 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 soyMsg) {
        // optimize for simple constant translations (very common)
        // this becomes: renderContext.getSoyMessge(<id>).getParts().get(o).getRawText()
        SoyExpression text = SoyExpression
                .forString(soyMsg.invoke(MethodRef.SOY_MSG_GET_PARTS).invoke(MethodRef.LIST_GET, constant(0))
                        .cast(SoyMsgRawTextPart.class).invoke(MethodRef.SOY_MSG_RAW_TEXT_PART_GET_RAW_TEXT));
        for (String directive : escapingDirectives) {
            text = text.applyPrintDirective(variableLookup.getRenderContext(), directive);
        }
        return appendableExpression.appendString(text.coerceToString()).toStatement();
    }

    /**
     * Handles a complex message with placeholders.
     */
    private Statement handleTranslationWithPlaceholders(MsgNode msg, List<String> escapingDirectives,
            Expression soyMsg, 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_WITH_PLACEHOLDERS.invokeVoid(soyMsg, placeholderMap,
                    appendableExpression);
        } else {
            // render into the handy buffer we already have!
            Statement renderToBuffer = MethodRef.RUNTIME_RENDER_SOY_MSG_WITH_PLACEHOLDERS.invokeVoid(soyMsg,
                    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(variableLookup.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));
        }
        // 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 {
                // the AST for MsgNodes guarantee that these are the only options
                throw new AssertionError();
            }
            placeholderNameToPutStatement.put(placeholder.getPlaceholderName(), putEntyInMap);
        }
    }

    /**
     * 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);
        if (rawText.isPresent()) {
            return 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);
            return Statement.concat(renderIntoBuffer, putBuffer);
        }
    }

    /**
     * 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);
    }

    /**
     * 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);
    }

    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();
    }
}