|  | // Copyright 2014 The Bazel Authors. All rights reserved. | 
|  | // | 
|  | // 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.devtools.build.lib.packages; | 
|  |  | 
|  | import static com.google.devtools.build.lib.syntax.SkylarkType.castMap; | 
|  | import static java.util.Collections.singleton; | 
|  | import static java.util.stream.Collectors.toCollection; | 
|  |  | 
|  | import com.google.auto.value.AutoValue; | 
|  | import com.google.common.collect.ImmutableList; | 
|  | import com.google.common.collect.ImmutableMap; | 
|  | import com.google.common.collect.ImmutableSet; | 
|  | import com.google.common.collect.Iterables; | 
|  | import com.google.common.collect.Lists; | 
|  | import com.google.common.collect.Sets; | 
|  | import com.google.common.escape.Escaper; | 
|  | import com.google.common.escape.Escapers; | 
|  | import com.google.devtools.build.lib.cmdline.Label; | 
|  | import com.google.devtools.build.lib.events.EventHandler; | 
|  | import com.google.devtools.build.lib.events.Location; | 
|  | import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec; | 
|  | import com.google.devtools.build.lib.skyframe.serialization.autocodec.AutoCodec.VisibleForSerialization; | 
|  | import com.google.devtools.build.lib.syntax.ClassObject; | 
|  | import com.google.devtools.build.lib.syntax.EvalException; | 
|  | import com.google.devtools.build.lib.syntax.Starlark; | 
|  | import com.google.devtools.build.lib.util.StringUtil; | 
|  | import com.google.devtools.build.lib.vfs.FileSystemUtils; | 
|  | import com.google.devtools.build.lib.vfs.PathFragment; | 
|  | import java.util.Arrays; | 
|  | import java.util.Collection; | 
|  | import java.util.Collections; | 
|  | import java.util.HashMap; | 
|  | import java.util.LinkedHashSet; | 
|  | import java.util.List; | 
|  | import java.util.Map; | 
|  | import java.util.Set; | 
|  |  | 
|  | /** | 
|  | * A function interface allowing rules to specify their set of implicit outputs in a more dynamic | 
|  | * way than just simple template-substitution. For example, the set of implicit outputs may be a | 
|  | * function of rule attributes. | 
|  | * | 
|  | * <p>In the case that attribute placeholders are configurable attributes, errors will be thrown as | 
|  | * output templates are expanded before configurable attributes are resolved. | 
|  | * | 
|  | * <p>In the case that attribute placeholders are invalid, the template string will be left | 
|  | * unexpanded. | 
|  | */ | 
|  | // TODO(http://b/69387932): refactor this entire class and all callers. | 
|  | public abstract class ImplicitOutputsFunction { | 
|  |  | 
|  | /** | 
|  | * Implicit output functions for Skylark supporting key value access of expanded implicit outputs. | 
|  | */ | 
|  | public abstract static class SkylarkImplicitOutputsFunction extends ImplicitOutputsFunction { | 
|  |  | 
|  | public abstract ImmutableMap<String, String> calculateOutputs( | 
|  | EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException; | 
|  |  | 
|  | @Override | 
|  | public Iterable<String> getImplicitOutputs(EventHandler eventHandler, AttributeMap map) | 
|  | throws EvalException, InterruptedException { | 
|  | return calculateOutputs(eventHandler, map).values(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** Implicit output functions executing Skylark code. */ | 
|  | @AutoCodec | 
|  | public static final class SkylarkImplicitOutputsFunctionWithCallback | 
|  | extends SkylarkImplicitOutputsFunction { | 
|  |  | 
|  | private final StarlarkCallbackHelper callback; | 
|  | private final Location loc; | 
|  |  | 
|  | public SkylarkImplicitOutputsFunctionWithCallback( | 
|  | StarlarkCallbackHelper callback, Location loc) { | 
|  | this.callback = callback; | 
|  | this.loc = loc; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ImmutableMap<String, String> calculateOutputs( | 
|  | EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException { | 
|  | Map<String, Object> attrValues = new HashMap<>(); | 
|  | for (String attrName : map.getAttributeNames()) { | 
|  | Type<?> attrType = map.getAttributeType(attrName); | 
|  | // Don't include configurable attributes: we don't know which value they might take | 
|  | // since we don't yet have a build configuration. | 
|  | if (!map.isConfigurable(attrName)) { | 
|  | Object value = map.get(attrName, attrType); | 
|  | attrValues.put( | 
|  | Attribute.getSkylarkName(attrName), Starlark.fromJava(value, /*mutability=*/ null)); | 
|  | } | 
|  | } | 
|  | ClassObject attrs = | 
|  | StructProvider.STRUCT.create( | 
|  | attrValues, | 
|  | "Attribute '%s' either doesn't exist " | 
|  | + "or uses a select() (i.e. could have multiple values)"); | 
|  | try { | 
|  | ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); | 
|  | for (Map.Entry<String, String> entry : | 
|  | castMap( | 
|  | callback.call(eventHandler, attrs), | 
|  | String.class, | 
|  | String.class, | 
|  | "implicit outputs function return value") | 
|  | .entrySet()) { | 
|  |  | 
|  | // Returns empty string only in case of invalid templates | 
|  | Iterable<String> substitutions = | 
|  | fromTemplates(entry.getValue()).getImplicitOutputs(eventHandler, map); | 
|  | if (Iterables.isEmpty(substitutions)) { | 
|  | throw new EvalException( | 
|  | loc, | 
|  | String.format( | 
|  | "For attribute '%s' in outputs: %s", | 
|  | entry.getKey(), "Invalid placeholder(s) in template")); | 
|  | } | 
|  |  | 
|  | builder.put(entry.getKey(), Iterables.getOnlyElement(substitutions)); | 
|  | } | 
|  | return builder.build(); | 
|  | } catch (IllegalArgumentException e) { | 
|  | throw new EvalException(loc, e.getMessage()); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /** Implicit output functions using a simple an output map. */ | 
|  | @AutoCodec | 
|  | public static final class SkylarkImplicitOutputsFunctionWithMap | 
|  | extends SkylarkImplicitOutputsFunction { | 
|  |  | 
|  | private final ImmutableMap<String, String> outputMap; | 
|  |  | 
|  | public SkylarkImplicitOutputsFunctionWithMap(ImmutableMap<String, String> outputMap) { | 
|  | this.outputMap = outputMap; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public ImmutableMap<String, String> calculateOutputs( | 
|  | EventHandler eventHandler, AttributeMap map) throws EvalException, InterruptedException { | 
|  |  | 
|  | ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); | 
|  | for (Map.Entry<String, String> entry : outputMap.entrySet()) { | 
|  | // Empty iff invalid placeholders present. | 
|  | ImplicitOutputsFunction outputsFunction = | 
|  | fromUnsafeTemplates(ImmutableList.of(entry.getValue())); | 
|  | Iterable<String> substitutions = outputsFunction.getImplicitOutputs(eventHandler, map); | 
|  | if (Iterables.isEmpty(substitutions)) { | 
|  | throw new EvalException( | 
|  | null, | 
|  | String.format( | 
|  | "For attribute '%s' in outputs: %s", | 
|  | entry.getKey(), "Invalid placeholder(s) in template")); | 
|  |  | 
|  | } | 
|  |  | 
|  | builder.put(entry.getKey(), Iterables.getOnlyElement(substitutions)); | 
|  | } | 
|  | return builder.build(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Implicit output functions which can not throw an EvalException. | 
|  | */ | 
|  | public abstract static class SafeImplicitOutputsFunction extends ImplicitOutputsFunction { | 
|  | @Override | 
|  | public abstract Iterable<String> getImplicitOutputs( | 
|  | EventHandler eventHandler, AttributeMap map); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * An interface to objects that can retrieve rule attributes. | 
|  | */ | 
|  | public interface AttributeValueGetter { | 
|  | /** | 
|  | * Returns the value(s) of attribute "attr" in "rule", or empty set if attribute unknown. | 
|  | */ | 
|  | Set<String> get(AttributeMap rule, String attr); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The default rule attribute retriever. | 
|  | * | 
|  | * <p>Custom {@link AttributeValueGetter} implementations may delegate to this object as a | 
|  | * fallback mechanism. | 
|  | */ | 
|  | public static final AttributeValueGetter DEFAULT_RULE_ATTRIBUTE_GETTER = | 
|  | new AttributeValueGetter() { | 
|  | @Override | 
|  | public Set<String> get(AttributeMap rule, String attr) { | 
|  | return attributeValues(rule, attr); | 
|  | } | 
|  | }; | 
|  |  | 
|  | private static final Escaper PERCENT_ESCAPER = Escapers.builder().addEscape('%', "%%").build(); | 
|  |  | 
|  | /** | 
|  | * Given a newly-constructed Rule instance (with attributes populated), returns the list of output | 
|  | * files that this rule produces implicitly. | 
|  | */ | 
|  | public abstract Iterable<String> getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) | 
|  | throws EvalException, InterruptedException; | 
|  |  | 
|  | /** The implicit output function that returns no files. */ | 
|  | @AutoCodec | 
|  | public static final SafeImplicitOutputsFunction NONE = | 
|  | new SafeImplicitOutputsFunction() { | 
|  | @Override | 
|  | public Iterable<String> getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) { | 
|  | return Collections.emptyList(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | /** | 
|  | * A convenience wrapper for {@link #fromTemplates(Iterable)}. | 
|  | */ | 
|  | public static SafeImplicitOutputsFunction fromTemplates(String... templates) { | 
|  | return fromTemplates(Arrays.asList(templates)); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The implicit output function that generates files based on a set of template substitutions | 
|  | * using rule attribute values. | 
|  | * | 
|  | * <p>This is not, actually, safe, and any use of configurable attributes will cause a hard | 
|  | * failure. | 
|  | * | 
|  | * @param templates The templates used to construct the name of the implicit output file target. | 
|  | *     The substring "%{foo}" will be replaced by the value of the attribute "foo". If multiple | 
|  | *     %{} substrings exist, the cross-product of them is generated. | 
|  | */ | 
|  | public static SafeImplicitOutputsFunction fromTemplates(final Iterable<String> templates) { | 
|  | return new TemplateImplicitOutputsFunction(templates); | 
|  | } | 
|  |  | 
|  | @VisibleForSerialization | 
|  | @AutoCodec | 
|  | static class TemplateImplicitOutputsFunction extends SafeImplicitOutputsFunction { | 
|  |  | 
|  | private final Iterable<String> templates; | 
|  |  | 
|  | @VisibleForSerialization | 
|  | TemplateImplicitOutputsFunction(Iterable<String> templates) { | 
|  | this.templates = templates; | 
|  | } | 
|  |  | 
|  | // TODO(bazel-team): parse the templates already here | 
|  | @Override | 
|  | public Iterable<String> getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) { | 
|  | ImmutableSet.Builder<String> result = new ImmutableSet.Builder<>(); | 
|  | for (String template : templates) { | 
|  | List<String> substitutions = substitutePlaceholderIntoTemplate(template, rule); | 
|  | if (substitutions.isEmpty()) { | 
|  | continue; | 
|  | } | 
|  | result.addAll(substitutions); | 
|  | } | 
|  |  | 
|  | return result.build(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return StringUtil.joinEnglishList(templates); | 
|  | } | 
|  | } | 
|  |  | 
|  | @AutoCodec | 
|  | @VisibleForSerialization | 
|  | static class UnsafeTemplatesImplicitOutputsFunction extends ImplicitOutputsFunction { | 
|  |  | 
|  | private final Iterable<String> templates; | 
|  |  | 
|  | @VisibleForSerialization | 
|  | UnsafeTemplatesImplicitOutputsFunction(Iterable<String> templates) { | 
|  | this.templates = templates; | 
|  | } | 
|  |  | 
|  | // TODO(bazel-team): parse the templates already here | 
|  | @Override | 
|  | public Iterable<String> getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) | 
|  | throws EvalException { | 
|  | ImmutableSet.Builder<String> result = new ImmutableSet.Builder<>(); | 
|  | for (String template : templates) { | 
|  | List<String> substitutions = | 
|  | substitutePlaceholderIntoUnsafeTemplate( | 
|  | template, rule, DEFAULT_RULE_ATTRIBUTE_GETTER); | 
|  | if (substitutions.isEmpty()) { | 
|  | continue; | 
|  | } | 
|  | result.addAll(substitutions); | 
|  | } | 
|  |  | 
|  | return result.build(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return StringUtil.joinEnglishList(templates); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The implicit output function that generates files based on a set of template substitutions | 
|  | * using rule attribute values. | 
|  | * | 
|  | * <p>This is not, actually, safe, and any use of configurable attributes will cause a hard | 
|  | * failure. | 
|  | * | 
|  | * @param templates The templates used to construct the name of the implicit output file target. | 
|  | *     The substring "%{foo}" will be replaced by the value of the attribute "foo". If multiple | 
|  | *     %{} substrings exist, the cross-product of them is generated. | 
|  | */ | 
|  | // It would be nice to unify this with fromTemplates above, but that's not possible because | 
|  | // substitutePlaceholderIntoUnsafeTemplate can throw an exception. | 
|  | public static ImplicitOutputsFunction fromUnsafeTemplates(Iterable<String> templates) { | 
|  | return new UnsafeTemplatesImplicitOutputsFunction(templates); | 
|  | } | 
|  |  | 
|  | /** A convenience wrapper for {@link #fromFunctions(Iterable)}. */ | 
|  | public static SafeImplicitOutputsFunction fromFunctions( | 
|  | SafeImplicitOutputsFunction... functions) { | 
|  | return fromFunctions(Arrays.asList(functions)); | 
|  | } | 
|  |  | 
|  | @AutoCodec | 
|  | @VisibleForSerialization | 
|  | static class FunctionCombinationImplicitOutputsFunction extends SafeImplicitOutputsFunction { | 
|  |  | 
|  | private final Iterable<SafeImplicitOutputsFunction> functions; | 
|  |  | 
|  | @VisibleForSerialization | 
|  | FunctionCombinationImplicitOutputsFunction(Iterable<SafeImplicitOutputsFunction> functions) { | 
|  | this.functions = functions; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public Iterable<String> getImplicitOutputs(EventHandler eventHandler, AttributeMap rule) { | 
|  | Collection<String> result = new LinkedHashSet<>(); | 
|  | for (SafeImplicitOutputsFunction function : functions) { | 
|  | Iterables.addAll(result, function.getImplicitOutputs(eventHandler, rule)); | 
|  | } | 
|  | return result; | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public String toString() { | 
|  | return StringUtil.joinEnglishList(functions); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * The implicit output function that generates files based on a set of | 
|  | * template substitutions using rule attribute values. | 
|  | * | 
|  | * @param functions The functions used to construct the name of the implicit | 
|  | *   output file target.  The substring "%{name}" will be replaced by the | 
|  | *   actual name of the rule, the substring "%{srcs}" will be replaced by the | 
|  | *   name of each source file without its extension.  If multiple %{} | 
|  | *   substrings exist, the cross-product of them is generated. | 
|  | */ | 
|  | public static SafeImplicitOutputsFunction fromFunctions( | 
|  | final Iterable<SafeImplicitOutputsFunction> functions) { | 
|  | return new FunctionCombinationImplicitOutputsFunction(functions); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Coerces attribute "attrName" of the specified rule into a sequence of | 
|  | * strings.  Helper function for {@link #fromTemplates(Iterable)}. | 
|  | */ | 
|  | private static Set<String> attributeValues(AttributeMap rule, String attrName) { | 
|  | if (attrName.equals("dirname")) { | 
|  | PathFragment dir = PathFragment.create(rule.getName()).getParentDirectory(); | 
|  | return (dir.segmentCount() == 0) ? singleton("") : singleton(dir.getPathString() + "/"); | 
|  | } else if (attrName.equals("basename")) { | 
|  | return singleton(PathFragment.create(rule.getName()).getBaseName()); | 
|  | } | 
|  |  | 
|  | Type<?> attrType = rule.getAttributeType(attrName); | 
|  | if (attrType == null) { | 
|  | return Collections.emptySet(); | 
|  | } | 
|  | // String attributes and lists are easy. | 
|  | if (Type.STRING == attrType) { | 
|  | return singleton(rule.get(attrName, Type.STRING)); | 
|  | } else if (Type.STRING_LIST == attrType) { | 
|  | return Sets.newLinkedHashSet(rule.get(attrName, Type.STRING_LIST)); | 
|  | } else if (BuildType.LABEL == attrType) { | 
|  | // Labels are most often used to change the extension, | 
|  | // e.g. %.foo -> %.java, so we return the basename w/o extension. | 
|  | Label label = rule.get(attrName, BuildType.LABEL); | 
|  | return singleton(FileSystemUtils.removeExtension(label.getName())); | 
|  | } else if (BuildType.LABEL_LIST == attrType) { | 
|  | // Labels are most often used to change the extension, | 
|  | // e.g. %.foo -> %.java, so we return the basename w/o extension. | 
|  | return rule.get(attrName, BuildType.LABEL_LIST) | 
|  | .stream() | 
|  | .map(label -> FileSystemUtils.removeExtension(label.getName())) | 
|  | .collect(toCollection(LinkedHashSet::new)); | 
|  | } else if (BuildType.OUTPUT == attrType) { | 
|  | Label out = rule.get(attrName, BuildType.OUTPUT); | 
|  | return singleton(out.getName()); | 
|  | } else if (BuildType.OUTPUT_LIST == attrType) { | 
|  | return rule.get(attrName, BuildType.OUTPUT_LIST) | 
|  | .stream() | 
|  | .map(Label::getName) | 
|  | .collect(toCollection(LinkedHashSet::new)); | 
|  | } | 
|  | throw new IllegalArgumentException( | 
|  | "Don't know how to handle " + attrName + " : " + attrType); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Collects all named placeholders from the template while replacing them with %s. | 
|  | * | 
|  | * <p>Example: for {@code template} "%{name}_%{locales}.foo", it will return "%s_%s.foo" and | 
|  | * store "name" and "locales" in {@code placeholders}. | 
|  | * | 
|  | * <p>Incomplete placeholders are treated like text: for "a-%{x}-%{y" this method returns | 
|  | * "a-%s-%%{y" and stores "x" in {@code placeholders}. | 
|  | * | 
|  | * @param template a string with placeholders of the format %{...} | 
|  | * @param placeholders a collection to collect placeholders into; may contain duplicates if not a | 
|  | *     Set | 
|  | * @return a format string for {@link String#format}, created from the template string with every | 
|  | *     placeholder replaced by %s | 
|  | */ | 
|  | public static String createPlaceholderSubstitutionFormatString(String template, | 
|  | Collection<String> placeholders) { | 
|  | return createPlaceholderSubstitutionFormatStringRecursive(template, placeholders, | 
|  | new StringBuilder()); | 
|  | } | 
|  |  | 
|  | private static String createPlaceholderSubstitutionFormatStringRecursive(String template, | 
|  | Collection<String> placeholders, StringBuilder formatBuilder) { | 
|  | int start = template.indexOf("%{"); | 
|  | if (start < 0) { | 
|  | return formatBuilder.append(PERCENT_ESCAPER.escape(template)).toString(); | 
|  | } | 
|  |  | 
|  | int end = template.indexOf('}', start + 2); | 
|  | if (end < 0) { | 
|  | return formatBuilder.append(PERCENT_ESCAPER.escape(template)).toString(); | 
|  | } | 
|  |  | 
|  | formatBuilder.append(PERCENT_ESCAPER.escape(template.substring(0, start))).append("%s"); | 
|  | placeholders.add(template.substring(start + 2, end)); | 
|  | return createPlaceholderSubstitutionFormatStringRecursive(template.substring(end + 1), | 
|  | placeholders, formatBuilder); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Given a template string, replaces all placeholders of the form %{...} with | 
|  | * the values from attributeSource.  If there are multiple placeholders, then | 
|  | * the output is the cross product of substitutions. | 
|  | */ | 
|  | public static ImmutableList<String> substitutePlaceholderIntoTemplate(String template, | 
|  | AttributeMap rule) { | 
|  | return substitutePlaceholderIntoTemplate(template, rule, DEFAULT_RULE_ATTRIBUTE_GETTER); | 
|  | } | 
|  |  | 
|  | @AutoValue | 
|  | abstract static class ParsedTemplate { | 
|  | abstract String template(); | 
|  |  | 
|  | abstract String formatStr(); | 
|  |  | 
|  | abstract List<String> attributeNames(); | 
|  |  | 
|  | static ParsedTemplate parse(String rawTemplate) { | 
|  | List<String> placeholders = Lists.<String>newArrayList(); | 
|  | String formatStr = createPlaceholderSubstitutionFormatString(rawTemplate, placeholders); | 
|  | if (placeholders.isEmpty()) { | 
|  | placeholders = ImmutableList.of(); | 
|  | } | 
|  | return new AutoValue_ImplicitOutputsFunction_ParsedTemplate( | 
|  | rawTemplate, formatStr, placeholders); | 
|  | } | 
|  |  | 
|  | ImmutableList<String> substituteAttributes( | 
|  | AttributeMap attributeMap, AttributeValueGetter attributeGetter) { | 
|  | if (attributeNames().isEmpty()) { | 
|  | return ImmutableList.of(template()); | 
|  | } | 
|  |  | 
|  | List<Set<String>> values = Lists.newArrayListWithCapacity(attributeNames().size()); | 
|  | for (String placeholder : attributeNames()) { | 
|  | Set<String> attrValues = attributeGetter.get(attributeMap, placeholder); | 
|  | if (attrValues.isEmpty()) { | 
|  | return ImmutableList.<String>of(); | 
|  | } | 
|  | values.add(attrValues); | 
|  | } | 
|  | ImmutableList.Builder<String> out = new ImmutableList.Builder<>(); | 
|  | for (List<String> combination : Sets.cartesianProduct(values)) { | 
|  | out.add(String.format(formatStr(), combination.toArray())); | 
|  | } | 
|  | return out.build(); | 
|  | } | 
|  | } | 
|  |  | 
|  | /** | 
|  | * Substitutes attribute-placeholders in a template string, producing all possible combinations. | 
|  | * | 
|  | * @param template the template string, may contain named placeholders for rule attributes, like | 
|  | *     <code>%{name}</code> or <code>%{deps}</code> | 
|  | * @param rule the rule whose attributes the placeholders correspond to | 
|  | * @param attributeGetter a helper for fetching attribute values | 
|  | * @return all possible combinations of the attributes referenced by the placeholders, substituted | 
|  | *     into the template; empty if any of the placeholders expands to no values | 
|  | */ | 
|  | public static ImmutableList<String> substitutePlaceholderIntoTemplate( | 
|  | String template, AttributeMap rule, AttributeValueGetter attributeGetter) { | 
|  | // Parse the template to get the attribute names and format string. | 
|  | ParsedTemplate parsedTemplate = ParsedTemplate.parse(template); | 
|  |  | 
|  | // Return the substituted strings. | 
|  | return parsedTemplate.substituteAttributes(rule, attributeGetter); | 
|  | } | 
|  |  | 
|  | private static ImmutableList<String> substitutePlaceholderIntoUnsafeTemplate( | 
|  | String unsafeTemplate, AttributeMap rule, AttributeValueGetter attributeGetter) | 
|  | throws EvalException { | 
|  | // Parse the template to get the attribute names and format string. | 
|  | ParsedTemplate parsedTemplate = ParsedTemplate.parse(unsafeTemplate); | 
|  |  | 
|  | // Make sure all attributes are valid. | 
|  | for (String placeholder : parsedTemplate.attributeNames()) { | 
|  | if (rule.isConfigurable(placeholder)) { | 
|  | throw new EvalException( | 
|  | /*location=*/ null, | 
|  | String.format( | 
|  | "Attribute %s is configurable and cannot be used in outputs", placeholder)); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Return the substituted strings. | 
|  | return parsedTemplate.substituteAttributes(rule, attributeGetter); | 
|  | } | 
|  | } |