/*******************************************************************************
* Copyright (c) 2004, 2006
* Thomas Hallgren, Kenneth Olwing, Mitch Sonies
* Pontus Rydin, Nils Unden, Peer Torngren
* The code, documentation and other materials contained herein have been
* licensed under the Eclipse Public License - v 1.0 by the individual
* copyright holders listed above, as Initial Contributors under such license.
* The text of such license is available at www.eclipse.org.
*******************************************************************************/
package org.eclipse.buckminster.core.common.model;
import java.io.IOException;
import java.io.OutputStream;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.eclipse.buckminster.core.helpers.BMProperties;
import org.eclipse.buckminster.core.helpers.MapUnion;
import org.eclipse.buckminster.sax.Utils;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.variables.IDynamicVariable;
import org.eclipse.core.variables.IStringVariableManager;
import org.eclipse.core.variables.IValueVariable;
import org.eclipse.core.variables.VariablesPlugin;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* <p>
* This class works just as a normal <code>Map<String,String></code> but
* with two additions:
* <ul>
* <li>Values are expanded using ant-style property expansion. The scope of the
* expansion is the instance itself.</li>
* <li>Any value can be declared immutable. When doing so, it is guaranteed that
* the value cannot be changed or removed from the map.</li>
* </ul>
* </p>
*
* @author Thomas Hallgren
*/
public class ExpandingProperties<T extends Object> implements IProperties<T>, IExpandingMap<String, T> {
class EntryWrapper implements Entry<String, T> {
private final Entry<String, ValueHolder<T>> entry;
public EntryWrapper(Entry<String, ValueHolder<T>> entry) {
this.entry = entry;
}
@Override
public String getKey() {
return entry.getKey();
}
@Override
public T getValue() {
T value = convertValue(entry.getValue(), 0);
if (value != null)
value = expand(ExpandingProperties.this, value, 0);
return value;
}
@Override
public synchronized T setValue(T value) {
String key = entry.getKey();
ValueHolder<T> vh = entry.getValue();
Constant<T> constant = new Constant<T>(value);
if (!(vh == null || vh.isMutable() || vh.equals(constant)))
throw new ImmutablePropertyException(key);
entry.setValue(constant);
return convertValue(vh, 0);
}
}
public static final int MAX_NESTING_DEPTH = 64;
public static <T> Map<String, T> createUnmodifiableProperties(Map<String, T> aMap) {
if (aMap == null || aMap.size() == 0)
aMap = Collections.emptyMap();
else
aMap = Collections.unmodifiableMap(new ExpandingProperties<T>(aMap));
return aMap;
}
public static <T> T expand(Map<String, ? extends Object> properties, T value, int nestingLevel) {
return checkedExpand(properties, value, value, nestingLevel);
}
@SuppressWarnings("unchecked")
private static <T> T checkedExpand(Map<String, ? extends Object> props, T topValue, T objVal, int recursionGuard) {
if (!(objVal instanceof String))
return objVal;
if (recursionGuard > MAX_NESTING_DEPTH)
throw new CircularExpansionException((String) topValue);
String value = (String) objVal;
StringBuilder bld = null;
int fragmentStart = 0;
int top = value.length();
if (top < 4)
return objVal;
--top; // Last character is not of interest
for (int idx = 0; idx < top; ++idx) {
char c = value.charAt(idx);
if (c != '$')
continue;
if (value.charAt(idx + 1) == '$') {
// Let '$$' mean '$'
//
if (bld == null)
bld = new StringBuilder();
if (idx > fragmentStart)
bld.append(value.substring(fragmentStart, idx));
fragmentStart = ++idx;
continue;
}
if (value.charAt(idx + 1) != '{' || idx + 3 >= top)
//
// Can't be a ${x} construction.
//
continue;
int startPos = idx + 2;
int endPos = parsePropertyName(value, startPos, true);
if (endPos < 0)
continue;
String propKey = value.substring(startPos, endPos);
// We must use the getUnexpandedProperty here if we don't want
// to put the CircularExpansion check out of business.
//
Object propVal = (props instanceof ExpandingProperties<?>) ? ((ExpandingProperties<?>) props).getExpandedProperty(propKey,
recursionGuard + 1) : props.get(propKey);
if (propVal != null) {
if (bld == null)
bld = new StringBuilder();
if (idx > fragmentStart)
bld.append(value.substring(fragmentStart, idx));
bld.append(checkedExpand(props, topValue, propVal, recursionGuard + 1));
fragmentStart = endPos + 1;
}
idx = endPos;
}
if (bld != null) {
++top; // Last character becomes interesting again
if (fragmentStart < top)
bld.append(value.substring(fragmentStart, top));
value = bld.toString();
}
// This cast is safe since we were passed a String to begin with.
//
return (T) value;
}
private static int parsePropertyName(String source, int startIndex, boolean inResolve) {
if (source == null)
return -1;
int top = source.length();
if (startIndex >= top)
return -1;
int idx = startIndex;
char c = source.charAt(idx++);
if (!Character.isJavaIdentifierStart(c))
return -1;
while (idx < top) {
char last = c;
c = source.charAt(idx++);
if (c == '.') {
// Check that the name does not:
// - have repeated dots, i.e. ".."
// - end with a dot
//
if (last == '.' || idx == top || (inResolve && source.charAt(idx) == '}'))
return -1;
} else {
if (inResolve && c == '}')
return idx - 1;
// ':' is allowed after the prefix for eclipse variables like
// env_vars:
if (!(Character.isJavaIdentifierPart(c) || c == ':'))
return -1; // Illegal character
}
}
return top;
}
private final Map<String, ValueHolder<T>> map;
public ExpandingProperties() {
map = new HashMap<String, ValueHolder<T>>();
}
public ExpandingProperties(int size) {
map = new HashMap<String, ValueHolder<T>>(size);
}
@SuppressWarnings("unchecked")
public ExpandingProperties(Map<String, ? extends T> dflts) {
Map<String, ValueHolder<T>> overlay = new HashMap<String, ValueHolder<T>>();
if (dflts == null || dflts.size() == 0) {
map = overlay;
return;
}
Map<String, ValueHolder<T>> dfltMap;
if (dflts instanceof ExpandingProperties<?>)
dfltMap = ((ExpandingProperties<T>) dflts).map;
else {
dfltMap = new HashMap<String, ValueHolder<T>>(dflts.size());
for (Map.Entry<String, ? extends T> de : dflts.entrySet()) {
ValueHolder<T> vh = new Constant<T>(de.getValue());
vh.setMutable(true);
dfltMap.put(de.getKey(), vh);
}
}
map = new MapUnion<String, ValueHolder<T>>(overlay, dfltMap);
}
@Override
public void clear() {
if (map.isEmpty())
return;
for (Map.Entry<String, ValueHolder<T>> ee : map.entrySet())
if (!ee.getValue().isMutable())
throw new ImmutablePropertyException(ee.getKey());
map.clear();
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public Set<Entry<String, T>> entrySet() {
return new AbstractSet<Entry<String, T>>() {
@Override
public Iterator<Entry<String, T>> iterator() {
return new Iterator<Entry<String, T>>() {
private final Iterator<Entry<String, ValueHolder<T>>> itor = map.entrySet().iterator();
@Override
public boolean hasNext() {
return itor.hasNext();
}
@Override
public Entry<String, T> next() {
return new EntryWrapper(itor.next());
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public int size() {
return map.size();
}
};
}
@Override
public boolean equals(Object o) {
if (o == this)
return true;
return (o instanceof ExpandingProperties<?>) && map.equals(((ExpandingProperties<?>) o).map);
}
@Override
public T get(Object key) {
return key instanceof String ? getExpandedProperty((String) key, 0) : null;
}
@Override
public T get(Object key, Map<String, T> expansionScope) {
ValueHolder<T> vh = map.get(key);
if (vh != null)
return vh.checkedGetValue(expansionScope, 0);
if (key instanceof String)
try {
@SuppressWarnings("unchecked")
T value = (T) resolveEclipseVariables((String) key);
return value;
} catch (ClassCastException e) {
}
return null;
}
@Override
public int hashCode() {
return map.hashCode();
}
@Override
public Set<String> immutableKeySet() {
HashSet<String> immutableSet = new HashSet<String>();
for (Map.Entry<String, ValueHolder<T>> me : map.entrySet()) {
if (!me.getValue().isMutable())
immutableSet.add(me.getKey());
}
return immutableSet;
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean isMutable(String key) {
ValueHolder<T> v = map.get(key);
return v == null || v.isMutable();
}
@Override
public Set<String> keySet() {
return map.keySet();
}
@Override
public Set<String> mutableKeySet() {
HashSet<String> mutableSet = new HashSet<String>();
for (Map.Entry<String, ValueHolder<T>> me : map.entrySet()) {
if (me.getValue().isMutable())
mutableSet.add(me.getKey());
}
return mutableSet;
}
@Override
public Set<String> overlayKeySet() {
return (map instanceof MapUnion<?, ?>) ? ((MapUnion<String, ValueHolder<T>>) map).overlayKeySet() : map.keySet();
}
@Override
public T put(String key, T propVal) {
return convertValue(setProperty(key, new Constant<T>(propVal)), 0);
}
@Override
public T put(String key, T propVal, boolean mutable) {
Constant<T> vh = new Constant<T>(propVal);
vh.setMutable(mutable);
return convertValue(setProperty(key, vh), 0);
}
@Override
public void putAll(Map<? extends String, ? extends T> t) {
putAll(t, false);
}
@SuppressWarnings("unchecked")
public void putAll(Map<? extends String, ? extends T> t, boolean mutable) {
if (t instanceof ExpandingProperties<?>) {
// Defer expansion until access.
//
for (Map.Entry<String, ValueHolder<T>> ee : ((ExpandingProperties<T>) t).map.entrySet()) {
ee.getValue().setMutable(mutable);
setProperty(ee.getKey(), ee.getValue());
}
} else {
for (Map.Entry<? extends String, ? extends T> ee : t.entrySet()) {
ValueHolder<T> vh = new Constant<T>(ee.getValue());
vh.setMutable(mutable);
setProperty(ee.getKey(), vh);
}
}
}
@Override
public T remove(Object key) {
if (key instanceof String) {
String strKey = (String) key;
ValueHolder<T> vh = map.remove(strKey);
if (vh != null) {
if (!vh.isMutable()) {
map.put(strKey, vh);
throw new ImmutablePropertyException(strKey);
}
return vh.checkedGetValue(this, 0);
}
}
return null;
}
@Override
public void setMutable(String key, boolean flag) {
ValueHolder<T> v = map.get(key);
if (v != null)
v.setMutable(flag);
}
public ValueHolder<T> setProperty(String key, ValueHolder<T> propertyHolder) {
ValueHolder<T> v = map.put(key, propertyHolder);
if (!(v == null || v.isMutable() || v.equals(propertyHolder))) {
map.put(key, v);
throw new ImmutablePropertyException(key);
}
return v;
}
@Override
public int size() {
return map.size();
}
@Override
public void store(OutputStream out, String comments) throws IOException {
BMProperties.store(this, out, comments);
}
@Override
public boolean supportsMutability() {
return true;
}
@Override
public Collection<T> values() {
return new AbstractCollection<T>() {
@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
private final Iterator<ValueHolder<T>> itor = map.values().iterator();
@Override
public boolean hasNext() {
return itor.hasNext();
}
@Override
public T next() {
T value = convertValue(itor.next(), 0);
if (value != null)
value = expand(ExpandingProperties.this, value, 0);
return value;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
public int size() {
return map.size();
}
};
}
void emitProperties(ContentHandler handler, String namespace, String prefix, boolean includeDefaults) throws SAXException {
String plName = "property"; //$NON-NLS-1$
String pqName = Utils.makeQualifiedName(prefix, plName);
String pelName = "propertyElement"; //$NON-NLS-1$
String peqName = Utils.makeQualifiedName(prefix, pelName);
AttributesImpl attrs = new AttributesImpl();
TreeSet<String> sorted = new TreeSet<String>();
if (includeDefaults) {
for (String key : keySet())
sorted.add(key);
} else {
for (String name : overlayKeySet())
sorted.add(name);
}
for (String name : sorted) {
ValueHolder<T> value = map.get(name);
if (value == null)
continue;
if (includeDefaults && value instanceof Constant<?>) {
// We still don't include unmodified system properties.
//
String sysValue = System.getProperty(name);
if (sysValue != null && sysValue.equals(value))
continue;
}
attrs.clear();
Utils.addAttribute(attrs, "key", name); //$NON-NLS-1$
if (value.isMutable())
Utils.addAttribute(attrs, "mutable", "true"); //$NON-NLS-1$ //$NON-NLS-2$
if (value instanceof Constant<?>) {
Utils.addAttribute(attrs, "value", value.toString()); //$NON-NLS-1$
handler.startElement(namespace, plName, pqName, attrs);
handler.endElement(namespace, plName, pqName);
} else {
handler.startElement(namespace, pelName, peqName, attrs);
value.toSax(handler, namespace, prefix, value.getDefaultTag());
handler.endElement(namespace, pelName, peqName);
}
}
}
@SuppressWarnings("unchecked")
T getExpandedProperty(String key, int recursionGuard) {
if (map.containsKey(key))
return convertValue(map.get(key), recursionGuard);
try {
return (T) resolveEclipseVariables(key);
} catch (ClassCastException e) {
// String is not compatible to T
}
return null;
}
private T convertValue(ValueHolder<T> vh, int recursionGuard) {
return vh == null ? null : vh.checkedGetValue(this, recursionGuard);
}
private String resolveEclipseVariables(String key) {
if (key == null)
return null;
IStringVariableManager variableManager = VariablesPlugin.getDefault().getStringVariableManager();
int index = key.indexOf(':');
// i.e. the key has a prefix and an argument like env_var:FOOBAR
if (index > 1) {
String varName = key.substring(0, index);
IDynamicVariable variable = variableManager.getDynamicVariable(varName);
if (variable == null)
return null;
try {
if (key.length() > index + 1)
return variable.getValue(key.substring(index + 1));
return variable.getValue(null);
} catch (CoreException e) {
// the value could not be resolved
return null;
}
}
// first try value variables
IValueVariable variable = variableManager.getValueVariable(key);
if (variable == null) {
// fall back to dynamic variables without arguments
IDynamicVariable dynamicVariable = variableManager.getDynamicVariable(key);
if (dynamicVariable == null)
return null;
try {
return dynamicVariable.getValue(null);
} catch (CoreException e) {
// the value could not be resolved
return null;
}
}
return variable.getValue();
}
}