/* * Copyright 2013 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.data; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import com.google.inject.Inject; import com.google.protobuf.ByteString; import com.google.protobuf.Descriptors.EnumValueDescriptor; import com.google.protobuf.ProtocolMessageEnum; import com.google.template.soy.data.internal.DictImpl; import com.google.template.soy.data.internal.EasyListImpl; import com.google.template.soy.data.internal.ListImpl; import com.google.template.soy.data.restricted.BooleanData; import com.google.template.soy.data.restricted.FloatData; 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.RenderResult; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Future; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * A converter that knows how to convert all expected Java objects into SoyValues or * SoyValueProviders. * * <p>IMPORTANT: This class is partially open for public use. Specifically, you may use the method * {@link #convert} and the static fields. But do not use the {@code new*} methods. Consider the * {@code new*} methods internal to Soy, since we haven't yet decided whether or not to make them * directly available. * */ // TODO(user): Make final after SoyValueHelper is removed public class SoyValueConverter { /** Static instance of this class that does not include any custom value converters. */ public static final SoyValueConverter UNCUSTOMIZED_INSTANCE = new SoyValueConverter(); /** An immutable empty dict. */ public static final SoyDict EMPTY_DICT = UNCUSTOMIZED_INSTANCE.newDict(); /** An immutable empty list. */ public static final SoyList EMPTY_LIST = UNCUSTOMIZED_INSTANCE.newList(); /** List of user-provided custom value converters. */ // Note: Using field injection instead of constructor injection because we want optional = true. @Inject(optional = true) private List<SoyCustomValueConverter> customValueConverters; @Inject SoyValueConverter() {} // ----------------------------------------------------------------------------------------------- // Creating. /** * Creates a new SoyDict initialized from the given keys and values. Values are converted eagerly. * Recognizes dotted-name syntax: adding {@code ("foo.goo", value)} will automatically create * {@code ['foo': ['goo': value]]}. * * @param alternatingKeysAndValues An alternating list of keys and values. * @return A new SoyDict initialized from the given keys and values. */ @VisibleForTesting public SoyDict newDict(Object... alternatingKeysAndValues) { Preconditions.checkArgument(alternatingKeysAndValues.length % 2 == 0); Map<String, Object> map = new HashMap<>(); for (int i = 0, n = alternatingKeysAndValues.length / 2; i < n; i++) { String key = (String) alternatingKeysAndValues[2 * i]; SoyValueProvider value = convert(alternatingKeysAndValues[2 * i + 1]); // convert eagerly insertIntoNestedMap(map, key, value); } return newDictFromMap(map); } /** * Creates a Soy dictionary from a Java string map. While this is O(N) with the map's shallow * size, the values are converted into Soy values lazily and only once. * * @param javaStringMap The map backing the dict. * @return A new SoyDict initialized from the given Java string-keyed map. */ public SoyDict newDictFromMap(Map<String, ?> javaStringMap) { // Create a dictionary backed by a map which has eagerly converted each value into a lazy // value provider. Specifically, the map iteration is done eagerly so that the lazy value // provider can cache its value. ImmutableMap.Builder<String, SoyValueProvider> builder = ImmutableMap.builder(); for (Map.Entry<String, ?> entry : javaStringMap.entrySet()) { builder.put(entry.getKey(), convertLazy(entry.getValue())); } return DictImpl.forProviderMap(builder.build()); } /** * Private helper to create nested maps based off of a given string of dot-separated field names, * then insert the value into the innermost map. * * <p>For example, {@code insertIntoNestedMap(new HashMap<>(), "foo.bar.baz", val)} will return a * map of {@code {"foo": {"bar": {"baz": val}}}}. * * @param map Top-level map to insert into * @param dottedName One or more field names, dot-separated. * @param value Value to insert */ private static void insertIntoNestedMap( Map<String, Object> map, String dottedName, SoyValueProvider value) { String[] names = dottedName.split("[.]"); int n = names.length; String lastName = names[n - 1]; Map<String, Object> lastMap; if (n == 1) { lastMap = map; } else { lastMap = map; for (int i = 0; i <= n - 2; i++) { Object o = lastMap.get(names[i]); if (o instanceof Map) { @SuppressWarnings("unchecked") Map<String, Object> m = (Map<String, Object>) o; lastMap = m; } else if (o == null) { Map<String, Object> newMap = new HashMap<>(); lastMap.put(names[i], newMap); lastMap = newMap; } else { throw new AssertionError("should not happen"); } } } lastMap.put(lastName, value); } /** * IMPORTANT: Do not use this method. Consider it internal to Soy. * * <p>Creates a new SoyEasyList initialized from a SoyList. * * @param list The list of initial values. * @return A new SoyEasyList initialized from the given SoyList. */ @Deprecated public SoyEasyList newEasyListFromList(SoyList list) { EasyListImpl result = new EasyListImpl(); for (SoyValueProvider provider : list.asJavaList()) { result.add(provider); } return result; } /** * IMPORTANT: Do not use this method. Consider it internal to Soy. * * <p>Creates a new SoyList initialized from the given values. Values are converted eagerly. * * @param items A list of values. * @return A new SoyEasyList initialized from the given values. */ @VisibleForTesting public SoyList newList(Object... items) { ImmutableList.Builder<SoyValueProvider> builder = ImmutableList.builder(); for (Object o : items) { builder.add(convert(o)); } return ListImpl.forProviderList(builder.build()); } /** * Creates a SoyList from a Java Iterable. * * <p>Values are converted into Soy types lazily and only once. * * @param items The collection of Java values * @return A new SoyList initialized from the given Java Collection. */ private SoyList newListFromIterable(Iterable<?> items) { // Create a list backed by a Java list which has eagerly converted each value into a lazy // value provider. Specifically, the list iteration is done eagerly so that the lazy value // provider can cache its value. ImmutableList.Builder<SoyValueProvider> builder = ImmutableList.builder(); for (Object item : items) { builder.add(convertLazy(item)); } return ListImpl.forProviderList(builder.build()); } // ----------------------------------------------------------------------------------------------- // Converting from existing data. /** * Converts a Java object into an equivalent SoyValueProvider. * * @param obj The object to convert. * @return An equivalent SoyValueProvider. * @throws SoyDataException If the given object cannot be converted. */ @Nonnull public SoyValueProvider convert(@Nullable Object obj) { SoyValueProvider convertedPrimitive = convertPrimitive(obj); if (convertedPrimitive != null) { return convertedPrimitive; } else if (obj instanceof Map<?, ?>) { // TODO: Instead of hoping that the map is string-keyed, we should only enter this case if we // know the map is string-keyed. Otherwise, we should fall through and let the user's custom // converters have a chance at converting the map. @SuppressWarnings("unchecked") Map<String, ?> objCast = (Map<String, ?>) obj; return newDictFromMap(objCast); } else if (obj instanceof Collection<?> || obj instanceof FluentIterable<?>) { // NOTE: We don't trap Iterable itself, because many types extend from Iterable but are not // meant to be enumerated. (e.g. ByteString implements Iterable<Byte>) return newListFromIterable((Iterable<?>) obj); } else if (obj instanceof SoyGlobalsValue) { return convert(((SoyGlobalsValue) obj).getSoyGlobalValue()); } else if (obj instanceof ByteString) { // Encode ByteStrings as base 64, as a safe and consistent way to send them to JS return StringData.forValue(BaseEncoding.base64().encode(((ByteString) obj).toByteArray())); } else if (obj instanceof EnumValueDescriptor) { // Proto enum that was obtained via reflection (e.g. from SoyProtoValue) return IntegerData.forValue(((EnumValueDescriptor) obj).getNumber()); } else if (obj instanceof ProtocolMessageEnum) { // Proto enum that was directly passed into the template return IntegerData.forValue(((ProtocolMessageEnum) obj).getNumber()); } else { if (customValueConverters != null) { for (SoyCustomValueConverter customConverter : customValueConverters) { SoyValueProvider result = customConverter.convert(this, obj); if (result != null) { return result; } } } throw new SoyDataException( "Attempting to convert unrecognized object to Soy value (object type " + obj.getClass().getName() + ")."); } } /** * Returns a SoyValueProvider corresponding to a Java object, but doesn't perform any work until * resolve() is called. */ private SoyValueProvider convertLazy(@Nullable final Object obj) { SoyValueProvider convertedPrimitive = convertPrimitive(obj); if (convertedPrimitive != null) { return convertedPrimitive; } else { return new SoyAbstractCachingValueProvider() { @Override protected SoyValue compute() { return convert(obj).resolve(); } @Override public RenderResult status() { return RenderResult.done(); } }; } } /** * Attempts to convert fast-converting primitive types. Returns null if obj is not a recognized * primitive. */ @Nullable private SoyValueProvider convertPrimitive(@Nullable Object obj) { if (obj == null) { return NullData.INSTANCE; } else if (obj instanceof SoyValueProvider) { return (SoyValueProvider) obj; } else if (obj instanceof String) { return StringData.forValue((String) obj); } else if (obj instanceof Boolean) { return BooleanData.forValue((Boolean) obj); } else if (obj instanceof Number) { if (obj instanceof Integer) { return IntegerData.forValue((Integer) obj); } else if (obj instanceof Long) { return IntegerData.forValue((Long) obj); } else if (obj instanceof Double) { return FloatData.forValue((Double) obj); } else if (obj instanceof Float) { // Automatically convert float to double. return FloatData.forValue((Float) obj); } } else if (obj instanceof Future<?>) { return new SoyFutureValueProvider(this, (Future<?>) obj); } return null; } }