| /* |
| * Copyright 2008 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.gwt.dev.cfg; |
| |
| import com.google.gwt.core.ext.linker.PropertyProviderGenerator; |
| import com.google.gwt.dev.util.collect.IdentityHashSet; |
| import com.google.gwt.dev.util.collect.Lists; |
| import com.google.gwt.dev.util.collect.Sets; |
| import com.google.gwt.thirdparty.guava.common.base.Objects; |
| import com.google.gwt.thirdparty.guava.common.collect.ImmutableMap; |
| |
| import java.io.Serializable; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.LinkedHashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Represents a single named deferred binding or configuration property that can |
| * answer with its value. The BindingProperty maintains two sets of values, the |
| * "defined" set and the "allowed" set. The allowed set must always be a subset |
| * of the defined set. |
| */ |
| public class BindingProperty extends Property { |
| |
| public static final String GLOB_STAR = "*"; |
| private static final String EMPTY = ""; |
| |
| private List<SortedSet<String>> collapsedValues = Lists.create(); |
| private final SortedSet<String> definedValues = new TreeSet<String>(); |
| private String fallback; |
| private HashMap<String,LinkedList<LinkedHashSet<String>>> fallbackValueMap; |
| private HashMap<String,LinkedList<String>> fallbackValues = new HashMap<String,LinkedList<String>>(); |
| private PropertyProvider provider; |
| private Class<? extends PropertyProviderGenerator> providerGenerator; |
| private final ConditionAll rootCondition = new ConditionAll(); |
| |
| /** |
| * The binding values that are allowed for each condition. |
| * (Used to determine what the properties were set to in the module file.) |
| */ |
| private final ConditionalValues allowedValues = new ConditionalValues(rootCondition); |
| |
| /** |
| * The binding values that we need to generate code for. (Affects the number of permutations.) |
| * In a normal compile, this is the same as allowedValues, but in some compilation modes, |
| * it's changed to restrict permutations. (For example, in Super Dev Mode.) |
| */ |
| private final ConditionalValues generatedValues = new ConditionalValues(rootCondition); |
| |
| public BindingProperty(String name) { |
| super(name); |
| fallback = EMPTY; |
| } |
| |
| /** |
| * Add an equivalence set of property values. |
| */ |
| public void addCollapsedValues(String... values) { |
| |
| // Sanity check caller |
| for (String value : values) { |
| if (value.contains(GLOB_STAR)) { |
| // Expanded in normalizeCollapsedValues() |
| continue; |
| } else if (!definedValues.contains(value)) { |
| throw new IllegalArgumentException( |
| "Attempting to collapse unknown value " + value); |
| } |
| } |
| |
| // We want a mutable set, because it simplifies normalizeCollapsedValues |
| SortedSet<String> temp = new TreeSet<String>(Arrays.asList(values)); |
| collapsedValues = Lists.add(collapsedValues, temp); |
| } |
| |
| public void addDefinedValue(Condition condition, String definedValue) { |
| definedValues.add(definedValue); |
| allowedValues.addValue(condition, definedValue); |
| generatedValues.addValue(condition, definedValue); |
| } |
| |
| /** |
| * Adds fall back value to given property name. |
| * @param value the property value. |
| * @param fallbackValue the fall back value for given property value. |
| */ |
| public void addFallbackValue(String value, String fallbackValue) { |
| LinkedList<String> values = fallbackValues.get(fallbackValue); |
| if (values == null) { |
| values = new LinkedList<String>(); |
| fallbackValues.put(fallbackValue, values); |
| } |
| values.addFirst(value); |
| } |
| |
| @Override |
| public boolean equals(Object object) { |
| if (object instanceof BindingProperty) { |
| BindingProperty that = (BindingProperty) object; |
| return Objects.equal(this.name, that.name) |
| && Objects.equal(this.collapsedValues, that.collapsedValues) |
| && Objects.equal(this.allowedValues, that.allowedValues) |
| && Objects.equal(this.generatedValues, that.generatedValues) |
| && Objects.equal(this.definedValues, that.definedValues) |
| && Objects.equal(this.fallback, that.fallback) |
| && Objects.equal(this.getFallbackValuesMap(), that.getFallbackValuesMap()) |
| && Objects.equal(this.fallbackValues, that.fallbackValues) |
| && Objects.equal(this.provider, that.provider) |
| && Objects.equal(this.providerGenerator, that.providerGenerator) |
| && Objects.equal(this.rootCondition, that.rootCondition); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the set of values defined in the module file. |
| * (For code generation, use {@link #getGeneratedValues} because this might be |
| * overridden.) |
| */ |
| public String[] getAllowedValues(Condition condition) { |
| return allowedValues.getValuesAsArray(condition); |
| } |
| |
| /** |
| * Returns the set of values for which the GWT compiler must generate permutations. |
| */ |
| public String[] getGeneratedValues(Condition condition) { |
| return generatedValues.getValuesAsArray(condition); |
| } |
| |
| public List<SortedSet<String>> getCollapsedValuesSets() { |
| return collapsedValues; |
| } |
| |
| /** |
| * Returns a map containing the generated values for each condition, in the order |
| * they were added to the module files. |
| */ |
| public ImmutableMap<Condition, SortedSet<String>> getConditionalValues() { |
| return generatedValues.toMap(); |
| } |
| |
| /** |
| * If the BindingProperty has exactly one generated value across all conditions and |
| * permutations, return that value otherwise return <code>null</code>. |
| */ |
| public String getConstrainedValue() { |
| if (!generatedValues.allConditionsHaveOneValue()) { |
| return null; |
| } |
| Set<String> values = generatedValues.getAllValues(); |
| if (values.size() != 1) { |
| return null; // For example, two conditions could each have a different value. |
| } |
| return values.iterator().next(); |
| } |
| |
| /** |
| * Returns the set of defined values in sorted order. |
| */ |
| public String[] getDefinedValues() { |
| return definedValues.toArray(new String[definedValues.size()]); |
| } |
| |
| /** |
| * Returns the fallback value for this property, or the empty string if none. |
| * |
| * @return the fallback value |
| */ |
| public String getFallback() { |
| return fallback; |
| } |
| |
| /** |
| * Returns the map of values to fall back values. the list of fall |
| * back values is in decreasing order of preference. |
| * @return map of property value to fall back values. |
| */ |
| public Map<String,? extends List<? extends Set<String>>> getFallbackValuesMap() { |
| if (fallbackValueMap == null) { |
| HashMap<String,LinkedList<LinkedHashSet<String>>> valuesMap = new HashMap<String,LinkedList<LinkedHashSet<String>>>(); |
| // compute closure of fall back values preserving order |
| for (Entry<String, LinkedList<String>> e : fallbackValues.entrySet()) { |
| String from = e.getKey(); |
| LinkedList<LinkedHashSet<String>> alternates = new LinkedList<LinkedHashSet<String>>(); |
| valuesMap.put(from, alternates); |
| LinkedList<String> childList = fallbackValues.get(from); |
| LinkedHashSet<String> children = new LinkedHashSet<String>(); |
| children.addAll(childList); |
| while (children != null && children.size() > 0) { |
| alternates.add(children); |
| LinkedHashSet<String> newChildren = new LinkedHashSet<String>(); |
| for (String child : children) { |
| childList = fallbackValues.get(child); |
| if (null == childList) { |
| continue; |
| } |
| for (String val : childList) { |
| newChildren.add(val); |
| } |
| } |
| children = newChildren; |
| } |
| } |
| fallbackValueMap = valuesMap; |
| } |
| return fallbackValueMap; |
| } |
| |
| /** |
| * Returns the first value from the list of defined values that is that is also allowed. |
| * @throws IllegalStateException if there is not at least one value that's both defined |
| * and allowed. |
| */ |
| public String getFirstAllowedValue() { |
| String value = allowedValues.getFirstMember(definedValues); |
| if (value == null) { |
| throw new IllegalStateException("binding property has no allowed values: " + name); |
| } |
| return value; |
| } |
| |
| /** |
| * Returns the first value from the list of defined values that is actually generated. |
| * @throws IllegalStateException if there is not at least one value that's both defined |
| * and generated. |
| */ |
| public String getFirstGeneratedValue() { |
| if (definedValues.isEmpty()) { |
| // This shouldn't happen but a DynamicPropertyOracleTest currently requires it. |
| // TODO(skybrian) we should probably require fallback values to be defined. |
| // (It's checked when parsing the XML.) |
| return fallback; |
| } |
| String value = generatedValues.getFirstMember(definedValues); |
| if (value == null) { |
| throw new IllegalStateException("binding property has no generated values: " + name); |
| } |
| return value; |
| } |
| |
| public PropertyProvider getProvider() { |
| return provider; |
| } |
| |
| /** |
| * @return the provider generator class, or null if none. |
| */ |
| public Class<? extends PropertyProviderGenerator> getProviderGenerator() { |
| return providerGenerator; |
| } |
| |
| public Set<String> getRequiredProperties() { |
| Set<String> toReturn = Sets.create(); |
| for (Condition cond : generatedValues.eachCondition()) { |
| toReturn = Sets.addAll(toReturn, cond.getRequiredProperties()); |
| } |
| return toReturn; |
| } |
| |
| public ConditionAll getRootCondition() { |
| return rootCondition; |
| } |
| |
| @Override |
| public int hashCode() { |
| return Objects.hashCode(name, collapsedValues, allowedValues, definedValues, fallback, |
| getFallbackValuesMap(), fallbackValues, provider, providerGenerator, rootCondition); |
| } |
| |
| /** |
| * Returns <code>true</code> if the value was previously provided to |
| * {@link #addDefinedValue(Condition,String)}. |
| */ |
| public boolean isDefinedValue(String value) { |
| return definedValues.contains(value); |
| } |
| |
| /** |
| * Returns true if the supplied value is used based on the module file. |
| */ |
| public boolean isAllowedValue(String value) { |
| return allowedValues.containsValue(value); |
| } |
| |
| /** |
| * Returns true if the supplied value will be used during code generation. |
| */ |
| public boolean isGeneratedValue(String value) { |
| return generatedValues.containsValue(value); |
| } |
| |
| /** |
| * Returns <code>true</code> if the value of this BindingProperty is always |
| * derived from other BindingProperties. That is, for each Condition in the |
| * BindingProperty, there is exactly one generated value. |
| */ |
| public boolean isDerived() { |
| return generatedValues.allConditionsHaveOneValue(); |
| } |
| |
| /** |
| * Undo any value restrictions that have been put in place specifically on the set of values used |
| * for code generation as opposed to being present in the actual module definition. |
| */ |
| public void resetGeneratedValues() { |
| generatedValues.valueMap.clear(); |
| generatedValues.valueMap.putAll(allowedValues.valueMap); |
| } |
| |
| public void setFallback(String token) { |
| fallback = token; |
| } |
| |
| public void setProvider(PropertyProvider provider) { |
| this.provider = provider; |
| } |
| |
| /** |
| * Set a provider generator for this property. |
| */ |
| public void setProviderGenerator(Class<? extends PropertyProviderGenerator> generator) { |
| providerGenerator = generator; |
| } |
| |
| /** |
| * Create a minimal number of equivalence sets, expanding any glob patterns. |
| */ |
| void normalizeCollapsedValues() { |
| if (collapsedValues.isEmpty()) { |
| return; |
| } |
| |
| // Expand globs |
| for (Set<String> set : collapsedValues) { |
| // Compile a regex that matches all glob expressions that we see |
| StringBuilder pattern = new StringBuilder(); |
| for (Iterator<String> it = set.iterator(); it.hasNext();) { |
| String value = it.next(); |
| if (value.contains(GLOB_STAR)) { |
| it.remove(); |
| if (pattern.length() > 0) { |
| pattern.append("|"); |
| } |
| |
| // a*b ==> (a.*b) |
| pattern.append("("); |
| // We know value is a Java ident, so no special escaping is needed |
| pattern.append(value.replace(GLOB_STAR, ".*")); |
| pattern.append(")"); |
| } |
| } |
| |
| if (pattern.length() == 0) { |
| continue; |
| } |
| |
| Pattern p = Pattern.compile(pattern.toString()); |
| for (String definedValue : definedValues) { |
| if (p.matcher(definedValue).matches()) { |
| set.add(definedValue); |
| } |
| } |
| } |
| |
| // Minimize number of sets |
| |
| // Maps a value to the set that contains that value |
| Map<String, SortedSet<String>> map = new HashMap<String, SortedSet<String>>(); |
| |
| // For each equivalence set we have |
| for (SortedSet<String> set : collapsedValues) { |
| // Examine each original value in the set |
| for (String value : new LinkedHashSet<String>(set)) { |
| // See if the value was previously assigned to another set |
| SortedSet<String> existing = map.get(value); |
| if (existing == null) { |
| map.put(value, set); |
| } else { |
| // If so, merge the existing set into this one and update pointers |
| set.addAll(existing); |
| for (String mergedValue : existing) { |
| map.put(mergedValue, set); |
| } |
| } |
| } |
| } |
| |
| // The values of the maps will now contain the minimal number of sets |
| collapsedValues = new ArrayList<SortedSet<String>>( |
| new IdentityHashSet<SortedSet<String>>(map.values())); |
| |
| // Sort the list |
| Lists.sort(collapsedValues, new Comparator<SortedSet<String>>() { |
| @Override |
| public int compare(SortedSet<String> o1, SortedSet<String> o2) { |
| String s1 = o1.toString(); |
| String s2 = o2.toString(); |
| assert !s1.equals(s2) : "Should not have seen equal sets"; |
| return s1.compareTo(s2); |
| } |
| }); |
| } |
| |
| /** |
| * Replaces the allowed and generated values for a condition. |
| * If it is the root condition, clears all other conditions. |
| * |
| * @throws IllegalArgumentException if any value isn't currently defined. |
| */ |
| public void setValues(Condition bindingPropertyCondition, String... values) { |
| List<String> valueList = Arrays.asList(values); |
| checkAllDefined(valueList); |
| allowedValues.putValues(bindingPropertyCondition, valueList); |
| generatedValues.putValues(bindingPropertyCondition, valueList); |
| } |
| |
| /** |
| * Overrides the generated values for the root condition and clears the |
| * generated values for other conditions. |
| * |
| * This has no effect on the allowed values and ignores them. |
| * It's intended for artificially restricting permutations |
| * in special modes like Super Dev Mode and the GWTTestCase runner. |
| * |
| * @throws IllegalArgumentException if any value isn't currently defined. |
| */ |
| public void setRootGeneratedValues(String... values) { |
| List<String> valueList = Arrays.asList(values); |
| checkAllDefined(valueList); |
| generatedValues.replaceAllValues(valueList); |
| } |
| |
| private void checkAllDefined(Collection<String> valueList) { |
| for (String value : valueList) { |
| if (!definedValues.contains(value)) { |
| throw new IllegalArgumentException( |
| "Attempted to set a binding property to a value that was not previously defined: " + |
| name + " = '" + value + "'"); |
| } |
| } |
| } |
| |
| /** |
| * Contains a set of binding values for each condition. |
| * |
| * <p>Remembers the order in which they were added. This is needed because |
| * the order in which properties were set in GWT module files is significant. |
| * (The last one wins.) |
| */ |
| private static class ConditionalValues implements Serializable { |
| private final Condition root; |
| private final Map<Condition, SortedSet<String>> valueMap = |
| new LinkedHashMap<Condition, SortedSet<String>>(); |
| |
| private ConditionalValues(Condition root) { |
| // The root condition always has a set of values. By default this is empty. |
| this.root = root; |
| valueMap.put(root, new TreeSet<String>()); |
| } |
| |
| /** |
| * Adds one more value under a condition. If there isn't any set of values for |
| * the given condition, creates it by copying the root condition. |
| */ |
| private void addValue(Condition condition, String value) { |
| SortedSet<String> set = valueMap.get(condition); |
| if (set == null) { |
| set = new TreeSet<String>(valueMap.get(root)); |
| valueMap.put(condition, set); |
| } |
| set.add(value); |
| } |
| |
| /** |
| * Replaces all the values for a condition and moves it to the end of the map. |
| * If it is the root condition, also clears the other conditions. |
| */ |
| private void putValues(Condition condition, Collection<String> values) { |
| |
| // XML has a last-one-wins semantic which we reflect in our evaluation order |
| if (condition == root) { |
| // An unconditional set-property would undo any previous conditional |
| // setters, so we can just clear out this map. |
| replaceAllValues(values); |
| } else { |
| // Otherwise, we'll just ensure that this condition is moved to the end. |
| valueMap.remove(condition); |
| valueMap.put(condition, new TreeSet<String>(values)); |
| } |
| } |
| |
| private void replaceAllValues(Collection<String> rootValues) { |
| valueMap.clear(); |
| valueMap.put(root, new TreeSet<String>(rootValues)); |
| } |
| |
| // === queries === |
| |
| private ImmutableMap<Condition, SortedSet<String>> toMap() { |
| return ImmutableMap.copyOf(valueMap); |
| } |
| |
| private String[] getValuesAsArray(Condition condition) { |
| Set<String> values = valueMap.get(condition); |
| return values.toArray(new String[values.size()]); |
| } |
| |
| private Iterable<Condition> eachCondition() { |
| return valueMap.keySet(); |
| } |
| |
| /** |
| * Returns true if the value appears for any condition. |
| */ |
| private boolean containsValue(String value) { |
| for (Set<String> values : valueMap.values()) { |
| if (values.contains(value)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns all values that appear under at least one condition. |
| */ |
| private Set<String> getAllValues() { |
| SortedSet<String> result = new TreeSet<String>(); |
| for (SortedSet<String> valueSet : valueMap.values()) { |
| result.addAll(valueSet); |
| } |
| return result; |
| } |
| |
| /** |
| * Returns the first value from the given list that's used in at least one condition, |
| * or null if none are. |
| */ |
| private String getFirstMember(Iterable<String> candidates) { |
| Set<String> members = getAllValues(); |
| for (String candidate : candidates) { |
| if (members.contains(candidate)) { |
| return candidate; |
| } |
| } |
| return null; |
| } |
| |
| private boolean allConditionsHaveOneValue() { |
| for (Set<String> values : valueMap.values()) { |
| if (values.size() != 1) { |
| return false; |
| } |
| } |
| return true; |
| } |
| } |
| } |