/*
* 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.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.BaseUtils;
import com.google.template.soy.base.internal.Identifier;
import com.google.template.soy.basetree.CopyState;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
import com.google.template.soy.soytree.SoyNode.RenderUnitNode;
import com.google.template.soy.soytree.defn.HeaderParam;
import com.google.template.soy.soytree.defn.TemplateParam;
import com.google.template.soy.soytree.defn.TemplateParam.DeclLoc;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
/**
* Node representing a template.
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
*/
public abstract class TemplateNode extends AbstractBlockCommandNode implements RenderUnitNode {
/** Priority for delegate templates. */
public enum Priority {
STANDARD(0),
HIGH_PRIORITY(1);
private final int value;
Priority(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return Integer.toString(value);
}
}
// TODO(sparhami): Add error for unused alias. Maybe make SoyParsingContext collect usages?
private static final SoyErrorKind INVALID_ALIAS_FOR_LAST_PART_OF_NAMESPACE =
SoyErrorKind.of(
"Not allowed to alias the last part of the file''s namespace ({0}) "
+ "to another namespace ({1}).");
private static final SoyErrorKind DUPLICATE_ALIAS =
SoyErrorKind.of("Duplicate alias definition ''{0}''.");
/**
* Info from the containing Soy file's {@code delpackage} and {@code namespace} declarations.
*
* <p>Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p>Note: Currently, there are only 2 delegate priority values: 0 and 1. Delegate templates that
* are not in a delegate package are given priority 0 (lowest). Delegate templates in a delegate
* package are given priority 1. There is currently no syntax for the user to override these
* default priority values.
*/
@Immutable
public static class SoyFileHeaderInfo {
/** Map from aliases to namespaces for this file. */
public final ImmutableMap<String, String> aliasToNamespaceMap;
/** Map from aliases to namespaces for this file. */
public final ImmutableList<AliasDeclaration> aliasDeclarations;
@Nullable public final String delPackageName;
final Priority priority;
@Nullable public final String namespace;
public final AutoescapeMode defaultAutoescapeMode;
public final StrictHtmlMode strictHtmlMode;
public SoyFileHeaderInfo(
ErrorReporter errorReporter,
@Nullable Identifier delpackageName,
NamespaceDeclaration namespaceDeclaration,
Collection<AliasDeclaration> aliases) {
this(
delpackageName == null ? null : delpackageName.identifier(),
namespaceDeclaration.getNamespace(),
namespaceDeclaration.getDefaultAutoescapeMode(),
namespaceDeclaration.getStrictHtmlMode(),
createAliasMap(errorReporter, namespaceDeclaration, aliases),
ImmutableList.copyOf(aliases));
}
@VisibleForTesting
public SoyFileHeaderInfo(String namespace) {
this(
null,
namespace,
AutoescapeMode.NONCONTEXTUAL,
StrictHtmlMode.UNSET, // Don't use stricthtml mode for testing.
ImmutableMap.<String, String>of(),
ImmutableList.<AliasDeclaration>of());
}
private SoyFileHeaderInfo(
@Nullable String delPackageName,
String namespace,
AutoescapeMode defaultAutoescapeMode,
StrictHtmlMode strictHtmlMode,
ImmutableMap<String, String> aliasToNamespaceMap,
ImmutableList<AliasDeclaration> aliasDeclarations) {
this.delPackageName = delPackageName;
this.priority = (delPackageName == null) ? Priority.STANDARD : Priority.HIGH_PRIORITY;
this.namespace = namespace;
this.defaultAutoescapeMode = defaultAutoescapeMode;
this.strictHtmlMode = strictHtmlMode;
this.aliasToNamespaceMap = aliasToNamespaceMap;
this.aliasDeclarations = aliasDeclarations;
}
private static ImmutableMap<String, String> createAliasMap(
ErrorReporter errorReporter,
NamespaceDeclaration namespaceDeclaration,
Collection<AliasDeclaration> aliases) {
Map<String, String> map = Maps.newLinkedHashMap();
String aliasForFileNamespace =
BaseUtils.extractPartAfterLastDot(namespaceDeclaration.getNamespace());
for (AliasDeclaration aliasDeclaration : aliases) {
String aliasNamespace = aliasDeclaration.getNamespace();
String alias = aliasDeclaration.getAlias();
if (alias.equals(aliasForFileNamespace)
&& !aliasNamespace.equals(namespaceDeclaration.getNamespace())) {
errorReporter.report(
aliasDeclaration.getLocation(),
INVALID_ALIAS_FOR_LAST_PART_OF_NAMESPACE,
namespaceDeclaration.getNamespace(),
aliasNamespace);
}
if (map.containsKey(alias)) {
errorReporter.report(aliasDeclaration.getLocation(), DUPLICATE_ALIAS, alias);
}
map.put(alias, aliasNamespace);
}
return ImmutableMap.copyOf(map);
}
}
// -----------------------------------------------------------------------------------------------
// TemplateNode body.
/** Info from the containing Soy file's header declarations. */
private final SoyFileHeaderInfo soyFileHeaderInfo;
/** This template's name. */
private final String templateName;
/** This template's partial name. Only applicable for V2. */
@Nullable private final String partialTemplateName;
/** A string suitable for display in user msgs as the template name. */
private final String templateNameForUserMsgs;
/** Visibility of this template. */
private final Visibility visibility;
/** The mode of autoescaping for this template. */
private final AutoescapeMode autoescapeMode;
/** Strict mode context. Nonnull iff autoescapeMode is strict. */
@Nullable private final ContentKind contentKind;
/** Required CSS namespaces. */
private final ImmutableList<String> requiredCssNamespaces;
/** Base namespace for package-relative class names. */
private final String cssBaseNamespace;
/** The full SoyDoc, including the start/end tokens, or null. */
private String soyDoc;
/** The description portion of the SoyDoc (before declarations), or null. */
private String soyDocDesc;
/** If the template is using strict html mode. */
private final boolean strictHtml;
/** The params from template header or SoyDoc. Null if no decls and no SoyDoc. */
@Nullable private ImmutableList<TemplateParam> params;
/** The injected params from template header. Null if no decls. */
@Nullable private ImmutableList<TemplateParam> injectedParams;
private int maxLocalVariableTableSize = -1;
/**
* Main constructor. This is package-private because Template*Node instances should be built using
* the Template*NodeBuilder classes.
*
* @param nodeBuilder Builder containing template initialization params.
* @param cmdName The command name.
* @param soyFileHeaderInfo Info from the containing Soy file's header declarations.
* @param visibility Visibility of this template.
* @param params The params from template header or SoyDoc. Null if no decls and no SoyDoc.
*/
TemplateNode(
TemplateNodeBuilder nodeBuilder,
String cmdName,
SoyFileHeaderInfo soyFileHeaderInfo,
Visibility visibility,
@Nullable ImmutableList<TemplateParam> params) {
super(nodeBuilder.getId(), nodeBuilder.sourceLocation, cmdName, nodeBuilder.getCmdText());
maybeSetSyntaxVersionUpperBound(nodeBuilder.getSyntaxVersionBound());
this.soyFileHeaderInfo = soyFileHeaderInfo;
this.templateName = nodeBuilder.getTemplateName();
this.partialTemplateName = nodeBuilder.getPartialTemplateName();
this.templateNameForUserMsgs = nodeBuilder.getTemplateNameForUserMsgs();
this.visibility = visibility;
this.autoescapeMode = nodeBuilder.getAutoescapeMode();
this.contentKind = nodeBuilder.getContentKind();
this.requiredCssNamespaces = nodeBuilder.getRequiredCssNamespaces();
this.cssBaseNamespace = nodeBuilder.getCssBaseNamespace();
this.soyDoc = nodeBuilder.getSoyDoc();
this.soyDocDesc = nodeBuilder.getSoyDocDesc();
if (nodeBuilder.getStrictHtmlMode() != StrictHtmlMode.UNSET) {
// use the value that is explicitly set in template.
this.strictHtml = nodeBuilder.getStrictHtmlMode() == StrictHtmlMode.YES;
} else if (soyFileHeaderInfo.strictHtmlMode != StrictHtmlMode.UNSET
&& contentKind == ContentKind.HTML) {
// If the value is not set, HTML templates will inherit from namespace declaration.
this.strictHtml = soyFileHeaderInfo.strictHtmlMode == StrictHtmlMode.YES;
} else {
// Default value is false.
this.strictHtml = false;
}
// Split out @inject params into a separate list because we don't want them
// to be visible to code that looks at the template's calling signature.
ImmutableList.Builder<TemplateParam> regularParams = ImmutableList.builder();
ImmutableList.Builder<TemplateParam> injectedParams = ImmutableList.builder();
if (params != null) {
for (TemplateParam param : params) {
if (param.isInjected()) {
injectedParams.add(param);
} else {
regularParams.add(param);
}
}
}
// Note: These used to be nullable, but now return an empty list.
this.params = regularParams.build();
this.injectedParams = injectedParams.build();
}
/**
* Copy constructor.
* @param orig The node to copy.
*/
protected TemplateNode(TemplateNode orig, CopyState copyState) {
super(orig, copyState);
this.soyFileHeaderInfo = orig.soyFileHeaderInfo; // immutable
this.templateName = orig.templateName;
this.partialTemplateName = orig.partialTemplateName;
this.templateNameForUserMsgs = orig.templateNameForUserMsgs;
this.visibility = orig.visibility;
this.autoescapeMode = orig.autoescapeMode;
this.contentKind = orig.contentKind;
this.requiredCssNamespaces = orig.requiredCssNamespaces;
this.cssBaseNamespace = orig.cssBaseNamespace;
this.soyDoc = orig.soyDoc;
this.soyDocDesc = orig.soyDocDesc;
// TODO(lukes): params and injectedParams are not really immutable, just mostly. Consider
// cloning them here and modifying SoyTreeUtils.cloneNode to reassign these as well.
this.params = orig.params; // immutable
this.injectedParams = orig.injectedParams;
this.maxLocalVariableTableSize = orig.maxLocalVariableTableSize;
this.strictHtml = orig.strictHtml;
}
/** Returns info from the containing Soy file's header declarations. */
public SoyFileHeaderInfo getSoyFileHeaderInfo() {
return soyFileHeaderInfo;
}
/** Returns the name of the containing delegate package, or null if none. */
public String getDelPackageName() {
return soyFileHeaderInfo.delPackageName;
}
/** Returns a template name suitable for display in user msgs. */
public String getTemplateNameForUserMsgs() {
return templateNameForUserMsgs;
}
/** Returns this template's name. */
public String getTemplateName() {
return templateName;
}
/** Returns this template's partial name. Only applicable for V2 (null for V1). */
@Nullable
public String getPartialTemplateName() {
return partialTemplateName;
}
/** Returns the visibility of this template. */
public Visibility getVisibility() {
return visibility;
}
/** Returns the mode of autoescaping, if any, done for this template. */
public AutoescapeMode getAutoescapeMode() {
return autoescapeMode;
}
/** Returns if this template is in strict html mode. */
public boolean isStrictHtml() {
return strictHtml;
}
/** Returns the content kind for strict autoescaping. Nonnull iff autoescapeMode is strict. */
@Override
@Nullable
public ContentKind getContentKind() {
return contentKind;
}
/**
* Returns required CSS namespaces.
*
* <p>CSS "namespaces" are monikers associated with CSS files that by convention, dot-separated
* lowercase names. They don't correspond to CSS features, but are processed by external tools
* that impose dependencies from templates to CSS.
*/
public ImmutableList<String> getRequiredCssNamespaces() {
return requiredCssNamespaces;
}
/**
* Returns the base CSS namespace for resolving package-relative class names. Package relative
* class names are ones beginning with a percent (%). The compiler will replace the percent sign
* with the name of the current CSS package converted to camel-case form.
*
* <p>Packages are defined using dotted-id syntax (foo.bar), which is identical to the syntax for
* required CSS namespaces. If no base CSS namespace is defined, it will use the first required
* css namespace, if any are present. If there is no base CSS name, and no required css
* namespaces, then use of package-relative class names will be reported as an error.
*/
public String getCssBaseNamespace() {
return cssBaseNamespace;
}
public void setMaxLocalVariableTableSize(int size) {
this.maxLocalVariableTableSize = size;
}
public int getMaxLocalVariableTableSize() {
return maxLocalVariableTableSize;
}
/** Clears the SoyDoc text, description, and param descriptions. */
public void clearSoyDocStrings() {
soyDoc = null;
soyDocDesc = null;
assert params != null; // prevent warnings
List<TemplateParam> newParams = Lists.newArrayListWithCapacity(params.size());
for (TemplateParam origParam : params) {
newParams.add(origParam.copyEssential());
}
params = ImmutableList.copyOf(newParams);
}
/** Returns the SoyDoc, or null. */
@Nullable
public String getSoyDoc() {
return soyDoc;
}
/** Returns the description portion of the SoyDoc (before @param tags), or null. */
@Nullable
public String getSoyDocDesc() {
return soyDocDesc;
}
/** Returns the params from template header or SoyDoc. */
public List<TemplateParam> getParams() {
return params;
}
/** Returns the injected params from template header. */
public List<TemplateParam> getInjectedParams() {
return injectedParams;
}
/** Returns all params from template header or SoyDoc, both regular and injected. */
@Nullable
public Iterable<TemplateParam> getAllParams() {
return Iterables.concat(params, injectedParams);
}
@Override
public SoyFileNode getParent() {
return (SoyFileNode) super.getParent();
}
@Override
public String toSourceString() {
StringBuilder sb = new StringBuilder();
// SoyDoc.
if (soyDoc != null) {
sb.append(soyDoc).append("\n");
}
// Begin tag.
sb.append(getTagString()).append("\n");
// Header.
if (params != null) {
for (TemplateParam param : params) {
if (param.declLoc() != DeclLoc.HEADER) {
continue;
}
HeaderParam headerParam = (HeaderParam) param;
sb.append(" {@param");
if (!headerParam.isRequired()) {
sb.append('?');
}
sb.append(' ')
.append(headerParam.name())
.append(": ")
.append(headerParam.type())
.append("}");
if (headerParam.desc() != null) {
sb.append(" /** ").append(headerParam.desc()).append(" */");
}
sb.append("\n");
}
}
// Body.
// If first or last char of template body is a space, must be turned into '{sp}'.
StringBuilder bodySb = new StringBuilder();
appendSourceStringForChildren(bodySb);
int bodyLen = bodySb.length();
if (bodyLen != 0) {
if (bodyLen != 1 && bodySb.charAt(bodyLen - 1) == ' ') {
bodySb.replace(bodyLen - 1, bodyLen, "{sp}");
}
if (bodySb.charAt(0) == ' ') {
bodySb.replace(0, 1, "{sp}");
}
}
sb.append(bodySb);
sb.append("\n");
// End tag.
sb.append("{/").append(getCommandName()).append("}\n");
return sb.toString();
}
/**
* Construct a StackTraceElement that will point to the given source location of the current
* template.
*/
public StackTraceElement createStackTraceElement(SourceLocation srcLocation) {
if (partialTemplateName == null) {
// V1 soy templates.
return new StackTraceElement(
/* declaringClass */ "(UnknownSoyNamespace)",
/* methodName */ templateName,
srcLocation.getFileName(),
srcLocation.getBeginLine());
} else {
// V2 soy templates.
return new StackTraceElement(
/* declaringClass */ soyFileHeaderInfo.namespace,
// The partial template name begins with a '.' that causes the stack trace element to
// print "namespace..templateName" otherwise.
/* methodName */ partialTemplateName.substring(1),
srcLocation.getFileName(),
srcLocation.getBeginLine());
}
}
}