Adds CollectionMerger, for merging maps
Project: http://git-wip-us.apache.org/repos/asf/brooklyn-server/repo Commit: http://git-wip-us.apache.org/repos/asf/brooklyn-server/commit/97807f24 Tree: http://git-wip-us.apache.org/repos/asf/brooklyn-server/tree/97807f24 Diff: http://git-wip-us.apache.org/repos/asf/brooklyn-server/diff/97807f24 Branch: refs/heads/master Commit: 97807f24efa49dceeaebfd71fdeb7766e2879ecd Parents: 9f7a778 Author: Aled Sage <[email protected]> Authored: Fri May 27 09:40:03 2016 +0100 Committer: Aled Sage <[email protected]> Committed: Mon Jun 6 15:10:08 2016 +0100 ---------------------------------------------------------------------- .../util/collections/CollectionMerger.java | 236 +++++++++++ .../util/collections/CollectionMergerTest.java | 409 +++++++++++++++++++ 2 files changed, 645 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/97807f24/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java ---------------------------------------------------------------------- diff --git a/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java new file mode 100644 index 0000000..c9baac0 --- /dev/null +++ b/utils/common/src/main/java/org/apache/brooklyn/util/collections/CollectionMerger.java @@ -0,0 +1,236 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.collections; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.apache.brooklyn.util.guava.Maybe; + +import com.google.common.annotations.Beta; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; + +@Beta +public class CollectionMerger { + + public static class Builder { + protected int depth = Integer.MAX_VALUE; + protected boolean mergeNestedMaps = true; + protected boolean mergeNestedLists = false; + + public Builder deep(boolean val) { + return depth(val ? Integer.MAX_VALUE : 1); + } + /** + * Depth 1 means a shallow copy - i.e. only looking one layer down (e.g. at the values within the top-level map). + * Depth 2 would mean going one-deep into the values inside the top-level map/list/set. + * + * By default, depth only applies to nested maps. One needs to set {@link #mergeNestedLists(boolean)} for + * it to do this to nested iterables. + */ + public Builder depth(int val) { + checkArgument(val > 0, "val %s must be positive", val); + this.depth = val; + return this; + } + public Builder mergeNestedMaps(boolean val) { + this.mergeNestedMaps = val; + return this; + } + public Builder mergeNestedLists(boolean val) { + this.mergeNestedLists = val; + return this; + } + public CollectionMerger build() { + return new CollectionMerger(this); + } + } + + public static Builder builder() { + return new Builder(); + } + + protected final int depth; + protected final boolean mergeNestedMaps; + protected final boolean mergeNestedLists; + + protected CollectionMerger(Builder builder) { + this.depth = builder.depth; + this.mergeNestedMaps = builder.mergeNestedMaps; + this.mergeNestedLists = builder.mergeNestedLists; + } + + public Map<?, ?> merge(Map<?, ?> map1, Map<?, ?> map2) { + checkNotNull(map1, "map1"); + checkNotNull(map2, "map2"); + return (Map<?,?>) mergeImpl(Maybe.of(map1), Maybe.of(map2), depth, new Visited()); + } + + protected Object mergeImpl(Maybe<?> val1, Maybe<?> val2, int depthRemaining, Visited visited) { + if (visited.isVisited(val1.orNull())) { + throw new IllegalStateException("Recursive self-reference, "+val1.get().getClass()+": "+val1.get()); + } + if (visited.isVisited(val2.orNull())) { + throw new IllegalStateException("Recursive self-reference, "+val2.get().getClass()+": "+val2.get()); + } + visited.recordVisit(val1.orNull()); + visited.recordVisit(val2.orNull()); + + if (depthRemaining < 0) { + throw new IllegalStateException("Invalid depth "+depthRemaining); + } + if (val2.isAbsent() || val2.isNull()) { + return (val1.isPresent() ? val1.get() : null); + } + if (val1.isAbsent()) { + return (val2.isPresent() ? val2.get() : null); + } + if (val1.isNull()) { + // An explicit null value is treated as a marker to mean "do-not-merge" + return val1.get(); + } + + if (val1.get() instanceof Map) { + Map<?,?> map1 = (Map<?, ?>) val1.get(); + if (val2.get() instanceof Map) { + return mergeMapsImpl(map1, (Map<?, ?>) val2.get(), depthRemaining, visited); + } else { + // incompatible types; not merging + return val1.get(); + } + } + if (val1.get() instanceof Iterable) { + if (!mergeNestedLists) { + return val1.get(); + } + Iterable<?> iter1 = (Iterable<?>) val1.get(); + if (val2.get() instanceof Iterable) { + return mergeIterablesImpl(iter1, (Iterable<?>) val2.get(), depthRemaining, visited); + } else { + // incompatible types; not merging + return val1.get(); + } + } + return val1.get(); + } + + private Map<?, ?> mergeMapsImpl(Map<?, ?> val1, Map<?, ?> val2, int depthRemaining, Visited visited) { + if (depthRemaining < 1) { + return val1; + } + MutableMap<Object, Object> result = MutableMap.of(); + for (Object key : Sets.union(val1.keySet(), val2.keySet())) { + Maybe<?> sub1 = val1.containsKey(key) ? Maybe.of(val1.get(key)) : Maybe.absent(); + Maybe<?> sub2 = val2.containsKey(key) ? Maybe.of(val2.get(key)) : Maybe.absent(); + result.put(key, mergeImpl(sub1, sub2, depthRemaining-1, visited)); + } + return result; + } + + private Iterable<?> mergeIterablesImpl(Iterable<?> val1, Iterable<?> val2, int depthRemaining, Visited visited) { + if (depthRemaining < 1) { + return val1; + } + if (val1 instanceof Set) { + return mergeSetsImpl((Set<?>)val1, MutableSet.copyOf(val2), depthRemaining, visited); + } else { + return mergeListsImpl(MutableList.copyOf(val1), val2, depthRemaining, visited); + } + } + + private Set<?> mergeSetsImpl(Set<?> val1, Set<?> val2, int depthRemaining, Visited visited) { + return MutableSet.builder() + .addAll(val1) + .addAll(val2) + .build(); + } + + private List<?> mergeListsImpl(List<?> val1, Iterable<?> val2, int depthRemaining, Visited visited) { + return MutableList.builder() + .addAll(val1) + .addAll(val2) + .build(); + } + + /** + * For avoiding infinite loops, we need to know which objects we have already visited. + * If we come across that object again, then want to return the same result (rather than + * re-visiting it). It is based on "same" (i.e. "=="). + */ + protected static class Visited { + private static final Set<Class<?>> TRIVIAL_CLASSES = ImmutableSet.<Class<?>>of( + Integer.class, Long.class, Boolean.class, Byte.class, Double.class, Float.class, Character.class, Short.class, + String.class, BigInteger.class, BigDecimal.class, Date.class); + + protected static class Ref { + protected final Object obj; + + protected Ref(Object obj) { + this.obj = checkNotNull(obj, "ref"); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Ref)) { + return false; + } + return ((Ref)o).obj == ((Ref)o).obj; + } + + @Override + public int hashCode() { + return System.identityHashCode(obj); + } + + @Override + public String toString() { + return "Ref["+obj+"]"; + } + } + + protected final Set<Ref> visited = Sets.newLinkedHashSet(); + + public boolean isVisited(Object o) { + if (isTrivial(o)) return false; + return visited.contains(new Ref(o)); + } + + public void recordVisit(Object o) { + if (isTrivial(o)) return; + visited.add(new Ref(o)); + } + + protected boolean isTrivial(Object o) { + if (o == null) return true; + if (o instanceof Map && ((Map)o).isEmpty()) return true; + if (o instanceof Iterable && Iterables.isEmpty(((Iterable)o))) return true; + Class<?> clazz = o.getClass(); + return clazz.isEnum() || clazz.isPrimitive() || TRIVIAL_CLASSES.contains(clazz); + } + } +} http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/97807f24/utils/common/src/test/java/org/apache/brooklyn/util/collections/CollectionMergerTest.java ---------------------------------------------------------------------- diff --git a/utils/common/src/test/java/org/apache/brooklyn/util/collections/CollectionMergerTest.java b/utils/common/src/test/java/org/apache/brooklyn/util/collections/CollectionMergerTest.java new file mode 100644 index 0000000..7ff7bba --- /dev/null +++ b/utils/common/src/test/java/org/apache/brooklyn/util/collections/CollectionMergerTest.java @@ -0,0 +1,409 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.brooklyn.util.collections; + +import static org.testng.Assert.assertEquals; + +import java.io.StringReader; +import java.util.Map; + +import org.apache.brooklyn.test.Asserts; +import org.testng.annotations.Test; +import org.yaml.snakeyaml.Yaml; + +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; + +public class CollectionMergerTest { + + @Test + public void testMergeMapsEmpty() { + Map<String, String> val1 = ImmutableMap.of(); + Map<String, Object> val2 = ImmutableMap.of(); + Map<?, ?> result = CollectionMerger.builder().build().merge(val1, val2); + + assertEquals(result, ImmutableMap.of()); + } + + @Test + public void testMergeMapsSimple() { + Map<?, ?> val1 = ImmutableMap.of("key1", "val1a", "key2", "val2a"); + Map<?, ?> val2 = ImmutableMap.of("key1", "val1b", "key3", "val3b"); + Map<?, ?> resultDeep = CollectionMerger.builder().build().merge(val1, val2); + Map<?, ?> resultShallow = CollectionMerger.builder().deep(false).build().merge(val1, val2); + + assertEquals(resultDeep, ImmutableMap.of("key1", "val1a", "key2", "val2a", "key3", "val3b")); + assertEquals(resultShallow, ImmutableMap.of("key1", "val1a", "key2", "val2a", "key3", "val3b")); + } + + @Test + public void testAvoidInfiniteLoop() { + { + Map<Object, Object> val1 = MutableMap.<Object, Object>of("key1", "val1a"); + val1.put("key2", val1); + Map<Object, Object> val2 = MutableMap.<Object, Object>of("key3", "val3a"); + try { + CollectionMerger.builder().build().merge(val1, val2); + Asserts.shouldHaveFailedPreviously(); + } catch (IllegalStateException e) { + Asserts.expectedFailureContains(e, "Recursive self-reference"); + } + } + + { + Map<Object, Object> val1 = MutableMap.<Object, Object>of("key1", "val1a"); + Map<Object, Object> val2 = MutableMap.<Object, Object>of("key3", "val3a"); + val1.put("key4", val2); + try { + CollectionMerger.builder().build().merge(val1, val2); + Asserts.shouldHaveFailedPreviously(); + } catch (IllegalStateException e) { + Asserts.expectedFailureContains(e, "Recursive self-reference"); + } + } + } + + @Test + public void testMergeMapsWithDeepSubMaps() { + String yaml1 = Joiner.on("\n").join( + "key1: val1", + "key2:", + " key2.1: val2.1a", + " key2.2:", + " key2.2.1: val2.2.1a", + " key2.2.2:", + " key2.2.2.1: val2.2.2.1a", + " key2.2.2.2:", + " key2.2.2.2.1: val2.2.2.2.1a"); + String yaml2 = Joiner.on("\n").join( + "key1: override-ignored", + "key1b: val1b", + "key2:", + " key2.1: override-ignored", + " key2.1b: val2.1b", + " key2.2:", + " key2.2.1: override-ignored", + " key2.2.1b: val2.2.1b", + " key2.2.2:", + " key2.2.2.1: override-ignored", + " key2.2.2.1b: val2.2.2.1b", + " key2.2.2.2:", + " key2.2.2.2.1: override-ignored", + " key2.2.2.2.1b: val2.2.2.2.1b"); + Map<?, ?> val1 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml1)); + Map<?, ?> val2 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml2)); + + Map<?, ?> resultDepth1 = CollectionMerger.builder().depth(1).build().merge(val1, val2); + Map<?, ?> resultDepth2 = CollectionMerger.builder().depth(2).build().merge(val1, val2); + Map<?, ?> resultDepth3 = CollectionMerger.builder().depth(3).build().merge(val1, val2); + Map<?, ?> resultDepth4 = CollectionMerger.builder().depth(4).build().merge(val1, val2); + Map<?, ?> resultDepth5 = CollectionMerger.builder().depth(5).build().merge(val1, val2); + Map<?, ?> resultShallow = CollectionMerger.builder().deep(false).build().merge(val1, val2); + Map<?, ?> resultDeep = CollectionMerger.builder().build().merge(val1, val2); + + assertEquals(resultDepth1, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:", + " key2.1: val2.1a", + " key2.2:", + " key2.2.1: val2.2.1a", + " key2.2.2:", + " key2.2.2.1: val2.2.2.1a", + " key2.2.2.2:", + " key2.2.2.2.1: val2.2.2.2.1a")))); + assertEquals(resultDepth2, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:", + " key2.1: val2.1a", + " key2.1b: val2.1b", + " key2.2:", + " key2.2.1: val2.2.1a", + " key2.2.2:", + " key2.2.2.1: val2.2.2.1a", + " key2.2.2.2:", + " key2.2.2.2.1: val2.2.2.2.1a")))); + assertEquals(resultDepth3, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:", + " key2.1: val2.1a", + " key2.1b: val2.1b", + " key2.2:", + " key2.2.1: val2.2.1a", + " key2.2.1b: val2.2.1b", + " key2.2.2:", + " key2.2.2.1: val2.2.2.1a", + " key2.2.2.2:", + " key2.2.2.2.1: val2.2.2.2.1a")))); + assertEquals(resultDepth4, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:", + " key2.1: val2.1a", + " key2.1b: val2.1b", + " key2.2:", + " key2.2.1: val2.2.1a", + " key2.2.1b: val2.2.1b", + " key2.2.2:", + " key2.2.2.1: val2.2.2.1a", + " key2.2.2.1b: val2.2.2.1b", + " key2.2.2.2:", + " key2.2.2.2.1: val2.2.2.2.1a")))); + assertEquals(resultDepth5, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:", + " key2.1: val2.1a", + " key2.1b: val2.1b", + " key2.2:", + " key2.2.1: val2.2.1a", + " key2.2.1b: val2.2.1b", + " key2.2.2:", + " key2.2.2.1: val2.2.2.1a", + " key2.2.2.1b: val2.2.2.1b", + " key2.2.2.2:", + " key2.2.2.2.1: val2.2.2.2.1a", + " key2.2.2.2.1b: val2.2.2.2.1b")))); + assertEquals(resultDeep, resultDepth5); + assertEquals(resultShallow, resultDepth1); + } + + @Test + public void testMergeMapsWithNullOverridesOther() { + // Expect "key2:" to have a null value (rather than just empty). + String yaml1 = Joiner.on("\n").join( + "key1: val1", + "key2:"); + String yaml2 = Joiner.on("\n").join( + "key1: override-ignored", + "key1b: val1b", + "key2:", + " key2.1b: val2.1b"); + Map<?, ?> val1 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml1)); + Map<?, ?> val2 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml2)); + + Map<?, ?> resultDepth1 = CollectionMerger.builder().depth(1).build().merge(val1, val2); + Map<?, ?> resultDepth2 = CollectionMerger.builder().depth(2).build().merge(val1, val2); + + assertEquals(resultDepth1, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:")))); + assertEquals(resultDepth2, resultDepth1); + } + + @Test + public void testMergeMapsWithEmptyIsMerged() { + // Expect "key2:" to have a null value (rather than just empty). + String yaml1 = Joiner.on("\n").join( + "key1: val1", + "key2: {}"); + String yaml2 = Joiner.on("\n").join( + "key1: override-ignored", + "key1b: val1b", + "key2:", + " key2.1b: val2.1b"); + Map<?, ?> val1 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml1)); + Map<?, ?> val2 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml2)); + + Map<?, ?> resultDepth1 = CollectionMerger.builder().depth(1).build().merge(val1, val2); + Map<?, ?> resultDepth2 = CollectionMerger.builder().depth(2).build().merge(val1, val2); + + assertEquals(resultDepth1, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2: {}")))); + assertEquals(resultDepth2, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1: val1", + "key1b: val1b", + "key2:", + " key2.1b: val2.1b")))); + } + + @Test + public void testMergeMapsDefaultsToOverridingSubLists() { + Map<?, ?> val1 = ImmutableMap.of("key1", ImmutableList.of("val1a")); + Map<?, ?> val2 = ImmutableMap.of("key1", ImmutableList.of("val1b")); + + Map<?, ?> resultDepth1 = CollectionMerger.builder().depth(1).build().merge(val1, val2); + Map<?, ?> resultDepth2 = CollectionMerger.builder().depth(2).build().merge(val1, val2); + + assertEquals(resultDepth1, ImmutableMap.of("key1", ImmutableList.of("val1a"))); + assertEquals(resultDepth2, resultDepth1); + } + + @Test + public void testMergeMapsWithMergingSubListsRespectsTypes() { + Map<?, ?> val1 = ImmutableMap.of("key1", ImmutableList.of("val1a")); + Map<?, ?> val2 = ImmutableMap.of("key1", ImmutableList.of("val1b")); + Map<?, ?> result = CollectionMerger.builder().mergeNestedLists(true).build().merge(val1, val2); + + assertEquals(result, ImmutableMap.of("key1", ImmutableList.of("val1a", "val1b"))); + } + + @Test + public void testMergeMapsWithMergingSubSetsRespectsTypes() { + Map<?, ?> val1 = ImmutableMap.of("key1", ImmutableSet.of("val1a")); + Map<?, ?> val2 = ImmutableMap.of("key1", ImmutableSet.of("val1b")); + Map<?, ?> result = CollectionMerger.builder().mergeNestedLists(true).build().merge(val1, val2); + + assertEquals(result, ImmutableMap.of("key1", ImmutableSet.of("val1a", "val1b"))); + } + + @Test + public void testMergeMapsWithMergingSubLists() { + String yaml1 = Joiner.on("\n").join( + "key1:", + "- key1.1", + "key2:", + " key2.1:", + " - key2.1.1", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1"); + String yaml2 = Joiner.on("\n").join( + "key1:", + "- key1.1b", + "key2:", + " key2.1:", + " - key2.1.1b", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1b", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1b"); + Map<?, ?> val1 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml1)); + Map<?, ?> val2 = (Map<?, ?>) Iterables.getOnlyElement(parseYaml(yaml2)); + + Map<?, ?> resultDepth1 = CollectionMerger.builder().mergeNestedLists(true).depth(1).build().merge(val1, val2); + Map<?, ?> resultDepth2 = CollectionMerger.builder().mergeNestedLists(true).depth(2).build().merge(val1, val2); + Map<?, ?> resultDepth3 = CollectionMerger.builder().mergeNestedLists(true).depth(3).build().merge(val1, val2); + Map<?, ?> resultDepth4 = CollectionMerger.builder().mergeNestedLists(true).depth(4).build().merge(val1, val2); + Map<?, ?> resultDepth5 = CollectionMerger.builder().mergeNestedLists(true).depth(5).build().merge(val1, val2); + Map<?, ?> resultShallow = CollectionMerger.builder().mergeNestedLists(true).deep(false).build().merge(val1, val2); + Map<?, ?> resultDeep = CollectionMerger.builder().mergeNestedLists(true).build().merge(val1, val2); + + assertEquals(resultDepth1, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1:", + "- key1.1", + "key2:", + " key2.1:", + " - key2.1.1", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1")))); + assertEquals(resultDepth2, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1:", + "- key1.1", + "- key1.1b", + "key2:", + " key2.1:", + " - key2.1.1", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1")))); + assertEquals(resultDepth3, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1:", + "- key1.1", + "- key1.1b", + "key2:", + " key2.1:", + " - key2.1.1", + " - key2.1.1b", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1")))); + assertEquals(resultDepth4, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1:", + "- key1.1", + "- key1.1b", + "key2:", + " key2.1:", + " - key2.1.1", + " - key2.1.1b", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1", + " - key3.1.1.1b", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1")))); + assertEquals(resultDepth5, Iterables.getOnlyElement(parseYaml(Joiner.on("\n").join( + "key1:", + "- key1.1", + "- key1.1b", + "key2:", + " key2.1:", + " - key2.1.1", + " - key2.1.1b", + "key3:", + " key3.1:", + " key3.1.1:", + " - key3.1.1.1", + " - key3.1.1.1b", + "key4:", + " key4.1:", + " key4.1.1:", + " key4.1.1.1:", + " - key4.1.1.1.1", + " - key4.1.1.1.1b")))); + assertEquals(resultDeep, resultDepth5); + assertEquals(resultShallow, resultDepth1); + } + + protected Iterable<?> parseYaml(String yaml) { + return new Yaml().loadAll(new StringReader(yaml)); + } +}
