package aQute.bnd.header; import java.io.IOException; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import aQute.bnd.version.Version; public class Attrs implements Map<String,String> { public interface DataType<T> { Type type(); } public static DataType<String> STRING = new DataType<String>() { public Type type() { return Type.STRING; } }; public static DataType<Long> LONG = new DataType<Long>() { public Type type() { return Type.LONG; } };; public static DataType<Double> DOUBLE = new DataType<Double>() { public Type type() { return Type.DOUBLE; } };; public static DataType<Version> VERSION = new DataType<Version>() { public Type type() { return Type.VERSION; } };; public static DataType<List<String>> LIST_STRING = new DataType<List<String>>() { public Type type() { return Type.STRINGS; } };; public static DataType<List<Long>> LIST_LONG = new DataType<List<Long>>() { public Type type() { return Type.LONGS; } };; public static DataType<List<Double>> LIST_DOUBLE = new DataType<List<Double>>() { public Type type() { return Type.DOUBLES; } };; public static DataType<List<Version>> LIST_VERSION = new DataType<List<Version>>() { public Type type() { return Type.VERSIONS; } };; public enum Type { STRING(null, "String"), LONG(null, "Long"), VERSION(null, "Version"), DOUBLE(null, "Double"), STRINGS(STRING, "List<String>"), LONGS(LONG, "List<Long>"), VERSIONS(VERSION, "List<Version>"), DOUBLES(DOUBLE, "List<Double>"); Type sub; String toString; Type(Type sub, String toString) { this.sub = sub; this.toString = toString; } public String toString() { return toString; } public Type plural() { switch (this) { case DOUBLE : return DOUBLES; case LONG : return LONGS; case STRING : return STRINGS; case VERSION : return VERSIONS; default : return null; } } } /** * <pre> * Provide-Capability ::= capability ::= name-space ::= typed-attr ::= * type ::= scalar ::= capability ( ',' capability )* name-space ( ’;’ * directive | typed-attr )* symbolic-name extended ( ’:’ type ) ’=’ * argument scalar | list ’String’ | ’Version’ | ’Long’ list ::= ’List<’ * scalar ’>’ * </pre> */ static String EXTENDED = "[\\-0-9a-zA-Z\\._]+"; static String SCALAR = "String|Version|Long|Double"; static String LIST = "List\\s*<\\s*(" + SCALAR + ")\\s*>"; public static final Pattern TYPED = Pattern .compile("\\s*(" + EXTENDED + ")\\s*:\\s*(" + SCALAR + "|" + LIST + ")\\s*"); private Map<String,String> map; private Map<String,Type> types = new LinkedHashMap<String,Type>(); static Map<String,String> EMPTY = Collections.emptyMap(); public static Attrs EMPTY_ATTRS = new Attrs(); static { EMPTY_ATTRS.map = Collections.emptyMap(); } public Attrs() {} public Attrs(Attrs... attrs) { for (Attrs a : attrs) { if (a != null) { putAll(a); if (a.types != null) types.putAll(a.types); } } } public void putAllTyped(Map<String,Object> attrs) { for (Map.Entry<String,Object> entry : attrs.entrySet()) { Object value = entry.getValue(); String key = entry.getKey(); putTyped(key, value); } } public void putTyped(String key, Object value) { if (value == null) { put(key, null); return; } if (!(value instanceof String)) { Type type; if (value instanceof Collection) value = ((Collection< ? >) value).toArray(); if (value.getClass().isArray()) { type = Type.STRINGS; int l = Array.getLength(value); StringBuilder sb = new StringBuilder(); String del = ""; boolean first = true; for (int i = 0; i < l; i++) { Object member = Array.get(value, i); if (member == null) { // TODO What do we do with null members? continue; } else if (first) { type = getObjectType(member).plural(); first = true; } sb.append(del); int n = sb.length(); sb.append(member); while (n < sb.length()) { char c = sb.charAt(n); if (c == '\\' || c == ',') { sb.insert(n, '\\'); n++; } n++; } del = ","; } value = sb; } else { type = getObjectType(value); } key += ":" + type.toString(); } put(key, value.toString()); } private Type getObjectType(Object member) { if (member instanceof Double || member instanceof Float) return Type.DOUBLE; if (member instanceof Number) return Type.LONG; if (member instanceof Version) return Type.VERSION; return Type.STRING; } public void clear() { map.clear(); } public boolean containsKey(String name) { if (map == null) return false; return map.containsKey(name); } @SuppressWarnings("cast") @Deprecated public boolean containsKey(Object name) { assert name instanceof String; if (map == null) return false; return map.containsKey(name); } public boolean containsValue(String value) { if (map == null) return false; return map.containsValue(value); } @SuppressWarnings("cast") @Deprecated public boolean containsValue(Object value) { assert value instanceof String; if (map == null) return false; return map.containsValue(value); } public Set<java.util.Map.Entry<String,String>> entrySet() { if (map == null) return EMPTY.entrySet(); return map.entrySet(); } @SuppressWarnings("cast") @Deprecated public String get(Object key) { assert key instanceof String; if (map == null) return null; return map.get(key); } public String get(String key) { if (map == null) return null; return map.get(key); } public String get(String key, String deflt) { String s = get(key); if (s == null) return deflt; return s; } public boolean isEmpty() { return map == null || map.isEmpty(); } public Set<String> keySet() { if (map == null) return EMPTY.keySet(); return map.keySet(); } public String put(String key, String value) { if (key == null) return null; if (map == null) map = new LinkedHashMap<String,String>(); Matcher m = TYPED.matcher(key); if (m.matches()) { key = m.group(1); String type = m.group(2); Type t = Type.STRING; if (type.startsWith("List")) { type = m.group(3); if ("String".equals(type)) t = Type.STRINGS; else if ("Long".equals(type)) t = Type.LONGS; else if ("Double".equals(type)) t = Type.DOUBLES; else if ("Version".equals(type)) t = Type.VERSIONS; } else { if ("String".equals(type)) t = Type.STRING; else if ("Long".equals(type)) t = Type.LONG; else if ("Double".equals(type)) t = Type.DOUBLE; else if ("Version".equals(type)) t = Type.VERSION; } types.put(key, t); // TODO verify value? } return map.put(key, value); } public Type getType(String key) { if (types == null) return Type.STRING; Type t = types.get(key); if (t == null) return Type.STRING; return t; } public void putAll(Map< ? extends String, ? extends String> map) { for (Map.Entry< ? extends String, ? extends String> e : map.entrySet()) put(e.getKey(), e.getValue()); } @SuppressWarnings("cast") @Deprecated public String remove(Object var0) { assert var0 instanceof String; if (map == null) return null; return map.remove(var0); } public String remove(String var0) { if (map == null) return null; return map.remove(var0); } public int size() { if (map == null) return 0; return map.size(); } public Collection<String> values() { if (map == null) return EMPTY.values(); return map.values(); } public String getVersion() { return get("version"); } @Override public String toString() { StringBuilder sb = new StringBuilder(); append(sb); return sb.toString(); } public void append(StringBuilder sb) { try { String del = ""; for (Map.Entry<String,String> e : entrySet()) { sb.append(del); append(sb, e); del = ";"; } } catch (Exception e) { // Cannot happen e.printStackTrace(); } } public void append(StringBuilder sb, Map.Entry<String,String> e) throws IOException { sb.append(e.getKey()); if (types != null) { Type type = types.get(e.getKey()); if (type != null) { sb.append(":").append(type); } } sb.append("="); OSGiHeader.quote(sb, e.getValue()); } @Override @Deprecated public boolean equals(Object other) { return super.equals(other); } @Override @Deprecated public int hashCode() { return super.hashCode(); } public boolean isEqual(Attrs other) { if (this == other) return true; if (other == null || size() != other.size()) return false; if (isEmpty()) return true; TreeSet<String> l = new TreeSet<String>(keySet()); TreeSet<String> lo = new TreeSet<String>(other.keySet()); if (!l.equals(lo)) return false; for (String key : keySet()) { String value = get(key); String valueo = other.get(key); if (!(value == valueo || (value != null && value.equals(valueo)))) return false; } return true; } public Object getTyped(String adname) { String s = get(adname); if (s == null) return null; Type t = getType(adname); return convert(t, s); } @SuppressWarnings("unchecked") public <T> T getTyped(DataType<T> type, String adname) { String s = get(adname); if (s == null) return null; Type t = getType(adname); if (t != type.type()) throw new IllegalArgumentException( "For key " + adname + ", expected " + type.type() + " but had a " + t + ". Value is " + s); return (T) convert(t, s); } public static Type toType(String type) { for (Type t : Type.values()) { if (t.toString.equals(type)) return t; } return null; } public static Object convert(String t, String s) { if (s == null) return null; Type type = toType(t); if (type == null) return s; return convert(type, s); } public static Object convert(Type t, String s) { if (t.sub == null) { switch (t) { case STRING : return s; case LONG : return Long.parseLong(s.trim()); case VERSION : return Version.parseVersion(s); case DOUBLE : return Double.parseDouble(s.trim()); case DOUBLES : case LONGS : case STRINGS : case VERSIONS : // Cannot happen since the sub is null return null; } return null; } List<Object> list = new ArrayList<Object>(); List<String> split = splitListAttribute(s); for (String p : split) list.add(convert(t.sub, p)); return list; } static List<String> splitListAttribute(String input) throws IllegalArgumentException { List<String> result = new LinkedList<String>(); StringBuilder builder = new StringBuilder(); for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); switch (c) { case '\\' : i++; if (i >= input.length()) throw new IllegalArgumentException("Trailing blackslash in multi-valued attribute value"); c = input.charAt(i); builder.append(c); break; case ',' : result.add(builder.toString()); builder = new StringBuilder(); break; default : builder.append(c); break; } } result.add(builder.toString()); return result; } /** * Merge the attributes */ public void mergeWith(Attrs other, boolean override) { for (Map.Entry<String,String> e : other.entrySet()) { String local = get(e.getKey()); if (override || local == null) put(e.getKey(), e.getValue()); } } /** * Check if a directive, if so, return directive name otherwise null */ public static String toDirective(String key) { if (key == null || !key.endsWith(":")) return null; return key.substring(0, key.length() - 1); } public static Attrs create(String key, String value) { Attrs attrs = new Attrs(); attrs.put(key, value); return attrs; } public Attrs with(String key, String value) { put(key, value); return this; } }