Java tutorial
/* * 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.shared.internal; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableListMultimap; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableTable; import com.google.common.collect.Table; import com.google.common.collect.Tables; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; /** * Utility for applying deltemplate selection logic to an arbitrary set of values. * * <p>This object allows selection logic to be shared between both tofu and jbcsrc which use * different runtime representations for templates, without needing to have hard dependencies on * those runtime representations. For example, tofu uses {@code TemplateDelegateNode} and jbcsrc * uses {@code CompiledTemplate}. * * <p>This logic should be kept in sync with the JS and Python runtime logic. See the JS * {@code soy.$$getDelegateFn} and {@code soy.$$registerDelegateFn} methods. * * @param <T> The type of the values in the selector */ public final class DelTemplateSelector<T> { private final ImmutableTable<String, String, Group<T>> nameAndVariantToGroup; private final ImmutableListMultimap<String, T> delTemplateNameToValues; private DelTemplateSelector(Builder<T> builder) { ImmutableTable.Builder<String, String, Group<T>> nameAndVariantBuilder = ImmutableTable.builder(); ImmutableListMultimap.Builder<String, T> delTemplateNameToValuesBuilder = ImmutableListMultimap.builder(); for (Table.Cell<String, String, Group.Builder<T>> entry : builder.nameAndVariantToGroup.cellSet()) { Group<T> group = entry.getValue().build(); nameAndVariantBuilder.put(entry.getRowKey(), entry.getColumnKey(), group); String delTemplateName = entry.getRowKey(); if (group.defaultValue != null) { delTemplateNameToValuesBuilder.put(delTemplateName, group.defaultValue); } delTemplateNameToValuesBuilder.putAll(delTemplateName, group.delpackageToValue.values()); } this.nameAndVariantToGroup = nameAndVariantBuilder.build(); this.delTemplateNameToValues = delTemplateNameToValuesBuilder.build(); } /** * Returns a multimap from deltemplate name to every member (disregarding variant). * * <p>This is useful for compiler passes that need to validate all members of deltemplate group. */ public ImmutableListMultimap<String, T> delTemplateNameToValues() { return delTemplateNameToValues; } public boolean hasDelTemplateNamed(String delTemplateName) { return nameAndVariantToGroup.containsRow(delTemplateName); } /** * Returns an active delegate for the given name, variant and active packages. If no active * delegate if found for the {@code variant} the we fallback to a non variant lookup. Finally, we * return {@code null} if no such template can be found. * * <p>See {@code soy.$$getDelegateFn} for the js version */ @Nullable public T selectTemplate(String delTemplateName, String variant, Set<String> activeDelPackages) { Group<T> group = nameAndVariantToGroup.get(delTemplateName, variant); if (group != null) { T selection = group.select(activeDelPackages); if (selection != null) { return selection; } } if (!variant.isEmpty()) { // Retry with an empty variant group = nameAndVariantToGroup.get(delTemplateName, ""); if (group != null) { return group.select(activeDelPackages); } } return null; } /** A Builder for DelTemplateSelector. */ public static final class Builder<T> { private final Table<String, String, Group.Builder<T>> nameAndVariantToGroup = Tables.newCustomTable( new LinkedHashMap<String, Map<String, Group.Builder<T>>>(), new Supplier<Map<String, Group.Builder<T>>>() { @Override public Map<String, Group.Builder<T>> get() { return new LinkedHashMap<>(); } }); /** Adds a template in the default delpackage. */ public T addDefault(String delTemplateName, String variant, T value) { return getBuilder(delTemplateName, variant).setDefault(value); } /** Adds a deltemplate. */ public T add(String delTemplateName, String delpackage, String variant, T value) { return getBuilder(delTemplateName, variant).add(delpackage, value); } private DelTemplateSelector.Group.Builder<T> getBuilder(String name, String variant) { checkArgument(!name.isEmpty()); Group.Builder<T> v = nameAndVariantToGroup.get(name, variant); if (v == null) { v = new Group.Builder<>(name + (variant.isEmpty() ? "" : ":" + variant)); nameAndVariantToGroup.put(name, variant, v); } return v; } public DelTemplateSelector<T> build() { return new DelTemplateSelector<T>(this); } } /** * Represents all the templates for a given deltemplate name and variant value. */ private static final class Group<T> { final String formattedName; @Nullable final T defaultValue; final ImmutableMap<String, T> delpackageToValue; private Group(Builder<T> builder) { this.formattedName = checkNotNull(builder.formattedName); this.defaultValue = builder.defaultValue; this.delpackageToValue = ImmutableMap.copyOf(builder.delpackageToValue); } /** * Returns the value from this group based on the current active packages, or the default if * one exists. */ T select(Set<String> activeDelPackages) { Map.Entry<String, T> selected = null; for (Map.Entry<String, T> entry : delpackageToValue.entrySet()) { if (activeDelPackages.contains(entry.getKey())) { if (selected != null) { // how important is this? maybe instead of checking at deltemplate selection time we // should validate active packages at the beginning of rendering (this is what the js // impl does, see soy.$$registerDelegateFn) throw new IllegalArgumentException(String.format( "For delegate template '%s', found two active implementations with equal" + " priority in delegate packages '%s' and '%s'.", formattedName, entry.getKey(), selected.getKey())); } selected = entry; } } if (selected != null) { return selected.getValue(); } return defaultValue; } static final class Builder<T> { final String formattedName; Map<String, T> delpackageToValue = new LinkedHashMap<>(); T defaultValue; Builder(String formattedName) { this.formattedName = checkNotNull(formattedName); } T setDefault(T defaultValue) { if (this.defaultValue != null) { return this.defaultValue; } checkState(this.defaultValue == null); this.defaultValue = checkNotNull(defaultValue); return null; } T add(String delpackage, T value) { checkArgument(!delpackage.isEmpty()); T prev = delpackageToValue.put(delpackage, checkNotNull(value)); return prev; } Group<T> build() { return new Group<>(this); } } } }