blob: fd2200768abae49a70a70738897a69097b08f90c [file] [log] [blame]
/*
* 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;
}
}
}