/* * 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.sharedpasses.render; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.template.soy.data.LazySanitizedContents; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.SoyAbstractCachingValueProvider; import com.google.template.soy.data.SoyAbstractCachingValueProvider.ValueAssertion; import com.google.template.soy.data.SoyDataException; import com.google.template.soy.data.SoyFutureValueProvider; import com.google.template.soy.data.SoyFutureValueProvider.FutureBlockCallback; import com.google.template.soy.data.SoyList; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.SoyValue; import com.google.template.soy.data.SoyValueProvider; import com.google.template.soy.data.UnsafeSanitizedContentOrdainer; import com.google.template.soy.data.internal.AugmentedParamStore; import com.google.template.soy.data.internal.BasicParamStore; import com.google.template.soy.data.internal.ParamStore; import com.google.template.soy.data.internal.RenderableThunk; import com.google.template.soy.data.restricted.IntegerData; import com.google.template.soy.data.restricted.NullData; import com.google.template.soy.data.restricted.StringData; import com.google.template.soy.data.restricted.UndefinedData; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.msgs.SoyMsgBundle; import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.shared.SoyIdRenamingMap; import com.google.template.soy.shared.internal.SharedRuntime; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.google.template.soy.sharedpasses.render.EvalVisitor.EvalVisitorFactory; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.CallBasicNode; import com.google.template.soy.soytree.CallDelegateNode; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.CallParamNode; import com.google.template.soy.soytree.CallParamValueNode; import com.google.template.soy.soytree.CssNode; import com.google.template.soy.soytree.DebuggerNode; import com.google.template.soy.soytree.ForNode; import com.google.template.soy.soytree.ForNode.RangeArgs; import com.google.template.soy.soytree.ForeachNode; import com.google.template.soy.soytree.ForeachNonemptyNode; import com.google.template.soy.soytree.IfCondNode; import com.google.template.soy.soytree.IfElseNode; import com.google.template.soy.soytree.IfNode; import com.google.template.soy.soytree.LetContentNode; import com.google.template.soy.soytree.LetValueNode; import com.google.template.soy.soytree.LogNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; import com.google.template.soy.soytree.MsgHtmlTagNode; import com.google.template.soy.soytree.PrintDirectiveNode; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.RawTextNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.BlockNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import com.google.template.soy.soytree.SoyNode.RenderUnitNode; import com.google.template.soy.soytree.SwitchCaseNode; import com.google.template.soy.soytree.SwitchDefaultNode; import com.google.template.soy.soytree.SwitchNode; import com.google.template.soy.soytree.TemplateDelegateNode; import com.google.template.soy.soytree.TemplateDelegateNode.DelTemplateKey; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.TemplateRegistry; import com.google.template.soy.soytree.XidNode; import com.google.template.soy.soytree.defn.LocalVar; import com.google.template.soy.soytree.defn.LoopVar; import com.google.template.soy.soytree.defn.TemplateParam; import com.google.template.soy.types.SoyType.Kind; import java.io.Flushable; import java.io.IOException; import java.util.ArrayDeque; import java.util.Collection; import java.util.Deque; import java.util.List; import javax.annotation.Nullable; /** * Visitor for rendering the template subtree rooted at a given SoyNode. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * * <p>The rendered output will be appended to the Appendable provided to the constructor. * */ public class RenderVisitor extends AbstractSoyNodeVisitor<Void> { /** Map of all SoyJavaPrintDirectives (name to directive). */ protected final ImmutableMap<String, ? extends SoyJavaPrintDirective> soyJavaDirectivesMap; /** Factory for creating an instance of EvalVisitor. */ protected final EvalVisitorFactory evalVisitorFactory; /** The bundle containing all the templates that may be rendered. */ protected final TemplateRegistry templateRegistry; /** The current template data. */ protected final SoyRecord data; /** The current injected data. */ protected final SoyRecord ijData; /** The current environment. */ protected Environment env; /** The predicate for testing whether a given delpackage is active. */ protected final Predicate<String> activeDelPackageSelector; /** The bundle of translated messages, or null to use the messages from the Soy source. */ protected final SoyMsgBundle msgBundle; /** xid renaming map. */ protected final SoyIdRenamingMap xidRenamingMap; /** CSS renaming map. */ protected final SoyCssRenamingMap cssRenamingMap; /** The EvalVisitor for this instance (can reuse since 'data' and 'env' references stay same). */ // Note: Don't use directly. Call eval() instead. private EvalVisitor evalVisitor; /** The assistant visitor for msgs (lazily initialized). */ private RenderVisitorAssistantForMsgs assistantForMsgs; /** The stack of output Appendables (current output buffer is top of stack). */ protected Deque<Appendable> outputBufStack; /** The current Appendable to append the output to. Equals the top element of outputStack. */ private Appendable currOutputBuf; /** * Render visitors have a stack of output buffers and RenderVisitors a nested (to render blocks) * with independent output buffers. Of those, if any, the first one that is passed in can be * flushed – all the others are StringBuilders. This instance variable holds the flushable root * output buffer. */ private CountingFlushableAppendable flushable; /** * @param soyJavaDirectivesMap Map of all SoyJavaPrintDirectives (name to directive). * @param evalVisitorFactory Factory for creating an instance of EvalVisitor. * @param outputBuf The Appendable to append the output to. * @param templateRegistry A registry of all templates. Should never be null (except in some unit * tests). * @param data The current template data. * @param ijData The current injected data. * @param activeDelPackageSelector The predicate for testing whether a given delpackage is active. * Allowed to be null when known to be irrelevant. * @param msgBundle The bundle of translated messages, or null to use the messages from the Soy * source. * @param cssRenamingMap The CSS renaming map, or null if not applicable. * @param xidRenamingMap The 'xid' renaming map, or null if not applicable. */ protected RenderVisitor( ImmutableMap<String, ? extends SoyJavaPrintDirective> soyJavaDirectivesMap, EvalVisitorFactory evalVisitorFactory, Appendable outputBuf, @Nullable TemplateRegistry templateRegistry, SoyRecord data, @Nullable SoyRecord ijData, @Nullable Predicate<String> activeDelPackageSelector, @Nullable SoyMsgBundle msgBundle, @Nullable SoyIdRenamingMap xidRenamingMap, @Nullable SoyCssRenamingMap cssRenamingMap) { Preconditions.checkNotNull(data); this.soyJavaDirectivesMap = soyJavaDirectivesMap; this.evalVisitorFactory = evalVisitorFactory; this.templateRegistry = templateRegistry; this.data = data; this.ijData = ijData; this.activeDelPackageSelector = activeDelPackageSelector; this.msgBundle = msgBundle; this.xidRenamingMap = xidRenamingMap; this.cssRenamingMap = cssRenamingMap; this.evalVisitor = null; // lazily initialized this.assistantForMsgs = null; // lazily initialized this.outputBufStack = new ArrayDeque<>(); if (outputBuf instanceof Flushable) { if (outputBuf instanceof CountingFlushableAppendable) { flushable = (CountingFlushableAppendable) outputBuf; } else { flushable = new CountingFlushableAppendable(outputBuf); } outputBuf = flushable; } pushOutputBuf(outputBuf); } @Override public Void exec(SoyNode node) { if (flushable != null) { // only do this in exec() so that all recursively called templates flush the correct top-level // output stream FutureBlockCallback old = SoyFutureValueProvider.futureBlockCallback.get(); SoyFutureValueProvider.futureBlockCallback.set(flushable); super.exec(node); SoyFutureValueProvider.futureBlockCallback.set(old); } else { super.exec(node); } return null; } /** * Creates a helper instance for rendering a subtemplate. * * @param outputBuf The Appendable to append the output to. * @param data The template data. * @return The newly created RenderVisitor instance. */ protected RenderVisitor createHelperInstance(Appendable outputBuf, SoyRecord data) { return new RenderVisitor( soyJavaDirectivesMap, evalVisitorFactory, outputBuf, templateRegistry, data, ijData, activeDelPackageSelector, msgBundle, xidRenamingMap, cssRenamingMap); } /** * This method must only be called by assistant visitors, in particular * RenderVisitorAssistantForMsgs. */ void visitForUseByAssistants(SoyNode node) { visit(node); } /** A private helper to render templates with optimized type checking. */ private void renderTemplate(TemplateNode template, Collection<TemplateParam> paramsToTypeCheck) { env = Environment.create(template, data, ijData); checkStrictParamTypes(template, paramsToTypeCheck); visitChildren(template); env = null; // unpin for gc } // ----------------------------------------------------------------------------------------------- // Implementations for specific nodes. @Override protected void visitTemplateNode(TemplateNode node) { // check all params of the node. This callpath should only be called in the case of external // calls into soy (e.g. RenderVisitor.exec(node)). For calls to templates from soy, the // renderTemplate() method is called directly. renderTemplate(node, node.getParams()); } @Override protected void visitRawTextNode(RawTextNode node) { append(currOutputBuf, node.getRawText()); } @Override protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) { if (assistantForMsgs == null) { assistantForMsgs = new RenderVisitorAssistantForMsgs(this, msgBundle); } if (!node.getEscapingDirectiveNames().isEmpty()) { // The entire message needs to be escaped, so we need to render to a temporary buffer. // Fortunately, for most messages (in HTML context) this is unnecessary. pushOutputBuf(new StringBuilder()); } assistantForMsgs.visitForUseByMaster(node); if (!node.getEscapingDirectiveNames().isEmpty()) { // Escape the entire message with the required directives. SoyValue wholeMsg = StringData.forValue(popOutputBuf().toString()); for (String directiveName : node.getEscapingDirectiveNames()) { wholeMsg = applyDirective(directiveName, wholeMsg, ImmutableList.<SoyValue>of(), node); } append(currOutputBuf, wholeMsg.stringValue()); } } @Override protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) { throw new AssertionError(); } @Override protected void visitPrintNode(PrintNode node) { SoyValue result = eval(node.getExpr(), node); if (result instanceof UndefinedData) { throw RenderException.createWithSource( "In 'print' tag, expression \"" + node.getExprText() + "\" evaluates to undefined.", node); } // Process directives. for (PrintDirectiveNode directiveNode : node.getChildren()) { // Evaluate directive args. List<ExprRootNode> argsExprs = directiveNode.getArgs(); List<SoyValue> argsSoyDatas = Lists.newArrayListWithCapacity(argsExprs.size()); for (ExprRootNode argExpr : argsExprs) { argsSoyDatas.add(eval(argExpr, directiveNode)); } // Apply directive. result = applyDirective(directiveNode.getName(), result, argsSoyDatas, node); } append(currOutputBuf, result, node); } @Override protected void visitXidNode(XidNode node) { String xid = node.getRenamedText(xidRenamingMap); append(currOutputBuf, xid); } @Override protected void visitCssNode(CssNode node) { ExprRootNode componentNameExpr = node.getComponentNameExpr(); if (componentNameExpr != null) { append(currOutputBuf, eval(componentNameExpr, node), node); append(currOutputBuf, "-"); } // CSS statements are of the form {css selector} or {css $component, selector}. // We only rename the selector text. The component must derive from a previous // css expression and thus is already renamed. // // For example, in Javascript calling Soy: // Js: var base = goog.getCssName('goog-custom-button'); // Soy: {css $base, hover} // // In a Soy template: // {call .helper} // {param base}{css goog-custom-button}{/param} // {/call} // // {template .helper} // {css $base, hover} // {/template} String className = node.getRenamedSelectorText(cssRenamingMap); append(currOutputBuf, className); } @Override protected void visitLetValueNode(LetValueNode node) { env.bind(node.getVar(), lazyEval(node.getValueExpr(), node)); } @Override protected void visitLetContentNode(LetContentNode node) { env.bind(node.getVar(), renderRenderUnitNode(node)); } @Override protected void visitIfNode(IfNode node) { for (SoyNode child : node.getChildren()) { if (child instanceof IfCondNode) { IfCondNode icn = (IfCondNode) child; if (eval(icn.getExpr(), node).coerceToBoolean()) { visit(icn); return; } } else if (child instanceof IfElseNode) { visit(child); return; } else { throw new AssertionError(); } } } @Override protected void visitSwitchNode(SwitchNode node) { SoyValue switchValue = eval(node.getExpr(), node); for (SoyNode child : node.getChildren()) { if (child instanceof SwitchCaseNode) { SwitchCaseNode scn = (SwitchCaseNode) child; for (ExprNode caseExpr : scn.getExprList()) { if (SharedRuntime.equal(switchValue, eval(caseExpr, scn))) { visit(scn); return; } } } else if (child instanceof SwitchDefaultNode) { visit(child); return; } else { throw new AssertionError(); } } } @Override protected void visitForeachNode(ForeachNode node) { SoyValue dataRefValue = eval(node.getExpr(), node); if (!(dataRefValue instanceof SoyList)) { throw RenderException.createWithSource( "In 'foreach' command " + node.toSourceString() + ", the data reference does not " + "resolve to a SoyList " + "(encountered type " + dataRefValue.getClass().getName() + ").", node); } SoyList foreachList = (SoyList) dataRefValue; int listLength = foreachList.length(); if (listLength > 0) { // Case 1: Nonempty list. ForeachNonemptyNode child = (ForeachNonemptyNode) node.getChild(0); LoopVar var = child.getVar(); for (int i = 0; i < listLength; ++i) { SoyValueProvider value = foreachList.getProvider(i); env.bind(var, value); env.bindCurrentIndex(var, i); env.bindIsLast(var, listLength - 1 == i); visitChildren(child); } } else { // Case 2: Empty list. If the 'ifempty' node exists, visit it. if (node.numChildren() == 2) { visit(node.getChild(1)); } } } @Override protected void visitForNode(ForNode node) { RangeArgs rangeArgs = node.getRangeArgs(); int increment = evalRangeArg(node, rangeArgs.increment()); int init = evalRangeArg(node, rangeArgs.start()); int limit = evalRangeArg(node, rangeArgs.limit()); LocalVar localVarName = node.getVar(); for (int i = init; i < limit; i += increment) { env.bind(localVarName, IntegerData.forValue(i)); visitChildren(node); } } private int evalRangeArg(ForNode node, ExprRootNode rangeArg) { SoyValue rangeArgValue = eval(rangeArg, node); if (!(rangeArgValue instanceof IntegerData)) { throw RenderException.createWithSource( "In 'for' command " + node.toSourceString() + ", the expression \"" + rangeArg.toSourceString() + "\" does not resolve to an integer.", node); } return rangeArgValue.integerValue(); } @Override protected void visitCallBasicNode(CallBasicNode node) { TemplateNode callee = templateRegistry.getBasicTemplate(node.getCalleeName()); if (callee == null) { throw RenderException.createWithSource( "Attempting to render undefined template '" + node.getCalleeName() + "'.", node); } visitCallNodeHelper(node, callee); } @Override protected void visitCallDelegateNode(CallDelegateNode node) { ExprRootNode variantExpr = node.getDelCalleeVariantExpr(); String variant; if (variantExpr == null) { variant = ""; } else { try { SoyValue variantData = eval(variantExpr, node); if (variantData instanceof IntegerData) { // An integer constant is being used as variant. Use the value string representation as // variant. variant = String.valueOf(variantData.longValue()); } else { // Variant is either a StringData or a SanitizedContent. Use the value as a string. If // the value is not a string, and exception will be thrown. variant = variantData.stringValue(); } } catch (SoyDataException e) { throw RenderException.createWithSource( String.format( "Variant expression \"%s\" doesn't evaluate to a valid type " + "(Only string and integer are supported).", variantExpr.toSourceString()), e, node); } } DelTemplateKey delegateKey = DelTemplateKey.create(node.getDelCalleeName(), variant); TemplateDelegateNode callee; try { callee = templateRegistry.selectDelTemplate(delegateKey, activeDelPackageSelector); } catch (IllegalArgumentException e) { throw RenderException.createWithSource(e.getMessage(), e, node); } if (callee != null) { visitCallNodeHelper(node, callee); } else if (node.allowsEmptyDefault()) { return; // no active delegate implementation, so the call output is empty string } else { throw RenderException.createWithSource( "Found no active impl for delegate call to '" + node.getDelCalleeName() + "' (and no attribute allowemptydefault=\"true\").", node); } } @SuppressWarnings("ConstantConditions") // for IntelliJ private void visitCallNodeHelper(CallNode node, TemplateNode callee) { // ------ Build the call data. ------ SoyRecord dataToPass; if (node.dataAttribute().isPassingAllData()) { dataToPass = data; } else if (node.dataAttribute().isPassingData()) { SoyValue dataRefValue = eval(node.dataAttribute().dataExpr(), node); if (!(dataRefValue instanceof SoyRecord)) { throw RenderException.create( "In 'call' command " + node.toSourceString() + ", the data reference does not resolve to a SoyRecord.") .addStackTraceElement(node); } dataToPass = (SoyRecord) dataRefValue; } else { dataToPass = null; } SoyRecord callData; int numChildren = node.numChildren(); if (numChildren == 0) { // --- Cases 1 and 2: Not passing params. --- if (dataToPass == null) { // Case 1: Not passing data and not passing params. callData = ParamStore.EMPTY_INSTANCE; } else { // Case 2: Passing data and not passing params. callData = dataToPass; } } else { // --- Cases 3 and 4: Passing params. --- ParamStore mutableCallData; if (dataToPass == null) { // Case 3: Not passing data and passing params. mutableCallData = new BasicParamStore(numChildren); } else { // Case 4: Passing data and passing params. mutableCallData = new AugmentedParamStore(dataToPass, numChildren); } for (CallParamNode child : node.getChildren()) { if (child instanceof CallParamValueNode) { mutableCallData.setField( child.getKey(), lazyEval(((CallParamValueNode) child).getExpr(), child)); } else if (child instanceof CallParamContentNode) { mutableCallData.setField( child.getKey(), renderRenderUnitNode((CallParamContentNode) child)); } else { throw new AssertionError(); } } callData = mutableCallData; } // ------ Render the callee template with the callData built above. ------ if (node.getEscapingDirectiveNames().isEmpty()) { // No escaping at the call site -- render directly into the output buffer. RenderVisitor rv = this.createHelperInstance(currOutputBuf, callData); try { rv.renderTemplate(callee, node.getParamsToRuntimeCheck(callee)); } catch (RenderException re) { // The {call .XXX} failed to render - a new partial stack trace element is added to capture // this template call. throw re.addStackTraceElement(node); } } else { // Escaping the call site's result, such as at a strict template boundary. // TODO: Some optimization is needed here before Strict Soy can be widely used: // - Only create this temporary buffer when contexts mismatch. We could run a pre-pass that // eliminates escaping directives when all callers are known. // - Instead of creating a temporary buffer and copying, wrap with an escaping StringBuilder. StringBuilder calleeBuilder = new StringBuilder(); RenderVisitor rv = this.createHelperInstance(calleeBuilder, callData); try { rv.renderTemplate(callee, node.getParamsToRuntimeCheck(callee)); } catch (RenderException re) { // The {call .XXX} failed to render - a new partial stack trace element is added to capture // this template call. throw re.addStackTraceElement(node); } SoyValue resultData = (callee.getContentKind() != null) ? UnsafeSanitizedContentOrdainer.ordainAsSafe( calleeBuilder.toString(), callee.getContentKind()) : StringData.forValue(calleeBuilder.toString()); for (String directiveName : node.getEscapingDirectiveNames()) { resultData = applyDirective(directiveName, resultData, ImmutableList.<SoyValue>of(), node); } append(currOutputBuf, resultData, node); } } @Override protected void visitCallParamNode(CallParamNode node) { // In this visitor, we never directly visit a CallParamNode. throw new AssertionError(); } @Override protected void visitLogNode(LogNode node) { renderBlock(node, System.out); System.out.println(); // add a newline } @Override protected void visitDebuggerNode(DebuggerNode node) { // The 'debugger' statement does nothing in Java rendering, but the user could theoretically // place a breakpoint at this method. } // ----------------------------------------------------------------------------------------------- // Fallback implementation. @Override protected void visitSoyNode(SoyNode node) { if (node instanceof ParentSoyNode<?>) { visitChildren((ParentSoyNode<?>) node); } } // ----------------------------------------------------------------------------------------------- // Helpers. /** Pushes the given output buffer onto the stack (it becomes the current output buffer). */ private void pushOutputBuf(Appendable outputBuf) { outputBufStack.push(outputBuf); currOutputBuf = outputBuf; } /** * Pops the top output buffer off the stack and returns it (changes the current output buffer). */ private Appendable popOutputBuf() { Appendable poppedOutputBuf = outputBufStack.pop(); currOutputBuf = outputBufStack.peek(); return poppedOutputBuf; } /** * This method must only be called by assistant visitors, in particular * RenderVisitorAssistantForMsgs. */ Appendable getCurrOutputBufForUseByAssistants() { return currOutputBuf; } /** * Private helper to render the children of a block into a separate string (not directly appended * to the current output buffer). * * @param block The block whose children are to be rendered. */ private void renderBlock(BlockNode block, Appendable to) { pushOutputBuf(to); visitChildren(block); popOutputBuf(); } private SoyValue renderRenderUnitNode(final RenderUnitNode renderUnitNode) { RenderableThunk thunk = new RenderableThunk() { @Override protected void doRender(Appendable appendable) throws IOException { renderBlock(renderUnitNode, appendable); } }; ContentKind contentKind = renderUnitNode.getContentKind(); if (contentKind != null) { return LazySanitizedContents.forThunk(thunk, contentKind); } else { return StringData.forThunk(thunk); } } /** * Private helper to evaluate an expression. Always use this helper instead of using evalVisitor * directly, because this helper creates and throws a RenderException if there's an error. */ private SoyValue eval(ExprNode expr, SoyNode node) { if (expr == null) { throw RenderException.create("Cannot evaluate expression in V1 syntax.") .addStackTraceElement(node); } // Lazily initialize evalVisitor. if (evalVisitor == null) { evalVisitor = evalVisitorFactory.create(ijData, env); } try { return evalVisitor.exec(expr); } catch (RenderException e) { // RenderExceptions can be thrown when evaluating lazy transclusions. throw RenderException.createFromRenderException( "When evaluating \"" + expr.toSourceString() + "\": " + e.getMessage(), e, node); } catch (Exception e) { throw RenderException.createWithSource( "When evaluating \"" + expr.toSourceString() + "\": " + e.getMessage(), e, node); } } /** * A lazy wrapper around {@link #eval}. * * <p>Useful for {@code {let ...}} and {@code {param ...}} commands where the expression may be * defined before being used. */ private SoyValueProvider lazyEval(final ExprNode expr, final SoyNode node) { return new SoyAbstractCachingValueProvider() { @Override protected SoyValue compute() { return eval(expr, node); } @Override public RenderResult status() { return RenderResult.done(); } }; } /** * This method must only be called by assistant visitors, in particular * RenderVisitorAssistantForMsgs. */ SoyValue evalForUseByAssistants(ExprNode expr, SoyNode node) { return eval(expr, node); } /** Helper to append text to the output, propagating any exceptions. */ static void append(Appendable outputBuf, CharSequence cs) { try { outputBuf.append(cs); } catch (IOException e) { throw new RuntimeException(e); } } /** Helper to append a SoyValue to the output, propagating any exceptions. */ static void append(Appendable outputBuf, SoyValue value, SoyNode node) { try { value.render(outputBuf); } catch (IOException e) { throw new RuntimeException(e); } catch (RenderException e) { throw e.addStackTraceElement(node); } } /** * Protected helper to apply a print directive. * * @param directiveName The name of the directive. * @param value The value to apply the directive on. * @param args The arguments to the directive. * @param node The node with the escaping. Only used for error reporting. * @return The result of applying the directive with the given arguments to the given value. */ private SoyValue applyDirective( String directiveName, SoyValue value, List<SoyValue> args, SoyNode node) { // Get directive. SoyJavaPrintDirective directive = soyJavaDirectivesMap.get(directiveName); if (directive == null) { throw RenderException.createWithSource( "Failed to find Soy print directive with name '" + directiveName + "'" + " (tag " + node.toSourceString() + ")", node); } // TODO: Add a pass to check num args at compile time. if (!directive.getValidArgsSizes().contains(args.size())) { throw RenderException.createWithSource( "Print directive '" + directiveName + "' used with the wrong number of arguments (tag " + node.toSourceString() + ").", node); } try { return directive.applyForJava(value, args); } catch (RuntimeException e) { throw RenderException.createWithSource( String.format( "Failed in applying directive '%s' in tag \"%s\" due to exception: %s", directiveName, node.toSourceString(), e.getMessage()), e, node); } } private void checkStrictParamTypes(TemplateNode node, Collection<TemplateParam> params) { for (TemplateParam param : params) { checkStrictParamType(node, param, env.getVarProvider(param)); } for (TemplateParam param : node.getInjectedParams()) { checkStrictParamType(node, param, env.getVarProvider(param)); } } /** Check that the given {@code paramValue} matches the static type of {@code param}. */ private void checkStrictParamType( final TemplateNode node, final TemplateParam param, @Nullable SoyValueProvider paramValue) { Kind kind = param.type().getKind(); if (kind == Kind.ANY || kind == Kind.UNKNOWN) { // Nothing to check. ANY and UKNOWN match all types. return; } if (paramValue == null) { paramValue = NullData.INSTANCE; } else if (paramValue instanceof SoyAbstractCachingValueProvider) { SoyAbstractCachingValueProvider typedValue = (SoyAbstractCachingValueProvider) paramValue; if (!typedValue.isComputed()) { // in order to preserve laziness we tell the value provider to assert the type when // computation is triggered typedValue.addValueAssertion( new ValueAssertion() { @Override public void check(SoyValue value) { checkValueType(param, value, node); } }); return; } } checkValueType(param, paramValue.resolve(), node); } /** Check that the value matches the given param type. */ private void checkValueType(TemplateParam param, SoyValue value, TemplateNode node) { if (!param.type().isInstance(value)) { // should this be a soydataexception? throw RenderException.createWithSource( "Parameter type mismatch: attempt to bind value '" + (value instanceof UndefinedData ? "(undefined)" : value) + "' to parameter '" + param.name() + "' which has declared type '" + param.type() + "'.", node); } } }