/* * 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.runtime; import com.google.common.collect.ImmutableList; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.SoyMap; 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.restricted.IntegerData; import com.google.template.soy.data.restricted.NullData; import com.google.template.soy.data.restricted.StringData; import com.google.template.soy.jbcsrc.api.AdvisingAppendable; import com.google.template.soy.jbcsrc.api.AdvisingStringBuilder; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.jbcsrc.shared.CompiledTemplate; import com.google.template.soy.jbcsrc.shared.RenderContext; import com.google.template.soy.msgs.restricted.SoyMsgPart; import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralRemainderPart; import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart; import com.google.template.soy.msgs.restricted.SoyMsgSelectPart; import com.google.template.soy.shared.internal.ShortCircuitable; import com.google.template.soy.shared.restricted.SoyJavaFunction; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.ibm.icu.util.ULocale; import java.io.IOException; import java.util.List; import java.util.Map; import javax.annotation.Nullable; /** * Runtime utilities uniquely for the {@code jbcsrc} backend. * * <p>This class is public so it can be be used by generated template code. Please do not use it * from client code. */ public final class Runtime { public static final SoyValueProvider NULL_PROVIDER = new SoyValueProvider() { @Override public RenderResult status() { return RenderResult.done(); } @Override public SoyValue resolve() { return null; } @Override public RenderResult renderAndResolve(AdvisingAppendable appendable, boolean isLast) throws IOException { appendable.append("null"); return RenderResult.done(); } @Override public String toString() { return "NULL_PROVIDER"; } }; public static AssertionError unexpectedStateError(int state) { return new AssertionError("Unexpected state requested: " + state); } public static boolean stringEqualsAsNumber(String expr, double number) { try { return Double.parseDouble(expr) == number; } catch (NumberFormatException nfe) { return false; } } /** Helper function to translate NullData -> null when resolving a SoyValueProvider. */ public static SoyValue resolveSoyValueProvider(SoyValueProvider provider) { SoyValue value = provider.resolve(); if (value instanceof NullData) { return null; } return value; } /** * Helper function to make SoyRecord.getFieldProvider a non-nullable function by returning {@link * #NULL_PROVIDER} for missing fields. */ public static SoyValueProvider getFieldProvider(SoyRecord record, String field) { if (record == null) { throw new NullPointerException("Attempted to access field '" + field + "' of null"); } // TODO(lukes): ideally this would be the behavior of getFieldProvider, but Tofu relies on it // returning null to interpret it as 'undefined'. http://b/20537225 describes the issues in Tofu SoyValueProvider provider = record.getFieldProvider(field); // | instead of || avoids a branch return (provider == null | provider instanceof NullData) ? NULL_PROVIDER : provider; } /** * Helper function to translate null -> NullData when calling SoyJavaFunctions that may expect it. * * <p>In the long run we should either fix ToFu (and all SoyJavaFunctions) to not use NullData or * we should introduce custom SoyFunction implementations for have come from SoyValueProvider. */ public static SoyValue callSoyFunction(SoyJavaFunction function, List<SoyValue> args) { for (int i = 0; i < args.size(); i++) { if (args.get(i) == null) { args.set(i, NullData.INSTANCE); } } return function.computeForJava(args); } /** * Helper function to translate null -> NullData when calling SoyJavaPrintDirectives that may * expect it. */ public static SoyValue applyPrintDirective( SoyJavaPrintDirective directive, SoyValue value, List<SoyValue> args) { value = value == null ? NullData.INSTANCE : value; for (int i = 0; i < args.size(); i++) { if (args.get(i) == null) { args.set(i, NullData.INSTANCE); } } return directive.applyForJava(value, args); } // TODO(msamuel): should access to these be restricted since it can be // used to mint typed strings. /** * Wraps a given template with a collection of escapers to apply. * * @param delegate The delegate template to render * @param directives The set of directives to apply */ public static CompiledTemplate applyEscapersDynamic( CompiledTemplate delegate, List<SoyJavaPrintDirective> directives) { ContentKind kind = delegate.kind(); if (canSkipEscaping(directives, kind)) { return delegate; } return new EscapedCompiledTemplate(delegate, directives, kind); } /** * Wraps a given template with a collection of escapers to apply. * * @param delegate The delegate template to render * @param directives The set of directives to apply */ public static CompiledTemplate applyEscapers( CompiledTemplate delegate, ContentKind kind, List<SoyJavaPrintDirective> directives) { return new EscapedCompiledTemplate(delegate, directives, kind); } /** * Identifies some cases where the combination of directives and content kind mean we can skip * applying the escapers. This is an opportunistic optimization, it is possible that we will fail * to skip escaping in some cases where we could and that is OK. However, there should never be a * case where we skip escaping and but the escapers would actually modify the input. */ private static boolean canSkipEscaping( List<SoyJavaPrintDirective> directives, @Nullable ContentKind kind) { if (kind == null) { return false; } for (SoyJavaPrintDirective directive : directives) { if (!(directive instanceof ShortCircuitable) || !((ShortCircuitable) directive).isNoopForKind(kind)) { return false; } } return true; } public static SoyValueProvider getSoyListItem(List<SoyValueProvider> list, long index) { if (list == null) { throw new NullPointerException("Attempted to access list item '" + index + "' of null"); } int size = list.size(); // use & instead of && to avoid a branch if (index < size & index >= 0) { SoyValueProvider soyValueProvider = list.get((int) index); return soyValueProvider == null ? NULL_PROVIDER : soyValueProvider; } return NULL_PROVIDER; } public static RenderResult getListStatus(List<? extends SoyValueProvider> soyValueProviders) { // avoid allocating an iterator int size = soyValueProviders.size(); for (int i = 0; i < size; i++) { RenderResult result = soyValueProviders.get(i).status(); if (!result.isDone()) { return result; } } return RenderResult.done(); } public static SoyValueProvider getSoyMapItem(SoyMap soyMap, SoyValue key) { if (soyMap == null) { throw new NullPointerException("Attempted to access map item '" + key + "' of null"); } SoyValueProvider soyValueProvider = soyMap.getItemProvider(key); return soyValueProvider == null ? NULL_PROVIDER : soyValueProvider; } /** Render a 'complex' message containing with placeholders. */ public static void renderSoyMsgPartsWithPlaceholders( ImmutableList<SoyMsgPart> msgParts, @Nullable ULocale locale, Map<String, Object> placeholders, Appendable out) throws IOException { if (msgParts.isEmpty()) { // TODO(lukes): RenderVisitorAssistantForMsgs does this... but this seems like a weird case // investigate eliminating it return; } // TODO(lukes): the initial plural/select nesting structure of the SoyMsg is determined at // compile time (though the number of cases varies per locale). // We could allow the generated code to call renderSelect/renderPlural directly as a // microoptimization. This could potentially allow us to eliminate the hashmap entry+boxing for // plural variables when rendering a direct plural tag. ditto for selects, though that is // more complicated due to the fact that selects can be nested. SoyMsgPart firstPart = msgParts.get(0); if (firstPart instanceof SoyMsgPluralPart) { renderPlural(locale, (SoyMsgPluralPart) firstPart, placeholders, out); } else if (firstPart instanceof SoyMsgSelectPart) { renderSelect(locale, (SoyMsgSelectPart) firstPart, placeholders, out); } else { // avoid allocating the iterator for (int i = 0; i < msgParts.size(); i++) { SoyMsgPart msgPart = msgParts.get(i); if (msgPart instanceof SoyMsgRawTextPart) { writeRawText((SoyMsgRawTextPart) msgPart, out); } else if (msgPart instanceof SoyMsgPlaceholderPart) { writePlaceholder((SoyMsgPlaceholderPart) msgPart, placeholders, out); } else { throw new AssertionError("unexpected part: " + msgPart); } } } } /** * Render a {@code {select}} part of a message. Most of the complexity is handled by {@link * SoyMsgSelectPart#lookupCase} all this needs to do is apply the placeholders to all the * children. */ private static void renderSelect( @Nullable ULocale locale, SoyMsgSelectPart firstPart, Map<String, Object> placeholders, Appendable out) throws IOException { String selectCase = getSelectCase(placeholders, firstPart.getSelectVarName()); for (SoyMsgPart casePart : firstPart.lookupCase(selectCase)) { if (casePart instanceof SoyMsgSelectPart) { renderSelect(locale, (SoyMsgSelectPart) casePart, placeholders, out); } else if (casePart instanceof SoyMsgPluralPart) { renderPlural(locale, (SoyMsgPluralPart) casePart, placeholders, out); } else if (casePart instanceof SoyMsgPlaceholderPart) { writePlaceholder((SoyMsgPlaceholderPart) casePart, placeholders, out); } else if (casePart instanceof SoyMsgRawTextPart) { writeRawText((SoyMsgRawTextPart) casePart, out); } else { // select cannot directly contain remainder nodes. throw new AssertionError("unexpected part: " + casePart); } } } /** * Render a {@code {plural}} part of a message. Most of the complexity is handled by {@link * SoyMsgPluralPart#lookupCase} all this needs to do is apply the placeholders to all the * children. */ private static void renderPlural( @Nullable ULocale locale, SoyMsgPluralPart plural, Map<String, Object> placeholders, Appendable out) throws IOException { int pluralValue = getPlural(placeholders, plural.getPluralVarName()); for (SoyMsgPart casePart : plural.lookupCase(pluralValue, locale)) { if (casePart instanceof SoyMsgPlaceholderPart) { writePlaceholder((SoyMsgPlaceholderPart) casePart, placeholders, out); } else if (casePart instanceof SoyMsgRawTextPart) { writeRawText((SoyMsgRawTextPart) casePart, out); } else if (casePart instanceof SoyMsgPluralRemainderPart) { out.append(String.valueOf(pluralValue - plural.getOffset())); } else { // Plural parts will not have nested plural/select parts. So, this is an error. throw new AssertionError("unexpected part: " + casePart); } } } /** Returns the select case variable value. */ private static String getSelectCase(Map<String, Object> placeholders, String selectVarName) { String selectCase = (String) placeholders.get(selectVarName); if (selectCase == null) { throw new IllegalArgumentException("No value provided for select: '" + selectVarName + "'"); } return selectCase; } /** Returns the plural case variable value. */ private static int getPlural(Map<String, Object> placeholders, String pluralVarName) { IntegerData pluralValue = (IntegerData) placeholders.get(pluralVarName); if (pluralValue == null) { throw new IllegalArgumentException("No value provided for plural: '" + pluralVarName + "'"); } return pluralValue.integerValue(); } /** Append the placeholder to the output stream. */ private static void writePlaceholder( SoyMsgPlaceholderPart placeholder, Map<String, Object> placeholders, Appendable out) throws IOException { String placeholderName = placeholder.getPlaceholderName(); String str = (String) placeholders.get(placeholderName); if (str == null) { throw new IllegalArgumentException( "No value provided for placeholder: '" + placeholderName + "'"); } out.append(str); } /** Append the raw text segment to the output stream. */ private static void writeRawText(SoyMsgRawTextPart msgPart, Appendable out) throws IOException { out.append(msgPart.getRawText()); } private static final AdvisingAppendable LOGGER = new AdvisingAppendable() { @Override public boolean softLimitReached() { return false; } @Override public AdvisingAppendable append(char c) throws IOException { System.out.append(c); return this; } @Override public AdvisingAppendable append(CharSequence csq, int start, int end) { System.out.append(csq, start, end); return this; } @Override public AdvisingAppendable append(CharSequence csq) { System.out.append(csq); return this; } }; public static AdvisingAppendable logger() { return LOGGER; } public static boolean coerceToBoolean(double v) { // NaN and 0 should both be falsy, all other numbers are truthy // use & instead of && to avoid a branch return v != 0.0 & !Double.isNaN(v); } public static String coerceToString(@Nullable SoyValue v) { return v == null ? "null" : v.coerceToString(); } /** Wraps a compiled template to apply escaping directives. */ private static final class EscapedCompiledTemplate implements CompiledTemplate { private final CompiledTemplate delegate; private final ImmutableList<SoyJavaPrintDirective> directives; @Nullable private final ContentKind kind; // TODO(user): tracks adding streaming print directives which would help with this, since // it would allow us to eliminate this buffer which fundamentally breaks incremental rendering // Note: render() may be called multiple times as part of a render operation that detaches // halfway through. So we need to store the buffer in a field, but we never need to reset it. private final AdvisingStringBuilder buffer = new AdvisingStringBuilder(); EscapedCompiledTemplate( CompiledTemplate delegate, List<SoyJavaPrintDirective> directives, @Nullable ContentKind kind) { this.delegate = delegate; this.directives = ImmutableList.copyOf(directives); this.kind = kind; } @Override public RenderResult render(AdvisingAppendable appendable, RenderContext context) throws IOException { RenderResult result = delegate.render(buffer, context); if (result.isDone()) { SoyValue resultData = kind == null ? StringData.forValue(buffer.toString()) : UnsafeSanitizedContentOrdainer.ordainAsSafe(buffer.toString(), kind); for (SoyJavaPrintDirective directive : directives) { resultData = directive.applyForJava(resultData, ImmutableList.<SoyValue>of()); } appendable.append(resultData.coerceToString()); } return result; } @Override @Nullable public ContentKind kind() { return kind; } } }