/* * 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.shared; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.Message; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.SoyProtoValue; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.SoyValueConverter; import com.google.template.soy.jbcsrc.api.AdvisingAppendable; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.msgs.SoyMsgBundle; import com.google.template.soy.msgs.restricted.SoyMsg; import com.google.template.soy.msgs.restricted.SoyMsgPart; import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.shared.SoyIdRenamingMap; import com.google.template.soy.shared.restricted.SoyJavaFunction; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.ibm.icu.util.ULocale; import java.util.Map; import javax.annotation.Nullable; /** * A collection of contextual rendering data. Each top level rendering operation will obtain a * single instance of this object and it will be propagated throughout the render tree. */ public final class RenderContext { private static final CompiledTemplate EMPTY_TEMPLATE = new CompiledTemplate() { @Override public RenderResult render(AdvisingAppendable appendable, RenderContext context) { return RenderResult.done(); } @Override @Nullable public ContentKind kind() { // The kind doesn't really matter, since the empty string can always be safely escaped return null; } }; // TODO(lukes): within this object most of these fields are constant across all renders while // some are expected to change frequently (the renaming maps, msgBundle and activeDelPackages). // Consider splitting this into two objects to represent the changing lifetimes. We are kind of // doing this now by having SoySauceImpl reuse the Builder, but this is a little strange and could // be theoretically made more efficient to construct. private final Predicate<String> activeDelPackageSelector; private final CompiledTemplates templates; private final SoyCssRenamingMap cssRenamingMap; private final SoyIdRenamingMap xidRenamingMap; private final ImmutableMap<String, SoyJavaFunction> soyJavaFunctionsMap; private final ImmutableMap<String, SoyJavaPrintDirective> soyJavaDirectivesMap; private final SoyValueConverter converter; /** The bundle of translated messages */ private final SoyMsgBundle msgBundle; private RenderContext(Builder builder) { this.activeDelPackageSelector = checkNotNull(builder.activeDelPackageSelector); this.templates = checkNotNull(builder.templates); this.cssRenamingMap = builder.cssRenamingMap; this.xidRenamingMap = builder.xidRenamingMap; this.soyJavaFunctionsMap = builder.soyJavaFunctionsMap; this.soyJavaDirectivesMap = builder.soyJavaDirectivesMap; this.converter = builder.converter; this.msgBundle = builder.msgBundle; } @Nullable public ULocale getLocale() { return msgBundle.getLocale(); } public String renameCssSelector(String selector) { String string = cssRenamingMap.get(selector); return string == null ? selector : string; } public String renameXid(String id) { String string = xidRenamingMap.get(id); return string == null ? id + "_" : string; } public SoyJavaFunction getFunction(String name) { SoyJavaFunction fn = soyJavaFunctionsMap.get(name); if (fn == null) { throw new IllegalStateException("Failed to find Soy function with name '" + name + "'"); } return fn; } public SoyJavaPrintDirective getPrintDirective(String name) { SoyJavaPrintDirective printDirective = soyJavaDirectivesMap.get(name); if (printDirective == null) { throw new IllegalStateException( "Failed to find Soy print directive with name '" + name + "'"); } return printDirective; } /** * Helper for boxing protos. We cannot currently box protos without calling out to the value * converter because the SoyProtoValue has a package private constructor and even if it was public * it would be hard/impossible to call it. * * <p>The difficulty is because SoyProtoValue currently depends on its SoyType for field * interpretation. In theory we could drop this and have it just use the descriptor directly * (since it has a Message instance it could just call message.getDescriptor()), but this may add * some overhead. This could all be made much easier if we had perfect type information (then we * would ~never need to box or rely on the SoyValue implementation). */ public SoyProtoValue box(Message proto) { if (proto == null) { return null; } return (SoyProtoValue) converter.convert(proto); } public CompiledTemplate getDelTemplate( String calleeName, String variant, boolean allowEmpty, SoyRecord params, SoyRecord ij) { CompiledTemplate.Factory callee = templates.selectDelTemplate(calleeName, variant, activeDelPackageSelector); if (callee == null) { if (allowEmpty) { return EMPTY_TEMPLATE; } throw new IllegalArgumentException( "Found no active impl for delegate call to '" + calleeName + "' (and no attribute allowemptydefault=\"true\")."); } return callee.create(params, ij); } /** Returns {@code true} if the primary msg should be used instead of the fallback. */ public boolean usePrimaryMsg(long msgId, long fallbackId) { // Note: we need to make sure the fallback msg is actually present if we are going to fallback. // use getMsgParts() since if the bundle is a RenderOnlySoyMsgBundleImpl then this will be // allocation free. return !msgBundle.getMsgParts(msgId).isEmpty() || msgBundle.getMsgParts(fallbackId).isEmpty(); } /** * Returns the {@link SoyMsg} associated with the {@code msgId} or the fallback (aka english) * translation if there is no such message. */ public ImmutableList<SoyMsgPart> getSoyMsgParts( long msgId, ImmutableList<SoyMsgPart> defaultMsgParts) { ImmutableList<SoyMsgPart> msgParts = msgBundle.getMsgParts(msgId); if (msgParts.isEmpty()) { return defaultMsgParts; } return msgParts; } @VisibleForTesting public Builder toBuilder() { return new Builder() .withActiveDelPackageSelector(this.activeDelPackageSelector) .withSoyFunctions(soyJavaFunctionsMap) .withSoyPrintDirectives(soyJavaDirectivesMap) .withCssRenamingMap(cssRenamingMap) .withXidRenamingMap(xidRenamingMap) .withConverter(converter) .withMessageBundle(msgBundle); } /** A builder for configuring the context. */ public static final class Builder { private CompiledTemplates templates; private Predicate<String> activeDelPackageSelector = Predicates.alwaysFalse(); private SoyCssRenamingMap cssRenamingMap = SoyCssRenamingMap.EMPTY; private SoyIdRenamingMap xidRenamingMap = SoyCssRenamingMap.EMPTY; private ImmutableMap<String, SoyJavaFunction> soyJavaFunctionsMap = ImmutableMap.of(); private ImmutableMap<String, SoyJavaPrintDirective> soyJavaDirectivesMap = ImmutableMap.of(); private SoyValueConverter converter = SoyValueConverter.UNCUSTOMIZED_INSTANCE; private SoyMsgBundle msgBundle = SoyMsgBundle.EMPTY; public Builder withCompiledTemplates(CompiledTemplates templates) { this.templates = checkNotNull(templates); return this; } public Builder withActiveDelPackageSelector(Predicate<String> activeDelPackageSelector) { this.activeDelPackageSelector = checkNotNull(activeDelPackageSelector); return this; } public Builder withCssRenamingMap(SoyCssRenamingMap cssRenamingMap) { this.cssRenamingMap = checkNotNull(cssRenamingMap); return this; } public Builder withXidRenamingMap(SoyIdRenamingMap xidRenamingMap) { this.xidRenamingMap = checkNotNull(xidRenamingMap); return this; } public Builder withSoyFunctions(ImmutableMap<String, SoyJavaFunction> functions) { this.soyJavaFunctionsMap = functions; return this; } public Builder withSoyPrintDirectives(Map<String, ? extends SoyJavaPrintDirective> directives) { this.soyJavaDirectivesMap = ImmutableMap.copyOf(directives); return this; } public Builder withConverter(SoyValueConverter converter) { this.converter = checkNotNull(converter); return this; } public Builder withMessageBundle(SoyMsgBundle msgBundle) { this.msgBundle = checkNotNull(msgBundle); return this; } public RenderContext build() { return new RenderContext(this); } } }