From f31e6c6c43fd48cf9180f48fcb649864f2d56e8d Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 14 Jun 2015 23:22:49 +0800 Subject: [PATCH 01/16] Artifact groupId to com.studium to separate fork --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index cc77956b..f4850b9d 100644 --- a/pom.xml +++ b/pom.xml @@ -18,7 +18,7 @@ 4.0.0 - de.danielbechler + com.studium java-object-diff Java Object Diff 0.92.2-SNAPSHOT From 8492ce1ef698b4ce992ebab30d71a25d1888bd2c Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Wed, 10 Jun 2015 22:19:15 +0800 Subject: [PATCH 02/16] First draft (with some fixes) (cherry picked from commit 9d2215e) --- .../diff/identity/IdentityStrategyIT.groovy | 158 ++++++++++++++++++ .../diff/ObjectDifferBuilder.java | 14 +- .../diff/access/CollectionItemAccessor.java | 33 +++- .../diff/differ/CollectionDiffer.java | 98 +++++++---- .../diff/identity/EqualsIdentityStrategy.java | 17 ++ .../diff/identity/IdentityConfigurer.java | 37 ++++ .../diff/identity/IdentityService.java | 121 ++++++++++++++ .../diff/identity/IdentityStrategy.java | 20 +++ .../identity/IdentityStrategyResolver.java | 23 +++ .../CollectionItemElementSelector.java | 27 ++- .../diff/differ/CollectionDifferShould.java | 20 ++- 11 files changed, 522 insertions(+), 46 deletions(-) create mode 100644 src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyIT.groovy create mode 100644 src/main/java/de/danielbechler/diff/identity/EqualsIdentityStrategy.java create mode 100644 src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java create mode 100644 src/main/java/de/danielbechler/diff/identity/IdentityService.java create mode 100644 src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java create mode 100644 src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyIT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyIT.groovy new file mode 100644 index 00000000..8116b953 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyIT.groovy @@ -0,0 +1,158 @@ +/* + * Copyright 2015 Daniel Bechler + * + * Licensed 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 de.danielbechler.diff.identity + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.node.Visit +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.CollectionItemElementSelector +import spock.lang.Specification + +class IdentityStrategyIT extends Specification { + + List list1 = [ + new A(id: "Id1", code: "Code1"), + new A(id: "Id2", code: "Code2"), + new A(id: "Id3", code: "Code3") + ] + List list2 = [ + new A(id: "Id1", code: "Code1"), + new A(id: "Id2", code: "Code2"), + new A(id: "Id3", code: "Code3") + ] + List list2b = [ + new A(id: "Id2", code: "Code2"), + new A(id: "Id3", code: "Code3"), + new A(id: "Id1", code: "Code1") + ] + List list3 = [ + new A(id: "Id1", code: "Code1"), + new A(id: "Id2", code: "newCode"), + new A(id: "newId", code: "Code2") + ] + +// def 'Test default equals SAME'() { +// when: +// def diffNode = ObjectDifferBuilder.startBuilding() +// .build().compare(list2, list1) +// then: +// diffNode.untouched +// } +// +// def 'Test default equals SAME B'() { +// when: +// def diffNode = ObjectDifferBuilder.startBuilding() +// .build().compare(list2b, list1) +// then: +// diffNode.untouched +// } +// +// def 'Test default equals CHANGED'() { +// when: +// def diffNode = ObjectDifferBuilder.startBuilding() +// .build().compare(list3, list1) +// then: +// diffNode.changed +// diffNode.getChild(new CollectionItemElementSelector(new A(id: "Id1"))) == null +// diffNode.getChild(new CollectionItemElementSelector(new A(id: "Id2"))).changed +// diffNode.getChild(new CollectionItemElementSelector(new A(id: "newId"))).added +// diffNode.getChild(new CollectionItemElementSelector(new A(id: "Id3"))).removed +// } +// +// def 'Test field CODE equals SAME'() { +// when: +// def diffNode = ObjectDifferBuilder.startBuilding() +// .comparison().ofType(A).toUseEqualsMethodOfValueProvidedByMethod("getCode").and() +// .build().compare(list2, list1) +// then: +// diffNode.state == DiffNode.State.UNTOUCHED +// } +// +// def 'Test field CODE equals SAME B'() { +// when: +// def diffNode = ObjectDifferBuilder.startBuilding() +// .identity().ofType(A).toUse(new CodeIdentity()).and() +// .build().compare(list2b, list1) +// then: +// diffNode.state == DiffNode.State.UNTOUCHED +// } + + def 'Test field CODE equals equals CHANGED'() { + when: + def codeStrategy = new CodeIdentity(); + def diffNode = ObjectDifferBuilder.startBuilding() + .identity().ofType(ArrayList) // TODO configuration shouldn't be like this! + .toUse(codeStrategy).and() + .build().compare(list3, list1) + then: + diffNode.state == DiffNode.State.CHANGED + diffNode.getChild(new CollectionItemElementSelector(new A(code: "Code1"), codeStrategy)) == null + diffNode.getChild(new CollectionItemElementSelector(new A(code: "newCode"), codeStrategy)).added + diffNode.getChild(new CollectionItemElementSelector(new A(code: "Code2"), codeStrategy)).changed + diffNode.getChild(new CollectionItemElementSelector(new A(code: "Code3"), codeStrategy)).removed + } + + private void print(final DiffNode diffNode, final Object working, + final Object base) { + diffNode.visit(new DiffNode.Visitor() { + @Override + void node(final DiffNode node, final Visit visit) { + System.out.println("" + node.getPath() + " " + node.getState() + " " + + node.canonicalGet(base) + " => " + node.canonicalGet(working)) + } + }) + } + + public static class A { + String id; + String code; + + String getCode() { + return code + } + + @Override + boolean equals(final o) { + if (this.is(o)) return true + if (!(o instanceof A)) return false + + A a = (A) o + + if (!Objects.equals(id, a.id)) return false + + return true + } + + @Override + int hashCode() { + return (id != null ? id.hashCode() : 0) + } + } + + public static class CodeIdentity implements IdentityStrategy { + @Override + boolean equals(final Object _this, final Object o) { + return Objects.equals(((A) _this).getCode(), ((A) o).getCode()); + } + + @Override + int hashCode(final Object _this) { + return Objects.hashCode(((A) _this).getCode()); + } + } +} diff --git a/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java b/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java index 1dd5f369..5a411bbd 100644 --- a/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java +++ b/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java @@ -34,6 +34,8 @@ import de.danielbechler.diff.differ.PrimitiveDiffer; import de.danielbechler.diff.filtering.FilteringConfigurer; import de.danielbechler.diff.filtering.ReturnableNodeService; +import de.danielbechler.diff.identity.IdentityConfigurer; +import de.danielbechler.diff.identity.IdentityService; import de.danielbechler.diff.inclusion.InclusionConfigurer; import de.danielbechler.diff.inclusion.InclusionService; import de.danielbechler.diff.introspection.IntrospectionConfigurer; @@ -57,6 +59,7 @@ public class ObjectDifferBuilder private final CategoryService categoryService = new CategoryService(this); private final InclusionService inclusionService = new InclusionService(categoryService, this); private final ComparisonService comparisonService = new ComparisonService(this); + private final IdentityService identityService = new IdentityService(this); private final ReturnableNodeService returnableNodeService = new ReturnableNodeService(this); private final CircularReferenceService circularReferenceService = new CircularReferenceService(this); private final DifferConfigurer differConfigurer = new DifferConfigurerImpl(); @@ -83,7 +86,7 @@ public ObjectDiffer build() returnableNodeService, introspectionService); differProvider.push(new BeanDiffer(differDispatcher, introspectionService, returnableNodeService, comparisonService, introspectionService)); - differProvider.push(new CollectionDiffer(differDispatcher, comparisonService)); + differProvider.push(new CollectionDiffer(differDispatcher, comparisonService, identityService)); differProvider.push(new MapDiffer(differDispatcher, comparisonService)); differProvider.push(new PrimitiveDiffer(comparisonService)); for (final DifferFactory differFactory : differFactories) @@ -140,6 +143,15 @@ public ComparisonConfigurer comparison() return comparisonService; } + /** + * Allows to configure the way objects identities are established when comparing collections by + * CollectionDiffer. + */ + public IdentityConfigurer identity() + { + return identityService; + } + /** * Allows to assign custom categories (or tags) to entire types or selected elements and properties. */ diff --git a/src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java b/src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java index b20dd87b..62fb3aa2 100644 --- a/src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java +++ b/src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java @@ -16,10 +16,13 @@ package de.danielbechler.diff.access; +import java.util.Collection; + +import de.danielbechler.diff.identity.IdentityService; +import de.danielbechler.diff.identity.IdentityStrategy; import de.danielbechler.diff.selector.CollectionItemElementSelector; import de.danielbechler.diff.selector.ElementSelector; - -import java.util.Collection; +import de.danielbechler.util.Assert; /** * @author Daniel Bechler @@ -27,10 +30,31 @@ public class CollectionItemAccessor implements TypeAwareAccessor, Accessor { private final Object referenceItem; + private final IdentityStrategy identityStrategy; + /** + * Default implementation uses IdentityService.EQUALS_IDENTITY_STRATEGY. + * + * @param referenceItem + */ public CollectionItemAccessor(final Object referenceItem) + { + this.referenceItem = referenceItem; + this.identityStrategy = IdentityService.EQUALS_IDENTITY_STRATEGY; + } + + /** + * Allows for custom IdentityStrategy. + * + * @param referenceItem + * @param identityStrategy + */ + public CollectionItemAccessor(final Object referenceItem, + final IdentityStrategy identityStrategy) { this.referenceItem = referenceItem; + Assert.notNull(identityStrategy, "identityStrategy"); + this.identityStrategy = identityStrategy; } @SuppressWarnings("unchecked") @@ -49,7 +73,8 @@ else if (object instanceof Collection) public ElementSelector getElementSelector() { - return new CollectionItemElementSelector(referenceItem); + return new CollectionItemElementSelector(referenceItem, + identityStrategy); } public void set(final Object target, final Object value) @@ -76,7 +101,7 @@ public Object get(final Object target) } for (final Object item : targetCollection) { - if (item != null && item.equals(referenceItem)) + if (item != null && identityStrategy.equals(item, referenceItem)) { return item; } diff --git a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java index 89c7a8a0..6273030e 100644 --- a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java +++ b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java @@ -16,18 +16,22 @@ package de.danielbechler.diff.differ; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; + import de.danielbechler.diff.access.Accessor; import de.danielbechler.diff.access.CollectionItemAccessor; import de.danielbechler.diff.access.Instances; import de.danielbechler.diff.comparison.ComparisonStrategy; import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategyResolver; import de.danielbechler.diff.node.DiffNode; import de.danielbechler.util.Assert; import de.danielbechler.util.Collections; -import java.util.ArrayList; -import java.util.Collection; - /** * Used to find differences between {@link Collection Collections}. * @@ -37,15 +41,20 @@ public final class CollectionDiffer implements Differ { private final DifferDispatcher differDispatcher; private final ComparisonStrategyResolver comparisonStrategyResolver; + private final IdentityStrategyResolver identityStrategyResolver; public CollectionDiffer(final DifferDispatcher differDispatcher, - final ComparisonStrategyResolver comparisonStrategyResolver) + final ComparisonStrategyResolver comparisonStrategyResolver, + final IdentityStrategyResolver identityStrategyResolver) { Assert.notNull(differDispatcher, "differDispatcher"); this.differDispatcher = differDispatcher; Assert.notNull(comparisonStrategyResolver, "comparisonStrategyResolver"); this.comparisonStrategyResolver = comparisonStrategyResolver; + + Assert.notNull(identityStrategyResolver, "identityStrategyResolver"); + this.identityStrategyResolver = identityStrategyResolver; } private static void compareUsingComparisonStrategy(final DiffNode collectionNode, @@ -62,29 +71,6 @@ private static DiffNode newNode(final DiffNode parentNode, final Instances colle return new DiffNode(parentNode, accessor, type); } - private static Collection addedItemsOf(final Instances instances) - { - final Collection working = instances.getWorking(Collection.class); - final Collection base = instances.getBase(Collection.class); - return Collections.filteredCopyOf(working, base); - } - - private static Collection removedItemsOf(final Instances instances) - { - final Collection working = instances.getWorking(Collection.class); - final Collection base = instances.getBase(Collection.class); - return Collections.filteredCopyOf(base, working); - } - - private static Iterable knownItemsOf(final Instances instances) - { - final Collection working = instances.getWorking(Collection.class); - final Collection changed = new ArrayList(working); - changed.removeAll(addedItemsOf(instances)); - changed.removeAll(removedItemsOf(instances)); - return changed; - } - public boolean accepts(final Class type) { return Collection.class.isAssignableFrom(type); @@ -93,16 +79,17 @@ public boolean accepts(final Class type) public final DiffNode compare(final DiffNode parentNode, final Instances collectionInstances) { final DiffNode collectionNode = newNode(parentNode, collectionInstances); + final IdentityStrategy identityStrategy = identityStrategyResolver.resolveIdentityStrategy(collectionNode); if (collectionInstances.hasBeenAdded()) { final Collection addedItems = collectionInstances.getWorking(Collection.class); - compareItems(collectionNode, collectionInstances, addedItems); + compareItems(collectionNode, collectionInstances, addedItems, identityStrategy); collectionNode.setState(DiffNode.State.ADDED); } else if (collectionInstances.hasBeenRemoved()) { final Collection removedItems = collectionInstances.getBase(Collection.class); - compareItems(collectionNode, collectionInstances, removedItems); + compareItems(collectionNode, collectionInstances, removedItems, identityStrategy); collectionNode.setState(DiffNode.State.REMOVED); } else if (collectionInstances.areSame()) @@ -114,7 +101,8 @@ else if (collectionInstances.areSame()) final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(collectionNode); if (comparisonStrategy == null) { - compareInternally(collectionNode, collectionInstances); + compareInternally(collectionNode, collectionInstances, + identityStrategy); } else { @@ -124,20 +112,58 @@ else if (collectionInstances.areSame()) return collectionNode; } - private void compareInternally(final DiffNode collectionNode, final Instances collectionInstances) + private void compareInternally(final DiffNode collectionNode, + final Instances collectionInstances, + final IdentityStrategy identityStrategy) { - compareItems(collectionNode, collectionInstances, addedItemsOf(collectionInstances)); - compareItems(collectionNode, collectionInstances, removedItemsOf(collectionInstances)); - compareItems(collectionNode, collectionInstances, knownItemsOf(collectionInstances)); + final Collection working = collectionInstances.getWorking(Collection.class); + final Collection base = collectionInstances.getBase(Collection.class); + + final Collection added = new LinkedList(working); + final Collection removed = new LinkedList(base); + final Collection known = new LinkedList(base); + + remove(added, base, identityStrategy); + remove(removed, working, identityStrategy); + remove(known, added, identityStrategy); + remove(known, removed, identityStrategy); + + // TODO I am not sure why these are separate exactly? (NagyGa1) + compareItems(collectionNode, collectionInstances, added, identityStrategy); + compareItems(collectionNode, collectionInstances, removed, identityStrategy); + compareItems(collectionNode, collectionInstances, known, identityStrategy); + } + + private void remove(final Collection from, final Collection these, final IdentityStrategy identityStrategy) { + final Iterator iterator = from.iterator(); + while(iterator.hasNext()) { + final Object _this = iterator.next(); + if(contains(these, _this, identityStrategy)) { + iterator.remove(); + } + } + } + + private boolean contains(final Collection these, + final Object _this, + final IdentityStrategy identityStrategy) { + for (Object o : these) { + if(identityStrategy.equals(_this, o)) { + return true; + } + } + + return false; } private void compareItems(final DiffNode collectionNode, final Instances collectionInstances, - final Iterable items) + final Iterable items, + final IdentityStrategy identityStrategy) { for (final Object item : items) { - final Accessor itemAccessor = new CollectionItemAccessor(item); + final Accessor itemAccessor = new CollectionItemAccessor(item, identityStrategy); differDispatcher.dispatch(collectionNode, collectionInstances, itemAccessor); } } diff --git a/src/main/java/de/danielbechler/diff/identity/EqualsIdentityStrategy.java b/src/main/java/de/danielbechler/diff/identity/EqualsIdentityStrategy.java new file mode 100644 index 00000000..58bac10c --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/EqualsIdentityStrategy.java @@ -0,0 +1,17 @@ +package de.danielbechler.diff.identity; + +import de.danielbechler.util.Objects; + +/** + * Default implementation that uses Object.equals. + */ +public class EqualsIdentityStrategy implements IdentityStrategy { + + public boolean equals(final Object _this, final Object o) { + return Objects.isEqual(_this, o); + } + + public int hashCode(final Object _this) { + return _this.hashCode(); + } +} diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java new file mode 100644 index 00000000..bfda322e --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 Daniel Bechler + * + * Licensed 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 de.danielbechler.diff.identity; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.path.NodePath; + +/** + * Allows to configure the way objects identities are established when comparing + * collections by CollectionDiffer. + */ +public interface IdentityConfigurer { + Of ofNode(NodePath nodePath); + + Of ofType(Class type); + + ObjectDifferBuilder and(); + + public interface Of { + IdentityConfigurer toUse(IdentityStrategy identityStrategy); + } + +} diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java new file mode 100644 index 00000000..a116e4f7 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -0,0 +1,121 @@ +/* + * Copyright 2014 Daniel Bechler + * + * Licensed 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 de.danielbechler.diff.identity; + +import java.util.HashMap; +import java.util.Map; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.comparison.ComparableComparisonStrategy; +import de.danielbechler.diff.comparison.ComparisonConfigurer; +import de.danielbechler.diff.comparison.ComparisonStrategy; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.comparison.EqualsOnlyComparisonStrategy; +import de.danielbechler.diff.comparison.ObjectDiffPropertyComparisonStrategyResolver; +import de.danielbechler.diff.comparison.PrimitiveDefaultValueMode; +import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver; +import de.danielbechler.diff.introspection.ObjectDiffEqualsOnlyType; +import de.danielbechler.diff.introspection.ObjectDiffProperty; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.diff.path.NodePathValueHolder; +import de.danielbechler.util.Classes; + +/** + * Resolves identity strategies, if none specified use EqualsIdentityStrategy(). + */ +public class IdentityService implements IdentityConfigurer, IdentityStrategyResolver +{ + public static final IdentityStrategy EQUALS_IDENTITY_STRATEGY = new EqualsIdentityStrategy(); + + private final NodePathValueHolder nodePathStrategies = NodePathValueHolder.of(IdentityStrategy.class); + private final Map, IdentityStrategy> typeStrategyMap = new HashMap, IdentityStrategy>(); + private final ObjectDifferBuilder objectDifferBuilder; + + public IdentityService(final ObjectDifferBuilder objectDifferBuilder) + { + this.objectDifferBuilder = objectDifferBuilder; + } + + public IdentityStrategy resolveIdentityStrategy(final DiffNode node) + { + final IdentityStrategy identityStrategy = nodePathStrategies.valueForNodePath(node.getPath()); + if (identityStrategy != null) + { + return identityStrategy; + } + + final Class valueType = node.getValueType(); + if (typeStrategyMap.containsKey(valueType)) + { + return typeStrategyMap.get(valueType); + } + + return EQUALS_IDENTITY_STRATEGY; + } + + public Of ofNode(final NodePath nodePath) + { + return new OfNodePath(nodePath); + } + + public Of ofType(final Class type) + { + return new OfType(type); + } + + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } + + private abstract static class AbstractOf implements Of + { + } + + private class OfType extends AbstractOf + { + private final Class type; + + public OfType(final Class type) + { + this.type = type; + } + + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) + { + typeStrategyMap.put(type, identityStrategy); + return IdentityService.this; + } + } + + private class OfNodePath extends AbstractOf + { + private final NodePath nodePath; + + public OfNodePath(final NodePath nodePath) + { + this.nodePath = nodePath; + } + + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) + { + nodePathStrategies.put(nodePath, identityStrategy); + return IdentityService.this; + } + } +} diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java new file mode 100644 index 00000000..aa02da9f --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java @@ -0,0 +1,20 @@ +package de.danielbechler.diff.identity; + +public interface IdentityStrategy { + + /** + * + * @param _this never null + * @param o + * @return + */ + public boolean equals(final Object _this, final Object o); + + /** + * + * @param _this never null + * @return + */ + public int hashCode(final Object _this); + +} diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java new file mode 100644 index 00000000..751ab5fa --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java @@ -0,0 +1,23 @@ +/* + * Copyright 2014 Daniel Bechler + * + * Licensed 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 de.danielbechler.diff.identity; + +import de.danielbechler.diff.node.DiffNode; + +public interface IdentityStrategyResolver { + IdentityStrategy resolveIdentityStrategy(DiffNode node); +} diff --git a/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java b/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java index 8aa090c6..23447b17 100644 --- a/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java +++ b/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java @@ -16,6 +16,9 @@ package de.danielbechler.diff.selector; +import de.danielbechler.diff.identity.IdentityService; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.util.Assert; import de.danielbechler.util.Strings; /** @@ -24,10 +27,30 @@ public final class CollectionItemElementSelector extends ElementSelector { private final Object item; + private final IdentityStrategy identityStrategy; + /** + * Default implementation uses IdentityService.EQUALS_IDENTITY_STRATEGY. + * + * @param item + */ public CollectionItemElementSelector(final Object item) { this.item = item; + this.identityStrategy = IdentityService.EQUALS_IDENTITY_STRATEGY; + } + + /** + * Allows for custom IdentityStrategy. + * + * @param item + * @param identityStrategy + */ + public CollectionItemElementSelector(final Object item, final IdentityStrategy identityStrategy) + { + this.item = item; + Assert.notNull(identityStrategy, "identityStrategy"); + this.identityStrategy = identityStrategy; } /** @@ -60,7 +83,7 @@ public boolean equals(final Object o) final CollectionItemElementSelector that = (CollectionItemElementSelector) o; - if (item != null ? !item.equals(that.item) : that.item != null) + if (item != null ? !identityStrategy.equals(item, that.item) : that.item != null) { return false; } @@ -71,6 +94,6 @@ public boolean equals(final Object o) @Override public int hashCode() { - return 31; + return item != null ? identityStrategy.hashCode(item) : 0; } } diff --git a/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java b/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java index b37a4593..b5bce45d 100644 --- a/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java +++ b/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java @@ -20,6 +20,9 @@ import de.danielbechler.diff.access.RootAccessor; import de.danielbechler.diff.comparison.ComparisonStrategy; import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.identity.IdentityService; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategyResolver; import de.danielbechler.diff.node.DiffNode; import de.danielbechler.diff.path.NodePath; import org.mockito.Mock; @@ -52,6 +55,11 @@ public class CollectionDifferShould { @Mock private ComparisonStrategyResolver comparisonStrategyResolver; + private IdentityStrategyResolver identityStrategyResolver = new IdentityStrategyResolver() { + public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { + return IdentityService.EQUALS_IDENTITY_STRATEGY; + } + }; @Mock private ComparisonStrategy comparisonStrategy; @Mock @@ -68,7 +76,7 @@ public class CollectionDifferShould public void setUp() throws Exception { initMocks(this); - collectionDiffer = new CollectionDiffer(differDispatcher, comparisonStrategyResolver); + collectionDiffer = new CollectionDiffer(differDispatcher, comparisonStrategyResolver, identityStrategyResolver); baseCollection = new HashSet(); workingCollection = new HashSet(); when(instances.getSourceAccessor()).thenReturn(RootAccessor.getInstance()); @@ -102,13 +110,19 @@ public void not_accept_non_collection_types(final Class type) @Test(expectedExceptions = IllegalArgumentException.class) public void fail_if_constructed_without_DifferDispatcher() { - new CollectionDiffer(null, comparisonStrategyResolver); + new CollectionDiffer(null, comparisonStrategyResolver, identityStrategyResolver); } @Test(expectedExceptions = IllegalArgumentException.class) public void fail_if_constructed_without_ComparisonStrategyResolver() { - new CollectionDiffer(differDispatcher, null); + new CollectionDiffer(differDispatcher, null, identityStrategyResolver); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void fail_if_constructed_without_IdentityStrategyResolver() + { + new CollectionDiffer(differDispatcher, comparisonStrategyResolver, null); } @Test From f029d4389b1f798670699e302d1d11465b1b68a0 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Thu, 11 Jun 2015 15:27:24 +0800 Subject: [PATCH 03/16] Implemented ofTypeAndProperty(final Class type, final String... propertyNames) configuration (cherry picked from commit c687807) --- .../identity/IdentityStrategyConfigIT.groovy | 215 ++++++++++++++++++ .../diff/identity/IdentityConfigurer.java | 2 + .../diff/identity/IdentityService.java | 43 +++- .../diff/identity/TypePropertyResolver.java | 118 ++++++++++ 4 files changed, 366 insertions(+), 12 deletions(-) create mode 100644 src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy create mode 100644 src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy new file mode 100644 index 00000000..487710e8 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy @@ -0,0 +1,215 @@ +/* + * Copyright 2015 Daniel Bechler + * + * Licensed 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 de.danielbechler.diff.identity + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.node.Visit +import de.danielbechler.diff.selector.CollectionItemElementSelector +import de.danielbechler.diff.selector.MapKeyElementSelector +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import spock.lang.Specification + +class IdentityStrategyConfigIT extends Specification { + + def working = new Container( + productMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]) + ], + otherMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]) + ] + ) + + def base = new Container( + productMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID3", code: "PVC1"), + new ProductVersion(id: "ID4", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID3", code: "PVC1"), + new ProductVersion(id: "ID4", code: "PVC2") + ]) + ], + otherMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]) + ] + ) + + def 'Without IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes all changed" + // print(node, working, base) + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are ADDED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1Selector).added + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV2Selector).added + and: "ID3 and ID4 are REMOVED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV3Selector).removed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV4Selector).removed + } + + def 'PropertyOfType configuration WITH IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .identity().ofTypeAndProperty(Product.class, "productVersions").toUse(codeIdentity).and() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes" + // print(node, working, base) + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are CHANGED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + and: "id changed, code untouched" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("id").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("code").untouched + } + + private void print(final DiffNode diffNode, final Object working, + final Object base) { + diffNode.visit(new DiffNode.Visitor() { + @Override + void node(final DiffNode node, final Visit visit) { + System.out.println("" + node.getPath() + " " + node.getState() + // + " " + node.canonicalGet(base) + " => " + node.canonicalGet(working) + ) + } + }) + } + + + public static class Container { + Map productMap; + Map otherMap; + } + + public interface CodeId { + String getCode(); + } + + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class Product implements CodeId { + String id; + String code; + List productVersions; + List others; + } + + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class ProductVersion implements CodeId { + String id; + String code; + } + + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class OtherClass implements CodeId { + String id; + String code; + List productVersions; + } + + def codeIdentity = new IdentityStrategy() { + @Override + boolean equals(final Object _this, final Object o) { + return Objects.equals(((CodeId) _this).getCode(), ((CodeId) o).getCode()); + } + + @Override + int hashCode(final Object _this) { + return Objects.hashCode(((CodeId) _this).getCode()); + } + } + + def PV1Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID1", code: "PVC1")); + def PV2Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID2", code: "PVC2")); + def PV3Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID3")); + def PV4Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID4")); + + // need to fill code as well because that's used for the codeIdentity cases + def PV1CodeSelector = new CollectionItemElementSelector(new ProductVersion(code: "PVC1"), codeIdentity); + def PV2CodeSelector = new CollectionItemElementSelector(new ProductVersion(code: "PVC2"), codeIdentity); +} diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java index bfda322e..bae98e3e 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java @@ -28,6 +28,8 @@ public interface IdentityConfigurer { Of ofType(Class type); + Of ofTypeAndProperty(Class type, String... propertyNames); + ObjectDifferBuilder and(); public interface Of { diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index a116e4f7..50e9682a 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -16,24 +16,15 @@ package de.danielbechler.diff.identity; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; + import java.util.HashMap; import java.util.Map; import de.danielbechler.diff.ObjectDifferBuilder; -import de.danielbechler.diff.comparison.ComparableComparisonStrategy; -import de.danielbechler.diff.comparison.ComparisonConfigurer; -import de.danielbechler.diff.comparison.ComparisonStrategy; -import de.danielbechler.diff.comparison.ComparisonStrategyResolver; -import de.danielbechler.diff.comparison.EqualsOnlyComparisonStrategy; -import de.danielbechler.diff.comparison.ObjectDiffPropertyComparisonStrategyResolver; -import de.danielbechler.diff.comparison.PrimitiveDefaultValueMode; -import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver; -import de.danielbechler.diff.introspection.ObjectDiffEqualsOnlyType; -import de.danielbechler.diff.introspection.ObjectDiffProperty; import de.danielbechler.diff.node.DiffNode; import de.danielbechler.diff.path.NodePath; import de.danielbechler.diff.path.NodePathValueHolder; -import de.danielbechler.util.Classes; /** * Resolves identity strategies, if none specified use EqualsIdentityStrategy(). @@ -44,6 +35,7 @@ public class IdentityService implements IdentityConfigurer, IdentityStrategyReso private final NodePathValueHolder nodePathStrategies = NodePathValueHolder.of(IdentityStrategy.class); private final Map, IdentityStrategy> typeStrategyMap = new HashMap, IdentityStrategy>(); + private final TypePropertyResolver typePropertyResolver = new TypePropertyResolver(); private final ObjectDifferBuilder objectDifferBuilder; public IdentityService(final ObjectDifferBuilder objectDifferBuilder) @@ -53,7 +45,12 @@ public IdentityService(final ObjectDifferBuilder objectDifferBuilder) public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { - final IdentityStrategy identityStrategy = nodePathStrategies.valueForNodePath(node.getPath()); + IdentityStrategy identityStrategy = typePropertyResolver.resolve(node); + if (identityStrategy != null) + { + return identityStrategy; + } + identityStrategy = nodePathStrategies.valueForNodePath(node.getPath()); if (identityStrategy != null) { return identityStrategy; @@ -78,6 +75,10 @@ public Of ofType(final Class type) return new OfType(type); } + public Of ofTypeAndProperty(final Class type, final String... propertyNames) { + return new OfTypeAndProperty(type, propertyNames); + } + public ObjectDifferBuilder and() { return objectDifferBuilder; @@ -118,4 +119,22 @@ public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) return IdentityService.this; } } + + private class OfTypeAndProperty extends AbstractOf + { + private final Class type; + + private final String[] properties; + + public OfTypeAndProperty(final Class type, final String[] properties) { + this.type = type; + this.properties = properties; + } + + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) + { + typePropertyResolver.setStrategy(type, identityStrategy, properties); + return IdentityService.this; + } + } } diff --git a/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java b/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java new file mode 100644 index 00000000..031a17d2 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014 Daniel Bechler + * + * Licensed 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 de.danielbechler.diff.identity; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +import java.util.HashMap; +import java.util.Map; + +import de.danielbechler.diff.inclusion.Inclusion; +import de.danielbechler.diff.inclusion.InclusionResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; + +class TypePropertyResolver +{ + private final Map strategies = new HashMap(); + + public IdentityStrategy resolve(final DiffNode node) + { + if (isQualified(node)) + { + final PropertyId propertyKey = new PropertyId(node.getParentNode().getValueType(), node.getPropertyName()); + final IdentityStrategy strategy = strategies.get(propertyKey); + return strategy; + } + return null; + } + + private static boolean isQualified(final DiffNode node) + { + if (node.isPropertyAware()) + { + if (node.getParentNode() == null || node.getParentNode().getValueType() == null) + { + return false; + } + if (node.getPropertyName() == null) + { + return false; + } + return true; + } + return false; + } + + + public void setStrategy(final Class type, final IdentityStrategy identityStrategy, final String... properties) + { + for (String property : properties) { + strategies.put(new PropertyId(type, property), identityStrategy); + } + } + + private static class PropertyId + { + private final Class type; + private final String property; + + private PropertyId(final Class type, final String property) + { + Assert.notNull(type, "type"); + Assert.notNull(property, "property"); + this.type = type; + this.property = property; + } + + @Override + public int hashCode() + { + int result = type.hashCode(); + result = 31 * result + property.hashCode(); + return result; + } + + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + + final PropertyId that = (PropertyId) o; + + if (!property.equals(that.property)) + { + return false; + } + if (!type.equals(that.type)) + { + return false; + } + + return true; + } + } +} From 6284926274f07377324f13d2fca670ad32c5e0d3 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Thu, 11 Jun 2015 15:58:20 +0800 Subject: [PATCH 04/16] Implemented ofType() to compare with element class (cherry picked from commit cfe2ffa) --- .../identity/IdentityStrategyConfigIT.groovy | 26 +++++++++++++++++++ .../diff/differ/CollectionDiffer.java | 24 +++++++++++++---- .../diff/identity/IdentityService.java | 14 ++++++++++ .../identity/IdentityStrategyResolver.java | 1 + .../diff/differ/CollectionDifferShould.java | 3 +++ 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy index 487710e8..eb2f5810 100644 --- a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy @@ -146,6 +146,32 @@ class IdentityStrategyConfigIT extends Specification { .getChild(PV1CodeSelector).getChild("code").untouched } + def 'OfType configuration WITH IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .identity().ofType(ProductVersion.class).toUse(codeIdentity).and() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes" +// print(node, working, base) + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are CHANGED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + and: "id changed, code untouched" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("id").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("code").untouched + } + + private void print(final DiffNode diffNode, final Object working, final Object base) { diffNode.visit(new DiffNode.Visitor() { diff --git a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java index 6273030e..be186073 100644 --- a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java +++ b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java @@ -16,7 +16,6 @@ package de.danielbechler.diff.differ; -import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; @@ -26,11 +25,11 @@ import de.danielbechler.diff.access.Instances; import de.danielbechler.diff.comparison.ComparisonStrategy; import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.identity.IdentityService; import de.danielbechler.diff.identity.IdentityStrategy; import de.danielbechler.diff.identity.IdentityStrategyResolver; import de.danielbechler.diff.node.DiffNode; import de.danielbechler.util.Assert; -import de.danielbechler.util.Collections; /** * Used to find differences between {@link Collection Collections}. @@ -138,7 +137,12 @@ private void remove(final Collection from, final Collection these, final I final Iterator iterator = from.iterator(); while(iterator.hasNext()) { final Object _this = iterator.next(); - if(contains(these, _this, identityStrategy)) { + IdentityStrategy elementIdentityStrategy = identityStrategy; + // try with element class if was not specified before + if(elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { + elementIdentityStrategy = identityStrategyResolver.resolveByCollectionElement(_this); + } + if(contains(these, _this, elementIdentityStrategy)) { iterator.remove(); } } @@ -148,7 +152,12 @@ private boolean contains(final Collection these, final Object _this, final IdentityStrategy identityStrategy) { for (Object o : these) { - if(identityStrategy.equals(_this, o)) { + IdentityStrategy elementIdentityStrategy = identityStrategy; + // try with element class if was not specified before + if(elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { + elementIdentityStrategy = identityStrategyResolver.resolveByCollectionElement(_this); + } + if(elementIdentityStrategy.equals(_this, o)) { return true; } } @@ -163,7 +172,12 @@ private void compareItems(final DiffNode collectionNode, { for (final Object item : items) { - final Accessor itemAccessor = new CollectionItemAccessor(item, identityStrategy); + IdentityStrategy elementIdentityStrategy = identityStrategy; + // try with element class if was not specified before + if(elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { + elementIdentityStrategy = identityStrategyResolver.resolveByCollectionElement(item); + } + final Accessor itemAccessor = new CollectionItemAccessor(item, elementIdentityStrategy); differDispatcher.dispatch(collectionNode, collectionInstances, itemAccessor); } } diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index 50e9682a..8f2d8aa9 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -65,6 +65,20 @@ public IdentityStrategy resolveIdentityStrategy(final DiffNode node) return EQUALS_IDENTITY_STRATEGY; } + public IdentityStrategy resolveByCollectionElement(final Object collectionElement) + { + if(collectionElement == null) { + return EQUALS_IDENTITY_STRATEGY; + } + + final IdentityStrategy identityStrategy = typeStrategyMap.get(collectionElement.getClass()); + if(identityStrategy != null) { + return identityStrategy; + } + + return EQUALS_IDENTITY_STRATEGY; + } + public Of ofNode(final NodePath nodePath) { return new OfNodePath(nodePath); diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java index 751ab5fa..b8e18d07 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java @@ -20,4 +20,5 @@ public interface IdentityStrategyResolver { IdentityStrategy resolveIdentityStrategy(DiffNode node); + IdentityStrategy resolveByCollectionElement(Object collectionElement); } diff --git a/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java b/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java index b5bce45d..cb504a99 100644 --- a/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java +++ b/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java @@ -59,6 +59,9 @@ public class CollectionDifferShould public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { return IdentityService.EQUALS_IDENTITY_STRATEGY; } + public IdentityStrategy resolveByCollectionElement(final Object collectionElement) { + return IdentityService.EQUALS_IDENTITY_STRATEGY; + } }; @Mock private ComparisonStrategy comparisonStrategy; From 0d533c3b36fe6a96d5ba427d642a48c5745f7764 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Thu, 11 Jun 2015 16:06:16 +0800 Subject: [PATCH 05/16] Added test for NodePath based configuration (cherry picked from commit 0155f38) --- .../identity/IdentityStrategyConfigIT.groovy | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy index eb2f5810..6baadb4b 100644 --- a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy @@ -19,6 +19,7 @@ package de.danielbechler.diff.identity import de.danielbechler.diff.ObjectDifferBuilder import de.danielbechler.diff.node.DiffNode import de.danielbechler.diff.node.Visit +import de.danielbechler.diff.path.NodePath import de.danielbechler.diff.selector.CollectionItemElementSelector import de.danielbechler.diff.selector.MapKeyElementSelector import groovy.transform.EqualsAndHashCode @@ -171,6 +172,35 @@ class IdentityStrategyConfigIT extends Specification { .getChild(PV1CodeSelector).getChild("code").untouched } + def 'OfNode configuration WITH IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .identity().ofNode( + // this is not very useful without wildcards on maps and collections... + NodePath.startBuilding().propertyName("productMap").mapKey("PROD1") + .propertyName("productVersions").build() + ).toUse(codeIdentity).and() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes" +// print(node, working, base) + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are CHANGED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + and: "id changed, code untouched" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("id").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("code").untouched + } + private void print(final DiffNode diffNode, final Object working, final Object base) { From d24503a6b5534b99b5e60afed83d3129ac1ee460 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 14 Jun 2015 09:03:10 +0800 Subject: [PATCH 06/16] Reformat of feature affected files (cherry picked from commit 96ee656) --- .../identity/IdentityStrategyConfigIT.groovy | 442 +++++++++--------- .../diff/ObjectDifferBuilder.java | 140 +++--- .../diff/differ/CollectionDiffer.java | 134 +++--- .../diff/identity/IdentityService.java | 79 ++-- .../diff/identity/IdentityStrategy.java | 6 +- .../diff/identity/TypePropertyResolver.java | 60 +-- .../CollectionItemElementSelector.java | 36 +- .../diff/differ/CollectionDifferShould.java | 166 ++++--- 8 files changed, 504 insertions(+), 559 deletions(-) diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy index 6baadb4b..4a9f069f 100644 --- a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy @@ -28,244 +28,244 @@ import spock.lang.Specification class IdentityStrategyConfigIT extends Specification { - def working = new Container( - productMap: [ - "PROD1": new Product( - id: "PROD1", - code: "Code1", - productVersions: [ - new ProductVersion(id: "ID1", code: "PVC1"), - new ProductVersion(id: "ID2", code: "PVC2") - ]), - "PROD2": new Product( - id: "PROD2", - code: "Code2", - productVersions: [ - new ProductVersion(id: "ID1", code: "PVC1"), - new ProductVersion(id: "ID2", code: "PVC2") - ]) - ], - otherMap: [ - "PROD1": new Product( - id: "PROD1", - code: "Code1", - productVersions: [ - new ProductVersion(id: "ID1", code: "PVC1"), - new ProductVersion(id: "ID2", code: "PVC2") - ]), - "PROD2": new Product( - id: "PROD2", - code: "Code2", - productVersions: [ - new ProductVersion(id: "ID1", code: "PVC1"), - new ProductVersion(id: "ID2", code: "PVC2") - ]) - ] - ) + def working = new Container( + productMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]) + ], + otherMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]) + ] + ) - def base = new Container( - productMap: [ - "PROD1": new Product( - id: "PROD1", - code: "Code1", - productVersions: [ - new ProductVersion(id: "ID3", code: "PVC1"), - new ProductVersion(id: "ID4", code: "PVC2") - ]), - "PROD2": new Product( - id: "PROD2", - code: "Code2", - productVersions: [ - new ProductVersion(id: "ID3", code: "PVC1"), - new ProductVersion(id: "ID4", code: "PVC2") - ]) - ], - otherMap: [ - "PROD1": new Product( - id: "PROD1", - code: "Code1", - productVersions: [ - new ProductVersion(id: "ID1", code: "PVC1"), - new ProductVersion(id: "ID2", code: "PVC2") - ]), - "PROD2": new Product( - id: "PROD2", - code: "Code2", - productVersions: [ - new ProductVersion(id: "ID1", code: "PVC1"), - new ProductVersion(id: "ID2", code: "PVC2") - ]) - ] - ) + def base = new Container( + productMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID3", code: "PVC1"), + new ProductVersion(id: "ID4", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID3", code: "PVC1"), + new ProductVersion(id: "ID4", code: "PVC2") + ]) + ], + otherMap: [ + "PROD1": new Product( + id: "PROD1", + code: "Code1", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]), + "PROD2": new Product( + id: "PROD2", + code: "Code2", + productVersions: [ + new ProductVersion(id: "ID1", code: "PVC1"), + new ProductVersion(id: "ID2", code: "PVC2") + ]) + ] + ) - def 'Without IdentityStrategy'() { - when: - def node = ObjectDifferBuilder - .startBuilding() - .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() - .build().compare(working, base); - then: "High level nodes all changed" - // print(node, working, base) - node.getChild("otherMap").untouched - node.getChild("productMap").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed - and: "ID1 and ID2 are ADDED" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1Selector).added - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV2Selector).added - and: "ID3 and ID4 are REMOVED" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV3Selector).removed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV4Selector).removed - } + def 'Without IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes all changed" + // print(node, working, base) + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are ADDED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1Selector).added + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV2Selector).added + and: "ID3 and ID4 are REMOVED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV3Selector).removed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV4Selector).removed + } - def 'PropertyOfType configuration WITH IdentityStrategy'() { - when: - def node = ObjectDifferBuilder - .startBuilding() - .identity().ofTypeAndProperty(Product.class, "productVersions").toUse(codeIdentity).and() - .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() - .build().compare(working, base); - then: "High level nodes" - // print(node, working, base) - node.getChild("otherMap").untouched - node.getChild("productMap").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed - and: "ID1 and ID2 are CHANGED" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).changed - and: "id changed, code untouched" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).getChild("id").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).getChild("code").untouched - } + def 'PropertyOfType configuration WITH IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .identity().ofTypeAndProperty(Product.class, "productVersions").toUse(codeIdentity).and() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes" + // print(node, working, base) + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are CHANGED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + and: "id changed, code untouched" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("id").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("code").untouched + } - def 'OfType configuration WITH IdentityStrategy'() { - when: - def node = ObjectDifferBuilder - .startBuilding() - .identity().ofType(ProductVersion.class).toUse(codeIdentity).and() - .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() - .build().compare(working, base); - then: "High level nodes" + def 'OfType configuration WITH IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .identity().ofType(ProductVersion.class).toUse(codeIdentity).and() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes" // print(node, working, base) - node.getChild("otherMap").untouched - node.getChild("productMap").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed - and: "ID1 and ID2 are CHANGED" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).changed - and: "id changed, code untouched" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).getChild("id").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).getChild("code").untouched - } + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are CHANGED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + and: "id changed, code untouched" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("id").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("code").untouched + } - def 'OfNode configuration WITH IdentityStrategy'() { - when: - def node = ObjectDifferBuilder - .startBuilding() - .identity().ofNode( - // this is not very useful without wildcards on maps and collections... - NodePath.startBuilding().propertyName("productMap").mapKey("PROD1") - .propertyName("productVersions").build() - ).toUse(codeIdentity).and() - .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() - .build().compare(working, base); - then: "High level nodes" + def 'OfNode configuration WITH IdentityStrategy'() { + when: + def node = ObjectDifferBuilder + .startBuilding() + .identity().ofNode( + // this is not very useful without wildcards on maps and collections... + NodePath.startBuilding().propertyName("productMap").mapKey("PROD1") + .propertyName("productVersions").build() + ).toUse(codeIdentity).and() + .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() + .build().compare(working, base); + then: "High level nodes" // print(node, working, base) - node.getChild("otherMap").untouched - node.getChild("productMap").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed - and: "ID1 and ID2 are CHANGED" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).changed - and: "id changed, code untouched" - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).getChild("id").changed - node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") - .getChild(PV1CodeSelector).getChild("code").untouched - } + node.getChild("otherMap").untouched + node.getChild("productMap").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions").changed + and: "ID1 and ID2 are CHANGED" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).changed + and: "id changed, code untouched" + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("id").changed + node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") + .getChild(PV1CodeSelector).getChild("code").untouched + } - private void print(final DiffNode diffNode, final Object working, - final Object base) { - diffNode.visit(new DiffNode.Visitor() { - @Override - void node(final DiffNode node, final Visit visit) { - System.out.println("" + node.getPath() + " " + node.getState() - // + " " + node.canonicalGet(base) + " => " + node.canonicalGet(working) - ) - } - }) - } + private void print(final DiffNode diffNode, final Object working, + final Object base) { + diffNode.visit(new DiffNode.Visitor() { + @Override + void node(final DiffNode node, final Visit visit) { + System.out.println("" + node.getPath() + " " + node.getState() + // + " " + node.canonicalGet(base) + " => " + node.canonicalGet(working) + ) + } + }) + } - public static class Container { - Map productMap; - Map otherMap; - } + public static class Container { + Map productMap; + Map otherMap; + } - public interface CodeId { - String getCode(); - } + public interface CodeId { + String getCode(); + } - @EqualsAndHashCode(includes = ["id"]) - @ToString(includePackage = false) - public static class Product implements CodeId { - String id; - String code; - List productVersions; - List others; - } + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class Product implements CodeId { + String id; + String code; + List productVersions; + List others; + } - @EqualsAndHashCode(includes = ["id"]) - @ToString(includePackage = false) - public static class ProductVersion implements CodeId { - String id; - String code; - } + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class ProductVersion implements CodeId { + String id; + String code; + } - @EqualsAndHashCode(includes = ["id"]) - @ToString(includePackage = false) - public static class OtherClass implements CodeId { - String id; - String code; - List productVersions; - } + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class OtherClass implements CodeId { + String id; + String code; + List productVersions; + } - def codeIdentity = new IdentityStrategy() { - @Override - boolean equals(final Object _this, final Object o) { - return Objects.equals(((CodeId) _this).getCode(), ((CodeId) o).getCode()); - } + def codeIdentity = new IdentityStrategy() { + @Override + boolean equals(final Object _this, final Object o) { + return Objects.equals(((CodeId) _this).getCode(), ((CodeId) o).getCode()); + } - @Override - int hashCode(final Object _this) { - return Objects.hashCode(((CodeId) _this).getCode()); - } - } + @Override + int hashCode(final Object _this) { + return Objects.hashCode(((CodeId) _this).getCode()); + } + } - def PV1Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID1", code: "PVC1")); - def PV2Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID2", code: "PVC2")); - def PV3Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID3")); - def PV4Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID4")); + def PV1Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID1", code: "PVC1")); + def PV2Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID2", code: "PVC2")); + def PV3Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID3")); + def PV4Selector = new CollectionItemElementSelector(new ProductVersion(id: "ID4")); - // need to fill code as well because that's used for the codeIdentity cases - def PV1CodeSelector = new CollectionItemElementSelector(new ProductVersion(code: "PVC1"), codeIdentity); - def PV2CodeSelector = new CollectionItemElementSelector(new ProductVersion(code: "PVC2"), codeIdentity); + // need to fill code as well because that's used for the codeIdentity cases + def PV1CodeSelector = new CollectionItemElementSelector(new ProductVersion(code: "PVC1"), codeIdentity); + def PV2CodeSelector = new CollectionItemElementSelector(new ProductVersion(code: "PVC2"), codeIdentity); } diff --git a/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java b/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java index 5a411bbd..628c3725 100644 --- a/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java +++ b/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java @@ -16,6 +16,10 @@ package de.danielbechler.diff; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Set; + import de.danielbechler.diff.category.CategoryConfigurer; import de.danielbechler.diff.category.CategoryService; import de.danielbechler.diff.circular.CircularReferenceConfigurer; @@ -42,168 +46,152 @@ import de.danielbechler.diff.introspection.IntrospectionService; import de.danielbechler.diff.node.DiffNode; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Set; - /** - * This is the entry point of every diffing operation. It acts as a factory to get hold of an actual {@link - * ObjectDiffer} instance and exposes a configuration API to customize its behavior to - * suit your needs. + * This is the entry point of every diffing operation. It acts as a factory to + * get hold of an actual {@link ObjectDiffer} instance and exposes a + * configuration API to customize its behavior to suit your needs. * * @author Daniel Bechler */ -public class ObjectDifferBuilder -{ - private final IntrospectionService introspectionService = new IntrospectionService(this); +public class ObjectDifferBuilder { + private final IntrospectionService introspectionService = new IntrospectionService( + this); private final CategoryService categoryService = new CategoryService(this); - private final InclusionService inclusionService = new InclusionService(categoryService, this); - private final ComparisonService comparisonService = new ComparisonService(this); + private final InclusionService inclusionService = new InclusionService( + categoryService, this); + private final ComparisonService comparisonService = new ComparisonService( + this); private final IdentityService identityService = new IdentityService(this); - private final ReturnableNodeService returnableNodeService = new ReturnableNodeService(this); - private final CircularReferenceService circularReferenceService = new CircularReferenceService(this); + private final ReturnableNodeService returnableNodeService = new ReturnableNodeService( + this); + private final CircularReferenceService circularReferenceService = new CircularReferenceService( + this); private final DifferConfigurer differConfigurer = new DifferConfigurerImpl(); private final NodeQueryService nodeQueryService = new NodeQueryServiceImpl(); private final Collection differFactories = new ArrayList(); - private ObjectDifferBuilder() - { + private ObjectDifferBuilder() { } - public static ObjectDiffer buildDefault() - { + public static ObjectDiffer buildDefault() { return startBuilding().build(); } - public ObjectDiffer build() - { + public ObjectDiffer build() { final DifferProvider differProvider = new DifferProvider(); final DifferDispatcher differDispatcher = new DifferDispatcher( - differProvider, - circularReferenceService, - circularReferenceService, - inclusionService, - returnableNodeService, - introspectionService); - differProvider.push(new BeanDiffer(differDispatcher, introspectionService, returnableNodeService, comparisonService, introspectionService)); - differProvider.push(new CollectionDiffer(differDispatcher, comparisonService, identityService)); + differProvider, circularReferenceService, + circularReferenceService, inclusionService, + returnableNodeService, introspectionService); + differProvider.push(new BeanDiffer(differDispatcher, + introspectionService, returnableNodeService, comparisonService, + introspectionService)); + differProvider.push(new CollectionDiffer(differDispatcher, + comparisonService, identityService)); differProvider.push(new MapDiffer(differDispatcher, comparisonService)); differProvider.push(new PrimitiveDiffer(comparisonService)); - for (final DifferFactory differFactory : differFactories) - { - differProvider.push(differFactory.createDiffer(differDispatcher, nodeQueryService)); + for (final DifferFactory differFactory : differFactories) { + differProvider.push(differFactory.createDiffer(differDispatcher, + nodeQueryService)); } return new ObjectDiffer(differDispatcher); } - public static ObjectDifferBuilder startBuilding() - { + public static ObjectDifferBuilder startBuilding() { return new ObjectDifferBuilder(); } /** - * Allows to exclude nodes from being added to the object graph based on criteria that are only known after - * the diff for the affected node and all its children has been determined. + * Allows to exclude nodes from being added to the object graph based on + * criteria that are only known after the diff for the affected node and all + * its children has been determined. */ - public FilteringConfigurer filtering() - { + public FilteringConfigurer filtering() { return returnableNodeService; } /** - * Allows to replace the default bean introspector with a custom implementation. + * Allows to replace the default bean introspector with a custom + * implementation. */ - public IntrospectionConfigurer introspection() - { + public IntrospectionConfigurer introspection() { return introspectionService; } /** - * Allows to define how the circular reference detector compares object instances. + * Allows to define how the circular reference detector compares object + * instances. */ - public CircularReferenceConfigurer circularReferenceHandling() - { + public CircularReferenceConfigurer circularReferenceHandling() { return circularReferenceService; } /** - * Allows to in- or exclude nodes based on property name, object type, category or location in the object - * graph. + * Allows to in- or exclude nodes based on property name, object type, + * category or location in the object graph. */ - public InclusionConfigurer inclusion() - { + public InclusionConfigurer inclusion() { return inclusionService; } /** * Allows to configure the way objects are compared. */ - public ComparisonConfigurer comparison() - { + public ComparisonConfigurer comparison() { return comparisonService; } /** - * Allows to configure the way objects identities are established when comparing collections by - * CollectionDiffer. + * Allows to configure the way objects identities are established when + * comparing collections by CollectionDiffer. */ - public IdentityConfigurer identity() - { + public IdentityConfigurer identity() { return identityService; } /** - * Allows to assign custom categories (or tags) to entire types or selected elements and properties. + * Allows to assign custom categories (or tags) to entire types or selected + * elements and properties. */ - public CategoryConfigurer categories() - { + public CategoryConfigurer categories() { return categoryService; } - public DifferConfigurer differs() - { + public DifferConfigurer differs() { return differConfigurer; } - public class DifferConfigurerImpl implements DifferConfigurer - { - public ObjectDifferBuilder register(final DifferFactory differFactory) - { + public class DifferConfigurerImpl implements DifferConfigurer { + public ObjectDifferBuilder register(final DifferFactory differFactory) { differFactories.add(differFactory); return ObjectDifferBuilder.this; } } - private class NodeQueryServiceImpl implements NodeQueryService - { - public Set resolveCategories(final DiffNode node) - { + private class NodeQueryServiceImpl implements NodeQueryService { + public Set resolveCategories(final DiffNode node) { return categoryService.resolveCategories(node); } - public boolean isIntrospectable(final DiffNode node) - { + public boolean isIntrospectable(final DiffNode node) { return introspectionService.isIntrospectable(node); } - public boolean isIgnored(final DiffNode node) - { + public boolean isIgnored(final DiffNode node) { return inclusionService.isIgnored(node); } - public boolean isReturnable(final DiffNode node) - { + public boolean isReturnable(final DiffNode node) { return returnableNodeService.isReturnable(node); } - public ComparisonStrategy resolveComparisonStrategy(final DiffNode node) - { + public ComparisonStrategy resolveComparisonStrategy(final DiffNode node) { return comparisonService.resolveComparisonStrategy(node); } - public PrimitiveDefaultValueMode resolvePrimitiveDefaultValueMode(final DiffNode node) - { + public PrimitiveDefaultValueMode resolvePrimitiveDefaultValueMode( + final DiffNode node) { return comparisonService.resolvePrimitiveDefaultValueMode(node); } } diff --git a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java index be186073..2f839bd7 100644 --- a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java +++ b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java @@ -36,16 +36,14 @@ * * @author Daniel Bechler */ -public final class CollectionDiffer implements Differ -{ +public final class CollectionDiffer implements Differ { private final DifferDispatcher differDispatcher; private final ComparisonStrategyResolver comparisonStrategyResolver; private final IdentityStrategyResolver identityStrategyResolver; public CollectionDiffer(final DifferDispatcher differDispatcher, final ComparisonStrategyResolver comparisonStrategyResolver, - final IdentityStrategyResolver identityStrategyResolver) - { + final IdentityStrategyResolver identityStrategyResolver) { Assert.notNull(differDispatcher, "differDispatcher"); this.differDispatcher = differDispatcher; @@ -56,56 +54,54 @@ public CollectionDiffer(final DifferDispatcher differDispatcher, this.identityStrategyResolver = identityStrategyResolver; } - private static void compareUsingComparisonStrategy(final DiffNode collectionNode, - final Instances collectionInstances, - final ComparisonStrategy comparisonStrategy) - { - comparisonStrategy.compare(collectionNode, collectionInstances.getType(), collectionInstances.getWorking(Collection.class), collectionInstances.getBase(Collection.class)); + private static void compareUsingComparisonStrategy( + final DiffNode collectionNode, final Instances collectionInstances, + final ComparisonStrategy comparisonStrategy) { + comparisonStrategy.compare(collectionNode, + collectionInstances.getType(), + collectionInstances.getWorking(Collection.class), + collectionInstances.getBase(Collection.class)); } - private static DiffNode newNode(final DiffNode parentNode, final Instances collectionInstances) - { + private static DiffNode newNode(final DiffNode parentNode, + final Instances collectionInstances) { final Accessor accessor = collectionInstances.getSourceAccessor(); final Class type = collectionInstances.getType(); return new DiffNode(parentNode, accessor, type); } - public boolean accepts(final Class type) - { + public boolean accepts(final Class type) { return Collection.class.isAssignableFrom(type); } - public final DiffNode compare(final DiffNode parentNode, final Instances collectionInstances) - { + public final DiffNode compare(final DiffNode parentNode, + final Instances collectionInstances) { final DiffNode collectionNode = newNode(parentNode, collectionInstances); - final IdentityStrategy identityStrategy = identityStrategyResolver.resolveIdentityStrategy(collectionNode); - if (collectionInstances.hasBeenAdded()) - { - final Collection addedItems = collectionInstances.getWorking(Collection.class); - compareItems(collectionNode, collectionInstances, addedItems, identityStrategy); + final IdentityStrategy identityStrategy = identityStrategyResolver + .resolveIdentityStrategy(collectionNode); + if (collectionInstances.hasBeenAdded()) { + final Collection addedItems = collectionInstances + .getWorking(Collection.class); + compareItems(collectionNode, collectionInstances, addedItems, + identityStrategy); collectionNode.setState(DiffNode.State.ADDED); - } - else if (collectionInstances.hasBeenRemoved()) - { - final Collection removedItems = collectionInstances.getBase(Collection.class); - compareItems(collectionNode, collectionInstances, removedItems, identityStrategy); + } else if (collectionInstances.hasBeenRemoved()) { + final Collection removedItems = collectionInstances + .getBase(Collection.class); + compareItems(collectionNode, collectionInstances, removedItems, + identityStrategy); collectionNode.setState(DiffNode.State.REMOVED); - } - else if (collectionInstances.areSame()) - { + } else if (collectionInstances.areSame()) { collectionNode.setState(DiffNode.State.UNTOUCHED); - } - else - { - final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(collectionNode); - if (comparisonStrategy == null) - { + } else { + final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver + .resolveComparisonStrategy(collectionNode); + if (comparisonStrategy == null) { compareInternally(collectionNode, collectionInstances, identityStrategy); - } - else - { - compareUsingComparisonStrategy(collectionNode, collectionInstances, comparisonStrategy); + } else { + compareUsingComparisonStrategy(collectionNode, + collectionInstances, comparisonStrategy); } } return collectionNode; @@ -113,10 +109,11 @@ else if (collectionInstances.areSame()) private void compareInternally(final DiffNode collectionNode, final Instances collectionInstances, - final IdentityStrategy identityStrategy) - { - final Collection working = collectionInstances.getWorking(Collection.class); - final Collection base = collectionInstances.getBase(Collection.class); + final IdentityStrategy identityStrategy) { + final Collection working = collectionInstances + .getWorking(Collection.class); + final Collection base = collectionInstances + .getBase(Collection.class); final Collection added = new LinkedList(working); final Collection removed = new LinkedList(base); @@ -128,36 +125,41 @@ private void compareInternally(final DiffNode collectionNode, remove(known, removed, identityStrategy); // TODO I am not sure why these are separate exactly? (NagyGa1) - compareItems(collectionNode, collectionInstances, added, identityStrategy); - compareItems(collectionNode, collectionInstances, removed, identityStrategy); - compareItems(collectionNode, collectionInstances, known, identityStrategy); + compareItems(collectionNode, collectionInstances, added, + identityStrategy); + compareItems(collectionNode, collectionInstances, removed, + identityStrategy); + compareItems(collectionNode, collectionInstances, known, + identityStrategy); } - private void remove(final Collection from, final Collection these, final IdentityStrategy identityStrategy) { + private void remove(final Collection from, final Collection these, + final IdentityStrategy identityStrategy) { final Iterator iterator = from.iterator(); - while(iterator.hasNext()) { + while (iterator.hasNext()) { final Object _this = iterator.next(); IdentityStrategy elementIdentityStrategy = identityStrategy; // try with element class if was not specified before - if(elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { - elementIdentityStrategy = identityStrategyResolver.resolveByCollectionElement(_this); + if (elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { + elementIdentityStrategy = identityStrategyResolver + .resolveByCollectionElement(_this); } - if(contains(these, _this, elementIdentityStrategy)) { + if (contains(these, _this, elementIdentityStrategy)) { iterator.remove(); } } } - private boolean contains(final Collection these, - final Object _this, - final IdentityStrategy identityStrategy) { + private boolean contains(final Collection these, final Object _this, + final IdentityStrategy identityStrategy) { for (Object o : these) { IdentityStrategy elementIdentityStrategy = identityStrategy; // try with element class if was not specified before - if(elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { - elementIdentityStrategy = identityStrategyResolver.resolveByCollectionElement(_this); + if (elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { + elementIdentityStrategy = identityStrategyResolver + .resolveByCollectionElement(_this); } - if(elementIdentityStrategy.equals(_this, o)) { + if (elementIdentityStrategy.equals(_this, o)) { return true; } } @@ -166,19 +168,19 @@ private boolean contains(final Collection these, } private void compareItems(final DiffNode collectionNode, - final Instances collectionInstances, - final Iterable items, - final IdentityStrategy identityStrategy) - { - for (final Object item : items) - { + final Instances collectionInstances, final Iterable items, + final IdentityStrategy identityStrategy) { + for (final Object item : items) { IdentityStrategy elementIdentityStrategy = identityStrategy; // try with element class if was not specified before - if(elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { - elementIdentityStrategy = identityStrategyResolver.resolveByCollectionElement(item); + if (elementIdentityStrategy == IdentityService.EQUALS_IDENTITY_STRATEGY) { + elementIdentityStrategy = identityStrategyResolver + .resolveByCollectionElement(item); } - final Accessor itemAccessor = new CollectionItemAccessor(item, elementIdentityStrategy); - differDispatcher.dispatch(collectionNode, collectionInstances, itemAccessor); + final Accessor itemAccessor = new CollectionItemAccessor(item, + elementIdentityStrategy); + differDispatcher.dispatch(collectionNode, collectionInstances, + itemAccessor); } } } diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index 8f2d8aa9..f11012e5 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -16,8 +16,6 @@ package de.danielbechler.diff.identity; -import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; - import java.util.HashMap; import java.util.Map; @@ -29,113 +27,102 @@ /** * Resolves identity strategies, if none specified use EqualsIdentityStrategy(). */ -public class IdentityService implements IdentityConfigurer, IdentityStrategyResolver -{ +public class IdentityService + implements + IdentityConfigurer, + IdentityStrategyResolver { public static final IdentityStrategy EQUALS_IDENTITY_STRATEGY = new EqualsIdentityStrategy(); - private final NodePathValueHolder nodePathStrategies = NodePathValueHolder.of(IdentityStrategy.class); + private final NodePathValueHolder nodePathStrategies = NodePathValueHolder + .of(IdentityStrategy.class); private final Map, IdentityStrategy> typeStrategyMap = new HashMap, IdentityStrategy>(); private final TypePropertyResolver typePropertyResolver = new TypePropertyResolver(); private final ObjectDifferBuilder objectDifferBuilder; - public IdentityService(final ObjectDifferBuilder objectDifferBuilder) - { + public IdentityService(final ObjectDifferBuilder objectDifferBuilder) { this.objectDifferBuilder = objectDifferBuilder; } - public IdentityStrategy resolveIdentityStrategy(final DiffNode node) - { + public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { IdentityStrategy identityStrategy = typePropertyResolver.resolve(node); - if (identityStrategy != null) - { + if (identityStrategy != null) { return identityStrategy; } identityStrategy = nodePathStrategies.valueForNodePath(node.getPath()); - if (identityStrategy != null) - { + if (identityStrategy != null) { return identityStrategy; } final Class valueType = node.getValueType(); - if (typeStrategyMap.containsKey(valueType)) - { + if (typeStrategyMap.containsKey(valueType)) { return typeStrategyMap.get(valueType); } return EQUALS_IDENTITY_STRATEGY; } - public IdentityStrategy resolveByCollectionElement(final Object collectionElement) - { - if(collectionElement == null) { + public IdentityStrategy resolveByCollectionElement( + final Object collectionElement) { + if (collectionElement == null) { return EQUALS_IDENTITY_STRATEGY; } - final IdentityStrategy identityStrategy = typeStrategyMap.get(collectionElement.getClass()); - if(identityStrategy != null) { + final IdentityStrategy identityStrategy = typeStrategyMap + .get(collectionElement.getClass()); + if (identityStrategy != null) { return identityStrategy; } return EQUALS_IDENTITY_STRATEGY; } - public Of ofNode(final NodePath nodePath) - { + public Of ofNode(final NodePath nodePath) { return new OfNodePath(nodePath); } - public Of ofType(final Class type) - { + public Of ofType(final Class type) { return new OfType(type); } - public Of ofTypeAndProperty(final Class type, final String... propertyNames) { + public Of ofTypeAndProperty(final Class type, + final String... propertyNames) { return new OfTypeAndProperty(type, propertyNames); } - public ObjectDifferBuilder and() - { + public ObjectDifferBuilder and() { return objectDifferBuilder; } - private abstract static class AbstractOf implements Of - { + private abstract static class AbstractOf implements Of { } - private class OfType extends AbstractOf - { + private class OfType extends AbstractOf { private final Class type; - public OfType(final Class type) - { + public OfType(final Class type) { this.type = type; } - public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) - { + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { typeStrategyMap.put(type, identityStrategy); return IdentityService.this; } } - private class OfNodePath extends AbstractOf - { + private class OfNodePath extends AbstractOf { private final NodePath nodePath; - public OfNodePath(final NodePath nodePath) - { + public OfNodePath(final NodePath nodePath) { this.nodePath = nodePath; } - public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) - { + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { nodePathStrategies.put(nodePath, identityStrategy); return IdentityService.this; } } - private class OfTypeAndProperty extends AbstractOf - { + private class OfTypeAndProperty extends AbstractOf { private final Class type; private final String[] properties; @@ -145,9 +132,9 @@ public OfTypeAndProperty(final Class type, final String[] properties) { this.properties = properties; } - public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) - { - typePropertyResolver.setStrategy(type, identityStrategy, properties); + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { + typePropertyResolver + .setStrategy(type, identityStrategy, properties); return IdentityService.this; } } diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java index aa02da9f..cc2a7014 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java @@ -4,7 +4,8 @@ public interface IdentityStrategy { /** * - * @param _this never null + * @param _this + * never null * @param o * @return */ @@ -12,7 +13,8 @@ public interface IdentityStrategy { /** * - * @param _this never null + * @param _this + * never null * @return */ public int hashCode(final Object _this); diff --git a/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java b/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java index 031a17d2..f0b2bf32 100644 --- a/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java +++ b/src/main/java/de/danielbechler/diff/identity/TypePropertyResolver.java @@ -16,43 +16,32 @@ package de.danielbechler.diff.identity; -import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; -import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; -import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; - import java.util.HashMap; import java.util.Map; -import de.danielbechler.diff.inclusion.Inclusion; -import de.danielbechler.diff.inclusion.InclusionResolver; import de.danielbechler.diff.node.DiffNode; import de.danielbechler.util.Assert; -class TypePropertyResolver -{ +class TypePropertyResolver { private final Map strategies = new HashMap(); - public IdentityStrategy resolve(final DiffNode node) - { - if (isQualified(node)) - { - final PropertyId propertyKey = new PropertyId(node.getParentNode().getValueType(), node.getPropertyName()); + public IdentityStrategy resolve(final DiffNode node) { + if (isQualified(node)) { + final PropertyId propertyKey = new PropertyId(node.getParentNode() + .getValueType(), node.getPropertyName()); final IdentityStrategy strategy = strategies.get(propertyKey); return strategy; } return null; } - private static boolean isQualified(final DiffNode node) - { - if (node.isPropertyAware()) - { - if (node.getParentNode() == null || node.getParentNode().getValueType() == null) - { + private static boolean isQualified(final DiffNode node) { + if (node.isPropertyAware()) { + if (node.getParentNode() == null + || node.getParentNode().getValueType() == null) { return false; } - if (node.getPropertyName() == null) - { + if (node.getPropertyName() == null) { return false; } return true; @@ -60,21 +49,18 @@ private static boolean isQualified(final DiffNode node) return false; } - - public void setStrategy(final Class type, final IdentityStrategy identityStrategy, final String... properties) - { + public void setStrategy(final Class type, + final IdentityStrategy identityStrategy, final String... properties) { for (String property : properties) { strategies.put(new PropertyId(type, property), identityStrategy); } } - private static class PropertyId - { + private static class PropertyId { private final Class type; private final String property; - private PropertyId(final Class type, final String property) - { + private PropertyId(final Class type, final String property) { Assert.notNull(type, "type"); Assert.notNull(property, "property"); this.type = type; @@ -82,33 +68,27 @@ private PropertyId(final Class type, final String property) } @Override - public int hashCode() - { + public int hashCode() { int result = type.hashCode(); result = 31 * result + property.hashCode(); return result; } @Override - public boolean equals(final Object o) - { - if (this == o) - { + public boolean equals(final Object o) { + if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) - { + if (o == null || getClass() != o.getClass()) { return false; } final PropertyId that = (PropertyId) o; - if (!property.equals(that.property)) - { + if (!property.equals(that.property)) { return false; } - if (!type.equals(that.type)) - { + if (!type.equals(that.type)) { return false; } diff --git a/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java b/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java index 23447b17..4bce078c 100644 --- a/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java +++ b/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java @@ -24,8 +24,7 @@ /** * @author Daniel Bechler */ -public final class CollectionItemElementSelector extends ElementSelector -{ +public final class CollectionItemElementSelector extends ElementSelector { private final Object item; private final IdentityStrategy identityStrategy; @@ -34,8 +33,7 @@ public final class CollectionItemElementSelector extends ElementSelector * * @param item */ - public CollectionItemElementSelector(final Object item) - { + public CollectionItemElementSelector(final Object item) { this.item = item; this.identityStrategy = IdentityService.EQUALS_IDENTITY_STRATEGY; } @@ -46,45 +44,42 @@ public CollectionItemElementSelector(final Object item) * @param item * @param identityStrategy */ - public CollectionItemElementSelector(final Object item, final IdentityStrategy identityStrategy) - { + public CollectionItemElementSelector(final Object item, + final IdentityStrategy identityStrategy) { this.item = item; Assert.notNull(identityStrategy, "identityStrategy"); this.identityStrategy = identityStrategy; } /** - * @deprecated Low-level API. Don't use in production code. May be removed in future versions. + * @deprecated Low-level API. Don't use in production code. May be removed + * in future versions. */ @SuppressWarnings({"UnusedDeclaration"}) @Deprecated - public Object getItem() - { + public Object getItem() { return item; } @Override - public String toHumanReadableString() - { + public String toHumanReadableString() { return "[" + Strings.toSingleLineString(item) + "]"; } @Override - public boolean equals(final Object o) - { - if (this == o) - { + public boolean equals(final Object o) { + if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) - { + if (o == null || getClass() != o.getClass()) { return false; } final CollectionItemElementSelector that = (CollectionItemElementSelector) o; - if (item != null ? !identityStrategy.equals(item, that.item) : that.item != null) - { + if (item != null + ? !identityStrategy.equals(item, that.item) + : that.item != null) { return false; } @@ -92,8 +87,7 @@ public boolean equals(final Object o) } @Override - public int hashCode() - { + public int hashCode() { return item != null ? identityStrategy.hashCode(item) : 0; } } diff --git a/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java b/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java index cb504a99..11287d4f 100644 --- a/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java +++ b/src/test/java/de/danielbechler/diff/differ/CollectionDifferShould.java @@ -16,25 +16,6 @@ package de.danielbechler.diff.differ; -import de.danielbechler.diff.access.Instances; -import de.danielbechler.diff.access.RootAccessor; -import de.danielbechler.diff.comparison.ComparisonStrategy; -import de.danielbechler.diff.comparison.ComparisonStrategyResolver; -import de.danielbechler.diff.identity.IdentityService; -import de.danielbechler.diff.identity.IdentityStrategy; -import de.danielbechler.diff.identity.IdentityStrategyResolver; -import de.danielbechler.diff.node.DiffNode; -import de.danielbechler.diff.path.NodePath; -import org.mockito.Mock; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import java.util.Collection; -import java.util.HashSet; -import java.util.List; - import static de.danielbechler.diff.helper.NodeMatchers.collectionItemAccessor; import static de.danielbechler.diff.helper.NodeMatchers.node; import static de.danielbechler.diff.helper.NodeMatchers.state; @@ -48,18 +29,38 @@ import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; + +import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.access.RootAccessor; +import de.danielbechler.diff.comparison.ComparisonStrategy; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.identity.IdentityService; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategyResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; + /** * @author Daniel Bechler */ -public class CollectionDifferShould -{ +public class CollectionDifferShould { @Mock private ComparisonStrategyResolver comparisonStrategyResolver; private IdentityStrategyResolver identityStrategyResolver = new IdentityStrategyResolver() { public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { return IdentityService.EQUALS_IDENTITY_STRATEGY; } - public IdentityStrategy resolveByCollectionElement(final Object collectionElement) { + public IdentityStrategy resolveByCollectionElement( + final Object collectionElement) { return IdentityService.EQUALS_IDENTITY_STRATEGY; } }; @@ -76,61 +77,55 @@ public IdentityStrategy resolveByCollectionElement(final Object collectionElemen private Collection workingCollection; @BeforeMethod - public void setUp() throws Exception - { + public void setUp() throws Exception { initMocks(this); - collectionDiffer = new CollectionDiffer(differDispatcher, comparisonStrategyResolver, identityStrategyResolver); + collectionDiffer = new CollectionDiffer(differDispatcher, + comparisonStrategyResolver, identityStrategyResolver); baseCollection = new HashSet(); workingCollection = new HashSet(); - when(instances.getSourceAccessor()).thenReturn(RootAccessor.getInstance()); - when(instances.getType()).thenAnswer(new Answer() - { - public Object answer(final InvocationOnMock invocation) throws Throwable - { + when(instances.getSourceAccessor()).thenReturn( + RootAccessor.getInstance()); + when(instances.getType()).thenAnswer(new Answer() { + public Object answer(final InvocationOnMock invocation) + throws Throwable { return List.class; } }); when(instances.getBase(Collection.class)).thenReturn(baseCollection); - when(instances.getWorking(Collection.class)).thenReturn(workingCollection); + when(instances.getWorking(Collection.class)).thenReturn( + workingCollection); } @Test(dataProviderClass = DifferAcceptTypeDataProvider.class, dataProvider = "collectionTypes") - public void accept_all_collection_types(final Class type) - { - assertThat(collectionDiffer.accepts(type)) - .as("accepts(" + type.getSimpleName() + ")") - .isTrue(); + public void accept_all_collection_types(final Class type) { + assertThat(collectionDiffer.accepts(type)).as( + "accepts(" + type.getSimpleName() + ")").isTrue(); } @Test(dataProviderClass = DifferAcceptTypeDataProvider.class, dataProvider = "beanTypes") - public void not_accept_non_collection_types(final Class type) - { - assertThat(collectionDiffer.accepts(type)) - .as("accepts(" + type.getSimpleName() + ")") - .isFalse(); + public void not_accept_non_collection_types(final Class type) { + assertThat(collectionDiffer.accepts(type)).as( + "accepts(" + type.getSimpleName() + ")").isFalse(); } @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_if_constructed_without_DifferDispatcher() - { - new CollectionDiffer(null, comparisonStrategyResolver, identityStrategyResolver); + public void fail_if_constructed_without_DifferDispatcher() { + new CollectionDiffer(null, comparisonStrategyResolver, + identityStrategyResolver); } @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_if_constructed_without_ComparisonStrategyResolver() - { + public void fail_if_constructed_without_ComparisonStrategyResolver() { new CollectionDiffer(differDispatcher, null, identityStrategyResolver); } @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_if_constructed_without_IdentityStrategyResolver() - { + public void fail_if_constructed_without_IdentityStrategyResolver() { new CollectionDiffer(differDispatcher, comparisonStrategyResolver, null); } @Test - public void return_untouched_node_when_instances_are_same() - { + public void return_untouched_node_when_instances_are_same() { given_instances_are_same(); node = collectionDiffer.compare(DiffNode.ROOT, instances); @@ -139,8 +134,8 @@ public void return_untouched_node_when_instances_are_same() } @Test - public void return_added_node_when_instance_has_been_added() throws Exception - { + public void return_added_node_when_instance_has_been_added() + throws Exception { given_instance_has_been_added(); node = collectionDiffer.compare(DiffNode.ROOT, instances); @@ -149,21 +144,21 @@ public void return_added_node_when_instance_has_been_added() throws Exception } @Test - public void delegate_added_items_to_dispatcher_when_instance_has_been_added() - { + public void delegate_added_items_to_dispatcher_when_instance_has_been_added() { final String addedItem = "foo"; given_instance_has_been_added(); given_instance_has_added_item(addedItem); node = collectionDiffer.compare(DiffNode.ROOT, instances); - verify(differDispatcher).dispatch(node(NodePath.withRoot()), same(instances), collectionItemAccessor(addedItem)); + verify(differDispatcher).dispatch(node(NodePath.withRoot()), + same(instances), collectionItemAccessor(addedItem)); verifyNoMoreInteractions(differDispatcher); } @Test - public void return_removed_node_when_instance_has_been_removed() throws Exception - { + public void return_removed_node_when_instance_has_been_removed() + throws Exception { given_instance_has_been_removed(); node = collectionDiffer.compare(DiffNode.ROOT, instances); @@ -172,99 +167,96 @@ public void return_removed_node_when_instance_has_been_removed() throws Exceptio } @Test - public void delegate_removed_items_to_dispatcher_when_instance_has_been_removed() - { + public void delegate_removed_items_to_dispatcher_when_instance_has_been_removed() { final String removedItem = "foo"; given_instance_has_been_removed(); given_instance_has_removed_item(removedItem); node = collectionDiffer.compare(DiffNode.ROOT, instances); - verify(differDispatcher).dispatch(node(NodePath.withRoot()), same(instances), collectionItemAccessor(removedItem)); + verify(differDispatcher).dispatch(node(NodePath.withRoot()), + same(instances), collectionItemAccessor(removedItem)); verifyNoMoreInteractions(differDispatcher); } @Test - public void compare_using_comparison_strategy_if_available() - { + public void compare_using_comparison_strategy_if_available() { given_a_comparison_strategy_can_be_resolved(); node = collectionDiffer.compare(DiffNode.ROOT, instances); - verify(comparisonStrategy, atLeastOnce()).compare(node(NodePath.withRoot()), same(List.class), eq(workingCollection), eq(baseCollection)); + verify(comparisonStrategy, atLeastOnce()).compare( + node(NodePath.withRoot()), same(List.class), + eq(workingCollection), eq(baseCollection)); } @Test - public void delegate_added_items_to_dispatcher_when_performaing_deep_comparison() - { + public void delegate_added_items_to_dispatcher_when_performaing_deep_comparison() { final String addedItem = "added"; given_instance_has_added_item(addedItem); node = collectionDiffer.compare(DiffNode.ROOT, instances); - verify(differDispatcher).dispatch(node(NodePath.withRoot()), same(instances), collectionItemAccessor(addedItem)); + verify(differDispatcher).dispatch(node(NodePath.withRoot()), + same(instances), collectionItemAccessor(addedItem)); verifyNoMoreInteractions(differDispatcher); } @Test - public void delegate_removed_items_to_dispatcher_when_performaing_deep_comparison() - { + public void delegate_removed_items_to_dispatcher_when_performaing_deep_comparison() { final String removedItem = "removed"; given_instance_has_removed_item(removedItem); node = collectionDiffer.compare(DiffNode.ROOT, instances); - verify(differDispatcher).dispatch(node(NodePath.withRoot()), same(instances), collectionItemAccessor(removedItem)); + verify(differDispatcher).dispatch(node(NodePath.withRoot()), + same(instances), collectionItemAccessor(removedItem)); verifyNoMoreInteractions(differDispatcher); } @Test - public void delegate_known_items_to_dispatcher_when_performaing_deep_comparison() - { + public void delegate_known_items_to_dispatcher_when_performaing_deep_comparison() { final String knownItem = "known"; given_instance_has_known_item(knownItem); node = collectionDiffer.compare(DiffNode.ROOT, instances); - verify(differDispatcher).dispatch(node(NodePath.withRoot()), same(instances), collectionItemAccessor(knownItem)); + verify(differDispatcher).dispatch(node(NodePath.withRoot()), + same(instances), collectionItemAccessor(knownItem)); verifyNoMoreInteractions(differDispatcher); } - private void given_instances_are_same() - { + private void given_instances_are_same() { when(instances.areSame()).thenReturn(true); } - private void given_instance_has_been_added() - { + private void given_instance_has_been_added() { when(instances.hasBeenAdded()).thenReturn(true); } - private void given_instance_has_been_removed() - { + private void given_instance_has_been_removed() { when(instances.hasBeenRemoved()).thenReturn(true); } - private void given_instance_has_added_item(final String item) - { + private void given_instance_has_added_item(final String item) { baseCollection.remove(item); workingCollection.add(item); } - private void given_instance_has_removed_item(final String item) - { + private void given_instance_has_removed_item(final String item) { baseCollection.add(item); workingCollection.remove(item); } - private void given_instance_has_known_item(final String item) - { + private void given_instance_has_known_item(final String item) { baseCollection.add(item); workingCollection.add(item); } - private void given_a_comparison_strategy_can_be_resolved() - { - when(comparisonStrategyResolver.resolveComparisonStrategy(any(DiffNode.class))).thenReturn(comparisonStrategy); + private void given_a_comparison_strategy_can_be_resolved() { + when( + comparisonStrategyResolver + .resolveComparisonStrategy(any(DiffNode.class))) + .thenReturn(comparisonStrategy); } } From 2bf503e38c5213b8efae7bc0128c3aa53b0447a2 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 14 Jun 2015 09:10:32 +0800 Subject: [PATCH 07/16] javadocs to indicate usage only by CollectionDiffer. (cherry picked from commit 76ece87) --- .../java/de/danielbechler/diff/identity/IdentityService.java | 5 ++++- .../de/danielbechler/diff/identity/IdentityStrategy.java | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index f11012e5..ff1b5ddf 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -26,6 +26,8 @@ /** * Resolves identity strategies, if none specified use EqualsIdentityStrategy(). + * + * At the moment only used by CollectionDiffer. */ public class IdentityService implements @@ -63,7 +65,8 @@ public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { public IdentityStrategy resolveByCollectionElement( final Object collectionElement) { - if (collectionElement == null) { + if (collectionElement == null) + { return EQUALS_IDENTITY_STRATEGY; } diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java index cc2a7014..aa072777 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java @@ -1,5 +1,9 @@ package de.danielbechler.diff.identity; +/** + * Allows to configure the way objects identities are established when comparing + * collections by CollectionDiffer. + */ public interface IdentityStrategy { /** From 085d50e362e4768734d41f017224cbeedfbe457e Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 14 Jun 2015 09:11:38 +0800 Subject: [PATCH 08/16] Removed public and final from interfaces (cherry picked from commit 0c89cd4) --- .../de/danielbechler/diff/identity/IdentityConfigurer.java | 2 +- .../java/de/danielbechler/diff/identity/IdentityStrategy.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java index bae98e3e..1d6c0531 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java @@ -32,7 +32,7 @@ public interface IdentityConfigurer { ObjectDifferBuilder and(); - public interface Of { + interface Of { IdentityConfigurer toUse(IdentityStrategy identityStrategy); } diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java index aa072777..ca1f5cda 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java @@ -13,7 +13,7 @@ public interface IdentityStrategy { * @param o * @return */ - public boolean equals(final Object _this, final Object o); + boolean equals(Object _this, Object o); /** * @@ -21,6 +21,6 @@ public interface IdentityStrategy { * never null * @return */ - public int hashCode(final Object _this); + int hashCode(Object _this); } From 405402a9f8d7fdb7e79931b8f2c5c9d78ec39da7 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 14 Jun 2015 23:40:31 +0800 Subject: [PATCH 09/16] Reverted 02b7b68ceea3691d8562d9d29d7b0634b1a19810 to still use hashCodes --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a2a61ed..97a94f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ To avoid this, returning a constant hashCode seems like a small price to pay. Yes, it may have a small performance impact, but we can still optimize when that turns out to be a problem. + + **NagyGa1: This is reverted in the NagyGa1 (com.studium) fork, using hashCode as per normal.** ## 0.92.1 From 2c99861cd12207eb9192490c79da7742c77f6654 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Wed, 17 Jun 2015 09:28:31 +0800 Subject: [PATCH 10/16] To apply rules to subclasses / interface implementations (cherry picked from commit 922632f) --- .../identity/IdentityStrategyConfigIT.groovy | 8 +- .../diff/identity/IdentityConfigurer.java | 2 + .../diff/identity/IdentityService.java | 237 +++++++++--------- 3 files changed, 133 insertions(+), 114 deletions(-) diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy index 4a9f069f..1c3dfa0b 100644 --- a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyConfigIT.groovy @@ -25,6 +25,7 @@ import de.danielbechler.diff.selector.MapKeyElementSelector import groovy.transform.EqualsAndHashCode import groovy.transform.ToString import spock.lang.Specification +import spock.lang.Unroll class IdentityStrategyConfigIT extends Specification { @@ -147,11 +148,12 @@ class IdentityStrategyConfigIT extends Specification { .getChild(PV1CodeSelector).getChild("code").untouched } + @Unroll("OfType configuration WITH IdentityStrategy #aClazz") def 'OfType configuration WITH IdentityStrategy'() { when: def node = ObjectDifferBuilder .startBuilding() - .identity().ofType(ProductVersion.class).toUse(codeIdentity).and() + .identity().ofType(aClazz).toUse(codeIdentity).and() .filtering().returnNodesWithState(DiffNode.State.UNTOUCHED).and() .build().compare(working, base); then: "High level nodes" @@ -170,6 +172,8 @@ class IdentityStrategyConfigIT extends Specification { .getChild(PV1CodeSelector).getChild("id").changed node.getChild("productMap").getChild(new MapKeyElementSelector("PROD1")).getChild("productVersions") .getChild(PV1CodeSelector).getChild("code").untouched + where: + aClazz << [ProductVersion, CodeId] } def 'OfNode configuration WITH IdentityStrategy'() { @@ -220,7 +224,7 @@ class IdentityStrategyConfigIT extends Specification { Map otherMap; } - public interface CodeId { + public static interface CodeId { String getCode(); } diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java index 1d6c0531..e088c7c1 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java @@ -22,6 +22,8 @@ /** * Allows to configure the way objects identities are established when comparing * collections by CollectionDiffer. + *

+ * Applies rules to subclasses / interface implementations. */ public interface IdentityConfigurer { Of ofNode(NodePath nodePath); diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index ff1b5ddf..a1f1bca3 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -26,119 +26,132 @@ /** * Resolves identity strategies, if none specified use EqualsIdentityStrategy(). - * + *

* At the moment only used by CollectionDiffer. */ public class IdentityService - implements - IdentityConfigurer, - IdentityStrategyResolver { - public static final IdentityStrategy EQUALS_IDENTITY_STRATEGY = new EqualsIdentityStrategy(); - - private final NodePathValueHolder nodePathStrategies = NodePathValueHolder - .of(IdentityStrategy.class); - private final Map, IdentityStrategy> typeStrategyMap = new HashMap, IdentityStrategy>(); - private final TypePropertyResolver typePropertyResolver = new TypePropertyResolver(); - private final ObjectDifferBuilder objectDifferBuilder; - - public IdentityService(final ObjectDifferBuilder objectDifferBuilder) { - this.objectDifferBuilder = objectDifferBuilder; - } - - public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { - IdentityStrategy identityStrategy = typePropertyResolver.resolve(node); - if (identityStrategy != null) { - return identityStrategy; - } - identityStrategy = nodePathStrategies.valueForNodePath(node.getPath()); - if (identityStrategy != null) { - return identityStrategy; - } - - final Class valueType = node.getValueType(); - if (typeStrategyMap.containsKey(valueType)) { - return typeStrategyMap.get(valueType); - } - - return EQUALS_IDENTITY_STRATEGY; - } - - public IdentityStrategy resolveByCollectionElement( - final Object collectionElement) { - if (collectionElement == null) - { - return EQUALS_IDENTITY_STRATEGY; - } - - final IdentityStrategy identityStrategy = typeStrategyMap - .get(collectionElement.getClass()); - if (identityStrategy != null) { - return identityStrategy; - } - - return EQUALS_IDENTITY_STRATEGY; - } - - public Of ofNode(final NodePath nodePath) { - return new OfNodePath(nodePath); - } - - public Of ofType(final Class type) { - return new OfType(type); - } - - public Of ofTypeAndProperty(final Class type, - final String... propertyNames) { - return new OfTypeAndProperty(type, propertyNames); - } - - public ObjectDifferBuilder and() { - return objectDifferBuilder; - } - - private abstract static class AbstractOf implements Of { - } - - private class OfType extends AbstractOf { - private final Class type; - - public OfType(final Class type) { - this.type = type; - } - - public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { - typeStrategyMap.put(type, identityStrategy); - return IdentityService.this; - } - } - - private class OfNodePath extends AbstractOf { - private final NodePath nodePath; - - public OfNodePath(final NodePath nodePath) { - this.nodePath = nodePath; - } - - public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { - nodePathStrategies.put(nodePath, identityStrategy); - return IdentityService.this; - } - } - - private class OfTypeAndProperty extends AbstractOf { - private final Class type; - - private final String[] properties; - - public OfTypeAndProperty(final Class type, final String[] properties) { - this.type = type; - this.properties = properties; - } - - public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { - typePropertyResolver - .setStrategy(type, identityStrategy, properties); - return IdentityService.this; - } - } + implements + IdentityConfigurer, + IdentityStrategyResolver { + public static final IdentityStrategy EQUALS_IDENTITY_STRATEGY = new EqualsIdentityStrategy(); + + private final NodePathValueHolder nodePathStrategies = NodePathValueHolder + .of(IdentityStrategy.class); + private final Map, IdentityStrategy> typeStrategyMap = new HashMap, IdentityStrategy>(); + private final TypePropertyResolver typePropertyResolver = new TypePropertyResolver(); + private final ObjectDifferBuilder objectDifferBuilder; + + public IdentityService(final ObjectDifferBuilder objectDifferBuilder) { + this.objectDifferBuilder = objectDifferBuilder; + } + + public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { + IdentityStrategy identityStrategy = typePropertyResolver.resolve(node); + if (identityStrategy != null) { + return identityStrategy; + } + identityStrategy = nodePathStrategies.valueForNodePath(node.getPath()); + if (identityStrategy != null) { + return identityStrategy; + } + + identityStrategy = resolveTypeStrategy(node.getValueType()); + if (identityStrategy != null) { + return identityStrategy; + } + + return EQUALS_IDENTITY_STRATEGY; + } + + /** + * Resolves by type including subclasses / interfaces. + * + * @param aClass + * @return + */ + private IdentityStrategy resolveTypeStrategy(final Class aClass) { + for (Class keyClass : typeStrategyMap.keySet()) { + if (keyClass.isAssignableFrom(aClass)) { + return typeStrategyMap.get(keyClass); + } + } + return null; + } + + public IdentityStrategy resolveByCollectionElement( + final Object collectionElement) { + if (collectionElement == null) { + return EQUALS_IDENTITY_STRATEGY; + } + + final IdentityStrategy identityStrategy = resolveTypeStrategy(collectionElement.getClass()); + if (identityStrategy != null) { + return identityStrategy; + } + + return EQUALS_IDENTITY_STRATEGY; + } + + public Of ofNode(final NodePath nodePath) { + return new OfNodePath(nodePath); + } + + public Of ofType(final Class type) { + return new OfType(type); + } + + public Of ofTypeAndProperty(final Class type, + final String... propertyNames) { + return new OfTypeAndProperty(type, propertyNames); + } + + public ObjectDifferBuilder and() { + return objectDifferBuilder; + } + + private abstract static class AbstractOf implements Of { + } + + private class OfType extends AbstractOf { + private final Class type; + + public OfType(final Class type) { + this.type = type; + } + + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { + typeStrategyMap.put(type, identityStrategy); + return IdentityService.this; + } + } + + private class OfNodePath extends AbstractOf { + private final NodePath nodePath; + + public OfNodePath(final NodePath nodePath) { + this.nodePath = nodePath; + } + + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { + nodePathStrategies.put(nodePath, identityStrategy); + return IdentityService.this; + } + } + + private class OfTypeAndProperty extends AbstractOf { + private final Class type; + + private final String[] properties; + + public OfTypeAndProperty(final Class type, final String[] properties) { + this.type = type; + this.properties = properties; + } + + public IdentityConfigurer toUse(final IdentityStrategy identityStrategy) { + typePropertyResolver + .setStrategy(type, identityStrategy, properties); + return IdentityService.this; + } + } } From 6b2f3c636d9ee0d58360df001c59856a91b6946b Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Wed, 17 Jun 2015 09:36:11 +0800 Subject: [PATCH 11/16] Reverted CollectionItemElementSelectorTest.groovy hashCode checks from constant --- .../CollectionItemElementSelectorTest.groovy | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy b/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy index 2032ae87..28116491 100644 --- a/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy +++ b/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy @@ -36,16 +36,13 @@ class CollectionItemElementSelectorTest extends Specification { !element.equals(null) } - def 'should have constant hashCode'() { - // NOTE(@SQiShER): In this case the hashCode is only used to use the ElementSelector - // as key in a Map. With introduction of the IdentityStrategy this adds an unnecessary - // source of confusion and complexity when implementing custom IdentityStrategies. To - // avoid this, returning a constant hashCode seems like a small fee to pay. Yes, it may - // have a small performance impact, but we can still optimize when it turns out to - // actually be a problem. + def 'should have same hashCode as item'() { + setup: + def item = "foo" + def element = new CollectionItemElementSelector(item) expect: - new CollectionItemElementSelector('foo').hashCode() == 31 + element.hashCode() == item.hashCode() } def 'should provide accessor for item'() { From 9b1e985bae780f425d4686d944e0a0451ecebf78 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Fri, 19 Jun 2015 10:02:22 +0800 Subject: [PATCH 12/16] Caching of typeStrategyMap resolution results (preformance patch) (cherry picked from commit f925689) --- .../diff/identity/IdentityService.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index a1f1bca3..1cce609a 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -38,6 +38,7 @@ public class IdentityService private final NodePathValueHolder nodePathStrategies = NodePathValueHolder .of(IdentityStrategy.class); private final Map, IdentityStrategy> typeStrategyMap = new HashMap, IdentityStrategy>(); + private final Map, IdentityStrategy> typeStrategyMapCache = new HashMap, IdentityStrategy>(); private final TypePropertyResolver typePropertyResolver = new TypePropertyResolver(); private final ObjectDifferBuilder objectDifferBuilder; @@ -67,15 +68,29 @@ public IdentityStrategy resolveIdentityStrategy(final DiffNode node) { * Resolves by type including subclasses / interfaces. * * @param aClass - * @return + * @return never null, EQUALS_IDENTITY_STRATEGY if not found */ private IdentityStrategy resolveTypeStrategy(final Class aClass) { - for (Class keyClass : typeStrategyMap.keySet()) { - if (keyClass.isAssignableFrom(aClass)) { - return typeStrategyMap.get(keyClass); + // check cache + IdentityStrategy ret = typeStrategyMapCache.get(aClass); + + if(ret == null) { + // not in cache, search + for (Class keyClass : typeStrategyMap.keySet()) { + if (keyClass.isAssignableFrom(aClass)) { + ret = typeStrategyMap.get(keyClass); + typeStrategyMapCache.put(aClass, ret); + return ret; + } } + // not found, use EQUALS_IDENTITY_STRATEGY + ret = EQUALS_IDENTITY_STRATEGY; + typeStrategyMapCache.put(aClass, ret); + return ret; + } else { + // in cache, return + return ret; } - return null; } public IdentityStrategy resolveByCollectionElement( From 9a44e0236b7325a035deb381e4d67325b9d181f5 Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Tue, 30 Jun 2015 16:27:06 +0800 Subject: [PATCH 13/16] distributionManagement to studium_releases and studium_snapshots --- pom.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index f4850b9d..339a65b0 100644 --- a/pom.xml +++ b/pom.xml @@ -440,10 +440,13 @@ - bintray-sqisher-maven-java-object-diff - sqisher-maven-java-object-diff - https://api.bintray.com/maven/sqisher/maven/java-object-diff + studium_releases + https://www.studium.sg/nexus/content/repositories/releases + + studium_snapshots + https://www.studium.sg/nexus/content/repositories/snapshots + From 6f7af091ad65c130e75a5d6053a078ea1c81b40b Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sat, 16 Jul 2016 16:07:22 +0800 Subject: [PATCH 14/16] com.studium:java-object-diff:0.92.2-SNAPSHOT => sg.studium:java-object-diff:0.92.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 339a65b0..2226a423 100644 --- a/pom.xml +++ b/pom.xml @@ -18,10 +18,10 @@ 4.0.0 - com.studium + sg.studium java-object-diff Java Object Diff - 0.92.2-SNAPSHOT + 0.92.2 2.2.1 From 9c5f8518c59f3a780bf95363cecc61f93bf1bbdd Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 17 Jul 2016 08:59:09 +0800 Subject: [PATCH 15/16] IdentityConfigurer javadoc --- .../diff/identity/IdentityConfigurer.java | 12 +++++++++++- .../danielbechler/diff/identity/IdentityService.java | 4 ++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java index e088c7c1..9072293d 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java @@ -26,10 +26,20 @@ * Applies rules to subclasses / interface implementations. */ public interface IdentityConfigurer { + /** + * @param nodePath of the collection to attach to. + */ Of ofNode(NodePath nodePath); - Of ofType(Class type); + /** + * @param collectionElementType of the elements within the collection to attach to. + */ + Of ofType(Class collectionElementType); + /** + * @param type of the host class of the collection to attach to. + * @param propertyNames property name of the collection in the host class + */ Of ofTypeAndProperty(Class type, String... propertyNames); ObjectDifferBuilder and(); diff --git a/src/main/java/de/danielbechler/diff/identity/IdentityService.java b/src/main/java/de/danielbechler/diff/identity/IdentityService.java index 1cce609a..843e4b21 100644 --- a/src/main/java/de/danielbechler/diff/identity/IdentityService.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -111,8 +111,8 @@ public Of ofNode(final NodePath nodePath) { return new OfNodePath(nodePath); } - public Of ofType(final Class type) { - return new OfType(type); + public Of ofType(final Class collectionElementType) { + return new OfType(collectionElementType); } public Of ofTypeAndProperty(final Class type, From b5931a9449b0901955f2dca916c2a4329a87f81b Mon Sep 17 00:00:00 2001 From: Gabor Nagy Date: Sun, 17 Jul 2016 08:59:32 +0800 Subject: [PATCH 16/16] README.md --- README.md | 100 ++++-------------------------------------------------- 1 file changed, 7 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 204ad2a8..0993f874 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,11 @@ -## Introduction +## Features added to upstream project -`java-object-diff` is a simple, yet powerful library to find differences between Java objects. It takes two objects and generates a tree structure that represents any differences between the objects and their children. This tree can then be traversed to extract more information or apply changes to the underlying data structures. - -[![Build Status](https://travis-ci.org/SQiShER/java-object-diff.svg?branch=master)](https://travis-ci.org/SQiShER/java-object-diff) -[![Coverage Status](https://coveralls.io/repos/SQiShER/java-object-diff/badge.png?branch=master)](https://coveralls.io/r/SQiShER/java-object-diff?branch=master) -[![Download](https://api.bintray.com/packages/sqisher/maven/java-object-diff/images/download.svg)](https://bintray.com/sqisher/maven/java-object-diff/_latestVersion) -[![Documentation Status](https://readthedocs.org/projects/java-object-diff/badge/?version=latest)](https://readthedocs.org/projects/java-object-diff/?badge=latest) - -## Features - -* Works out-of-the-box with with almost any kind of object and arbitrarily deep nesting -* Finds the differences between two objects -* Returns the differences in shape of an easily traversable tree structure -* Tells you everything there is to know about the detected changes -* Provides read and write access to the underlying objects, allowing you not only to extract the changed values but even to apply the diff as a patch -* Requires no changes to your existing classes (in most cases) -* Provides a very flexible configuration API to tailor everything to your needs -* Tiny, straightforward, yet very powerful API -* Detects and handles circular references in the object graph -* No runtime dependencies except for [SLF4J](http://www.slf4j.org/) -* Compatible with Java 1.5 and above - -## Support this Project - -If you like this project, there are a few things you can do to show your support: - -* [**Follow me** on Twitter (@SQiShER)](https://twitter.com/SQiShER) -* [**Surprise me** with something from my Amazon Wishlist](http://www.amazon.de/registry/wishlist/2JFW27V71CBGM) -* [**Contribute** code, documentation, ideas, or insights into your use-case](https://github.com/SQiShER/java-object-diff/blob/master/CONTRIBUTING.md) -* Star this repository (stars make me very happy!) -* Talk about it, write about it, recommend it to others - -But most importantly: **don't ever hesitate to ask me for help**, if you're having trouble getting this library to work. The only way to make it better is by hearing about your use-cases and pushing the limits! - -## Getting Started - -To learn how to use **Java Object Diff** have a look at the [Getting Started Guide](http://java-object-diff.readthedocs.org/en/latest/getting-started/). - -### Using with Maven - -```xml - - de.danielbechler - java-object-diff - 0.92.1 - -``` - -### Using with Gradle - -```groovy -compile 'de.danielbechler:java-object-diff:0.92.1' -``` - -## Documentation - -The documentation can be found over at [ReadTheDocs](http://java-object-diff.readthedocs.org/en/latest/). - -## Caveats - -* Introspection of values other than primitives and collection types is curently done via standard JavaBean introspection, which requires your objects to provide getters and setters for their properties. However, you don't need to provide setters, if you don't need write access to the properties (e.g. you don't want to apply the diff as a patch.) - - If this does not work for you, don't worry: you can easily write your own introspectors and just plug them in via configuration API. - -* Ordered lists are currently not properly supported (they are just treated as Sets). While this is something I definitely want to add before version `1.0` comes out, its a pretty big task and will be very time consuming. So far there have been quite a few people who needed this feature, but not as many as I imagined. So your call to action: if you need to diff and/or merge collection types like `ArrayList`, perhaps even with multiple occurence of the same value, please let me know. The more I'm aware of the demand and about the use-cases, the more likely it is, that I start working on it. - -## Why would you need this? - -Sometimes you need to figure out, how one version of an object differs from another one. One of the simplest solutions that'll cross your mind is most certainly to use reflection to scan the object for fields or getters and use them to compare the values of the different object instances. In many cases this is a perfectly valid strategy and the way to go. After all, we want to keep things simple, don't we? - -However, there are some cases that can increase the complexity dramatically. What if you need to find differences in collections or maps? What if you have to deal with nested objects that also need to be compared on a per-property basis? Or even worse: what if you need to merge such objects? - -You suddenly realize that you need to scan the objects recursively, figure out which collection items have been added, removed or changed; find a way to return your results in a way that allows you to easily access the information you are looking for and provide accessors to apply changes. - -While all this isn't exactly rocket science, it is complex enough to add quite a lot of extra code to your project. Code that needs to be tested and maintained. Since the best code is the code you didn't write, this library aims to help you with all things related to diffing and merging of Java objects by providing a robust foundation and a simple, yet powerful API. - -This library will hide all the complexities of deep object comparison behind one line of code: +* IdentityStrategy adheres the Java spec on equals() and hashCode() +* IdentityConfigurer allows to configure by element type within the collection ```java -Node root = ObjectDifferBuilder.buildDefault().compare(workingObject, baseObject); +ObjectDifferBuilder + .startBuilding() + .identity().ofType(ElementClass.class).toUse(codeIdentity).and() + ... ``` - -This generates a tree structure of the given object type and lets you traverse its nodes via visitors. Each node represents one property (or collection item) of the underlying object and tells you exactly if and how the value differs from the base version. It also provides accessors to read, write and remove the value from or to any given instance. This way, all you need to worry about is **how to treat** changes and **not how to find** them. - -This library has been battle-tested in a rather big project of mine, where I use it to generate **activity streams**, resolve database **update conflics**, display **change logs** and limit the scope of entity updates to only a **subset of properties**, based on the context or user permissions. It didn't let me down so far and I hope it can help you too! - -## Contribute - -You discovered a bug or have an idea for a new feature? Great, why don't you send me a [Pull -Request](https://help.github.com/articles/using-pull-requests) so everyone can benefit from it? To help you getting started, [here](https://github.com/SQiShER/java-object-diff/blob/master/CONTRIBUTING.md) is a brief guide with everyting you need to know to get involved! - ---- - -Thanks to JetBrains for supporting this project with a free open source license for their amazing IDE **IntelliJ IDEA**. - -[![IntelliJ IDEA](https://www.jetbrains.com/idea/docs/logo_intellij_idea.png)](https://www.jetbrains.com/idea/)