package org.deephacks.confit.internal.core.property.typesafe.impl; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.deephacks.confit.internal.core.property.typesafe.ConfigException.BugOrBroken; import org.deephacks.confit.internal.core.property.typesafe.ConfigException.NotResolved; import org.deephacks.confit.internal.core.property.typesafe.ConfigException.WrongType; import org.deephacks.confit.internal.core.property.typesafe.ConfigRenderOptions; import org.deephacks.confit.internal.core.property.typesafe.ConfigValueType; import org.deephacks.confit.internal.core.property.typesafe.ConfigObject; import org.deephacks.confit.internal.core.property.typesafe.ConfigOrigin; /** * A ConfigConcatenation represents a list of values to be concatenated (see the * spec). It only has to exist if at least one value is an unresolved * substitution, otherwise we could go ahead and collapse the list into a single * value. * * Right now this is always a list of strings and ${} references, but in the * future should support a list of ConfigList. We may also support * concatenations of objects, but ConfigDelayedMerge should be used for that * since a concat of objects really will merge, not concatenate. */ final class ConfigConcatenation extends AbstractConfigValue implements Unmergeable { final private List<AbstractConfigValue> pieces; ConfigConcatenation(ConfigOrigin origin, List<AbstractConfigValue> pieces) { super(origin); this.pieces = pieces; if (pieces.size() < 2) throw new BugOrBroken("Created concatenation with less than 2 items: " + this); boolean hadUnmergeable = false; for (AbstractConfigValue p : pieces) { if (p instanceof ConfigConcatenation) throw new BugOrBroken( "ConfigConcatenation should never be nested: " + this); if (p instanceof Unmergeable) hadUnmergeable = true; } if (!hadUnmergeable) throw new BugOrBroken( "Created concatenation without an unmergeable in it: " + this); } private NotResolved notResolved() { return new NotResolved( "need to Config#resolve(), see the API docs for Config#resolve(); substitution not resolved: " + this); } @Override public ConfigValueType valueType() { throw notResolved(); } @Override public Object unwrapped() { throw notResolved(); } @Override protected ConfigConcatenation newCopy(ConfigOrigin newOrigin) { return new ConfigConcatenation(newOrigin, pieces); } @Override protected boolean ignoresFallbacks() { // we can never ignore fallbacks because if a child ConfigReference // is self-referential we have to look lower in the merge stack // for its value. return false; } @Override public Collection<ConfigConcatenation> unmergedValues() { return Collections.singleton(this); } /** * Add left and right, or their merger, to builder. */ private static void join(ArrayList<AbstractConfigValue> builder, AbstractConfigValue origRight) { AbstractConfigValue left = builder.get(builder.size() - 1); AbstractConfigValue right = origRight; // check for an object which can be converted to a list // (this will be an object with numeric keys, like foo.0, foo.1) if (left instanceof ConfigObject && right instanceof SimpleConfigList) { left = DefaultTransformer.transform(left, ConfigValueType.LIST); } else if (left instanceof SimpleConfigList && right instanceof ConfigObject) { right = DefaultTransformer.transform(right, ConfigValueType.LIST); } // Since this depends on the type of two instances, I couldn't think // of much alternative to an instanceof chain. Visitors are sometimes // used for multiple dispatch but seems like overkill. AbstractConfigValue joined = null; if (left instanceof ConfigObject && right instanceof ConfigObject) { joined = right.withFallback(left); } else if (left instanceof SimpleConfigList && right instanceof SimpleConfigList) { joined = ((SimpleConfigList)left).concatenate((SimpleConfigList)right); } else if (left instanceof ConfigConcatenation || right instanceof ConfigConcatenation) { throw new BugOrBroken("unflattened ConfigConcatenation"); } else if (left instanceof Unmergeable || right instanceof Unmergeable) { // leave joined=null, cannot join } else { // handle primitive type or primitive type mixed with object or list String s1 = left.transformToString(); String s2 = right.transformToString(); if (s1 == null || s2 == null) { throw new WrongType(left.origin(), "Cannot concatenate object or list with a non-object-or-list, " + left + " and " + right + " are not compatible"); } else { ConfigOrigin joinedOrigin = SimpleConfigOrigin.mergeOrigins(left.origin(), right.origin()); joined = new ConfigString(joinedOrigin, s1 + s2); } } if (joined == null) { builder.add(right); } else { builder.remove(builder.size() - 1); builder.add(joined); } } static List<AbstractConfigValue> consolidate(List<AbstractConfigValue> pieces) { if (pieces.size() < 2) { return pieces; } else { List<AbstractConfigValue> flattened = new ArrayList<AbstractConfigValue>(pieces.size()); for (AbstractConfigValue v : pieces) { if (v instanceof ConfigConcatenation) { flattened.addAll(((ConfigConcatenation) v).pieces); } else { flattened.add(v); } } ArrayList<AbstractConfigValue> consolidated = new ArrayList<AbstractConfigValue>( flattened.size()); for (AbstractConfigValue v : flattened) { if (consolidated.isEmpty()) consolidated.add(v); else join(consolidated, v); } return consolidated; } } static AbstractConfigValue concatenate(List<AbstractConfigValue> pieces) { List<AbstractConfigValue> consolidated = consolidate(pieces); if (consolidated.isEmpty()) { return null; } else if (consolidated.size() == 1) { return consolidated.get(0); } else { ConfigOrigin mergedOrigin = SimpleConfigOrigin.mergeOrigins(consolidated); return new ConfigConcatenation(mergedOrigin, consolidated); } } @Override AbstractConfigValue resolveSubstitutions(ResolveContext context) throws NotPossibleToResolve { List<AbstractConfigValue> resolved = new ArrayList<AbstractConfigValue>(pieces.size()); for (AbstractConfigValue p : pieces) { // to concat into a string we have to do a full resolve, // so unrestrict the context AbstractConfigValue r = context.unrestricted().resolve(p); if (r == null) { // it was optional... omit } else { resolved.add(r); } } // now need to concat everything List<AbstractConfigValue> joined = consolidate(resolved); if (joined.size() != 1) throw new BugOrBroken( "Resolved list should always join to exactly one value, not " + joined); return joined.get(0); } @Override ResolveStatus resolveStatus() { return ResolveStatus.UNRESOLVED; } // when you graft a substitution into another object, // you have to prefix it with the location in that object // where you grafted it; but save prefixLength so // system property and env variable lookups don't lookup // broken. @Override ConfigConcatenation relativized(Path prefix) { List<AbstractConfigValue> newPieces = new ArrayList<AbstractConfigValue>(); for (AbstractConfigValue p : pieces) { newPieces.add(p.relativized(prefix)); } return new ConfigConcatenation(origin(), newPieces); } @Override protected boolean canEqual(Object other) { return other instanceof ConfigConcatenation; } @Override public boolean equals(Object other) { // note that "origin" is deliberately NOT part of equality if (other instanceof ConfigConcatenation) { return canEqual(other) && this.pieces.equals(((ConfigConcatenation) other).pieces); } else { return false; } } @Override public int hashCode() { // note that "origin" is deliberately NOT part of equality return pieces.hashCode(); } @Override protected void render(StringBuilder sb, int indent, ConfigRenderOptions options) { for (AbstractConfigValue p : pieces) { p.render(sb, indent, options); } } static List<AbstractConfigValue> valuesFromPieces(ConfigOrigin origin, List<Object> pieces) { List<AbstractConfigValue> values = new ArrayList<AbstractConfigValue>(pieces.size()); for (Object p : pieces) { if (p instanceof SubstitutionExpression) { values.add(new ConfigReference(origin, (SubstitutionExpression) p)); } else if (p instanceof String) { values.add(new ConfigString(origin, (String) p)); } else { throw new BugOrBroken("Unexpected piece " + p); } } return values; } }