/* * 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.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.template.soy.base.SourceLocation; import com.google.template.soy.base.internal.BaseUtils; import com.google.template.soy.basetree.CopyState; import com.google.template.soy.error.ErrorReporter.Checkpoint; 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.ExprNode; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.exprtree.StringNode; import com.google.template.soy.soytree.CommandTextAttributesParser.Attribute; import com.google.template.soy.soytree.defn.TemplateParam; import java.util.Collection; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; /** * Node representing a call to a delegate template. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * */ public final class CallDelegateNode extends CallNode { private static final SoyErrorKind MISSING_CALLEE_NAME = SoyErrorKind.of( "The ''delcall'' command text must contain the callee name " + "(encountered command text \"{0}\")."); public static final SoyErrorKind INVALID_DELEGATE_NAME = SoyErrorKind.of("Invalid delegate name \"{0}\" for ''delcall'' command."); private static final SoyErrorKind INVALID_VARIANT_EXPRESSION = SoyErrorKind.of( "Invalid variant expression \"{0}\" in ''delcall''" + " (variant expression must evaluate to an identifier)."); /** * Private helper class used by constructors. Encapsulates all the info derived from the command * text. */ @Immutable private static class CommandTextInfo extends CallNode.CommandTextInfo { public final String delCalleeName; @Nullable public final ExprRootNode delCalleeVariantExpr; public final Boolean allowsEmptyDefault; public CommandTextInfo( String commandText, String delCalleeName, @Nullable ExprRootNode delCalleeVariantExpr, Boolean allowsEmptyDefault, DataAttribute dataAttr, @Nullable String userSuppliedPlaceholderName) { super(commandText, dataAttr, userSuppliedPlaceholderName, null); this.delCalleeName = delCalleeName; this.delCalleeVariantExpr = delCalleeVariantExpr; this.allowsEmptyDefault = allowsEmptyDefault; } } /** Pattern for a callee name. */ private static final Pattern NONATTRIBUTE_CALLEE_NAME = Pattern.compile("^\\s* ([.\\w]+) (?= \\s | $)", Pattern.COMMENTS); /** Parser for the command text. */ private static final CommandTextAttributesParser ATTRIBUTES_PARSER = new CommandTextAttributesParser( "delcall", new Attribute("variant", Attribute.ALLOW_ALL_VALUES, null), new Attribute("data", Attribute.ALLOW_ALL_VALUES, null), new Attribute("allowemptydefault", Attribute.BOOLEAN_VALUES, null)); /** The name of the delegate template being called. */ private final String delCalleeName; /** The variant expression for the delegate being called, or null. */ @Nullable private final ExprRootNode delCalleeVariantExpr; /** * User-specified value of whether this delegate call defaults to empty string if there's no * active implementation, or null if the attribute is not specified. */ private Boolean allowsEmptyDefault; /** * The list of params that need to be type checked when this node is run on a per delegate basis. * All the params that could be statically verified will be checked up front (by the {@code * CheckCallingParamTypesVisitor}), this list contains the params that could not be statically * checked. * * <p>NOTE:This list will be a subset of the params of the callee, not a subset of the params * passed from this caller. */ private ImmutableMap<TemplateDelegateNode, ImmutableList<TemplateParam>> paramsToRuntimeCheckByDelegate; public static final class Builder extends CallNode.Builder { private static CallDelegateNode error() { return new Builder(-1, SourceLocation.UNKNOWN) .commandText("error.error") .build(SoyParsingContext.exploding()); // guaranteed to be valid } private final int id; private final SourceLocation sourceLocation; private boolean allowEmptyDefault; private DataAttribute dataAttribute = DataAttribute.none(); private ImmutableList<String> escapingDirectiveNames = ImmutableList.of(); @Nullable private String commandText; @Nullable private String delCalleeName; @Nullable private ExprRootNode delCalleeVariantExpr; @Nullable private String userSuppliedPlaceholderName; public Builder(int id, SourceLocation sourceLocation) { this.id = id; this.sourceLocation = sourceLocation; } public Builder allowEmptyDefault(boolean allowEmptyDefault) { this.allowEmptyDefault = allowEmptyDefault; return this; } @Override public SourceLocation getSourceLocation() { return sourceLocation; } @Override public Builder commandText(String commandText) { this.commandText = commandText; return this; } public Builder delCalleeName(String delCalleeName) { this.delCalleeName = delCalleeName; return this; } public Builder delCalleeVariantExpr(ExprRootNode delCalleeVariantExpr) { this.delCalleeVariantExpr = delCalleeVariantExpr; return this; } public Builder escapingDirectiveNames(ImmutableList<String> escapingDirectiveNames) { this.escapingDirectiveNames = escapingDirectiveNames; return this; } public Builder dataAttribute(DataAttribute dataAttribute) { this.dataAttribute = dataAttribute; return this; } @Override public Builder userSuppliedPlaceholderName(String userSuppliedPlaceholderName) { this.userSuppliedPlaceholderName = userSuppliedPlaceholderName; return this; } @Override public CallDelegateNode build(SoyParsingContext context) { Checkpoint checkpoint = context.errorReporter().checkpoint(); CommandTextInfo commandTextInfo = commandText != null ? parseCommandText(context) : buildCommandText(); if (context.errorReporter().errorsSince(checkpoint)) { return error(); } CallDelegateNode callDelegateNode = new CallDelegateNode(id, sourceLocation, commandTextInfo, escapingDirectiveNames); return callDelegateNode; } private CommandTextInfo parseCommandText(SoyParsingContext context) { String commandTextWithoutPhnameAttr = this.commandText; String commandText = commandTextWithoutPhnameAttr + ((userSuppliedPlaceholderName != null) ? " phname=\"" + userSuppliedPlaceholderName + "\"" : ""); // Handle callee name not listed as an attribute. Matcher ncnMatcher = NONATTRIBUTE_CALLEE_NAME.matcher(commandTextWithoutPhnameAttr); String delCalleeName; if (ncnMatcher.find()) { delCalleeName = ncnMatcher.group(1); if (!BaseUtils.isDottedIdentifier(delCalleeName)) { context.report(sourceLocation, INVALID_DELEGATE_NAME, delCalleeName); } commandTextWithoutPhnameAttr = commandTextWithoutPhnameAttr.substring(ncnMatcher.end()).trim(); } else { delCalleeName = null; context.report(sourceLocation, MISSING_CALLEE_NAME, commandText); } Map<String, String> attributes = ATTRIBUTES_PARSER.parse(commandTextWithoutPhnameAttr, context, sourceLocation); String variantExprText = attributes.get("variant"); ExprRootNode delCalleeVariantExpr; if (variantExprText == null) { delCalleeVariantExpr = null; } else { ExprNode expr = new ExpressionParser(variantExprText, sourceLocation, context).parseExpression(); // If the variant is a fixed string, do a sanity check. if (expr instanceof StringNode) { String fixedVariantStr = ((StringNode) expr).getValue(); if (!BaseUtils.isIdentifier(fixedVariantStr)) { context.report(sourceLocation, INVALID_VARIANT_EXPRESSION, variantExprText); } } delCalleeVariantExpr = new ExprRootNode(expr); } DataAttribute dataAttrInfo = parseDataAttributeHelper(attributes.get("data"), sourceLocation, context); String allowemptydefaultAttr = attributes.get("allowemptydefault"); Boolean allowsEmptyDefault = (allowemptydefaultAttr == null) ? null : allowemptydefaultAttr.equals("true"); return new CommandTextInfo( commandText, delCalleeName, delCalleeVariantExpr, allowsEmptyDefault, dataAttrInfo, userSuppliedPlaceholderName); } private CommandTextInfo buildCommandText() { Preconditions.checkArgument(BaseUtils.isDottedIdentifier(delCalleeName)); String commandText = ""; commandText += delCalleeName; if (dataAttribute.isPassingAllData()) { commandText += " data=\"all\""; } else if (dataAttribute.isPassingData()) { assert dataAttribute.dataExpr() != null; // suppress warnings commandText += " data=\"" + dataAttribute.dataExpr().toSourceString() + '"'; } if (userSuppliedPlaceholderName != null) { commandText += " phname=\"" + userSuppliedPlaceholderName + '"'; } return new CommandTextInfo( commandText, delCalleeName, delCalleeVariantExpr, allowEmptyDefault, dataAttribute, userSuppliedPlaceholderName); } } private CallDelegateNode( int id, SourceLocation sourceLocation, CommandTextInfo commandTextInfo, ImmutableList<String> escapingDirectiveNames) { super(id, sourceLocation, "delcall", commandTextInfo, escapingDirectiveNames); this.delCalleeName = commandTextInfo.delCalleeName; this.delCalleeVariantExpr = commandTextInfo.delCalleeVariantExpr; this.allowsEmptyDefault = commandTextInfo.allowsEmptyDefault; } /** * Copy constructor. * * @param orig The node to copy. */ @SuppressWarnings("ConstantConditions") // for IntelliJ private CallDelegateNode(CallDelegateNode orig, CopyState copyState) { super(orig, copyState); this.delCalleeName = orig.delCalleeName; this.delCalleeVariantExpr = (orig.delCalleeVariantExpr != null) ? orig.delCalleeVariantExpr.copy(copyState) : null; this.allowsEmptyDefault = orig.allowsEmptyDefault; this.paramsToRuntimeCheckByDelegate = orig.paramsToRuntimeCheckByDelegate; } @Override public Kind getKind() { return Kind.CALL_DELEGATE_NODE; } /** Returns the name of the delegate template being called. */ public String getDelCalleeName() { return delCalleeName; } /** Returns the variant expression for the delegate being called, or null if it's a string. */ @Nullable public ExprRootNode getDelCalleeVariantExpr() { return delCalleeVariantExpr; } /** * Sets the template params that require runtime type checking for each possible delegate target. */ public void setParamsToRuntimeCheck( ImmutableMap<TemplateDelegateNode, ImmutableList<TemplateParam>> paramsToRuntimeCheck) { this.paramsToRuntimeCheckByDelegate = Preconditions.checkNotNull(paramsToRuntimeCheck); } @Override public Collection<TemplateParam> getParamsToRuntimeCheck(TemplateNode callee) { if (paramsToRuntimeCheckByDelegate == null) { return callee.getParams(); } ImmutableList<TemplateParam> params = paramsToRuntimeCheckByDelegate.get(callee); if (params == null) { // The callee was not known when we performed static type checking. Check all params. return callee.getParams(); } return params; } /** Returns whether this delegate call defaults to empty string if there's no active impl. */ public boolean allowsEmptyDefault() { // Default to 'false' if not specified. if (allowsEmptyDefault == null) { return false; } return allowsEmptyDefault; } @Override public ImmutableList<ExprRootNode> getExprList() { ImmutableList.Builder<ExprRootNode> allExprs = ImmutableList.builder(); if (delCalleeVariantExpr != null) { allExprs.add(delCalleeVariantExpr); } allExprs.addAll(super.getExprList()); return allExprs.build(); } @Override public CallDelegateNode copy(CopyState copyState) { return new CallDelegateNode(this, copyState); } }