/** * Copyright (C) 2011-2012 Typesafe Inc. <http://typesafe.com> */ package org.deephacks.confit.internal.core.property.typesafe.impl; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EnumMap; import java.util.Iterator; import java.util.List; import java.util.Map; import org.deephacks.confit.internal.core.property.typesafe.ConfigException; import org.deephacks.confit.internal.core.property.typesafe.ConfigOrigin; import org.deephacks.confit.internal.core.property.typesafe.impl.SerializedConfigValue.SerializedField; // it would be cleaner to have a class hierarchy for various origin types, // but was hoping this would be enough simpler to be a little messy. eh. final class SimpleConfigOrigin implements ConfigOrigin { final private String description; final private int lineNumber; final private int endLineNumber; final private OriginType originType; final private String urlOrNull; final private List<String> commentsOrNull; protected SimpleConfigOrigin(String description, int lineNumber, int endLineNumber, OriginType originType, String urlOrNull, List<String> commentsOrNull) { if (description == null) throw new ConfigException.BugOrBroken("description may not be null"); this.description = description; this.lineNumber = lineNumber; this.endLineNumber = endLineNumber; this.originType = originType; this.urlOrNull = urlOrNull; this.commentsOrNull = commentsOrNull; } static SimpleConfigOrigin newSimple(String description) { return new SimpleConfigOrigin(description, -1, -1, OriginType.GENERIC, null, null); } static SimpleConfigOrigin newFile(String filename) { String url; try { url = (new File(filename)).toURI().toURL().toExternalForm(); } catch (MalformedURLException e) { url = null; } return new SimpleConfigOrigin(filename, -1, -1, OriginType.FILE, url, null); } static SimpleConfigOrigin newURL(URL url) { String u = url.toExternalForm(); return new SimpleConfigOrigin(u, -1, -1, OriginType.URL, u, null); } static SimpleConfigOrigin newResource(String resource, URL url) { return new SimpleConfigOrigin(resource, -1, -1, OriginType.RESOURCE, url != null ? url.toExternalForm() : null, null); } static SimpleConfigOrigin newResource(String resource) { return newResource(resource, null); } SimpleConfigOrigin setLineNumber(int lineNumber) { if (lineNumber == this.lineNumber && lineNumber == this.endLineNumber) { return this; } else { return new SimpleConfigOrigin(this.description, lineNumber, lineNumber, this.originType, this.urlOrNull, this.commentsOrNull); } } SimpleConfigOrigin addURL(URL url) { return new SimpleConfigOrigin(this.description, this.lineNumber, this.endLineNumber, this.originType, url != null ? url.toExternalForm() : null, this.commentsOrNull); } SimpleConfigOrigin setComments(List<String> comments) { if (ConfigImplUtil.equalsHandlingNull(comments, this.commentsOrNull)) { return this; } else { return new SimpleConfigOrigin(this.description, this.lineNumber, this.endLineNumber, this.originType, this.urlOrNull, comments); } } @Override public String description() { // not putting the URL in here for files and resources, because people // parsing "file: line" syntax would hit the ":" in the URL. if (lineNumber < 0) { return description; } else if (endLineNumber == lineNumber) { return description + ": " + lineNumber; } else { return description + ": " + lineNumber + "-" + endLineNumber; } } @Override public boolean equals(Object other) { if (other instanceof SimpleConfigOrigin) { SimpleConfigOrigin otherOrigin = (SimpleConfigOrigin) other; return this.description.equals(otherOrigin.description) && this.lineNumber == otherOrigin.lineNumber && this.endLineNumber == otherOrigin.endLineNumber && this.originType == otherOrigin.originType && ConfigImplUtil.equalsHandlingNull(this.urlOrNull, otherOrigin.urlOrNull); } else { return false; } } @Override public int hashCode() { int h = 41 * (41 + description.hashCode()); h = 41 * (h + lineNumber); h = 41 * (h + endLineNumber); h = 41 * (h + originType.hashCode()); if (urlOrNull != null) h = 41 * (h + urlOrNull.hashCode()); return h; } @Override public String toString() { // the url is only really useful on top of description for resources if (originType == OriginType.RESOURCE && urlOrNull != null) { return "ConfigOrigin(" + description + "," + urlOrNull + ")"; } else { return "ConfigOrigin(" + description + ")"; } } @Override public String filename() { if (originType == OriginType.FILE) { return description; } else if (urlOrNull != null) { URL url; try { url = new URL(urlOrNull); } catch (MalformedURLException e) { return null; } if (url.getProtocol().equals("file")) { return url.getFile(); } else { return null; } } else { return null; } } @Override public URL url() { if (urlOrNull == null) { return null; } else { try { return new URL(urlOrNull); } catch (MalformedURLException e) { return null; } } } @Override public String resource() { if (originType == OriginType.RESOURCE) { return description; } else { return null; } } @Override public int lineNumber() { return lineNumber; } @Override public List<String> comments() { if (commentsOrNull != null) { return commentsOrNull; } else { return Collections.emptyList(); } } static final String MERGE_OF_PREFIX = "merge of "; private static SimpleConfigOrigin mergeTwo(SimpleConfigOrigin a, SimpleConfigOrigin b) { String mergedDesc; int mergedStartLine; int mergedEndLine; List<String> mergedComments; OriginType mergedType; if (a.originType == b.originType) { mergedType = a.originType; } else { mergedType = OriginType.GENERIC; } // first use the "description" field which has no line numbers // cluttering it. String aDesc = a.description; String bDesc = b.description; if (aDesc.startsWith(MERGE_OF_PREFIX)) aDesc = aDesc.substring(MERGE_OF_PREFIX.length()); if (bDesc.startsWith(MERGE_OF_PREFIX)) bDesc = bDesc.substring(MERGE_OF_PREFIX.length()); if (aDesc.equals(bDesc)) { mergedDesc = aDesc; if (a.lineNumber < 0) mergedStartLine = b.lineNumber; else if (b.lineNumber < 0) mergedStartLine = a.lineNumber; else mergedStartLine = Math.min(a.lineNumber, b.lineNumber); mergedEndLine = Math.max(a.endLineNumber, b.endLineNumber); } else { // this whole merge song-and-dance was intended to avoid this case // whenever possible, but we've lost. Now we have to lose some // structured information and cram into a string. // description() method includes line numbers, so use it instead // of description field. String aFull = a.description(); String bFull = b.description(); if (aFull.startsWith(MERGE_OF_PREFIX)) aFull = aFull.substring(MERGE_OF_PREFIX.length()); if (bFull.startsWith(MERGE_OF_PREFIX)) bFull = bFull.substring(MERGE_OF_PREFIX.length()); mergedDesc = MERGE_OF_PREFIX + aFull + "," + bFull; mergedStartLine = -1; mergedEndLine = -1; } String mergedURL; if (ConfigImplUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) { mergedURL = a.urlOrNull; } else { mergedURL = null; } if (ConfigImplUtil.equalsHandlingNull(a.commentsOrNull, b.commentsOrNull)) { mergedComments = a.commentsOrNull; } else { mergedComments = new ArrayList<String>(); if (a.commentsOrNull != null) mergedComments.addAll(a.commentsOrNull); if (b.commentsOrNull != null) mergedComments.addAll(b.commentsOrNull); } return new SimpleConfigOrigin(mergedDesc, mergedStartLine, mergedEndLine, mergedType, mergedURL, mergedComments); } private static int similarity(SimpleConfigOrigin a, SimpleConfigOrigin b) { int count = 0; if (a.originType == b.originType) count += 1; if (a.description.equals(b.description)) { count += 1; // only count these if the description field (which is the file // or resource name) also matches. if (a.lineNumber == b.lineNumber) count += 1; if (a.endLineNumber == b.endLineNumber) count += 1; if (ConfigImplUtil.equalsHandlingNull(a.urlOrNull, b.urlOrNull)) count += 1; } return count; } // this picks the best pair to merge, because the pair has the most in // common. we want to merge two lines in the same file rather than something // else with one of the lines; because two lines in the same file can be // better consolidated. private static SimpleConfigOrigin mergeThree(SimpleConfigOrigin a, SimpleConfigOrigin b, SimpleConfigOrigin c) { if (similarity(a, b) >= similarity(b, c)) { return mergeTwo(mergeTwo(a, b), c); } else { return mergeTwo(a, mergeTwo(b, c)); } } static ConfigOrigin mergeOrigins(ConfigOrigin a, ConfigOrigin b) { return mergeTwo((SimpleConfigOrigin) a, (SimpleConfigOrigin) b); } static ConfigOrigin mergeOrigins(List<? extends AbstractConfigValue> stack) { List<ConfigOrigin> origins = new ArrayList<ConfigOrigin>(stack.size()); for (AbstractConfigValue v : stack) { origins.add(v.origin()); } return mergeOrigins(origins); } static ConfigOrigin mergeOrigins(Collection<? extends ConfigOrigin> stack) { if (stack.isEmpty()) { throw new ConfigException.BugOrBroken("can't merge empty list of origins"); } else if (stack.size() == 1) { return stack.iterator().next(); } else if (stack.size() == 2) { Iterator<? extends ConfigOrigin> i = stack.iterator(); return mergeTwo((SimpleConfigOrigin) i.next(), (SimpleConfigOrigin) i.next()); } else { List<SimpleConfigOrigin> remaining = new ArrayList<SimpleConfigOrigin>(); for (ConfigOrigin o : stack) { remaining.add((SimpleConfigOrigin) o); } while (remaining.size() > 2) { SimpleConfigOrigin c = remaining.get(remaining.size() - 1); remaining.remove(remaining.size() - 1); SimpleConfigOrigin b = remaining.get(remaining.size() - 1); remaining.remove(remaining.size() - 1); SimpleConfigOrigin a = remaining.get(remaining.size() - 1); remaining.remove(remaining.size() - 1); SimpleConfigOrigin merged = mergeThree(a, b, c); remaining.add(merged); } // should be down to either 1 or 2 return mergeOrigins(remaining); } } Map<SerializedField, Object> toFields() { Map<SerializedField, Object> m = new EnumMap<SerializedField, Object>(SerializedField.class); m.put(SerializedField.ORIGIN_DESCRIPTION, description); if (lineNumber >= 0) m.put(SerializedField.ORIGIN_LINE_NUMBER, lineNumber); if (endLineNumber >= 0) m.put(SerializedField.ORIGIN_END_LINE_NUMBER, endLineNumber); m.put(SerializedField.ORIGIN_TYPE, originType.ordinal()); if (urlOrNull != null) m.put(SerializedField.ORIGIN_URL, urlOrNull); if (commentsOrNull != null) m.put(SerializedField.ORIGIN_COMMENTS, commentsOrNull); return m; } Map<SerializedField, Object> toFieldsDelta(SimpleConfigOrigin baseOrigin) { Map<SerializedField, Object> baseFields; if (baseOrigin != null) baseFields = baseOrigin.toFields(); else baseFields = Collections.<SerializedField, Object> emptyMap(); return fieldsDelta(baseFields, toFields()); } // Here we're trying to avoid serializing the same info over and over // in the common case that child objects have the same origin fields // as their parent objects. e.g. we don't need to store the source // filename with every single value. static Map<SerializedField, Object> fieldsDelta(Map<SerializedField, Object> base, Map<SerializedField, Object> child) { Map<SerializedField, Object> m = new EnumMap<SerializedField, Object>(child); for (Map.Entry<SerializedField, Object> baseEntry : base.entrySet()) { SerializedField f = baseEntry.getKey(); if (m.containsKey(f) && ConfigImplUtil.equalsHandlingNull(baseEntry.getValue(), m.get(f))) { // if field is unchanged, just remove it so we inherit m.remove(f); } else if (!m.containsKey(f)) { // if field has been removed, we have to add a deletion entry switch (f) { case ORIGIN_DESCRIPTION: throw new ConfigException.BugOrBroken("origin missing description field? " + child); case ORIGIN_LINE_NUMBER: m.put(SerializedField.ORIGIN_LINE_NUMBER, -1); break; case ORIGIN_END_LINE_NUMBER: m.put(SerializedField.ORIGIN_END_LINE_NUMBER, -1); break; case ORIGIN_TYPE: throw new ConfigException.BugOrBroken("should always be an ORIGIN_TYPE field"); case ORIGIN_URL: m.put(SerializedField.ORIGIN_NULL_URL, ""); break; case ORIGIN_COMMENTS: m.put(SerializedField.ORIGIN_NULL_COMMENTS, ""); break; case ORIGIN_NULL_URL: // FALL THRU case ORIGIN_NULL_COMMENTS: throw new ConfigException.BugOrBroken( "computing delta, base object should not contain " + f + " " + base); case END_MARKER: case ROOT_VALUE: case ROOT_WAS_CONFIG: case UNKNOWN: case VALUE_DATA: case VALUE_ORIGIN: throw new ConfigException.BugOrBroken("should not appear here: " + f); } } else { // field is in base and child, but differs, so leave it } } return m; } static SimpleConfigOrigin fromFields(Map<SerializedField, Object> m) throws IOException { String description = (String) m.get(SerializedField.ORIGIN_DESCRIPTION); Integer lineNumber = (Integer) m.get(SerializedField.ORIGIN_LINE_NUMBER); Integer endLineNumber = (Integer) m.get(SerializedField.ORIGIN_END_LINE_NUMBER); Number originTypeOrdinal = (Number) m.get(SerializedField.ORIGIN_TYPE); if (originTypeOrdinal == null) throw new IOException("Missing ORIGIN_TYPE field"); OriginType originType = OriginType.values()[originTypeOrdinal.byteValue()]; String urlOrNull = (String) m.get(SerializedField.ORIGIN_URL); @SuppressWarnings("unchecked") List<String> commentsOrNull = (List<String>) m.get(SerializedField.ORIGIN_COMMENTS); return new SimpleConfigOrigin(description, lineNumber != null ? lineNumber : -1, endLineNumber != null ? endLineNumber : -1, originType, urlOrNull, commentsOrNull); } static Map<SerializedField, Object> applyFieldsDelta(Map<SerializedField, Object> base, Map<SerializedField, Object> delta) throws IOException { Map<SerializedField, Object> m = new EnumMap<SerializedField, Object>(delta); for (Map.Entry<SerializedField, Object> baseEntry : base.entrySet()) { SerializedField f = baseEntry.getKey(); if (delta.containsKey(f)) { // delta overrides when keys are in both // "m" should already contain the right thing } else { // base has the key and delta does not. // we inherit from base unless a "NULL" key blocks. switch (f) { case ORIGIN_DESCRIPTION: m.put(f, base.get(f)); break; case ORIGIN_URL: if (delta.containsKey(SerializedField.ORIGIN_NULL_URL)) { m.remove(SerializedField.ORIGIN_NULL_URL); } else { m.put(f, base.get(f)); } break; case ORIGIN_COMMENTS: if (delta.containsKey(SerializedField.ORIGIN_NULL_COMMENTS)) { m.remove(SerializedField.ORIGIN_NULL_COMMENTS); } else { m.put(f, base.get(f)); } break; case ORIGIN_NULL_URL: // FALL THRU case ORIGIN_NULL_COMMENTS: // FALL THRU // base objects shouldn't contain these, should just // lack the field. these are only in deltas. throw new ConfigException.BugOrBroken( "applying fields, base object should not contain " + f + " " + base); case ORIGIN_END_LINE_NUMBER: // FALL THRU case ORIGIN_LINE_NUMBER: // FALL THRU case ORIGIN_TYPE: m.put(f, base.get(f)); break; case END_MARKER: case ROOT_VALUE: case ROOT_WAS_CONFIG: case UNKNOWN: case VALUE_DATA: case VALUE_ORIGIN: throw new ConfigException.BugOrBroken("should not appear here: " + f); } } } return m; } static SimpleConfigOrigin fromBase(SimpleConfigOrigin baseOrigin, Map<SerializedField, Object> delta) throws IOException { Map<SerializedField, Object> baseFields; if (baseOrigin != null) baseFields = baseOrigin.toFields(); else baseFields = Collections.<SerializedField, Object> emptyMap(); Map<SerializedField, Object> fields = applyFieldsDelta(baseFields, delta); return fromFields(fields); } }