package osgi.dto.provider; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Formatter; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.WeakHashMap; import java.util.regex.Pattern; import org.osgi.dto.DTO; import osgi.enroute.dto.api.DTOs; import osgi.enroute.dto.api.IDTO; import osgi.enroute.dto.api.TypeReference; import aQute.lib.json.Decoder; import aQute.lib.json.Encoder; import aQute.lib.json.JSONCodec; /** * This class provides utility functions for DTOs */ public class DTOsProvider implements DTOs { private final static Field[] EMPTY_FIELDS = new Field[0]; private final static JSONCodec codec = new JSONCodec(); private final static Map<Class< ? >,Field[]> cache = Collections .synchronizedMap(new WeakHashMap<Class< ? >,Field[]>()); private final Link root = new Link(null, null, null); // // The link class is to keep track of cycles traversing and to // maintain the path at minimum cost. // static class Link { final Link prev; final Object object; final Object name; public Link(Link link, Object name, Object object) { this.prev = link; this.name = name; this.object = object; } boolean isCycle(Object t) { if (this.object == t) return true; else if (prev == null) return false; else return prev.isCycle(t); } String[] getPath(int n) { if (prev == null) { String[] path = new String[n]; return path; } String[] path = prev.getPath(n + 1); path[path.length - n - 1] = name.toString(); return path; } void verifyCycle(Object o) { if (isCycle(o)) { throw new IllegalArgumentException("Cycle in DTO " + getPath(0)); } } } // // Helper methods to return a Difference object // for diffing. // static class Diff extends Difference { public Diff(Reason reason, Link link) { this.reason = reason; this.path = link.getPath(0); } } // // Indicates a problem while traversing a path // or contains the result // static class Answer extends Retrieve { // // public Answer(Object dto, String failure) { value = dto; this.failure = failure; } public String toString() { if (failure != null) return "Fail: " + failure; else return "Value: " + value; } public boolean isFailure() { return failure != null; } } @Override public DTOs.Converter convert(final Object source) { return new DTOs.Converter() { public <T> T to(Class<T> dest) throws Exception { return aQute.lib.converter.Converter.cnv(dest, source); } @SuppressWarnings("unchecked") public <T> T to(TypeReference<T> dest) throws Exception { return (T) aQute.lib.converter.Converter.cnv(dest.getType(), source); } public Object to(Type dest) throws Exception { return aQute.lib.converter.Converter.cnv(dest, source); } }; } @Override public Map<String,Object> asMap(Object dto) throws Exception { return new DTOMap(this, dto); } @Override public String toString(Object dto) { if (dto == null) return null + ""; Field[] fields = getFields(dto); if (fields.length == 0) return dto.toString(); try { try (Formatter format = new Formatter()) { for (Field f : fields) { format.format("%s: %s%n", f.getName(), f.get(dto)); } return format.toString(); } } catch (IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException(e); } } @Override public boolean equals(Object a, Object b) { if (b == a) return true; if (b == null || a == null) return false; Class< ? > ac = a.getClass(); if (ac != b.getClass()) return false; try { Field fields[] = getFields(a); if (fields.length == 0) return a.equals(b); for (Field f : getFields(a)) { Object aa = f.get(a); Object bb = f.get(b); if (!equals(aa, bb)) return false; } return true; } catch (Exception e) { return false; } } @Override public int hashCode(Object dto) { if (dto instanceof IDTO) return dto.hashCode(); Field[] fields = getFields(dto); if (fields.length == 0) return dto.hashCode(); int prime = 31; int result = 1; try { for (Field f : fields) { Object a = f.get(this); result = prime * result + (a == null ? 0 : hashCode(dto)); } return result; } catch (Exception e) { return result; } } @Override public Answer get(Object dto, String path) throws Exception { return get(dto, fromPathToSegments(path)); } @Override public Answer get(Object dto, String... path) throws Exception { return get(dto, path, 0, path.length); } private Answer get(Object dto, String[] path, int i, int max) throws Exception { if (i > path.length) throw new IllegalArgumentException("Incorrect index in path " + Arrays.toString(path) + "[" + i + "]"); if (i == path.length || i == max) return new Answer(dto, null); if (dto == null) return new Answer(null, "null encountered @ " + Arrays.toString(path) + " " + i); String name = path[i]; if (dto.getClass().isArray()) { int index = Integer.parseInt(name); if (index >= Array.getLength(dto)) return new Answer(null, "path access contains an array but the corresponding index is not an integer: " + Arrays.toString(path) + "[" + i + "]"); return get(Array.get(dto, index), path, i + 1, max); } if (dto instanceof Collection) { Collection< ? > coll = (Collection< ? >) dto; int index = Integer.parseInt(name); if (index >= coll.size()) return new Answer(null, "path access contains a collection but the corresponding index is not an integer: " + Arrays.toString(path) + "[" + i + "]"); if (coll instanceof List) { return get(((List< ? >) coll).get(index), path, i + 1, max); } for (Object o : coll) { if (index-- == 0) return get(o, path, i + 1, max); } assert false; return null; // unreachable } if (dto instanceof Map) { Object value = ((Map< ? , ? >) dto).get(name); return get(value, path, i + 1, max); } Field fields[] = getFields(dto); if (fields.length > 0) { for (Field field : fields) { if (field.getName().equals(name)) { return get(field.get(dto), path, i + 1, max); } } } return new Answer(null, "Unknown type to traverse " + dto.getClass() + " for " + name); } @Override public Answer set(Object dto, Object value, String path) throws Exception { return set(dto, value, fromPathToSegments(path)); } @SuppressWarnings("unchecked") public Answer set(Object dto, Object value, String... path) throws Exception { try { if (path.length == 0) throw new IllegalArgumentException( "To set a value, you need at least one path segment, this one is empty "); Answer target = get(dto, path, 0, path.length - 1); if (target.isFailure()) return target; String name = path[path.length - 1]; if (dto instanceof Collection) { Collection<Object> coll = (Collection<Object>) dto; switch (name) { case "+" : coll.add(value); break; case "-" : coll.add(value); break; default : int index = Integer.parseInt(name); if (coll instanceof List) { List<Object> l = (List<Object>) coll; while (l.size() < index + 1) l.add(null); l.set(index, value); return new Answer(dto, null); } return new Answer(null, "Cannot " + Arrays.toString(path) + "[" + index + "]"); } return new Answer(null, "Only List can be indexed, is " + coll.getClass()); } if (dto.getClass().isArray()) { int index = Integer.parseInt(name); if (index >= Array.getLength(dto)) return new Answer(null, "path access contains an array but the corresponding index is not an integer: " + Arrays.toString(path) + "[" + index + "]"); Array.set(dto, index, value); return new Answer(dto, null); } if (dto instanceof Map) { Map<Object,Object> map = (Map<Object,Object>) dto; map.put(name, value); return new Answer(dto, null); } Field fields[] = getFields(dto); if (fields.length > 0) { for (Field field : fields) { if (field.getName().equals(name)) { field.set(dto, value); return new Answer(dto, null); } } } return new Answer(null, "Unknown type to set value for " + dto.getClass()); } catch (final Exception e) { return new Answer(null, e.getMessage()); } } @Override public List<Difference> diff(Object older, final Object newer) throws Exception { List<Difference> diffs = new ArrayList<>(); diff(diffs, root, older, newer); return diffs; } private boolean diff(List<Difference> diffs, Link link, Object older, Object newer) throws Exception { if (older == newer) return false; if (older == null) { diffs.add(new Diff(Reason.ADDED, link)); return true; } if (newer == null) { diffs.add(new Diff(Reason.REMOVED, link)); return true; } Class< ? > oc = older.getClass(); Class< ? > nc = newer.getClass(); if (oc != nc) { diffs.add(new Diff(Reason.DIFFERENT_TYPES, link)); return true; } if (older.equals(newer)) return false; if (older instanceof Collection< ? >) { Collection< ? > co = (Collection< ? >) older; Collection< ? > cn = (Collection< ? >) newer; if (co.size() != cn.size()) { diffs.add(new Diff(Reason.SIZE, link)); return true; } if (co.equals(cn)) return false; // // They're different, if it is a list we can find out which // if (older instanceof List< ? >) { List< ? > clo = (List< ? >) older; List< ? > cln = (List< ? >) newer; for (int i = 0; i < co.size(); i++) { Object lo = clo.get(i); Object ln = cln.get(i); diff(diffs, new Link(link, i, older), lo, ln); } return true; } // // If not a list, we're lost ... // diffs.add(new Diff(Reason.UNEQUAL, link)); return true; } if (oc.isArray()) { Object[] ao = new Object[] { older }; Object[] an = new Object[] { newer }; if (Arrays.deepEquals(ao, an)) { return false; } int lo = Array.getLength(older); int ln = Array.getLength(newer); if (lo != ln) { diffs.add(new Diff(Reason.SIZE, link)); return true; } for (int i = 0; i < lo; i++) { diff(diffs, new Link(link, i, older), Array.get(older, i), Array.get(newer, i)); } return true; } if (older instanceof Map< ? , ? >) { Map< ? , ? > co = (Map< ? , ? >) older; Map< ? , ? > cn = (Map< ? , ? >) newer; if (co.size() != cn.size()) { diffs.add(new Diff(Reason.SIZE, link)); return true; } if (co.equals(cn)) return false; if (!co.keySet().equals(cn.keySet())) { diffs.add(new Diff(Reason.KEYS, link)); return true; } for (Map.Entry< ? , ? > e : co.entrySet()) { Object key = e.getKey(); if (!(key instanceof String)) { diffs.add(new Diff(Reason.NO_STRING_MAP, link)); return true; } String k = escape((String) key); Object no = co.get(key); Object nn = cn.get(key); diff(diffs, new Link(link, k, older), no, nn); } return true; } Field[] fields = getFields(older); if (fields.length > 0) { for (Field field : fields) { Object o = field.get(older); Object n = field.get(older); diff(diffs, new Link(link, field.getName(), older), o, n); } return true; } diffs.add(new Diff(Reason.UNEQUAL, link)); return true; } static Pattern ESCAPE_P = Pattern.compile("(\\.|\\\\)"); static Pattern UNESCAPE_P = Pattern.compile("\\\\(\\.|\\\\)"); public String escape(String unescaped) { return ESCAPE_P.matcher(unescaped).replaceAll("\\\\$1"); } public String unescape(String unescaped) { return UNESCAPE_P.matcher(unescaped).replaceAll("$1"); } public boolean isComplex(Object a) { return a != null && (a instanceof Map || a instanceof Collection || a instanceof DTO || a.getClass().isArray() || getFields(a).length > 0); } public boolean isDTO(Object o) { return getFields(o).length != 0; } class EncImpl implements Enc { private Encoder enc = codec.enc(); private Object source; public EncImpl(Object source) { this.source = source; } @Override public void put(OutputStream out) throws Exception { codec.enc().charset("UTF-8").to(out).put(source); } @Override public void put(OutputStream out, String charset) throws Exception { enc.charset(charset).put(out); } @Override public void put(Appendable out) throws Exception { enc.put(out); } @Override public String put() throws Exception { return enc.to().put(source).toString(); } @Override public Enc pretty() { enc.indent("\t"); return this; } @Override public Enc ignoreNull() { return this; } } @SuppressWarnings("unchecked") class DecImpl<T> implements Dec<T> { private Decoder dec = codec.dec(); private Type type; public DecImpl(Type type) { this.type = type; } @Override public T get(InputStream in) throws Exception { return (T) dec.charset("UTF-8").from(in).get(type); } @Override public T get(InputStream in, String charset) throws Exception { return (T) dec.charset(charset).from(in).get(type); } @Override public T get(Reader in) throws Exception { return (T) dec.from(in).get(type); } @Override public T get(CharSequence in) throws Exception { return (T) dec.charset("UTF-8").from(in.toString()).get(type); } } @Override public Enc encoder(Object source) throws Exception { return new EncImpl(source); } @Override public <T> Dec<T> decoder(Class<T> type) throws Exception { return new DecImpl<T>(type); } @Override public <T> Dec<T> decoder(TypeReference<T> type) throws Exception { return new DecImpl<T>(type.getType()); } @Override public Dec< ? > decoder(Type type, InputStream source) throws Exception { return new DecImpl<Object>(type); } Field[] getFields(Object o) { if (o == null) return EMPTY_FIELDS; return getFields(o.getClass()); } Field[] getFields(Class< ? > c) { Field fields[] = cache.get(c); if (fields == null) { List<Field> publicFields = new ArrayList<>(); for (Field field : getClass().getFields()) { if (field.isEnumConstant() || field.isSynthetic() || Modifier.isStatic(field.getModifiers())) continue; publicFields.add(field); } Collections.sort(publicFields, new Comparator<Field>() { @Override public int compare(Field o1, Field o2) { return o1.getName().compareTo(o2.getName()); } }); cache.put(getClass(), fields = publicFields.toArray(new Field[publicFields.size()])); } return fields; } @SuppressWarnings("unchecked") @Override public <T> Comparator<T> getComparator(Class<T> dtoClass) { Field[] fields = getFields(dtoClass); if (fields.length == 0) return null; List<Comparator<T>> comparables = new ArrayList<>(); for (final Field f : fields) { if (Comparable.class.isAssignableFrom(f.getType())) { comparables.add(new Comparator<T>() { @Override public int compare(T o1, T o2) { try { Comparable<Object> oo1 = (Comparable<Object>) f.get(o1); Comparable<Object> oo2 = (Comparable<Object>) f.get(o2); return oo1.compareTo(oo2); } catch (Exception e) { // cannot happen since we only look at public fields return 0; } } }); } else { final Comparator<Object> comparator = (Comparator<Object>) getComparator(f.getType()); if (comparator != null) { comparables.add(new Comparator<T>() { @Override public int compare(T o1, T o2) { try { Object oo1 = f.get(o1); Object oo2 = f.get(o2); return comparator.compare(oo1, oo2); } catch (Exception e) { // cannot happen since we only look at public // fields return 0; } } }); } } } // // Check if we found any fields to sort on // if (comparables.isEmpty()) return null; final Comparator<T> comparators[] = comparables.toArray(new Comparator[comparables.size()]); return new Comparator<T>() { @Override public int compare(T a, T b) { try { for (Comparator<T> c : comparators) { int result = c.compare(a, b); if (result != 0) return result; } return 0; } catch (Exception e) { throw new RuntimeException(e); } } }; } Field getField(Field[] fields, String name) { int index = bsearch(fields, 0, fields.length, name); if (index < 0) return null; else return fields[index]; } int bsearch(Field[] a, int fromIndex, int toIndex, String key) { int low = fromIndex; int high = toIndex - 1; while (low <= high) { int mid = (low + high) >>> 1; Field midVal = a[mid]; int cmp = midVal.getName().compareTo(key); if (cmp < 0) low = mid + 1; else if (cmp > 0) high = mid - 1; else return mid; // key found } return -(low + 1); // key not found. } /** * Shallow copy */ @SuppressWarnings({ "unchecked", "rawtypes" }) @Override public <T> T shallowCopy(T source) throws Exception { if (!isComplex(source)) return source; Class<T> c = (Class<T>) source.getClass(); if (c.isArray()) { int l = Array.getLength(source); T dest = (T) Array.newInstance(c.getComponentType(), l); System.arraycopy(source, 0, dest, 0, l); return dest; } T dest = c.newInstance(); if (source instanceof Map) { ((Map) dest).putAll((Map) source); return dest; } if (source instanceof Collection) { ((Collection) dest).addAll((Collection) source); return dest; } for (Field field : getFields(c)) { field.set(dest, field.get(source)); } return dest; } /** * Deep copy */ public <T> T deepCopy(T source) throws Exception { return deepCopy(source, root); } @SuppressWarnings({ "unchecked", "rawtypes" }) <T> T deepCopy(T source, Link link) throws Exception { if (!isComplex(source)) return source; link.verifyCycle(source); Class<T> c = (Class<T>) source.getClass(); if (c.isArray()) { int l = Array.getLength(source); T dest = (T) Array.newInstance(c.getComponentType(), l); for (int i = 0; i < l; i++) { Object s = Array.get(source, i); Array.set(dest, i, deepCopy(s, new Link(link, i, source))); } return dest; } T dest = c.newInstance(); if (source instanceof Map) { Map<Object,Object> d = (Map<Object,Object>) dest; Map<Object,Object> s = (Map<Object,Object>) source; for (Entry< ? , ? > entry : s.entrySet()) { Link next = new Link(link, entry.getKey(), source); d.put(deepCopy(entry.getKey(), next), deepCopy(entry.getValue(), next)); } return dest; } if (source instanceof Collection) { Collection s = (Collection) source; Collection d = (Collection) dest; int i = 0; for (Object o : s) { Link next = new Link(link, i++, source); d.add(deepCopy(o, next)); } return dest; } for (Field field : getFields(c)) { Link next = new Link(link, field.getName(), source); field.set(dest, deepCopy(field.get(source), next)); } return dest; } public String[] fromPathToSegments(String path) { return fromPathToSegments(path, 0, 0); } String[] fromPathToSegments(String path, int start, int n) { if (start >= path.length()) { return new String[n]; } StringBuilder sb = new StringBuilder(); int i = start; outer: for (; i < path.length(); i++) { char c = path.charAt(i); switch (c) { case '.' : break outer; case '\\' : c = path.charAt(++i); assert c == '.' || c == '\\'; default : sb.append(c); break; } } String[] result = fromPathToSegments(path, i + 1, n + 1); result[n] = sb.toString(); return result; } @Override public String fromSegmentsToPath(String[] segments) { StringBuilder sb = new StringBuilder(); String del = ""; for (String segment : segments) { sb.append(del); for (int i = 0; i < segment.length(); i++) { char c = segment.charAt(i); switch (c) { case '\\' : case '.' : sb.append('\\'); // FALL THROUGH default : sb.append(c); break; } } del = "."; } return sb.toString(); } @Override public boolean deepEquals(Object a, Object b) { try { return diff(a,b).isEmpty(); } catch (Exception e) { throw new RuntimeException(e); } } @Override public boolean isValidDTO(Object o) throws Exception { // TODO Auto-generated method stub return false; } }