/* * 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.tofu.internal; import com.google.common.base.Preconditions; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSortedSet; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import com.google.template.soy.data.SanitizedContent; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.SoyValueConverter; import com.google.template.soy.data.UnsafeSanitizedContentOrdainer; import com.google.template.soy.data.internalutils.NodeContentKinds; import com.google.template.soy.msgs.SoyMsgBundle; import com.google.template.soy.parseinfo.SoyTemplateInfo; import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.shared.SoyIdRenamingMap; import com.google.template.soy.shared.internal.ApiCallScopeUtils; import com.google.template.soy.shared.internal.GuiceSimpleScope; import com.google.template.soy.shared.internal.GuiceSimpleScope.InScope; import com.google.template.soy.shared.restricted.ApiCallScopeBindingAnnotations.ApiCall; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.google.template.soy.shared.restricted.SoyPrintDirective; import com.google.template.soy.sharedpasses.render.RenderException; import com.google.template.soy.sharedpasses.render.RenderVisitor; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.TemplateRegistry; import com.google.template.soy.soytree.Visibility; import com.google.template.soy.tofu.SoyTofu; import com.google.template.soy.tofu.SoyTofuException; import java.util.Map; import javax.annotation.Nullable; /** * Represents a compiled Soy file set. This is the result of compiling Soy to a Java object. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). * */ public class BaseTofu implements SoyTofu { /** * Injectable factory for creating an instance of this class. * * <p>Important: Do not use outside of Soy code (treat as superpackage-private). */ public interface BaseTofuFactory { /** * @param templates The full set of templates. * @param templateToIjParamsInfoMap the ij params for each template. * @param printDirectives The map of print directives. */ BaseTofu create( TemplateRegistry templates, ImmutableMap<String, ImmutableSortedSet<String>> templateToIjParamsInfoMap, ImmutableMap<String, ? extends SoyPrintDirective> printDirectives); } private final SoyValueConverter valueConverter; /** The scope object that manages the API call scope. */ private final GuiceSimpleScope apiCallScope; private final TofuRenderVisitorFactory tofuRenderVisitorFactory; private final TemplateRegistry templateRegistry; private final ImmutableMap<String, ImmutableSortedSet<String>> templateToIjParamsInfoMap; private final ImmutableMap<String, ? extends SoyJavaPrintDirective> printDirectives; /** * @param valueConverter Instance of SoyValueConverter to use. * @param apiCallScope The scope object that manages the API call scope. * @param tofuRenderVisitorFactory Factory for creating an instance of TofuRenderVisitor. */ @AssistedInject public BaseTofu( SoyValueConverter valueConverter, @ApiCall GuiceSimpleScope apiCallScope, TofuRenderVisitorFactory tofuRenderVisitorFactory, @Assisted TemplateRegistry templates, @Assisted ImmutableMap<String, ImmutableSortedSet<String>> templateToIjParamsInfoMap, @Assisted ImmutableMap<String, ? extends SoyPrintDirective> printDirectives) { this.valueConverter = valueConverter; this.apiCallScope = apiCallScope; this.tofuRenderVisitorFactory = tofuRenderVisitorFactory; this.templateRegistry = templates; this.templateToIjParamsInfoMap = templateToIjParamsInfoMap; ImmutableMap.Builder<String, SoyJavaPrintDirective> builder = ImmutableMap.builder(); for (Map.Entry<String, ? extends SoyPrintDirective> entry : printDirectives.entrySet()) { if (entry.getValue() instanceof SoyJavaPrintDirective) { builder.put(entry.getKey(), (SoyJavaPrintDirective) entry.getValue()); } } this.printDirectives = builder.build(); } /** * {@inheritDoc} * * <p>For objects of this class, the namespace is always null. */ @Override public String getNamespace() { return null; } @Override public SoyTofu forNamespace(@Nullable String namespace) { return (namespace == null) ? this : new NamespacedTofu(this, namespace); } @Override public Renderer newRenderer(SoyTemplateInfo templateInfo) { return new RendererImpl(this, templateInfo.getName()); } @Override public Renderer newRenderer(String templateName) { return new RendererImpl(this, templateName); } @Override public ImmutableSortedSet<String> getUsedIjParamsForTemplate(SoyTemplateInfo templateInfo) { return getUsedIjParamsForTemplate(templateInfo.getName()); } @Override public ImmutableSortedSet<String> getUsedIjParamsForTemplate(String templateName) { ImmutableSortedSet<String> ijParams = templateToIjParamsInfoMap.get(templateName); if (ijParams == null) { throw new SoyTofuException("Template '" + templateName + "' not found."); } // TODO: Ideally we'd check that there are no external calls, but we find that in practice many // users have written templates that conditionally call to undefined templates. Instead, // we'll return a best effor set of what we have here, and over time, we'll encourage users to // enforce the "assertNoExternalCalls" flag. return ijParams; } // ----------------------------------------------------------------------------------------------- // Private methods. /** * @param outputBuf The Appendable to write the output to. * @param templateName The full name of the template to render. * @param data The data to call the template with. Can be null if the template has no parameters. * @param ijData The injected data to call the template with. Can be null if not used. * @param activeDelPackageNames The set of active delegate package names, or null if none. * @param msgBundle The bundle of translated messages, or null to use the messages from the Soy * source. * @param cssRenamingMap Map for renaming selectors in 'css' tags, or null if not used. * @return The template that was rendered. */ private TemplateNode renderMain( Appendable outputBuf, String templateName, @Nullable SoyRecord data, @Nullable SoyRecord ijData, @Nullable Predicate<String> activeDelPackageNames, @Nullable SoyMsgBundle msgBundle, @Nullable SoyIdRenamingMap idRenamingMap, @Nullable SoyCssRenamingMap cssRenamingMap) { if (activeDelPackageNames == null) { activeDelPackageNames = Predicates.alwaysFalse(); } try (InScope inScope = apiCallScope.enter()) { // Seed the scoped parameters. ApiCallScopeUtils.seedSharedParams(inScope, msgBundle); // Do the rendering. return renderMainHelper( templateRegistry, outputBuf, templateName, data, ijData, activeDelPackageNames, msgBundle, idRenamingMap, cssRenamingMap); } } /** * Renders a template and appends the result to a StringBuilder. * * @param templateRegistry A registry of all templates. * @param outputBuf The Appendable to append the rendered text to. * @param templateName The full name of the template to render. * @param data The data to call the template with. Can be null if the template has no parameters. * @param ijData The injected data to call the template with. Can be null if not used. * @param activeDelPackageNames The set of active delegate package names. * @param msgBundle The bundle of translated messages, or null to use the messages from the Soy * source. * @param cssRenamingMap Map for renaming selectors in 'css' tags, or null if not used. * @return The template that was rendered. */ private TemplateNode renderMainHelper( TemplateRegistry templateRegistry, Appendable outputBuf, String templateName, @Nullable SoyRecord data, @Nullable SoyRecord ijData, Predicate<String> activeDelPackageNames, @Nullable SoyMsgBundle msgBundle, @Nullable SoyIdRenamingMap idRenamingMap, @Nullable SoyCssRenamingMap cssRenamingMap) { TemplateNode template = templateRegistry.getBasicTemplate(templateName); if (template == null) { throw new SoyTofuException("Attempting to render undefined template '" + templateName + "'."); } else if (template.getVisibility() == Visibility.PRIVATE) { throw new SoyTofuException("Attempting to render private template '" + templateName + "'."); } if (data == null) { data = SoyValueConverter.EMPTY_DICT; } if (ijData == null) { ijData = SoyValueConverter.EMPTY_DICT; } try { RenderVisitor rv = tofuRenderVisitorFactory.create( outputBuf, templateRegistry, printDirectives, data, ijData, activeDelPackageNames, msgBundle, idRenamingMap, cssRenamingMap); rv.exec(template); } catch (RenderException re) { throw new SoyTofuException(re); } return template; } // ----------------------------------------------------------------------------------------------- // Renderer implementation. /** Simple implementation of the Renderer interface. */ private static class RendererImpl implements Renderer { private final BaseTofu baseTofu; private final String templateName; private SoyRecord data; private SoyRecord ijData; private SoyMsgBundle msgBundle; private SoyIdRenamingMap idRenamingMap; private SoyCssRenamingMap cssRenamingMap; private Predicate<String> activeDelPackageNames; private SanitizedContent.ContentKind expectedContentKind; private boolean contentKindExplicitlySet; /** * @param baseTofu The underlying BaseTofu object used to perform the rendering. * @param templateName The full template name (including namespace). */ public RendererImpl(BaseTofu baseTofu, String templateName) { this.baseTofu = baseTofu; this.templateName = templateName; this.data = null; this.ijData = null; this.activeDelPackageNames = null; this.msgBundle = null; this.cssRenamingMap = null; this.idRenamingMap = null; this.expectedContentKind = SanitizedContent.ContentKind.HTML; this.contentKindExplicitlySet = false; } @Override public Renderer setData(Map<String, ?> data) { this.data = (data == null) ? null : baseTofu.valueConverter.newDictFromMap(data); return this; } @Override public Renderer setData(SoyRecord data) { this.data = data; return this; } @Override public Renderer setIjData(Map<String, ?> ijData) { this.ijData = (ijData == null) ? null : baseTofu.valueConverter.newDictFromMap(ijData); return this; } @Override public Renderer setIjData(SoyRecord ijData) { this.ijData = ijData; return this; } @Override public Renderer setActiveDelegatePackageSelector(Predicate<String> activeDelegatePackageNames) { this.activeDelPackageNames = activeDelegatePackageNames; return this; } @Override public Renderer setMsgBundle(SoyMsgBundle msgBundle) { this.msgBundle = msgBundle; return this; } @Override public Renderer setIdRenamingMap(SoyIdRenamingMap idRenamingMap) { this.idRenamingMap = idRenamingMap; return this; } @Override public Renderer setCssRenamingMap(SoyCssRenamingMap cssRenamingMap) { this.cssRenamingMap = cssRenamingMap; return this; } @Override public Renderer setContentKind(SanitizedContent.ContentKind contentKind) { this.expectedContentKind = Preconditions.checkNotNull(contentKind); this.contentKindExplicitlySet = true; return this; } @Override public String render() { StringBuilder sb = new StringBuilder(); render(sb); return sb.toString(); } @Override public SanitizedContent.ContentKind render(Appendable out) { TemplateNode template = baseTofu.renderMain( out, templateName, data, ijData, activeDelPackageNames, msgBundle, idRenamingMap, cssRenamingMap); if (contentKindExplicitlySet || template.getContentKind() != null) { // Enforce the content kind if: // - The caller explicitly set a content kind to validate. // - The template is strict. This avoids accidentally using a text strict template in a // place where HTML was implicitly expected. enforceContentKind(template); } return template.getContentKind(); } @Override public SanitizedContent renderStrict() { StringBuilder sb = new StringBuilder(); TemplateNode template = baseTofu.renderMain( sb, templateName, data, ijData, activeDelPackageNames, msgBundle, idRenamingMap, cssRenamingMap); enforceContentKind(template); // Use the expected instead of actual content kind; that way, if an HTML template is rendered // as TEXT, we will return TEXT. return UnsafeSanitizedContentOrdainer.ordainAsSafe(sb.toString(), expectedContentKind); } private void enforceContentKind(TemplateNode template) { if (expectedContentKind == SanitizedContent.ContentKind.TEXT) { // Allow any template to be called as text. This is consistent with the fact that // kind="text" templates can call any other template. return; } if (template.getContentKind() == null) { throw new SoyTofuException( "Expected template to be autoescape=\"strict\" " + "but was autoescape=\"" + template.getAutoescapeMode().getAttributeValue() + "\": " + template.getTemplateName()); } if (expectedContentKind != template.getContentKind()) { throw new SoyTofuException( "Expected template to be kind=\"" + NodeContentKinds.toAttributeValue(expectedContentKind) + "\" but was kind=\"" + NodeContentKinds.toAttributeValue(template.getContentKind()) + "\": " + template.getTemplateName()); } } } // ----------------------------------------------------------------------------------------------- // Old render methods. @Deprecated @Override public String render( SoyTemplateInfo templateInfo, @Nullable SoyRecord data, @Nullable SoyMsgBundle msgBundle) { return (new RendererImpl(this, templateInfo.getName())) .setData(data) .setMsgBundle(msgBundle) .render(); } @Deprecated @Override public String render( String templateName, @Nullable Map<String, ?> data, @Nullable SoyMsgBundle msgBundle) { return (new RendererImpl(this, templateName)).setData(data).setMsgBundle(msgBundle).render(); } @Deprecated @Override public String render( String templateName, @Nullable SoyRecord data, @Nullable SoyMsgBundle msgBundle) { return (new RendererImpl(this, templateName)).setData(data).setMsgBundle(msgBundle).render(); } }