/******************************************************************************* * 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.model.common.util; 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 org.eclipse.buckminster.model.common.CommonFactory; import org.eclipse.buckminster.model.common.Constant; import org.eclipse.buckminster.model.common.Value; import org.eclipse.buckminster.model.common.impl.ValueImpl; /** * <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 implements IProperties { class EntryWrapper implements Entry<String, String> { private final Entry<String, Value> entry; public EntryWrapper(Entry<String, Value> entry) { this.entry = entry; } @Override public String getKey() { return entry.getKey(); } @Override public String getValue() { String value = convertValue(entry.getValue(), 0); if (value != null) value = expand(ExpandingProperties.this, value, 0); return value; } @Override public synchronized String setValue(String value) { String key = entry.getKey(); Value vh = entry.getValue(); Constant constant = CommonFactory.eINSTANCE.createConstant(); constant.setValue(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 Map<String, String> createUnmodifiableProperties(Map<String, String> aMap) { if (aMap == null || aMap.size() == 0) aMap = Collections.emptyMap(); else aMap = Collections.unmodifiableMap(new ExpandingProperties(aMap)); return aMap; } public static String expand(Map<String, String> properties, String value, int nestingLevel) { return checkedExpand(properties, value, value, nestingLevel); } private static String checkedExpand(Map<String, String> props, String topValue, String value, int recursionGuard) { if (recursionGuard > MAX_NESTING_DEPTH) throw new CircularExpansionException(topValue); if (value == null) return null; StringBuilder bld = null; int fragmentStart = 0; int top = value.length(); if (top < 4) return value; --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. // String 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 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; if (!Character.isJavaIdentifierPart(c)) return -1; // Illegal character } } return top; } private final Map<String, Value> map; public ExpandingProperties() { map = new HashMap<String, Value>(); } public ExpandingProperties(int size) { map = new HashMap<String, Value>(size); } public ExpandingProperties(Map<String, String> dflts) { Map<String, Value> overlay = new HashMap<String, Value>(); if (dflts == null || dflts.size() == 0) { map = overlay; return; } Map<String, Value> dfltMap; if (dflts instanceof ExpandingProperties) dfltMap = ((ExpandingProperties) dflts).map; else { dfltMap = new HashMap<String, Value>(dflts.size()); for (Map.Entry<String, String> de : dflts.entrySet()) { Constant constant = CommonFactory.eINSTANCE.createConstant(); constant.setValue(de.getValue()); constant.setMutable(true); dfltMap.put(de.getKey(), constant); } } map = new MapUnion<String, Value>(overlay, dfltMap); } public ExpandingProperties(Map<String, Value> map, Map<String, Value> overlay) { if (overlay != null) map = new MapUnion<String, Value>(overlay, map); this.map = map; } @Override public void clear() { if (map.isEmpty()) return; for (Map.Entry<String, Value> 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, String>> entrySet() { return new AbstractSet<Entry<String, String>>() { @Override public Iterator<Entry<String, String>> iterator() { return new Iterator<Entry<String, String>>() { private final Iterator<Entry<String, Value>> itor = map.entrySet().iterator(); @Override public boolean hasNext() { return itor.hasNext(); } @Override public Entry<String, String> 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 String get(Object key) { return key instanceof String ? getExpandedProperty((String) key, 0) : null; } public String getExpandedProperty(String key, int recursionGuard) { return convertValue(map.get(key), recursionGuard); } public Value getInternalValue(String key) { return map.get(key); } @Override public int hashCode() { return map.hashCode(); } @Override public Set<String> immutableKeySet() { HashSet<String> immutableSet = new HashSet<String>(); for (Map.Entry<String, Value> 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) { Value 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, Value> me : map.entrySet()) { if (me.getValue().isMutable()) mutableSet.add(me.getKey()); } return mutableSet; } @Override public Set<String> overlayKeySet() { return (map instanceof MapUnion<?, ?>) ? ((MapUnion<String, Value>) map).overlayKeySet() : map.keySet(); } @Override public String put(String key, String propVal) { Constant constant = CommonFactory.eINSTANCE.createConstant(); constant.setValue(propVal); return convertValue(setProperty(key, constant), 0); } @Override public String put(String key, String propVal, boolean mutable) { Constant constant = CommonFactory.eINSTANCE.createConstant(); constant.setValue(propVal); constant.setMutable(mutable); return convertValue(setProperty(key, constant), 0); } @Override public void putAll(Map<? extends String, ? extends String> t) { putAll(t, false); } public void putAll(Map<? extends String, ? extends String> t, boolean mutable) { if (t instanceof ExpandingProperties) { // Defer expansion until access. // for (Map.Entry<String, Value> ee : ((ExpandingProperties) t).map.entrySet()) { ee.getValue().setMutable(mutable); setProperty(ee.getKey(), ee.getValue()); } } else { for (Map.Entry<? extends String, ? extends String> ee : t.entrySet()) { Constant constant = CommonFactory.eINSTANCE.createConstant(); constant.setValue(ee.getValue()); constant.setMutable(mutable); setProperty(ee.getKey(), constant); } } } @Override public String remove(Object key) { if (key instanceof String) { String strKey = (String) key; Value vh = map.remove(strKey); if (vh != null) { if (!vh.isMutable()) { map.put(strKey, vh); throw new ImmutablePropertyException(strKey); } return ((ValueImpl) vh).checkedGetValue(this, 0); } } return null; } @Override public void setMutable(String key, boolean flag) { Value v = map.get(key); if (v != null) v.setMutable(flag); } public Value setProperty(String key, Value propertyHolder) { Value 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<String> values() { return new AbstractCollection<String>() { @Override public Iterator<String> iterator() { return new Iterator<String>() { private final Iterator<Value> itor = map.values().iterator(); @Override public boolean hasNext() { return itor.hasNext(); } @Override public String next() { String 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(); } }; } private String convertValue(Value vh, int recursionGuard) { return vh == null ? null : ((ValueImpl) vh).checkedGetValue(this, recursionGuard); } }