diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 00000000..d1422f20 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,38 @@ +# Contributing + +You discovered a bug or have an idea for a new feature? Great, why don't you send me a [Pull +Request (PR)](https://help.github.com/articles/using-pull-requests) so everyone can benefit from it? + +Getting started is easy: + +* Fork the __java-object-diff__ repository on Github +* Clone the forked repository to your computer +* Switch to the root project directory and run `mvn clean package` + +If everything went well, this should build, test and package the project. Now you can start making your changes. + +There are some things to help you getting started: + +* Make yourself familiar with the [__User Guide__](http://java-object-diff.readthedocs.org/en/latest/user-guide/), so you understand the basic architecture. +* [__Check for open issues__](https://github.com/SQiShER/java-object-diff/issues) that interest you or look for issues with the [__Contributor Friendly__](https://github.com/SQiShER/java-object-diff/labels/difficulty:%20contributor%20friendly) tag. These issues are especially well suited to get more familiar with the codebase without being overwhelming. +* In case you have an idea for a new feature, check the issue tracker to see if there were already some discussions regarding that feature. If not, feel free to open a new discussion to see what others think about it. + +So you found something you want to work on? That's great! If you run into any problems or are not entirely sure how to tackle the problem, feel free to ask me on [Twitter](https://twitter.com/SQiShER) or post your question to the [issue tracker](https://github.com/SQiShER/java-object-diff/issues) if it is more complex. + +Before you submit your PR with the result, please make sure to: + +* __Write at least one fully integrated test__ (no mocks and comparison done via public API) to show +that the fix or feature works as promised - from a user perspective. What you are doing here is +basically saying: "In case of X you can expect the library to behave like Y". You shouldn't cover +every possible execution path this way but rather focus on proving that the feature works and under +which circumstances it will take effect. Doing this will help others to keep your feature intact, +when the library evolves. +* __Write unit tests__! Lots of them! Keep them small and readable and try to cover as much of your logic as possible. But don't go overboard: focus on actual logic and avoid testing simple getters and setters just to reach the magical 100% test coverage. +* __Write your tests with Groovy and [Spock](http://spock-framework.readthedocs.org/en/latest/data_driven_testing.html#introduction)!__ +Spock is an amazing testing framework that makes many things much, much easier to accomplish. +It's not hard to learn and a lot of fun to use. Yes, I know that there are still some TestNG tests +in the codebase, but they are getting replaced one by one until the dependency can finally be removed. + +When you've done that, nothing should hold you back from sending me a pull request and bug me until it gets merged and published. :wink: + +Thanks for your support and happy coding! diff --git a/.gitignore b/.gitignore index 34732307..613b92d3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,11 @@ .idea/tasks.xml *.iml +# Eclipse # +.project +.classpath +.settings + # Maven Generated Files # target @@ -12,3 +17,8 @@ target *.jar *.war *.ear +.idea + +build +.gradle +.DS_Store \ No newline at end of file diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index cb093f48..00000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -java-object-diff \ No newline at end of file diff --git a/.idea/ant.xml b/.idea/ant.xml deleted file mode 100644 index f6e673ad..00000000 --- a/.idea/ant.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml deleted file mode 100644 index 29b87733..00000000 --- a/.idea/codeStyleSettings.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - diff --git a/.idea/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index 777ea293..00000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - diff --git a/.idea/copyright/Apache_License_2_0.xml b/.idea/copyright/Apache_License_2_0.xml deleted file mode 100644 index 9bddebfb..00000000 --- a/.idea/copyright/Apache_License_2_0.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copyright/GPL.xml b/.idea/copyright/GPL.xml deleted file mode 100644 index 3f98d9bf..00000000 --- a/.idea/copyright/GPL.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copyright/MIT.xml b/.idea/copyright/MIT.xml deleted file mode 100644 index 880a73c1..00000000 --- a/.idea/copyright/MIT.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/copyright/profiles_settings.xml b/.idea/copyright/profiles_settings.xml deleted file mode 100644 index 2ae59579..00000000 --- a/.idea/copyright/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/dictionaries/dbechler.xml b/.idea/dictionaries/dbechler.xml deleted file mode 100644 index 503f9287..00000000 --- a/.idea/dictionaries/dbechler.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - appendable - delegator - jarrah - kwon - shephard - - - \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index 03bb84e6..00000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index f6e673ad..00000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index 63fded89..00000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,292 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml deleted file mode 100644 index 3b312839..00000000 --- a/.idea/inspectionProfiles/profiles_settings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__cglib_cglib_nodep_3_1.xml b/.idea/libraries/Maven__cglib_cglib_nodep_3_1.xml deleted file mode 100644 index f3a14347..00000000 --- a/.idea/libraries/Maven__cglib_cglib_nodep_3_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_1_1.xml b/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_1_1.xml deleted file mode 100644 index 6f8c5c90..00000000 --- a/.idea/libraries/Maven__ch_qos_logback_logback_classic_1_1_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__ch_qos_logback_logback_core_1_1_1.xml b/.idea/libraries/Maven__ch_qos_logback_logback_core_1_1_1.xml deleted file mode 100644 index e9c2383e..00000000 --- a/.idea/libraries/Maven__ch_qos_logback_logback_core_1_1_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__com_beust_jcommander_1_27.xml b/.idea/libraries/Maven__com_beust_jcommander_1_27.xml deleted file mode 100644 index f0f9060d..00000000 --- a/.idea/libraries/Maven__com_beust_jcommander_1_27.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__junit_junit_dep_4_10.xml b/.idea/libraries/Maven__junit_junit_dep_4_10.xml deleted file mode 100644 index c29a6ee0..00000000 --- a/.idea/libraries/Maven__junit_junit_dep_4_10.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_codehaus_groovy_groovy_all_2_2_1.xml b/.idea/libraries/Maven__org_codehaus_groovy_groovy_all_2_2_1.xml deleted file mode 100644 index 4bab2713..00000000 --- a/.idea/libraries/Maven__org_codehaus_groovy_groovy_all_2_2_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_easytesting_fest_assert_core_2_0M10.xml b/.idea/libraries/Maven__org_easytesting_fest_assert_core_2_0M10.xml deleted file mode 100644 index ec681858..00000000 --- a/.idea/libraries/Maven__org_easytesting_fest_assert_core_2_0M10.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_easytesting_fest_util_1_2_5.xml b/.idea/libraries/Maven__org_easytesting_fest_util_1_2_5.xml deleted file mode 100644 index 79cdbc69..00000000 --- a/.idea/libraries/Maven__org_easytesting_fest_util_1_2_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_hamcrest_hamcrest_all_1_3.xml b/.idea/libraries/Maven__org_hamcrest_hamcrest_all_1_3.xml deleted file mode 100644 index 56193163..00000000 --- a/.idea/libraries/Maven__org_hamcrest_hamcrest_all_1_3.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml b/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml deleted file mode 100644 index f58bbc11..00000000 --- a/.idea/libraries/Maven__org_hamcrest_hamcrest_core_1_3.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml b/.idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml deleted file mode 100644 index 7797878d..00000000 --- a/.idea/libraries/Maven__org_mockito_mockito_all_1_9_5.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml b/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml deleted file mode 100644 index 65280d39..00000000 --- a/.idea/libraries/Maven__org_slf4j_slf4j_api_1_7_6.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_spockframework_spock_core_0_7_groovy_2_0.xml b/.idea/libraries/Maven__org_spockframework_spock_core_0_7_groovy_2_0.xml deleted file mode 100644 index 2a3ead97..00000000 --- a/.idea/libraries/Maven__org_spockframework_spock_core_0_7_groovy_2_0.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/libraries/Maven__org_testng_testng_6_8_7.xml b/.idea/libraries/Maven__org_testng_testng_6_8_7.xml deleted file mode 100644 index 74e6a1f5..00000000 --- a/.idea/libraries/Maven__org_testng_testng_6_8_7.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index fac47b03..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - http://www.w3.org/1999/xhtml - - - - - - - diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 2b06cc7a..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/.idea/runConfigurations/Integration_Tests.xml b/.idea/runConfigurations/Integration_Tests.xml deleted file mode 100644 index 1a3fc321..00000000 --- a/.idea/runConfigurations/Integration_Tests.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Integration_Tests2.xml b/.idea/runConfigurations/Integration_Tests2.xml deleted file mode 100644 index b4759ae2..00000000 --- a/.idea/runConfigurations/Integration_Tests2.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Unit_Tests.xml b/.idea/runConfigurations/Unit_Tests.xml deleted file mode 100644 index 18733f8f..00000000 --- a/.idea/runConfigurations/Unit_Tests.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Unit_Tests2.xml b/.idea/runConfigurations/Unit_Tests2.xml deleted file mode 100644 index 6e787f03..00000000 --- a/.idea/runConfigurations/Unit_Tests2.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml deleted file mode 100644 index 922003b8..00000000 --- a/.idea/scopes/scope_settings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index 3b000203..00000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,125 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 275077f8..00000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/.travis.yml b/.travis.yml index dff5f3a5..2915ff01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1 +1,7 @@ language: java + +jdk: +- oraclejdk8 + +after_success: +- ./gradlew cobertura coveralls \ No newline at end of file diff --git a/NOTICE b/NOTICE index 3749a95c..a7452488 100644 --- a/NOTICE +++ b/NOTICE @@ -16,12 +16,12 @@ specific language governing permissions and limitations under the License. Lists of 9 third-party dependencies. - (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.0.7 - http://logback.qos.ch) - (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.0.7 - http://logback.qos.ch) - (Apache License, Version 2.0) TestNG (org.testng:testng:6.8 - http://testng.org) - (Apache License, Version 2.0) JCommander (com.beust:jcommander:1.27 - http://beust.com/jcommander) - (Apache License, Version 2.0) FEST Fluent Assertions (Core) (org.easytesting:fest-assert-core:2.0M8 - http://fest.easytesting.org/assert) - (Apache License, Version 2.0) FEST Util (org.easytesting:fest-util:1.2.3 - http://fest.easytesting.org) - (MIT License) Mockito (org.mockito:mockito-all:1.9.0 - http://www.mockito.org) - (MIT License) SLF4J API Module (org.slf4j:slf4j-api:1.7.2 - http://www.slf4j.org) - (New BSD License) Hamcrest All (org.hamcrest:hamcrest-all:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-all) + (ASF 2.0) cglib-nodep (cglib:cglib-nodep:3.2.1 - https://github.com/cglib/cglib/cglib-nodep) + (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Classic Module (ch.qos.logback:logback-classic:1.1.6 - http://logback.qos.ch/logback-classic) + (Eclipse Public License - v 1.0) (GNU Lesser General Public License) Logback Core Module (ch.qos.logback:logback-core:1.1.6 - http://logback.qos.ch/logback-core) + (Eclipse Public License 1.0) JUnit (junit:junit:4.12 - http://junit.org) + (The Apache Software License, Version 2.0) Apache Groovy (org.codehaus.groovy:groovy-all:2.4.6 - http://groovy-lang.org) + (New BSD License) Hamcrest Core (org.hamcrest:hamcrest-core:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-core) + (Apache 2) Objenesis (org.objenesis:objenesis:2.2 - http://objenesis.org) + (MIT License) SLF4J API Module (org.slf4j:slf4j-api:1.7.18 - http://www.slf4j.org) + (The Apache Software License, Version 2.0) Spock Framework - Core Module (org.spockframework:spock-core:1.0-groovy-2.4 - http://spockframework.org) diff --git a/README.md b/README.md index e4d653ca..617512e7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,69 @@ -[![Build Status](https://travis-ci.org/SQiShER/java-object-diff.png?branch=master)](https://travis-ci.org/SQiShER/java-object-diff) - ## Introduction `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.svg?branch=master&service=github)](https://coveralls.io/github/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 -* Generates an easily traversable tree structure to analyze and modify with surgical precision -* Detects whether a value or item has been added, removed or changed and shows the changes -* Allows to manipulate the underlying objects directly through the generated tree nodes -* Works with almost any kind of object (Beans, Lists, Maps, Primitives, Strings, etc.) -* Properties can be marked with category tags to easily filter for specific subsets -* No configuration needed (but possible) +* 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/) -* When needed, it leaves it up to you, whether you want to use declarative configuration or annotations +* 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**, please have a look at the [Starter Guide](https://github.com/SQiShER/java-object-diff/wiki/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.95 + +``` + +### Using with Gradle + +```groovy +compile 'de.danielbechler:java-object-diff:0.95' +``` + +## 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? @@ -31,32 +77,21 @@ While all this isn't exactly rocket science, it is complex enough to add quite a This library will hide all the complexities of deep object comparison behind one line of code: - Node root = ObjectDifferFactory.getInstance().compare(workingObject, baseObject); +```java +DiffNode root = ObjectDifferBuilder.buildDefault().compare(workingObject, baseObject); +``` 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 that it can help you too! - -## Use Cases - -**Java Object Diff** is currently used (but not limited) to... - -* Generate Facebook-like activity streams -* Visualize the differences between object versions -* Automatically resolve conflicts on conflicting database updates -* Detect and persist only properties that were actually changed +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! -## Contributing +## Contribute -* [Check for open issues](https://github.com/SQiShER/java-object-diff/issues) or open a fresh issue to start a discussion around a feature idea or a bug. There is a *Contributor Friendly* tag for issues that should be ideal for people who are not very familiar with the codebase yet. -* Fork the java-object-diff repository on Github to start making your changes. -* Write some tests which show that the bug was fixed or that the feature works as expected. -* Send a pull request and bug the maintainer until it gets merged and published. :) - -## Buy Me a Beer +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! -I put a lot of effort into this project and I really enjoy working on it. However, I do all of the work in my spare time, of which I don’t have very much. Unfortunately. If you’d like to support this project, how about you buy me a beer by clicking the Pledgie button down below? Alternatively you could send me a nice tweet, write a blog post, tell your friends or star the project. I'm basically happy about everything that keeps the morale up and shows me the effort is appreciated and my time well spent. +--- -Click here to lend your support to: java-object-diff and make a donation at pledgie.com ! +Thanks to JetBrains for supporting this project with a free open source license for their amazing IDE **IntelliJ IDEA**. -Bitcoin: `19kRmHJ4qMnYCY6rnY6kCf96Prj6WGxisk` +[![IntelliJ IDEA](https://www.jetbrains.com/idea/docs/logo_intellij_idea.png)](https://www.jetbrains.com/idea/) diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..7e128f63 --- /dev/null +++ b/build.gradle @@ -0,0 +1,222 @@ +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'ru.vyarus:gradle-animalsniffer-plugin:1.2.0' + classpath "com.smokejumperit.gradle.license:Gradle-License-Report:0.0.2" + } +} + +plugins { + id 'java' + id 'groovy' + id 'osgi' + id 'net.saliman.cobertura' version '2.4.0' + id 'com.github.kt3k.coveralls' version '2.8.1' + id 'com.github.ben-manes.versions' version '0.14.0' + id 'com.jfrog.bintray' version '1.7.3' + id 'net.researchgate.release' version '2.5.0' + id 'com.github.jk1.dependency-license-report' version '0.3.8' +} + +apply plugin: 'ru.vyarus.animalsniffer' +apply plugin: 'maven-publish' + +group = 'de.danielbechler' + +description = """Java Object Diff""" + +sourceCompatibility = 1.5 +targetCompatibility = 1.5 + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +repositories { + mavenCentral() +} + +sourceSets { + main { + java { + srcDirs = ['src/main/java'] + } + groovy { + srcDirs = [] + } + } + test { + java { + srcDirs = [] + } + groovy { + srcDirs = ['src/test/java'] + } + } + intTest { + groovy { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDirs = ['src/integration-test/java'] + } + } +} + +configurations { + intTestCompile.extendsFrom testCompile + intTestRuntime.extendsFrom testRuntime +} + +task intTest(type: Test) { + testClassesDir = sourceSets.intTest.output.classesDir + classpath = sourceSets.intTest.runtimeClasspath +} + +check.dependsOn intTest + +compileGroovy { + // somehow the groovy compile deletes the java compiled classes from the build directory + dependsOn = [] +} + +jar { + manifest { + instruction 'Bundle-Vendor', 'Daniel Bechler' + instruction 'Bundle-DocURL', 'https://github.com/SQiShER/java-object-diff' + instruction 'Export-Package', '{local-packages}' + } +} + +dependencies { + signature 'org.codehaus.mojo.signature:java15:1.0@signature' + signature 'org.codehaus.mojo.signature:java16:1.1@signature' + signature 'org.codehaus.mojo.signature:java17:1.0@signature' + // For Android support: + // signature 'net.sf.androidscents.signature:android-api-level-23:6.0_r3@signature' + compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.22' + testCompile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.4.8' + testCompile group: 'ch.qos.logback', name: 'logback-core', version: '1.2.1' + testCompile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.1' + testCompile group: 'org.spockframework', name: 'spock-core', version: '1.0-groovy-2.4' + testCompile group: 'cglib', name: 'cglib-nodep', version: '3.2.4' + testCompile group: 'org.objenesis', name: 'objenesis', version: '2.5.1' +} + +cobertura { + coverageFormats = ['html', 'xml'] + coverageIgnoreTrivial = true +} + +bintray { + user = System.getenv('BINTRAY_USER') + key = System.getenv('BINTRAY_KEY') + publications = ['mavenJava'] + publish = true + pkg { + repo = 'maven' + name = 'java-object-diff' + licenses = ['Apache-2.0'] + vcsUrl = 'https://github.com/SQiShER/java-object-diff.git' + githubRepo = 'SQiShER/java-object-diff' + githubReleaseNotesFile = 'README.md' + version { + name = project.version + released = new Date() + vcsTag = rootProject.name + '-' + project.version + gpg { + sign = true + passphrase = System.getenv('BINTRAY_GPG_PASSPHRASE') + } + mavenCentralSync { + user = System.getenv('MAVEN_CENTRAL_USER_TOKEN_KEY') + password = System.getenv('MAVEN_CENTRAL_USER_TOKEN_PASSWORD') + } + } + } +} + +javadoc { + failOnError = false +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives sourcesJar, javadocJar +} + +def pomConfig = { + licenses { + license { + name "The Apache Software License, Version 2.0" + url "http://www.apache.org/licenses/LICENSE-2.0.txt" + distribution "repo" + } + } + developers { + developer { + id "sqisher" + name "Daniel Bechler" + url "https://github.com/SQiShER" + } + } + scm { + url "https://github.com/SQiShER/java-object-diff" + } +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + pom.withXml { + def root = asNode() + root.appendNode('description', 'Library to diff and merge Java objects with ease') + root.appendNode('name', 'java-object-diff') + root.appendNode('url', 'https://github.com/SQiShER/java-object-diff') + root.children().last() + pomConfig + } + } + } +} + +release { + tagTemplate = '$name-$version' +} + +@SuppressWarnings("GroovyAssignabilityCheck") +static processFileInplace(File file, Closure processText) { + String text = file.text + file.write(processText(text)) +} + +task updateDocs { + doLast { + def updateVersion = { String text -> + text = text.replaceAll('[^<]+', "${version}") + text = text.replaceAll('de\\.danielbechler:java-object-diff:[0-9-A-Z\\-.]+', "de.danielbechler:java-object-diff:${version}") + return text + } + processFileInplace(file('README.md'), updateVersion) + processFileInplace(file('docs/maven.md'), updateVersion) + } +} + +afterReleaseBuild.dependsOn bintrayUpload +afterReleaseBuild.dependsOn updateDocs + +task wrapper(type: Wrapper) { + gradleVersion = '3.3' +} \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 00000000..6b301a37 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,100 @@ +# Getting Started + +_java-object-diff_ provides a very simple API and tries to make everything as self-explanatory as possible. It can handle a wide variety of object structures without the need for any configuration. However, in more complex scenarios it is flexible enough to let you tailor it to your needs. + +## Creating a Diff + +Now let's start with a very easy scenario: Comparing two maps. + +``` +Map working = Collections.singletonMap("item", "foo"); +Map base = Collections.singletonMap("item", "bar"); +DiffNode diff = ObjectDifferBuilder.buildDefault().compare(working, base); +``` + +Let's break down what we did here. We created a new `ObjectDiffer` and invoked its `compare` method, passing it two `java.util.Map` objects as arguments. The call returned a `DiffNode`. This represents the root of the object graph and contains other `DiffNode` instances, which represent the next level in the graph. In this case this means they represent the entries of the maps. Each node contains information about how the associated object has changed between the two given objects: + +``` +assert diff.hasChanges(); +assert diff.childCount() == 1; +NodePath itemPath = NodePath.startBuilding().mapKey("item").build(); +assert diff.getChild(itemPath).getState() == DiffNode.State.CHANGED; +``` + +In this case it's very simple. The map contains one entry with the same key and different values. So the root object (the map) contains changes. Since only one item has changed, the root node has one child node. That's the node representing the map entry with key `"item"`. And since its value differs between the base and the working object, `getState` returns the state `CHANGED`. + +In this example we query the child node directly via `NodePath`, but in most cases you probably just want to iterate over all changes. Of course there is an easy way to do that, too. + +## Traversal + +Let's say we want to print the path of each node along with its state: + +``` +diff.visit(new DiffNode.Visitor() +{ + public void node(DiffNode node, Visit visit) + { + System.out.println(node.getPath() + " => " + node.getState()); + } +}); +``` + +The `DiffNode` provides a `visit` method which takes a `Visitor` as argument. This visitor will be called for every single node, including the root node itself. This way you don't need to know the structure of the objects your are diffing and can simply deal with the returned nodes how you see fit. This visitor pattern is very powerful when combined with the next feature: object accessors. + +## Reading Values + +Let's say we want to expand the example above by printing the actual values of the base and the working version. In order to do that, we need to extract those values from the input objects. Thankfully the `DiffNode` provides a method called `canonicalGet`, which knows exactly how to do that. + +``` +diff.visit(new DiffNode.Visitor() +{ + public void node(DiffNode node, Visit visit) + { + final Object baseValue = node.canonicalGet(base); + final Object workingValue = node.canonicalGet(working); + final String message = node.getPath() + " changed from " + + baseValue + " to " + workingValue; + System.out.println(message); + } +}); +``` +This will generate the following output: + +``` +/ changed from {item=bar} to {item=foo} +/{item} changed from bar to foo +``` + +The output not only contains a line for the changed map entry, but also for the changed map itself. In order to avoid that, we could add a check for child nodes and only print nodes that don't contain any child nodes, as those are the ones that represent the actual change. Since that alone isn't very interesting, let's do it while looking at another feature: changing values. + +## Changing Values + +So far we have created a nice diff, but you know what's cooler than diffing? Patching! Imagine we want to merge our changes into a new map. We can easily do that: + +``` +final Map head = new HashMap(base); +head.put("another", "map"); +diff.visit(new DiffNode.Visitor() +{ + public void node(DiffNode node, Visit visit) + { + // only leaf-nodes with changes + if (node.hasChanges() && !node.hasChildren()) { + node.canonicalSet(head, node.canonicalGet(working)); + } + } +}); + +assert head.get("item").equals("foo"); +assert head.get("another").equals("map"); +``` + +The method `canonicalSet` provides write access to the value at the location of the node in the object graph, just as `canonicalGet` provides read access. There is a third method called `canonicalUnset`, which behaves differently based on the underlying object. It removes items from collections, nulls out objects and assigns default values to primitives. + +There are also non-canonical versions of those methods, which operate relative to their parent object. They can be useful in more advanced scenarios, but most of the time you'll probably not need them. + +## Conclusion + +You now know how to create a diff, how to extract information from it and how to apply it as a patch. Congratulations, you are ready to try it out on your own objects. This example was pretty simple and we only worked on a simple `Map`. But don't let this foul you. Working with other objects isn't any different. + +Of course there is much more to cover. Depending on your objects, there may be the need to do do some adjustments and tell _java-object-diff_ how certain values should be handled. There are tons of configuration options that allow you to tame almost any kind of object. If you run into any trouble, check the documentation for more information, as more and more features and frequently asked questions will get documented there. If that doesn't help, check out the [issue tracker](https://github.com/SQiShER/java-object-diff/issues) over at GitHub and ask for help. \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..4d3d63b9 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,21 @@ +## Introduction + +`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. + +## 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: + +```java +Node root = ObjectDifferBuilder.buildDefault().compare(workingObject, baseObject); +``` + +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. \ No newline at end of file diff --git a/docs/maven.md b/docs/maven.md new file mode 100644 index 00000000..aa86da70 --- /dev/null +++ b/docs/maven.md @@ -0,0 +1,13 @@ +In order to use java-object-diff with Maven, all you need to do is to add this dependency to your POM: + + + de.danielbechler + java-object-diff + 0.95 + + +That's it! Now you're ready to go! + +## Dependency not found? + +It can take up to 2 hours after the release of a new version until it is actually mirrored to the Maven Central repository. So if it isn't available right away, please try again a few hours later. If after a couple of hours it's still not available, feel free to report the issue, so I can investigate. \ No newline at end of file diff --git a/docs/merging.md b/docs/merging.md new file mode 100644 index 00000000..12aca855 --- /dev/null +++ b/docs/merging.md @@ -0,0 +1,5 @@ +`java-object-diff` not only makes it easy to find differences between objects, it also allows you to merge them, by providing a simple, yet effective assignment mechanism. Every node in the object graph returned by the `ObjectDiffer` provides setter methods, which can be used to change the state of an underlying object instance, as long as it is of the same type as the compared objects. + +Since the requirements to a merging mechanism can vary strongly, this library doesn't try to implement every possible way and rather strives to make it as easy as possible to implement your own one. However, as a starting point, here is a simple [Example Merger](https://github.com/SQiShER/java-object-diff/blob/master/src/main/java/de/danielbechler/diff/ObjectMerger.java) to show you how easily it can be done. + +I hope the code is self-explanatory. If not, please let me know. \ No newline at end of file diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 00000000..fe92eaac --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,83 @@ +# User Guide + +This guide will give you a brief overview over the most important parts of the public API. + +## ObjectDifferBuilder + +The `ObjectDifferBuilder` is the entry point of every diffing operation. It acts as a factory to get hold of an actual `ObjectDiffer` instance and exposes a powerful configuration API in order to customize its behavior to suit your needs. + +### Configuration API + +The following sections describes the different parts of the configuration API. + +#### ReturnableNodeConfiguration + +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. + +Currently it is only possible to configure returnability based on the state (_added_, _changed_, _untouched_, etc.) of the `DiffNode`. But this is just the beginning. Nothing speaks against adding more powerful options. It would be nice for example to be able to pass some kind of matcher to determine returnability based on dynamic criteria at runtime. + +#### IntrospectionConfiguration + +Allows to replace the default bean introspector with a custom implementation. The default introspector internally uses the `java.beans.Introspector` which has some limitations. The most important one being that it only operates on getters and setters. In case field introspection is needed a custom introspector must be used. An introspector can be set as global default or on a per-property basis. It is also possible to turn off introspection for specific properties in which case they will simply be compared via `equals` method. + +#### CircularReferenceConfiguration + +Allows to define how the circular reference detector compares object instances. By default it uses the equality operator (`==`) which should be fine in mose cases. + +When dealing with object models that return copies of its properties on every access, it's possible to end up in infinite loops, because even though the objects may look the same, they would be different instances. In those cases it is possible to switch the instance detection mode to use the equals method instead of the equality operator. This way objects will be considered to be "the same" whenever `equals` returns `true`. + +This configuration interface also allows to register a custom handler for exception thrown, whenever a circular reference is detected. The default handler simply logs a warning. + +#### InclusionConfiguration + +Allows to in- or exclude nodes based on property name, object type, category or location in the object graph. Excluded nodes will not be compared, to make sure their accessors won't get called. This is useful in cases where getters could throw exceptions under certain conditions or when certain accessors are expensive to call or simply not relevant for the use-case. + +In combination with categories this allows to define sub-sets of properties, in order to compare only relevant parts of an object (e.g. exclude all properties marked as _metadata_.) + +#### ComparisonConfiguration + +Allows to configure the way objects are compared. Sometimes introspection is just not the way to go. Let it be for performance reasons or simply because the object doesn't expose any useful properties. In those cases it's possible to define alternative comparison strategies, like using the equals method, a comparator or even a custom strategy. These settings can be made for specific nodes or entire types. + +#### CategoryConfiguration + +Allows to assign custom categories (or tags) to entire types or selected elements and properties. These categories come in very handy, when combined with the `InclusionConfiguration`. They make it very easy to limit the comparison to a specific subset of the object graph. + +## ObjectDiffer + +The `ObjectDiffer` is created by the `ObjectDifferBuilder`. It takes two objects to compare and eventually returns a `DiffNode` representing the entire object graph. It is thread-safe and can be reused. + +The objects given to the `ObjectDiffer` are called _working_ and _base_, where _working_ is considered to be a modified version of _base_. There is actually no technical reason for this. The `ObjectDiffer` doesn't care how the objects relate to each other. They could actually be two completely different objects that never shared a mutual history. The reason for the naming is simply to make it easier to express changes by using words like "added" and "removed". + +## DiffNode + +`DiffNodes` are used to build a directed graph representing every element (bean properties, collection items, map entries, etc.) of the compared objects. Each `DiffNode` can be queried for information on how the working version differs from the base. + +Every `DiffNode` has exactly one parent and any number of child nodes. The only one that doesn't need or has a parent is the **root node**, which is the one returned by the `ObjectDiffer`. This node simply represents the compared objects themselve. + +`DiffNodes` also provide methods to read and write the elements they represent from and to any object that resembles the type of the compared object. This is useful for reading the values from the working and base object to visualize changes or to merge non-conflicting changes on concurrent database updates. + +Due to the fact that `DiffNodes` form a tree structure, each node has its own unique path. That allows it to easily collect selected information from the object graph. `DiffNodes` can be queried for their `NodePath` and many of the configuration APIs use it to attach metadata or inclusion rules to selected nodes. + +Of course, it is not always possible or desirable to hard-code knowledge about the structure of the object graph into your software. In those cases it is also possible to use visitors to traverse the object graph and collect information about changes programmatically. But more about that in the next chapter. + +## DiffNode.Visitor + +The simplistic `Visitor` inteface allows to implement very powerful logic to transform the `DiffNode` graph into any representation needed. By passing a visitor to the `visit` or `visitChildren` method of a `DiffNode`, one can decide whether it should be exclusively applied to the single node or also recursively to all its children. + +Every encountered `DiffNode` will be passed to the `Visitor` along with a `Visit` object. The diff information and the accessor methods of the node allow the visitor to query and change the values of the base and working object (or any other instance of the same object type) - as long as the visitor has access to those objects. Once the visitor has fulfilled its purpose it can either stop the traversal altogether or just prevent it for the children of the current node by calling `stop()` or `dontGoDeeper()` on the `Visit` object. + +_java-object-diff_ comes with a bunch of visitors that do a variety of things. Be it simply printing the differences between base and working; locating nodes with a specific category or even merging the differences between diffed versions into an even newer head version. However, these visitors merely serve as examples since probably none of them will fit your use-case as good as your own custom implementation could. + +It is **highly encouraged** to do all the processing of the object graph via `Visitors` as it is the most flexible, reliable and maintainable way. + +## NodePath + +Every `DiffNode` in the object graph has its unique path identifier - simply called the `NodePath`. Such a path is composed by a sequence of `Elements`. Each element describes how to transition from one object to the next. For example by getting an entry from a map or an item from a list or just by calling a simple property getter. + +A sequence of these actions describes one exact location in the object graph and therefore serves as the single best way to query specific nodes. There is also one special element that every NodePath needs to start with: the root element. It is used to reference the objects that have been passed to the compare method of the `ObjectDiffer`. + +A `NodePath` can be created by calling one of its numerous static builder methods. + +# Conclusion + +Hopefully this brief overview over the most important classes and concepts of java-object-diff helps you getting the best out of this library. If you think something important is missing or if anything isn't as well explained as you'd hoped, please let me know! \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..71e6ecfd --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version=0.96 diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..5c76105c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..942c6550 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 15 23:14:03 CET 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..4453ccea --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save ( ) { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..c1d0eaea --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,9 @@ +site_name: java-object-diff Documentation +repo_url: https://github.com/SQiShER/java-object-diff +pages: +- Home: index.md +- Using with Maven: maven.md +- Getting Started: getting-started.md +- User Guide: + - User Guide: user-guide.md +- Merging: merging.md diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 26b24e2f..00000000 --- a/pom.xml +++ /dev/null @@ -1,483 +0,0 @@ - - - - - - - org.sonatype.oss - oss-parent - 7 - - - 4.0.0 - de.danielbechler - java-object-diff - Java Object Diff - 0.90.0-SNAPSHOT - - - 2.2.1 - - - - - SQiShER - Daniel Bechler - https://github.com/SQiShER - - - - https://github.com/SQiShER/java-object-diff - Framework to detect and handle differences between Java objects - - - - Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0 - - - - - https://github.com/SQiShER/java-object-diff - scm:git:ssh://git@github.com/SQiShER/java-object-diff.git - scm:git:ssh://git@github.com/SQiShER/java-object-diff.git - - - - GitHub - https://github.com/SQiShER/java-object-diff/issues - - - - UTF-8 - - - - - - maven-compiler-plugin - - 1.5 - 1.5 - UTF-8 - groovy-eclipse-compiler - - - - org.codehaus.groovy - groovy-eclipse-compiler - 2.8.0-01 - - - org.codehaus.groovy - groovy-eclipse-batch - 2.1.5-03 - - - - - maven-javadoc-plugin - - - site - - javadoc - - - - - UTF-8 - - - - maven-release-plugin - - - - - - maven-deploy-plugin - - - org.apache.maven.wagon - wagon-ssh - 2.0 - - - - - maven-surefire-plugin - - - org.apache.maven.surefire - surefire-junit4 - 2.16 - - - org.apache.maven.surefire - surefire-testng - 2.16 - - - - true - true - - **/*Test.java - - **/*Should.java - **/*Spec.java - **/*Specification.java - - - - - org.apache.maven.plugins - maven-failsafe-plugin - 2.16 - - UTF-8 - true - - **/*IT.java - **/*ITCase.java - **/*ITSpec.java - - - - - - integration-test - verify - - - none:none - - - - integration-test-testng - - integration-test - verify - - - none:none - - - - - - org.codehaus.mojo - license-maven-plugin - - - add-third-party - - add-third-party - - - - - - org.apache.maven.plugins - maven-dependency-plugin - - - copy-dependencies - compile - - copy-dependencies - - - - - - org.apache.felix - maven-bundle-plugin - true - - - true - - - Daniel Bechler - ${project.url} - {local-packages} - - - - - bundle-manifest - process-classes - - manifest - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-integration-test-source - generate-test-sources - - add-test-source - - - - src/integration-test/java - - - - - - - org.jvnet - animal-sniffer - 1.2 - - - animal-sniffer - compile - - check - - - - org.jvnet.animal-sniffer - java1.5 - 1.0 - - - - - - - org.jvnet.animal-sniffer - java1.5 - 1.0 - sig - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.6 - - - - - clean - - - - - - - - - - - - - - - - - - - - maven-compiler-plugin - 3.1 - - - maven-javadoc-plugin - 2.9.1 - - - maven-deploy-plugin - 2.8.1 - - - maven-source-plugin - 2.2.1 - - - maven-surefire-plugin - 2.16 - - - maven-resources-plugin - 2.6 - - - maven-release-plugin - 2.4.2 - - - maven-jar-plugin - 2.4 - - - org.apache.felix - maven-bundle-plugin - 2.4.0 - - - org.codehaus.mojo - license-maven-plugin - 1.6 - - - org.codehaus.mojo - build-helper-maven-plugin - 1.8 - - - - - - - - org.slf4j - slf4j-api - 1.7.6 - - - org.testng - testng - 6.8.7 - test - - - junit - junit - - - org.hamcrest - hamcrest-core - - - org.beanshell - bsh - - - org.yaml - snakeyaml - - - - - org.hamcrest - hamcrest-all - 1.3 - test - - - org.mockito - mockito-all - 1.9.5 - test - - - ch.qos.logback - logback-core - 1.1.1 - test - - - ch.qos.logback - logback-classic - 1.1.1 - test - - - org.easytesting - fest-assert-core - 2.0M10 - test - - - org.codehaus.groovy - groovy-all - 2.2.1 - test - - - org.spockframework - spock-core - 0.7-groovy-2.0 - test - - - cglib - cglib-nodep - 3.1 - test - - - - - - - org.apache.maven.plugins - maven-pmd-plugin - 3.0.1 - - true - utf-8 - 100 - 1.5 - - **/*Bean.java - **/generated/*.java - - - target/generated-sources/stubs - - - - - org.apache.maven.plugins - maven-pmd-plugin - 3.0.1 - - true - utf-8 - 100 - 1.7 - - - - - - - - - - - - org.codehaus.mojo - cobertura-maven-plugin - 2.6 - - - maven-javadoc-plugin - - UTF-8 - - - - - diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..b758d796 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'java-object-diff' diff --git a/src/integration-test/java/de/danielbechler/diff/ObjectDifferBuilderIT.groovy b/src/integration-test/java/de/danielbechler/diff/ObjectDifferBuilderIT.groovy new file mode 100644 index 00000000..d26b0ba3 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/ObjectDifferBuilderIT.groovy @@ -0,0 +1,52 @@ +/* + * 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 + +import de.danielbechler.diff.differ.Differ +import de.danielbechler.diff.differ.DifferDispatcher +import de.danielbechler.diff.differ.DifferFactory +import spock.lang.Specification + +/** + * Created by Daniel Bechler. + */ +@SuppressWarnings("GroovyAssignabilityCheck") +class ObjectDifferBuilderIT extends Specification { + def differMock = Mock(Differ) + + def 'Register a DifferExtension'() { + given: + def differFactory = new DifferFactory() { + @Override + Differ createDiffer(DifferDispatcher differDispatcher, NodeQueryService nodeQueryService) { + return differMock + } + } + def objectDiffer = ObjectDifferBuilder.startBuilding() + .differs().register(differFactory) + .build() + + when: + objectDiffer.compare("foo", "bar") + + then: + 1 * differMock.accepts(*_) >> true + + and: + 1 * differMock.compare(*_) + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/ObjectDifferIT.groovy b/src/integration-test/java/de/danielbechler/diff/ObjectDifferIT.groovy new file mode 100644 index 00000000..58da3b65 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/ObjectDifferIT.groovy @@ -0,0 +1,363 @@ +/* + * 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 + +import de.danielbechler.diff.mock.* +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.CollectionItemElementSelector +import de.danielbechler.diff.selector.MapKeyElementSelector +import spock.lang.Ignore +import spock.lang.Specification + +public class ObjectDifferIT extends Specification { + + def objectDiffer = ObjectDifferBuilder.buildDefault() + + def 'compare bean with ignored map property'() { + given: + def working = new ObjectWithIgnoredMap(map: [foo: 'bar']) + def base = new ObjectWithIgnoredMap() + when: + def node = objectDiffer.compare(working, base) + then: + node.untouched == true + node.hasChildren() == false + } + + def 'compare collection with ignored collection property'() { + given: + def working = new ObjectWithCollection(collection: ['foo']) + and: + def objectDiffer = ObjectDifferBuilder.startBuilding() + .inclusion() + .exclude().node(NodePath.with('collection')) + .and() + .build() + when: + def node = objectDiffer.compare(working, new ObjectWithCollection()) + then: + node.untouched + node.childCount() == 0 + } + + def 'compare collection with added item'() { + given: + def working = ['foo'] + def base = [] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.changed + and: + node.getChild(new CollectionItemElementSelector('foo')).added + } + + def 'compare collection with removed item'() { + given: + def working = [] + def base = ['foo'] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.changed + and: + node.getChild(new CollectionItemElementSelector('foo')).removed + } + + def 'ignored properties are never accessed'() { + given: + def working = new ObjectWithAccessTrackingIgnoredProperty() + def base = new ObjectWithAccessTrackingIgnoredProperty() + when: + ObjectDifferBuilder.buildDefault().compare(working, base) + then: + !working.accessed + !base.accessed + } + + def 'object graph for added objects gets returned'() { + given: + def workingChildChild = new ObjectWithNestedObject('foo') + def workingChild = new ObjectWithNestedObject('2', workingChildChild) + def working = new ObjectWithNestedObject('1', workingChild) + def base = new ObjectWithNestedObject('1') + when: + def node = ObjectDifferBuilder.startBuilding().build().compare(working, base) + then: + node.changed + node.getChild('object').added + node.getChild('object').getChild('object').added + } + + def 'compare collection with different collection implementations succeeds'() { + given: + def working = ['one', 'three'] as TreeSet + def base = ['one', 'two'] as LinkedHashSet + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.changed + } + + def 'compare collection with changed item'() { + given: + def working = [new ObjectWithIdentityAndValue("foo", "1")] + def base = [new ObjectWithIdentityAndValue("foo", "2")] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.hasChanges() + and: + node.getChild(new CollectionItemElementSelector(new ObjectWithIdentityAndValue("foo"))).changed + } + + def 'with new map in working and none in base'() { + given: + def working = [foo: 'bar'] as TreeMap + def base = null + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.added + node.getChild(new MapKeyElementSelector('foo')).added + } + + def 'with addition of simple type to working map'() { + given: + def working = [foo: 'bar'] + def base = [:] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.changed + node.getChild(new MapKeyElementSelector('foo')).added + } + + def 'with same entry in base and working'() { + given: + def base = [foo: 'bar'] + def working = [foo: 'bar'] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.untouched + node.childCount() == 0 + } + + def 'with single entry in base and missing working'() { + given: + def base = [foo: 'bar'] + def working = null + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.removed + node.getChild(new MapKeyElementSelector('foo')).removed + } + + /** + * Ensures that the map can handle null values in both, + * the base and the working version, in which case no + * type can be detected. + */ + def 'with all null map item'() { + given: + def working = [foo: null] + def base = [foo: null] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.untouched + } + + def 'with same entries'() { + given: + def working = [foo: 'bar'] + def base = working.clone() + working.put('ping', 'pong') + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.childCount() == 1 + node.getChild(new MapKeyElementSelector('foo')) == null + and: + with(node.getChild(new MapKeyElementSelector('ping'))) { pingNode -> + assert pingNode.added + assert pingNode.childCount() == 0 + } + } + + def 'with changed entry'() { + given: + def working = [foo: 'bar'] + def base = [foo: 'woot'] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.childCount() == 1 + node.getChild(new MapKeyElementSelector("foo")).changed + } + + def 'compare with different map implementations succeeds'() { + given: + def base = [test: 'foo'] as LinkedHashMap + def working = [test: 'bar'] as TreeMap + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.changed + } + + def 'compare with different strings'() { + expect: + ObjectDifferBuilder.buildDefault().compare('foo', 'bar').changed + } + + def 'compare with different types'() { + expect: + ObjectDifferBuilder.buildDefault().compare('foo', 1337).changed + } + + def 'compare with ignored property'() { + given: + def working = new ObjectWithIdentityAndValue('1', 'foo') + def base = new ObjectWithIdentityAndValue('1', 'bar') + when: + def objectDiffer = ObjectDifferBuilder.startBuilding() + .inclusion() + .exclude().node(NodePath.with('value')).and() + .filtering().returnNodesWithState(DiffNode.State.IGNORED).and() + .build() + def node = objectDiffer.compare(working, base) + then: + node.getChild('value').state == DiffNode.State.IGNORED + } + + def 'compare with complex type'() { + given: + def working = new ObjectWithIdentityAndValue('a', '1') + def base = new ObjectWithIdentityAndValue('a', '2') + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.changed + } + + @Ignore('Currently this is simply not possible because of the way, the CollectionItemAccessor works. Would be great, to support this.') + def 'compare with list containing object twice detects if one gets removed'() { + given: + def base = [ + new ObjectWithHashCodeAndEquals('foo'), + new ObjectWithHashCodeAndEquals('foo') + ] + def working = [ + new ObjectWithHashCodeAndEquals('foo') + ] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.getChild(new ObjectWithHashCodeAndEquals('foo')).removed + } + + @Ignore('Currently this is simply not possible because of the way, the CollectionItemAccessor works. Would be great, to support this.') + def 'compare with list containing object once detects if another instance of it gets added'() { + given: + def base = [ + new ObjectWithHashCodeAndEquals('foo'), + ] + def working = [ + new ObjectWithHashCodeAndEquals('foo'), + new ObjectWithHashCodeAndEquals('foo') + ] + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.getChild(new ObjectWithHashCodeAndEquals('foo')).added + } + + def 'compare bean with equals only value provider method on get collection property no change in method result'() { + given: + def working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(['one']) + def base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(['uno']) + when: + def node = objectDiffer.compare(working, base); + then: + node.untouched + node.childCount() == 0 + } + + def 'compare bean with equal only value provider method on get collection property with change in method result'() { + given: + final working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(['one', 'two']) + final base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(['uno']) + when: + final node = objectDiffer.compare(working, base) + then: + node.changed + node.getChild('collection').changed + } + + def 'compare bean with equals only value provider method on get object property no change in method result'() { + given: + def working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject("id", new ObjectWithNestedObject('childid', new ObjectWithNestedObject("grandchildid"))); + def base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject("id", new ObjectWithNestedObject('childid', new ObjectWithNestedObject("differentgrandchildid"))); + when: + final DiffNode node = objectDiffer.compare(working, base) + then: + node.untouched + node.childCount() == 0 + } + + def 'compare bean with equals only value provider method on get object property with change in method result'() { + given: + def working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject('id', + new ObjectWithNestedObject('childid', + new ObjectWithNestedObject('grandchildid'))) + and: + def base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject('id', + new ObjectWithNestedObject('differentchildid', + new ObjectWithNestedObject('differentgrandchildid'))) + when: + def node = objectDiffer.compare(working, base) + then: + node.changed + node.getChild('object').changed + } + + def 'compare bean with equals only value provider method on get map property no change in method result'() { + given: + final working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap([key1: 'val1']) + final base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap([keyone: 'valone']) + when: + final node = objectDiffer.compare(working, base) + then: + node.untouched + node.childCount() == 0 + } + + def 'compare bean with equals only value provider method on get map property with change in method result'() { + given: + final working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap([key1: 'val1', key2: 'val2']) + final base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap([keyone: 'valone']) + when: + final node = objectDiffer.compare(working, base) + then: + node.changed + node.getChild('map').changed + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/ObjectDifferITCase.java b/src/integration-test/java/de/danielbechler/diff/ObjectDifferITCase.java deleted file mode 100644 index 27e58431..00000000 --- a/src/integration-test/java/de/danielbechler/diff/ObjectDifferITCase.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.diff.collection.*; -import de.danielbechler.diff.helper.*; -import de.danielbechler.diff.map.*; -import de.danielbechler.diff.mock.*; -import de.danielbechler.diff.visitor.*; -import org.fest.assertions.api.*; -import org.testng.annotations.*; - -import java.util.ArrayList; -import java.util.*; - -import static de.danielbechler.diff.helper.NodeAssertions.*; -import static de.danielbechler.diff.helper.TestGroups.*; -import static java.util.Arrays.*; - -/** @author Daniel Bechler */ -@Test(groups = TestGroups.INTEGRATION) -public class ObjectDifferITCase -{ - private ObjectDiffer objectDiffer; - - @BeforeMethod - public void setUp() throws Exception - { - objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); - } - - public void testCompareBeanWithIgnoredMapProperty() - { - final ObjectWithIgnoredMap working = new ObjectWithIgnoredMap(); - working.getMap().put("foo", "bar"); - - final DiffNode node = objectDiffer.compare(working, new ObjectWithIgnoredMap()); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - NodeAssertions.assertThat(node).self().hasNoChildren(); - } - - public void testCompareCollectionWithIgnoredCollectionProperty() - { - final ObjectWithCollection working = new ObjectWithCollection(); - working.getCollection().add("foo"); - final ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding(); - objectDifferBuilder.configure().inclusion() - .toExclude() - .nodes(NodePath.buildWith("collection")); - objectDiffer = objectDifferBuilder.build(); - - final DiffNode node = objectDiffer.compare(working, new ObjectWithCollection()); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - NodeAssertions.assertThat(node).self().hasNoChildren(); - } - - public void testCompareCollectionWithAddedItem() throws Exception - { - final Collection working = new LinkedList(asList("foo")); - final Collection base = new LinkedList(); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - NodeAssertions.assertThat(node).child(new CollectionElement("foo")).hasState(DiffNode.State.ADDED); - } - - public void testCompareCollectionWithRemovedItem() throws Exception - { - final Collection working = new LinkedList(); - final Collection base = new LinkedList(asList("foo")); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - NodeAssertions.assertThat(node).child(new CollectionElement("foo")).hasState(DiffNode.State.REMOVED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testThatIgnoredPropertiesAreNeverAccessed() - { - final ObjectWithAccessTrackingIgnoredProperty working = new ObjectWithAccessTrackingIgnoredProperty(); - final ObjectWithAccessTrackingIgnoredProperty base = new ObjectWithAccessTrackingIgnoredProperty(); - - ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - Assertions.assertThat(working.accessed).isFalse(); - Assertions.assertThat(base.accessed).isFalse(); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testThatObjectGraphForAddedObjectsGetsReturned() - { - final ObjectWithNestedObject base = new ObjectWithNestedObject("1"); - final ObjectWithNestedObject working = new ObjectWithNestedObject("1", new ObjectWithNestedObject("2", new ObjectWithNestedObject("foo"))); - final ObjectDifferBuilder configuration = ObjectDifferBuilder.startBuilding(); - // final Configuration2 configuration = new Configuration2().withChildrenOfAddedNodes(); - - final DiffNode node = configuration.build().compare(working, base); - - node.visit(new NodeHierarchyVisitor()); - NodeAssertions.assertThat(node).root().hasState(DiffNode.State.CHANGED); - NodeAssertions.assertThat(node).child("object").hasState(DiffNode.State.ADDED); - NodeAssertions.assertThat(node).child("object", "object").hasState(DiffNode.State.ADDED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testCompareCollectionWithDifferentCollectionImplementationsSucceeds() throws Exception - { - final Collection base = new LinkedHashSet(asList("one", "two")); - final Collection working = new TreeSet(asList("one", "three")); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(base, working); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testCompareCollectionWithChangedItem() throws Exception - { - final List working = asList(new ObjectWithIdentityAndValue("foo", "1")); - final List base = asList(new ObjectWithIdentityAndValue("foo", "2")); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - assertThat(node).self().hasChanges(); - assertThat(node).child(new CollectionElement(new ObjectWithIdentityAndValue("foo"))) - .hasState(DiffNode.State.CHANGED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testWithNewMapInWorkingAndNoneInBase() - { - final Map base = null; - final Map working = new TreeMap(); - working.put("foo", "bar"); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.ADDED); - NodeAssertions.assertThat(node).child(new MapElement("foo")).hasState(DiffNode.State.ADDED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testWithAdditionOfSimpleTypeToWorkingMap() - { - final Map base = new TreeMap(); - final Map working = new TreeMap(); - working.put("foo", "bar"); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - NodeAssertions.assertThat(node).child(new MapElement("foo")).hasState(DiffNode.State.ADDED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testWithSameEntryInBaseAndWorking() - { - final Map base = new TreeMap(); - base.put("foo", "bar"); - final Map working = new TreeMap(); - working.put("foo", "bar"); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - NodeAssertions.assertThat(node).self().hasNoChildren(); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testWithSingleEntryAddedToWorkingMap() - { - final Map base = new TreeMap(); - base.put("foo", "bar"); - final Map working = null; - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.REMOVED); - NodeAssertions.assertThat(node).child(new MapElement("foo")).hasState(DiffNode.State.REMOVED); - } - - /** - * Ensures that the map can handle null values in both, the base and the working version, in which case no - * type can be detected. - */ - @Test(groups = TestGroups.INTEGRATION) - public void testWithAllNullMapItem() - { - final Map working = new HashMap(1); - working.put("foo", null); - - final Map base = new HashMap(1); - base.put("foo", null); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testWithSameEntries() - { - final Map modified = new LinkedHashMap(1); - modified.put("foo", "bar"); - final Map base = new LinkedHashMap(modified); - modified.put("ping", "pong"); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(modified, base); - - NodeAssertions.assertThat(node).root().hasChildren(1); - NodeAssertions.assertThat(node) - .child(NodePath.createBuilder().withRoot().withMapKey("foo")) - .doesNotExist(); - NodeAssertions.assertThat(node).child(NodePath.createBuilder().withRoot().withMapKey("ping")) - .hasState(DiffNode.State.ADDED) - .hasNoChildren(); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testWithChangedEntry() - { - final Map working = new LinkedHashMap(1); - working.put("foo", "bar"); - - final Map base = new LinkedHashMap(1); - base.put("foo", "woot"); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - NodeAssertions.assertThat(node).self().hasChildren(1); - NodeAssertions.assertThat(node).child(new MapElement("foo")).hasState(DiffNode.State.CHANGED); - } - - @Test(groups = TestGroups.INTEGRATION) - public void testCompareWithDifferentMapImplementationsSucceeds() throws Exception - { - final Map base = new LinkedHashMap(Collections.singletonMap("test", "foo")); - final Map working = new TreeMap(Collections.singletonMap("test", "bar")); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - } - - @Test(groups = INTEGRATION) - public void testCompareWithDifferentStrings() throws Exception - { - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare("foo", "bar"); - - assertThat(node).self().hasState(DiffNode.State.CHANGED); - } - - @Test(expectedExceptions = IllegalArgumentException.class, groups = INTEGRATION) - public void testCompareWithDifferentTypes() - { - ObjectDifferBuilder.buildDefaultObjectDiffer().compare("foo", 1337); - } - - @Test(groups = INTEGRATION) - public void testCompareWithIgnoredProperty() - { - final ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding(); - objectDifferBuilder.configure().inclusion().toExclude().nodes(NodePath.buildRootPath()); - objectDiffer = objectDifferBuilder.build(); - - final DiffNode node = objectDiffer.compare("foo", "bar"); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.IGNORED); - } - - @Test(groups = INTEGRATION) - public void testCompareWithComplexType() - { -// when(introspector.introspect(any(Class.class))).thenReturn(Arrays.asList(accessor)); -// when(delegate.delegate(any(Node.class), any(Instances.class))).thenReturn(node); -// when(configuration.isIntrospectible(any(Node.class))).thenReturn(true); -// when(configuration.isReturnable(any(Node.class))).thenReturn(true); -// when(node.hasChanges()).thenReturn(true); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare( - new ObjectWithIdentityAndValue("a", "1"), - new ObjectWithIdentityAndValue("a", "2")); - - assertThat(node).self().hasState(DiffNode.State.CHANGED); - } - - @Test(enabled = false, description = "Currently this is simply not possible because of the way, the CollectionItemAccessor works. Would be great, to support this.") - public void testCompareWithListContainingObjectTwiceDetectsIfOneGetsRemoved() throws Exception - { - final List base = asList(new ObjectWithHashCodeAndEquals("foo"), new ObjectWithHashCodeAndEquals("foo")); - final List working = asList(new ObjectWithHashCodeAndEquals("foo")); - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - node.visit(new NodeHierarchyVisitor()); - assertThat(node) - .child(NodePath.createBuilder() - .withRoot() - .withCollectionItem(new ObjectWithHashCodeAndEquals("foo")) - .build()) - .hasState(DiffNode.State.REMOVED); - } - - @Test(enabled = false, description = "Currently this is simply not possible because of the way, the CollectionItemAccessor works. Would be great, to support this.") - public void testCompareWithListContainingObjectOnceDetectsIfAnotherInstanceOfItGetsAdded() throws Exception - { - final List base = asList(new ObjectWithHashCodeAndEquals("foo")); - final List working = asList(new ObjectWithHashCodeAndEquals("foo"), new ObjectWithHashCodeAndEquals("foo")); - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - node.visit(new NodeHierarchyVisitor()); - assertThat(node).child(NodePath.createBuilder() - .withRoot() - .withCollectionItem(new ObjectWithHashCodeAndEquals("foo")) - .build()).hasState(DiffNode.State.ADDED); - } - - public void testCompareBeanWithEqualsOnlyValueProviderMethodOnGetCollectionPropertyNoChangeInMethodResult() - { - final List forWorking = new ArrayList(); - forWorking.add("one"); - final List forBase = new ArrayList(); - forBase.add("uno"); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(forWorking); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(forBase); - - final DiffNode node = objectDiffer.compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - NodeAssertions.assertThat(node).self().hasNoChildren(); - } - - public void testCompareBeanWithEqualOnlyValueProviderMethodOnGetCollectionPropertyWithChangeInMethodResult() - { - final List forWorking = new ArrayList(); - forWorking.add("one"); - forWorking.add("two"); - final List forBase = new ArrayList(); - forBase.add("uno"); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(forWorking); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection(forBase); - - final DiffNode node = objectDiffer.compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - assertThat(node) - .child(NodePath.buildWith("collection")) - .hasState(DiffNode.State.CHANGED); - } - - public void testCompareBeanWithEqualsOnlyValueProviderMethodOnGetObjectPropertyNoChangeInMethodResult() - { - final ObjectWithNestedObject forWorking = new ObjectWithNestedObject("childid"); - forWorking.setObject(new ObjectWithNestedObject("grandchildid")); - final ObjectWithNestedObject forBase = new ObjectWithNestedObject("childid"); - forBase.setObject(new ObjectWithNestedObject("differentgrandchildid")); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject("id", forWorking); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject("id", forBase); - - final DiffNode node = objectDiffer.compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - NodeAssertions.assertThat(node).self().hasNoChildren(); - } - - public void testCompareBeanWithEqualsOnlyValueProviderMethodOnGetObjectPropertyWithChangeInMethodResult() - { - final ObjectWithNestedObject forWorking = new ObjectWithNestedObject("childid"); - forWorking.setObject(new ObjectWithNestedObject("grandchildid")); - final ObjectWithNestedObject forBase = new ObjectWithNestedObject("differentchildid"); - forBase.setObject(new ObjectWithNestedObject("differentgrandchildid")); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject("id", forWorking); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject("id", forBase); - - final DiffNode node = objectDiffer.compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - assertThat(node) - .child(NodePath.buildWith("object")) - .hasState(DiffNode.State.CHANGED); - } - - public void testCompareBeanWithEqualsOnlyValueProviderMethodOnGetMapPropertyNoChangeInMethodResult() - { - final Map forWorking = new HashMap(); - forWorking.put("key1", "val1"); - final Map forBase = new HashMap(); - forBase.put("keyone", "valone"); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap(forWorking); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap(forBase); - - final DiffNode node = objectDiffer.compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.UNTOUCHED); - NodeAssertions.assertThat(node).self().hasNoChildren(); - } - - public void testCompareBeanWithEqualsOnlyValueProviderMethodOnGetMapPropertyWithChangeInMethodResult() - { - final Map forWorking = new HashMap(); - forWorking.put("key1", "val1"); - forWorking.put("key2", "val2"); - final Map forBase = new HashMap(); - forBase.put("keyone", "valone"); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap working = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap(forWorking); - final ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap base = new ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap(forBase); - - final DiffNode node = objectDiffer.compare(working, base); - - NodeAssertions.assertThat(node).self().hasState(DiffNode.State.CHANGED); - assertThat(node) - .child(NodePath.buildWith("map")) - .hasState(DiffNode.State.CHANGED); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/category/CategoriesTestIT.groovy b/src/integration-test/java/de/danielbechler/diff/category/CategoriesTestIT.groovy new file mode 100644 index 00000000..aaa463e2 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/category/CategoriesTestIT.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2016 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.category + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.introspection.ObjectDiffProperty +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +class CategoriesTestIT extends Specification { + + def "should return all categories"() { + setup: + def obj1 = new MyObject("aaa", "aaa") + def obj2 = new MyObject("bbb", "bbb") + def differ = ObjectDifferBuilder.startBuilding() + .categories() + .ofNode(NodePath.with("firstString")).toBe("cat1") + .ofNode(NodePath.with("secondString")).toBe("cat1") + .and() + .build() + def node = differ.compare(obj1, obj2) + + expect: + node.getChild("firstString").getCategories() == ["cat1"] as Set + node.getChild("secondString").getCategories() == ["cat1", "catAnnotation"] as Set + } + + @SuppressWarnings("GroovyUnusedDeclaration") + class MyObject { + def firstString + def secondString + + MyObject(firstString, secondString) { + this.firstString = firstString + this.secondString = secondString + } + + @ObjectDiffProperty(categories = ["catAnnotation"]) + def getSecondString() { + return secondString + } + + void setSecondString(secondString) { + this.secondString = secondString + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/comparison/DateComparisonIT.groovy b/src/integration-test/java/de/danielbechler/diff/comparison/DateComparisonIT.groovy new file mode 100644 index 00000000..4672d626 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/comparison/DateComparisonIT.groovy @@ -0,0 +1,50 @@ +/* + * 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.comparison + +import de.danielbechler.diff.ObjectDifferBuilder +import spock.lang.Specification +import spock.lang.Unroll + +import java.sql.Time +import java.sql.Timestamp + +class DateComparisonIT extends Specification { + + @Unroll + def 'should consider java.util.Date and #subclass equal when they represent the same date'() { + def now = new Timestamp(System.currentTimeMillis()).time + + given: + def utilDate = new Date(now) + def sqlDate = subclass.newInstance([now] as Object[]) + expect: + ObjectDifferBuilder.buildDefault().compare(utilDate, sqlDate).untouched + where: + subclass << [java.sql.Date, Time, Timestamp] + } + + def 'should consider java.sql.Time and java.util.Date equal when they represent the same date'() { + def now = System.currentTimeMillis() + + given: + def utilDate = new Date(now) + def time = new Time(now) + expect: + ObjectDifferBuilder.buildDefault().compare(utilDate, time).untouched + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/ByteArrayDiffer.java b/src/integration-test/java/de/danielbechler/diff/example/ByteArrayDiffer.java new file mode 100644 index 00000000..50f3ab7b --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/example/ByteArrayDiffer.java @@ -0,0 +1,66 @@ +/* + * 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.example; + +import de.danielbechler.diff.NodeQueryService; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.differ.Differ; +import de.danielbechler.diff.differ.DifferDispatcher; +import de.danielbechler.diff.differ.DifferFactory; +import de.danielbechler.diff.node.DiffNode; + +import java.util.Arrays; + +public class ByteArrayDiffer implements Differ +{ + public boolean accepts(final Class type) + { + return type == byte[].class; + } + + public DiffNode compare(final DiffNode parentNode, final Instances instances) + { + final DiffNode node = new DiffNode(parentNode, instances.getSourceAccessor()); + if (instances.hasBeenAdded()) + { + node.setState(DiffNode.State.ADDED); + } + else if (instances.hasBeenRemoved()) + { + node.setState(DiffNode.State.REMOVED); + } + else + { + final byte[] baseValue = instances.getBase(byte[].class); + final byte[] workingValue = instances.getWorking(byte[].class); + if (!Arrays.equals(baseValue, workingValue)) + { + node.setState(DiffNode.State.CHANGED); + } + } + return node; + } + + public static class Factory implements DifferFactory + { + public Differ createDiffer(final DifferDispatcher differDispatcher, + final NodeQueryService nodeQueryService) + { + return new ByteArrayDiffer(); + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/CanonicalAccessorExample.java b/src/integration-test/java/de/danielbechler/diff/example/CanonicalAccessorExample.java index 62ee2b03..e5842163 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/CanonicalAccessorExample.java +++ b/src/integration-test/java/de/danielbechler/diff/example/CanonicalAccessorExample.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,14 +16,19 @@ package de.danielbechler.diff.example; -import de.danielbechler.diff.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.NodeHierarchyVisitor; +import de.danielbechler.diff.selector.CollectionItemElementSelector; -import java.util.*; +import java.util.LinkedList; +import java.util.List; -import static java.util.Arrays.*; +import static java.util.Arrays.asList; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ class CanonicalAccessorExample { private CanonicalAccessorExample() @@ -41,11 +46,11 @@ public static void main(final String[] args) final Contact baseContact = new Contact("Walter White"); baseAddressBook.setContacts(asList(baseContact)); - final DiffNode rootNode = ObjectDifferBuilder.buildDefaultObjectDiffer() - .compare(workingAddressBook, baseAddressBook); - final DiffNode contactsNode = getFirstChildOf(rootNode); - final DiffNode contactNode = getFirstChildOf(contactsNode); - final DiffNode nicknameNode = getFirstChildOf(contactNode); + final DiffNode rootNode = ObjectDifferBuilder.buildDefault() + .compare(workingAddressBook, baseAddressBook); + final DiffNode contactsNode = rootNode.getChild("contacts"); + final DiffNode contactNode = contactsNode.getChild(new CollectionItemElementSelector(baseContact)); + final DiffNode nicknameNode = contactNode.getChild("nickname"); rootNode.visit(new NodeHierarchyVisitor()); @@ -70,11 +75,6 @@ public static void main(final String[] args) assert nicknameNode.get(contact) == nicknameNode.canonicalGet(workingAddressBook); } - private static DiffNode getFirstChildOf(final DiffNode rootNode) - { - return rootNode.getChildren().iterator().next(); - } - public static class AddressBook { private List contacts = new LinkedList(); diff --git a/src/integration-test/java/de/danielbechler/diff/example/CustomDifferExampleIT.groovy b/src/integration-test/java/de/danielbechler/diff/example/CustomDifferExampleIT.groovy new file mode 100644 index 00000000..7a5bfee7 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/example/CustomDifferExampleIT.groovy @@ -0,0 +1,49 @@ +/* + * 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.example + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import spock.lang.Specification + +import java.nio.charset.Charset + +class CustomDifferExampleIT extends Specification { + + class User { + int id + String name + byte[] avatar + } + + def 'equals-only byte array diff via custom Differ'() { + given: + def originalUser = new User(id: 1, name: 'Foo', avatar: 'Test'.getBytes(Charset.forName('utf-8'))) + def updatedUser = new User(id: 1, name: 'Foo', avatar: 'New Avatar'.getBytes(Charset.forName('utf-8'))) + ObjectDiffer objectDiffer = ObjectDifferBuilder.startBuilding() + .differs().register(new ByteArrayDiffer.Factory()) + .build() + def node = objectDiffer.compare(updatedUser, originalUser) + expect: + node.getChild('avatar').changed + node.getChild('avatar').hasChildren() == false + when: + node.getChild('avatar').set(originalUser, 'Even newer avatar'.getBytes(Charset.forName('utf-8'))) + then: + node.getChild('avatar').get(originalUser) == 'Even newer avatar'.getBytes(Charset.forName('utf-8')) + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/EqualsOnlyValueProviderMethodExample.java b/src/integration-test/java/de/danielbechler/diff/example/EqualsOnlyValueProviderMethodExample.java index c8cff95a..4014b802 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/EqualsOnlyValueProviderMethodExample.java +++ b/src/integration-test/java/de/danielbechler/diff/example/EqualsOnlyValueProviderMethodExample.java @@ -1,7 +1,25 @@ +/* + * 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.example; -import de.danielbechler.diff.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.PrintingVisitor; +import de.danielbechler.diff.path.NodePath; class EqualsOnlyValueProviderMethodExample { @@ -19,9 +37,9 @@ public static void main(final String[] args) final ObjectDifferBuilder builder = ObjectDifferBuilder.startBuilding(); // (Option 1) Causes the ObjectDiffer to compare using the method "getProp1" on the 'prop' property of the root object - builder.configure().comparison() - .ofNode(NodePath.buildWith("prop")) - .toUseEqualsMethodOfValueProvidedByMethod("getProp1"); + builder.comparison() + .ofNode(NodePath.with("prop")) + .toUseEqualsMethodOfValueProvidedByMethod("getProp1"); final DiffNode node = builder.build().compare(working, base); diff --git a/src/integration-test/java/de/danielbechler/diff/example/Example1IT.groovy b/src/integration-test/java/de/danielbechler/diff/example/Example1IT.groovy new file mode 100644 index 00000000..7e08bce1 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/example/Example1IT.groovy @@ -0,0 +1,66 @@ +/* + * 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.example + +import de.danielbechler.diff.ObjectDifferBuilder +import spock.lang.Specification + +import static de.danielbechler.diff.node.DiffNode.State.CHANGED +import static de.danielbechler.diff.node.DiffNode.State.UNTOUCHED + +public class Example1IT extends Specification { + def "Comparing Objects via compareTo instead of equals"() { + given: "an object differ configured to compare the given type via compareTo method" + def builder = ObjectDifferBuilder.startBuilding() + builder.comparison().ofType(ComparableObject).toUseCompareToMethod() + def objectDiffer = builder.build() + + expect: + objectDiffer.compare(working, base).state == expectedState + + where: + base | working || expectedState + new ComparableObject("foo", 1) | new ComparableObject("foo", 2) || UNTOUCHED + new ComparableObject("foo", 1) | new ComparableObject("bar", 1) || CHANGED + } + + public static class ComparableObject implements Comparable { + private final String value + private final int index + + public ComparableObject(String value, int index) { + this.value = value + this.index = index + } + + public String getValue() { + return value + } + + public int compareTo(ComparableObject o) { + return value.compareToIgnoreCase(o.value) + } + + public boolean equals(Object o) { + return false + } + + public int hashCode() { + return 0 + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/Example1ITSpec.groovy b/src/integration-test/java/de/danielbechler/diff/example/Example1ITSpec.groovy deleted file mode 100644 index aa0fb62d..00000000 --- a/src/integration-test/java/de/danielbechler/diff/example/Example1ITSpec.groovy +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2013 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.example - -import de.danielbechler.diff.ObjectDifferBuilder -import spock.lang.Specification - -import static de.danielbechler.diff.DiffNode.State.CHANGED -import static de.danielbechler.diff.DiffNode.State.UNTOUCHED - -public class Example1ITSpec extends Specification -{ - def "Comparing Objects via compareTo instead of equals"() - { - given: "an object differ configured to compare the given type via compareTo method" - def builder = ObjectDifferBuilder.startBuilding() - builder.configure().comparison().ofType(ComparableObject).toUseCompareToMethod() - def objectDiffer = builder.build() - - expect: - objectDiffer.compare(working, base).state == expectedState - - where: - base | working || expectedState - new ComparableObject("foo", 1) | new ComparableObject("foo", 2) || UNTOUCHED - new ComparableObject("foo", 1) | new ComparableObject("bar", 1) || CHANGED - } - - public static class ComparableObject implements Comparable - { - private final String value - private final int index - - public ComparableObject(String value, int index) - { - this.value = value - this.index = index - } - - public String getValue() - { - return value - } - - public int compareTo(ComparableObject o) - { - return value.compareToIgnoreCase(o.value) - } - - public boolean equals(Object o) - { - return false - } - - public int hashCode() - { - return 0 - } - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/example/Example2IT.groovy b/src/integration-test/java/de/danielbechler/diff/example/Example2IT.groovy new file mode 100644 index 00000000..319f9e27 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/example/Example2IT.groovy @@ -0,0 +1,114 @@ +/* + * 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.example + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.introspection.ObjectDiffEqualsOnlyType +import de.danielbechler.diff.introspection.ObjectDiffProperty +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +import static de.danielbechler.diff.node.DiffNode.State.UNTOUCHED + +class Example2IT extends Specification { + def "Comparing an object via equalsOnlyValueProvider (via property annotation)"() { + given: + def base = new Company() + base.manager = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") + + and: + def working = new Company() + working.manager = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") + + expect: + ObjectDifferBuilder.buildDefault().compare(working, base).state == UNTOUCHED + } + + def "Comparing an object via equalsOnlyValueProvider (via type annotation)"() { + given: + def base = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") + def working = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") + + expect: + ObjectDifferBuilder.buildDefault().compare(working, base).state == UNTOUCHED + } + + def "Comparing an object via equalsOnlyValueProvider (via configuration using property path)"() { + given: + def builder = ObjectDifferBuilder.startBuilding() + builder.comparison().ofNode(NodePath.withRoot()).toUseEqualsMethodOfValueProvidedByMethod("presentationForDiffer") + + def base = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") + def working = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") + + expect: + builder.build().compare(working, base).state == UNTOUCHED + } + + def "Comparing an object via equalsOnlyValueProvider (via configuration using type)"() { + given: + def builder = ObjectDifferBuilder.startBuilding() + builder.comparison().ofType(Person).toUseEqualsMethodOfValueProvidedByMethod("presentationForDiffer") + def base = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") + def working = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") + + expect: + builder.build().compare(working, base).state == UNTOUCHED + } + + + public static class Company { + private Person manager; + + @ObjectDiffProperty(equalsOnly = true, equalsOnlyValueProviderMethod = "presentationForDiffer") + public Person getManager() { + return manager + } + + void setManager(final Person manager) { + this.manager = manager + } + } + + public static class Person { + private String firstname + private String lastname + private String nickname + + public Person(String firstname, String nickname, String lastname) { + this.firstname = firstname + this.nickname = nickname + this.lastname = lastname + } + + public String presentationForDiffer() { + return firstname + " " + lastname + } + + boolean equals(final o) { + return false + } + } + + @ObjectDiffEqualsOnlyType(valueProviderMethod = "presentationForDiffer") + public static class PersonWithEqualsOnlyTypeAnnotation extends Person { + public PersonWithEqualsOnlyTypeAnnotation( + final String firstname, final String nickname, final String lastname) { + super(firstname, nickname, lastname) + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/Example2ITSpec.groovy b/src/integration-test/java/de/danielbechler/diff/example/Example2ITSpec.groovy deleted file mode 100644 index c1a8567a..00000000 --- a/src/integration-test/java/de/danielbechler/diff/example/Example2ITSpec.groovy +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2013 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.example - -import de.danielbechler.diff.NodePath -import de.danielbechler.diff.ObjectDifferBuilder -import de.danielbechler.diff.annotation.ObjectDiffEqualsOnlyType -import de.danielbechler.diff.annotation.ObjectDiffProperty -import spock.lang.Specification - -import static de.danielbechler.diff.DiffNode.State.UNTOUCHED - -class Example2ITSpec extends Specification -{ - def "Comparing an object via equalsOnlyValueProvider (via property annotation)"() - { - given: - def base = new Company() - base.manager = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") - - and: - def working = new Company() - working.manager = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") - - expect: - ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base).state == UNTOUCHED - } - - def "Comparing an object via equalsOnlyValueProvider (via type annotation)"() - { - given: - def base = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") - def working = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") - - expect: - ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base).state == UNTOUCHED - } - - def "Comparing an object via equalsOnlyValueProvider (via configuration using property path)"() - { - given: - def builder = ObjectDifferBuilder.startBuilding() - builder.configure().comparison().ofNode(NodePath.buildRootPath()).toUseEqualsMethodOfValueProvidedByMethod("presentationForDiffer") - - def base = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") - def working = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") - - expect: - builder.build().compare(working, base).state == UNTOUCHED - } - - def "Comparing an object via equalsOnlyValueProvider (via configuration using type)"() - { - given: - def builder = ObjectDifferBuilder.startBuilding() - builder.configure().comparison().ofType(Person).toUseEqualsMethodOfValueProvidedByMethod("presentationForDiffer") - def base = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Walt", "White") - def working = new PersonWithEqualsOnlyTypeAnnotation("Walter", "Heisenberg", "White") - - expect: - builder.build().compare(working, base).state == UNTOUCHED - } - - - public static class Company - { - private Person manager; - - @ObjectDiffProperty(equalsOnly = true, equalsOnlyValueProviderMethod = "presentationForDiffer") - public Person getManager() - { - return manager - } - - void setManager(final Person manager) - { - this.manager = manager - } - } - - public static class Person - { - private String firstname - private String lastname - private String nickname - - public Person(String firstname, String nickname, String lastname) - { - this.firstname = firstname - this.nickname = nickname - this.lastname = lastname - } - - public String presentationForDiffer() - { - return firstname + " " + lastname - } - - boolean equals(final o) - { - return false - } - } - - @ObjectDiffEqualsOnlyType(valueProviderMethod = "presentationForDiffer") - public static class PersonWithEqualsOnlyTypeAnnotation extends Person - { - public PersonWithEqualsOnlyTypeAnnotation(final String firstname, final String nickname, final String lastname) - { - super(firstname, nickname, lastname) - } - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/example/IgnoreExample.java b/src/integration-test/java/de/danielbechler/diff/example/IgnoreExample.java index 85db2c3d..7d0c13a9 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/IgnoreExample.java +++ b/src/integration-test/java/de/danielbechler/diff/example/IgnoreExample.java @@ -1,10 +1,30 @@ +/* + * 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.example; -import de.danielbechler.diff.*; -import de.danielbechler.diff.annotation.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.introspection.ObjectDiffProperty; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.PrintingVisitor; +import de.danielbechler.diff.path.NodePath; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @SuppressWarnings("UnusedDeclaration") class IgnoreExample { @@ -20,7 +40,7 @@ public static void main(final String[] args) final ObjectDifferBuilder builder = ObjectDifferBuilder.startBuilding(); // (Option 1) Causes the ObjectDiffer to ignore the 'password' property of the root object - builder.configure().inclusion().toExclude().nodes(NodePath.buildWith("password")); + builder.inclusion().exclude().node(NodePath.with("password")); final DiffNode node = builder.build().compare(working, base); diff --git a/src/integration-test/java/de/danielbechler/diff/example/MapEntryValueAccessExample.java b/src/integration-test/java/de/danielbechler/diff/example/MapEntryValueAccessExample.java index 2f725d34..f1ad85f0 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/MapEntryValueAccessExample.java +++ b/src/integration-test/java/de/danielbechler/diff/example/MapEntryValueAccessExample.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,13 +16,17 @@ package de.danielbechler.diff.example; -import de.danielbechler.diff.*; -import de.danielbechler.diff.map.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.Visit; +import de.danielbechler.diff.selector.MapKeyElementSelector; -import java.util.*; +import java.util.Map; +import java.util.TreeMap; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ class MapEntryValueAccessExample { private MapEntryValueAccessExample() @@ -40,12 +44,12 @@ public static void main(final String[] args) working.put(23, "Shephard"); working.put(42, "Kwon"); - final DiffNode mapNode = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); + final DiffNode mapNode = ObjectDifferBuilder.buildDefault().compare(working, base); mapNode.visitChildren(new DiffNode.Visitor() { - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { - final Object key = ((MapElement) node.getPathElement()).getKey(); + final Object key = ((MapKeyElementSelector) node.getElementSelector()).getKey(); // ^^^ I do not encourage this, but currently it's the only way final Object value = node.canonicalGet(working); System.out.println(key + " => " + value); diff --git a/src/integration-test/java/de/danielbechler/diff/example/RuntimeTypeResolutionExample.java b/src/integration-test/java/de/danielbechler/diff/example/RuntimeTypeResolutionExample.java index 922ba04d..c327bb37 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/RuntimeTypeResolutionExample.java +++ b/src/integration-test/java/de/danielbechler/diff/example/RuntimeTypeResolutionExample.java @@ -16,9 +16,12 @@ package de.danielbechler.diff.example; -import de.danielbechler.diff.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class RuntimeTypeResolutionExample { private RuntimeTypeResolutionExample() @@ -82,7 +85,7 @@ public static void main(final String[] args) { final Point base = new Point(new Coordinate3D(1, 2, 3)); final Point working = new Point(new Coordinate3D(1, 2, 30)); - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); + final DiffNode node = ObjectDifferBuilder.buildDefault().compare(working, base); assert node.getChild("coordinate").getChild("z").isChanged() : "The changed 'z' coordinate should have been detected because the property type should be resolved to Coordinate3D at runtime."; diff --git a/src/integration-test/java/de/danielbechler/diff/example/SimpleNodeExample.java b/src/integration-test/java/de/danielbechler/diff/example/SimpleNodeExample.java index 6ac12f8f..3b3e6b33 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/SimpleNodeExample.java +++ b/src/integration-test/java/de/danielbechler/diff/example/SimpleNodeExample.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,16 +16,36 @@ package de.danielbechler.diff.example; -import de.danielbechler.diff.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.NodeHierarchyVisitor; +import de.danielbechler.diff.node.PrintingVisitor; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ class SimpleNodeExample { private SimpleNodeExample() { } + public static void main(final String[] args) + { + final Person bruceWayne = new Person("Bruce", "Wayne"); + final Person batman = new Person("Batman", null); + final DiffNode rootNode = ObjectDifferBuilder.buildDefault().compare(batman, bruceWayne); + rootNode.visit(new NodeHierarchyVisitor(10)); + rootNode.visit(new PrintingVisitor(batman, bruceWayne) + { + @Override + protected boolean filter(final DiffNode node) + { + return true; + } + }); + } + private static class Person { private String firstName; @@ -72,20 +92,4 @@ public String toString() return sb.toString(); } } - - public static void main(final String[] args) - { - final Person bruceWayne = new Person("Bruce", "Wayne"); - final Person batman = new Person("Batman", null); - final DiffNode rootNode = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(batman, bruceWayne); - rootNode.visit(new NodeHierarchyVisitor(10)); - rootNode.visit(new PrintingVisitor(batman, bruceWayne) - { - @Override - protected boolean filter(final DiffNode node) - { - return true; - } - }); - } } diff --git a/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/GettingStarted.java b/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/GettingStarted.java index 0cc2d58a..1d0dcfb9 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/GettingStarted.java +++ b/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/GettingStarted.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,11 +16,17 @@ package de.danielbechler.diff.example.gettingstarted; -import de.danielbechler.diff.*; -import de.danielbechler.diff.example.phonebook.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDiffer; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.example.phonebook.Contact; +import de.danielbechler.diff.example.phonebook.PhoneBook; +import de.danielbechler.diff.example.phonebook.PhoneNumber; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.PrintingVisitor; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class GettingStarted { private GettingStarted() @@ -35,7 +41,7 @@ public static void main(final String[] args) private static void helloWorldExample() { - final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); + final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefault(); final String working = "Hello"; final String base = "World"; @@ -61,7 +67,7 @@ private static void phoneBookExample() modifiedPhoneBook.getContact("Jesse", "Pinkman").setMiddleName("Bruce"); modifiedPhoneBook.getContact("Walter", "White").setMiddleName("Hartwell"); - final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); + final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefault(); final DiffNode root = objectDiffer.compare(modifiedPhoneBook, phoneBook); final DiffNode.Visitor visitor = new PrintingVisitor(modifiedPhoneBook, phoneBook); root.visit(visitor); diff --git a/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/StarterGuide.java b/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/StarterGuide.java new file mode 100644 index 00000000..f49d89c0 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/example/gettingstarted/StarterGuide.java @@ -0,0 +1,81 @@ +/* + * 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.example.gettingstarted; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.Visit; +import de.danielbechler.diff.path.NodePath; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("LocalCanBeFinal") +public class StarterGuide +{ + private StarterGuide() + { + } + + public static void main(final String[] args) + { + final Map working = Collections.singletonMap("item", "foo"); + final Map base = Collections.singletonMap("item", "bar"); + DiffNode diff = ObjectDifferBuilder.buildDefault().compare(working, base); + + assert diff.hasChanges(); + assert diff.childCount() == 1; + NodePath itemPath = NodePath.startBuilding().mapKey("item").build(); + assert diff.getChild(itemPath).getState() == DiffNode.State.CHANGED; + + diff.visit(new DiffNode.Visitor() + { + public void node(DiffNode node, Visit visit) + { + System.out.println(node.getPath() + " => " + node.getState()); + } + }); + + diff.visit(new DiffNode.Visitor() + { + public void node(DiffNode node, Visit visit) + { + final Object baseValue = node.canonicalGet(base); + final Object workingValue = node.canonicalGet(working); + final String message = node.getPath() + " changed from " + + baseValue + " to " + workingValue; + System.out.println(message); + } + }); + + final Map head = new HashMap(base); + head.put("another", "map"); + diff.visit(new DiffNode.Visitor() + { + public void node(DiffNode node, Visit visit) + { + if (node.hasChanges() && !node.hasChildren()) + { + node.canonicalSet(head, node.canonicalGet(working)); + } + } + }); + assert head.get("item").equals("foo"); + assert head.get("another").equals("map"); + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/phonebook/Contact.java b/src/integration-test/java/de/danielbechler/diff/example/phonebook/Contact.java index 972da84d..2ae22a40 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/phonebook/Contact.java +++ b/src/integration-test/java/de/danielbechler/diff/example/phonebook/Contact.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,22 +16,17 @@ package de.danielbechler.diff.example.phonebook; -import de.danielbechler.diff.annotation.*; -import de.danielbechler.util.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; +import de.danielbechler.util.Strings; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.Map; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class Contact { - public static Contact from(final Contact contact) - { - final Contact copy = new Contact(contact.firstName, contact.lastName); - copy.setMiddleName(contact.middleName); - copy.setPhoneNumbers(new LinkedHashMap(contact.phoneNumbers)); - return copy; - } - private final String firstName; private final String lastName; private String middleName; @@ -43,6 +38,14 @@ public Contact(final String firstName, final String lastName) this.lastName = lastName; } + public static Contact from(final Contact contact) + { + final Contact copy = new Contact(contact.firstName, contact.lastName); + copy.setMiddleName(contact.middleName); + copy.setPhoneNumbers(new LinkedHashMap(contact.phoneNumbers)); + return copy; + } + @ObjectDiffProperty(categories = "name") public String getFirstName() { diff --git a/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneBookIT.groovy b/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneBookIT.groovy new file mode 100644 index 00000000..dc17b5d7 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneBookIT.groovy @@ -0,0 +1,70 @@ +/* + * Copyright 2012 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.example.phonebook + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.selector.CollectionItemElementSelector +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +public class PhoneBookIT extends Specification { + + def 'contact middle name changes'() { + given: + PhoneBook phoneBook = new PhoneBook("Breaking Bad") + and: + Contact walterWhite = new Contact("Walter", "White") + walterWhite.setPhoneNumber("Home", new PhoneNumber("1", "505", "316-7871")) + walterWhite.setPhoneNumber("Work", new PhoneNumber("1", "505", "456-3788")) + phoneBook.addContact(walterWhite) + and: + Contact jessePinkman = new Contact("Jesse", "Pinkman") + jessePinkman.setPhoneNumber("Home", new PhoneNumber("1", "505", "234-4628")) + phoneBook.addContact(jessePinkman) + and: + PhoneBook modifiedPhoneBook = PhoneBook.from(phoneBook) + modifiedPhoneBook.getContact("Jesse", "Pinkman").middleName = "Bruce" + modifiedPhoneBook.getContact("Walter", "White").middleName = "Hartwell" + when: + def node = ObjectDifferBuilder.buildDefault().compare(modifiedPhoneBook, phoneBook) + then: + node.hasChanges() + node.hasChildren() + node.childCount() == 1 + and: + def contactsNode = node.getChild("contacts") + contactsNode.hasChanges() + and: + def pinkmanNode = contactsNode.getChild(new CollectionItemElementSelector(jessePinkman)) + pinkmanNode.hasChanges() + and: + def middleNameNode = pinkmanNode.getChild("middleName") + middleNameNode.hasChanges() + middleNameNode.canonicalGet(phoneBook) == null + middleNameNode.canonicalGet(modifiedPhoneBook) == "Bruce" + and: + def whiteNode = contactsNode.getChild(new CollectionItemElementSelector(walterWhite)) + whiteNode.hasChanges() + and: + def whiteMiddleNameNode = whiteNode.getChild("middleName") + whiteMiddleNameNode.hasChanges() + whiteMiddleNameNode.canonicalGet(phoneBook) == null + whiteMiddleNameNode.canonicalGet(modifiedPhoneBook) == "Hartwell" + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneBookITCase.java b/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneBookITCase.java deleted file mode 100644 index afb24bab..00000000 --- a/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneBookITCase.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2012 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.example.phonebook; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.collection.*; -import org.hamcrest.core.*; -import org.testng.annotations.*; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.core.Is.*; - -/** @author Daniel Bechler */ -public class PhoneBookITCase -{ - @Test - public void testContactMiddleNameChanges() - { - final PhoneBook phoneBook = new PhoneBook("Breaking Bad"); - - final Contact walterWhite = new Contact("Walter", "White"); - walterWhite.setPhoneNumber("Home", new PhoneNumber("1", "505", "316-7871")); - walterWhite.setPhoneNumber("Work", new PhoneNumber("1", "505", "456-3788")); - phoneBook.addContact(walterWhite); - - final Contact jessePinkman = new Contact("Jesse", "Pinkman"); - jessePinkman.setPhoneNumber("Home", new PhoneNumber("1", "505", "234-4628")); - phoneBook.addContact(jessePinkman); - - final PhoneBook modifiedPhoneBook = PhoneBook.from(phoneBook); - modifiedPhoneBook.getContact("Jesse", "Pinkman").setMiddleName("Bruce"); - modifiedPhoneBook.getContact("Walter", "White").setMiddleName("Hartwell"); - - final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); - final DiffNode node = objectDiffer.compare(modifiedPhoneBook, phoneBook); - - assertThat(node.hasChanges(), is(true)); - assertThat(node.hasChildren(), is(true)); - assertThat(node.getChildren().size(), is(1)); - - final DiffNode contactsNode = node.getChild("contacts"); - assertThat(contactsNode, IsNull.notNullValue()); - assertThat(contactsNode.hasChanges(), is(true)); - - final DiffNode pinkmanNode = contactsNode.getChild(new CollectionElement(jessePinkman)); - assertThat(pinkmanNode.hasChanges(), is(true)); - - final DiffNode middleNameNode = pinkmanNode.getChild("middleName"); - assertThat(middleNameNode.hasChanges(), is(true)); - assertThat(middleNameNode.canonicalGet(phoneBook), IsNull.nullValue()); - assertThat((String) middleNameNode.canonicalGet(modifiedPhoneBook), IsEqual.equalTo("Bruce")); - - final DiffNode whiteNode = contactsNode.getChild(new CollectionElement(walterWhite)); - assertThat(whiteNode.hasChanges(), is(true)); - - final DiffNode whiteMiddleNameNode = whiteNode.getChild("middleName"); - assertThat(whiteMiddleNameNode.hasChanges(), is(true)); - assertThat(whiteMiddleNameNode.canonicalGet(phoneBook), IsNull.nullValue()); - assertThat((String) whiteMiddleNameNode.canonicalGet(modifiedPhoneBook), IsEqual.equalTo("Hartwell")); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneNumber.java b/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneNumber.java index f179568c..c11cddc1 100644 --- a/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneNumber.java +++ b/src/integration-test/java/de/danielbechler/diff/example/phonebook/PhoneNumber.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,9 +16,11 @@ package de.danielbechler.diff.example.phonebook; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffEqualsOnlyType; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @ObjectDiffEqualsOnlyType public final class PhoneNumber { diff --git a/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyAT.groovy b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyAT.groovy new file mode 100644 index 00000000..dd07456e --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyAT.groovy @@ -0,0 +1,87 @@ +/* + * 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.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +class IdentityStrategyAT extends Specification { + + static class ObjectWithListProperty { + List collection = [] + } + + static class NonMatchingIdentityStrategy implements IdentityStrategy { + + @Override + boolean equals(Object working, Object base) { + return false + } + } + + def 'configure IdentityStrategy for property of specific type'() { + def strategy = new NonMatchingIdentityStrategy() + ObjectDiffer objectDiffer = ObjectDifferBuilder.startBuilding() + .identity().ofCollectionItems(ObjectWithListProperty, 'collection').via(strategy) + .and().build() + + def working = new ObjectWithListProperty(collection: ['a']) + def base = new ObjectWithListProperty(collection: ['a']) + + when: + def node = objectDiffer.compare(working, base) + then: + node.untouched + } + + def 'configure IdentityStrategy for element at specific path'() { + def strategy = new NonMatchingIdentityStrategy() + ObjectDiffer objectDiffer = ObjectDifferBuilder.startBuilding() + .identity() + .ofCollectionItems(NodePath.with('collection')).via(strategy) + .and().build() + + def working = new ObjectWithListProperty(collection: ['a']) + def base = new ObjectWithListProperty(collection: ['a']) + + when: + def node = objectDiffer.compare(working, base) + then: + node.untouched + } + + def 'configure custom IdentityStrategyResolver'() { + given: + def strategy = new IdentityStrategy() { + + boolean equals(Object working, Object base) { + return working.getAt('id') == base.getAt('id') + } + } + def objectDiffer = ObjectDifferBuilder.startBuilding() + .identity() + .setDefaultCollectionItemIdentityStrategy(strategy) + .and() + .build() + when: + def node = objectDiffer.compare([[id: '1', value: 'original']], [[id: '1', value: 'changed']]) + then: + node.getChild(NodePath.startBuilding().collectionItem([id:'1']).build()).changed + } +} 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..ed9d9705 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/identity/IdentityStrategyIT.groovy @@ -0,0 +1,144 @@ +/* + * 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.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.access.CollectionItemAccessor +import de.danielbechler.diff.differ.CollectionDiffer +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.CollectionItemElementSelector +import de.danielbechler.diff.selector.ElementSelector +import groovy.transform.AutoClone +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import spock.lang.Specification +import spock.lang.Subject + +@Subject([IdentityStrategy, CollectionItemElementSelector, CollectionItemAccessor, CollectionDiffer]) +class IdentityStrategyIT extends Specification { + + def '#scenario IdentityStrategy to determine item identity'() { + def ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding() + def ObjectDiffer objectDiffer + def ElementSelector baseSelector + def ElementSelector workingSelector + + given: + def baseCode = new Code(id: "foo", code: "original code") + def workingCode = new Code(id: "foo", code: "modified code") + + expect: + if (identityStrategyEnabled) { + def identityStrategy = new CodeIdentityStrategy() + objectDiffer = objectDifferBuilder.identity() + .ofCollectionItems(NodePath.withRoot()) + .via(identityStrategy) + .and().build() + baseSelector = new CollectionItemElementSelector(baseCode.clone()) + workingSelector = new CollectionItemElementSelector(workingCode.clone()) + } else { + objectDiffer = objectDifferBuilder.build() + baseSelector = new CollectionItemElementSelector(baseCode.clone()) + workingSelector = new CollectionItemElementSelector(workingCode.clone()) + } + def node = objectDiffer.compare([workingCode], [baseCode]) + node.getChild(baseSelector).state == expectedBaseState + node.getChild(workingSelector).state == expectedWorkingState + + where: + identityStrategyEnabled | expectedBaseState | expectedWorkingState + false | DiffNode.State.CHANGED | DiffNode.State.CHANGED + true | DiffNode.State.REMOVED | DiffNode.State.ADDED + + scenario = identityStrategyEnabled ? 'should use' : 'should not use' + } + + def 'should properly get item'() { + given: + def identityStrategy = new CodeIdentityStrategy() + def working = [new Code(id: 'working', code: '1')] + def base = [new Code(id: 'base', code: '1')] + def node = ObjectDifferBuilder.startBuilding() + .identity().ofCollectionItems(NodePath.withRoot()).via(identityStrategy) + .and().build() + .compare(working, base) + and: + def targetCode = new Code(code: '1') + def target = [targetCode] + def itemPath = NodePath.startBuilding().collectionItem(targetCode).build() + + expect: + node.getChild(itemPath).canonicalGet(target).is(targetCode) + } + + def 'should properly set item'() { + given: + def identityStrategy = new CodeIdentityStrategy() + def working = [new Code(id: 'working', code: '1')] + def base = [new Code(id: 'base', code: '1')] + def node = ObjectDifferBuilder.startBuilding() + .identity().ofCollectionItems(NodePath.withRoot()).via(identityStrategy) + .and().build() + .compare(working, base) + and: + def targetCode = new Code(code: '1') + def target = [targetCode] + def itemPath = NodePath.startBuilding().collectionItem(targetCode).build() + + when: + node.getChild(itemPath).canonicalSet(target, new Code(id: 'set', code: '1')) + then: + node.getChild(itemPath).canonicalGet(target) == new Code(id: 'set', code: '1') + } + + def 'should properly unset (remove) item'() { + given: + def identityStrategy = new CodeIdentityStrategy() + def working = [new Code(id: 'working', code: '1')] + def base = [new Code(id: 'base', code: '1')] + def node = ObjectDifferBuilder.startBuilding() + .identity().ofCollectionItems(NodePath.withRoot()).via(identityStrategy) + .and().build() + .compare(working, base) + and: + def targetCode = new Code(code: '1') + def target = [targetCode] + def itemPath = NodePath.startBuilding().collectionItem(targetCode).build() + + when: + node.getChild(itemPath).canonicalUnset(target) + then: + node.getChild(itemPath).canonicalGet(target) == null + } + + @AutoClone + @EqualsAndHashCode(includes = ['id']) + @ToString(includePackage = false) + public static class Code { + String id + String code + } + + public static class CodeIdentityStrategy implements IdentityStrategy { + @Override + boolean equals(final Object working, final Object base) { + return Objects.equals(((Code) working).getCode(), ((Code) base).getCode()) + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/ExclusiveInclusionIT.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/ExclusiveInclusionIT.groovy new file mode 100644 index 00000000..15dd0bd7 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/ExclusiveInclusionIT.groovy @@ -0,0 +1,142 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +class ExclusiveInclusionIT extends Specification { + + def objectDifferBuilder = ObjectDifferBuilder.startBuilding() + .filtering() + .returnNodesWithState(DiffNode.State.IGNORED).and() + + def 'inclusion via type based property does NOT exclude nodes that are not explicitly included'() { + given: + def base = new A(b: new B(foo: 'base', bar: 'base'), c: new C(baz: 'base')) + def working = new A(b: new B(foo: 'working', bar: 'base'), c: new C(baz: 'working')) + + when: + def node = objectDifferBuilder + .inclusion() + .include().propertyNameOfType(B, 'foo').and() + .build() + .compare(working, base) + + then: + node.getChild('b').getChild('foo').isChanged() + node.getChild('b').getChild('bar').isIgnored() + node.getChild('c').getChild('baz').isChanged() + } + + class A { + B b + C c + } + + class B { + String foo + String bar + } + + class C { + String baz + } + + def 'inclusion via property name does exclude nodes that are not explicitly included'() { + given: + def base = new A(b: new B(foo: 'base', bar: 'base'), c: new C(baz: 'base')) + def working = new A(b: new B(foo: 'working', bar: 'working'), c: new C(baz: 'working')) + + when: + def node = objectDifferBuilder + .inclusion() + .include().propertyName('b').and() + .build() + .compare(working, base) + + then: + node.getChild('b').hasChanges() + node.getChild('b').getChild('foo').isChanged() + node.getChild('b').getChild('bar').isChanged() + node.getChild('c').isIgnored() + } + + def 'inclusion via node path does exclude nodes that are not explicitly included'() { + given: + def base = new A(b: new B(foo: 'base', bar: 'base'), c: new C(baz: 'base')) + def working = new A(b: new B(foo: 'working', bar: 'working'), c: new C(baz: 'working')) + + when: + def node = objectDifferBuilder + .inclusion() + .include().node(NodePath.with('b')).and() + .build() + .compare(working, base) + + then: + node.getChild('b').hasChanges() + node.getChild('b').getChild('foo').isChanged() + node.getChild('b').getChild('bar').isChanged() + node.getChild('c').isIgnored() + } + + def 'inclusion via type does exclude nodes that are not explicitly included - even its children'() { + given: + def base = new A(b: new B(foo: 'base', bar: 'base'), c: new C(baz: 'base')) + def working = new A(b: new B(foo: 'working', bar: 'working'), c: new C(baz: 'working')) + + when: 'the property with type B is explicitly included' + def node = objectDifferBuilder + .inclusion() + .include().type(B).and() + .build() + .compare(working, base) + + then: 'the included type should not be ignored' + node.getChild('b').isIgnored() == false + + and: 'its sibling with a different type schould be ignored' + node.getChild('c').isIgnored() + + and: 'the children are also ignored, even though the parent is included' + node.getChild('b').getChild('foo').isIgnored() + node.getChild('b').getChild('bar').isIgnored() + } + + def 'inclusion via category does exclude nodes that are not explicitly included'() { + given: + def base = new A(b: new B(foo: 'base', bar: 'base'), c: new C(baz: 'base')) + def working = new A(b: new B(foo: 'working', bar: 'working'), c: new C(baz: 'working')) + + when: + def node = objectDifferBuilder + .categories().ofNode(NodePath.with('b')).toBe('included-category').and() + .inclusion() + .include().category('included-category').and() + .build() + .compare(working, base) + + then: + node.getChild('b').hasChanges() + node.getChild('b').getChild('foo').isChanged() + node.getChild('b').getChild('bar').isChanged() + node.getChild('c').isIgnored() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/InclusionAT.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/InclusionAT.groovy new file mode 100644 index 00000000..fc4e21c0 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/InclusionAT.groovy @@ -0,0 +1,184 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.introspection.ObjectDiffProperty +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.CollectionItemElementSelector +import groovy.transform.EqualsAndHashCode +import spock.lang.Specification + +/** + * Created by Daniel Bechler. + */ +@SuppressWarnings("GroovyUnusedDeclaration") +class InclusionAT extends Specification { + + def objectDifferBuilder = ObjectDifferBuilder.startBuilding() + def base = new Album(artist: new Artist(name: 'Pharrell Williams'), songs: [new Song(durationInSeconds: 130, title: 'Happy')]) + def working = new Album(artist: new Artist(name: 'N.E.R.D.'), songs: [new Song(durationInSeconds: 150, title: 'It')]) + + def 'exclude an element by type'() { + given: + objectDifferBuilder.inclusion().exclude().type(ArrayList) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs') == null + node.getChild('artist').changed + } + + def 'exclude an element by property name'() { + given: + objectDifferBuilder.inclusion().exclude().propertyName('songs') + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs') == null + node.getChild('artist').changed + } + + def 'exclude an element by node path'() { + given: + objectDifferBuilder.inclusion().exclude().node(NodePath.with('songs')) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs') == null + node.getChild('artist').changed + } + + def 'exclude an element by category'() { + given: + objectDifferBuilder.inclusion().exclude().category('foo') + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs') == null + node.getChild('artist').changed + } + + def 'include an element by type'() { + given: + objectDifferBuilder.inclusion().include().type(ArrayList).type(Song) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs').changed + node.getChild('artist') == null + } + + def 'include an element by property name'() { + given: + objectDifferBuilder.inclusion().include().propertyName('songs') + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs').changed + node.getChild('artist') == null + } + + def 'include an element by node path'() { + given: + objectDifferBuilder.inclusion().include().node(NodePath.with('songs')) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs').changed + node.getChild('artist') == null + } + + def 'include an element by category'() { + given: + objectDifferBuilder.inclusion().include().category('foo') + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs').changed + node.getChild('artist') == null + } + + def 'excludes always win over includes'() { + given: + objectDifferBuilder.inclusion() + .exclude().node(NodePath.with('songs')).also() + .include().node(NodePath.startBuildingFrom(NodePath.with('songs')).collectionItem('Happy').build()) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.childCount() == 0 + } + + def 'including an element implicitly excludes its siblings'() { + given: + objectDifferBuilder.inclusion().include().propertyName('artist') + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.childCount() == 1 + node.getChild('artist').changed + } + + def 'including an element implicitly includes its children'() { + given: + objectDifferBuilder.inclusion().include().node(NodePath.with('songs')) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs').getChild(new CollectionItemElementSelector(new Song(title: 'Happy'))).removed + node.getChild('songs').getChild(new CollectionItemElementSelector(new Song(title: 'It'))).added + } + + def 'including an element by path implicitly includes its parents'() { + given: + def includedNodePath = NodePath.startBuilding() + .propertyName('songs') + .collectionItem(new Song(title: 'Happy')) + objectDifferBuilder.inclusion().include().node(includedNodePath.build()) + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('songs').getChild(new CollectionItemElementSelector(new Song(title: 'Happy'))).removed + node.getChild('songs').childCount() == 1 + } + + class Album { + def Artist artist + def ArrayList songs + + @ObjectDiffProperty(categories = ['foo']) + ArrayList getSongs() { + return songs + } + + void setSongs(ArrayList songs) { + this.songs = songs + } + } + + @EqualsAndHashCode(includeFields = true) + class Artist { + def String name + } + + @EqualsAndHashCode(includeFields = true, includes = ['title']) + class Song { + def String title + def int durationInSeconds + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/InclusionIT.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/InclusionIT.groovy new file mode 100644 index 00000000..df25d986 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/InclusionIT.groovy @@ -0,0 +1,257 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.CollectionItemElementSelector +import spock.lang.Specification + +import static PhoneBookSetup.Contact + +/** + * Created by dbechler. + */ +class InclusionIT extends Specification { + + static final GEORGE_SELECTOR = new CollectionItemElementSelector(new Contact(id: 'george')) + static final KRAMER_SELECTOR = new CollectionItemElementSelector(new Contact(id: 'kramer')) + + def builder = ObjectDifferBuilder.startBuilding() + def working = PhoneBookSetup.getWorking() + def base = PhoneBookSetup.getBase() + + def "sanity check"() { + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + + then: + node.getChild('name').changed + + and: "only two contacts should have changed" + node.getChild('contacts').changed + node.getChild('contacts').childCount() == 2 + + and: "only Georges name should have changed" + node.getChild('contacts').getChild(GEORGE_SELECTOR).childCount() == 1 + node.getChild('contacts').getChild(GEORGE_SELECTOR).getChild('name').changed + + and: "only Kramers number should have changed" + + node.getChild('contacts').getChild(KRAMER_SELECTOR).childCount() == 1 + node.getChild('contacts').getChild(KRAMER_SELECTOR).getChild('number').changed + } + + def "Property with specific name excluded via configuration"() { + given: + builder.inclusion().exclude().propertyName("name") + + when: + def node = builder.build().compare(working, base) + + then: "Georges name change should be ignored" + node.getChild('contacts').getChild(GEORGE_SELECTOR) == null + + and: "The name change of the phone book should be ignored too" + node.getChild('name') == null + } + + def "Category excluded via configuration"() { + given: + builder.inclusion().exclude().category('private') + + when: + def node = builder.build().compare(working, base) + + then: "Kramers changed number should be ignored" + node.getChild('contacts').getChild(KRAMER_SELECTOR) == null + } + + def "Type excluded via configuration"() { + given: + builder.inclusion().exclude().type(Contact) + + when: + def node = builder.build().compare(working, base) + + then: "all contact changes should be ignored" + node.getChild('contacts') == null + } + + def "Node at specific NodePath excluded via configuration"() { + given: + builder.inclusion().exclude().node(NodePath + .startBuilding() + .propertyName('contacts') + .element(GEORGE_SELECTOR) + .build()) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('contacts').getChild(GEORGE_SELECTOR) == null + } + + def "Property excluded via @ObjectDiffProperty annotation"() { + given: + builder.inclusion().exclude().node(NodePath.with('name')) + + when: + def node = builder.build().compare(working, base) + + then: "the name change of the phone book should be ignored" + node.getChild('revision') == null + } + + def "including an element via property name"() { + given: + builder.inclusion().include().propertyName('name') + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild("name").changed + node.getChild("contacts") == null + } + + def "including an element via property name includes all its children"() { + given: + builder.inclusion().include().propertyName('contacts') + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('contacts').changed + node.getChild("contacts").childCount() == 2 + } + + def "including an element via path includes all its children"() { + given: + builder.inclusion().include().node(NodePath.with('contacts')) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('contacts').changed + node.getChild("contacts").childCount() == 2 + } + + def "including an element via category"() { + def includedCategory = "representation" + + given: "the name property of the phonebook is part of our included category" + builder.categories().ofNode(NodePath.with("name")).toBe(includedCategory) + + and: "the category is included" + builder.inclusion().include().category(includedCategory) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('name').changed + } + + def "including an element implicitly includes its children"() { + given: + builder.inclusion().include().node(NodePath.with('contacts')) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('contacts').changed + node.getChild('contacts').childCount() == 2 + } + + def "include all but some specific elements"() { + given: + builder.inclusion().include().node(NodePath.startBuilding().propertyName('contacts').build()) + builder.inclusion().exclude().node(NodePath.startBuilding().propertyName('contacts').element(KRAMER_SELECTOR).build()) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('contacts').changed + node.getChild('contacts').childCount() == 1 + } + + def "when an element is included by property name, all its children will be implicitly included"() { + given: + builder.inclusion().include().node(NodePath.startBuilding().propertyName('contacts').build()) + + when: + def node = builder.build().compare(working, base) + + then: + node.childCount() == 1 + node.getChild('contacts').childCount() == 2 + node.getChild('contacts').getChild(GEORGE_SELECTOR).changed + node.getChild('contacts').getChild(KRAMER_SELECTOR).changed + } + + def "when an element is included by category, all its children will be implicitly included"() { + given: + builder.categories().ofNode(NodePath.startBuilding().propertyName('contacts').build()).toBe('identifier') + builder.inclusion().include().category('identifier') + + when: + def node = builder.build().compare(working, base) + + then: + node.childCount() == 1 + node.getChild('contacts').changed + } + + def "included children of excluded nodes should also be excluded"() { + given: + builder.inclusion().exclude().node(NodePath.startBuilding().propertyName('contacts').build()) + builder.inclusion().include().node(NodePath.startBuilding().propertyName('contacts').element(GEORGE_SELECTOR).build()) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('contacts') == null + } + + def "including a category includes matching properties only if they can be reached due to other inclusion rules"() { + def includedCategory = "representation" + def nodePathToKramer = NodePath.startBuilding() + .propertyName("contacts") + .element(KRAMER_SELECTOR) + .build() + + given: + builder.categories().ofNode(NodePath.with("name")).toBe(includedCategory) + builder.categories().ofNode(nodePathToKramer).toBe(includedCategory) + + and: "the category is included" + builder.inclusion().include().category(includedCategory) + + when: + def node = builder.build().compare(working, base) + + then: + node.getChild('name').changed + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/ParentTypeBasedPropertyInclusionViaAnnotationAT.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/ParentTypeBasedPropertyInclusionViaAnnotationAT.groovy new file mode 100644 index 00000000..7cd61861 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/ParentTypeBasedPropertyInclusionViaAnnotationAT.groovy @@ -0,0 +1,187 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.introspection.ObjectDiffProperty +import spock.lang.Specification + +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED +import static de.danielbechler.diff.node.DiffNode.State.IGNORED + +/** + * Created by Daniel Bechler. + */ +class ParentTypeBasedPropertyInclusionViaAnnotationAT extends Specification { + + def "a property can be included via annotation"() { + given: + def working = new ObjectWithInclusion(name: 'working') + def base = new ObjectWithInclusion(name: 'base') + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.childCount() == 1 + node.getChild('name').isChanged() + } + + class ObjectWithInclusion { + String name + + @ObjectDiffProperty(inclusion = INCLUDED) + String getName() { + return name + } + + void setName(String name) { + this.name = name + } + } + + def "a property can be excluded via annotation inclusion flag"() { + given: + def working = new ObjectWithExclusionViaInclusion(name: 'working') + def base = new ObjectWithExclusionViaInclusion(name: 'base') + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.childCount() == 0 + } + + private class ObjectWithExclusionViaInclusion { + String name + + @ObjectDiffProperty(inclusion = EXCLUDED) + String getName() { + return name + } + + void setName(String name) { + this.name = name + } + } + + def "a property can be excluded via annotation exclude flag"() { + given: + def working = new ObjectWithExclusionViaExcludedFlag(name: 'working') + def base = new ObjectWithExclusionViaExcludedFlag(name: 'base') + when: + def node = ObjectDifferBuilder.startBuilding() + .filtering().returnNodesWithState(IGNORED).and() + .build() + .compare(working, base) + then: + node.childCount() == 1 + node.getChild('name').isIgnored() + } + + private class ObjectWithExclusionViaExcludedFlag { + String name + + @ObjectDiffProperty(excluded = true) + String getName() { + return name + } + + void setName(String name) { + this.name = name + } + } + + def "when a property is included its siblings will be ignored unless they are explicitly included"() { + given: + def working = new ObjectWithInclusionAndSibling(name: 'working', sibling: 'working') + def base = new ObjectWithInclusionAndSibling(name: 'base', sibling: 'base') + when: + def node = ObjectDifferBuilder.startBuilding() + .filtering().returnNodesWithState(IGNORED).and() + .build() + .compare(working, base) + then: + node.childCount() == 2 + node.getChild('name').isChanged() + node.getChild('sibling').isIgnored() + } + + private class ObjectWithInclusionAndSibling { + def name + def sibling + + @ObjectDiffProperty(inclusion = INCLUDED) + def getName() { + return name + } + + void setName(def name) { + this.name = name + } + } + + def "when a property is excluded it doesn't affect the inclusion of its siblings"() { + given: + def working = new ObjectWithExclusionAndSibling(name: 'working', sibling: 'working') + def base = new ObjectWithExclusionAndSibling(name: 'base', sibling: 'base') + when: + def node = ObjectDifferBuilder.startBuilding() + .filtering().returnNodesWithState(IGNORED).and() + .build() + .compare(working, base) + then: + node.childCount() == 2 + node.getChild('name').isIgnored() + node.getChild('sibling').isChanged() + } + + private class ObjectWithExclusionAndSibling { + def name + def sibling + + @ObjectDiffProperty(inclusion = EXCLUDED) + def getName() { + return name + } + + void setName(def name) { + this.name = name + } + } + + def "when a property is excluded via 'excluded' flag and included via 'inclusion' at the same time, the inclusion flag always wins"() { + given: + def working = new ObjectWithConflictingAnnotationFlags(name: 'changed') + def base = new ObjectWithConflictingAnnotationFlags(name: 'about-to-get-changed') + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.childCount() == 1 + node.getChild('name').isChanged() + } + + private class ObjectWithConflictingAnnotationFlags { + def name + + @ObjectDiffProperty(excluded = true, inclusion = INCLUDED) + def getName() { + return name + } + + void setName(def name) { + this.name = name + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/ParentTypeBasedPropertyInclusionViaConfigAT.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/ParentTypeBasedPropertyInclusionViaConfigAT.groovy new file mode 100644 index 00000000..1103a250 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/ParentTypeBasedPropertyInclusionViaConfigAT.groovy @@ -0,0 +1,142 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import spock.lang.Specification + +import static de.danielbechler.diff.node.DiffNode.State.IGNORED + +/** + * Created by Daniel Bechler. + */ +class ParentTypeBasedPropertyInclusionViaConfigAT extends Specification { + + def objectDifferBuilderPreconfiguredToReturnIgnoredNodes + + def "setup"() { + objectDifferBuilderPreconfiguredToReturnIgnoredNodes = ObjectDifferBuilder + .startBuilding() + .filtering().returnNodesWithState(IGNORED).and() + } + + def "a property can be included via config API"() { + given: + def base = new ObjectWithOneProperty(name: 'about-to-change') + def working = new ObjectWithOneProperty(name: 'changed') + when: + def node = objectDifferBuilderPreconfiguredToReturnIgnoredNodes.inclusion() + .include().propertyNameOfType(ObjectWithOneProperty, 'name').and() + .build() + .compare(working, base) + then: + node.childCount() == 1 + node.getChild('name').isChanged() + } + + private class ObjectWithOneProperty { + def name + } + + def "a property can be excluded via config API"() { + given: + def base = new ObjectWithOneProperty(name: 'about-to-change') + def working = new ObjectWithOneProperty(name: 'changed') + when: + def node = objectDifferBuilderPreconfiguredToReturnIgnoredNodes.inclusion() + .exclude().propertyNameOfType(ObjectWithOneProperty, 'name').and() + .build() + .compare(working, base) + then: + node.childCount() == 1 + node.getChild('name').isIgnored() + } + + def "when a property is included all siblings that have not been explicitly included will be ignored"() { + given: + def base = new ObjectWithTwoProperties(name: 'about-to-change', description: 'about-to-change') + def working = new ObjectWithTwoProperties(name: 'changed', description: 'changed') + when: + def node = objectDifferBuilderPreconfiguredToReturnIgnoredNodes.inclusion() + .include().propertyNameOfType(ObjectWithTwoProperties, 'name').and() + .build() + .compare(working, base) + then: + node.childCount() == 2 + node.getChild('name').isChanged() + node.getChild('description').isIgnored() + } + + private class ObjectWithTwoProperties { + def name + def description + } + + def "when a property is included it does not affect properties of other types"() { + given: + def base = new ObjectWithTwoProperties(name: 'about-to-change', description: 'about-to-change') + def working = new ObjectWithTwoProperties(name: 'changed', description: 'changed') + + and: 'an ObjectDiffer with inclusions for a different object type' + def objectDiffer = objectDifferBuilderPreconfiguredToReturnIgnoredNodes.inclusion() + .include().propertyNameOfType(ObjectWithOneProperty, 'name').and() + .build() + + when: + def node = objectDiffer.compare(working, base) + + then: + node.childCount() == 2 + node.getChild('name').isChanged() + node.getChild('description').isChanged() + } + + def "when a property is excluded it does not affect its siblings"() { + given: + def base = new ObjectWithTwoProperties(name: 'about-to-change', description: 'about-to-change') + def working = new ObjectWithTwoProperties(name: 'changed', description: 'changed') + when: + def node = objectDifferBuilderPreconfiguredToReturnIgnoredNodes.inclusion() + .exclude().propertyNameOfType(ObjectWithTwoProperties, 'description').and() + .build() + .compare(working, base) + then: + node.childCount() == 2 + node.getChild('name').isChanged() + node.getChild('description').isIgnored() + } + + def "when a property is excluded it does not affect properties of other types"() { + given: + def base = new ObjectWithTwoProperties(name: 'about-to-change', description: 'about-to-change') + def working = new ObjectWithTwoProperties(name: 'changed', description: 'changed') + + and: 'an ObjectDiffer with inclusions for a different object type' + def objectDiffer = objectDifferBuilderPreconfiguredToReturnIgnoredNodes + .inclusion() + .exclude().propertyNameOfType(ObjectWithOneProperty, 'name').and() + .build() + + when: + def node = objectDiffer.compare(working, base) + + then: + node.childCount() == 2 + node.getChild('name').isChanged() + node.getChild('description').isChanged() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/PhoneBookSetup.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/PhoneBookSetup.groovy new file mode 100644 index 00000000..2f36e3ec --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/PhoneBookSetup.groovy @@ -0,0 +1,90 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.introspection.ObjectDiffProperty +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString + +/** + * Created by Daniel Bechler. + */ +public class PhoneBookSetup { + + static def getBase() { + return new PhoneBook( + name: "Jerry's Phone Book", + revision: 1, + contacts: [ + new Contact(id: "elaine", name: "Elaine", number: "917-555-0186"), + new Contact(id: "george", name: "George", number: "917-555-0102"), + new Contact(id: "kramer", name: "Kramer", number: "917-555-3456") + ]) + } + + static def getWorking() { + return new PhoneBook( + // Jerry decided to give his phone book a more formal title + name: "Jerry Seinfeld's Phone Book", + revision: 2, + contacts: [ + new Contact(id: "elaine", name: "Elaine", number: "917-555-0186"), + + // George has a new nickname + new Contact(id: "george", name: "Koko", number: "917-555-0102"), + + // Jerry always ended up at the Moviefone hotline until he realized Kramers new number actually ends with 5, not 6 + new Contact(id: "kramer", name: "Kramer", number: "917-555-3455") + ]) + } + + @EqualsAndHashCode + @ToString(includePackage = false) + public static class PhoneBook { + def name + def contacts = [] + def revision + + @SuppressWarnings("GroovyUnusedDeclaration") + @ObjectDiffProperty(excluded = true) + def getRevision() { + return revision + } + + void setRevision(revision) { + this.revision = revision + } + } + + @EqualsAndHashCode(includes = ["id"]) + @ToString(includePackage = false) + public static class Contact { + def id + def name + def number + + @SuppressWarnings("GroovyUnusedDeclaration") + @ObjectDiffProperty(categories = ['private']) + def getNumber() { + return number + } + + void setNumber(number) { + this.number = number + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/inclusion/PropertyInclusionViaInclusionResolverAT.groovy b/src/integration-test/java/de/danielbechler/diff/inclusion/PropertyInclusionViaInclusionResolverAT.groovy new file mode 100644 index 00000000..8a9b851e --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/inclusion/PropertyInclusionViaInclusionResolverAT.groovy @@ -0,0 +1,64 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import spock.lang.Specification + +class PropertyInclusionViaInclusionResolverAT extends Specification { + + def 'custom inclusion resolvers can be registered via config API'() { + def base = [foo: 'original', bar: 'original'] as TestObject + def working = [foo: 'changed', bar: 'changed'] as TestObject + + given: "custom inclusion resolver that excludes all properties named 'foo'" + def inclusionResolver = new InclusionResolver() { + @Override + boolean enablesStrictIncludeMode() { + return false + } + + @Override + Inclusion getInclusion(DiffNode node) { + if (node.propertyAware && node.propertyName == 'foo') { + return Inclusion.EXCLUDED + } + return Inclusion.INCLUDED + } + } + when: + def node = ObjectDifferBuilder.startBuilding() + .inclusion() + .resolveUsing(inclusionResolver) + .and() + .build() + .compare(working, base) + then: + node.getChild('foo') == null + node.getChild('bar').changed + } + + @ToString + @EqualsAndHashCode + class TestObject { + def foo + def bar + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/integration/AdditionIntegrationIT.groovy b/src/integration-test/java/de/danielbechler/diff/integration/AdditionIntegrationIT.groovy new file mode 100644 index 00000000..ad0b5186 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/integration/AdditionIntegrationIT.groovy @@ -0,0 +1,68 @@ +/* + * Copyright 2012 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.integration + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.example.phonebook.Contact +import de.danielbechler.diff.example.phonebook.PhoneBook +import de.danielbechler.diff.mock.ObjectWithString +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +public class AdditionIntegrationIT extends Specification { + + def 'detects change from null to object referenct as addition'() { + given: + def base = new ObjectWithString() + def working = new ObjectWithString("foo") + when: + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + then: + node.getChild('value').state == DiffNode.State.ADDED + } + + def 'detects change for duplicates in list'() { + def node + def joe = new Contact("Joe", "Smith") + def objectDiffer = ObjectDifferBuilder.buildDefault() + + given: + def phoneBookServer = new PhoneBook("devs") + phoneBookServer.addContact(joe) + and: + def phoneBookMobile = new PhoneBook("devs") + phoneBookMobile.addContact(joe) + + when: + node = objectDiffer.compare(phoneBookMobile, phoneBookServer) + then: + node.state == DiffNode.State.UNTOUCHED + + when: + phoneBookMobile.addContact(joe) + node = objectDiffer.compare(phoneBookMobile, phoneBookServer) + then: + phoneBookMobile.contacts.size() == 2 + and: + node.state == DiffNode.State.UNTOUCHED +// // Eventually it should be able to detect the change as addition... +// node.state == DiffNode.State.ADDED + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/integration/AdditionIntegrationITCase.java b/src/integration-test/java/de/danielbechler/diff/integration/AdditionIntegrationITCase.java deleted file mode 100644 index 073a66c6..00000000 --- a/src/integration-test/java/de/danielbechler/diff/integration/AdditionIntegrationITCase.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2012 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.integration; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.example.phonebook.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.helper.NodeAssertions.assertThat; -import static org.testng.AssertJUnit.assertEquals; - -/** @author Daniel Bechler */ -public class AdditionIntegrationITCase -{ - @Test - public void detects_change_from_null_to_object_referenct_as_addition() throws Exception - { - final ObjectWithString base = new ObjectWithString(); - final ObjectWithString working = new ObjectWithString("foo"); - - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - - assertThat(node).child("value").hasState(DiffNode.State.ADDED); - } - - @Test - public void testDetectsChangeForDuplicatesInList() throws Exception - { - final Contact joe = new Contact("Joe", "Smith"); - final PhoneBook phoneBookServer = new PhoneBook("devs"); - phoneBookServer.addContact(joe); - final PhoneBook phoneBookMobile = new PhoneBook("devs"); - phoneBookMobile.addContact(joe); - - assertEquals(DiffNode.State.UNTOUCHED, ObjectDifferBuilder.buildDefaultObjectDiffer().compare(phoneBookMobile, phoneBookServer).getState()); - phoneBookMobile.addContact(joe); - assertEquals(2, phoneBookMobile.getContacts().size()); - //Should be ADDED! - //assertEquals(DiffNode.State.ADDED, ObjectDifferFactory.getInstance().compare(phoneBookMobile, phoneBookServer).getState()); - assertEquals(DiffNode.State.UNTOUCHED, ObjectDifferBuilder.buildDefaultObjectDiffer().compare(phoneBookMobile, phoneBookServer).getState()); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/integration/DeepDiffingCollectionItemChangeIT.groovy b/src/integration-test/java/de/danielbechler/diff/integration/DeepDiffingCollectionItemChangeIT.groovy new file mode 100644 index 00000000..7f2f47d4 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/integration/DeepDiffingCollectionItemChangeIT.groovy @@ -0,0 +1,43 @@ +/* + * 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.integration + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.mock.ObjectWithString +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +/** + * @author Daniel Bechler + */ +public class DeepDiffingCollectionItemChangeIT extends Specification { + + def 'returns full property graph of added collection items'() { + given: + def base = [:] + def working = [foo: new ObjectWithString('bar')] + when: + def objectDiffer = ObjectDifferBuilder.startBuilding().build() + def node = objectDiffer.compare(working, base) + then: + def fooMapEntryPath = NodePath.startBuilding().mapKey("foo").build() + node.getChild(fooMapEntryPath).state == DiffNode.State.ADDED + and: + def fooMapEntryValuePath = NodePath.startBuildingFrom(fooMapEntryPath).propertyName('value').build() + node.getChild(fooMapEntryValuePath).state == DiffNode.State.ADDED + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/integration/DeepDiffingCollectionItemChangeITCase.java b/src/integration-test/java/de/danielbechler/diff/integration/DeepDiffingCollectionItemChangeITCase.java deleted file mode 100644 index b09a1337..00000000 --- a/src/integration-test/java/de/danielbechler/diff/integration/DeepDiffingCollectionItemChangeITCase.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012 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.integration; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.mock.*; -import de.danielbechler.diff.visitor.*; -import org.testng.annotations.*; - -import java.util.*; - -import static de.danielbechler.diff.helper.NodeAssertions.*; - -/** @author Daniel Bechler */ -public class DeepDiffingCollectionItemChangeITCase -{ - @Test - public void test_returns_full_property_graph_of_added_collection_items() - { - final Map base = Collections.emptyMap(); - final Map working = Collections.singletonMap("foo", new ObjectWithString("bar")); - - final ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding(); - final ObjectDiffer differ = objectDifferBuilder.build(); - - final DiffNode node = differ.compare(working, base); - - node.visit(new NodeHierarchyVisitor()); - - assertThat(node).child(NodePath.createBuilder() - .withRoot() - .withMapKey("foo")).hasState(DiffNode.State.ADDED); - - assertThat(node).child(NodePath.createBuilder() - .withRoot() - .withMapKey("foo") - .withPropertyName("value")).hasState(DiffNode.State.ADDED); - } - - @Test - public void test_collection_with_null_item() - { - ObjectDifferBuilder.buildDefaultObjectDiffer() - .compare(Arrays.asList((String) null), Arrays.asList("foobar")); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/integration/IgnoreIT.groovy b/src/integration-test/java/de/danielbechler/diff/integration/IgnoreIT.groovy new file mode 100644 index 00000000..af2b1585 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/integration/IgnoreIT.groovy @@ -0,0 +1,57 @@ +/* + * 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.integration + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.mock.ObjectWithCircularReference +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +public class IgnoreIT extends Specification { + + def 'verify that ignore rules with complex paths works properly'() { + given: + def obj1 = new ObjectWithCircularReference("1") + def obj2 = new ObjectWithCircularReference("2") + def obj3 = new ObjectWithCircularReference("3") + obj1.reference = obj2 + obj2.reference = obj3 + and: + def modifiedObj1 = new ObjectWithCircularReference("1") + def modifiedObj2 = new ObjectWithCircularReference("2") + def modifiedObj3 = new ObjectWithCircularReference("4") + modifiedObj1.reference = modifiedObj2 + modifiedObj2.reference = modifiedObj3 + and: + def nodePath = NodePath.with("reference", "reference") + when: + def objectDiffer1 = ObjectDifferBuilder.startBuilding().build() + def verification = objectDiffer1.compare(obj1, modifiedObj1) + then: "verify that the node can be found when it's not excluded" + verification.getChild(nodePath).state == DiffNode.State.CHANGED + verification.getChild(nodePath).childCount() == 1 + when: + def objectDiffer2 = ObjectDifferBuilder.startBuilding().inclusion().exclude().node(nodePath).and().build() + def node = objectDiffer2.compare(obj1, modifiedObj1) + then: "verify that the node can't be found, when it's excluded" + node.getChild(nodePath) == null + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/integration/IgnoreITCase.java b/src/integration-test/java/de/danielbechler/diff/integration/IgnoreITCase.java deleted file mode 100644 index 36832d4b..00000000 --- a/src/integration-test/java/de/danielbechler/diff/integration/IgnoreITCase.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2012 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.integration; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.mock.*; -import de.danielbechler.diff.visitor.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.helper.NodeAssertions.*; - -/** @author Daniel Bechler */ -public class IgnoreITCase -{ - @Test - public void verify_that_ignore_rules_with_complex_paths_works_properly() throws Exception - { - final ObjectWithCircularReference obj1 = new ObjectWithCircularReference("1"); - final ObjectWithCircularReference obj2 = new ObjectWithCircularReference("2"); - final ObjectWithCircularReference obj3 = new ObjectWithCircularReference("3"); - obj1.setReference(obj2); - obj2.setReference(obj3); - - final ObjectWithCircularReference modifiedObj1 = new ObjectWithCircularReference("1"); - final ObjectWithCircularReference modifiedObj2 = new ObjectWithCircularReference("2"); - final ObjectWithCircularReference modifiedObj3 = new ObjectWithCircularReference("4"); - modifiedObj1.setReference(modifiedObj2); - modifiedObj2.setReference(modifiedObj3); - - final NodePath nodePath = NodePath.buildWith("reference", "reference"); - - // verify that the node can be found when it's not excluded - ObjectDiffer objectDiffer = ObjectDifferBuilder.startBuilding().build(); - final DiffNode verification = objectDiffer.compare(obj1, modifiedObj1); - verification.visit(new PrintingVisitor(obj1, modifiedObj1)); - assertThat(verification).child(nodePath).hasState(DiffNode.State.CHANGED).hasChildren(1); - - // verify that the node can't be found, when it's excluded - final ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding(); - objectDifferBuilder.configure().inclusion().toExclude().nodes(nodePath); - objectDiffer = objectDifferBuilder.build(); - final DiffNode node = objectDiffer.compare(obj1, modifiedObj1); - node.visit(new PrintingVisitor(obj1, modifiedObj1)); - assertThat(node).child(nodePath).doesNotExist(); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/introspection/HandlingPropertyAccessExceptionsAT.groovy b/src/integration-test/java/de/danielbechler/diff/introspection/HandlingPropertyAccessExceptionsAT.groovy new file mode 100644 index 00000000..bf11fc85 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/introspection/HandlingPropertyAccessExceptionsAT.groovy @@ -0,0 +1,100 @@ +/* + * 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.introspection + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +import spock.lang.Subject + +@Subject(PropertyAccessExceptionHandler) +class HandlingPropertyAccessExceptionsAT extends Specification { + + def exceptionHandler = Mock(PropertyAccessExceptionHandler) + def objectDiffer = ObjectDifferBuilder + .startBuilding() + .introspection() + .handlePropertyAccessExceptionsUsing(exceptionHandler) + .and().build() + + @SuppressWarnings("GroovyAssignabilityCheck") + def 'exceptions that occur when accessing getters can be handled via PropertyAccessExceptionHandler'() { + given: + def working = new StubForPropertyAccessExceptionTesting(value: 'foo', exceptionOnRead: true) + def base = new StubForPropertyAccessExceptionTesting(value: 'bar', exceptionOnRead: true) + + when: + objectDiffer.compare(working, base) + + then: + 1 * exceptionHandler.onPropertyReadException(_ as PropertyReadException, _ as DiffNode) >> { + PropertyReadException exception = it[0] + assert exception.message == "Failed to read value from property 'value' of type " + + "'de.danielbechler.diff.introspection.HandlingPropertyAccessExceptionsAT.StubForPropertyAccessExceptionTesting'" + assert exception.targetType == StubForPropertyAccessExceptionTesting + assert exception.propertyName == 'value' + + DiffNode node = it[1] + assert node.propertyName == 'value' + assert node.path.matches(NodePath.with('value')) + assert node.state == DiffNode.State.INACCESSIBLE + } + } + + def 'exceptions that occur when accessing setters can be handled by catching the thrown PropertyWriteException'() { + given: + def working = new StubForPropertyAccessExceptionTesting(value: 'foo') + def base = new StubForPropertyAccessExceptionTesting(value: 'bar') + def diff = objectDiffer.compare(working, base) + + expect: + diff.getChild('value') != null + + when: + def target = new StubForPropertyAccessExceptionTesting(exceptionOnWrite: true) + diff.getChild('value').canonicalSet(target, 'baz') + + then: + PropertyWriteException exception = thrown(PropertyWriteException) + exception.newValue == 'baz' + exception.message == "Failed to write new value 'baz' to property 'value' of type " + + "'de.danielbechler.diff.introspection.HandlingPropertyAccessExceptionsAT.StubForPropertyAccessExceptionTesting'" + exception.targetType == StubForPropertyAccessExceptionTesting + exception.propertyName == 'value' + } + + private static class StubForPropertyAccessExceptionTesting { + String value + def exceptionOnRead = false + def exceptionOnWrite = false + + String getValue() { + if (exceptionOnRead) { + throw new RuntimeException() + } + return value; + } + + void setValue(String value) { + if (exceptionOnWrite) { + throw new RuntimeException() + } + this.value = value + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/introspection/IntrospectionAT.groovy b/src/integration-test/java/de/danielbechler/diff/introspection/IntrospectionAT.groovy new file mode 100644 index 00000000..d2a99ef2 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/introspection/IntrospectionAT.groovy @@ -0,0 +1,126 @@ +/* + * 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.introspection + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.instantiation.TypeInfo +import de.danielbechler.diff.path.NodePath +import groovy.transform.EqualsAndHashCode +import groovy.transform.ToString +import spock.lang.Shared +import spock.lang.Specification + +/** + * Created by Daniel Bechler. + */ +class IntrospectionAT extends Specification { + + ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding() + + Introspector introspector = Mock(Introspector) + + @Shared + Role base = new Role(name: 'Daario Naharis', actor: new Actor(firstName: 'Ed', lastName: 'Skrein')) + @Shared + Role working = new Role(name: 'Daario Naharis', actor: new Actor(firstName: 'Michiel', lastName: 'Huisman')) + + def 'set default introspector'() { + given: + objectDifferBuilder.introspection().setDefaultIntrospector(introspector) + when: + objectDifferBuilder.build().compare(working, base) + then: + 1 * introspector.introspect(Role) >> new TypeInfo(Role) + } + + def 'disable introspection for node'() { + given: + objectDifferBuilder.introspection().ofNode(NodePath.with('actor')).toBeDisabled() + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.isUntouched() + } + + def 'disable introspection for type'() { + given: + objectDifferBuilder.introspection().ofType(Actor).toBeDisabled() + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.isUntouched() + } + + def 'enable introspection for node'() { + given: + objectDifferBuilder.introspection().ofNode(NodePath.with('actor')).toBeDisabled() + and: + objectDifferBuilder.introspection().ofNode(NodePath.with('actor')).toBeEnabled() + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('actor').childCount() == 2 + } + + def 'enable introspection for type'() { + given: + objectDifferBuilder.introspection().ofType(Actor).toBeDisabled() + and: + objectDifferBuilder.introspection().ofType(Actor).toBeEnabled() + when: + def node = objectDifferBuilder.build().compare(working, base) + then: + node.getChild('actor').childCount() == 2 + } + + def 'configure custom introspector for node'() { + given: + objectDifferBuilder.introspection().ofNode(NodePath.with('actor')).toUse(introspector) + when: + objectDifferBuilder.build().compare(working, base) + then: + 0 * introspector.introspect(Role) + and: + 1 * introspector.introspect(Actor) >> new TypeInfo(Actor) + } + + def 'configure custom introspector for type'() { + given: + objectDifferBuilder.introspection().ofType(Actor).toUse(introspector) + when: + objectDifferBuilder.build().compare(working, base) + then: + 0 * introspector.introspect(Role) + and: + 1 * introspector.introspect(Actor) >> new TypeInfo(Actor) + } + + @ToString + @EqualsAndHashCode(includes = 'name') + static class Role { + String name + Actor actor + } + + @ToString + @EqualsAndHashCode(includes = ['firstName', 'lastName']) + static class Actor { + String firstName + String lastName + } + +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue107/EnterLeaveSequenceInconsistencyIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue107/EnterLeaveSequenceInconsistencyIT.groovy new file mode 100644 index 00000000..a6d0a0eb --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue107/EnterLeaveSequenceInconsistencyIT.groovy @@ -0,0 +1,86 @@ +/* + * 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.issues.issue107 + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.circular.CircularReferenceDetector +import de.danielbechler.diff.differ.DifferDispatcher +import de.danielbechler.diff.node.PrintingVisitor +import spock.lang.Issue +import spock.lang.Specification +import spock.lang.Subject + +@Subject([CircularReferenceDetector, DifferDispatcher]) +@Issue("https://github.com/SQiShER/java-object-diff/issues/107") +class EnterLeaveSequenceInconsistencyIT extends Specification { + + def "Causes: 'Detected inconsistency in enter/leave sequence. Must always be LIFO'"() { + + given: + ObjectDiffer differ = ObjectDifferBuilder.buildDefault() + + and: "a working version in which 'a2' and 'c2' reference each other" + A a2 = new A(s1: "a2") + B b1 = new B(s2: "b1") + C c1 = new C(s3: "c1", a: a2) + A working = new A(s1: "a1", map: [(b1): c1]) + B b2 = new B(s2: "b2") + C c2 = new C(s3: "c2", a: a2) + a2.map.put(b2, c2) + + and: "a base version in which 'a3' references 'b1' of the working version and 'a3' and 'c3' reference each other" + A base = new A(s1: "a3") + C c3 = new C(a: base, s3: "s3") + base.map.put(b1, c3) + + when: + differ.compare(working, base).visit(new PrintingVisitor(working, base)) + + then: + noExceptionThrown() + } + + static class A { + def String s1 + def Map map = [:] + + @Override + String toString() { + "A(${s1})" + } + } + + static class B { + def String s2 + + @Override + String toString() { + "B(${s2})" + } + } + + static class C { + def String s3 + def A a + + @Override + String toString() { + "C(${s3})" + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue115/ObjectMergerIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue115/ObjectMergerIT.groovy new file mode 100644 index 00000000..704a75e9 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue115/ObjectMergerIT.groovy @@ -0,0 +1,67 @@ +/* + * 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.issues.issue115 + +import de.danielbechler.diff.ObjectMerger +import groovy.transform.ToString +import spock.lang.Issue +import spock.lang.Specification +import spock.lang.Subject + +@Issue('https://github.com/SQiShER/java-object-diff/issues/115') +@Subject(ObjectMerger) +class ObjectMergerIT extends Specification { + + def 'should write changes to delta object and create missing parent objects along the way'() { + given: + A base = new A(); + base.b = new B(); + base.b.y = 10; + base.x = 10; + + and: + A change = new A(); + change.b = new B(); + change.b.y = 12; + change.x = 10; + + + when: + def objectMerger = new ObjectMerger() + def delta = new A() + objectMerger.merge(change, base, delta); + + then: + delta.b instanceof B + delta.b.y == 12 + } + + @ToString + public static class A { + int x + B b + + public A() {} + } + + @ToString + public static class B { + int y + + public B() {} + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue127/Issue127Main.java b/src/integration-test/java/de/danielbechler/diff/issues/issue127/Issue127Main.java new file mode 100644 index 00000000..e2e7f098 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue127/Issue127Main.java @@ -0,0 +1,110 @@ +/* + * 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.issues.issue127; + +import de.danielbechler.diff.ObjectDiffer; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.Visit; +import de.danielbechler.diff.path.NodePath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +class Issue127Main +{ + private static final Logger LOGGER = LoggerFactory.getLogger(Issue127Main.class); + + private Issue127Main() + { + } + + public static void main(final String[] args) throws IOException + { + final Map map1 = Collections.singletonMap("lng", 123.45); + final Map map2 = Collections.singletonMap("lat", 67.89); + + final Community c1 = new Community(); + c1.setName("Foo"); + c1.setGeolocation(map1); + + final Community c2 = new Community(); + c2.setName("Bar"); + c2.setGeolocation(map2); + + final ObjectDiffer objectDiffer = ObjectDifferBuilder + .startBuilding() + .comparison().ofNode(NodePath.with("geolocation")).toUseEqualsMethod() + .and() + .build(); + final DiffNode diff = objectDiffer.compare(c1, c2); + + if (diff.hasChanges()) + { + diff.visit(new DiffNode.Visitor() + { + public void node(final DiffNode node, final Visit visit) + { + final Object oldValue = node.canonicalGet(c1); + final Object newValue = node.canonicalGet(c2); + LOGGER.warn("{}: {} => {}", node.getPath(), oldValue, newValue); + } + }); + } + } + + private static class Community + { + private String name; + private List children; + private Map geolocation; + + public String getName() + { + return this.name; + } + + public void setName(final String name) + { + this.name = name; + } + + public List getChildren() + { + return this.children; + } + + public void setChildren(final List children) + { + this.children = children; + } + + public Map getGeolocation() + { + return this.geolocation; + } + + public void setGeolocation(final Map geolocation) + { + this.geolocation = geolocation; + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue148/ConcurrencyIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue148/ConcurrencyIT.groovy new file mode 100644 index 00000000..8132b90f --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue148/ConcurrencyIT.groovy @@ -0,0 +1,62 @@ +/* + * Copyright 2016 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.issues.issue148 + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification + +import java.util.concurrent.Callable +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.Future + +class ConcurrencyIT extends Specification { + + static class TestObject { + def value + } + + def test_concurrent() { + ExecutorService executor = Executors.newFixedThreadPool(2) + + given: "one shared ObjectDiffer instance" + ObjectDiffer differ = ObjectDifferBuilder.buildDefault() + + when: "two Threads use the ObjectDiffer at the same time" + List nodes = (0..1).collect({ + return executor.submit(new Callable() { + @Override + DiffNode call() throws Exception { + TestObject working = new TestObject(value: "") + TestObject base = new TestObject() + def node = differ.compare(working, base) + Thread.yield() + return node + } + }) + }).collect({ Future future -> future.get() }) + + then: + noExceptionThrown() + and: + nodes[0].hasChanges() + and: + nodes[1].hasChanges() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnEqualsIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnEqualsIT.groovy new file mode 100644 index 00000000..c32e595b --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnEqualsIT.groovy @@ -0,0 +1,58 @@ +/* + * 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.issues.issue15 + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.mock.ObjectWithNestedObject +import spock.lang.Specification + +import static de.danielbechler.diff.circular.CircularReferenceMatchingMode.EQUALS_METHOD + +/** + * @author Daniel Bechler + */ +public class CircularReferenceDetectionBasedOnEqualsIT extends Specification { + + ObjectDiffer objectDiffer + + def setup() { + objectDiffer = ObjectDifferBuilder.startBuilding() + .circularReferenceHandling() + .matchCircularReferencesUsing(EQUALS_METHOD).and() + .build() + } + + def 'detects circular reference when encountering same object twice'() { + given: + def object = new ObjectWithNestedObject('foo') + object.object = object + when: + def node = objectDiffer.compare(object, null) + then: + node.getChild('object').isCircular() + } + + def 'detects circular reference when encountering different but equal objects twice'() { + given: + def object = new ObjectWithNestedObject('foo', new ObjectWithNestedObject('foo')) + when: + def node = objectDiffer.compare(object, null) + then: + node.getChild('object').isCircular() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnEqualsTest.java b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnEqualsTest.java deleted file mode 100644 index 07464a1e..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnEqualsTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2013 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.issues.issue15; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.helper.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.CircularReferenceMatchingMode.*; - -/** @author Daniel Bechler */ -public class CircularReferenceDetectionBasedOnEqualsTest -{ - private ObjectDiffer objectDiffer; - - @BeforeMethod - public void setUp() throws Exception - { - final ObjectDifferBuilder configuration = ObjectDifferBuilder.startBuilding(); - configuration.configure() - .circularReferenceHandling() - .matchCircularReferencesUsing(EQUALS_METHOD); - objectDiffer = configuration.build(); - } - - @Test - public void detectsCircularReference_whenEncounteringSameObjectTwice() throws Exception - { - final ObjectWithNestedObject object = new ObjectWithNestedObject("foo"); - object.setObject(object); - final DiffNode node = objectDiffer.compare(object, null); - NodeAssertions.assertThat(node).child("object").isCircular(); - } - - @Test - public void detectsCircularReference_whenEncounteringDifferentButEqualObjectsTwice() throws Exception - { - final ObjectWithNestedObject object = new ObjectWithNestedObject("foo", new ObjectWithNestedObject("foo")); - final DiffNode node = objectDiffer.compare(object, null); - NodeAssertions.assertThat(node).child("object").isCircular(); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnIdentityIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnIdentityIT.groovy new file mode 100644 index 00000000..04024d71 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnIdentityIT.groovy @@ -0,0 +1,59 @@ +/* + * 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.issues.issue15 + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.mock.ObjectWithNestedObject +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification + +import static de.danielbechler.diff.circular.CircularReferenceMatchingMode.EQUALITY_OPERATOR + +/** + * @author Daniel Bechler + */ +public class CircularReferenceDetectionBasedOnIdentityIT extends Specification { + + ObjectDiffer objectDiffer + + def setup() { + objectDiffer = ObjectDifferBuilder.startBuilding() + .circularReferenceHandling() + .matchCircularReferencesUsing(EQUALITY_OPERATOR).and() + .build() + } + + def 'detects circular reference when encountering same object twice'() { + given: + def object = new ObjectWithNestedObject('foo') + object.object = object + when: + def node = objectDiffer.compare(object, null) + then: + node.getChild('object').isCircular() + } + + def 'detects no circular reference when encountering different but equal objects twice'() { + given: + def object = new ObjectWithNestedObject('foo', new ObjectWithNestedObject('foo')) + when: + def node = objectDiffer.compare(object, null) + then: + node.getChild('object').state == DiffNode.State.ADDED + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnIdentityTest.java b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnIdentityTest.java deleted file mode 100644 index 7a93005d..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceDetectionBasedOnIdentityTest.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2013 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.issues.issue15; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.helper.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.CircularReferenceMatchingMode.*; - -/** @author Daniel Bechler */ -public class CircularReferenceDetectionBasedOnIdentityTest -{ - private ObjectDiffer objectDiffer; - - @BeforeMethod - public void setUp() throws Exception - { - final ObjectDifferBuilder configuration = ObjectDifferBuilder.startBuilding(); - configuration.configure() - .circularReferenceHandling() - .matchCircularReferencesUsing(EQUALITY_OPERATOR); - objectDiffer = configuration.build(); - } - - @Test - public void detectsCircularReference_whenEncounteringSameObjectTwice() throws Exception - { - final ObjectWithNestedObject object = new ObjectWithNestedObject("foo"); - object.setObject(object); - final DiffNode node = objectDiffer.compare(object, null); - NodeAssertions.assertThat(node).child("object").isCircular(); - } - - @Test - public void detectsNoCircularReference_whenEncounteringDifferentButEqualObjectsTwice() throws Exception - { - final ObjectWithNestedObject object = new ObjectWithNestedObject("foo", new ObjectWithNestedObject("foo")); - final DiffNode node = objectDiffer.compare(object, null); - NodeAssertions.assertThat(node).child("object").hasState(DiffNode.State.ADDED); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceIT.groovy new file mode 100644 index 00000000..22844550 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceIT.groovy @@ -0,0 +1,70 @@ +/* + * 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.issues.issue15 + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.mock.ObjectWithCircularReference +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +public class CircularReferenceIT extends Specification { + + def 'circular reference'() { + given: + def workingA = new ObjectWithCircularReference('a') + def workingB = new ObjectWithCircularReference('b') + workingA.reference = workingB + workingB.reference = workingA + and: + def baseA = new ObjectWithCircularReference('a') + def baseB = new ObjectWithCircularReference('c') + baseA.reference = baseB + baseB.reference = baseA + when: + def diffRoot = ObjectDifferBuilder.buildDefault().compare(workingA, baseA) + then: + def circularNode = diffRoot.getChild(NodePath.with('reference', 'reference')) + circularNode.isCircular() + circularNode.circleStartPath == NodePath.withRoot() + circularNode.circleStartNode.is(diffRoot) + } + + // FIXME: I feel like this test doesn't add much more than the other one. Delete? + def 'circular reference should be added when enabled in configuration'() { + given: + def workingA = new ObjectWithCircularReference('a') + def workingB = new ObjectWithCircularReference('b') + def workingC = new ObjectWithCircularReference('c') + workingA.reference = workingB + workingB.reference = workingC + workingC.reference = workingA + and: + def baseA = new ObjectWithCircularReference('a') + def baseB = new ObjectWithCircularReference('b') + def baseC = new ObjectWithCircularReference('d') + baseA.reference = baseB + baseB.reference = baseC + baseC.reference = baseA + when: + def node = ObjectDifferBuilder.buildDefault().compare(workingA, baseA) + then: + node.getChild(NodePath.with('reference', 'reference', 'reference')).isCircular() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceIntegrationTest.java b/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceIntegrationTest.java deleted file mode 100644 index e72ab6ca..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue15/CircularReferenceIntegrationTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2012 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.issues.issue15; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.mock.*; -import de.danielbechler.diff.visitor.*; -import org.fest.assertions.api.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.helper.NodeAssertions.*; - -/** @author Daniel Bechler */ -public class CircularReferenceIntegrationTest -{ - @Test - public void testCircularReference() - { - final ObjectWithCircularReference workingA = new ObjectWithCircularReference("a"); - final ObjectWithCircularReference workingB = new ObjectWithCircularReference("b"); - workingA.setReference(workingB); - workingB.setReference(workingA); - - final ObjectWithCircularReference baseA = new ObjectWithCircularReference("a"); - final ObjectWithCircularReference baseB = new ObjectWithCircularReference("c"); - baseA.setReference(baseB); - baseB.setReference(baseA); - - final DiffNode root = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(workingA, baseA); - assertThat(root).child("reference", "reference").isCircular(); - assertThat(root).child("reference", "reference") - .hasCircularStartPathEqualTo(NodePath.buildRootPath()); - - Assertions.assertThat(root.canonicalGet(workingA)) - .isSameAs(root.getChild("reference").getChild("reference").canonicalGet(workingA)); - Assertions.assertThat(root.getChild("reference").getChild("reference").getCircleStartNode()) - .isSameAs(root); - } - - @Test - public void testCircularReferenceShouldBeAddedWhenEnabledInConfiguration() - { - final ObjectWithCircularReference workingA = new ObjectWithCircularReference("a"); - final ObjectWithCircularReference workingB = new ObjectWithCircularReference("b"); - final ObjectWithCircularReference workingC = new ObjectWithCircularReference("c"); - workingA.setReference(workingB); - workingB.setReference(workingC); - workingC.setReference(workingA); - - final ObjectWithCircularReference baseA = new ObjectWithCircularReference("a"); - final ObjectWithCircularReference baseB = new ObjectWithCircularReference("b"); - final ObjectWithCircularReference baseC = new ObjectWithCircularReference("d"); - baseA.setReference(baseB); - baseB.setReference(baseC); - baseC.setReference(baseA); - - final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); -// objectDiffer.getConfiguration().withoutCircularNodes(); - final DiffNode root = objectDiffer.compare(workingA, baseA); - root.visit(new PrintingVisitor(workingA, baseA)); - assertThat(root).child("reference", "reference", "reference").isCircular(); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphIT.groovy new file mode 100644 index 00000000..5e218eb7 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphIT.groovy @@ -0,0 +1,406 @@ +/* + * 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.issues.issue15 + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.circular.CircularReferenceDetector +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.node.PrintingVisitor +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +import static de.danielbechler.diff.path.NodePath.startBuilding + +/** + * This tests have been provided by a user who had some trouble with circular relationships. The introduction + * of the {@link CircularReferenceDetector} fixed his problems. To avoid regression, these integration tests + * will be kept until other tests cover the same scenarios. + * + * @author https://github.com/oplohmann (original author) + * @author Daniel Bechler (modifications) + */ +public class GraphIT extends Specification { + + private static final boolean PRINT_ENABLED = true + + private static DiffNode compareAndPrint(GraphNode modified, GraphNode base) { + DiffNode root = ObjectDifferBuilder.buildDefault().compare(modified, base) + if (PRINT_ENABLED) { + root.visit(new PrintingVisitor(modified, base)) + } + return root + } + + def basicNode() { + given: + GraphNode base = new GraphNode(1) + GraphNode a = new GraphNode(2, "a") + base.directReference = a + and: + GraphNode modified = new GraphNode(1) + GraphNode modifiedA = new GraphNode(2, "ax") + modified.directReference = modifiedA + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.childCount() == 1 + root.getChild('directReference').getChild('value').state == DiffNode.State.CHANGED + root.getChild('directReference').getChild('value').childCount() == 0 + } + + def basicNodeWithDirectReferences() { + given: + GraphNode base = new GraphNode(1) + GraphNode a = new GraphNode(2, "a") + base.directReference = a + and: + GraphNode modified = new GraphNode(1) + GraphNode modifiedA = new GraphNode(2, "ax") + modified.directReference = modifiedA + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.childCount() == 1 + root.getChild('directReference').getChild('value').state == DiffNode.State.CHANGED + root.getChild('directReference').getChild('value').childCount() == 0 + } + + def basicBidirectionalWithChildren() { + given: + GraphNode base = new GraphNode(1) + GraphNode a = new GraphNode(2, "a") + GraphNode b = new GraphNode(3, "b") + base.addChildren(a, b) + a.directReference = b + and: + GraphNode modified = new GraphNode(1) + GraphNode modifiedA = new GraphNode(2, "ax") + GraphNode modifiedB = new GraphNode(3, "by") + modified.addChildren(modifiedA, modifiedB) + modifiedA.directReference = modifiedB + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.childCount() == 1 + root.getChild('children').childCount() == 2 + root.getChild(startBuilding().propertyName('children').collectionItem(a).build()).state == DiffNode.State.CHANGED + root.getChild(startBuilding().propertyName('children').collectionItem(b).build()).state == DiffNode.State.CHANGED + } + + + def basicBidirectionalWithChildrenAndMaps() { + given: + GraphNode a = new GraphNode(2, "a") + GraphNode b = new GraphNode(3, "b") + establishCircularDirectReference(a, b) + a.getMap().put("node-b", b) + a.getMap().put("node-b-again", b) + b.getMap().put("node-a", a) + and: + GraphNode base = new GraphNode(1) + base.getMap().put("a", "a") + base.getChildren().add(b) + and: + GraphNode modified = new GraphNode(1) + GraphNode modifiedA = new GraphNode(2, "ax") + modified.getMap().put("a", "az") + GraphNode modifiedB = new GraphNode(3, "by") + modified.addChild(modifiedB) + establishCircularDirectReference(modifiedA, modifiedB) + modifiedA.getMap().put("node-b", modifiedB) + modifiedB.getMap().put("node-a", modifiedA) + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.getChild(nodePath.build())?.state == expectedState + where: + nodePath || expectedState + startBuilding().propertyName("map").mapKey("a") || DiffNode.State.CHANGED + startBuilding().propertyName("children").collectionItem(new GraphNode(3)).propertyName("value") || DiffNode.State.CHANGED + startBuilding().propertyName("children").collectionItem(new GraphNode(3)).propertyName("map").mapKey("node-a").propertyName("value") || DiffNode.State.CHANGED + startBuilding().propertyName("children").collectionItem(new GraphNode(3)).propertyName("map").mapKey("node-a").propertyName("map").mapKey("node-b-again") || DiffNode.State.CIRCULAR + startBuilding().propertyName("children").collectionItem(new GraphNode(3)).propertyName("directReference").propertyName("value") || DiffNode.State.CHANGED + startBuilding().propertyName("children").collectionItem(new GraphNode(3)).propertyName("directReference").propertyName("map").mapKey("node-b-again") || DiffNode.State.CIRCULAR + } + + def basicBidirectionalWithoutChildren() { + given: + GraphNode a = new GraphNode(1, "a") + GraphNode b = new GraphNode(2, "b") + a.directReference = b + and: + GraphNode modifiedA = new GraphNode(1, "ax") + GraphNode modifiedB = new GraphNode(2, "by") + modifiedA.directReference = modifiedB + when: + DiffNode root = compareAndPrint(modifiedA, a) + then: + root.getChild("directReference").getChild("value").state == DiffNode.State.CHANGED + root.getChild("directReference").getChild("value").childCount() == 0 + and: + root.getChild("value").state == DiffNode.State.CHANGED + root.getChild("value").childCount() == 0 + } + + def basicNodeWithDirectReferences2() { + given: + GraphNode base = new GraphNode("base") + GraphNode a = new GraphNode("a") + base.directReference = a + and: + GraphNode modified = new GraphNode("modified") + GraphNode modifiedA = new GraphNode("ax") + modified.directReference = modifiedA + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.getChild("directReference").getChild("value").state == DiffNode.State.CHANGED + root.getChild("directReference").getChild("value").childCount() == 0 + and: + root.getChild("value").state == DiffNode.State.CHANGED + root.getChild("value").childCount() == 0 + } + + /** + * Does not detect any changes since no primary key defined for each node + */ + def basicBidirectionalNodeWithChildNodes() { + given: + GraphNode base = new GraphNode() + GraphNode a = new GraphNode("a") + GraphNode b = new GraphNode("b") + base.getChildren().add(a) + base.getChildren().add(b) + and: + GraphNode modified = new GraphNode() + GraphNode modifiedA = new GraphNode("a") + GraphNode modifiedB = new GraphNode("bx") + modified.getChildren().add(modifiedA) + modified.getChildren().add(modifiedB) + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.isUntouched() == true + and: + // NOTE: This is expected, since Collections (and java-object-diff) rely heavily on the proper + // implementation of hashCode and equals. The GraphNode uses the ID as sole criteria in it's + // equality check. Since all instances created without an explicit ID, will default to -1, all + // GraphNodes in this example are technically equal. + // + // The following test proves this: + GraphNode actualA = base.children.get(0) + GraphNode actualB = base.children.get(1) + actualA == a + actualB == b + actualA == actualB + } + + def basicBidirectionalNodeWithChildNodesWithIds() { + given: + GraphNode a = new GraphNode(2, "a") + GraphNode b = new GraphNode(3, "b") + GraphNode base = new GraphNode(1) + base.addChild(a) + base.addChild(b) + and: + GraphNode modifiedA = new GraphNode(2, "ax") + GraphNode modifiedB = new GraphNode(3, "by") + GraphNode modified = new GraphNode(1) + modified.addChild(modifiedA) + modified.addChild(modifiedB) + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.getChild(startBuilding().propertyName("children").collectionItem(a).build()).state == DiffNode.State.CHANGED + root.getChild(startBuilding().propertyName("children").collectionItem(a).build()).childCount() == 1 + and: + root.getChild(startBuilding().propertyName("children").collectionItem(b).build()).state == DiffNode.State.CHANGED + root.getChild(startBuilding().propertyName("children").collectionItem(b).build()).childCount() == 1 + } + + def simpleGraphWithChanges() { + given: + GraphNode base = new GraphNode(1) + GraphNode a = new GraphNode(2, "a") + a.parent = base + base.addChild(a) + and: + GraphNode modified = new GraphNode(1) + GraphNode modifiedA = new GraphNode(2, "ax") + modifiedA.parent = modified + modified.addChild(modifiedA) + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.getChild(startBuilding().propertyName("children").collectionItem(modifiedA).propertyName("value").build()).state == DiffNode.State.CHANGED + } + + def simpleGraphWithoutChanges() { + given: + GraphNode base = new GraphNode(1) + GraphNode a = new GraphNode(2, "a") + a.parent = base + base.addChild(a) + and: + GraphNode modified = new GraphNode(1) + GraphNode modifiedA = new GraphNode(2, "a") + modifiedA.parent = modified + modified.addChild(modifiedA) + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.isUntouched() + } + + def simpleGraphExtended() { + given: + GraphNode a = new GraphNode(2, "a") + GraphNode b = new GraphNode(3, "b") + GraphNode base = new GraphNode(1) + a.parent = base + base.addChild(a) + b.parent = base + base.addChild(b) + and: + GraphNode modifiedA = new GraphNode(2, "a") + GraphNode modifiedB = new GraphNode(3, "bx") + GraphNode modified = new GraphNode(1) + modifiedA.parent = modified + modified.addChild(modifiedA) + modifiedB.parent = modified + modified.addChild(modifiedB) + when: + DiffNode root = compareAndPrint(modified, base) + then: + root.getChild(startBuilding().propertyName("children").collectionItem(b).propertyName("value").build()).state == DiffNode.State.CHANGED + } + + private static void establishCircularDirectReference(GraphNode a, GraphNode b) { + a.setDirectReference(b) + b.setDirectReference(a) + } + + private static void establishCircularChildRelationship(GraphNode a, GraphNode b) { + a.addChild(b) + b.addChild(a) + } + + private static void establishParentChildRelationship(GraphNode parent, GraphNode child) { + child.setParent(parent) + parent.addChild(child) + } + + def bidirectionalGraphStackOverflow() { + given: + GraphNode a = new GraphNode(2, "a") + GraphNode b = new GraphNode(3, "b") + establishCircularDirectReference(a, b) + and: + GraphNode base = new GraphNode(1) + establishParentChildRelationship(base, a) + establishParentChildRelationship(base, b) + and: + GraphNode aa = new GraphNode(4, "aa") + GraphNode ba = new GraphNode(5, "ba") + establishCircularChildRelationship(aa, ba) + and: + establishParentChildRelationship(a, aa) + establishParentChildRelationship(b, ba) + and: + GraphNode baa = new GraphNode(6, "baa") + establishParentChildRelationship(ba, baa) + and: + GraphNode modifiedA = new GraphNode(2, "a") + GraphNode modifiedB = new GraphNode(3, "b") + establishCircularDirectReference(modifiedA, modifiedB) + and: + GraphNode modified = new GraphNode(1) + establishParentChildRelationship(modified, modifiedA) + establishParentChildRelationship(modified, modifiedB) + and: + GraphNode modifiedAA = new GraphNode(4, "aa") + GraphNode modifiedBA = new GraphNode(5, "ba-x") + establishCircularChildRelationship(modifiedAA, modifiedBA) + and: + establishParentChildRelationship(modifiedA, modifiedAA) + establishParentChildRelationship(modifiedB, modifiedBA) + and: + GraphNode modifieBAA = new GraphNode(6, "baa-y") + establishParentChildRelationship(modifiedBA, modifieBAA) + when: + DiffNode root = compareAndPrint(modified, base) + then: + def nodePathExpectedToBechanged = [ + startBuilding().propertyName("children").collectionItem(a).propertyName("children").collectionItem(aa).propertyName("children").collectionItem(ba).propertyName("value").build(), + startBuilding().propertyName("children").collectionItem(a).propertyName("children").collectionItem(aa).propertyName("children").collectionItem(ba).propertyName("children").collectionItem(baa).build(), + startBuilding().propertyName("children").collectionItem(a).propertyName("directReference").propertyName("children").collectionItem(ba).propertyName("value").build(), + startBuilding().propertyName("children").collectionItem(a).propertyName("directReference").propertyName("children").collectionItem(ba).propertyName("children").collectionItem(baa).propertyName("value").build(), + startBuilding().propertyName("children").collectionItem(b).propertyName("children").collectionItem(ba).propertyName("value").build(), + startBuilding().propertyName("children").collectionItem(b).propertyName("children").collectionItem(ba).propertyName("children").collectionItem(baa).propertyName("value").build(), + startBuilding().propertyName("children").collectionItem(b).propertyName("directReference").propertyName("children").collectionItem(aa).propertyName("children").collectionItem(ba).propertyName("value").build(), + startBuilding().propertyName("children").collectionItem(b).propertyName("directReference").propertyName("children").collectionItem(aa).propertyName("children").collectionItem(ba).propertyName("children").collectionItem(baa).propertyName("value").build(), + ] + nodePathExpectedToBechanged.each { NodePath nodePath -> + assert root.getChild(nodePath)?.state == DiffNode.State.CHANGED + } + } + + + def testWithSimpleBiDirectionalConnection() { + given: + GraphNode working1 = new GraphNode(1, "foo") + GraphNode working2 = new GraphNode(2, "bar") + establishCircularDirectReference(working1, working2) + and: + GraphNode base1 = new GraphNode(1) + GraphNode base2 = new GraphNode(2) + establishCircularDirectReference(base1, base2) + when: + DiffNode node = ObjectDifferBuilder.startBuilding() + .filtering() + .returnNodesWithState(DiffNode.State.CIRCULAR).and() + .build() + .compare(working1, base1) + then: + node.getChild('value').state == DiffNode.State.ADDED + node.getChild('directReference').state == DiffNode.State.CHANGED + node.getChild('directReference').getChild('value').state == DiffNode.State.ADDED + node.getChild('directReference').getChild('directReference').isCircular() + } + + def testWithMapBasedBiDirectionalConnection() { + given: + GraphNode working1 = new GraphNode(1, "foo") + GraphNode working2 = new GraphNode(2, "bar") + working1.getMap().put("foo", working2) + working2.getMap().put("bar", working1) + and: + GraphNode base1 = new GraphNode(1) + GraphNode base2 = new GraphNode(2) + base1.getMap().put("foo", base2) + base2.getMap().put("bar", base1) + and: + ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding() + objectDifferBuilder.filtering().returnNodesWithState(DiffNode.State.CIRCULAR) + ObjectDiffer differ = objectDifferBuilder.build() + when: + DiffNode node = differ.compare(working1, base1) + then: + node.getChild(startBuilding().propertyName("map").mapKey("foo").propertyName("map").mapKey("bar").build()).isCircular() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphITCase.java b/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphITCase.java deleted file mode 100644 index ec3b9d3b..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphITCase.java +++ /dev/null @@ -1,561 +0,0 @@ -/* - * Copyright 2012 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.issues.issue15; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.helper.*; -import de.danielbechler.diff.visitor.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.NodePath.*; -import static de.danielbechler.diff.helper.NodeAssertions.assertThat; -import static org.fest.assertions.api.Assertions.assertThat; - -/** - * This tests have been provided by a user who had some trouble with circular relationships. The introduction - * of the {@link CircularReferenceDetector} fixed his problems. To avoid regression, these integration tests - * will be kept until other tests cover the same scenarios. - * - * @author https://github.com/oplohmann (original author) - * @author Daniel Bechler (modifications) - */ -public class GraphITCase -{ - private static final boolean PRINT_ENABLED = true; - - @Test - public void basicNode() - { - final GraphNode base = new GraphNode(1); - final GraphNode a = new GraphNode(2, "a"); - base.setDirectReference(a); - - final GraphNode modified = new GraphNode(1); - final GraphNode modifiedA = new GraphNode(2, "ax"); - modified.setDirectReference(modifiedA); - - final DiffNode root = compareAndPrint(modified, base); - - assertThat(root).root().hasChildren(1); - assertThat(root).child("directReference", "value").hasState(DiffNode.State.CHANGED).hasNoChildren(); - } - - @Test - public void basicNodeWithDirectReferences() - { - final GraphNode base = new GraphNode(1); - final GraphNode a = new GraphNode(2, "a"); - base.setDirectReference(a); - - final GraphNode modified = new GraphNode(1); - final GraphNode modifiedA = new GraphNode(2, "ax"); - modified.setDirectReference(modifiedA); - - final DiffNode root = compareAndPrint(modified, base); - - assertThat(root).root().hasChildren(1); - assertThat(root).child("directReference", "value").hasState(DiffNode.State.CHANGED).hasNoChildren(); - } - - @Test - public void basicBidirectionalWithChildren() - { - final GraphNode base = new GraphNode(1); - final GraphNode a = new GraphNode(2, "a"); - final GraphNode b = new GraphNode(3, "b"); - base.addChildren(a, b); - establishCircularDirectReference(a, b); - - final GraphNode modified = new GraphNode(1); - final GraphNode modifiedA = new GraphNode(2, "ax"); - final GraphNode modifiedB = new GraphNode(3, "by"); - modified.addChildren(modifiedA, modifiedB); - establishCircularDirectReference(modifiedA, modifiedB); - - final DiffNode root = compareAndPrint(modified, base); - - assertThat(root).root().hasChildren(1); - assertThat(root).child("children").hasChildren(2); - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(a)) - .hasState(DiffNode.State.CHANGED); - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b)) - .hasState(DiffNode.State.CHANGED); - } - - @Test //(timeout = 1000) - public void basicBidirectionalWithChildrenAndMaps() - { - final GraphNode a = new GraphNode(2, "a"); - final GraphNode b = new GraphNode(3, "b"); - establishCircularDirectReference(a, b); - a.getMap().put("node-b", b); - a.getMap().put("node-b-again", b); - b.getMap().put("node-a", a); - - final GraphNode base = new GraphNode(1); - base.getMap().put("a", "a"); - base.getChildren().add(b); - - final GraphNode modified = new GraphNode(1); - final GraphNode modifiedA = new GraphNode(2, "ax"); - modified.getMap().put("a", "az"); - final GraphNode modifiedB = new GraphNode(3, "by"); - modified.addChild(modifiedB); - establishCircularDirectReference(modifiedA, modifiedB); - modifiedA.getMap().put("node-b", modifiedB); - modifiedB.getMap().put("node-a", modifiedA); - - final DiffNode root = compareAndPrint(modified, base); - - /* - * one could assume that this node would be marked as removed, but since the instance it represents - * is a parent of it, we cannot reliably test, whether the node has been changed. (added or removed - * is easier, but since changed is not yet possible, that would be inconsistent) - */ - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("directReference") - .withPropertyName("map") - .withMapKey("node-b-again")) - .hasState(DiffNode.State.CIRCULAR); // TODO Change circular reference detection, to allow returning the expected "REMOVED" state - - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("directReference") - .withPropertyName("value")).hasState(DiffNode.State.CHANGED); - - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("map") - .withMapKey("node-a") - .withPropertyName("map") - .withMapKey("node-b-again")) - .hasState(DiffNode.State.CIRCULAR); // TODO Change circular reference detection, to allow returning the expected "REMOVED" state - - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("map") - .withMapKey("node-a") - .withPropertyName("value")).hasState(DiffNode.State.CHANGED); - - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("value")).hasState(DiffNode.State.CHANGED); - - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("map") - .withMapKey("a")).hasState(DiffNode.State.CHANGED); - } - - @Test - public void basicBidirectionalWithoutChildren() - { - final GraphNode a = new GraphNode(1, "a"); - final GraphNode b = new GraphNode(2, "b"); - establishCircularDirectReference(a, b); - - final GraphNode modifiedA = new GraphNode(1, "ax"); - final GraphNode modifiedB = new GraphNode(2, "by"); - establishCircularDirectReference(modifiedA, modifiedB); - - final DiffNode root = compareAndPrint(modifiedA, a); - - assertThat(root).child("directReference", "value").hasState(DiffNode.State.CHANGED).hasNoChildren(); - assertThat(root).child("value").hasState(DiffNode.State.CHANGED).hasNoChildren(); - } - - @Test - public void basicNodeWithDirectReferences2() - { - final GraphNode base = new GraphNode("base"); - final GraphNode a = new GraphNode("a"); - base.setDirectReference(a); - - final GraphNode modified = new GraphNode("modified"); - final GraphNode modifiedA = new GraphNode("ax"); - modified.setDirectReference(modifiedA); - - final DiffNode root = compareAndPrint(modified, base); - - assertThat(root).child("directReference", "value").hasState(DiffNode.State.CHANGED).hasNoChildren(); - assertThat(root).child("value").hasState(DiffNode.State.CHANGED).hasNoChildren(); - } - - /** Does not detect any changes since no primary key defined for each node */ - @Test - public void basicBidirectionalNodeWithChildNodes() - { - final GraphNode base = new GraphNode(); - final GraphNode a = new GraphNode("a"); - final GraphNode b = new GraphNode("b"); - base.getChildren().add(a); - base.getChildren().add(b); - - final GraphNode modified = new GraphNode(); - final GraphNode modifiedA = new GraphNode("a"); - final GraphNode modifiedB = new GraphNode("bx"); - modified.getChildren().add(modifiedA); - modified.getChildren().add(modifiedB); - - final DiffNode root = compareAndPrint(modified, base); - - NodeAssertions.assertThat(root).root().hasState(DiffNode.State.UNTOUCHED); // not a bug! - - // NOTE: This is expected, since Collections (and java-object-diff) rely heavily on the proper - // implementation of hashCode and equals. The GraphNode uses the ID as sole criteria in it's - // equality check. Since all instances created without an explicit ID, will default to -1, all - // GraphNodes in this example are technically equal. - // - // The following test proves this: - - final GraphNode actualA = base.getChildren().get(0); - final GraphNode actualB = base.getChildren().get(1); - assertThat(actualA).isEqualTo(a); - assertThat(actualB).isEqualTo(b); - assertThat(actualA).isEqualTo(actualB); - } - - @Test - public void basicBidirectionalNodeWithChildNodesWithIds() - { - final GraphNode a = new GraphNode(2, "a"); - final GraphNode b = new GraphNode(3, "b"); - final GraphNode base = new GraphNode(1); - base.addChild(a); - base.addChild(b); - - final GraphNode modifiedA = new GraphNode(2, "ax"); - final GraphNode modifiedB = new GraphNode(3, "by"); - final GraphNode modified = new GraphNode(1); - modified.addChild(modifiedA); - modified.addChild(modifiedB); - - final DiffNode root = compareAndPrint(modified, base); - - NodeAssertions.assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(a)) - .hasState(DiffNode.State.CHANGED) - .hasChildren(1); - - NodeAssertions.assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b)) - .hasState(DiffNode.State.CHANGED) - .hasChildren(1); - } - - @Test - public void simpleGraphWithChanges() - { - final GraphNode base = new GraphNode(1); - final GraphNode a = new GraphNode(2, "a"); - establishParentChildRelationship(base, a); - final GraphNode modified = new GraphNode(1); - final GraphNode modifiedA = new GraphNode(2, "ax"); - establishParentChildRelationship(modified, modifiedA); - - final DiffNode root = compareAndPrint(modified, base); - - NodeAssertions.assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(modifiedA) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - } - - @Test - public void simpleGraphWithoutChanges() - { - final GraphNode base = new GraphNode(1); - final GraphNode a = new GraphNode(2, "a"); - establishParentChildRelationship(base, a); - final GraphNode modified = new GraphNode(1); - final GraphNode modifiedA = new GraphNode(2, "a"); - establishParentChildRelationship(modified, modifiedA); - - final DiffNode root = compareAndPrint(modified, base); - - assertThat(root).root().hasState(DiffNode.State.UNTOUCHED); - } - - @Test - public void simpleGraphExtended() - { - final GraphNode a = new GraphNode(2, "a"); - final GraphNode b = new GraphNode(3, "b"); - final GraphNode base = new GraphNode(1); - establishParentChildRelationship(base, a); - establishParentChildRelationship(base, b); - - final GraphNode modifiedA = new GraphNode(2, "a"); - final GraphNode modifiedB = new GraphNode(3, "bx"); - final GraphNode modified = new GraphNode(1); - establishParentChildRelationship(modified, modifiedA); - establishParentChildRelationship(modified, modifiedB); - - final DiffNode root = compareAndPrint(modified, base); - - assertThat(root).child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - } - - @Test - public void bidirectionalGraphStackOverflow() - { - final GraphNode a = new GraphNode(2, "a"); - final GraphNode b = new GraphNode(3, "b"); - establishCircularDirectReference(a, b); - - final GraphNode base = new GraphNode(1); - establishParentChildRelationship(base, a); - establishParentChildRelationship(base, b); - - final GraphNode aa = new GraphNode(4, "aa"); - final GraphNode ba = new GraphNode(5, "ba"); - establishCircularChildRelationship(aa, ba); - - establishParentChildRelationship(a, aa); - establishParentChildRelationship(b, ba); - - final GraphNode baa = new GraphNode(6, "baa"); - establishParentChildRelationship(ba, baa); - - final GraphNode modifiedA = new GraphNode(2, "a"); - final GraphNode modifiedB = new GraphNode(3, "b"); - establishCircularDirectReference(modifiedA, modifiedB); - - final GraphNode modified = new GraphNode(1); - establishParentChildRelationship(modified, modifiedA); - establishParentChildRelationship(modified, modifiedB); - - final GraphNode modifiedAA = new GraphNode(4, "aa"); - final GraphNode modifiedBA = new GraphNode(5, "ba-x"); - establishCircularChildRelationship(modifiedAA, modifiedBA); - - establishParentChildRelationship(modifiedA, modifiedAA); - establishParentChildRelationship(modifiedB, modifiedBA); - - final GraphNode modifieBAA = new GraphNode(6, "baa-y"); - establishParentChildRelationship(modifiedBA, modifieBAA); - - final DiffNode root = compareAndPrint(modified, base); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(a) - .withPropertyName("children") - .withCollectionItem(aa) - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("children") - .withCollectionItem(baa)) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(a) - .withPropertyName("children") - .withCollectionItem(aa) - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(a) - .withPropertyName("directReference") - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("children") - .withCollectionItem(baa) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(a) - .withPropertyName("directReference") - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("children") - .withCollectionItem(baa) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("directReference") - .withPropertyName("children") - .withCollectionItem(aa) - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("children") - .withCollectionItem(baa) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - - NodeAssertions.assertThat(root) - .child(createBuilder() - .withRoot() - .withPropertyName("children") - .withCollectionItem(b) - .withPropertyName("directReference") - .withPropertyName("children") - .withCollectionItem(aa) - .withPropertyName("children") - .withCollectionItem(ba) - .withPropertyName("value")) - .hasState(DiffNode.State.CHANGED); - } - - private static DiffNode compareAndPrint(final GraphNode modified, final GraphNode base) - { - final DiffNode root = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(modified, base); - if (PRINT_ENABLED) - { - root.visit(new PrintingVisitor(modified, base)); - } - return root; - } - - @Test - public void testWithSimpleBiDirectionalConnection() - { - final GraphNode working1 = new GraphNode(1, "foo"); - final GraphNode working2 = new GraphNode(2, "bar"); - establishCircularDirectReference(working1, working2); - - final GraphNode base1 = new GraphNode(1); - final GraphNode base2 = new GraphNode(2); - establishCircularDirectReference(base1, base2); - - final ObjectDifferBuilder configuration = ObjectDifferBuilder.startBuilding(); - configuration.configure().filtering().returnNodesWithState(DiffNode.State.CIRCULAR); - final DiffNode node = configuration.build().compare(working1, base1); - node.visit(new NodeHierarchyVisitor()); - - assertThat(node).child("value").hasState(DiffNode.State.ADDED); - assertThat(node).child("directReference").hasState(DiffNode.State.CHANGED); - assertThat(node).child("directReference", "value").hasState(DiffNode.State.ADDED); - assertThat(node).child("directReference", "directReference").isCircular(); - } - - @Test - public void testWithMapBasedBiDirectionalConnection() - { - final GraphNode working1 = new GraphNode(1, "foo"); - final GraphNode working2 = new GraphNode(2, "bar"); - working1.getMap().put("foo", working2); - working2.getMap().put("bar", working1); - - final GraphNode base1 = new GraphNode(1); - final GraphNode base2 = new GraphNode(2); - base1.getMap().put("foo", base2); - base2.getMap().put("bar", base1); - - final ObjectDifferBuilder objectDifferBuilder = ObjectDifferBuilder.startBuilding(); - objectDifferBuilder.configure().filtering().returnNodesWithState(DiffNode.State.CIRCULAR); - final ObjectDiffer differ = objectDifferBuilder.build(); - - final DiffNode node = differ.compare(working1, base1); - node.visit(new NodeHierarchyVisitor()); - - NodeAssertions.assertThat(node) - .child(createBuilder() - .withRoot() - .withPropertyName("map") - .withMapKey("foo") - .withPropertyName("map") - .withMapKey("bar")) - .isCircular(); - } - - private static void establishParentChildRelationship(final GraphNode parent, final GraphNode child) - { - child.setParent(parent); - parent.addChild(child); - } - - private static void establishCircularChildRelationship(final GraphNode a, final GraphNode b) - { - a.addChild(b); - b.addChild(a); - } - - private static void establishCircularDirectReference(final GraphNode a, final GraphNode b) - { - a.setDirectReference(b); - b.setDirectReference(a); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphNode.java b/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphNode.java index cc6aeead..11df3119 100644 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphNode.java +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue15/GraphNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,11 +16,15 @@ package de.danielbechler.diff.issues.issue15; -import org.slf4j.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.util.*; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; -import static java.util.Arrays.*; +import static java.util.Arrays.asList; /** * @author https://github.com/oplohmann (original author) @@ -39,6 +43,11 @@ public class GraphNode private GraphNode parent; private GraphNode directReference; + public GraphNode(final int id) + { + this(id, null); + } + public GraphNode(final int id, final String value) { if (id == -1) @@ -50,13 +59,8 @@ public GraphNode(final int id, final String value) this.value = value; } - public GraphNode(final int id) - { - this(id, null); - } - /** - * @see GraphNode(int, String) + * @see #GraphNode(int, String) * @deprecated You should always provide an ID. */ @Deprecated @@ -66,7 +70,7 @@ public GraphNode(final String value) } /** - * @see GraphNode(int, String) + * @see #GraphNode(int, String) * @deprecated You should always provide an ID. */ @Deprecated @@ -75,11 +79,6 @@ public GraphNode() this(-1, null); } - public void setDirectReference(final GraphNode directReference) - { - this.directReference = directReference; - } - public void addChild(final GraphNode child) { addChildren(child); @@ -115,6 +114,11 @@ public GraphNode getDirectReference() return directReference; } + public void setDirectReference(final GraphNode directReference) + { + this.directReference = directReference; + } + public List getChildren() { return children; diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue155/MapIssuesTest.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue155/MapIssuesTest.groovy new file mode 100644 index 00000000..afe8edcf --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue155/MapIssuesTest.groovy @@ -0,0 +1,80 @@ +/* + * Copyright 2016 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.issues.issue155 + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.identity.IdentityStrategy +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 spock.lang.Specification + +public class MapIssuesTest extends Specification { + + static class LocationIdentityStrategy implements IdentityStrategy { + + @Override + boolean equals(workingLocation, baseLocation) { + if (workingLocation.city == baseLocation.city) { + return true + } + return false + } + } + + static class LeafNodeCountingVisitor implements DiffNode.Visitor { + int leafNodeCount = 0 + + @Override + void node(DiffNode node, Visit visit) { + if (node.childCount() == 0) { + leafNodeCount++; + } + } + } + + def 'compare with collections'() { + given: + def sharedCity = 'city' + def working = [name: 'alice', locations: [[street: 'street1', city: sharedCity]]] + def base = [name: 'alice', locations: [[street: 'street2', city: sharedCity]]] + + when: + def locationPath = NodePath.startBuilding().mapKey('locations').build() + def locationIdentityStrategy = new LocationIdentityStrategy() + def node = ObjectDifferBuilder.startBuilding() + .identity() + .ofCollectionItems(locationPath).via(locationIdentityStrategy) + .and().build() + .compare(working, base); + + then: + def leafNodeCountingVisitor = new LeafNodeCountingVisitor() + node.visit(leafNodeCountingVisitor) + leafNodeCountingVisitor.leafNodeCount == 1 + + and: + def streetNode = node.getChild([ + new MapKeyElementSelector('locations'), + new CollectionItemElementSelector([city: sharedCity]), + new MapKeyElementSelector('street') + ]) + streetNode.state == DiffNode.State.CHANGED + } +} \ No newline at end of file diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue3/IgnoreByPropertyNameIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue3/IgnoreByPropertyNameIT.groovy new file mode 100644 index 00000000..4649f18b --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue3/IgnoreByPropertyNameIT.groovy @@ -0,0 +1,66 @@ +/* + * 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.issues.issue3 + +import de.danielbechler.diff.ObjectDifferBuilder +import spock.lang.Specification + +/** + * Created by dbechler. + */ +class IgnoreByPropertyNameIT extends Specification { + + def "ignoring by property name"() { + given: + def base = new FooContainer("foo-base", "bar-base", "test-base") + def working = new FooContainer("foo-working", "bar-working", "test-working") + + and: "some properties are excluded by name" + def builder = ObjectDifferBuilder.startBuilding() + builder.inclusion().exclude().propertyName('foo').propertyName('bar') + def objectDiffer = builder.build() + + when: + def result = objectDiffer.compare(working, base) + + then: "excluded properties should not have been diffed" + result.childCount() == 1 + result.getChild("barContainer") != null + result.getChild("barContainer").childCount() == 1 + result.getChild("barContainer").getChild("test") != null + } + + class FooContainer { + def foo + def barContainer + + FooContainer(foo, bar, test) { + this.foo = foo + this.barContainer = new BarContainer(bar, test) + } + } + + class BarContainer { + def bar + def test + + BarContainer(bar, test) { + this.bar = bar + this.test = test + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue3/IgnoreTest.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue3/IgnoreTest.groovy deleted file mode 100644 index ed2fd5dc..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue3/IgnoreTest.groovy +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.issues.issue3 - -import de.danielbechler.diff.ObjectDifferBuilder -import spock.lang.Ignore -import spock.lang.Specification - -/** - * Created by dbechler. - */ -class IgnoreTest extends Specification { - - @Ignore - def "ignoring by property name"() { - given: - def base = new FooContainer("foo-base", "bar-base", "test-base") - def working = new FooContainer("foo-working", "bar-working", "test-working") - - and: "some properties are excluded by name" - def builder = ObjectDifferBuilder.startBuilding() - builder.configure().inclusion().toExclude().propertyNames('foo', 'bar') - def objectDiffer = builder.build() - - when: - def result = objectDiffer.compare(working, base) - - then: "excluded properties should not have been diffed" - result.children.size() == 1 - result.getChild("barContainer") != null - result.getChild("barContainer").children.size() == 1 - result.getChild("barContainer").getChild("test") != null - } - - class FooContainer { - def foo - def barContainer - - FooContainer(foo, bar, test) { - this.foo = foo - this.barContainer = new BarContainer(bar, test) - } - } - - class BarContainer { - def bar - def test - - BarContainer(bar, test) { - this.bar = bar - this.test = test - } - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue38/Issue38.java b/src/integration-test/java/de/danielbechler/diff/issues/issue38/Issue38.java index 2a785bc4..b44a9d07 100644 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue38/Issue38.java +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue38/Issue38.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,8 +16,10 @@ package de.danielbechler.diff.issues.issue38; -import de.danielbechler.diff.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDiffer; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.PrintingVisitor; public class Issue38 { @@ -25,52 +27,6 @@ private Issue38() { } - // class has public access - public static class TestIntBeanPublic - { - - private int value; - - public int getValue() - { - return this.value; - } - - public void setValue(final int value) - { - this.value = value; - } - - @Override - public String toString() - { - return "TestIntBeanPublic [value=" + this.value + "]"; - } - } - - // class has private access - private static class TestIntBeanPrivate - { - - private int value; - - public int getValue() - { - return this.value; - } - - public void setValue(final int value) - { - this.value = value; - } - - @Override - public String toString() - { - return "TestIntBeanPrivate [value=" + this.value + "]"; - } - } - public static void testIntegerFailsPublic() { @@ -80,7 +36,7 @@ public static void testIntegerFailsPublic() TestIntBeanPublic base = new TestIntBeanPublic(); base.setValue(1); - ObjectDiffer differ = ObjectDifferBuilder.buildDefaultObjectDiffer(); + ObjectDiffer differ = ObjectDifferBuilder.buildDefault(); final DiffNode root = differ.compare(working, base); root.visit(new PrintingVisitor(working, base)); @@ -95,7 +51,7 @@ public static void testIntegerWorksPrivate() TestIntBeanPrivate base = new TestIntBeanPrivate(); base.setValue(1); - ObjectDiffer differ = ObjectDifferBuilder.buildDefaultObjectDiffer(); + ObjectDiffer differ = ObjectDifferBuilder.buildDefault(); final DiffNode root = differ.compare(working, base); root.visit(new PrintingVisitor(working, base)); @@ -110,7 +66,7 @@ public static void testIntegerWorksPublic() TestIntBeanPublic base = new TestIntBeanPublic(); base.setValue(1); - ObjectDiffer differ = ObjectDifferBuilder.buildDefaultObjectDiffer(); + ObjectDiffer differ = ObjectDifferBuilder.buildDefault(); final DiffNode root = differ.compare(working, base); root.visit(new PrintingVisitor(working, base)); @@ -124,7 +80,53 @@ public static void main(final String[] args) System.out.println("The following comparison does not work properly (detects removal)"); Issue38.testIntegerFailsPublic(); System.out - .println("The following comparison works properly bewcause the class has private access (detects change)"); + .println("The following comparison works properly bewcause the class has private access (detects change)"); Issue38.testIntegerWorksPrivate(); } + + // class has public access + public static class TestIntBeanPublic + { + + private int value; + + public int getValue() + { + return this.value; + } + + public void setValue(final int value) + { + this.value = value; + } + + @Override + public String toString() + { + return "TestIntBeanPublic [value=" + this.value + "]"; + } + } + + // class has private access + private static class TestIntBeanPrivate + { + + private int value; + + public int getValue() + { + return this.value; + } + + public void setValue(final int value) + { + this.value = value; + } + + @Override + public String toString() + { + return "TestIntBeanPrivate [value=" + this.value + "]"; + } + } } diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue43/Issue43IT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue43/Issue43IT.groovy new file mode 100644 index 00000000..c82ffaed --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue43/Issue43IT.groovy @@ -0,0 +1,77 @@ +/* + * 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.issues.issue43 + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.path.NodePath +import groovy.transform.EqualsAndHashCode +import spock.lang.Issue +import spock.lang.Specification + +@Issue('https://github.com/SQiShER/java-object-diff/issues/43') +public class Issue43IT extends Specification { + + def objectDifferBuilder = ObjectDifferBuilder.startBuilding() + + def 'should diff things'() { + given: + // @formatter:off + ['things', 'include'].collect({ property -> NodePath.with property }).each { + objectDifferBuilder + .comparison() + .ofNode(it).toUseEqualsMethod() + .and() + .inclusion() + .include().node(it) + } + // @formatter:on + and: + def thingOne = new Thing('a', 'b') + def thingTwo = new Thing('aa', 'bb') + and: + def first = new ThingHolder([thingOne] as Set, 'ignore', 'include') + def second = new ThingHolder([thingTwo] as Set, 'ignore this change', 'include') + when: + def node = objectDifferBuilder.build().compare(first, second) + then: + node.changed + } + + @EqualsAndHashCode + private static class Thing { + final String a + final String b + + Thing(String a, String b) { + this.a = a + this.b = b + } + } + + @EqualsAndHashCode + private static class ThingHolder { + final Set things + final String ignore + final String include + + ThingHolder(Set things, String ignore, String include) { + this.things = things + this.ignore = ignore + this.include = include + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue43/Issue43ITCase.java b/src/integration-test/java/de/danielbechler/diff/issues/issue43/Issue43ITCase.java deleted file mode 100644 index 21ce8d3a..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue43/Issue43ITCase.java +++ /dev/null @@ -1,162 +0,0 @@ -package de.danielbechler.diff.issues.issue43; - -import de.danielbechler.diff.*; -import org.testng.annotations.*; - -import java.util.*; - -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.MatcherAssert.*; - -/** https://github.com/SQiShER/java-object-diff/issues/43 */ -@SuppressWarnings("ALL") -public class Issue43ITCase -{ - private class Thing - { - private final String a; - private final String b; - - public Thing(final String a, final String b) - { - this.a = a; - this.b = b; - } - - public String getA() - { - return a; - } - - public String getB() - { - return b; - } - - @Override - public boolean equals(final Object o) - { - if (this == o) - { - return true; - } - if (!(o instanceof Thing)) - { - return false; - } - - final Thing thing = (Thing) o; - - if (a != null ? !a.equals(thing.a) : thing.a != null) - { - return false; - } - if (b != null ? !b.equals(thing.b) : thing.b != null) - { - return false; - } - - return true; - } - - @Override - public int hashCode() - { - int result = a != null ? a.hashCode() : 0; - result = 31 * result + (b != null ? b.hashCode() : 0); - return result; - } - } - - private class ThingHolder - { - private final Set things; - private final String ignore; - private final String include; - - private ThingHolder(final Set things, final String ignore, final String include) - { - this.things = things; - this.ignore = ignore; - this.include = include; - } - - public Set getThings() - { - return things; - } - - public String getIgnore() - { - return ignore; - } - - public String getInclude() - { - return include; - } - - @Override - public boolean equals(final Object o) - { - if (this == o) - { - return true; - } - if (!(o instanceof ThingHolder)) - { - return false; - } - - final ThingHolder that = (ThingHolder) o; - - if (ignore != null ? !ignore.equals(that.ignore) : that.ignore != null) - { - return false; - } - if (include != null ? !include.equals(that.include) : that.include != null) - { - return false; - } - if (things != null ? !things.equals(that.things) : that.things != null) - { - return false; - } - - return true; - } - - @Override - public int hashCode() - { - int result = things != null ? things.hashCode() : 0; - result = 31 * result + (ignore != null ? ignore.hashCode() : 0); - result = 31 * result + (include != null ? include.hashCode() : 0); - return result; - } - } - - @Test - public void shouldDiffThings() - { - final List propertyNames = asList("things", "include"); - ObjectDifferBuilder builder = ObjectDifferBuilder.startBuilding(); - for (final String name : propertyNames) - { - final NodePath nodePath = NodePath.buildWith(name); - builder.configure().comparison().ofNode(nodePath).toUseEqualsMethod(); - builder.configure().inclusion().toInclude().nodes(nodePath); - } - - final Thing thingOne = new Thing("a", "b"); - final Thing thingTwo = new Thing("aa", "bb"); - - final ThingHolder first = new ThingHolder(singleton(thingOne), "ignore", "include"); - final ThingHolder second = new ThingHolder(singleton(thingTwo), "ignore this change", "include"); - final DiffNode compareResults = builder.build().compare(first, second); - - assertThat(compareResults.isChanged(), is(true)); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue66/MainApp.java b/src/integration-test/java/de/danielbechler/diff/issues/issue66/MainApp.java index 97591cfb..67b4ec36 100644 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue66/MainApp.java +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue66/MainApp.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -16,8 +16,10 @@ package de.danielbechler.diff.issues.issue66; -import de.danielbechler.diff.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.ObjectDiffer; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.Visit; public class MainApp { @@ -27,7 +29,7 @@ private MainApp() public static void main(final String[] args) { - final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); + final ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefault(); final TopHat hat1 = new TopHat(1, 10); final TopHat hat2 = new TopHat(2, 20); @@ -39,7 +41,7 @@ public static void main(final String[] args) root.visit(new DiffNode.Visitor() { - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { System.out.print(node.getPath() + " :: "); System.out.print(node.canonicalGet(p1)); diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue70/PersonDiffIT.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue70/PersonDiffIT.groovy new file mode 100644 index 00000000..69ceaab2 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue70/PersonDiffIT.groovy @@ -0,0 +1,49 @@ +/* + * 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.issues.issue70 + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.PrintingVisitor +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +public class PersonDiffIT extends Specification { + + def 'include collection attribute'() { + given: + def base = new Person('Gulen Chongtham', [ + 'Hola Espanyol', + 'Vicky Boss' + ]) + def working = new Person('Gulen Chongthamm', [ + 'Hola Espanyol', + 'Vicky Boss', + 'Roger Harper' + ]) + and: + def objectDiffer = ObjectDifferBuilder.startBuilding() + .inclusion() + .include().node(NodePath.with('aliases')).and() + .build() + when: + def node = objectDiffer.compare(working, base) + def printingVisitor = new PrintingVisitor(working, base) + node.visit(printingVisitor) + then: + node.hasChanges() + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue70/PersonDiffITCase.java b/src/integration-test/java/de/danielbechler/diff/issues/issue70/PersonDiffITCase.java deleted file mode 100644 index 8d0984dc..00000000 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue70/PersonDiffITCase.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danielbechler.diff.issues.issue70; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.helper.*; -import de.danielbechler.diff.visitor.*; -import org.testng.annotations.*; - -import java.util.*; - -public class PersonDiffITCase -{ - @Test - public void testIncludeCollectionAttribute() - { - final Person a = new Person("Gulen Chongtham", Arrays.asList("Hola Espanyol", "Vicky Boss")); - final Person b = new Person("Gulen Chongthamm", Arrays.asList("Hola Espanyol", "Vicky Boss", "Roger Harper")); - - final ObjectDifferBuilder builder = ObjectDifferBuilder.startBuilding(); - builder.configure().inclusion().toInclude().nodes(NodePath.buildWith("aliases")); - final ObjectDiffer differ = builder.build(); - - final DiffNode root = differ.compare(b, a); - root.visit(new PrintingVisitor(b, a)); - - NodeAssertions.assertThat(root).root().hasChanges(); - } -} diff --git a/src/integration-test/java/de/danielbechler/diff/issues/issue77/Issue77ITCase.groovy b/src/integration-test/java/de/danielbechler/diff/issues/issue77/Issue77IT.groovy similarity index 60% rename from src/integration-test/java/de/danielbechler/diff/issues/issue77/Issue77ITCase.groovy rename to src/integration-test/java/de/danielbechler/diff/issues/issue77/Issue77IT.groovy index 3e354cf3..36c2dc5b 100644 --- a/src/integration-test/java/de/danielbechler/diff/issues/issue77/Issue77ITCase.groovy +++ b/src/integration-test/java/de/danielbechler/diff/issues/issue77/Issue77IT.groovy @@ -22,31 +22,25 @@ import spock.lang.Specification /** * @author Daniel Bechler */ -class Issue77ITCase extends Specification -{ - def "NullPointerException on null unsafe Comparable implementations"() - { - expect: - ObjectDifferBuilder.buildDefaultObjectDiffer().compare(new SomeObject(BigDecimal.ONE), new SomeObject(null)); - } +class Issue77IT extends Specification { + def "NullPointerException on null unsafe Comparable implementations"() { + expect: + ObjectDifferBuilder.buildDefault().compare(new SomeObject(BigDecimal.ONE), new SomeObject(null)); + } - class SomeObject - { - BigDecimal value; + class SomeObject { + BigDecimal value; - SomeObject(BigDecimal value) - { - this.value = value; - } + SomeObject(BigDecimal value) { + this.value = value; + } - BigDecimal getValue() - { - return value - } + BigDecimal getValue() { + return value + } - void setValue(final BigDecimal value) - { - this.value = value - } - } + void setValue(final BigDecimal value) { + this.value = value + } + } } diff --git a/src/integration-test/java/de/danielbechler/diff/node/AccessFieldAnnotationIT.groovy b/src/integration-test/java/de/danielbechler/diff/node/AccessFieldAnnotationIT.groovy new file mode 100644 index 00000000..6f4884e6 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/node/AccessFieldAnnotationIT.groovy @@ -0,0 +1,49 @@ +/* + * 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.node + +import de.danielbechler.diff.ObjectDiffer +import de.danielbechler.diff.ObjectDifferBuilder +import spock.lang.Specification + +class AccessFieldAnnotationIT extends Specification { + + ObjectDiffer objectDiffer = ObjectDifferBuilder.buildDefault() + + def 'access field annotation'() { + given: + def working = new Foo(value: 'working') + def base = new Foo(value: 'base') + when: + DiffNode node = objectDiffer.compare(working, base) + then: + node.getChild('value').getFieldAnnotation(TestAnnotation).value() == "it works" + } + + class Foo { + @TestAnnotation("it works") + private String value + + public String getValue() { + return value + } + + public void setValue(String value) { + this.value = value + } + } +} diff --git a/src/integration-test/java/de/danielbechler/diff/node/DiffNodeIT.groovy b/src/integration-test/java/de/danielbechler/diff/node/DiffNodeIT.groovy new file mode 100644 index 00000000..205845e9 --- /dev/null +++ b/src/integration-test/java/de/danielbechler/diff/node/DiffNodeIT.groovy @@ -0,0 +1,100 @@ +/* + * 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.node + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.instantiation.InstanceFactory +import de.danielbechler.diff.instantiation.TypeInstantiationException +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +import spock.lang.Subject + +@Subject(DiffNode) +class DiffNodeIT extends Specification { + + def 'canonicalSet() should create missing objects along the path'() { + given: + def base = new A(b: new B(c: new C(d: new D(x: 'to-be-changed')))) + def working = new A(b: new B(c: new C(d: new D(x: 'changed')))) + + and: + def instanceFactory = new InstanceFactory() { + Object newInstanceOfType(Class type) { + if (type.isAssignableFrom(C)) { + return C.newInstance() + } + return null + } + } + + when: + def target = new A() + def node = ObjectDifferBuilder.startBuilding() + .introspection() + .setInstanceFactory(instanceFactory) + .and() + .build() + .compare(working, base) + node.getChild(NodePath.with('b', 'c', 'd', 'x')).canonicalSet(target, 'yay') + + then: + target.b instanceof B + target.b.c instanceof C + target.b.c.d instanceof D + target.b.c.d.x == 'yay' + } + + def 'canonicalSet() should throw exception when missing object along the path cannot be instantiated'() { + given: + def base = new A(b: new B(c: new C(d: new D(x: 'to-be-changed')))) + def working = new A(b: new B(c: new C(d: new D(x: 'changed')))) + + when: + def target = new A() + def node = ObjectDifferBuilder.buildDefault().compare(working, base) + node.getChild(NodePath.with('b', 'c', 'd', 'x')).canonicalSet(target, 'yay') + + then: + TypeInstantiationException exception = thrown(TypeInstantiationException) + exception.type == C + exception.reason == "Type doesn't have a public non-arg constructur" + exception.message == "Failed to create instance of type '" + exception.type + "'. Reason: " + exception.reason + } + + public static class A { + B b + } + + public static class B { + C c + } + + public static class C { + D d + + private C() { + } + + public static C newInstance() { + return new C() + } + } + + public static class D { + def x + } +} diff --git a/src/test/java/de/danielbechler/diff/helper/AssertionsExtensions.java b/src/integration-test/java/de/danielbechler/diff/node/TestAnnotation.java similarity index 59% rename from src/test/java/de/danielbechler/diff/helper/AssertionsExtensions.java rename to src/integration-test/java/de/danielbechler/diff/node/TestAnnotation.java index cb3ebd2d..3b8a3463 100644 --- a/src/test/java/de/danielbechler/diff/helper/AssertionsExtensions.java +++ b/src/integration-test/java/de/danielbechler/diff/node/TestAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,23 +14,18 @@ * limitations under the License. */ -package de.danielbechler.diff.helper; +package de.danielbechler.diff.node; -import org.fest.assertions.core.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -/** @author Daniel Bechler */ -public class AssertionsExtensions +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Inherited +public @interface TestAnnotation { - public static Condition> ofType(final Class aClass) - { - return new Condition>() - { - @Override - public boolean matches(final Class value) - { - return aClass.equals(value); - } - }; - } - + String value(); } diff --git a/src/main/java/de/danielbechler/diff/CategoryAwareAccessor.java b/src/main/java/de/danielbechler/diff/CategoryAwareAccessor.java deleted file mode 100644 index 96ac151f..00000000 --- a/src/main/java/de/danielbechler/diff/CategoryAwareAccessor.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.danielbechler.diff; - -import java.util.*; - -/** @author Daniel Bechler */ -public interface CategoryAwareAccessor extends Accessor -{ - Set getCategories(); -} diff --git a/src/main/java/de/danielbechler/diff/CategoryConfiguration.java b/src/main/java/de/danielbechler/diff/CategoryConfiguration.java deleted file mode 100644 index 33553f18..00000000 --- a/src/main/java/de/danielbechler/diff/CategoryConfiguration.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface CategoryConfiguration -{ - Of ofNode(NodePath nodePath); - - Of ofType(Class type); - - public interface Of - { - CategoryConfiguration toBe(String... categories); - } -} diff --git a/src/main/java/de/danielbechler/diff/CategoryService.java b/src/main/java/de/danielbechler/diff/CategoryService.java deleted file mode 100644 index 55527d93..00000000 --- a/src/main/java/de/danielbechler/diff/CategoryService.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.danielbechler.diff; - -import java.util.*; - -import static java.util.Arrays.*; -import static java.util.Collections.*; - -/** - * - */ -class CategoryService implements CategoryConfiguration, CategoryResolver -{ - private final NodePathValueHolder nodePathCategories = NodePathValueHolder.of(String[].class); - private final Map, String[]> typeCategories = new HashMap, String[]>(); - - public Set resolveCategories(final DiffNode node) - { - final Set categories = new TreeSet(); - categories.addAll(categoriesFromNodePathConfiguration(node)); - categories.addAll(categoriesFromTypeConfiguration(node)); - categories.addAll(categoriesFromNode(node)); - return categories; - } - - private Collection categoriesFromNodePathConfiguration(final DiffNode node) - { - final String[] categories = nodePathCategories.valueForNodePath(node.getPath()); - if (categories != null) - { - return asList(categories); - } - return emptySet(); - } - - private Collection categoriesFromTypeConfiguration(final DiffNode node) - { - final Class nodeType = node.getValueType(); - if (nodeType != null) - { - final String[] categories = typeCategories.get(nodeType); - if (categories != null) - { - return asList(categories); - } - } - return emptySet(); - } - - private static Collection categoriesFromNode(final DiffNode node) - { - return node.getCategories(); - } - - public Of ofNode(final NodePath nodePath) - { - return new Of() - { - public CategoryConfiguration toBe(final String... categories) - { - nodePathCategories.put(nodePath, categories); - return CategoryService.this; - } - }; - } - - public Of ofType(final Class type) - { - return new Of() - { - public CategoryConfiguration toBe(final String... categories) - { - typeCategories.put(type, categories); - return CategoryService.this; - } - }; - } -} diff --git a/src/main/java/de/danielbechler/diff/CircularReferenceConfiguration.java b/src/main/java/de/danielbechler/diff/CircularReferenceConfiguration.java deleted file mode 100644 index 017cd769..00000000 --- a/src/main/java/de/danielbechler/diff/CircularReferenceConfiguration.java +++ /dev/null @@ -1,9 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface CircularReferenceConfiguration -{ - CircularReferenceConfiguration matchCircularReferencesUsing(CircularReferenceMatchingMode matchingMode); - - CircularReferenceConfiguration handleCircularReferenceExceptionsUsing(CircularReferenceExceptionHandler exceptionHandler); -} diff --git a/src/main/java/de/danielbechler/diff/CircularReferenceExceptionHandler.java b/src/main/java/de/danielbechler/diff/CircularReferenceExceptionHandler.java deleted file mode 100644 index ec0001d2..00000000 --- a/src/main/java/de/danielbechler/diff/CircularReferenceExceptionHandler.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface CircularReferenceExceptionHandler -{ - void onCircularReferenceException(DiffNode node); -} diff --git a/src/main/java/de/danielbechler/diff/ComparisonConfiguration.java b/src/main/java/de/danielbechler/diff/ComparisonConfiguration.java deleted file mode 100644 index 6105b345..00000000 --- a/src/main/java/de/danielbechler/diff/ComparisonConfiguration.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface ComparisonConfiguration -{ - Of ofNode(NodePath nodePath); - - Of ofType(Class type); - - OfPrimitiveTypes ofPrimitiveTypes(); - - public interface Of - { - ComparisonConfiguration toUse(ComparisonStrategy comparisonStrategy); - - ComparisonConfiguration toUseEqualsMethod(); - - ComparisonConfiguration toUseEqualsMethodOfValueProvidedByMethod(String propertyName); - - ComparisonConfiguration toUseCompareToMethod(); - } - - public interface OfPrimitiveTypes - { - ComparisonConfiguration toTreatDefaultValuesAs(PrimitiveDefaultValueMode primitiveDefaultValueMode); - } -} diff --git a/src/main/java/de/danielbechler/diff/ComparisonService.java b/src/main/java/de/danielbechler/diff/ComparisonService.java deleted file mode 100644 index cee5c1d4..00000000 --- a/src/main/java/de/danielbechler/diff/ComparisonService.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2013 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; - -import de.danielbechler.util.*; - -import java.util.*; - -/** - * - */ -class ComparisonService implements ComparisonConfiguration, ComparisonStrategyResolver, PrimitiveDefaultValueModeResolver -{ - private final NodePathValueHolder nodePathComparisonStrategies = NodePathValueHolder.of(ComparisonStrategy.class); - private final Map, ComparisonStrategy> typeComparisonStrategyMap = new HashMap, ComparisonStrategy>(); - - private PrimitiveDefaultValueMode primitiveDefaultValueMode = PrimitiveDefaultValueMode.UNASSIGNED; - - public ComparisonStrategy resolveComparisonStrategy(final DiffNode node) - { - final ComparisonStrategy comparisonStrategy = nodePathComparisonStrategies.valueForNodePath(node.getPath()); - if (comparisonStrategy != null) - { - return comparisonStrategy; - } - - if (typeComparisonStrategyMap.containsKey(node.getValueType())) - { - return typeComparisonStrategyMap.get(node.getValueType()); - } - - if (Classes.isComparableType(node.getValueType())) - { - return new ComparableComparisonStrategy(); - } - - if (Classes.isSimpleType(node.getValueType())) - { - return new EqualsOnlyComparisonStrategy(); - } - - if (node.getComparisonStrategy() != null) - { - return node.getComparisonStrategy(); - } - - return null; - } - - public PrimitiveDefaultValueMode resolvePrimitiveDefaultValueMode(final DiffNode node) - { - return primitiveDefaultValueMode; - } - - public Of ofNode(final NodePath nodePath) - { - return new OfNodePath(nodePath); - } - - public Of ofType(final Class type) - { - return new OfType(type); - } - - public OfPrimitiveTypes ofPrimitiveTypes() - { - return new OfPrimitiveTypesImpl(); - } - - private abstract static class AbstractOf implements Of - { - public ComparisonConfiguration toUseEqualsMethod() - { - return toUse(new EqualsOnlyComparisonStrategy()); - } - - public ComparisonConfiguration toUseEqualsMethodOfValueProvidedByMethod(final String propertyName) - { - return toUse(new EqualsOnlyComparisonStrategy(propertyName)); - } - - public ComparisonConfiguration toUseCompareToMethod() - { - return toUse(new ComparableComparisonStrategy()); - } - } - - private class OfType extends AbstractOf - { - private final Class type; - - public OfType(final Class type) - { - this.type = type; - } - - public ComparisonConfiguration toUse(final ComparisonStrategy comparisonStrategy) - { - typeComparisonStrategyMap.put(type, comparisonStrategy); - return ComparisonService.this; - } - } - - private class OfNodePath extends AbstractOf - { - private final NodePath nodePath; - - public OfNodePath(final NodePath nodePath) - { - this.nodePath = nodePath; - } - - public ComparisonConfiguration toUse(final ComparisonStrategy comparisonStrategy) - { - nodePathComparisonStrategies.put(nodePath, comparisonStrategy); - return ComparisonService.this; - } - } - - private class OfPrimitiveTypesImpl implements OfPrimitiveTypes - { - public ComparisonConfiguration toTreatDefaultValuesAs(final PrimitiveDefaultValueMode mode) - { - primitiveDefaultValueMode = mode; - return null; - } - } -} diff --git a/src/main/java/de/danielbechler/diff/ComparisonStrategyAwareAccessor.java b/src/main/java/de/danielbechler/diff/ComparisonStrategyAwareAccessor.java deleted file mode 100644 index 06162e3c..00000000 --- a/src/main/java/de/danielbechler/diff/ComparisonStrategyAwareAccessor.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface ComparisonStrategyAwareAccessor extends Accessor -{ - ComparisonStrategy getComparisonStrategy(); -} diff --git a/src/main/java/de/danielbechler/diff/DefaultNodeQueryService.java b/src/main/java/de/danielbechler/diff/DefaultNodeQueryService.java new file mode 100644 index 00000000..3d287940 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/DefaultNodeQueryService.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016 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; + +import de.danielbechler.diff.category.CategoryResolver; +import de.danielbechler.diff.comparison.ComparisonStrategy; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.comparison.PrimitiveDefaultValueMode; +import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver; +import de.danielbechler.diff.filtering.IsReturnableResolver; +import de.danielbechler.diff.inclusion.IsIgnoredResolver; +import de.danielbechler.diff.introspection.IsIntrospectableResolver; +import de.danielbechler.diff.node.DiffNode; + +import java.util.Set; + +class DefaultNodeQueryService implements NodeQueryService +{ + private final CategoryResolver categoryResolver; + private final IsIntrospectableResolver introspectableResolver; + private final IsIgnoredResolver ignoredResolver; + private final IsReturnableResolver returnableResolver; + private final ComparisonStrategyResolver comparisonStrategyResolver; + private final PrimitiveDefaultValueModeResolver primitiveDefaultValueModeResolver; + + public DefaultNodeQueryService(final CategoryResolver categoryResolver, + final IsIntrospectableResolver introspectableResolver, + final IsIgnoredResolver ignoredResolver, + final IsReturnableResolver returnableResolver, + final ComparisonStrategyResolver comparisonStrategyResolver, + final PrimitiveDefaultValueModeResolver primitiveDefaultValueModeResolver) + { + this.categoryResolver = categoryResolver; + this.introspectableResolver = introspectableResolver; + this.ignoredResolver = ignoredResolver; + this.returnableResolver = returnableResolver; + this.comparisonStrategyResolver = comparisonStrategyResolver; + this.primitiveDefaultValueModeResolver = primitiveDefaultValueModeResolver; + } + + public Set resolveCategories(final DiffNode node) + { + return categoryResolver.resolveCategories(node); + } + + public boolean isIntrospectable(final DiffNode node) + { + return introspectableResolver.isIntrospectable(node); + } + + public boolean isIgnored(final DiffNode node) + { + return ignoredResolver.isIgnored(node); + } + + public boolean isReturnable(final DiffNode node) + { + return returnableResolver.isReturnable(node); + } + + public ComparisonStrategy resolveComparisonStrategy(final DiffNode node) + { + return comparisonStrategyResolver.resolveComparisonStrategy(node); + } + + public PrimitiveDefaultValueMode resolvePrimitiveDefaultValueMode(final DiffNode node) + { + return primitiveDefaultValueModeResolver.resolvePrimitiveDefaultValueMode(node); + } +} diff --git a/src/main/java/de/danielbechler/diff/DifferDispatcher.java b/src/main/java/de/danielbechler/diff/DifferDispatcher.java deleted file mode 100644 index f08f7c1f..00000000 --- a/src/main/java/de/danielbechler/diff/DifferDispatcher.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.util.*; - -import static de.danielbechler.diff.CircularReferenceDetector.*; - -/** @author Daniel Bechler */ -public class DifferDispatcher -{ - private final DifferProvider differProvider; - private final CircularReferenceDetectorFactory circularReferenceDetectorFactory; - private final CircularReferenceExceptionHandler circularReferenceExceptionHandler; - private final IsIgnoredResolver isIgnoredResolver; - private final IsReturnableResolver isReturnableResolver; - private CircularReferenceDetector workingCircularReferenceDetector; - private CircularReferenceDetector baseCircularReferenceDetector; - - public DifferDispatcher(final DifferProvider differProvider, - final CircularReferenceDetectorFactory circularReferenceDetectorFactory, - final CircularReferenceExceptionHandler circularReferenceExceptionHandler, - final IsIgnoredResolver ignoredResolver, - final IsReturnableResolver returnableResolver) - { - Assert.notNull(differProvider, "differFactory"); - this.differProvider = differProvider; - - Assert.notNull(ignoredResolver, "ignoredResolver"); - this.isIgnoredResolver = ignoredResolver; - - this.circularReferenceDetectorFactory = circularReferenceDetectorFactory; - this.circularReferenceExceptionHandler = circularReferenceExceptionHandler; - this.isReturnableResolver = returnableResolver; - - resetInstanceMemory(); - } - - /** - * Delegates the call to an appropriate {@link de.danielbechler.diff.Differ}. - * - * @return A node representing the difference between the given {@link de.danielbechler.diff.Instances}. - */ - public DiffNode dispatch(final DiffNode parentNode, - final Instances parentInstances, - final Accessor accessor) - { - Assert.notNull(parentInstances, "parentInstances"); - Assert.notNull(accessor, "accessor"); - - final DiffNode node = compare(parentNode, parentInstances, accessor); - if (parentNode != null && isReturnableResolver.isReturnable(node)) - { - parentNode.addChild(node); - } - return node; - } - - private DiffNode compare(final DiffNode parentNode, - final Instances parentInstances, - final Accessor accessor) - { - final DiffNode node = new DiffNode(parentNode, accessor, null); - if (isIgnoredResolver.isIgnored(node)) - { - node.setState(DiffNode.State.IGNORED); - return node; - } - - final Instances accessedInstances = parentInstances.access(accessor); - if (accessedInstances.areNull()) - { - return new DiffNode(parentNode, accessedInstances.getSourceAccessor(), accessedInstances.getType()); - } - else - { - return compareWithCircularReferenceTracking(parentNode, accessedInstances); - } - } - - private DiffNode compareWithCircularReferenceTracking(final DiffNode parentNode, - final Instances instances) - { - DiffNode node; - try - { - rememberInstances(parentNode, instances); - try - { - node = compare(parentNode, instances); - } - finally - { - forgetInstances(instances); - } - } - catch (CircularReferenceException e) - { - node = newCircularNode(parentNode, instances, e.getNodePath()); - circularReferenceExceptionHandler.onCircularReferenceException(node); - } - if (parentNode == null) - { - resetInstanceMemory(); - } - return node; - } - - private static DiffNode findNodeMatchingPropertyPath(final DiffNode node, final NodePath nodePath) - { - if (node == null) - { - return null; - } - if (node.matches(nodePath)) - { - return node; - } - return findNodeMatchingPropertyPath(node.getParentNode(), nodePath); - } - - private static DiffNode newCircularNode(final DiffNode parentNode, - final Instances instances, - final NodePath circleStartPath) - { - final DiffNode node = new DiffNode(parentNode, instances.getSourceAccessor(), instances.getType()); - node.setState(DiffNode.State.CIRCULAR); - node.setCircleStartPath(circleStartPath); - node.setCircleStartNode(findNodeMatchingPropertyPath(parentNode, circleStartPath)); - return node; - } - - private DiffNode compare(final DiffNode parentNode, final Instances instances) - { - final Differ differ = differProvider.retrieveDifferForType(instances.getType()); - if (differ == null) - { - throw new IllegalStateException("Couldn't create Differ for type '" + instances.getType() + - "'. This mustn't happen, as there should always be a fallback differ."); - } - return differ.compare(parentNode, instances); - } - - protected final void resetInstanceMemory() - { - workingCircularReferenceDetector = circularReferenceDetectorFactory.createCircularReferenceDetector(); - baseCircularReferenceDetector = circularReferenceDetectorFactory.createCircularReferenceDetector(); - } - - protected void forgetInstances(final Instances instances) - { - workingCircularReferenceDetector.remove(instances.getWorking()); - baseCircularReferenceDetector.remove(instances.getBase()); - } - - protected void rememberInstances(final DiffNode parentNode, final Instances instances) - { - final NodePath nodePath = instances.getPropertyPath(parentNode); - workingCircularReferenceDetector.push(instances.getWorking(), nodePath); - baseCircularReferenceDetector.push(instances.getBase(), nodePath); - } -} diff --git a/src/main/java/de/danielbechler/diff/ExclusionAwareAccessor.java b/src/main/java/de/danielbechler/diff/ExclusionAwareAccessor.java deleted file mode 100644 index 3ba5b2fc..00000000 --- a/src/main/java/de/danielbechler/diff/ExclusionAwareAccessor.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface ExclusionAwareAccessor extends Accessor -{ - boolean isExcluded(); -} diff --git a/src/main/java/de/danielbechler/diff/InclusionConfiguration.java b/src/main/java/de/danielbechler/diff/InclusionConfiguration.java deleted file mode 100644 index 6e285ec9..00000000 --- a/src/main/java/de/danielbechler/diff/InclusionConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package de.danielbechler.diff; - -/** - * @author Daniel Bechler - */ -public interface InclusionConfiguration -{ - To toInclude(); - - To toExclude(); - - public interface To - { - To categories(String... categories); - - To types(Class... types); - - To nodes(NodePath... nodePath); - - To propertyNames(String... propertyNames); - } -} diff --git a/src/main/java/de/danielbechler/diff/InclusionService.java b/src/main/java/de/danielbechler/diff/InclusionService.java deleted file mode 100644 index 05feec88..00000000 --- a/src/main/java/de/danielbechler/diff/InclusionService.java +++ /dev/null @@ -1,256 +0,0 @@ -package de.danielbechler.diff; - -import de.danielbechler.diff.bean.*; -import de.danielbechler.util.*; - -import java.util.*; - -import static de.danielbechler.diff.Inclusion.EXCLUDED; -import static de.danielbechler.diff.Inclusion.INCLUDED; - -/** - * - */ -class InclusionService implements InclusionConfiguration, IsIgnoredResolver -{ - private final CategoryResolver categoryResolver; - private final NodePathValueHolder nodeInclusions = NodePathValueHolder.of(Inclusion.class); - private final Map, Inclusion> typeInclusions = new HashMap, Inclusion>(); - private final Map categoryInclusions = new TreeMap(); - private final Map propertyNameInclusions = new TreeMap(); - - public InclusionService(final CategoryResolver categoryResolver) - { - Assert.notNull(categoryResolver, "categoryResolver"); - this.categoryResolver = categoryResolver; - } - - private static String getNodePropertyName(final DiffNode node) - { - final Element pathElement = node.getPathElement(); - if (pathElement instanceof NamedPropertyElement) - { - return ((NamedPropertyElement) pathElement).getPropertyName(); - } - return null; - } - - public boolean isIgnored(final DiffNode node) - { - return node.isExcluded() || !isIncluded(node) || isExcluded(node); - } - - private boolean isIncluded(final DiffNode node) - { - if (hasInclusions(INCLUDED)) - { - if (node.isRootNode()) - { - return true; - } - else if (isIncludedByPath(node)) - { - return true; - } - else if (isIncludedByCategory(node)) - { - return true; - } - else if (isIncludedByType(node)) - { - return true; - } - else if (isIncludedByPropertyName(node)) - { - return true; - } - return false; - } - return true; - } - - private boolean isIncludedByPropertyName(final DiffNode node) - { - final String propertyName = getNodePropertyName(node); - if (propertyName != null) - { - return propertyNameInclusions.get(propertyName) == INCLUDED; - } - return false; - } - - private boolean isIncludedByCategory(final DiffNode node) - { - return hasCategoryWithInclusion(node, INCLUDED); - } - - private boolean isIncludedByPath(final DiffNode node) - { - return getInclusionByPath(node.getPath(), INCLUDED) == INCLUDED; - } - - private boolean isExcluded(final DiffNode node) - { - if (hasInclusions(EXCLUDED)) - { - if (isExcludedByPath(node)) - { - return true; - } - else if (isExcludedByCategory(node)) - { - return true; - } - else if (isExcludedByType(node)) - { - return true; - } - else if (isExcludedByPropertyName(node)) - { - return true; - } - } - return false; - } - - @SuppressWarnings("TypeMayBeWeakened") // we don't want to weaken the type for consistency reasons - private boolean isExcludedByPropertyName(final DiffNode node) - { - final String propertyName = getNodePropertyName(node); - if (propertyName != null) - { - return propertyNameInclusions.get(propertyName) == EXCLUDED; - } - return false; - } - - private boolean isExcludedByPath(final DiffNode node) - { - return getInclusionByPath(node.getPath(), EXCLUDED) == EXCLUDED; - } - - private boolean isExcludedByCategory(final DiffNode node) - { - return hasCategoryWithInclusion(node, EXCLUDED); - } - - private boolean hasInclusions(final Inclusion inclusion) - { - if (nodeInclusions.containsValue(inclusion)) - { - return true; - } - if (typeInclusions.containsValue(inclusion)) - { - return true; - } - if (categoryInclusions.containsValue(inclusion)) - { - return true; - } - if (propertyNameInclusions.containsValue(inclusion)) - { - return true; - } - return false; - } - - private Inclusion getInclusionByPath(final NodePath nodePath, final Inclusion inclusion) - { - final List inclusions = nodeInclusions.accumulatedValuesForNodePath(nodePath); - if (inclusions.contains(inclusion)) - { - return inclusion; - } - else - { - return nodeInclusions.valueForNodePath(nodePath); - } - } - - private boolean hasCategoryWithInclusion(final DiffNode node, final Inclusion inclusion) - { - for (final String category : categoryResolver.resolveCategories(node)) - { - if (categoryInclusions.get(category) == inclusion) - { - return true; - } - } - return false; - } - - private boolean isIncludedByType(final DiffNode node) - { - if (node.getValueType() != null) - { - return typeInclusions.get(node.getValueType()) == INCLUDED; - } - return false; - } - - private boolean isExcludedByType(final DiffNode node) - { - if (node.getValueType() != null) - { - return typeInclusions.get(node.getValueType()) == EXCLUDED; - } - return false; - } - - public To toInclude() - { - return new ToImpl(INCLUDED); - } - - public To toExclude() - { - return new ToImpl(EXCLUDED); - } - - private class ToImpl implements To - { - private final Inclusion inclusion; - - public ToImpl(final Inclusion inclusion) - { - this.inclusion = inclusion; - } - - public To categories(final String... categories) - { - for (final String category : categories) - { - categoryInclusions.put(category, inclusion); - } - return this; - } - - public To types(final Class... types) - { - for (final Class type : types) - { - typeInclusions.put(type, inclusion); - } - return this; - } - - public To nodes(final NodePath... nodePaths) - { - for (final NodePath nodePath : nodePaths) - { - nodeInclusions.put(nodePath, inclusion); - } - return this; - } - - public To propertyNames(final String... propertyNames) - { - for (final String propertyName : propertyNames) - { - propertyNameInclusions.put(propertyName, inclusion); - } - return this; - } - } -} diff --git a/src/main/java/de/danielbechler/diff/IntrospectionConfiguration.java b/src/main/java/de/danielbechler/diff/IntrospectionConfiguration.java deleted file mode 100644 index c8e223cf..00000000 --- a/src/main/java/de/danielbechler/diff/IntrospectionConfiguration.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface IntrospectionConfiguration -{ - IntrospectionConfiguration setDefaultIntrospector(Introspector introspector); - - Of ofType(Class type); - - Of ofNode(NodePath path); - - public static interface Of - { - IntrospectionConfiguration toUse(Introspector introspector); - - IntrospectionConfiguration toBeEnabled(); - - IntrospectionConfiguration toBeDisabled(); - } - -} diff --git a/src/main/java/de/danielbechler/diff/IntrospectionService.java b/src/main/java/de/danielbechler/diff/IntrospectionService.java deleted file mode 100644 index d5d2d4f0..00000000 --- a/src/main/java/de/danielbechler/diff/IntrospectionService.java +++ /dev/null @@ -1,124 +0,0 @@ -package de.danielbechler.diff; - -import de.danielbechler.diff.bean.*; -import de.danielbechler.util.*; - -import java.util.*; - -/** @author Daniel Bechler */ -class IntrospectionService implements IntrospectionConfiguration, IsIntrospectableResolver, IntrospectorResolver -{ - private final Map, Introspector> typeIntrospectorMap = new HashMap, Introspector>(); - private final Map, IntrospectionMode> typeIntrospectionModeMap = new HashMap, IntrospectionMode>(); - private final NodePathValueHolder nodePathIntrospectorHolder = new NodePathValueHolder(); - private final NodePathValueHolder nodePathIntrospectionModeHolder = new NodePathValueHolder(); - private Introspector defaultIntrospector = new StandardBeanIntrospector(); - - public enum IntrospectionMode - { - ENABLED, - DISABLED - } - - public boolean isIntrospectable(final DiffNode node) - { - final Class nodeType = node.getValueType(); - if (nodeType == null) - { - return false; - } - else if (isPrimitiveTypeEnumOrArray(nodeType)) - { - return false; - } - else if (nodePathIntrospectionModeHolder.valueForNodePath(node.getPath()) == IntrospectionMode.DISABLED) - { - return false; - } - else if (typeIntrospectionModeMap.get(nodeType) == IntrospectionMode.DISABLED) - { - return false; - } - return true; - } - - public Introspector introspectorForNode(final DiffNode node) - { - final Introspector typeIntrospector = typeIntrospectorMap.get(node.getValueType()); - if (typeIntrospector != null) - { - return typeIntrospector; - } - - final Introspector nodePathIntrospector = nodePathIntrospectorHolder.valueForNodePath(node.getPath()); - if (nodePathIntrospector != null) - { - return nodePathIntrospector; - } - - return defaultIntrospector; - } - - private static boolean isPrimitiveTypeEnumOrArray(final Class nodeType) - { - return Classes.isPrimitiveType(nodeType) - || Classes.isPrimitiveWrapperType(nodeType) - || nodeType.isEnum() - || nodeType.isArray(); - } - - public IntrospectionConfiguration setDefaultIntrospector(final Introspector introspector) - { - Assert.notNull(introspector, "The default introspector must not be null"); - defaultIntrospector = introspector; - return this; - } - - public Of ofType(final Class type) - { - return new Of() - { - public IntrospectionConfiguration toUse(final Introspector introspector) - { - typeIntrospectorMap.put(type, introspector); - return IntrospectionService.this; - } - - public IntrospectionConfiguration toBeEnabled() - { - typeIntrospectionModeMap.put(type, IntrospectionMode.ENABLED); - return IntrospectionService.this; - } - - public IntrospectionConfiguration toBeDisabled() - { - typeIntrospectionModeMap.put(type, IntrospectionMode.DISABLED); - return IntrospectionService.this; - } - }; - } - - public Of ofNode(final NodePath path) - { - return new Of() - { - public IntrospectionConfiguration toUse(final Introspector introspector) - { - nodePathIntrospectorHolder.put(path, introspector); - return IntrospectionService.this; - } - - public IntrospectionConfiguration toBeEnabled() - { - nodePathIntrospectionModeHolder.put(path, IntrospectionMode.ENABLED); - return IntrospectionService.this; - } - - public IntrospectionConfiguration toBeDisabled() - { - nodePathIntrospectionModeHolder.put(path, IntrospectionMode.DISABLED); - return IntrospectionService.this; - } - }; - } -} diff --git a/src/main/java/de/danielbechler/diff/NodeMatcher.java b/src/main/java/de/danielbechler/diff/NodeMatcher.java deleted file mode 100644 index 4da150df..00000000 --- a/src/main/java/de/danielbechler/diff/NodeMatcher.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface NodeMatcher -{ - boolean matches(DiffNode node); -} diff --git a/src/main/java/de/danielbechler/diff/NodePath.java b/src/main/java/de/danielbechler/diff/NodePath.java deleted file mode 100644 index c6c16857..00000000 --- a/src/main/java/de/danielbechler/diff/NodePath.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.diff.bean.*; -import de.danielbechler.diff.collection.*; -import de.danielbechler.diff.map.*; -import de.danielbechler.util.*; - -import java.util.*; -import java.util.Collections; - -/** @author Daniel Bechler */ -public final class NodePath implements Comparable -{ - private final List elements; - - private NodePath(final List elements) - { - this.elements = Collections.unmodifiableList(elements); - } - - public List getElements() - { - return elements; - } - - public boolean matches(final NodePath nodePath) - { - return nodePath.equals(this); - } - - public boolean isParentOf(final NodePath nodePath) - { - final Iterator iterator1 = elements.iterator(); - final Iterator iterator2 = nodePath.getElements().iterator(); - while (iterator1.hasNext() && iterator2.hasNext()) - { - final Element next1 = iterator1.next(); - final Element next2 = iterator2.next(); - if (!next1.equals(next2)) - { - return false; - } - } - return !iterator1.hasNext(); - } - - @Override - public String toString() - { - final StringBuilder sb = new StringBuilder(); - final Iterator iterator = elements.iterator(); - Element previousElement = null; - while (iterator.hasNext()) - { - final Element element = iterator.next(); - if (element instanceof RootElement) - { - sb.append("/"); - } - else if (element instanceof CollectionElement || element instanceof MapElement) - { - sb.append(element); - } - else if (previousElement instanceof RootElement) - { - sb.append(element); - } - else - { - sb.append('/'); - sb.append(element); - } - previousElement = element; - } - return sb.toString(); - } - - @Override - public boolean equals(final Object o) - { - if (this == o) - { - return true; - } - if (o == null || getClass() != o.getClass()) - { - return false; - } - - final NodePath that = (NodePath) o; - - if (!elements.equals(that.elements)) - { - return false; - } - - return true; - } - - @Override - public int hashCode() - { - return elements.hashCode(); - } - - public static InitialBuilder createBuilder() - { - return new InitialBuilderImpl(); - } - - public static NodePath buildWith(final String propertyName, final String... additionalPropertyNames) - { - return createBuilder().withRoot().withPropertyName(propertyName, additionalPropertyNames).build(); - } - - public static NodePath buildRootPath() - { - return createBuilder().withRoot().build(); - } - - public int compareTo(final NodePath that) - { - if (this.getElements().size() <= that.getElements().size()) - { - return -1; - } - else if (this.matches(that)) - { - return 0; - } - else if (this.getElements().size() > that.getElements().size()) - { - return 1; - } - else - { - return 1; - } - } - - /** @author Daniel Bechler */ - @SuppressWarnings({"UnusedDeclaration"}) - private static final class InitialBuilderImpl implements InitialBuilder - { - private InitialBuilderImpl() - { - } - - public AppendableBuilder withRoot() - { - final List elements = new ArrayList(1); - elements.add(RootElement.getInstance()); - return new AppendableBuilderImpl(elements); - } - - public AppendableBuilder withPropertyPath(final NodePath nodePath) - { - Assert.notNull(nodePath, "propertyPath"); - return new AppendableBuilderImpl(new ArrayList(nodePath.getElements())); - } - } - - private static final class AppendableBuilderImpl implements AppendableBuilder - { - private final List elements; - - public AppendableBuilderImpl(final List elements) - { - Assert.notEmpty(elements, "elements"); - this.elements = new ArrayList(elements); - } - - public AppendableBuilder withElement(final Element element) - { - Assert.notNull(element, "element"); - elements.add(element); - return this; - } - - public AppendableBuilder withPropertyName(final String name, final String... names) - { - elements.add(new NamedPropertyElement(name)); - for (final String s : names) - { - elements.add(new NamedPropertyElement(s)); - } - return this; - } - - public AppendableBuilder withCollectionItem(final T item) - { - elements.add(new CollectionElement(item)); - return this; - } - - public AppendableBuilder withMapKey(final K key) - { - Assert.notNull(key, "key"); - elements.add(new MapElement(key)); - return this; - } - - public NodePath build() - { - if (elements.isEmpty()) - { - throw new IllegalStateException("A property path cannot be empty"); - } - else if (!(elements.get(0) instanceof RootElement)) - { - throw new IllegalStateException("A property path must start with a root element"); - } - else if (elementCount(RootElement.class) > 1) - { - throw new IllegalStateException("A property path cannot contain multiple root elements"); - } - return new NodePath(elements); - } - - private int elementCount(final Class type) - { - assert type != null : "Type must not be null"; - int count = 0; - for (final Element element : elements) - { - if (type.isAssignableFrom(element.getClass())) - { - count++; - } - } - return count; - } - } - - public static interface InitialBuilder - { - AppendableBuilder withPropertyPath(NodePath nodePath); - - AppendableBuilder withRoot(); - } - - public static interface AppendableBuilder - { - AppendableBuilder withElement(Element element); - - AppendableBuilder withPropertyName(String name, String... names); - - AppendableBuilder withCollectionItem(T item); - - AppendableBuilder withMapKey(K key); - - NodePath build(); - } -} diff --git a/src/main/java/de/danielbechler/diff/NodePathValueHolder.java b/src/main/java/de/danielbechler/diff/NodePathValueHolder.java deleted file mode 100644 index 5c4f24b5..00000000 --- a/src/main/java/de/danielbechler/diff/NodePathValueHolder.java +++ /dev/null @@ -1,107 +0,0 @@ -package de.danielbechler.diff; - -import de.danielbechler.util.*; - -import java.util.*; - -/** @author Daniel Bechler */ -public class NodePathValueHolder -{ - public static NodePathValueHolder of(final Class type) - { - Assert.notNull(type, "type"); - return new NodePathValueHolder(); - } - - private final Map> elementValueHolders = new HashMap>(); - - private T value; - - public NodePathValueHolder put(final NodePath nodePath, final T value) - { - put(nodePath.getElements(), value); - return this; - } - - private void put(final List nodePathElements, final T value) - { - if (nodePathElements.isEmpty()) - { - return; - } - final Element element = nodePathElements.get(0); - NodePathValueHolder nodePathValueHolder = elementValueHolders.get(element); - if (nodePathValueHolder == null) - { - nodePathValueHolder = new NodePathValueHolder(); - elementValueHolders.put(element, nodePathValueHolder); - } - if (nodePathElements.size() == 1) - { - nodePathValueHolder.value = value; - } - else - { - final List nodePathElementsTail = new ArrayList(nodePathElements.size() - 1); - nodePathElementsTail.addAll(nodePathElements.subList(1, nodePathElements.size())); - nodePathValueHolder.put(nodePathElementsTail, value); - } - } - - private T visit(final List accumulator, final Iterator elementIterator) - { - if (value != null) - { - accumulator.add(value); - } - if (elementIterator.hasNext()) - { - final Element element = elementIterator.next(); - final NodePathValueHolder valueHolder = elementValueHolders.get(element); - if (valueHolder != null) - { - return valueHolder.visit(accumulator, elementIterator); - } - return null; - } - else - { - return value; - } - } - - public T valueForNodePath(final NodePath nodePath) - { - return visit(new LinkedList(), nodePath.getElements().iterator()); - } - - public List accumulatedValuesForNodePath(final NodePath nodePath) - { - final List accumulator = new LinkedList(); - visit(accumulator, nodePath.getElements().iterator()); - return accumulator; - } - - public boolean containsValue(final T value) - { - if (value == null && this.value == null) - { - return true; - } - else if (value != null && value.equals(this.value)) - { - return true; - } - else - { - for (final NodePathValueHolder valueHolder : elementValueHolders.values()) - { - if (valueHolder.containsValue(value)) - { - return true; - } - } - return false; - } - } -} diff --git a/src/main/java/de/danielbechler/diff/NodeQueryService.java b/src/main/java/de/danielbechler/diff/NodeQueryService.java new file mode 100644 index 00000000..ab0a9c0b --- /dev/null +++ b/src/main/java/de/danielbechler/diff/NodeQueryService.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; + +import de.danielbechler.diff.category.CategoryResolver; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver; +import de.danielbechler.diff.filtering.IsReturnableResolver; +import de.danielbechler.diff.inclusion.IsIgnoredResolver; +import de.danielbechler.diff.introspection.IsIntrospectableResolver; + +/** + * Created by Daniel Bechler. + */ +public interface NodeQueryService extends + CategoryResolver, + IsIntrospectableResolver, + IsIgnoredResolver, + IsReturnableResolver, + ComparisonStrategyResolver, + PrimitiveDefaultValueModeResolver +{ +} diff --git a/src/main/java/de/danielbechler/diff/ObjectDiffer.java b/src/main/java/de/danielbechler/diff/ObjectDiffer.java index 74922b8c..164f8c27 100644 --- a/src/main/java/de/danielbechler/diff/ObjectDiffer.java +++ b/src/main/java/de/danielbechler/diff/ObjectDiffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,9 +16,14 @@ package de.danielbechler.diff; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.access.RootAccessor; +import de.danielbechler.diff.differ.DifferDispatcher; +import de.danielbechler.diff.node.DiffNode; + /** * This is the entry point for all comparisons. It determines the type of the given objects and passes them to - * the appropriate {@link Differ}. + * the appropriate {@link de.danielbechler.diff.differ.Differ}. * * @author Daniel Bechler */ @@ -26,7 +31,7 @@ public class ObjectDiffer { private final DifferDispatcher dispatcher; - ObjectDiffer(final DifferDispatcher differDispatcher) + public ObjectDiffer(final DifferDispatcher differDispatcher) { this.dispatcher = differDispatcher; } @@ -35,14 +40,20 @@ public class ObjectDiffer * Recursively inspects the given objects and returns a node representing their differences. Both objects * have be have the same type. * - * @param working This object will be treated as the successor of the base object. + * @param working This object will be treated as the successor of the `base` object. * @param base This object will be treated as the predecessor of the working object. - * @param The type of the objects to compare. - * * @return A node representing the differences between the given objects. */ public DiffNode compare(final T working, final T base) { - return dispatcher.dispatch(DiffNode.ROOT, Instances.of(working, base), RootAccessor.getInstance()); + dispatcher.resetInstanceMemory(); + try + { + return dispatcher.dispatch(DiffNode.ROOT, Instances.of(working, base), RootAccessor.getInstance()); + } + finally + { + dispatcher.clearInstanceMemory(); + } } } diff --git a/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java b/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java index 4f5a85f9..92573462 100644 --- a/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java +++ b/src/main/java/de/danielbechler/diff/ObjectDifferBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -16,74 +16,206 @@ package de.danielbechler.diff; -import de.danielbechler.diff.bean.BeanDiffer; -import de.danielbechler.diff.collection.CollectionDiffer; -import de.danielbechler.diff.map.MapDiffer; -import de.danielbechler.diff.primitive.PrimitiveDiffer; +import de.danielbechler.diff.category.CategoryConfigurer; +import de.danielbechler.diff.category.CategoryService; +import de.danielbechler.diff.circular.CircularReferenceConfigurer; +import de.danielbechler.diff.circular.CircularReferenceService; +import de.danielbechler.diff.comparison.ComparisonConfigurer; +import de.danielbechler.diff.comparison.ComparisonService; +import de.danielbechler.diff.differ.BeanDiffer; +import de.danielbechler.diff.differ.CollectionDiffer; +import de.danielbechler.diff.differ.Differ; +import de.danielbechler.diff.differ.DifferConfigurer; +import de.danielbechler.diff.differ.DifferDispatcher; +import de.danielbechler.diff.differ.DifferFactory; +import de.danielbechler.diff.differ.DifferProvider; +import de.danielbechler.diff.differ.DifferService; +import de.danielbechler.diff.differ.MapDiffer; +import de.danielbechler.diff.differ.PrimitiveDiffer; +import de.danielbechler.diff.filtering.FilteringConfigurer; +import de.danielbechler.diff.filtering.ReturnableNodeService; +import de.danielbechler.diff.identity.IdentityService; +import de.danielbechler.diff.inclusion.InclusionConfigurer; +import de.danielbechler.diff.inclusion.InclusionService; +import de.danielbechler.diff.introspection.IntrospectionConfigurer; +import de.danielbechler.diff.introspection.IntrospectionService; + +import java.util.ArrayList; +import java.util.Collection; /** + * 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 final class ObjectDifferBuilder { - private final CategoryService categoryService = new CategoryService(); - private final ComparisonService comparisonService = new ComparisonService(); - private final ReturnableNodeService returnableNodeService = new ReturnableNodeService(); - private final IntrospectionService introspectionService = new IntrospectionService(); - private final InclusionService inclusionService = new InclusionService(categoryService); - private final CircularReferenceService circularReferenceService = new CircularReferenceService(); - private final Configurable configurable = new Configurable(); - - private ObjectDifferBuilder() { - } - - public static ObjectDiffer buildDefaultObjectDiffer() { - return startBuilding().build(); - } - - public static ObjectDifferBuilder startBuilding() { - return new ObjectDifferBuilder(); - } - - public ObjectDiffer build() { - final DifferProvider differProvider = new DifferProvider(); - final DifferDispatcher differDispatcher = new DifferDispatcher(differProvider, circularReferenceService, circularReferenceService, inclusionService, returnableNodeService); - differProvider.push(new BeanDiffer(differDispatcher, introspectionService, returnableNodeService, comparisonService, introspectionService)); - differProvider.push(new CollectionDiffer(differDispatcher, comparisonService)); - differProvider.push(new MapDiffer(differDispatcher, comparisonService)); - differProvider.push(new PrimitiveDiffer(comparisonService)); - return new ObjectDiffer(differDispatcher); - } - - public final Configurable configure() { - return configurable; - } - - public class Configurable { - private Configurable() { - } - - public ReturnableNodeConfiguration filtering() { - return returnableNodeService; - } - - public IntrospectionConfiguration introspection() { - return introspectionService; - } - - public CircularReferenceConfiguration circularReferenceHandling() { - return circularReferenceService; - } - - public InclusionConfiguration inclusion() { - return inclusionService; - } - - public ComparisonConfiguration comparison() { - return comparisonService; - } - - public CategoryConfiguration categories() { - return categoryService; - } - } +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 IdentityService identityService = new IdentityService(this); + private final ReturnableNodeService returnableNodeService = new ReturnableNodeService(this); + private final CircularReferenceService circularReferenceService = new CircularReferenceService(this); + private final DifferService differService = new DifferService(this); + private final NodeQueryService nodeQueryService; + + private ObjectDifferBuilder() + { + nodeQueryService = new DefaultNodeQueryService(categoryService, + introspectionService, + inclusionService, + returnableNodeService, + comparisonService, + comparisonService); + } + + /** + * 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() + { + return returnableNodeService; + } + + /** + * Allows to replace the default bean introspector with a custom + * implementation. + */ + public IntrospectionConfigurer introspection() + { + return introspectionService; + } + + /** + * Allows to define how the circular reference detector compares object + * instances. + */ + public CircularReferenceConfigurer circularReferenceHandling() + { + return circularReferenceService; + } + + /** + * Allows to in- or exclude nodes based on property name, object type, + * category or location in the object graph. + */ + public InclusionConfigurer inclusion() + { + return inclusionService; + } + + /** + * Allows to configure the way objects are compared. + */ + public ComparisonConfigurer comparison() + { + return comparisonService; + } + + /** + * EXPERT FEATURE: Allows to configure the way the identity of objects is determined in order to establish + * the relationship between different versions. By default this is done via the equals method; but + * sometimes that's just not possible. For example when you are dealing with generated classes or you are dealing + * with third-party code. + *

+ * Please keep in mind that this only alters the way this library establishes the connection between two objects. + * This doesn't extend to the underlying collections. So keep what in mind when you start merging your collections + * and weird things start to happen. + *

+ * WARNING: Personally I'd try to avoid this feature as long as possible and only use it when there is + * absolutely no other way. + */ + public IdentityService identity() + { + return identityService; + } + + /** + * Allows to assign custom categories (or tags) to entire types or selected + * elements and properties. + */ + public CategoryConfigurer categories() + { + return categoryService; + } + + public DifferConfigurer differs() + { + return differService; + } + + public static ObjectDiffer buildDefault() + { + return startBuilding().build(); + } + + public static ObjectDifferBuilder startBuilding() + { + return new ObjectDifferBuilder(); + } + + public ObjectDiffer build() + { + final DifferProvider differProvider = new DifferProvider(); + final DifferDispatcher differDispatcher = newDifferDispatcher(differProvider); + differProvider.push(newBeanDiffer(differDispatcher)); + differProvider.push(newCollectionDiffer(differDispatcher)); + differProvider.push(newMapDiffer(differDispatcher)); + differProvider.push(newPrimitiveDiffer()); + differProvider.pushAll(createCustomDiffers(differDispatcher)); + return new ObjectDiffer(differDispatcher); + } + + private DifferDispatcher newDifferDispatcher(final DifferProvider differProvider) + { + return new DifferDispatcher( + differProvider, + circularReferenceService, + circularReferenceService, + inclusionService, + returnableNodeService, + introspectionService, + categoryService); + } + + private Differ newBeanDiffer(final DifferDispatcher differDispatcher) + { + return new BeanDiffer( + differDispatcher, + introspectionService, + returnableNodeService, + comparisonService, + introspectionService); + } + + private Differ newCollectionDiffer(final DifferDispatcher differDispatcher) + { + return new CollectionDiffer(differDispatcher, comparisonService, identityService); + } + + private Differ newMapDiffer(final DifferDispatcher differDispatcher) + { + return new MapDiffer(differDispatcher, comparisonService); + } + + private Differ newPrimitiveDiffer() + { + return new PrimitiveDiffer(comparisonService); + } + + private Iterable createCustomDiffers(final DifferDispatcher differDispatcher) + { + final Collection differFactories = differService.getDifferFactories(); + final Collection differs = new ArrayList(differFactories.size()); + for (final DifferFactory differFactory : differFactories) + { + differs.add(differFactory.createDiffer(differDispatcher, nodeQueryService)); + } + return differs; + } } diff --git a/src/main/java/de/danielbechler/merge/ObjectMerger.java b/src/main/java/de/danielbechler/diff/ObjectMerger.java similarity index 84% rename from src/main/java/de/danielbechler/merge/ObjectMerger.java rename to src/main/java/de/danielbechler/diff/ObjectMerger.java index 317efb8b..ae9fdbfe 100644 --- a/src/main/java/de/danielbechler/merge/ObjectMerger.java +++ b/src/main/java/de/danielbechler/diff/ObjectMerger.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,10 +14,10 @@ * limitations under the License. */ -package de.danielbechler.merge; +package de.danielbechler.diff; -import de.danielbechler.diff.*; -import de.danielbechler.diff.visitor.*; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.node.Visit; /** * Careful: This class has not yet been tested very thoroughly and serves more as an example for your own @@ -32,7 +32,7 @@ public final class ObjectMerger public ObjectMerger() { - this.objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); + this.objectDiffer = ObjectDifferBuilder.buildDefault(); } public ObjectMerger(final ObjectDiffer objectDiffer) @@ -60,7 +60,7 @@ public MergingDifferenceVisitor(final T head, final T modified) this.modified = modified; } - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { if (node.getState() == DiffNode.State.ADDED) { @@ -70,10 +70,6 @@ else if (node.getState() == DiffNode.State.REMOVED) { node.canonicalUnset(head); } - else if (node.getState() == DiffNode.State.UNTOUCHED) - { - // not touched - nothing to do - } else if (node.getState() == DiffNode.State.CHANGED) { if (node.hasChildren()) diff --git a/src/main/java/de/danielbechler/diff/ReturnableNodeConfiguration.java b/src/main/java/de/danielbechler/diff/ReturnableNodeConfiguration.java deleted file mode 100644 index 4a493a18..00000000 --- a/src/main/java/de/danielbechler/diff/ReturnableNodeConfiguration.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.danielbechler.diff; - -/** @author Daniel Bechler */ -public interface ReturnableNodeConfiguration -{ - ReturnableNodeConfiguration returnNodesWithState(DiffNode.State state, boolean enabled); - - ReturnableNodeConfiguration returnNodesWithState(DiffNode.State state); - - ReturnableNodeConfiguration omitNodesWithState(DiffNode.State state); -} diff --git a/src/main/java/de/danielbechler/diff/Accessor.java b/src/main/java/de/danielbechler/diff/access/Accessor.java similarity index 81% rename from src/main/java/de/danielbechler/diff/Accessor.java rename to src/main/java/de/danielbechler/diff/access/Accessor.java index 9fa3e54b..613356f2 100644 --- a/src/main/java/de/danielbechler/diff/Accessor.java +++ b/src/main/java/de/danielbechler/diff/access/Accessor.java @@ -14,12 +14,16 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.access; -/** @author Daniel Bechler */ +import de.danielbechler.diff.selector.ElementSelector; + +/** + * @author Daniel Bechler + */ public interface Accessor { - Element getPathElement(); + ElementSelector getElementSelector(); Object get(Object target); diff --git a/src/test/java/de/danielbechler/diff/helper/TestGroups.java b/src/main/java/de/danielbechler/diff/access/CategoryAware.java similarity index 73% rename from src/test/java/de/danielbechler/diff/helper/TestGroups.java rename to src/main/java/de/danielbechler/diff/access/CategoryAware.java index 5c7d3a64..d61a87f6 100644 --- a/src/test/java/de/danielbechler/diff/helper/TestGroups.java +++ b/src/main/java/de/danielbechler/diff/access/CategoryAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,13 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff.helper; +package de.danielbechler.diff.access; -public final class TestGroups -{ - public static final String INTEGRATION = "integration"; +import java.util.Set; - private TestGroups() - { - } +/** + * @author Daniel Bechler + */ +public interface CategoryAware +{ + Set getCategoriesFromAnnotation(); } diff --git a/src/main/java/de/danielbechler/diff/collection/CollectionItemAccessor.java b/src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java similarity index 51% rename from src/main/java/de/danielbechler/diff/collection/CollectionItemAccessor.java rename to src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java index b803f005..653b2d51 100644 --- a/src/main/java/de/danielbechler/diff/collection/CollectionItemAccessor.java +++ b/src/main/java/de/danielbechler/diff/access/CollectionItemAccessor.java @@ -14,40 +14,64 @@ * limitations under the License. */ -package de.danielbechler.diff.collection; +package de.danielbechler.diff.access; -import de.danielbechler.diff.*; +import de.danielbechler.diff.identity.EqualsIdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.selector.CollectionItemElementSelector; +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.util.Assert; -import java.util.*; +import java.util.Collection; +import java.util.Iterator; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ 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, EqualsIdentityStrategy.getInstance()); + } + + /** + * Allows for custom IdentityStrategy. + * + * @param referenceItem + * @param identityStrategy + */ + public CollectionItemAccessor(final Object referenceItem, + final IdentityStrategy identityStrategy) + { + Assert.notNull(identityStrategy, "identityStrategy"); this.referenceItem = referenceItem; + this.identityStrategy = identityStrategy; } - public Element getPathElement() + public Class getType() { - return new CollectionElement(referenceItem); + return referenceItem != null ? referenceItem.getClass() : null; } - public void set(final Object target, final Object value) + @Override + public String toString() { - final Collection targetCollection = objectAsCollection(target); - if (targetCollection == null) - { - return; - } - final Object previous = get(target); - if (previous != null) - { - targetCollection.remove(previous); - } - targetCollection.add(value); + return "collection item " + getElementSelector(); + } + + public ElementSelector getElementSelector() + { + final CollectionItemElementSelector selector = new CollectionItemElementSelector(referenceItem); + return identityStrategy == null ? selector : selector.copyWithIdentityStrategy(identityStrategy); } public Object get(final Object target) @@ -59,7 +83,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; } @@ -67,11 +91,22 @@ public Object get(final Object target) return null; } - public Class getType() + public void set(final Object target, final Object value) { - return referenceItem != null ? referenceItem.getClass() : null; + final Collection targetCollection = objectAsCollection(target); + if (targetCollection == null) + { + return; + } + final Object previous = get(target); + if (previous != null) + { + unset(target); + } + targetCollection.add(value); } + @SuppressWarnings("unchecked") private static Collection objectAsCollection(final Object object) { if (object == null) @@ -80,7 +115,6 @@ private static Collection objectAsCollection(final Object object) } else if (object instanceof Collection) { - //noinspection unchecked return (Collection) object; } throw new IllegalArgumentException(object.getClass().toString()); @@ -88,16 +122,20 @@ else if (object instanceof Collection) public void unset(final Object target) { - final Collection targetCollection = objectAsCollection(target); - if (targetCollection != null) + final Collection targetCollection = objectAsCollection(target); + if (targetCollection == null) { - targetCollection.remove(referenceItem); + return; + } + final Iterator iterator = targetCollection.iterator(); + while (iterator.hasNext()) + { + final Object item = iterator.next(); + if (item != null && identityStrategy.equals(item, referenceItem)) + { + iterator.remove(); + break; + } } - } - - @Override - public String toString() - { - return "collection item " + getPathElement(); } } diff --git a/src/main/java/de/danielbechler/diff/access/ExclusionAware.java b/src/main/java/de/danielbechler/diff/access/ExclusionAware.java new file mode 100644 index 00000000..e13120ce --- /dev/null +++ b/src/main/java/de/danielbechler/diff/access/ExclusionAware.java @@ -0,0 +1,30 @@ +/* + * 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.access; + +/** + * @author Daniel Bechler + * @see de.danielbechler.diff.inclusion.TypePropertyAnnotationInclusionResolver + * @deprecated This was a shortcut to extract the "exclude" flag from the ObjectDiffProperty annotation. Since we found + * a better way to do that, it is not needed anymore and will be removed in future versions. + */ +@Deprecated +public interface ExclusionAware +{ + @Deprecated + boolean isExcludedByAnnotation(); +} diff --git a/src/main/java/de/danielbechler/diff/Instances.java b/src/main/java/de/danielbechler/diff/access/Instances.java similarity index 75% rename from src/main/java/de/danielbechler/diff/Instances.java rename to src/main/java/de/danielbechler/diff/access/Instances.java index 6ebc2713..c9261bd5 100644 --- a/src/main/java/de/danielbechler/diff/Instances.java +++ b/src/main/java/de/danielbechler/diff/access/Instances.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,85 +14,68 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.access; -import de.danielbechler.diff.bean.*; -import de.danielbechler.util.*; +import de.danielbechler.util.Assert; +import de.danielbechler.util.Classes; import de.danielbechler.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.lang.reflect.*; -import java.util.*; +import java.util.Collection; +import java.util.Map; +import java.util.Set; -import static de.danielbechler.util.Objects.*; +import static de.danielbechler.util.Objects.isEqual; -/** @author Daniel Bechler */ -@SuppressWarnings({"UnusedDeclaration"}) public class Instances { + private static final Logger logger = LoggerFactory.getLogger(Instances.class); private final Accessor sourceAccessor; private final Object working; private final Object base; private final Object fresh; - static Instances of(final Accessor sourceAccessor, - final T working, - final T base, - final T fresh) + Instances(final Accessor sourceAccessor, + final Object working, + final Object base, + final Object fresh) { - return new Instances(sourceAccessor, working, base, fresh); + Assert.notNull(sourceAccessor, "sourceAccessor"); + this.sourceAccessor = sourceAccessor; + this.working = working; + this.base = base; + this.fresh = fresh; } - static Instances of(final Accessor sourceAccessor, final T working, final T base) + public static Instances of(final Accessor sourceAccessor, + final T working, + final T base, + final T fresh) { - final Object fresh = (working != null) ? Classes.freshInstanceOf(working.getClass()) : null; return new Instances(sourceAccessor, working, base, fresh); } - static Instances of(final T working, final T base) + public static Instances of(final Accessor sourceAccessor, final T working, final T base) { final Object fresh = (working != null) ? Classes.freshInstanceOf(working.getClass()) : null; - return new Instances(RootAccessor.getInstance(), working, base, fresh); + return new Instances(sourceAccessor, working, base, fresh); } - Instances(final Accessor sourceAccessor, - final Object working, - final Object base, - final Object fresh) + public static Instances of(final T working, final T base) { - Assert.notNull(sourceAccessor, "sourceAccessor"); - this.sourceAccessor = sourceAccessor; - this.working = working; - this.base = base; - this.fresh = fresh; + final Object fresh = (working != null) ? Classes.freshInstanceOf(working.getClass()) : null; + return new Instances(RootAccessor.getInstance(), working, base, fresh); } - /** @return The {@link Accessor} that has been used to get to these instances. */ + /** + * @return The {@link Accessor} that has been used to get to these instances. + */ public Accessor getSourceAccessor() { return sourceAccessor; } - public Instances access(final String methodName) - { - Assert.hasText(methodName, "methodName"); - final Class type = getType(); - if (type != null) - { - try - { - final Method method = type.getMethod(methodName); - method.setAccessible(true); - final BeanPropertyAccessor accessor = new BeanPropertyAccessor(methodName, method, null); - return new Instances(accessor, accessor.get(working), accessor.get(base), accessor.get(fresh)); - } - catch (NoSuchMethodException e) - { - throw Exceptions.escalate(type.getName(), e); - } - } - return null; - } - public Instances access(final Accessor accessor) { Assert.notNull(accessor, "accessor"); @@ -135,6 +118,7 @@ else if (isPrimitiveBooleanType()) return fresh; } + @SuppressWarnings("UnusedDeclaration") public T getFresh(final Class type) { final Object o = getFresh(); @@ -172,45 +156,12 @@ public boolean isPrimitiveType() return Classes.isPrimitiveType(getType()); } + @SuppressWarnings("UnusedDeclaration") public boolean isPrimitiveWrapperType() { return Classes.isPrimitiveWrapperType(getType()); } - public boolean isPrimitiveNumericType() - { - return Classes.isPrimitiveNumericType(getType()); - } - - private boolean isPrimitiveBooleanType() - { - return getType() == boolean.class; - } - - public boolean areEqual() - { - return isEqual(base, working); - } - - public boolean areSame() - { - return working == base; - } - - public boolean areNull() - { - return working == null && base == null; - } - - private Class tryToGetTypeFromSourceAccessor() - { - if (sourceAccessor instanceof TypeAwareAccessor) - { - return ((TypeAwareAccessor) sourceAccessor).getType(); - } - return null; - } - public Class getType() { final Set> types = Classes.typesOf(working, base, fresh); @@ -277,22 +228,44 @@ else if (sourceAccessorType != null) // special handling for beans and arrays should go here } } - throw new IllegalArgumentException("Detected instances of different types " + types + ". " + - "Instances must either be null or have the exact same type."); + + logger.info("Detected instances of different types " + types + ". " + + "These objects will only be compared via equals method."); + return Object.class; } - public NodePath getPropertyPath(final DiffNode parentNode) + private Class tryToGetTypeFromSourceAccessor() { - if (parentNode != null) - { - return NodePath.createBuilder() - .withPropertyPath(parentNode.getPath()) - .withElement(sourceAccessor.getPathElement()) - .build(); - } - else + if (sourceAccessor instanceof TypeAwareAccessor) { - return NodePath.createBuilder().withRoot().build(); + return ((TypeAwareAccessor) sourceAccessor).getType(); } + return null; + } + + public boolean isPrimitiveNumericType() + { + return Classes.isPrimitiveNumericType(getType()); } + + private boolean isPrimitiveBooleanType() + { + return getType() == boolean.class; + } + + public boolean areEqual() + { + return isEqual(base, working); + } + + public boolean areSame() + { + return working == base; + } + + public boolean areNull() + { + return working == null && base == null; + } + } diff --git a/src/main/java/de/danielbechler/diff/map/MapEntryAccessor.java b/src/main/java/de/danielbechler/diff/access/MapEntryAccessor.java similarity index 89% rename from src/main/java/de/danielbechler/diff/map/MapEntryAccessor.java rename to src/main/java/de/danielbechler/diff/access/MapEntryAccessor.java index 329b2105..0cad795e 100644 --- a/src/main/java/de/danielbechler/diff/map/MapEntryAccessor.java +++ b/src/main/java/de/danielbechler/diff/access/MapEntryAccessor.java @@ -14,10 +14,10 @@ * limitations under the License. */ -package de.danielbechler.diff.map; +package de.danielbechler.diff.access; -import de.danielbechler.diff.Accessor; -import de.danielbechler.diff.Element; +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.diff.selector.MapKeyElementSelector; import de.danielbechler.util.Assert; import java.util.Map; @@ -76,12 +76,12 @@ public boolean equals(final Object o) @Override public String toString() { - return "map key " + getPathElement(); + return "map key " + getElementSelector(); } - public Element getPathElement() + public ElementSelector getElementSelector() { - return new MapElement(referenceKey); + return new MapKeyElementSelector(referenceKey); } public Object get(final Object target) diff --git a/src/main/java/de/danielbechler/diff/PropertyAwareAccessor.java b/src/main/java/de/danielbechler/diff/access/PropertyAwareAccessor.java similarity index 68% rename from src/main/java/de/danielbechler/diff/PropertyAwareAccessor.java rename to src/main/java/de/danielbechler/diff/access/PropertyAwareAccessor.java index cacf2b59..290f722f 100644 --- a/src/main/java/de/danielbechler/diff/PropertyAwareAccessor.java +++ b/src/main/java/de/danielbechler/diff/access/PropertyAwareAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,14 +14,22 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.access; -import java.lang.annotation.*; -import java.util.*; +import java.lang.annotation.Annotation; +import java.util.Set; -/** @author Daniel Bechler */ -public interface PropertyAwareAccessor extends TypeAwareAccessor, CategoryAwareAccessor, ComparisonStrategyAwareAccessor, ExclusionAwareAccessor +/** + * @author Daniel Bechler + */ +public interface PropertyAwareAccessor extends TypeAwareAccessor, CategoryAware, ExclusionAware { + String getPropertyName(); + + Set getFieldAnnotations(); + + T getFieldAnnotation(Class annotationClass); + Set getReadMethodAnnotations(); T getReadMethodAnnotation(Class annotationClass); diff --git a/src/main/java/de/danielbechler/diff/RootAccessor.java b/src/main/java/de/danielbechler/diff/access/RootAccessor.java similarity index 82% rename from src/main/java/de/danielbechler/diff/RootAccessor.java rename to src/main/java/de/danielbechler/diff/access/RootAccessor.java index 323884fb..b431ef94 100644 --- a/src/main/java/de/danielbechler/diff/RootAccessor.java +++ b/src/main/java/de/danielbechler/diff/access/RootAccessor.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.access; + +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.diff.selector.RootElementSelector; -/** @author Daniel Bechler */ public final class RootAccessor implements Accessor { private static final RootAccessor instance = new RootAccessor(); @@ -45,9 +47,9 @@ public void unset(final Object target) throw new UnsupportedOperationException(); } - public Element getPathElement() + public ElementSelector getElementSelector() { - return RootElement.getInstance(); + return RootElementSelector.getInstance(); } @Override diff --git a/src/main/java/de/danielbechler/diff/TypeAwareAccessor.java b/src/main/java/de/danielbechler/diff/access/TypeAwareAccessor.java similarity index 90% rename from src/main/java/de/danielbechler/diff/TypeAwareAccessor.java rename to src/main/java/de/danielbechler/diff/access/TypeAwareAccessor.java index d8258aa4..d7f95102 100644 --- a/src/main/java/de/danielbechler/diff/TypeAwareAccessor.java +++ b/src/main/java/de/danielbechler/diff/access/TypeAwareAccessor.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.access; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public interface TypeAwareAccessor extends Accessor { Class getType(); diff --git a/src/main/java/de/danielbechler/diff/bean/BeanPropertyAccessExceptionHandler.java b/src/main/java/de/danielbechler/diff/bean/BeanPropertyAccessExceptionHandler.java deleted file mode 100644 index 87beb226..00000000 --- a/src/main/java/de/danielbechler/diff/bean/BeanPropertyAccessExceptionHandler.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.danielbechler.diff.bean; - -import de.danielbechler.diff.*; - -/** @author Daniel Bechler */ -public interface BeanPropertyAccessExceptionHandler -{ - DiffNode onPropertyWriteException(BeanPropertyWriteException ex, DiffNode propertyNode); - - DiffNode onPropertyReadException(BeanPropertyReadException ex, DiffNode propertyNode); -} diff --git a/src/main/java/de/danielbechler/diff/bean/BeanPropertyAccessor.java b/src/main/java/de/danielbechler/diff/bean/BeanPropertyAccessor.java deleted file mode 100644 index aa804ab2..00000000 --- a/src/main/java/de/danielbechler/diff/bean/BeanPropertyAccessor.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.*; -import de.danielbechler.util.*; -import org.slf4j.*; - -import java.lang.annotation.*; -import java.lang.reflect.*; -import java.util.*; - -/** - * @author Daniel Bechler - */ -public class BeanPropertyAccessor implements PropertyAwareAccessor, Accessor -{ - private static final Logger logger = LoggerFactory.getLogger(BeanPropertyAccessor.class); - - private final String propertyName; - private final Class type; - private final Method readMethod; - private final Method writeMethod; - private Set categories = new TreeSet(); - private boolean excluded; - private ComparisonStrategy comparisonStrategy; - - public BeanPropertyAccessor(final String propertyName, final Method readMethod, final Method writeMethod) - { - Assert.notNull(propertyName, "propertyName"); - Assert.notNull(readMethod, "readMethod"); - this.propertyName = propertyName; - this.readMethod = makeAccessible(readMethod); - this.writeMethod = makeAccessible(writeMethod); - this.type = this.readMethod.getReturnType(); - } - - private static Method makeAccessible(final Method method) - { - if (method != null) - { - method.setAccessible(true); - } - return method; - } - - private static boolean tryToReplaceCollectionContent(final Collection target, - final Collection value) - { - if (target == null) - { - return false; - } - try - { - target.clear(); - target.addAll(value); - return true; - } - catch (final Exception unmodifiable) - { - logger.debug("Failed to replace content of existing Collection", unmodifiable); - return false; - } - } - - private static boolean tryToReplaceMapContent(final Map target, - final Map value) - { - if (target == null) - { - return false; - } - try - { - target.clear(); - target.putAll(value); - return true; - } - catch (final Exception unmodifiable) - { - logger.debug("Failed to replace content of existing Map", unmodifiable); - return false; - } - } - - public final Set getCategories() - { - return categories; - } - - public final void setCategories(final Set categories) - { - this.categories = categories; - } - - public boolean isExcluded() - { - return excluded; - } - - public void setExcluded(final boolean excluded) - { - this.excluded = excluded; - } - - public ComparisonStrategy getComparisonStrategy() - { - return comparisonStrategy; - } - - public void setComparisonStrategy(final ComparisonStrategy comparisonStrategy) - { - this.comparisonStrategy = comparisonStrategy; - } - - public void set(final Object target, final Object value) - { - if (target == null) - { - logger.debug("The target object is null"); - logFailedSet(value); - } - else if (writeMethod == null) - { - logger.debug("No setter found for property '{}'", propertyName); - tryToReplaceContentOfCollectionTypes(target, value); - } - else - { - invokeWriteMethod(target, value); - } - } - - private void logFailedSet(final Object value) - { - logger.info("Couldn't set new value '{}' for property '{}'", value, propertyName); - } - - private void invokeWriteMethod(final Object target, final Object value) - { - try - { - writeMethod.invoke(target, value); - } - catch (Exception e) - { - final BeanPropertyWriteException ex = new BeanPropertyWriteException(e, value); - ex.setPropertyName(propertyName); - ex.setTargetType(getType()); - throw ex; - } - } - - private void tryToReplaceContentOfCollectionTypes(final Object target, final Object value) - { - if (Collection.class.isAssignableFrom(readMethod.getReturnType())) - { - //noinspection unchecked - tryToReplaceCollectionContent((Collection) get(target), (Collection) value); - return; - } - - if (Map.class.isAssignableFrom(readMethod.getReturnType())) - { - //noinspection unchecked - tryToReplaceMapContent((Map) get(target), (Map) value); - return; - } - - logFailedSet(value); - } - - public Object get(final Object target) - { - if (target == null) - { - return null; - } - try - { - return readMethod.invoke(target); - } - catch (final Exception e) - { - final BeanPropertyReadException ex = new BeanPropertyReadException(e); - ex.setPropertyName(propertyName); - ex.setTargetType(target.getClass()); - throw ex; - } - } - - public void unset(final Object target) - { - set(target, null); - } - - public Class getType() - { - return this.type; - } - - public String getPropertyName() - { - return this.propertyName; - } - - public Element getPathElement() - { - return new NamedPropertyElement(this.propertyName); - } - - /** - * @return The annotations of the getter used to access this property. - */ - public Set getReadMethodAnnotations() - { - return new LinkedHashSet(Arrays.asList(readMethod.getAnnotations())); - } - - public T getReadMethodAnnotation(final Class annotationClass) - { - final Set annotations = getReadMethodAnnotations(); - assert (annotations != null) : "Something is wrong here. " + - "The contract of getReadAnnotations() guarantees a non-null return value."; - for (final Annotation annotation : annotations) - { - if (annotationClass.isAssignableFrom(annotation.annotationType())) - { - return annotationClass.cast(annotation); - } - } - return null; - } - - @Override - public String toString() - { - return "property '" + propertyName + "'"; - } -} diff --git a/src/main/java/de/danielbechler/diff/bean/DefaultPropertyAccessExceptionHandler.java b/src/main/java/de/danielbechler/diff/bean/DefaultPropertyAccessExceptionHandler.java deleted file mode 100644 index ba264ba1..00000000 --- a/src/main/java/de/danielbechler/diff/bean/DefaultPropertyAccessExceptionHandler.java +++ /dev/null @@ -1,24 +0,0 @@ -package de.danielbechler.diff.bean; - -import de.danielbechler.diff.*; -import org.slf4j.*; - -public class DefaultPropertyAccessExceptionHandler implements BeanPropertyAccessExceptionHandler -{ - private static final Logger logger = LoggerFactory.getLogger(DefaultPropertyAccessExceptionHandler.class); - - public DiffNode onPropertyWriteException(final BeanPropertyWriteException exception, - final DiffNode propertyNode) - { - final Object newValue = exception.getNewValue(); - final String propertyName = exception.getPropertyName(); - logger.info("Couldn't set new value '{}' for property '{}'", newValue, propertyName); - throw exception; - } - - public DiffNode onPropertyReadException(final BeanPropertyReadException exception, - final DiffNode propertyNode) - { - throw exception; - } -} diff --git a/src/main/java/de/danielbechler/diff/bean/StandardBeanIntrospector.java b/src/main/java/de/danielbechler/diff/bean/StandardBeanIntrospector.java deleted file mode 100644 index 501f825b..00000000 --- a/src/main/java/de/danielbechler/diff/bean/StandardBeanIntrospector.java +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.annotation.*; -import de.danielbechler.util.*; -import de.danielbechler.util.Collections; - -import java.beans.*; -import java.beans.Introspector; -import java.lang.reflect.*; -import java.util.*; - -/** - * Resolves the accessors of a given type by using the standard Java {@link Introspector}. - * - * @author Daniel Bechler - */ -public class StandardBeanIntrospector implements de.danielbechler.diff.Introspector -{ - private static PropertyAwareAccessor handlePropertyDescriptor(final PropertyDescriptor descriptor) - { - if (shouldSkip(descriptor)) - { - return null; - } - - final String propertyName = descriptor.getName(); - final Method readMethod = descriptor.getReadMethod(); - final Method writeMethod = descriptor.getWriteMethod(); - - final BeanPropertyAccessor accessor = new BeanPropertyAccessor(propertyName, readMethod, writeMethod); - - handleObjectDiffPropertyAnnotation(readMethod, accessor); - handleEqualsOnlyTypeAnnotation(readMethod, accessor); - - return accessor; - } - - private static void handleEqualsOnlyTypeAnnotation(final Method readMethod, - final BeanPropertyAccessor propertyAccessor) - { - final Class returnType = readMethod.getReturnType(); - final ObjectDiffEqualsOnlyType annotation = returnType.getAnnotation(ObjectDiffEqualsOnlyType.class); - if (annotation != null) - { - final EqualsOnlyComparisonStrategy comparisonStrategy; - if (Strings.hasText(annotation.valueProviderMethod())) - { - comparisonStrategy = new EqualsOnlyComparisonStrategy(annotation.valueProviderMethod()); - } - else - { - comparisonStrategy = new EqualsOnlyComparisonStrategy(); - } - propertyAccessor.setComparisonStrategy(comparisonStrategy); - } - } - - private static void handleObjectDiffPropertyAnnotation(final Method readMethod, - final BeanPropertyAccessor propertyAccessor) - { - final ObjectDiffProperty annotation = readMethod.getAnnotation(ObjectDiffProperty.class); - if (annotation != null) - { - if (annotation.equalsOnly()) - { - final String equalsOnlyValueProviderMethod = annotation.equalsOnlyValueProviderMethod(); - final EqualsOnlyComparisonStrategy comparisonStrategy; - if (Strings.hasText(equalsOnlyValueProviderMethod)) - { - comparisonStrategy = new EqualsOnlyComparisonStrategy(equalsOnlyValueProviderMethod); - } - else - { - comparisonStrategy = new EqualsOnlyComparisonStrategy(); - } - propertyAccessor.setComparisonStrategy(comparisonStrategy); - } - propertyAccessor.setExcluded(annotation.excluded()); - propertyAccessor.setCategories(Collections.setOf(annotation.categories())); - } - } - - private static boolean shouldSkip(final PropertyDescriptor descriptor) - { - return descriptor.getName().equals("class") || descriptor.getReadMethod() == null; - } - - public Iterable introspect(final Class type) - { - Assert.notNull(type, "type"); - try - { - return internalIntrospect(type); - } - catch (IntrospectionException e) - { - throw Exceptions.escalate(e); - } - } - - private Iterable internalIntrospect(final Class type) throws IntrospectionException - { - final PropertyDescriptor[] descriptors = getBeanInfo(type).getPropertyDescriptors(); - final Collection accessors = new ArrayList(descriptors.length); - for (final PropertyDescriptor descriptor : descriptors) - { - final PropertyAwareAccessor accessor = handlePropertyDescriptor(descriptor); - if (accessor != null) - { - accessors.add(accessor); - } - } - return accessors; - } - - protected BeanInfo getBeanInfo(final Class type) throws IntrospectionException - { - return Introspector.getBeanInfo(type); - } -} diff --git a/src/main/java/de/danielbechler/diff/category/CategoryConfigurer.java b/src/main/java/de/danielbechler/diff/category/CategoryConfigurer.java new file mode 100644 index 00000000..172c30c7 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/category/CategoryConfigurer.java @@ -0,0 +1,41 @@ +/* + * 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.category; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.path.NodePath; + +/** + * Allows to assign custom categories (or tags) to entire types or selected elements and properties. These categories + * come in very handy, when combined with the `InclusionConfiguration`. They make it very easy to limit the comparison + * to a specific subset of the object graph. + * + * @author Daniel Bechler + */ +public interface CategoryConfigurer +{ + Of ofNode(NodePath nodePath); + + Of ofType(Class type); + + ObjectDifferBuilder and(); + + public interface Of + { + CategoryConfigurer toBe(String... categories); + } +} diff --git a/src/main/java/de/danielbechler/diff/CategoryResolver.java b/src/main/java/de/danielbechler/diff/category/CategoryResolver.java similarity index 78% rename from src/main/java/de/danielbechler/diff/CategoryResolver.java rename to src/main/java/de/danielbechler/diff/category/CategoryResolver.java index 71a1c119..73274cdb 100644 --- a/src/main/java/de/danielbechler/diff/CategoryResolver.java +++ b/src/main/java/de/danielbechler/diff/category/CategoryResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,11 +14,15 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.category; -import java.util.*; +import de.danielbechler.diff.node.DiffNode; -/** @author Daniel Bechler */ +import java.util.Set; + +/** + * @author Daniel Bechler + */ public interface CategoryResolver { Set resolveCategories(DiffNode node); diff --git a/src/main/java/de/danielbechler/diff/category/CategoryService.java b/src/main/java/de/danielbechler/diff/category/CategoryService.java new file mode 100644 index 00000000..46d5fc5b --- /dev/null +++ b/src/main/java/de/danielbechler/diff/category/CategoryService.java @@ -0,0 +1,116 @@ +/* + * 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.category; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.diff.path.NodePathValueHolder; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; + +/** + * + */ +public class CategoryService implements CategoryConfigurer, CategoryResolver +{ + private final NodePathValueHolder nodePathCategories = NodePathValueHolder.of(String[].class); + private final Map, String[]> typeCategories = new HashMap, String[]>(); + private final ObjectDifferBuilder objectDifferBuilder; + + public CategoryService(final ObjectDifferBuilder objectDifferBuilder) + { + this.objectDifferBuilder = objectDifferBuilder; + } + + public Set resolveCategories(final DiffNode node) + { + final Set categories = new TreeSet(); + categories.addAll(categoriesFromNodePathConfiguration(node)); + categories.addAll(categoriesFromTypeConfiguration(node)); + categories.addAll(categoriesFromNode(node)); + return categories; + } + + private Collection categoriesFromNodePathConfiguration(final DiffNode node) + { + final Collection allCategories = new HashSet(); + final List accumulatedValues = nodePathCategories.accumulatedValuesForNodePath(node.getPath()); + for (final String[] categoriesForElement : accumulatedValues) + { + allCategories.addAll(asList(categoriesForElement)); + } + return allCategories; + } + + private Collection categoriesFromTypeConfiguration(final DiffNode node) + { + final Class nodeType = node.getValueType(); + if (nodeType != null) + { + final String[] categories = typeCategories.get(nodeType); + if (categories != null) + { + return asList(categories); + } + } + return emptySet(); + } + + private static Collection categoriesFromNode(final DiffNode node) + { + return node.getCategories(); + } + + public Of ofNode(final NodePath nodePath) + { + return new Of() + { + public CategoryConfigurer toBe(final String... categories) + { + nodePathCategories.put(nodePath, categories); + return CategoryService.this; + } + }; + } + + public Of ofType(final Class type) + { + return new Of() + { + public CategoryConfigurer toBe(final String... categories) + { + typeCategories.put(type, categories); + return CategoryService.this; + } + }; + } + + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } +} diff --git a/src/main/java/de/danielbechler/diff/circular/CircularReferenceConfigurer.java b/src/main/java/de/danielbechler/diff/circular/CircularReferenceConfigurer.java new file mode 100644 index 00000000..6b2d7d68 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/circular/CircularReferenceConfigurer.java @@ -0,0 +1,42 @@ +/* + * 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.circular; + +import de.danielbechler.diff.ObjectDifferBuilder; + +/** + * Allows to define how the circular reference detector compares object instances. By default it uses the equality + * operator (`==`) which should be fine in most cases. + *

+ * When dealing with object models that return copies of its properties on every access, it's possible to end up in + * infinite loops, because even though the objects may look the same, they would be different instances. In those cases + * it is possible to switch the instance detection mode to use the equals method instead of the equality operator. This + * way objects will be considered to be "the same" whenever `equals` returns `true`. + *

+ * This configuration interface also allows to register a custom handler for exception thrown, whenever a circular + * reference is detected. The default handler simply logs a warning. + * + * @author Daniel Bechler + */ +public interface CircularReferenceConfigurer +{ + CircularReferenceConfigurer matchCircularReferencesUsing(CircularReferenceMatchingMode matchingMode); + + CircularReferenceConfigurer handleCircularReferenceExceptionsUsing(CircularReferenceExceptionHandler exceptionHandler); + + ObjectDifferBuilder and(); +} diff --git a/src/main/java/de/danielbechler/diff/CircularReferenceDetector.java b/src/main/java/de/danielbechler/diff/circular/CircularReferenceDetector.java similarity index 86% rename from src/main/java/de/danielbechler/diff/CircularReferenceDetector.java rename to src/main/java/de/danielbechler/diff/circular/CircularReferenceDetector.java index a2878336..6e671a11 100644 --- a/src/main/java/de/danielbechler/diff/CircularReferenceDetector.java +++ b/src/main/java/de/danielbechler/diff/circular/CircularReferenceDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,16 +14,19 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.circular; -import de.danielbechler.util.*; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.util.Assert; -import java.util.*; +import java.util.LinkedList; -import static de.danielbechler.util.Objects.*; +import static de.danielbechler.util.Objects.isEqual; -/** @author Daniel Bechler */ -class CircularReferenceDetector +/** + * @author Daniel Bechler + */ +public class CircularReferenceDetector { /** * It would be better to use java.util.Deque instead of the linked list, but that was first @@ -34,28 +37,6 @@ class CircularReferenceDetector private ReferenceMatchingMode referenceMatchingMode = ReferenceMatchingMode.EQUALITY_OPERATOR; - private static class Entry - { - private final NodePath nodePath; - private final Object instance; - - private Entry(final NodePath nodePath, final Object instance) - { - this.nodePath = nodePath; - this.instance = instance; - } - - public NodePath getNodePath() - { - return nodePath; - } - - public Object getInstance() - { - return instance; - } - } - public CircularReferenceDetector(final ReferenceMatchingMode referenceMatchingMode) { Assert.notNull(referenceMatchingMode, "referenceMatchingMode"); @@ -89,6 +70,18 @@ public boolean knows(final Object needle) return false; } + private Entry entryForInstance(final Object instance) + { + for (final Entry entry : stack) + { + if (isMatch(instance, entry.getInstance())) + { + return entry; + } + } + return null; + } + protected boolean isMatch(final Object anObject, final Object anotherObject) { if (referenceMatchingMode == ReferenceMatchingMode.EQUALITY_OPERATOR) @@ -105,18 +98,6 @@ else if (referenceMatchingMode == ReferenceMatchingMode.EQUALS_METHOD) } } - private Entry entryForInstance(final Object instance) - { - for (final Entry entry : stack) - { - if (isMatch(instance, entry.getInstance())) - { - return entry; - } - } - return null; - } - public void remove(final Object instance) { if (instance == null) @@ -138,6 +119,47 @@ public int size() return stack.size(); } + public static enum ReferenceMatchingMode + { + /** + * Compares objects using the == operator. + */ + EQUALITY_OPERATOR, + + /** + * Compares objects using {@linkplain Object#equals(Object)}. + */ + EQUALS_METHOD + } + + private static class Entry + { + private final NodePath nodePath; + private final Object instance; + + private Entry(final NodePath nodePath, final Object instance) + { + this.nodePath = nodePath; + this.instance = instance; + } + + public NodePath getNodePath() + { + return nodePath; + } + + public Object getInstance() + { + return instance; + } + + @Override + public String toString() + { + return nodePath.toString() + "{" + instance.toString() + "}"; + } + } + public static class CircularReferenceException extends RuntimeException { private static final long serialVersionUID = 1L; @@ -161,13 +183,4 @@ public Throwable fillInStackTrace() return this; } } - - public static enum ReferenceMatchingMode - { - /** Compares objects using the == operator. */ - EQUALITY_OPERATOR, - - /** Compares objects using {@linkplain Object#equals(Object)}. */ - EQUALS_METHOD - } } diff --git a/src/main/java/de/danielbechler/diff/CircularReferenceDetectorFactory.java b/src/main/java/de/danielbechler/diff/circular/CircularReferenceDetectorFactory.java similarity index 86% rename from src/main/java/de/danielbechler/diff/CircularReferenceDetectorFactory.java rename to src/main/java/de/danielbechler/diff/circular/CircularReferenceDetectorFactory.java index 07b2c9d8..6aa3a511 100644 --- a/src/main/java/de/danielbechler/diff/CircularReferenceDetectorFactory.java +++ b/src/main/java/de/danielbechler/diff/circular/CircularReferenceDetectorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,9 +14,11 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.circular; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public interface CircularReferenceDetectorFactory { CircularReferenceDetector createCircularReferenceDetector(); diff --git a/src/main/java/de/danielbechler/diff/circular/CircularReferenceExceptionHandler.java b/src/main/java/de/danielbechler/diff/circular/CircularReferenceExceptionHandler.java new file mode 100644 index 00000000..724eebe1 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/circular/CircularReferenceExceptionHandler.java @@ -0,0 +1,27 @@ +/* + * 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.circular; + +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ +public interface CircularReferenceExceptionHandler +{ + void onCircularReferenceException(DiffNode node); +} diff --git a/src/main/java/de/danielbechler/diff/CircularReferenceMatchingMode.java b/src/main/java/de/danielbechler/diff/circular/CircularReferenceMatchingMode.java similarity index 80% rename from src/main/java/de/danielbechler/diff/CircularReferenceMatchingMode.java rename to src/main/java/de/danielbechler/diff/circular/CircularReferenceMatchingMode.java index e111c515..a1ddc7e4 100644 --- a/src/main/java/de/danielbechler/diff/CircularReferenceMatchingMode.java +++ b/src/main/java/de/danielbechler/diff/circular/CircularReferenceMatchingMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,10 +14,10 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.circular; /** - * Defines how the {@link de.danielbechler.diff.CircularReferenceDetector} compares object instances. The + * Defines how the {@link CircularReferenceDetector} compares object instances. The * default is {@link CircularReferenceMatchingMode#EQUALITY_OPERATOR} and this should be sufficient in mose * cases. However, you may be dealing with an object model that returns copies of its properties, instead of * reusing the exact same instance. In this cases it would be easy to end up in infinite loops, as the default @@ -27,9 +27,13 @@ */ public enum CircularReferenceMatchingMode { - /** Compares objects using the == operator. */ + /** + * Compares objects using the == operator. + */ EQUALITY_OPERATOR, - /** Compares objects using {@linkplain Object#equals(Object)}. */ + /** + * Compares objects using {@linkplain Object#equals(Object)}. + */ EQUALS_METHOD } diff --git a/src/main/java/de/danielbechler/diff/CircularReferenceService.java b/src/main/java/de/danielbechler/diff/circular/CircularReferenceService.java similarity index 50% rename from src/main/java/de/danielbechler/diff/CircularReferenceService.java rename to src/main/java/de/danielbechler/diff/circular/CircularReferenceService.java index f847ddb7..52b2c239 100644 --- a/src/main/java/de/danielbechler/diff/CircularReferenceService.java +++ b/src/main/java/de/danielbechler/diff/circular/CircularReferenceService.java @@ -1,11 +1,33 @@ -package de.danielbechler.diff; +/* + * 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. + */ -import org.slf4j.*; +package de.danielbechler.diff.circular; -/** @author Daniel Bechler */ -class CircularReferenceService implements CircularReferenceConfiguration, CircularReferenceDetectorFactory, CircularReferenceExceptionHandler +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Daniel Bechler + */ +public class CircularReferenceService implements CircularReferenceConfigurer, CircularReferenceDetectorFactory, CircularReferenceExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(CircularReferenceService.class); + private final ObjectDifferBuilder objectDifferBuilder; private CircularReferenceMatchingMode circularReferenceMatchingMode = CircularReferenceMatchingMode.EQUALITY_OPERATOR; private CircularReferenceExceptionHandler circularReferenceExceptionHandler = new CircularReferenceExceptionHandler() @@ -19,18 +41,28 @@ public void onCircularReferenceException(final DiffNode node) } }; - public CircularReferenceConfiguration matchCircularReferencesUsing(final CircularReferenceMatchingMode matchingMode) + public CircularReferenceService(final ObjectDifferBuilder objectDifferBuilder) + { + this.objectDifferBuilder = objectDifferBuilder; + } + + public CircularReferenceConfigurer matchCircularReferencesUsing(final CircularReferenceMatchingMode matchingMode) { this.circularReferenceMatchingMode = matchingMode; return this; } - public CircularReferenceConfiguration handleCircularReferenceExceptionsUsing(final CircularReferenceExceptionHandler exceptionHandler) + public CircularReferenceConfigurer handleCircularReferenceExceptionsUsing(final CircularReferenceExceptionHandler exceptionHandler) { this.circularReferenceExceptionHandler = exceptionHandler; return this; } + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } + public CircularReferenceDetector createCircularReferenceDetector() { if (circularReferenceMatchingMode == CircularReferenceMatchingMode.EQUALS_METHOD) diff --git a/src/main/java/de/danielbechler/diff/collection/CollectionDiffer.java b/src/main/java/de/danielbechler/diff/collection/CollectionDiffer.java deleted file mode 100644 index c3a7dfe2..00000000 --- a/src/main/java/de/danielbechler/diff/collection/CollectionDiffer.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2012 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.collection; - -import de.danielbechler.diff.*; -import de.danielbechler.util.*; -import de.danielbechler.util.Collections; - -import java.util.*; - -/** - * Used to find differences between {@link Collection Collections}. - * - * @author Daniel Bechler - */ -public final class CollectionDiffer implements Differ -{ - private final DifferDispatcher differDispatcher; - private final ComparisonStrategyResolver comparisonStrategyResolver; - - public CollectionDiffer(final DifferDispatcher differDispatcher, - final ComparisonStrategyResolver comparisonStrategyResolver) - { - Assert.notNull(differDispatcher, "differDispatcher"); - this.differDispatcher = differDispatcher; - - Assert.notNull(comparisonStrategyResolver, "comparisonStrategyResolver"); - this.comparisonStrategyResolver = comparisonStrategyResolver; - } - - public boolean accepts(final Class type) - { - return Collection.class.isAssignableFrom(type); - } - - public final DiffNode compare(final DiffNode parentNode, final Instances collectionInstances) - { - final DiffNode collectionNode = newNode(parentNode, collectionInstances); - if (collectionInstances.hasBeenAdded()) - { - final Collection addedItems = collectionInstances.getWorking(Collection.class); - compareItems(collectionNode, collectionInstances, addedItems); - collectionNode.setState(DiffNode.State.ADDED); - } - else if (collectionInstances.hasBeenRemoved()) - { - final Collection removedItems = collectionInstances.getBase(Collection.class); - compareItems(collectionNode, collectionInstances, removedItems); - collectionNode.setState(DiffNode.State.REMOVED); - } - else if (collectionInstances.areSame()) - { - collectionNode.setState(DiffNode.State.UNTOUCHED); - } - else - { - final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(collectionNode); - if (comparisonStrategy == null) - { - compareInternally(collectionNode, collectionInstances); - } - else - { - compareUsingComparisonStrategy(collectionNode, collectionInstances, comparisonStrategy); - } - } - return collectionNode; - } - - private static void compareUsingComparisonStrategy(final DiffNode collectionNode, - final Instances collectionInstances, - final ComparisonStrategy comparisonStrategy) - { - comparisonStrategy.compare(collectionNode, collectionInstances); - } - - private void compareInternally(final DiffNode collectionNode, final Instances collectionInstances) - { - compareItems(collectionNode, collectionInstances, addedItemsOf(collectionInstances)); - compareItems(collectionNode, collectionInstances, removedItemsOf(collectionInstances)); - compareItems(collectionNode, collectionInstances, knownItemsOf(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); - } - - private void compareItems(final DiffNode collectionNode, - final Instances collectionInstances, - final Iterable items) - { - for (final Object item : items) - { - final Accessor itemAccessor = new CollectionItemAccessor(item); - differDispatcher.dispatch(collectionNode, collectionInstances, itemAccessor); - } - } - - 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; - } -} diff --git a/src/main/java/de/danielbechler/diff/collection/CollectionElement.java b/src/main/java/de/danielbechler/diff/collection/CollectionElement.java deleted file mode 100644 index adea3813..00000000 --- a/src/main/java/de/danielbechler/diff/collection/CollectionElement.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2012 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.collection; - -import de.danielbechler.diff.*; -import de.danielbechler.util.*; - -/** @author Daniel Bechler */ -public final class CollectionElement extends Element -{ - private final Object item; - - public CollectionElement(final Object item) - { - this.item = item; - } - - /** @deprecated Low-level API. Don't use in production code. May be removed in future versions. */ - @SuppressWarnings({"UnusedDeclaration"}) - @Deprecated - public Object getItem() - { - return item; - } - - @Override - public String toHumanReadableString() - { - return "[" + Strings.toSingleLineString(item) + "]"; - } - - @Override - public boolean equals(final Object o) - { - if (this == o) - { - return true; - } - if (o == null || getClass() != o.getClass()) - { - return false; - } - - final CollectionElement that = (CollectionElement) o; - - if (item != null ? !item.equals(that.item) : that.item != null) - { - return false; - } - - return true; - } - - @Override - public int hashCode() - { - return item != null ? item.hashCode() : 0; - } -} diff --git a/src/main/java/de/danielbechler/diff/ComparableComparisonStrategy.java b/src/main/java/de/danielbechler/diff/comparison/ComparableComparisonStrategy.java similarity index 65% rename from src/main/java/de/danielbechler/diff/ComparableComparisonStrategy.java rename to src/main/java/de/danielbechler/diff/comparison/ComparableComparisonStrategy.java index 4e5852f3..96114c1c 100644 --- a/src/main/java/de/danielbechler/diff/ComparableComparisonStrategy.java +++ b/src/main/java/de/danielbechler/diff/comparison/ComparableComparisonStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,21 +14,22 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.comparison; -import static de.danielbechler.util.Comparables.*; +import de.danielbechler.diff.node.DiffNode; -/** @author Daniel Bechler */ +import static de.danielbechler.util.Comparables.isEqualByComparison; + +/** + * @author Daniel Bechler + */ public class ComparableComparisonStrategy implements ComparisonStrategy { - public void compare(final DiffNode node, final Instances instances) + public void compare(final DiffNode node, final Class type, final Object working, final Object base) { - final Class type = instances.getType(); if (Comparable.class.isAssignableFrom(type)) { - final Comparable working = (Comparable) instances.getWorking(); - final Comparable base = (Comparable) instances.getBase(); - if (isEqualByComparison(working, base)) + if (isEqualByComparison((Comparable) working, (Comparable) base)) { node.setState(DiffNode.State.UNTOUCHED); } diff --git a/src/main/java/de/danielbechler/diff/comparison/ComparisonConfigurer.java b/src/main/java/de/danielbechler/diff/comparison/ComparisonConfigurer.java new file mode 100644 index 00000000..2b3735a9 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/comparison/ComparisonConfigurer.java @@ -0,0 +1,55 @@ +/* + * 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.comparison; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.path.NodePath; + +/** + * Allows to configure the way objects are compared. Sometimes introspection is just not the way to go. Let it be for + * performance reasons or simply because the object doesn't expose any useful properties. In those cases it's possible + * to define alternative comparison strategies, like using the equals method, a comparator or even a custom strategy. + * These settings can be made for specific nodes or entire types. + * + * @author Daniel Bechler + */ +public interface ComparisonConfigurer +{ + Of ofNode(NodePath nodePath); + + Of ofType(Class type); + + OfPrimitiveTypes ofPrimitiveTypes(); + + ObjectDifferBuilder and(); + + interface Of + { + ComparisonConfigurer toUse(ComparisonStrategy comparisonStrategy); + + ComparisonConfigurer toUseEqualsMethod(); + + ComparisonConfigurer toUseEqualsMethodOfValueProvidedByMethod(String propertyName); + + ComparisonConfigurer toUseCompareToMethod(); + } + + interface OfPrimitiveTypes + { + ComparisonConfigurer toTreatDefaultValuesAs(PrimitiveDefaultValueMode primitiveDefaultValueMode); + } +} diff --git a/src/main/java/de/danielbechler/diff/comparison/ComparisonService.java b/src/main/java/de/danielbechler/diff/comparison/ComparisonService.java new file mode 100644 index 00000000..726f306d --- /dev/null +++ b/src/main/java/de/danielbechler/diff/comparison/ComparisonService.java @@ -0,0 +1,184 @@ +/* + * 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.comparison; + +import de.danielbechler.diff.ObjectDifferBuilder; +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; + +import java.util.HashMap; +import java.util.Map; + +public class ComparisonService implements ComparisonConfigurer, ComparisonStrategyResolver, PrimitiveDefaultValueModeResolver +{ + private static final ComparisonStrategy COMPARABLE_COMPARISON_STRATEGY = new ComparableComparisonStrategy(); + private static final ComparisonStrategy EQUALS_ONLY_COMPARISON_STRATEGY = new EqualsOnlyComparisonStrategy(); + + private final NodePathValueHolder nodePathComparisonStrategies = NodePathValueHolder.of(ComparisonStrategy.class); + private final Map, ComparisonStrategy> typeComparisonStrategyMap = new HashMap, ComparisonStrategy>(); + private final ObjectDifferBuilder objectDifferBuilder; + + private PrimitiveDefaultValueMode primitiveDefaultValueMode = PrimitiveDefaultValueMode.UNASSIGNED; + + public ComparisonService(final ObjectDifferBuilder objectDifferBuilder) + { + this.objectDifferBuilder = objectDifferBuilder; + } + + public ComparisonStrategy resolveComparisonStrategy(final DiffNode node) + { + final ComparisonStrategy comparisonStrategy = nodePathComparisonStrategies.valueForNodePath(node.getPath()); + if (comparisonStrategy != null) + { + return comparisonStrategy; + } + + final Class valueType = node.getValueType(); + if (typeComparisonStrategyMap.containsKey(valueType)) + { + return typeComparisonStrategyMap.get(valueType); + } + + if (Classes.isSimpleType(valueType)) + { + // if the simple type implements comparable we use that, since its contract + // dictates that compareTo == zero carries the same semantics as equals + if (Classes.isComparableType(valueType)) + { + return COMPARABLE_COMPARISON_STRATEGY; + } + else + { + return EQUALS_ONLY_COMPARISON_STRATEGY; + } + } + + final ObjectDiffPropertyComparisonStrategyResolver comparisonStrategyResolver = ObjectDiffPropertyComparisonStrategyResolver.instance; + + final ObjectDiffProperty objectDiffProperty = node.getPropertyAnnotation(ObjectDiffProperty.class); + final ComparisonStrategy comparisonStrategyFromObjectDiffPropertyAnnotation = comparisonStrategyResolver.comparisonStrategyForAnnotation(objectDiffProperty); + if (comparisonStrategyFromObjectDiffPropertyAnnotation != null) + { + return comparisonStrategyFromObjectDiffPropertyAnnotation; + } + + if (valueType != null) + { + final ObjectDiffEqualsOnlyType objectDiffEqualsOnlyType = valueType.getAnnotation(ObjectDiffEqualsOnlyType.class); + final ComparisonStrategy comparisonStrategyFromObjectDiffEqualsOnlyTypeAnnotation = comparisonStrategyResolver.comparisonStrategyForAnnotation(objectDiffEqualsOnlyType); + if (comparisonStrategyFromObjectDiffEqualsOnlyTypeAnnotation != null) + { + return comparisonStrategyFromObjectDiffEqualsOnlyTypeAnnotation; + } + } + + if (valueType == Object.class) + { + return EQUALS_ONLY_COMPARISON_STRATEGY; + } + + return null; + } + + public PrimitiveDefaultValueMode resolvePrimitiveDefaultValueMode(final DiffNode node) + { + return primitiveDefaultValueMode; + } + + public Of ofNode(final NodePath nodePath) + { + return new OfNodePath(nodePath); + } + + public Of ofType(final Class type) + { + return new OfType(type); + } + + public OfPrimitiveTypes ofPrimitiveTypes() + { + return new OfPrimitiveTypesImpl(); + } + + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } + + private abstract static class AbstractOf implements Of + { + public ComparisonConfigurer toUseEqualsMethod() + { + return toUse(new EqualsOnlyComparisonStrategy()); + } + + public ComparisonConfigurer toUseEqualsMethodOfValueProvidedByMethod(final String propertyName) + { + return toUse(new EqualsOnlyComparisonStrategy(propertyName)); + } + + public ComparisonConfigurer toUseCompareToMethod() + { + return toUse(new ComparableComparisonStrategy()); + } + } + + private class OfType extends AbstractOf + { + private final Class type; + + public OfType(final Class type) + { + this.type = type; + } + + public ComparisonConfigurer toUse(final ComparisonStrategy comparisonStrategy) + { + typeComparisonStrategyMap.put(type, comparisonStrategy); + return ComparisonService.this; + } + } + + private class OfNodePath extends AbstractOf + { + private final NodePath nodePath; + + public OfNodePath(final NodePath nodePath) + { + this.nodePath = nodePath; + } + + public ComparisonConfigurer toUse(final ComparisonStrategy comparisonStrategy) + { + nodePathComparisonStrategies.put(nodePath, comparisonStrategy); + return ComparisonService.this; + } + } + + private class OfPrimitiveTypesImpl implements OfPrimitiveTypes + { + public ComparisonConfigurer toTreatDefaultValuesAs(final PrimitiveDefaultValueMode mode) + { + primitiveDefaultValueMode = mode; + return ComparisonService.this; + } + } +} diff --git a/src/main/java/de/danielbechler/diff/comparison/ComparisonStrategy.java b/src/main/java/de/danielbechler/diff/comparison/ComparisonStrategy.java new file mode 100644 index 00000000..96db6fd1 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/comparison/ComparisonStrategy.java @@ -0,0 +1,27 @@ +/* + * 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.comparison; + +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ +public interface ComparisonStrategy +{ + void compare(final DiffNode node, @Deprecated Class type, final Object working, final Object base); +} diff --git a/src/main/java/de/danielbechler/diff/ComparisonStrategyResolver.java b/src/main/java/de/danielbechler/diff/comparison/ComparisonStrategyResolver.java similarity index 81% rename from src/main/java/de/danielbechler/diff/ComparisonStrategyResolver.java rename to src/main/java/de/danielbechler/diff/comparison/ComparisonStrategyResolver.java index a1bece0f..b130128f 100644 --- a/src/main/java/de/danielbechler/diff/ComparisonStrategyResolver.java +++ b/src/main/java/de/danielbechler/diff/comparison/ComparisonStrategyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,9 +14,13 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.comparison; -/** @author Daniel Bechler */ +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ public interface ComparisonStrategyResolver { ComparisonStrategy resolveComparisonStrategy(DiffNode node); diff --git a/src/main/java/de/danielbechler/diff/EqualsOnlyComparisonStrategy.java b/src/main/java/de/danielbechler/diff/comparison/EqualsOnlyComparisonStrategy.java similarity index 52% rename from src/main/java/de/danielbechler/diff/EqualsOnlyComparisonStrategy.java rename to src/main/java/de/danielbechler/diff/comparison/EqualsOnlyComparisonStrategy.java index 69b61abb..d307a2ee 100644 --- a/src/main/java/de/danielbechler/diff/EqualsOnlyComparisonStrategy.java +++ b/src/main/java/de/danielbechler/diff/comparison/EqualsOnlyComparisonStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,9 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.comparison; -import de.danielbechler.util.*; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; +import de.danielbechler.util.Exceptions; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import static de.danielbechler.util.Objects.isEqual; @@ -38,9 +43,20 @@ public EqualsOnlyComparisonStrategy(final String equalsValueProviderMethod) this.equalsValueProviderMethod = equalsValueProviderMethod; } - public void compare(final DiffNode node, final Instances instances) + public void compare(final DiffNode node, final Class type, final Object working, final Object base) { - if (hasEqual(instances)) + final boolean result; + if (equalsValueProviderMethod != null) + { + final Object workingValue = access(working, equalsValueProviderMethod); + final Object baseValue = access(base, equalsValueProviderMethod); + result = isEqual(workingValue, baseValue); + } + else + { + result = isEqual(working, base); + } + if (result) { node.setState(DiffNode.State.UNTOUCHED); } @@ -50,25 +66,29 @@ public void compare(final DiffNode node, final Instances instances) } } - private boolean hasEqual(final Instances instances) + private static Object access(final Object target, final String methodName) { - if (equalsValueProviderMethod != null) + if (target == null) { - return instances.access(equalsValueProviderMethod).areEqual(); + return null; } - else + try { - return isEqual(instances.getWorking(), instances.getBase()); + final Method method = target.getClass().getMethod(methodName); + method.setAccessible(true); + return method.invoke(target); + } + catch (final NoSuchMethodException e) + { + throw Exceptions.escalate(e); + } + catch (final InvocationTargetException e) + { + throw Exceptions.escalate(e); + } + catch (final IllegalAccessException e) + { + throw Exceptions.escalate(e); } - } - - public boolean hasValueProviderMethod() - { - return equalsValueProviderMethod != null; - } - - public String getEqualsValueProviderMethod() - { - return equalsValueProviderMethod; } } diff --git a/src/main/java/de/danielbechler/diff/comparison/ObjectDiffPropertyComparisonStrategyResolver.java b/src/main/java/de/danielbechler/diff/comparison/ObjectDiffPropertyComparisonStrategyResolver.java new file mode 100644 index 00000000..09c59512 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/comparison/ObjectDiffPropertyComparisonStrategyResolver.java @@ -0,0 +1,66 @@ +/* + * 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.comparison; + +import de.danielbechler.diff.introspection.ObjectDiffEqualsOnlyType; +import de.danielbechler.diff.introspection.ObjectDiffProperty; +import de.danielbechler.util.Strings; + +/** + * Created by Daniel Bechler. + */ +public class ObjectDiffPropertyComparisonStrategyResolver +{ + public static final ObjectDiffPropertyComparisonStrategyResolver instance; + + static + { + instance = new ObjectDiffPropertyComparisonStrategyResolver(); + } + + private ObjectDiffPropertyComparisonStrategyResolver() + { + } + + @SuppressWarnings("MethodMayBeStatic") + public ComparisonStrategy comparisonStrategyForAnnotation(final ObjectDiffProperty annotation) + { + if (annotation == null || !annotation.equalsOnly()) + { + return null; + } + if (Strings.hasText(annotation.equalsOnlyValueProviderMethod())) + { + return new EqualsOnlyComparisonStrategy(annotation.equalsOnlyValueProviderMethod()); + } + return new EqualsOnlyComparisonStrategy(); + } + + @SuppressWarnings("MethodMayBeStatic") + public ComparisonStrategy comparisonStrategyForAnnotation(final ObjectDiffEqualsOnlyType annotation) + { + if (annotation == null) + { + return null; + } + if (Strings.hasText(annotation.valueProviderMethod())) + { + return new EqualsOnlyComparisonStrategy(annotation.valueProviderMethod()); + } + return new EqualsOnlyComparisonStrategy(); + } +} diff --git a/src/main/java/de/danielbechler/diff/PrimitiveDefaultValueMode.java b/src/main/java/de/danielbechler/diff/comparison/PrimitiveDefaultValueMode.java similarity index 82% rename from src/main/java/de/danielbechler/diff/PrimitiveDefaultValueMode.java rename to src/main/java/de/danielbechler/diff/comparison/PrimitiveDefaultValueMode.java index 0b20399e..e691bb43 100644 --- a/src/main/java/de/danielbechler/diff/PrimitiveDefaultValueMode.java +++ b/src/main/java/de/danielbechler/diff/comparison/PrimitiveDefaultValueMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.comparison; /** * Defines how default values of primitive types (int, long, short, byte, char, boolean, float, double) will @@ -29,15 +29,15 @@ public enum PrimitiveDefaultValueMode { /** * Default values of primitive types will be treated like any other value. Since there is no distinction, any - * change to a primitive value will be marked as {@linkplain DiffNode.State#CHANGED}. + * change to a primitive value will be marked as {@linkplain de.danielbechler.diff.node.DiffNode.State#CHANGED}. */ ASSIGNED, /** * Default values of primitive types will be treated as if the property has not been set. The consequence of * this is that a change from default value to something else will be marked as {@linkplain - * DiffNode.State#ADDED} and from something else to the default value as {@linkplain - * DiffNode.State#REMOVED}. + * de.danielbechler.diff.node.DiffNode.State#ADDED} and from something else to the default value as {@linkplain + * de.danielbechler.diff.node.DiffNode.State#REMOVED}. */ UNASSIGNED } diff --git a/src/main/java/de/danielbechler/diff/PrimitiveDefaultValueModeResolver.java b/src/main/java/de/danielbechler/diff/comparison/PrimitiveDefaultValueModeResolver.java similarity index 81% rename from src/main/java/de/danielbechler/diff/PrimitiveDefaultValueModeResolver.java rename to src/main/java/de/danielbechler/diff/comparison/PrimitiveDefaultValueModeResolver.java index 391a5dba..c6352259 100644 --- a/src/main/java/de/danielbechler/diff/PrimitiveDefaultValueModeResolver.java +++ b/src/main/java/de/danielbechler/diff/comparison/PrimitiveDefaultValueModeResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,9 +14,13 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.comparison; -/** @author Daniel Bechler */ +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ public interface PrimitiveDefaultValueModeResolver { PrimitiveDefaultValueMode resolvePrimitiveDefaultValueMode(DiffNode node); diff --git a/src/main/java/de/danielbechler/diff/bean/BeanDiffer.java b/src/main/java/de/danielbechler/diff/differ/BeanDiffer.java similarity index 73% rename from src/main/java/de/danielbechler/diff/bean/BeanDiffer.java rename to src/main/java/de/danielbechler/diff/differ/BeanDiffer.java index 2187e347..77096221 100644 --- a/src/main/java/de/danielbechler/diff/bean/BeanDiffer.java +++ b/src/main/java/de/danielbechler/diff/differ/BeanDiffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,14 +14,22 @@ * limitations under the License. */ -package de.danielbechler.diff.bean; +package de.danielbechler.diff.differ; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.diff.comparison.ComparisonStrategy; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.filtering.IsReturnableResolver; +import de.danielbechler.diff.introspection.IsIntrospectableResolver; +import de.danielbechler.diff.instantiation.TypeInfo; +import de.danielbechler.diff.introspection.TypeInfoResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; /** * Used to find differences between objects that were not handled by one of the other (specialized) {@link - * de.danielbechler.diff.Differ Differs}. + * Differ Differs}. * * @author Daniel Bechler */ @@ -30,14 +38,14 @@ public final class BeanDiffer implements Differ private final IsIntrospectableResolver isIntrospectableResolver; private final IsReturnableResolver isReturnableResolver; private final ComparisonStrategyResolver comparisonStrategyResolver; - private final IntrospectorResolver introspectorResolver; private final DifferDispatcher differDispatcher; + private final TypeInfoResolver typeInfoResolver; public BeanDiffer(final DifferDispatcher differDispatcher, final IsIntrospectableResolver introspectableResolver, final IsReturnableResolver returnableResolver, final ComparisonStrategyResolver comparisonStrategyResolver, - final IntrospectorResolver introspectorResolver) + final TypeInfoResolver typeInfoResolver) { Assert.notNull(differDispatcher, "differDispatcher"); this.differDispatcher = differDispatcher; @@ -51,8 +59,8 @@ public BeanDiffer(final DifferDispatcher differDispatcher, Assert.notNull(comparisonStrategyResolver, "comparisonStrategyResolver"); this.comparisonStrategyResolver = comparisonStrategyResolver; - Assert.notNull(introspectorResolver, "introspectorResolver"); - this.introspectorResolver = introspectorResolver; + Assert.notNull(typeInfoResolver, "typeInfoResolver"); + this.typeInfoResolver = typeInfoResolver; } public boolean accepts(final Class type) @@ -63,11 +71,6 @@ public boolean accepts(final Class type) public final DiffNode compare(final DiffNode parentNode, final Instances instances) { final DiffNode beanNode = new DiffNode(parentNode, instances.getSourceAccessor(), instances.getType()); -// if (isIgnoredResolver.isIgnored(beanNode)) -// { -// beanNode.setState(Node.State.IGNORED); -// } -// else if (instances.areNull() || instances.areSame()) { beanNode.setState(DiffNode.State.UNTOUCHED); @@ -94,7 +97,7 @@ private void compareUsingAppropriateMethod(final DiffNode beanNode, final Instan final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(beanNode); if (comparisonStrategy != null) { - comparisonStrategy.compare(beanNode, instances); + comparisonStrategy.compare(beanNode, instances.getType(), instances.getWorking(), instances.getBase()); } else if (isIntrospectableResolver.isIntrospectable(beanNode)) { @@ -104,10 +107,9 @@ else if (isIntrospectableResolver.isIntrospectable(beanNode)) private void compareUsingIntrospection(final DiffNode beanNode, final Instances beanInstances) { - final Class beanType = beanInstances.getType(); - final Introspector introspector = introspectorResolver.introspectorForNode(beanNode); - final Iterable propertyAccessors = introspector.introspect(beanType); - for (final PropertyAwareAccessor propertyAccessor : propertyAccessors) + final TypeInfo typeInfo = typeInfoResolver.typeInfoForNode(beanNode); + beanNode.setValueTypeInfo(typeInfo); + for (final PropertyAwareAccessor propertyAccessor : typeInfo.getAccessors()) { final DiffNode propertyNode = differDispatcher.dispatch(beanNode, beanInstances, propertyAccessor); if (isReturnableResolver.isReturnable(propertyNode)) diff --git a/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java new file mode 100644 index 00000000..ba23c9e9 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/differ/CollectionDiffer.java @@ -0,0 +1,177 @@ +/* + * 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.differ; + +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 java.util.Collection; +import java.util.Iterator; +import java.util.LinkedList; + +/** + * Used to find differences between {@link Collection Collections}. + * + * @author Daniel Bechler + */ +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) + { + Assert.notNull(differDispatcher, "differDispatcher"); + this.differDispatcher = differDispatcher; + + Assert.notNull(comparisonStrategyResolver, "comparisonStrategyResolver"); + this.comparisonStrategyResolver = comparisonStrategyResolver; + + Assert.notNull(identityStrategyResolver, "identityStrategyResolver"); + this.identityStrategyResolver = identityStrategyResolver; + } + + public boolean accepts(final Class type) + { + return Collection.class.isAssignableFrom(type); + } + + public final DiffNode compare(final DiffNode parentNode, final Instances collectionInstances) + { + final DiffNode collectionNode = newNode(parentNode, collectionInstances); + final IdentityStrategy identityStrategy = identityStrategyResolver.resolveIdentityStrategy(collectionNode); + if (identityStrategy != null) + { + collectionNode.setChildIdentityStrategy(identityStrategy); + } + 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); + collectionNode.setState(DiffNode.State.REMOVED); + } + else if (collectionInstances.areSame()) + { + collectionNode.setState(DiffNode.State.UNTOUCHED); + } + else + { + final ComparisonStrategy comparisonStrategy = comparisonStrategyResolver.resolveComparisonStrategy(collectionNode); + if (comparisonStrategy == null) + { + compareInternally(collectionNode, collectionInstances, identityStrategy); + } + else + { + compareUsingComparisonStrategy(collectionNode, collectionInstances, comparisonStrategy); + } + } + return collectionNode; + } + + 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); + } + + private void compareItems(final DiffNode collectionNode, + final Instances collectionInstances, + final Iterable items, + final IdentityStrategy identityStrategy) + { + for (final Object item : items) + { + final Accessor itemAccessor = new CollectionItemAccessor(item, identityStrategy); + differDispatcher.dispatch(collectionNode, collectionInstances, itemAccessor); + } + } + + 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 Iterable added = new LinkedList(working); + final Iterable removed = new LinkedList(base); + final Iterable known = new LinkedList(base); + + remove(added, base, identityStrategy); + remove(removed, working, identityStrategy); + remove(known, added, identityStrategy); + remove(known, removed, identityStrategy); + + compareItems(collectionNode, collectionInstances, added, identityStrategy); + compareItems(collectionNode, collectionInstances, removed, identityStrategy); + compareItems(collectionNode, collectionInstances, known, identityStrategy); + } + + 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 void remove(final Iterable from, final Iterable these, final IdentityStrategy identityStrategy) + { + final Iterator iterator = from.iterator(); + while (iterator.hasNext()) + { + final Object item = iterator.next(); + if (contains(these, item, identityStrategy)) + { + iterator.remove(); + } + } + } + + private boolean contains(final Iterable haystack, final Object needle, final IdentityStrategy identityStrategy) + { + for (final Object item : haystack) + { + if (identityStrategy.equals(needle, item)) + { + return true; + } + } + return false; + } +} diff --git a/src/main/java/de/danielbechler/diff/Differ.java b/src/main/java/de/danielbechler/diff/differ/Differ.java similarity index 81% rename from src/main/java/de/danielbechler/diff/Differ.java rename to src/main/java/de/danielbechler/diff/differ/Differ.java index 779f94bc..bea4dcbb 100644 --- a/src/main/java/de/danielbechler/diff/Differ.java +++ b/src/main/java/de/danielbechler/diff/differ/Differ.java @@ -14,9 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.differ; -/** @author Daniel Bechler */ +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ public interface Differ { boolean accepts(Class type); diff --git a/src/main/java/de/danielbechler/diff/differ/DifferConfigurer.java b/src/main/java/de/danielbechler/diff/differ/DifferConfigurer.java new file mode 100644 index 00000000..48a86cb1 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/differ/DifferConfigurer.java @@ -0,0 +1,36 @@ +/* + * 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.differ; + +import de.danielbechler.diff.ObjectDifferBuilder; + +/** + * Created by Daniel Bechler. + */ +public interface DifferConfigurer +{ + /** + * Registers a new Differ to be used when comparing objects. Differs that have been registered later always win + * over earlier Differs. This way it is easily possible to override the default behavior, without actually removing + * the standard differs. + * + * @param differFactory Factory that creates a new Differ. Will be called exactly once and the resulting Differ + * will be pushed to the underlying {@link de.danielbechler.diff.differ.DifferProvider}. + * @return The {@link de.danielbechler.diff.ObjectDifferBuilder} for chaining. + */ + ObjectDifferBuilder register(DifferFactory differFactory); +} diff --git a/src/main/java/de/danielbechler/diff/differ/DifferDispatcher.java b/src/main/java/de/danielbechler/diff/differ/DifferDispatcher.java new file mode 100644 index 00000000..49f355d7 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/differ/DifferDispatcher.java @@ -0,0 +1,274 @@ +/* + * 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.differ; + +import de.danielbechler.diff.access.Accessor; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.diff.category.CategoryResolver; +import de.danielbechler.diff.introspection.PropertyReadException; +import de.danielbechler.diff.circular.CircularReferenceDetector; +import de.danielbechler.diff.circular.CircularReferenceDetectorFactory; +import de.danielbechler.diff.circular.CircularReferenceExceptionHandler; +import de.danielbechler.diff.filtering.IsReturnableResolver; +import de.danielbechler.diff.inclusion.IsIgnoredResolver; +import de.danielbechler.diff.introspection.PropertyAccessExceptionHandler; +import de.danielbechler.diff.introspection.PropertyAccessExceptionHandlerResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.util.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static de.danielbechler.diff.circular.CircularReferenceDetector.CircularReferenceException; + +/** + * @author Daniel Bechler + */ +public class DifferDispatcher +{ + private static final Logger logger = LoggerFactory.getLogger(DifferDispatcher.class); + private final DifferProvider differProvider; + private final CircularReferenceDetectorFactory circularReferenceDetectorFactory; + private final CircularReferenceExceptionHandler circularReferenceExceptionHandler; + private final IsIgnoredResolver isIgnoredResolver; + private final CategoryResolver categoryResolver; + private final IsReturnableResolver isReturnableResolver; + private final PropertyAccessExceptionHandlerResolver propertyAccessExceptionHandlerResolver; + private static final ThreadLocal workingThreadLocal = new ThreadLocal(); + private static final ThreadLocal baseThreadLocal = new ThreadLocal(); + + public DifferDispatcher(final DifferProvider differProvider, + final CircularReferenceDetectorFactory circularReferenceDetectorFactory, + final CircularReferenceExceptionHandler circularReferenceExceptionHandler, + final IsIgnoredResolver ignoredResolver, + final IsReturnableResolver returnableResolver, + final PropertyAccessExceptionHandlerResolver propertyAccessExceptionHandlerResolver, + final CategoryResolver categoryResolver) + { + Assert.notNull(differProvider, "differFactory"); + this.differProvider = differProvider; + + Assert.notNull(ignoredResolver, "ignoredResolver"); + this.isIgnoredResolver = ignoredResolver; + + Assert.notNull(categoryResolver, "categoryResolver"); + this.categoryResolver = categoryResolver; + + this.circularReferenceDetectorFactory = circularReferenceDetectorFactory; + this.circularReferenceExceptionHandler = circularReferenceExceptionHandler; + this.isReturnableResolver = returnableResolver; + this.propertyAccessExceptionHandlerResolver = propertyAccessExceptionHandlerResolver; + + resetInstanceMemory(); + } + + public final void resetInstanceMemory() + { + workingThreadLocal.set(circularReferenceDetectorFactory.createCircularReferenceDetector()); + baseThreadLocal.set(circularReferenceDetectorFactory.createCircularReferenceDetector()); + } + + public final void clearInstanceMemory() + { + workingThreadLocal.remove(); + baseThreadLocal.remove(); + } + + /** + * Delegates the call to an appropriate {@link Differ}. + * + * @return A node representing the difference between the given {@link Instances}. + */ + public DiffNode dispatch(final DiffNode parentNode, + final Instances parentInstances, + final Accessor accessor) + { + Assert.notNull(parentInstances, "parentInstances"); + Assert.notNull(accessor, "accessor"); + + final DiffNode node = compare(parentNode, parentInstances, accessor); + if (parentNode != null && isReturnableResolver.isReturnable(node)) + { + parentNode.addChild(node); + } + if (node != null) + { + node.addCategories(categoryResolver.resolveCategories(node)); + } + return node; + } + + private DiffNode compare(final DiffNode parentNode, final Instances parentInstances, final Accessor accessor) + { + final DiffNode node = new DiffNode(parentNode, accessor, null); + if (isIgnoredResolver.isIgnored(node)) + { + node.setState(DiffNode.State.IGNORED); + return node; + } + + final Instances accessedInstances; + if (accessor instanceof PropertyAwareAccessor) + { + final PropertyAwareAccessor propertyAwareAccessor = (PropertyAwareAccessor) accessor; + try + { + accessedInstances = parentInstances.access(accessor); + } + catch (final PropertyReadException e) + { + node.setState(DiffNode.State.INACCESSIBLE); + final Class parentType = parentInstances.getType(); + final String propertyName = propertyAwareAccessor.getPropertyName(); + final PropertyAccessExceptionHandler exceptionHandler = propertyAccessExceptionHandlerResolver + .resolvePropertyAccessExceptionHandler(parentType, propertyName); + if (exceptionHandler != null) + { + exceptionHandler.onPropertyReadException(e, node); + } + return node; + } + } + else + { + accessedInstances = parentInstances.access(accessor); + } + + if (accessedInstances.areNull()) + { + return new DiffNode(parentNode, accessedInstances.getSourceAccessor(), accessedInstances.getType()); + } + else + { + return compareWithCircularReferenceTracking(parentNode, accessedInstances); + } + } + + private DiffNode compareWithCircularReferenceTracking(final DiffNode parentNode, + final Instances instances) + { + DiffNode node = null; + try + { + rememberInstances(parentNode, instances); + try + { + node = compare(parentNode, instances); + } + finally + { + if (node != null) + { + forgetInstances(parentNode, instances); + } + } + } + catch (final CircularReferenceException e) + { + node = newCircularNode(parentNode, instances, e.getNodePath()); + circularReferenceExceptionHandler.onCircularReferenceException(node); + } + if (parentNode == null) + { + resetInstanceMemory(); + } + return node; + } + + private DiffNode compare(final DiffNode parentNode, final Instances instances) + { + final Differ differ = differProvider.retrieveDifferForType(instances.getType()); + if (differ == null) + { + throw new IllegalStateException("Couldn't create Differ for type '" + instances.getType() + + "'. This mustn't happen, as there should always be a fallback differ."); + } + return differ.compare(parentNode, instances); + } + + protected static void forgetInstances(final DiffNode parentNode, final Instances instances) + { + final NodePath nodePath = getNodePath(parentNode, instances); + logger.debug("[ {} ] Forgetting --- WORKING: {} <=> BASE: {}", nodePath, instances.getWorking(), instances.getBase()); + workingThreadLocal.get().remove(instances.getWorking()); + baseThreadLocal.get().remove(instances.getBase()); + } + + private static NodePath getNodePath(final DiffNode parentNode, final Instances instances) + { + if (parentNode == null) + { + return NodePath.withRoot(); + } + else + { + final NodePath parentPath = parentNode.getPath(); + final ElementSelector elementSelector = instances.getSourceAccessor().getElementSelector(); + return NodePath.startBuildingFrom(parentPath).element(elementSelector).build(); + } + } + + protected static void rememberInstances(final DiffNode parentNode, final Instances instances) + { + final NodePath nodePath = getNodePath(parentNode, instances); + logger.debug("[ {} ] Remembering --- WORKING: {} <=> BASE: {}", nodePath, instances.getWorking(), instances.getBase()); + transactionalPushToCircularReferenceDetectors(nodePath, instances); + } + + private static void transactionalPushToCircularReferenceDetectors(final NodePath nodePath, final Instances instances) + { + workingThreadLocal.get().push(instances.getWorking(), nodePath); + + // TODO This needs to be solved more elegantly. If the push for one of these detectors fails, + // we need to make sure to revert the push to the other one, if it already happened. + try + { + baseThreadLocal.get().push(instances.getBase(), nodePath); + } + catch (final CircularReferenceException e) + { + workingThreadLocal.get().remove(instances.getWorking()); // rollback + throw e; + } + } + + private static DiffNode findNodeMatchingPropertyPath(final DiffNode node, final NodePath nodePath) + { + if (node == null) + { + return null; + } + if (node.matches(nodePath)) + { + return node; + } + return findNodeMatchingPropertyPath(node.getParentNode(), nodePath); + } + + private static DiffNode newCircularNode(final DiffNode parentNode, + final Instances instances, + final NodePath circleStartPath) + { + final DiffNode node = new DiffNode(parentNode, instances.getSourceAccessor(), instances.getType()); + node.setState(DiffNode.State.CIRCULAR); + node.setCircleStartPath(circleStartPath); + node.setCircleStartNode(findNodeMatchingPropertyPath(parentNode, circleStartPath)); + return node; + } +} diff --git a/src/main/java/de/danielbechler/diff/annotation/ObjectDiffAnnotation.java b/src/main/java/de/danielbechler/diff/differ/DifferFactory.java similarity index 67% rename from src/main/java/de/danielbechler/diff/annotation/ObjectDiffAnnotation.java rename to src/main/java/de/danielbechler/diff/differ/DifferFactory.java index bbe16bd5..c8c01383 100644 --- a/src/main/java/de/danielbechler/diff/annotation/ObjectDiffAnnotation.java +++ b/src/main/java/de/danielbechler/diff/differ/DifferFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,14 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff.annotation; +package de.danielbechler.diff.differ; -import java.lang.annotation.*; +import de.danielbechler.diff.NodeQueryService; -/** @author Daniel Bechler */ -@Target({ElementType.ANNOTATION_TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -public @interface ObjectDiffAnnotation +/** + * Created by Daniel Bechler. + */ +public interface DifferFactory { + Differ createDiffer(DifferDispatcher differDispatcher, NodeQueryService nodeQueryService); } diff --git a/src/main/java/de/danielbechler/diff/DifferProvider.java b/src/main/java/de/danielbechler/diff/differ/DifferProvider.java similarity index 81% rename from src/main/java/de/danielbechler/diff/DifferProvider.java rename to src/main/java/de/danielbechler/diff/differ/DifferProvider.java index 65fa5f6b..7a704d27 100644 --- a/src/main/java/de/danielbechler/diff/DifferProvider.java +++ b/src/main/java/de/danielbechler/diff/differ/DifferProvider.java @@ -14,11 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.differ; -import java.util.*; +import java.util.LinkedList; +import java.util.List; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class DifferProvider { private final List differs = new LinkedList(); @@ -28,6 +31,14 @@ public void push(final Differ differ) differs.add(0, differ); } + public void pushAll(final Iterable differs) + { + for (final Differ differ : differs) + { + push(differ); + } + } + public Differ retrieveDifferForType(final Class type) { if (type == null) diff --git a/src/main/java/de/danielbechler/diff/differ/DifferService.java b/src/main/java/de/danielbechler/diff/differ/DifferService.java new file mode 100644 index 00000000..cf505d60 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/differ/DifferService.java @@ -0,0 +1,48 @@ +/* + * Copyright 2016 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.differ; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.util.Assert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +public class DifferService implements DifferConfigurer +{ + private final ObjectDifferBuilder objectDifferBuilder; + private final Collection differFactories = new ArrayList(); + + public DifferService(final ObjectDifferBuilder objectDifferBuilder) + { + Assert.notNull(objectDifferBuilder, "objectDifferBuilder"); + this.objectDifferBuilder = objectDifferBuilder; + } + + public ObjectDifferBuilder register(final DifferFactory differFactory) + { + Assert.notNull(differFactory, "differFactory"); + differFactories.add(differFactory); + return objectDifferBuilder; + } + + public Collection getDifferFactories() + { + return Collections.unmodifiableCollection(differFactories); + } +} diff --git a/src/main/java/de/danielbechler/diff/map/MapDiffer.java b/src/main/java/de/danielbechler/diff/differ/MapDiffer.java similarity index 89% rename from src/main/java/de/danielbechler/diff/map/MapDiffer.java rename to src/main/java/de/danielbechler/diff/differ/MapDiffer.java index aae1c9ef..948fbfc9 100644 --- a/src/main/java/de/danielbechler/diff/map/MapDiffer.java +++ b/src/main/java/de/danielbechler/diff/differ/MapDiffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,9 +14,12 @@ * limitations under the License. */ -package de.danielbechler.diff.map; +package de.danielbechler.diff.differ; -import de.danielbechler.diff.*; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.access.MapEntryAccessor; +import de.danielbechler.diff.comparison.ComparisonStrategyResolver; +import de.danielbechler.diff.node.DiffNode; import de.danielbechler.util.Assert; import de.danielbechler.util.Collections; @@ -41,6 +44,29 @@ public MapDiffer(final DifferDispatcher differDispatcher, final ComparisonStrate this.comparisonStrategyResolver = comparisonStrategyResolver; } + private static Collection findAddedKeys(final Instances instances) + { + final Set source = instances.getWorking(Map.class).keySet(); + final Set filter = instances.getBase(Map.class).keySet(); + return Collections.filteredCopyOf(source, filter); + } + + private static Collection findRemovedKeys(final Instances instances) + { + final Set source = instances.getBase(Map.class).keySet(); + final Set filter = instances.getWorking(Map.class).keySet(); + return Collections.filteredCopyOf(source, filter); + } + + private static Iterable findKnownKeys(final Instances instances) + { + final Set keys = instances.getWorking(Map.class).keySet(); + final Collection changed = Collections.setOf(keys); + changed.removeAll(findAddedKeys(instances)); + changed.removeAll(findRemovedKeys(instances)); + return changed; + } + public boolean accepts(final Class type) { if (type != null) @@ -69,7 +95,7 @@ else if (instances.areSame()) } else if (comparisonStrategyResolver.resolveComparisonStrategy(mapNode) != null) { - comparisonStrategyResolver.resolveComparisonStrategy(mapNode).compare(mapNode, instances); + comparisonStrategyResolver.resolveComparisonStrategy(mapNode).compare(mapNode, instances.getType(), instances.getWorking(Map.class), instances.getBase(Map.class)); } else { @@ -87,27 +113,4 @@ private void compareEntries(final DiffNode mapNode, final Instances mapInstances differDispatcher.dispatch(mapNode, mapInstances, new MapEntryAccessor(key)); } } - - private static Collection findAddedKeys(final Instances instances) - { - final Set source = instances.getWorking(Map.class).keySet(); - final Set filter = instances.getBase(Map.class).keySet(); - return Collections.filteredCopyOf(source, filter); - } - - private static Collection findRemovedKeys(final Instances instances) - { - final Set source = instances.getBase(Map.class).keySet(); - final Set filter = instances.getWorking(Map.class).keySet(); - return Collections.filteredCopyOf(source, filter); - } - - private static Iterable findKnownKeys(final Instances instances) - { - final Set keys = instances.getWorking(Map.class).keySet(); - final Collection changed = Collections.setOf(keys); - changed.removeAll(findAddedKeys(instances)); - changed.removeAll(findRemovedKeys(instances)); - return changed; - } } diff --git a/src/main/java/de/danielbechler/diff/primitive/PrimitiveDiffer.java b/src/main/java/de/danielbechler/diff/differ/PrimitiveDiffer.java similarity index 76% rename from src/main/java/de/danielbechler/diff/primitive/PrimitiveDiffer.java rename to src/main/java/de/danielbechler/diff/differ/PrimitiveDiffer.java index 53e03f06..91107f00 100644 --- a/src/main/java/de/danielbechler/diff/primitive/PrimitiveDiffer.java +++ b/src/main/java/de/danielbechler/diff/differ/PrimitiveDiffer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,20 +14,26 @@ * limitations under the License. */ -package de.danielbechler.diff.primitive; +package de.danielbechler.diff.differ; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.diff.access.Instances; +import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; +import de.danielbechler.util.Classes; -import static de.danielbechler.diff.PrimitiveDefaultValueMode.*; +import static de.danielbechler.diff.comparison.PrimitiveDefaultValueMode.UNASSIGNED; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public final class PrimitiveDiffer implements Differ { private final PrimitiveDefaultValueModeResolver primitiveDefaultValueModeResolver; public PrimitiveDiffer(final PrimitiveDefaultValueModeResolver primitiveDefaultValueModeResolver) { + Assert.notNull(primitiveDefaultValueModeResolver, "primitiveDefaultValueModeResolver"); this.primitiveDefaultValueModeResolver = primitiveDefaultValueModeResolver; } @@ -38,7 +44,7 @@ public boolean accepts(final Class type) public final DiffNode compare(final DiffNode parentNode, final Instances instances) { - if (!instances.getType().isPrimitive()) + if (!accepts(instances.getType())) { throw new IllegalArgumentException("The primitive differ can only deal with primitive types."); } diff --git a/src/main/java/de/danielbechler/diff/filtering/FilteringConfigurer.java b/src/main/java/de/danielbechler/diff/filtering/FilteringConfigurer.java new file mode 100644 index 00000000..125568e5 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/filtering/FilteringConfigurer.java @@ -0,0 +1,43 @@ +/* + * 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.filtering; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; + +/** + * 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. + *

+ * Currently it is only possible to configure returnability based on the state (_added_, _changed_, _untouched_, etc.) + * of the `DiffNode`. But this is just the beginning. Nothing speaks against adding more powerful options. It would be + * nice for example to be able to pass some kind of matcher to determine returnability based on dynamic criteria at + * runtime. + * + * @author Daniel Bechler + */ +public interface FilteringConfigurer +{ + FilteringConfigurer returnNodesWithState(DiffNode.State state, boolean enabled); + + FilteringConfigurer returnNodesWithState(DiffNode.State state); + + FilteringConfigurer omitNodesWithState(DiffNode.State state); + + ObjectDifferBuilder and(); +} diff --git a/src/main/java/de/danielbechler/diff/IsReturnableResolver.java b/src/main/java/de/danielbechler/diff/filtering/IsReturnableResolver.java similarity index 80% rename from src/main/java/de/danielbechler/diff/IsReturnableResolver.java rename to src/main/java/de/danielbechler/diff/filtering/IsReturnableResolver.java index d6d442f2..94aade23 100644 --- a/src/main/java/de/danielbechler/diff/IsReturnableResolver.java +++ b/src/main/java/de/danielbechler/diff/filtering/IsReturnableResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,9 +14,13 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.filtering; -/** @author Daniel Bechler */ +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ public interface IsReturnableResolver { boolean isReturnable(DiffNode node); diff --git a/src/main/java/de/danielbechler/diff/ReturnableNodeService.java b/src/main/java/de/danielbechler/diff/filtering/ReturnableNodeService.java similarity index 73% rename from src/main/java/de/danielbechler/diff/ReturnableNodeService.java rename to src/main/java/de/danielbechler/diff/filtering/ReturnableNodeService.java index 6047ef33..17be1c0f 100644 --- a/src/main/java/de/danielbechler/diff/ReturnableNodeService.java +++ b/src/main/java/de/danielbechler/diff/filtering/ReturnableNodeService.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,22 +14,34 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.filtering; -import java.util.*; +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.node.DiffNode; -import static de.danielbechler.util.Collections.*; -import static java.util.Arrays.*; +import java.util.Collection; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Set; -/** @author Daniel Bechler */ -public class ReturnableNodeService implements ReturnableNodeConfiguration, IsReturnableResolver +import static de.danielbechler.util.Collections.filteredCopyOf; +import static java.util.Arrays.asList; + +/** + * @author Daniel Bechler + */ +public class ReturnableNodeService implements FilteringConfigurer, IsReturnableResolver { private final Map stateFilterSettings; + private final ObjectDifferBuilder objectDifferBuilder; - public ReturnableNodeService() + public ReturnableNodeService(final ObjectDifferBuilder objectDifferBuilder) { + this.objectDifferBuilder = objectDifferBuilder; this.stateFilterSettings = new EnumMap(DiffNode.State.class); this.stateFilterSettings.put(DiffNode.State.IGNORED, false); + this.stateFilterSettings.put(DiffNode.State.INACCESSIBLE, false); this.stateFilterSettings.put(DiffNode.State.UNTOUCHED, false); this.stateFilterSettings.put(DiffNode.State.CIRCULAR, true); this.stateFilterSettings.put(DiffNode.State.ADDED, true); @@ -79,4 +91,9 @@ public ReturnableNodeService omitNodesWithState(final DiffNode.State state) { return returnNodesWithState(state, false); } + + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } } diff --git a/src/main/java/de/danielbechler/diff/identity/CollectionItemIdentityService.java b/src/main/java/de/danielbechler/diff/identity/CollectionItemIdentityService.java new file mode 100644 index 00000000..75484b4f --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/CollectionItemIdentityService.java @@ -0,0 +1,103 @@ +/* + * 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.inclusion.ValueNode; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.util.Assert; + +class CollectionItemIdentityService implements IdentityStrategyResolver +{ + private final ValueNode nodePathIdentityStrategies; + private final TypePropertyIdentityStrategyResolver typePropertyIdentityStrategyResolver; + private final IdentityConfigurer identityConfigurer; + private IdentityStrategy defaultIdentityStrategy = EqualsIdentityStrategy.getInstance(); + + CollectionItemIdentityService(final IdentityConfigurer identityConfigurer) + { + this.identityConfigurer = identityConfigurer; + this.nodePathIdentityStrategies = new ValueNode(); + this.typePropertyIdentityStrategyResolver = new TypePropertyIdentityStrategyResolver(); + } + + public IdentityStrategy resolveIdentityStrategy(final DiffNode node) + { + IdentityStrategy identityStrategy = typePropertyIdentityStrategyResolver.resolve(node); + if (identityStrategy != null) + { + return identityStrategy; + } + identityStrategy = nodePathIdentityStrategies.getNodeForPath(node.getPath()).getValue(); + if (identityStrategy != null) + { + return identityStrategy; + } + return defaultIdentityStrategy; + } + + IdentityConfigurer.OfCollectionItems ofCollectionItems(final NodePath nodePath) + { + return new OfCollectionItemsByNodePath(nodePath); + } + + IdentityConfigurer.OfCollectionItems ofCollectionItems(final Class type, final String propertyName) + { + return new OfCollectionItemsByTypeProperty(type, propertyName); + } + + IdentityConfigurer setDefaultIdentityStrategy(final IdentityStrategy identityStrategy) + { + Assert.notNull(identityStrategy, "identityStrategy"); + this.defaultIdentityStrategy = identityStrategy; + return identityConfigurer; + } + + private class OfCollectionItemsByNodePath implements IdentityConfigurer.OfCollectionItems + { + private final NodePath nodePath; + + OfCollectionItemsByNodePath(final NodePath nodePath) + { + this.nodePath = nodePath; + } + + public IdentityConfigurer via(final IdentityStrategy identityStrategy) + { + nodePathIdentityStrategies.getNodeForPath(nodePath).setValue(identityStrategy); + return identityConfigurer; + } + } + + private class OfCollectionItemsByTypeProperty implements IdentityConfigurer.OfCollectionItems + { + private final Class type; + private final String propertyName; + + OfCollectionItemsByTypeProperty(final Class type, final String propertyName) + { + this.type = type; + this.propertyName = propertyName; + } + + public IdentityConfigurer via(final IdentityStrategy identityStrategy) + { + typePropertyIdentityStrategyResolver.setStrategy(identityStrategy, type, propertyName); + return identityConfigurer; + } + } +} 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..1b7b2d65 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/EqualsIdentityStrategy.java @@ -0,0 +1,21 @@ +package de.danielbechler.diff.identity; + +import de.danielbechler.util.Objects; + +/** + * Default implementation that uses Object.equals. + */ +public class EqualsIdentityStrategy implements IdentityStrategy +{ + private static final EqualsIdentityStrategy instance = new EqualsIdentityStrategy(); + + public boolean equals(final Object working, final Object base) + { + return Objects.isEqual(working, base); + } + + public static EqualsIdentityStrategy getInstance() + { + return instance; + } +} 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..bd92d203 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityConfigurer.java @@ -0,0 +1,42 @@ +/* + * 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.path.NodePath; + +public interface IdentityConfigurer +{ + /** + * Allows to configure the way object identities are established between collection items. + */ + OfCollectionItems ofCollectionItems(NodePath nodePath); + + /** + * Allows to configure the way object identities are established between collection items. + */ + OfCollectionItems ofCollectionItems(Class type, String propertyName); + + IdentityConfigurer setDefaultCollectionItemIdentityStrategy(IdentityStrategy identityStrategy); + + ObjectDifferBuilder and(); + + interface OfCollectionItems + { + IdentityConfigurer via(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..cc0f7bc6 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityService.java @@ -0,0 +1,57 @@ +/* + * 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.path.NodePath; + +public class IdentityService implements IdentityConfigurer, IdentityStrategyResolver +{ + private final CollectionItemIdentityService collectionItemIdentityService = new CollectionItemIdentityService(this); + private final ObjectDifferBuilder objectDifferBuilder; + + public IdentityService(final ObjectDifferBuilder objectDifferBuilder) + { + this.objectDifferBuilder = objectDifferBuilder; + } + + public OfCollectionItems ofCollectionItems(final NodePath nodePath) + { + return collectionItemIdentityService.ofCollectionItems(nodePath); + } + + public OfCollectionItems ofCollectionItems(final Class type, final String propertyName) + { + return collectionItemIdentityService.ofCollectionItems(type, propertyName); + } + + public IdentityConfigurer setDefaultCollectionItemIdentityStrategy(final IdentityStrategy identityStrategy) + { + return collectionItemIdentityService.setDefaultIdentityStrategy(identityStrategy); + } + + public IdentityStrategy resolveIdentityStrategy(final DiffNode node) + { + return collectionItemIdentityService.resolveIdentityStrategy(node); + } + + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } +} 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..3504760e --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategy.java @@ -0,0 +1,18 @@ +package de.danielbechler.diff.identity; + +/** + * Allows to configure the way objects identities are established when comparing + * collections via {@linkplain de.danielbechler.diff.differ.CollectionDiffer}. + */ +public interface IdentityStrategy +{ + /** + * TODO Contract: {@linkplain IdentityStrategy#equals(Object working, Object base)} must always be true + * when working == base + * + * @param working + * @param base + * @return + */ + boolean equals(Object working, Object base); +} diff --git a/src/main/java/de/danielbechler/diff/IntrospectorResolver.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyAware.java similarity index 72% rename from src/main/java/de/danielbechler/diff/IntrospectorResolver.java rename to src/main/java/de/danielbechler/diff/identity/IdentityStrategyAware.java index 275f3d44..b4c41c6b 100644 --- a/src/main/java/de/danielbechler/diff/IntrospectorResolver.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyAware.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,10 +14,11 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.identity; -/** @author Daniel Bechler */ -public interface IntrospectorResolver +public interface IdentityStrategyAware { - Introspector introspectorForNode(DiffNode node); + IdentityStrategy getIdentityStrategy(); + + void setIdentityStrategy(IdentityStrategy identityStrategy); } diff --git a/src/main/java/de/danielbechler/diff/ComparisonStrategy.java b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java similarity index 72% rename from src/main/java/de/danielbechler/diff/ComparisonStrategy.java rename to src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java index 386383f0..0148acd5 100644 --- a/src/main/java/de/danielbechler/diff/ComparisonStrategy.java +++ b/src/main/java/de/danielbechler/diff/identity/IdentityStrategyResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,10 +14,11 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.identity; -/** @author Daniel Bechler */ -public interface ComparisonStrategy +import de.danielbechler.diff.node.DiffNode; + +public interface IdentityStrategyResolver { - void compare(final DiffNode node, Instances instances); + IdentityStrategy resolveIdentityStrategy(DiffNode node); } diff --git a/src/main/java/de/danielbechler/diff/identity/TypePropertyIdentityStrategyResolver.java b/src/main/java/de/danielbechler/diff/identity/TypePropertyIdentityStrategyResolver.java new file mode 100644 index 00000000..c1a3e0ba --- /dev/null +++ b/src/main/java/de/danielbechler/diff/identity/TypePropertyIdentityStrategyResolver.java @@ -0,0 +1,111 @@ +/* + * 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; +import de.danielbechler.util.Assert; + +import java.util.HashMap; +import java.util.Map; + +public class TypePropertyIdentityStrategyResolver +{ + 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()); + return strategies.get(propertyKey); + } + 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 IdentityStrategy identityStrategy, final Class type, final String... properties) + { + for (final 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; + } + } +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/CategoryInclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/CategoryInclusionResolver.java new file mode 100644 index 00000000..521f2d63 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/CategoryInclusionResolver.java @@ -0,0 +1,83 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.category.CategoryResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +class CategoryInclusionResolver implements InclusionResolver +{ + private final CategoryResolver categoryResolver; + private final Map categoryInclusions = new HashMap(); + private boolean containsIncluded; + private boolean containsExcluded; + + public CategoryInclusionResolver(final CategoryResolver categoryResolver) + { + Assert.notNull(categoryResolver, "categoryResolver"); + this.categoryResolver = categoryResolver; + } + + public Inclusion getInclusion(final DiffNode node) + { + if (isInactive()) + { + return DEFAULT; + } + final Set categories = categoryResolver.resolveCategories(node); + Inclusion resolvedInclusion = DEFAULT; + for (final String category : categories) + { + final Inclusion configuredInclusion = categoryInclusions.get(category); + if (configuredInclusion == EXCLUDED) + { + return EXCLUDED; + } + if (configuredInclusion == INCLUDED) + { + resolvedInclusion = INCLUDED; + } + } + return resolvedInclusion; + } + + private boolean isInactive() + { + return !containsIncluded && !containsExcluded; + } + + public boolean enablesStrictIncludeMode() + { + return containsIncluded; + } + + public void setInclusion(final String category, final Inclusion inclusion) + { + categoryInclusions.put(category, inclusion); + containsIncluded = categoryInclusions.containsValue(INCLUDED); + containsExcluded = categoryInclusions.containsValue(EXCLUDED); + } +} diff --git a/src/main/java/de/danielbechler/diff/Inclusion.java b/src/main/java/de/danielbechler/diff/inclusion/Inclusion.java similarity index 83% rename from src/main/java/de/danielbechler/diff/Inclusion.java rename to src/main/java/de/danielbechler/diff/inclusion/Inclusion.java index 8ecf9a13..07ea97b3 100644 --- a/src/main/java/de/danielbechler/diff/Inclusion.java +++ b/src/main/java/de/danielbechler/diff/inclusion/Inclusion.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,11 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.inclusion; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public enum Inclusion { + DEFAULT, INCLUDED, EXCLUDED } diff --git a/src/main/java/de/danielbechler/diff/inclusion/InclusionConfigurer.java b/src/main/java/de/danielbechler/diff/inclusion/InclusionConfigurer.java new file mode 100644 index 00000000..841bfdc3 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/InclusionConfigurer.java @@ -0,0 +1,108 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.path.NodePath; + +/** + * Allows to in- or exclude nodes based on property name, object type, category or location in the object graph. + * Excluded nodes will not be compared, to make sure their accessors won't get called. This is useful in cases where + * getters could throw exceptions under certain conditions or when certain accessors are expensive to call or simply + * not relevant for the use-case. + *

+ * In combination with categories this allows to define sub-sets of properties, in order to compare only relevant parts + * of an object (e.g. exclude all properties marked as _metadata_.) + * + * @author Daniel Bechler + */ +@SuppressWarnings("UnusedDeclaration") +public interface InclusionConfigurer +{ + /** + * Includes elements (and implicitly all their children) based on certain criteria, unless their parent element + * is excluded. + */ + ToInclude include(); + + ToExclude exclude(); + + /** + * Registers a custom {@link de.danielbechler.diff.inclusion.InclusionResolver}. Some objects may not be relevant + * or suitable for the comparison process. Using an {@link de.danielbechler.diff.inclusion.InclusionResolver} is a + * powerful and flexible way to detect and exclude those objects. + *

+ * Keep in mind that every single node in the object graph will be checked against each and every registered {@link + * de.danielbechler.diff.inclusion.InclusionResolver}. If performance is important to you, make sure that calling + * its methods is as cheap as possible. + */ + InclusionConfigurer resolveUsing(InclusionResolver resolver); + + ObjectDifferBuilder and(); + + public interface ToInclude + { + ToInclude category(String category); + + ToInclude type(Class type); + + ToInclude node(NodePath nodePath); + + ToInclude propertyName(String propertyName); + + /** + * Include one or more properties of the given type. This automatically causes all other properties of that + * type to be implicitly excluded. However, unlike other inclusion mechanisms, this doesn't exclude properties + * of other types. + * + * @param type The parent type for which the included properties should be specified. + * @param propertyNames One or more property names to include. + * @see de.danielbechler.diff.introspection.ObjectDiffProperty#inclusion() + */ + ToInclude propertyNameOfType(Class type, String... propertyNames); + + InclusionConfigurer also(); + + ObjectDifferBuilder and(); + } + + public interface ToExclude + { + ToExclude category(String category); + + ToExclude type(Class type); + + ToExclude node(NodePath nodePath); + + ToExclude propertyName(String property); + + /** + * Excludes one or more properties of the given type. + * + * @param type The parent type for which the excluded properties should be specified. + * @param propertyNames One or more property names to exclude. + * @see de.danielbechler.diff.introspection.ObjectDiffProperty#inclusion() + * @see de.danielbechler.diff.introspection.ObjectDiffProperty#excluded() + */ + ToExclude propertyNameOfType(Class type, String... propertyNames); + + InclusionConfigurer also(); + + ObjectDifferBuilder and(); + } + +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/InclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/InclusionResolver.java new file mode 100644 index 00000000..fe361113 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/InclusionResolver.java @@ -0,0 +1,55 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.node.DiffNode; + +/** + * This can be used to implement custom inclusion mechanisms. Some objects may not be relevant or suitable for the + * comparison process. Using an {@link de.danielbechler.diff.inclusion.InclusionResolver} is a powerful and flexible + * way to detect and exclude those objects. + *

+ * Keep in mind that every single node in the object graph will be checked against each and every registered {@link + * de.danielbechler.diff.inclusion.InclusionResolver}. If performance is important to you, make sure that calling its + * methods is as cheap as possible. + */ +public interface InclusionResolver +{ + /** + * Determines whether a given {@link de.danielbechler.diff.node.DiffNode} should be included into the comparison + * process. + * + * @param node The node to determine the inclusion for. Keep in mind that the {@link + * de.danielbechler.diff.node.DiffNode} doesn't contain any children at this point and + * albeit it is already linked to its parent node, the parent node also probably hasn't been fully + * processed yet. It is only safe to examine the node path and type related properties along the path + * up to the root node, but definitely not to make any decisions based on the state or number of child + * nodes. + * @return Returns either {@link de.danielbechler.diff.inclusion.Inclusion#INCLUDED} to indicate an explicit + * inclusion, {@link de.danielbechler.diff.inclusion.Inclusion#EXCLUDED} to inidicate an explicit exclusion or + * {@link de.danielbechler.diff.inclusion.Inclusion#DEFAULT} in case this resolver doesn't want to influence the + * decision. This method should never return null. + */ + Inclusion getInclusion(DiffNode node); + + /** + * When this method returns true, it causes the inclusion service to exclude all nodes that are not + * explicitly included via {@link de.danielbechler.diff.inclusion.Inclusion#INCLUDED}. Otherwise nodes with {@link + * de.danielbechler.diff.inclusion.Inclusion#DEFAULT} will also be included. + */ + boolean enablesStrictIncludeMode(); +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/InclusionService.java b/src/main/java/de/danielbechler/diff/inclusion/InclusionService.java new file mode 100644 index 00000000..186ce873 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/InclusionService.java @@ -0,0 +1,282 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.category.CategoryResolver; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.util.Assert; + +import java.util.Collection; +import java.util.LinkedList; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +@SuppressWarnings("OverlyComplexAnonymousInnerClass") +public class InclusionService implements InclusionConfigurer, IsIgnoredResolver +{ + private final ObjectDifferBuilder rootConfiguration; + private final CategoryResolver categoryResolver; + private final Collection inclusionResolvers = new LinkedList(); + private TypeInclusionResolver typeInclusionResolver; + private TypePropertyConfigInclusionResolver typePropertyConfigInclusionResolver; + private CategoryInclusionResolver categoryInclusionResolver; + private NodePathInclusionResolver nodePathInclusionResolver; + private PropertyNameInclusionResolver propertyNameInclusionResolver; + + public InclusionService(final CategoryResolver categoryResolver, final ObjectDifferBuilder rootConfiguration) + { + Assert.notNull(rootConfiguration, "rootConfiguration"); + Assert.notNull(categoryResolver, "categoryResolver"); + this.rootConfiguration = rootConfiguration; + this.categoryResolver = categoryResolver; + addAlwaysOnInclusionResolvers(); + } + + private void addAlwaysOnInclusionResolvers() + { + inclusionResolvers.add(new TypePropertyAnnotationInclusionResolver()); + } + + Collection getInclusionResolvers() + { + return inclusionResolvers; + } + + public boolean isIgnored(final DiffNode node) + { + if (node.isRootNode()) + { + return false; + } + boolean strictIncludeModeEnabled = false; + boolean isExplicitlyIncluded = false; + for (final InclusionResolver inclusionResolver : inclusionResolvers) + { + if (inclusionResolver.enablesStrictIncludeMode()) + { + strictIncludeModeEnabled = true; + } + switch (getInclusion(node, inclusionResolver)) + { + case EXCLUDED: + return true; + case INCLUDED: + isExplicitlyIncluded = true; + break; + } + } + if (strictIncludeModeEnabled && !isExplicitlyIncluded) + { + return true; + } + return false; + } + + private static Inclusion getInclusion(final DiffNode node, final InclusionResolver inclusionResolver) + { + final Inclusion inclusion = inclusionResolver.getInclusion(node); + return inclusion != null ? inclusion : DEFAULT; + } + + public ToInclude include() + { + return new ToInclude() + { + public ToInclude category(final String category) + { + setCategoryInclusion(INCLUDED, category); + return this; + } + + public ToInclude type(final Class type) + { + setTypeInclusion(INCLUDED, type); + return this; + } + + public ToInclude node(final NodePath nodePath) + { + setNodePathInclusion(INCLUDED, nodePath); + return this; + } + + public ToInclude propertyName(final String propertyName) + { + setPropertyNameInclusion(INCLUDED, propertyName); + return this; + } + + public ToInclude propertyNameOfType(final Class type, final String... propertyNames) + { + setPropertyNameOfTypeInclusion(INCLUDED, type, propertyNames); + return this; + } + + public InclusionConfigurer also() + { + return InclusionService.this; + } + + public ObjectDifferBuilder and() + { + return rootConfiguration; + } + }; + } + + void setCategoryInclusion(final Inclusion inclusion, final String category) + { + if (categoryInclusionResolver == null) + { + categoryInclusionResolver = newCategoryInclusionResolver(); + inclusionResolvers.add(categoryInclusionResolver); + } + categoryInclusionResolver.setInclusion(category, inclusion); + } + + void setTypeInclusion(final Inclusion inclusion, final Class type) + { + if (typeInclusionResolver == null) + { + typeInclusionResolver = newTypeInclusionResolver(); + inclusionResolvers.add(typeInclusionResolver); + } + typeInclusionResolver.setInclusion(type, inclusion); + } + + void setNodePathInclusion(final Inclusion inclusion, final NodePath nodePath) + { + if (nodePathInclusionResolver == null) + { + nodePathInclusionResolver = newNodePathInclusionResolver(); + inclusionResolvers.add(nodePathInclusionResolver); + } + nodePathInclusionResolver.setInclusion(nodePath, inclusion); + } + + void setPropertyNameInclusion(final Inclusion inclusion, final String propertyName) + { + if (propertyNameInclusionResolver == null) + { + propertyNameInclusionResolver = newPropertyNameInclusionResolver(); + inclusionResolvers.add(propertyNameInclusionResolver); + } + propertyNameInclusionResolver.setInclusion(propertyName, inclusion); + } + + private void setPropertyNameOfTypeInclusion(final Inclusion inclusion, final Class type, final String... propertyNames) + { + Assert.notNull(type, "type"); + for (final String propertyName : propertyNames) + { + Assert.hasText(propertyName, "propertyName in propertyNames"); + if (typePropertyConfigInclusionResolver == null) + { + typePropertyConfigInclusionResolver = newTypePropertyConfigInclusionResolver(); + inclusionResolvers.add(typePropertyConfigInclusionResolver); + } + typePropertyConfigInclusionResolver.setInclusion(type, propertyName, inclusion); + } + } + + CategoryInclusionResolver newCategoryInclusionResolver() + { + return new CategoryInclusionResolver(categoryResolver); + } + + TypeInclusionResolver newTypeInclusionResolver() + { + return new TypeInclusionResolver(); + } + + NodePathInclusionResolver newNodePathInclusionResolver() + { + return new NodePathInclusionResolver(); + } + + PropertyNameInclusionResolver newPropertyNameInclusionResolver() + { + return new PropertyNameInclusionResolver(); + } + + TypePropertyConfigInclusionResolver newTypePropertyConfigInclusionResolver() + { + return new TypePropertyConfigInclusionResolver(); + } + + public ToExclude exclude() + { + return new ToExclude() + { + public ToExclude category(final String category) + { + setCategoryInclusion(EXCLUDED, category); + return this; + } + + public ToExclude type(final Class type) + { + setTypeInclusion(EXCLUDED, type); + return this; + } + + public ToExclude node(final NodePath nodePath) + { + setNodePathInclusion(EXCLUDED, nodePath); + return this; + } + + public ToExclude propertyName(final String propertyName) + { + setPropertyNameInclusion(EXCLUDED, propertyName); + return this; + } + + public ToExclude propertyNameOfType(final Class type, final String... propertyNames) + { + setPropertyNameOfTypeInclusion(EXCLUDED, type, propertyNames); + return this; + } + + public InclusionConfigurer also() + { + return InclusionService.this; + } + + public ObjectDifferBuilder and() + { + return rootConfiguration; + } + }; + } + + public InclusionConfigurer resolveUsing(final InclusionResolver inclusionResolver) + { + Assert.notNull(inclusionResolver, "inclusionResolver"); + inclusionResolvers.add(inclusionResolver); + return this; + } + + public ObjectDifferBuilder and() + { + return rootConfiguration; + } +} \ No newline at end of file diff --git a/src/main/java/de/danielbechler/diff/IsIgnoredResolver.java b/src/main/java/de/danielbechler/diff/inclusion/IsIgnoredResolver.java similarity index 80% rename from src/main/java/de/danielbechler/diff/IsIgnoredResolver.java rename to src/main/java/de/danielbechler/diff/inclusion/IsIgnoredResolver.java index dc00dffa..71a8a3cb 100644 --- a/src/main/java/de/danielbechler/diff/IsIgnoredResolver.java +++ b/src/main/java/de/danielbechler/diff/inclusion/IsIgnoredResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,9 +14,13 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.inclusion; -/** @author Daniel Bechler */ +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ public interface IsIgnoredResolver { boolean isIgnored(DiffNode node); diff --git a/src/main/java/de/danielbechler/diff/inclusion/NodePathInclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/NodePathInclusionResolver.java new file mode 100644 index 00000000..930e7ac3 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/NodePathInclusionResolver.java @@ -0,0 +1,95 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +class NodePathInclusionResolver implements InclusionResolver +{ + private final ValueNode inclusions = new ValueNode(); + private boolean containsIncluded; + private boolean containsExcluded; + + public Inclusion getInclusion(final DiffNode node) + { + if (isInactive()) + { + return DEFAULT; + } + return resolveInclusion(inclusions.getNodeForPath(node.getPath())); + } + + public boolean enablesStrictIncludeMode() + { + return containsIncluded; + } + + private boolean isInactive() + { + return !containsIncluded && !containsExcluded; + } + + public void setInclusion(final NodePath nodePath, final Inclusion inclusion) + { + inclusions.getNodeForPath(nodePath).setValue(inclusion); + containsIncluded = inclusions.containsValue(INCLUDED); + containsExcluded = inclusions.containsValue(EXCLUDED); + } + + private Inclusion resolveInclusion(final ValueNode inclusionNode) + { + // When the node has been explicitly excluded it's clear what to do + if (inclusionNode.getValue() == EXCLUDED) + { + return EXCLUDED; + } + + // Since excluding a parent node wins over an explicit inclusion + // of the current node we need to check that first (in fact it shouldn't + // even be possible to get to this point, if the parent is excluded) + final Inclusion parentInclusion = resolveParentInclusion(inclusionNode); + if (parentInclusion == INCLUDED || parentInclusion == EXCLUDED) + { + return parentInclusion; + } + + // Only after the parent has been checked we can honor the explicit + // inclusion of this node by checking whether itself or any of its + // children is included + if (inclusionNode.containsValue(INCLUDED)) + { + return INCLUDED; + } + + return DEFAULT; + } + + private Inclusion resolveParentInclusion(final ValueNode inclusionNode) + { + final ValueNode parentWithInclusion = inclusionNode.getClosestParentWithValue(); + if (parentWithInclusion != null) + { + return resolveInclusion(parentWithInclusion); + } + return DEFAULT; + } +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/PropertyNameInclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/PropertyNameInclusionResolver.java new file mode 100644 index 00000000..c59bd82d --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/PropertyNameInclusionResolver.java @@ -0,0 +1,68 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.node.DiffNode; + +import java.util.HashMap; +import java.util.Map; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +class PropertyNameInclusionResolver implements InclusionResolver +{ + private final Map propertyNameInclusions = new HashMap(); + private boolean containsIncluded; + private boolean containsExcluded; + + public Inclusion getInclusion(final DiffNode node) + { + if (node != null && !isInactive()) + { + final Inclusion inclusion = propertyNameInclusions.get(node.getPropertyName()); + if (inclusion == INCLUDED || inclusion == EXCLUDED) + { + return inclusion; + } + final Inclusion parentNodeInclusion = getInclusion(node.getParentNode()); + if (parentNodeInclusion == INCLUDED) + { + return INCLUDED; + } + } + return DEFAULT; + } + + public boolean enablesStrictIncludeMode() + { + return containsIncluded; + } + + private boolean isInactive() + { + return !containsIncluded && !containsExcluded; + } + + public void setInclusion(final String propertyName, final Inclusion inclusion) + { + propertyNameInclusions.put(propertyName, inclusion); + containsIncluded = propertyNameInclusions.containsValue(INCLUDED); + containsExcluded = propertyNameInclusions.containsValue(EXCLUDED); + } +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/TypeInclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/TypeInclusionResolver.java new file mode 100644 index 00000000..11549e17 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/TypeInclusionResolver.java @@ -0,0 +1,68 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.node.DiffNode; + +import java.util.HashMap; +import java.util.Map; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +class TypeInclusionResolver implements InclusionResolver +{ + private final Map, Inclusion> typeInclusions = new HashMap, Inclusion>(); + private boolean containsIncluded; + private boolean containsExcluded; + + public Inclusion getInclusion(final DiffNode node) + { + if (isInactive()) + { + return DEFAULT; + } + final Class valueType = node.getValueType(); + if (valueType != null && typeInclusions.containsKey(valueType)) + { + final Inclusion inclusion = typeInclusions.get(valueType); + if (inclusion == Inclusion.INCLUDED || inclusion == Inclusion.EXCLUDED) + { + return inclusion; + } + } + return Inclusion.DEFAULT; + } + + private boolean isInactive() + { + return !(containsIncluded || containsExcluded); + } + + public boolean enablesStrictIncludeMode() + { + return containsIncluded; + } + + void setInclusion(final Class type, final Inclusion inclusion) + { + typeInclusions.put(type, inclusion); + containsIncluded = typeInclusions.containsValue(INCLUDED); + containsExcluded = typeInclusions.containsValue(EXCLUDED); + } +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/TypePropertyAnnotationInclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/TypePropertyAnnotationInclusionResolver.java new file mode 100644 index 00000000..2f4d5b89 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/TypePropertyAnnotationInclusionResolver.java @@ -0,0 +1,86 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.diff.instantiation.TypeInfo; +import de.danielbechler.diff.introspection.ObjectDiffProperty; +import de.danielbechler.diff.node.DiffNode; + +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 static java.util.Collections.emptyList; + +/** + * Created by Daniel Bechler. + */ +class TypePropertyAnnotationInclusionResolver implements InclusionResolver +{ + public boolean enablesStrictIncludeMode() + { + return false; + } + + public Inclusion getInclusion(final DiffNode node) + { + final ObjectDiffProperty propertyAnnotation = node.getPropertyAnnotation(ObjectDiffProperty.class); + if (propertyAnnotation != null) + { + if (propertyAnnotation.inclusion() == INCLUDED || propertyAnnotation.inclusion() == EXCLUDED) + { + return propertyAnnotation.inclusion(); + } + else if (propertyAnnotation.excluded()) + { + return EXCLUDED; + } + } + else if (hasIncludedSibling(node)) + { + return EXCLUDED; + } + return DEFAULT; + } + + private static boolean hasIncludedSibling(final DiffNode node) + { + for (final PropertyAwareAccessor accessor : getSiblingAccessors(node)) + { + final ObjectDiffProperty annotation = accessor.getReadMethodAnnotation(ObjectDiffProperty.class); + if (annotation != null && annotation.inclusion() == INCLUDED) + { + return true; + } + } + return false; + } + + private static Iterable getSiblingAccessors(final DiffNode node) + { + final DiffNode parentNode = node.getParentNode(); + if (parentNode != null) + { + final TypeInfo typeInfo = parentNode.getValueTypeInfo(); + if (typeInfo != null) + { + return typeInfo.getAccessors(); + } + } + return emptyList(); + } +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/TypePropertyConfigInclusionResolver.java b/src/main/java/de/danielbechler/diff/inclusion/TypePropertyConfigInclusionResolver.java new file mode 100644 index 00000000..1599af4d --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/TypePropertyConfigInclusionResolver.java @@ -0,0 +1,143 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.util.Assert; + +import java.util.HashMap; +import java.util.Map; + +import static de.danielbechler.diff.inclusion.Inclusion.DEFAULT; +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED; +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED; + +/** + * Created by Daniel Bechler. + */ +class TypePropertyConfigInclusionResolver implements InclusionResolver +{ + private final Map inclusions = new HashMap(); + + public Inclusion getInclusion(final DiffNode node) + { + if (isQualified(node)) + { + final PropertyId propertyKey = new PropertyId(node.getParentNode().getValueType(), node.getPropertyName()); + final Inclusion inclusion = inclusions.get(propertyKey); + if (inclusion != null && inclusion != DEFAULT) + { + return inclusion; + } + else + { + if (hasIncludedSiblings(node)) + { + return EXCLUDED; + } + } + } + return DEFAULT; + } + + public boolean enablesStrictIncludeMode() + { + return false; + } + + 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; + } + + private boolean hasIncludedSiblings(final DiffNode node) + { + for (final Map.Entry entry : inclusions.entrySet()) + { + if (entry.getKey().type == node.getParentNode().getValueType() && entry.getValue() == INCLUDED) + { + return true; + } + } + return false; + } + + public void setInclusion(final Class type, final String property, final Inclusion inclusion) + { + inclusions.put(new PropertyId(type, property), inclusion); + } + + 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; + } + } +} diff --git a/src/main/java/de/danielbechler/diff/inclusion/ValueNode.java b/src/main/java/de/danielbechler/diff/inclusion/ValueNode.java new file mode 100644 index 00000000..0c61b4b6 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/inclusion/ValueNode.java @@ -0,0 +1,159 @@ +/* + * 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.inclusion; + +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.diff.selector.RootElementSelector; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * INTERNAL CLASS. DON'T USE UNLESS YOU ARE READY TO DEAL WITH API CHANGES + */ +public class ValueNode +{ + protected final Map> children = new HashMap>(); + protected final ValueNode parent; + protected final ElementSelector elementSelector; + protected V value; + + public ValueNode() + { + this(RootElementSelector.getInstance(), null); + } + + protected ValueNode(final ElementSelector elementSelector, final ValueNode parent) + { + this.elementSelector = elementSelector; + this.parent = parent; + } + + public ElementSelector getElementSelector() + { + return elementSelector; + } + + public ValueNode getParent() + { + return parent; + } + + public ValueNode getNodeForPath(final NodePath nodePath) + { + if (parent == null) + { + final List elementSelectors = nodePath.getElementSelectors(); + if (elementSelectors.size() != 1) + { + return getChild(elementSelectors.subList(1, elementSelectors.size())); + } + return this; + } + return parent.getNodeForPath(nodePath); + } + + public ValueNode getChild(final ElementSelector childSelector) + { + if (childSelector == RootElementSelector.getInstance()) + { + throw new IllegalArgumentException("A child node can never be the root"); + } + if (children.containsKey(childSelector)) + { + return children.get(childSelector); + } + else + { + final ValueNode childNode = newNode(childSelector); + children.put(childSelector, childNode); + return childNode; + } + } + + protected ValueNode newNode(final ElementSelector childSelector) + { + return new ValueNode(childSelector, this); + } + + private ValueNode getChild(final List childSelectors) + { + if (childSelectors.size() == 1) + { + return getChild(childSelectors.get(0)); + } + else + { + final ValueNode child = getChild(childSelectors.get(0)); + return child.getChild(childSelectors.subList(1, childSelectors.size())); + } + } + + public boolean hasChild(final ElementSelector childSelector) + { + return children.get(childSelector) != null; + } + + public ValueNode getClosestParentWithValue() + { + if (parent != null) + { + if (parent.hasValue()) + { + return parent; + } + else + { + return parent.getClosestParentWithValue(); + } + } + return null; + } + + public boolean hasValue() + { + return value != null; + } + + public boolean containsValue(final V value) + { + if (this.value == value) + { + return true; + } + for (final ValueNode child : children.values()) + { + if (child.containsValue(value)) + { + return true; + } + } + return false; + } + + public V getValue() + { + return value; + } + + public void setValue(final V value) + { + this.value = value; + } +} diff --git a/src/main/java/de/danielbechler/diff/instantiation/InstanceFactory.java b/src/main/java/de/danielbechler/diff/instantiation/InstanceFactory.java new file mode 100644 index 00000000..a33fda3e --- /dev/null +++ b/src/main/java/de/danielbechler/diff/instantiation/InstanceFactory.java @@ -0,0 +1,41 @@ +/* + * 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.instantiation; + +/** + * Serves as factory for objects. It is mainly used when {@link de.danielbechler.diff.node.DiffNode#canonicalSet(Object, + * Object)} is called on a node for which the target returns null for its parent. The parent (and + * subsequently all of its predecessors along the path) will be created via this factory and inserted into the target + * object. + */ +public interface InstanceFactory +{ + /** + * @param type The type for which a new instance should be created + * @return A new instance of the given type or null if it doesn't know how to instantiate + * the type. In case of the latter, the {@link de.danielbechler.diff.ObjectDiffer} will automatically fallback to + * instantiation via public non-arg constructor. If that also fails, an {@link TypeInstantiationException} + * will be thrown. + *

+ * Note from the author: it wasn't an easy decision, but in the end I favored an exception over + * logging a warning, because this way it is much harder to accidentally end up with incomplete merges without + * noticing. If this turns out to be a problem for you, please let me know in the issue tracker. We could probably + * add a flag to switch to the logging-only mode. But as long as nobody complains, I'll just leave it as it is right + * now. + */ + Object newInstanceOfType(Class type); +} diff --git a/src/main/java/de/danielbechler/diff/instantiation/PublicNoArgsConstructorInstanceFactory.java b/src/main/java/de/danielbechler/diff/instantiation/PublicNoArgsConstructorInstanceFactory.java new file mode 100644 index 00000000..d210bd43 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/instantiation/PublicNoArgsConstructorInstanceFactory.java @@ -0,0 +1,38 @@ +/* + * 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.instantiation; + +public class PublicNoArgsConstructorInstanceFactory implements InstanceFactory +{ + public Object newInstanceOfType(final Class type) + { + try + { + return type.getConstructor().newInstance(); + } + catch (final NoSuchMethodException e) + { + throw new TypeInstantiationException(type, + "Type doesn't have a public non-arg constructur", e); + } + catch (final Exception e) + { + throw new TypeInstantiationException(type, + "Attempt to access the public no-args constructor caused an exception", e); + } + } +} diff --git a/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java b/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java new file mode 100644 index 00000000..4ffdce72 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/instantiation/TypeInfo.java @@ -0,0 +1,70 @@ +/* + * 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.instantiation; + +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.util.Assert; + +import java.util.Collection; +import java.util.LinkedList; + +/** + * Holds type information gathered during introspection and serves as factory for new instances of that type. + */ +public class TypeInfo +{ + private final Class type; + private final Collection accessors = new LinkedList(); + private InstanceFactory instanceFactory; + + public TypeInfo(final Class type) + { + Assert.notNull(type, "type"); + this.type = type; + } + + public void addPropertyAccessor(final PropertyAwareAccessor propertyAccessor) + { + if (propertyAccessor != null) + { + accessors.add(propertyAccessor); + } + } + + public Class getType() + { + return type; + } + + /** + * Creates a new instance of the represented type using the underlying {@link de.danielbechler.diff.instantiation.InstanceFactory}. + */ + public Object newInstance() + { + return instanceFactory.newInstanceOfType(type); + } + + public Collection getAccessors() + { + return accessors; + } + + public void setInstanceFactory(final InstanceFactory instanceFactory) + { + this.instanceFactory = instanceFactory; + } +} diff --git a/src/main/java/de/danielbechler/diff/instantiation/TypeInstantiationException.java b/src/main/java/de/danielbechler/diff/instantiation/TypeInstantiationException.java new file mode 100644 index 00000000..85ae2ce9 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/instantiation/TypeInstantiationException.java @@ -0,0 +1,41 @@ +/* + * 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.instantiation; + +public class TypeInstantiationException extends RuntimeException +{ + private static final long serialVersionUID = 2794794949615814237L; + private final Class type; + private final String reason; + + public TypeInstantiationException(final Class type, final String reason, final Throwable cause) + { + super("Failed to create instance of type '" + type + "'. Reason: " + reason, cause); + this.type = type; + this.reason = reason; + } + + public Class getType() + { + return type; + } + + public String getReason() + { + return reason; + } +} diff --git a/src/main/java/de/danielbechler/diff/introspection/DefaultPropertyAccessExceptionHandler.java b/src/main/java/de/danielbechler/diff/introspection/DefaultPropertyAccessExceptionHandler.java new file mode 100644 index 00000000..a98b2172 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/DefaultPropertyAccessExceptionHandler.java @@ -0,0 +1,27 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.node.DiffNode; + +public class DefaultPropertyAccessExceptionHandler implements PropertyAccessExceptionHandler +{ + public void onPropertyReadException(final PropertyReadException exception, final DiffNode node) + { + throw exception; + } +} diff --git a/src/main/java/de/danielbechler/diff/introspection/InstanceFactoryFallbackDecorator.java b/src/main/java/de/danielbechler/diff/introspection/InstanceFactoryFallbackDecorator.java new file mode 100644 index 00000000..573576bb --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/InstanceFactoryFallbackDecorator.java @@ -0,0 +1,46 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.instantiation.InstanceFactory; +import de.danielbechler.diff.instantiation.PublicNoArgsConstructorInstanceFactory; +import de.danielbechler.util.Assert; + +class InstanceFactoryFallbackDecorator implements InstanceFactory +{ + private final InstanceFactory fallbackInstanceFactory = new PublicNoArgsConstructorInstanceFactory(); + private final InstanceFactory instanceFactory; + + InstanceFactoryFallbackDecorator(final InstanceFactory instanceFactory) + { + Assert.notNull(instanceFactory, "instanceFactory"); + this.instanceFactory = instanceFactory; + } + + public Object newInstanceOfType(final Class type) + { + final Object instance = instanceFactory.newInstanceOfType(type); + if (instance != null) + { + return instance; + } + else + { + return fallbackInstanceFactory.newInstanceOfType(type); + } + } +} diff --git a/src/main/java/de/danielbechler/diff/introspection/IntrospectionConfigurer.java b/src/main/java/de/danielbechler/diff/introspection/IntrospectionConfigurer.java new file mode 100644 index 00000000..f4d5b7b7 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/IntrospectionConfigurer.java @@ -0,0 +1,69 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.instantiation.InstanceFactory; +import de.danielbechler.diff.path.NodePath; + +/** + * Allows to replace the default bean introspector with a custom implementation. The default introspector internally + * uses the `java.beans.Introspector` which has some limitations. The most important one being that it only operates on + * getters and setters. In case field introspection is needed a custom introspector must be used. An introspector can + * be + * set as global default or on a per-property basis. It is also possible to turn off introspection for specific + * properties in which case they will simply be compared via `equals` method. + * + * @author Daniel Bechler + */ +public interface IntrospectionConfigurer +{ + /** + * When assigning new values via {@link de.danielbechler.diff.node.DiffNode} (e.g. during merging) it will + * implicitly create missing instances of its parent objects along the path to the root object. By default those + * instances will be created via public non-arg constructor. If that fails a {@link + * de.danielbechler.diff.instantiation.TypeInstantiationException} will be thrown. + *

+ * To add support for types that need to be instantiated differently you can overide the default behavior via + * custom {@link de.danielbechler.diff.instantiation.InstanceFactory}. When doing so, don't worry about types + * actually that are suitable for the default behavior, as it will automatically kick in, whenever the custom + * factroy returns {@code null}. + * + * @param instanceFactory A custom instance factory + * @throws java.lang.IllegalArgumentException when the instanceFactory is null + */ + IntrospectionConfigurer setInstanceFactory(InstanceFactory instanceFactory); + + IntrospectionConfigurer setDefaultIntrospector(Introspector introspector); + + IntrospectionConfigurer handlePropertyAccessExceptionsUsing(PropertyAccessExceptionHandler exceptionHandler); + + Of ofType(Class type); + + Of ofNode(NodePath path); + + ObjectDifferBuilder and(); + + public static interface Of + { + IntrospectionConfigurer toUse(Introspector introspector); + + IntrospectionConfigurer toBeEnabled(); + + IntrospectionConfigurer toBeDisabled(); + } +} diff --git a/src/main/java/de/danielbechler/diff/introspection/IntrospectionService.java b/src/main/java/de/danielbechler/diff/introspection/IntrospectionService.java new file mode 100644 index 00000000..b8de35dd --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/IntrospectionService.java @@ -0,0 +1,195 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.ObjectDifferBuilder; +import de.danielbechler.diff.instantiation.InstanceFactory; +import de.danielbechler.diff.instantiation.PublicNoArgsConstructorInstanceFactory; +import de.danielbechler.diff.instantiation.TypeInfo; +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.diff.path.NodePathValueHolder; +import de.danielbechler.util.Assert; +import de.danielbechler.util.Classes; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Daniel Bechler + */ +public class IntrospectionService implements IntrospectionConfigurer, IsIntrospectableResolver, TypeInfoResolver, PropertyAccessExceptionHandlerResolver +{ + private final Map, Introspector> typeIntrospectorMap = new HashMap, Introspector>(); + private final Map, IntrospectionMode> typeIntrospectionModeMap = new HashMap, IntrospectionMode>(); + private final NodePathValueHolder nodePathIntrospectorHolder = new NodePathValueHolder(); + private final NodePathValueHolder nodePathIntrospectionModeHolder = new NodePathValueHolder(); + private final ObjectDifferBuilder objectDifferBuilder; + private Introspector defaultIntrospector; + private InstanceFactory instanceFactory = new PublicNoArgsConstructorInstanceFactory(); + private PropertyAccessExceptionHandler defaultPropertyAccessExceptionHandler = new DefaultPropertyAccessExceptionHandler(); + + public IntrospectionService(final ObjectDifferBuilder objectDifferBuilder) + { + this.objectDifferBuilder = objectDifferBuilder; + } + + public boolean isIntrospectable(final DiffNode node) + { + final Class nodeType = node.getValueType(); + if (nodeType == null) + { + return false; + } + else if (isPrimitiveTypeEnumOrArray(nodeType)) + { + return false; + } + else if (nodePathIntrospectionModeHolder.valueForNodePath(node.getPath()) == IntrospectionMode.DISABLED) + { + return false; + } + else if (typeIntrospectionModeMap.get(nodeType) == IntrospectionMode.DISABLED) + { + return false; + } + return true; + } + + private static boolean isPrimitiveTypeEnumOrArray(final Class nodeType) + { + return Classes.isPrimitiveType(nodeType) + || Classes.isPrimitiveWrapperType(nodeType) + || nodeType.isEnum() + || nodeType.isArray(); + } + + public PropertyAccessExceptionHandler resolvePropertyAccessExceptionHandler(final Class parentType, final String propertyName) + { + return defaultPropertyAccessExceptionHandler; + } + + public TypeInfo typeInfoForNode(final DiffNode node) + { + final Class beanType = node.getValueType(); + final Introspector introspector = introspectorForNode(node); + final TypeInfo typeInfo = introspector.introspect(beanType); + typeInfo.setInstanceFactory(instanceFactory); + return typeInfo; + } + + public Introspector introspectorForNode(final DiffNode node) + { + final Introspector typeIntrospector = typeIntrospectorMap.get(node.getValueType()); + if (typeIntrospector != null) + { + return typeIntrospector; + } + + final Introspector nodePathIntrospector = nodePathIntrospectorHolder.valueForNodePath(node.getPath()); + if (nodePathIntrospector != null) + { + return nodePathIntrospector; + } + + if (defaultIntrospector == null) + { + defaultIntrospector = new StandardIntrospector(); + } + return defaultIntrospector; + } + + public IntrospectionConfigurer setInstanceFactory(final InstanceFactory instanceFactory) + { + Assert.notNull(instanceFactory, "instanceFactory"); + this.instanceFactory = new InstanceFactoryFallbackDecorator(instanceFactory); + return this; + } + + public IntrospectionConfigurer setDefaultIntrospector(final Introspector introspector) + { + Assert.notNull(introspector, "The default introspector must not be null"); + defaultIntrospector = introspector; + return this; + } + + public IntrospectionConfigurer handlePropertyAccessExceptionsUsing(final PropertyAccessExceptionHandler exceptionHandler) + { + Assert.notNull(exceptionHandler, "exceptionHandler"); + defaultPropertyAccessExceptionHandler = exceptionHandler; + return this; + } + + public Of ofType(final Class type) + { + return new Of() + { + public IntrospectionConfigurer toUse(final Introspector introspector) + { + typeIntrospectorMap.put(type, introspector); + return IntrospectionService.this; + } + + public IntrospectionConfigurer toBeEnabled() + { + typeIntrospectionModeMap.put(type, IntrospectionMode.ENABLED); + return IntrospectionService.this; + } + + public IntrospectionConfigurer toBeDisabled() + { + typeIntrospectionModeMap.put(type, IntrospectionMode.DISABLED); + return IntrospectionService.this; + } + }; + } + + public Of ofNode(final NodePath path) + { + return new Of() + { + public IntrospectionConfigurer toUse(final Introspector introspector) + { + nodePathIntrospectorHolder.put(path, introspector); + return IntrospectionService.this; + } + + public IntrospectionConfigurer toBeEnabled() + { + nodePathIntrospectionModeHolder.put(path, IntrospectionMode.ENABLED); + return IntrospectionService.this; + } + + public IntrospectionConfigurer toBeDisabled() + { + nodePathIntrospectionModeHolder.put(path, IntrospectionMode.DISABLED); + return IntrospectionService.this; + } + }; + } + + public ObjectDifferBuilder and() + { + return objectDifferBuilder; + } + + public enum IntrospectionMode + { + ENABLED, + DISABLED + } +} diff --git a/src/main/java/de/danielbechler/diff/Introspector.java b/src/main/java/de/danielbechler/diff/introspection/Introspector.java similarity index 69% rename from src/main/java/de/danielbechler/diff/Introspector.java rename to src/main/java/de/danielbechler/diff/introspection/Introspector.java index c1e9bcf8..b957f1ce 100644 --- a/src/main/java/de/danielbechler/diff/Introspector.java +++ b/src/main/java/de/danielbechler/diff/introspection/Introspector.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,7 +14,9 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.introspection; + +import de.danielbechler.diff.instantiation.TypeInfo; /** * Resolves the accessors of a given type. @@ -24,11 +26,10 @@ public interface Introspector { /** - * Resolves the accessors of the given type. + * Gathers information about a given type. * * @param type The type to introspect. - * - * @return All valid accessors. + * @return A summary of the given type containing all information needed to handle it's properties. */ - Iterable introspect(Class type); + TypeInfo introspect(Class type); } diff --git a/src/main/java/de/danielbechler/diff/IsIntrospectableResolver.java b/src/main/java/de/danielbechler/diff/introspection/IsIntrospectableResolver.java similarity index 81% rename from src/main/java/de/danielbechler/diff/IsIntrospectableResolver.java rename to src/main/java/de/danielbechler/diff/introspection/IsIntrospectableResolver.java index 8d23cc7c..cb9d3b98 100644 --- a/src/main/java/de/danielbechler/diff/IsIntrospectableResolver.java +++ b/src/main/java/de/danielbechler/diff/introspection/IsIntrospectableResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 Daniel Bechler + * 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. @@ -14,14 +14,18 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.introspection; -/** @author Daniel Bechler */ +import de.danielbechler.diff.node.DiffNode; + +/** + * @author Daniel Bechler + */ public interface IsIntrospectableResolver { /** * @return Returns true if the object represented by the given node should be compared via - * introspection. + * introspection. */ boolean isIntrospectable(DiffNode node); } diff --git a/src/main/java/de/danielbechler/diff/annotation/ObjectDiffEqualsOnlyType.java b/src/main/java/de/danielbechler/diff/introspection/ObjectDiffEqualsOnlyType.java similarity index 78% rename from src/main/java/de/danielbechler/diff/annotation/ObjectDiffEqualsOnlyType.java rename to src/main/java/de/danielbechler/diff/introspection/ObjectDiffEqualsOnlyType.java index 62d8182c..c5a21a72 100644 --- a/src/main/java/de/danielbechler/diff/annotation/ObjectDiffEqualsOnlyType.java +++ b/src/main/java/de/danielbechler/diff/introspection/ObjectDiffEqualsOnlyType.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,15 +14,20 @@ * limitations under the License. */ -package de.danielbechler.diff.annotation; +package de.danielbechler.diff.introspection; -import java.lang.annotation.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited -@ObjectDiffAnnotation public @interface ObjectDiffEqualsOnlyType { /** diff --git a/src/main/java/de/danielbechler/diff/annotation/ObjectDiffProperty.java b/src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java similarity index 67% rename from src/main/java/de/danielbechler/diff/annotation/ObjectDiffProperty.java rename to src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java index 69c1341e..63f99c6c 100644 --- a/src/main/java/de/danielbechler/diff/annotation/ObjectDiffProperty.java +++ b/src/main/java/de/danielbechler/diff/introspection/ObjectDiffProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,9 +14,15 @@ * limitations under the License. */ -package de.danielbechler.diff.annotation; +package de.danielbechler.diff.introspection; -import java.lang.annotation.*; +import de.danielbechler.diff.inclusion.Inclusion; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Annotation to be used on property getters in order to configure if and how they should be treated during @@ -27,18 +33,22 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Inherited -@ObjectDiffAnnotation public @interface ObjectDiffProperty { /** - * Causes the {@link de.danielbechler.diff.Differ Differs} to skip the marked property and all its children. + * Causes the {@link de.danielbechler.diff.differ.Differ Differs} to skip the marked property and all its children. * * @return true if the property should be ignored. + * @deprecated Please use {@linkplain #inclusion()} instead. When used in conjunction with {@linkplain + * #inclusion()}, the latter one will win over {@linkplain #excluded()}. */ + @Deprecated public boolean excluded() default false; + public Inclusion inclusion() default Inclusion.DEFAULT; + /** - * Causes the {@link de.danielbechler.diff.Differ Differs} to compare the object by using the {@link + * Causes the {@link de.danielbechler.diff.differ.Differ Differs} to compare the object by using the {@link * Object#equals(Object)} method instead of introspection. * * @return true if the property should be compared via {@link Object#equals(Object)}. diff --git a/src/main/java/de/danielbechler/diff/bean/BeanPropertyException.java b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessException.java similarity index 61% rename from src/main/java/de/danielbechler/diff/bean/BeanPropertyException.java rename to src/main/java/de/danielbechler/diff/introspection/PropertyAccessException.java index 6b9e6bb4..9e3388ad 100644 --- a/src/main/java/de/danielbechler/diff/bean/BeanPropertyException.java +++ b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,25 +14,23 @@ * limitations under the License. */ -package de.danielbechler.diff.bean; +package de.danielbechler.diff.introspection; -/** @author Daniel Bechler */ -public class BeanPropertyException extends RuntimeException +/** + * @author Daniel Bechler + */ +public abstract class PropertyAccessException extends RuntimeException { private static final long serialVersionUID = 1L; - private String propertyName; - private Class targetType; + private final String propertyName; + private final Class targetType; - public BeanPropertyException(final Throwable cause) + protected PropertyAccessException(final String propertyName, final Class targetType, final Throwable cause) { super(cause); - } - - @Override - public String getMessage() - { - return String.format("Property '%s' on target of type %s.", propertyName, targetType); + this.propertyName = propertyName; + this.targetType = targetType; } public String getPropertyName() @@ -40,18 +38,8 @@ public String getPropertyName() return propertyName; } - public void setPropertyName(final String propertyName) - { - this.propertyName = propertyName; - } - public Class getTargetType() { return targetType; } - - public void setTargetType(final Class targetType) - { - this.targetType = targetType; - } } diff --git a/src/main/java/de/danielbechler/diff/introspection/PropertyAccessExceptionHandler.java b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessExceptionHandler.java new file mode 100644 index 00000000..510ea11f --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessExceptionHandler.java @@ -0,0 +1,30 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.node.DiffNode; + +public interface PropertyAccessExceptionHandler +{ + /** + * Will be called whenever the value of a property could not be retrieved due to an underlying exception. + * + * @param exception This exception object contains more details about the property. + * @param node The {@link DiffNode} for which the exception occured. + */ + void onPropertyReadException(PropertyReadException exception, DiffNode node); +} diff --git a/src/main/java/de/danielbechler/diff/introspection/PropertyAccessExceptionHandlerResolver.java b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessExceptionHandlerResolver.java new file mode 100644 index 00000000..22ea0fad --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessExceptionHandlerResolver.java @@ -0,0 +1,26 @@ +/* + * 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.introspection; + +public interface PropertyAccessExceptionHandlerResolver +{ + /** + * @param parentType The type of the object to which the property belongs. + * @param propertyName The name of the property. + */ + PropertyAccessExceptionHandler resolvePropertyAccessExceptionHandler(Class parentType, String propertyName); +} diff --git a/src/main/java/de/danielbechler/diff/introspection/PropertyAccessor.java b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessor.java new file mode 100644 index 00000000..c2d07122 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/PropertyAccessor.java @@ -0,0 +1,294 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.diff.selector.BeanPropertyElementSelector; +import de.danielbechler.util.Assert; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import static java.util.Arrays.asList; + +public class PropertyAccessor implements PropertyAwareAccessor +{ + private static final Logger logger = LoggerFactory.getLogger(PropertyAccessor.class); + + private final String propertyName; + private final Class type; + private final Method readMethod; + private final Method writeMethod; + + public PropertyAccessor(final String propertyName, final Method readMethod, final Method writeMethod) + { + Assert.notNull(propertyName, "propertyName"); + Assert.notNull(readMethod, "readMethod"); + this.propertyName = propertyName; + this.readMethod = makeAccessible(readMethod); + this.writeMethod = makeAccessible(writeMethod); + this.type = this.readMethod.getReturnType(); + } + + private static Method makeAccessible(final Method method) + { + if (method != null && !method.isAccessible()) + { + logger.debug("Making method accessible: {}", method.toString()); + method.setAccessible(true); + } + return method; + } + + public final Set getCategoriesFromAnnotation() + { + final ObjectDiffProperty annotation = readMethod.getAnnotation(ObjectDiffProperty.class); + if (annotation != null) + { + return new TreeSet(asList(annotation.categories())); + } + return Collections.emptySet(); + } + + public boolean isExcludedByAnnotation() + { + final ObjectDiffProperty annotation = readMethod.getAnnotation(ObjectDiffProperty.class); + return annotation != null && annotation.excluded(); + } + + public String getPropertyName() + { + return this.propertyName; + } + + /** + * Private function to allow looking for the field recursively up the superclasses. + * + * @param clazz + * @return + */ + private Set getFieldAnnotations(final Class clazz) + { + try + { + return new LinkedHashSet(asList(clazz.getDeclaredField(propertyName).getAnnotations())); + } + catch (final NoSuchFieldException e) + { + if (clazz.getSuperclass() != null) + { + return getFieldAnnotations(clazz.getSuperclass()); + } + else + { + logger.debug("Cannot find propertyName: {}, declaring class: {}", propertyName, clazz); + return new LinkedHashSet(0); + } + } + } + + /** + * @return The annotations of the field, or an empty set if there is no field with the name derived from the getter. + */ + public Set getFieldAnnotations() + { + return getFieldAnnotations(readMethod.getDeclaringClass()); + } + + /** + * @return The given annotation of the field, or null if not annotated or if there is no field with the name derived + * from the getter. + */ + public T getFieldAnnotation(final Class annotationClass) + { + final Set annotations = getFieldAnnotations(); + assert (annotations != null) : "Something is wrong here. " + + "The contract of getReadAnnotations() guarantees a non-null return value."; + for (final Annotation annotation : annotations) + { + if (annotationClass.isAssignableFrom(annotation.annotationType())) + { + return annotationClass.cast(annotation); + } + } + return null; + } + + /** + * @return The annotations of the getter used to access this property. + */ + public Set getReadMethodAnnotations() + { + return new LinkedHashSet(asList(readMethod.getAnnotations())); + } + + public T getReadMethodAnnotation(final Class annotationClass) + { + final Set annotations = getReadMethodAnnotations(); + assert (annotations != null) : "Something is wrong here. " + + "The contract of getReadAnnotations() guarantees a non-null return value."; + for (final Annotation annotation : annotations) + { + if (annotationClass.isAssignableFrom(annotation.annotationType())) + { + return annotationClass.cast(annotation); + } + } + return null; + } + + public BeanPropertyElementSelector getElementSelector() + { + return new BeanPropertyElementSelector(this.propertyName); + } + + public Object get(final Object target) + { + if (target == null) + { + return null; + } + try + { + return readMethod.invoke(target); + } + catch (final Exception cause) + { + throw new PropertyReadException(propertyName, target.getClass(), cause); + } + } + + public void set(final Object target, final Object value) + { + if (target == null) + { + logger.info("Couldn't set new value '{}' for property '{}' " + + "because the target object is null", value, propertyName); + } + else if (writeMethod == null) + { + logger.debug("No setter found for property '{}'", propertyName); + tryToReplaceContentOfCollectionTypes(target, value); + } + else + { + invokeWriteMethod(target, value); + } + } + + public void unset(final Object target) + { + set(target, null); + } + + @SuppressWarnings("unchecked") + private void tryToReplaceContentOfCollectionTypes(final Object target, final Object value) + { + if (Collection.class.isAssignableFrom(readMethod.getReturnType())) + { + if (tryToReplaceCollectionContent((Collection) get(target), (Collection) value)) + { + return; + } + } + if (Map.class.isAssignableFrom(readMethod.getReturnType())) + { + if (tryToReplaceMapContent((Map) get(target), (Map) value)) + { + return; + } + } + logger.info("Couldn't set new value '{}' for property '{}'", value, propertyName); + } + + private void invokeWriteMethod(final Object target, final Object value) + { + try + { + writeMethod.invoke(target, value); + } + catch (final Exception cause) + { + throw new PropertyWriteException(propertyName, target.getClass(), value, cause); + } + } + + private static boolean tryToReplaceCollectionContent(final Collection target, + final Collection value) + { + if (target == null) + { + return false; + } + try + { + target.clear(); + target.addAll(value); + return true; + } + catch (final Exception unmodifiable) + { + logger.debug("Failed to replace content of existing Collection", unmodifiable); + return false; + } + } + + private static boolean tryToReplaceMapContent(final Map target, + final Map value) + { + if (target == null) + { + return false; + } + try + { + target.clear(); + target.putAll(value); + return true; + } + catch (final Exception unmodifiable) + { + logger.debug("Failed to replace content of existing Map", unmodifiable); + return false; + } + } + + public Class getType() + { + return this.type; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder("PropertyAccessor{"); + sb.append("propertyName='").append(propertyName).append('\''); + sb.append(", type=").append(type.getCanonicalName()); + sb.append(", source=").append(readMethod.getDeclaringClass().getCanonicalName()); + sb.append(", hasWriteMethod=").append(writeMethod != null); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/de/danielbechler/diff/bean/BeanPropertyWriteException.java b/src/main/java/de/danielbechler/diff/introspection/PropertyReadException.java similarity index 59% rename from src/main/java/de/danielbechler/diff/bean/BeanPropertyWriteException.java rename to src/main/java/de/danielbechler/diff/introspection/PropertyReadException.java index 0d4c40cf..7b956935 100644 --- a/src/main/java/de/danielbechler/diff/bean/BeanPropertyWriteException.java +++ b/src/main/java/de/danielbechler/diff/introspection/PropertyReadException.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,28 +14,23 @@ * limitations under the License. */ -package de.danielbechler.diff.bean; +package de.danielbechler.diff.introspection; -/** @author Daniel Bechler */ -public class BeanPropertyWriteException extends BeanPropertyException +/** + * @author Daniel Bechler + */ +public class PropertyReadException extends PropertyAccessException { private static final long serialVersionUID = 1L; - private final Object newValue; - public BeanPropertyWriteException(final Throwable cause, final Object newValue) + public PropertyReadException(final String propertyName, final Class targetType, final Throwable cause) { - super(cause); - this.newValue = newValue; + super(propertyName, targetType, cause); } @Override public String getMessage() { - return "Error while invoking write method. "; - } - - public Object getNewValue() - { - return newValue; + return String.format("Failed to read value from property '%s' of type '%s'", getPropertyName(), getTargetType().getCanonicalName()); } } diff --git a/src/main/java/de/danielbechler/diff/introspection/PropertyWriteException.java b/src/main/java/de/danielbechler/diff/introspection/PropertyWriteException.java new file mode 100644 index 00000000..8186af65 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/PropertyWriteException.java @@ -0,0 +1,45 @@ +/* + * 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.introspection; + +/** + * @author Daniel Bechler + */ +public class PropertyWriteException extends PropertyAccessException +{ + private static final long serialVersionUID = 1L; + + @SuppressWarnings("TransientFieldNotInitialized") + private final transient Object newValue; + + public PropertyWriteException(final String propertyName, final Class targetType, final Object newValue, final Throwable cause) + { + super(propertyName, targetType, cause); + this.newValue = newValue; + } + + @Override + public String getMessage() + { + return String.format("Failed to write new value '%s' to property '%s' of type '%s'", newValue, getPropertyName(), getTargetType().getCanonicalName()); + } + + public Object getNewValue() + { + return newValue; + } +} diff --git a/src/main/java/de/danielbechler/diff/introspection/StandardIntrospector.java b/src/main/java/de/danielbechler/diff/introspection/StandardIntrospector.java new file mode 100644 index 00000000..21c3399a --- /dev/null +++ b/src/main/java/de/danielbechler/diff/introspection/StandardIntrospector.java @@ -0,0 +1,90 @@ +/* + * 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.introspection; + +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.diff.instantiation.TypeInfo; +import de.danielbechler.util.Assert; +import de.danielbechler.util.Exceptions; + +import java.beans.BeanInfo; +import java.beans.IntrospectionException; +import java.beans.Introspector; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; + +/** + * Resolves the accessors of a given type by using the standard Java {@link Introspector}. + * + * @author Daniel Bechler + */ +public class StandardIntrospector implements de.danielbechler.diff.introspection.Introspector +{ + public TypeInfo introspect(final Class type) + { + Assert.notNull(type, "type"); + try + { + return internalIntrospect(type); + } + catch (final IntrospectionException e) + { + throw Exceptions.escalate(e); + } + } + + private TypeInfo internalIntrospect(final Class type) throws IntrospectionException + { + final TypeInfo typeInfo = new TypeInfo(type); + final PropertyDescriptor[] descriptors = getBeanInfo(type).getPropertyDescriptors(); + for (final PropertyDescriptor descriptor : descriptors) + { + if (shouldSkip(descriptor)) + { + continue; + } + final String propertyName = descriptor.getName(); + final Method readMethod = descriptor.getReadMethod(); + final Method writeMethod = descriptor.getWriteMethod(); + final PropertyAwareAccessor accessor = new PropertyAccessor(propertyName, readMethod, writeMethod); + typeInfo.addPropertyAccessor(accessor); + } + return typeInfo; + } + + protected BeanInfo getBeanInfo(final Class type) throws IntrospectionException + { + return Introspector.getBeanInfo(type); + } + + private static boolean shouldSkip(final PropertyDescriptor descriptor) + { + if (descriptor.getName().equals("class")) // Java & Groovy + { + return true; + } + if (descriptor.getName().equals("metaClass")) // Groovy + { + return true; + } + if (descriptor.getReadMethod() == null) + { + return true; + } + return false; + } +} diff --git a/src/main/java/de/danielbechler/diff/CanonicalAccessor.java b/src/main/java/de/danielbechler/diff/introspection/TypeInfoResolver.java similarity index 69% rename from src/main/java/de/danielbechler/diff/CanonicalAccessor.java rename to src/main/java/de/danielbechler/diff/introspection/TypeInfoResolver.java index 7c066256..524ab256 100644 --- a/src/main/java/de/danielbechler/diff/CanonicalAccessor.java +++ b/src/main/java/de/danielbechler/diff/introspection/TypeInfoResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,9 +14,12 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.introspection; -/** @author Daniel Bechler */ -public interface CanonicalAccessor extends Accessor { +import de.danielbechler.diff.node.DiffNode; +import de.danielbechler.diff.instantiation.TypeInfo; +public interface TypeInfoResolver +{ + TypeInfo typeInfoForNode(DiffNode type); } diff --git a/src/main/java/de/danielbechler/diff/visitor/AbstractFilteringVisitor.java b/src/main/java/de/danielbechler/diff/node/AbstractFilteringVisitor.java similarity index 77% rename from src/main/java/de/danielbechler/diff/visitor/AbstractFilteringVisitor.java rename to src/main/java/de/danielbechler/diff/node/AbstractFilteringVisitor.java index ab152b4a..cafb4d2f 100644 --- a/src/main/java/de/danielbechler/diff/visitor/AbstractFilteringVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/AbstractFilteringVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,29 +14,19 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; +import java.util.Collection; +import java.util.LinkedList; -import java.util.*; - -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public abstract class AbstractFilteringVisitor implements DiffNode.Visitor { - private final Collection matches = new ArrayList(30); - - protected abstract boolean accept(final DiffNode node); - - protected void onAccept(final DiffNode node, final Visit visit) - { - matches.add(node); - } - - protected void onDismiss(final DiffNode node, final Visit visit) - { - } + private final Collection matches = new LinkedList(); - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { if (accept(node)) { @@ -48,6 +38,17 @@ public void accept(final DiffNode node, final Visit visit) } } + protected abstract boolean accept(final DiffNode node); + + protected void onAccept(final DiffNode node, final Visit visit) + { + matches.add(node); + } + + protected void onDismiss(final DiffNode node, final Visit visit) + { + } + public Collection getMatches() { return matches; diff --git a/src/main/java/de/danielbechler/diff/visitor/CategoryFilteringVisitor.java b/src/main/java/de/danielbechler/diff/node/CategoryFilteringVisitor.java similarity index 77% rename from src/main/java/de/danielbechler/diff/visitor/CategoryFilteringVisitor.java rename to src/main/java/de/danielbechler/diff/node/CategoryFilteringVisitor.java index c3529fac..5c6213cb 100644 --- a/src/main/java/de/danielbechler/diff/visitor/CategoryFilteringVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/CategoryFilteringVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,16 +14,18 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; import de.danielbechler.util.Collections; -import java.util.*; +import java.util.Collection; +import java.util.TreeSet; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @SuppressWarnings("UnusedDeclaration") -public abstract class CategoryFilteringVisitor extends AbstractFilteringVisitor +public class CategoryFilteringVisitor extends AbstractFilteringVisitor { private final Collection include = new TreeSet(); private final Collection exclude = new TreeSet(); @@ -70,7 +72,13 @@ public final CategoryFilteringVisitor include(final String category) return this; } - @SuppressWarnings({"UnusedDeclaration"}) + /** + * @deprecated This method is confusing. The name implies only nodes with the given category + * will be included, but that's not the case. Instead every previously included category will + * also be included. On top of that every excluded category will remain excluded. This method + * will be removed in future versions until the feature is explicitly requested. + */ + @Deprecated public final CategoryFilteringVisitor includeOnly(final String category) { include(category); @@ -84,7 +92,6 @@ public final CategoryFilteringVisitor includeAllNonExcluded(final boolean value) return this; } - @SuppressWarnings({"UnusedDeclaration"}) public final CategoryFilteringVisitor exclude(final String category) { exclude.add(category); diff --git a/src/main/java/de/danielbechler/diff/DiffNode.java b/src/main/java/de/danielbechler/diff/node/DiffNode.java similarity index 51% rename from src/main/java/de/danielbechler/diff/DiffNode.java rename to src/main/java/de/danielbechler/diff/node/DiffNode.java index 5706fdf9..2c02fed9 100644 --- a/src/main/java/de/danielbechler/diff/DiffNode.java +++ b/src/main/java/de/danielbechler/diff/node/DiffNode.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,16 +14,32 @@ * limitations under the License. */ -package de.danielbechler.diff; - -import de.danielbechler.diff.bean.*; -import de.danielbechler.diff.visitor.*; -import de.danielbechler.util.*; - -import java.lang.annotation.*; -import java.util.*; +package de.danielbechler.diff.node; + +import de.danielbechler.diff.access.Accessor; +import de.danielbechler.diff.access.CategoryAware; +import de.danielbechler.diff.access.ExclusionAware; +import de.danielbechler.diff.access.PropertyAwareAccessor; +import de.danielbechler.diff.access.RootAccessor; +import de.danielbechler.diff.access.TypeAwareAccessor; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.diff.instantiation.TypeInfo; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.diff.selector.BeanPropertyElementSelector; +import de.danielbechler.diff.selector.CollectionItemElementSelector; +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.diff.selector.RootElementSelector; +import de.danielbechler.util.Assert; + +import java.lang.annotation.Annotation; import java.util.Collections; -import java.util.concurrent.atomic.*; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Collections.unmodifiableSet; @@ -36,17 +52,40 @@ * * @author Daniel Bechler */ -public class DiffNode implements Accessor +// TODO This thing is massive and needs to be broken down into subclasses +@SuppressWarnings("UnusedDeclaration") +public class DiffNode { public static final DiffNode ROOT = null; + private final Accessor accessor; - private final Map children = new LinkedHashMap(10); + private final Map children = new LinkedHashMap(10); private State state = State.UNTOUCHED; private DiffNode parentNode; private NodePath circleStartPath; private DiffNode circleStartNode; private Class valueType; + private TypeInfo valueTypeInfo; + private IdentityStrategy childIdentityStrategy; + private final Collection additionalCategories = new TreeSet(); + + public void setChildIdentityStrategy(final IdentityStrategy identityStrategy) + { + this.childIdentityStrategy = identityStrategy; + } + + public static DiffNode newRootNode() + { + return new DiffNode(); + } + + public static DiffNode newRootNodeWithType(final Class valueType) + { + final DiffNode rootNode = newRootNode(); + rootNode.setType(valueType); + return rootNode; + } public DiffNode(final DiffNode parentNode, final Accessor accessor, final Class valueType) { @@ -56,19 +95,15 @@ public DiffNode(final DiffNode parentNode, final Accessor accessor, final Class< setParentNode(parentNode); } - public DiffNode(final Accessor accessor, final Class valueType) + public DiffNode(final DiffNode parentNode, final Accessor accessor) { - this(ROOT, accessor, valueType); + this(parentNode, accessor, null); } - public DiffNode(final Class valueType) + private DiffNode() { - this(ROOT, RootAccessor.getInstance(), valueType); - } - - public DiffNode() - { - this(ROOT, RootAccessor.getInstance(), null); + this.parentNode = ROOT; + this.accessor = RootAccessor.getInstance(); } /** @@ -102,7 +137,7 @@ public boolean hasChanges() final AtomicBoolean result = new AtomicBoolean(false); visitChildren(new Visitor() { - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { if (node.hasChanges()) { @@ -157,28 +192,27 @@ public boolean isCircular() /** * @return The absolute property path from the object root up to this node. */ - public final NodePath getPath() + public NodePath getPath() { if (parentNode != null) { - return NodePath.createBuilder() - .withPropertyPath(parentNode.getPath()) - .withElement(accessor.getPathElement()) + return NodePath.startBuildingFrom(parentNode.getPath()) + .element(accessor.getElementSelector()) .build(); } else if (accessor instanceof RootAccessor) { - return NodePath.createBuilder().withRoot().build(); + return NodePath.withRoot(); } else { - return NodePath.createBuilder().withRoot().withElement(accessor.getPathElement()).build(); + return NodePath.startBuilding().element(accessor.getElementSelector()).build(); } } - public Element getPathElement() + public ElementSelector getElementSelector() { - return accessor.getPathElement(); + return accessor.getElementSelector(); } /** @@ -190,6 +224,10 @@ public Class getValueType() { return valueType; } + if (valueTypeInfo != null) + { + return valueTypeInfo.getType(); + } if (accessor instanceof TypeAwareAccessor) { return ((TypeAwareAccessor) accessor).getType(); @@ -198,16 +236,26 @@ public Class getValueType() } /** - * Allows for explicit type definition. However, if the accessor is TypeAware, {@link #getValueType()} will - * always return the type returned by the accessor. + * Allows to explicity set the type of this node. This will overshadow the type returned by {@linkplain + * #getValueTypeInfo()} as well as the one returned by the accessor. * * @param aClass The type of the value represented by this node. */ - public void setType(final Class aClass) + public final void setType(final Class aClass) { this.valueType = aClass; } + public TypeInfo getValueTypeInfo() + { + return valueTypeInfo; + } + + public void setValueTypeInfo(final TypeInfo typeInfo) + { + this.valueTypeInfo = typeInfo; + } + /** * @return true if this node has children. */ @@ -216,12 +264,9 @@ public boolean hasChildren() return !children.isEmpty(); } - /** - * @return The child nodes of this node. - */ - public Collection getChildren() + public int childCount() { - return children.values(); + return children.size(); } /** @@ -232,31 +277,83 @@ public Collection getChildren() */ public DiffNode getChild(final String propertyName) { - return children.get(new NamedPropertyElement(propertyName)); + return getChild(new BeanPropertyElementSelector(propertyName)); + } + + /** + * Retrieve a child that matches the given path element relative to this node. + * + * @param elementSelector The path element of the child node to get. + * @return The requested child node or null. + */ + public DiffNode getChild(final ElementSelector elementSelector) + { + if (elementSelector instanceof CollectionItemElementSelector && childIdentityStrategy != null) + { + return children.get(((CollectionItemElementSelector) elementSelector).copyWithIdentityStrategy(childIdentityStrategy)); + } + else + { + return children.get(elementSelector); + } } /** * Retrieve a child that matches the given absolute path, starting from the current node. * - * @param path The path from the object root to the requested child node. + * @param nodePath The path from the object root to the requested child node. * @return The requested child node or null. */ - public DiffNode getChild(final NodePath path) + public DiffNode getChild(final NodePath nodePath) { - final PropertyVisitor visitor = new PropertyVisitor(path); - visitChildren(visitor); - return visitor.getNode(); + if (parentNode != null) + { + return parentNode.getChild(nodePath.getElementSelectors()); + } + else + { + return getChild(nodePath.getElementSelectors()); + } } /** * Retrieve a child that matches the given path element relative to this node. * - * @param pathElement The path element of the child node to get. + * @param selectors The path element of the child node to get. * @return The requested child node or null. */ - public DiffNode getChild(final Element pathElement) + public DiffNode getChild(final List selectors) { - return children.get(pathElement); + Assert.notEmpty(selectors, "selectors"); + final ElementSelector selector = selectors.get(0); + if (selectors.size() == 1) + { + if (selector == RootElementSelector.getInstance()) + { + return isRootNode() ? this : null; + } + else + { + return getChild(selector); + } + } + else if (selectors.size() > 1) + { + final DiffNode child; + if (selector == RootElementSelector.getInstance()) + { + child = isRootNode() ? this : null; + } + else + { + child = getChild(selector); + } + if (child != null) + { + return child.getChild(selectors.subList(1, selectors.size())); + } + } + return null; } /** @@ -264,43 +361,33 @@ public DiffNode getChild(final Element pathElement) * * @param node The node to add. */ - public boolean addChild(final DiffNode node) + public void addChild(final DiffNode node) { - if (node.isRootNode()) - { - throw new IllegalArgumentException("Detected attempt to add root node as child. " + - "This is not allowed and must be a mistake."); - } - else if (node == this) + if (node == this) { throw new IllegalArgumentException("Detected attempt to add a node to itself. " + "This would cause inifite loops and must never happen."); } + else if (node.isRootNode()) + { + throw new IllegalArgumentException("Detected attempt to add root node as child. " + + "This is not allowed and must be a mistake."); + } else if (node.getParentNode() != null && node.getParentNode() != this) { throw new IllegalArgumentException("Detected attempt to add child node that is already the " + "child of another node. Adding nodes multiple times is not allowed, since it could " + "cause infinite loops."); } - final Element pathElement = node.getPathElement(); if (node.getParentNode() == null) { node.setParentNode(this); - children.put(pathElement, node); - } - else if (node.getParentNode() == this) - { - children.put(pathElement, node); - } - else - { - throw new IllegalStateException("Detected attempt to replace the parent node of node at path '" + getPath() + "'"); } + children.put(node.getElementSelector(), node); if (state == State.UNTOUCHED && node.hasChanges()) { state = State.CHANGED; } - return true; } /** @@ -324,7 +411,7 @@ protected final void visit(final Visitor visitor, final Visit visit) { try { - visitor.accept(this, visit); + visitor.node(this, visit); } catch (final StopVisitationException e) { @@ -347,19 +434,63 @@ protected final void visit(final Visitor visitor, final Visit visit) */ public final void visitChildren(final Visitor visitor) { - for (final DiffNode child : getChildren()) + for (final DiffNode child : children.values()) { try { child.visit(visitor); } - catch (StopVisitationException e) + catch (final StopVisitationException e) { return; } } } + public final void visitParents(final Visitor visitor) + { + final Visit visit = new Visit(); + if (parentNode != null) + { + visitor.node(parentNode, visit); + if (!visit.isStopped()) + { + parentNode.visitParents(visitor); + } + } + } + + /** + * If this node represents a bean property this method returns all annotations of its field. + *

+ * Only works for fields having a name that matches the name derived from the getter. + * + * @return The annotations of the field, or an empty set if there is no field with the name derived from the getter. + */ + public Set getFieldAnnotations() + { + if (accessor instanceof PropertyAwareAccessor) + { + return unmodifiableSet(((PropertyAwareAccessor) accessor).getFieldAnnotations()); + } + return unmodifiableSet(Collections.emptySet()); + } + + /** + * @param annotationClass the annotation we are looking for + * @param + * @return The given annotation of the field, or null if not annotated or if there is no field with the name derived + * from the getter. + */ + public T getFieldAnnotation(final Class annotationClass) + { + if (accessor instanceof PropertyAwareAccessor) + { + return ((PropertyAwareAccessor) accessor).getFieldAnnotation(annotationClass); + } + return null; + } + /** * If this node represents a bean property this method returns all annotations of its getter. * @@ -383,6 +514,37 @@ public T getPropertyAnnotation(final Class annotationC return null; } + /** + * If this node represents a bean property, this method will simply return its name. Otherwise it will return the + * property name of its closest bean property representing ancestor. This way intermediate nodes like those + * representing collection, map or array items will be semantically tied to their container objects. + *

+ * That is especially useful for inclusion and exclusion rules. For example, when a List is explicitly included by + * property name, it would be weird if the inclusion didn't also apply to its items. + */ + public String getPropertyName() + { + if (isPropertyAware()) + { + return ((PropertyAwareAccessor) accessor).getPropertyName(); + } + else if (parentNode != null) + { + return parentNode.getPropertyName(); + } + return null; + } + + /** + * Returns true when this node represents a bean property and can therefore be queried for property + * specific information like annotations or property types. But there will also be nodes that represent collection + * items, map entries, etc. In those cases this method will return false. + */ + public final boolean isPropertyAware() + { + return accessor instanceof PropertyAwareAccessor; + } + public final boolean isRootNode() { return accessor instanceof RootAccessor; @@ -396,24 +558,28 @@ public final boolean isIgnored() return state == State.IGNORED; } - public ComparisonStrategy getComparisonStrategy() - { - if (accessor instanceof ComparisonStrategyAwareAccessor) - { - return ((ComparisonStrategyAwareAccessor) accessor).getComparisonStrategy(); - } - return null; - } - + /** + * @see de.danielbechler.diff.inclusion.TypePropertyAnnotationInclusionResolver + * @deprecated This method was a shortcut to extract the "exclude" flag from the ObjectDiffProperty + * annotation. Since we found a better way to do that, it is not needed anymore and will be removed in future + * versions. The name is also misleading. It implies that here lies the truth about the exclusion, but only the + * InclusionService can tell for sure. This flag is just only little piece of the puzzle. + */ + @Deprecated public boolean isExcluded() { - if (accessor instanceof ExclusionAwareAccessor) + if (accessor instanceof ExclusionAware) { - return ((ExclusionAwareAccessor) accessor).isExcluded(); + return ((ExclusionAware) accessor).isExcludedByAnnotation(); } return false; } + /** + * Returns an unmodifiable {@link java.util.Set} of {@link java.lang.String} with the categories of this node. + * + * @return an unmodifiable {@link java.util.Set} of {@link java.lang.String} with the categories of this node + */ public final Set getCategories() { final Set categories = new TreeSet(); @@ -421,16 +587,17 @@ public final Set getCategories() { categories.addAll(parentNode.getCategories()); } - if (accessor instanceof CategoryAwareAccessor) + if (accessor instanceof CategoryAware) { - final CategoryAwareAccessor categoryAwareAccessor = (CategoryAwareAccessor) accessor; - final Set categoriesFromAccessor = categoryAwareAccessor.getCategories(); + final Set categoriesFromAccessor = ((CategoryAware) accessor).getCategoriesFromAnnotation(); if (categoriesFromAccessor != null) { categories.addAll(categoriesFromAccessor); } } - return categories; + categories.addAll(additionalCategories); + + return unmodifiableSet(categories); } /** @@ -446,7 +613,7 @@ public DiffNode getParentNode() * * @param parentNode The parent of this node. May be null, if this is a root node. */ - public final void setParentNode(final DiffNode parentNode) + protected final void setParentNode(final DiffNode parentNode) { if (this.parentNode != null && this.parentNode != parentNode) { @@ -476,16 +643,31 @@ public Object canonicalGet(Object target) { target = parentNode.canonicalGet(target); } - return accessor.get(target); + return get(target); } public void canonicalSet(Object target, final Object value) { if (parentNode != null) { - target = parentNode.canonicalGet(target); + Object parent = parentNode.canonicalGet(target); + if (parent == null) + { + parent = parentNode.newInstance(); + parentNode.canonicalSet(target, parent); + } + target = parent; } - accessor.set(target, value); + set(target, value); + } + + private Object newInstance() + { + if (valueTypeInfo != null) + { + return valueTypeInfo.newInstance(); + } + return null; } public void canonicalUnset(Object target) @@ -494,7 +676,35 @@ public void canonicalUnset(Object target) { target = parentNode.canonicalGet(target); } - accessor.unset(target); + unset(target); + } + + @Override + public int hashCode() + { + return accessor.hashCode(); + } + + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + + final DiffNode that = (DiffNode) o; + + if (!accessor.equals(that.accessor)) + { + return false; + } + + return true; } @Override @@ -509,13 +719,13 @@ public String toString() { sb.append(", type=").append(getValueType().getCanonicalName()); } - if (getChildren().size() == 1) + if (childCount() == 1) { - sb.append(", ").append(getChildren().size()).append(" child"); + sb.append(", ").append(childCount()).append(" child"); } - else if (getChildren().size() > 1) + else if (childCount() > 1) { - sb.append(", ").append(getChildren().size()).append(" children"); + sb.append(", ").append(childCount()).append(" children"); } else { @@ -530,32 +740,10 @@ else if (getChildren().size() > 1) return sb.toString(); } - @Override - public boolean equals(final Object o) + public void addCategories(final Collection additionalCategories) { - if (this == o) - { - return true; - } - if (o == null || getClass() != o.getClass()) - { - return false; - } - - final DiffNode that = (DiffNode) o; - - if (!accessor.equals(that.accessor)) - { - return false; - } - - return true; - } - - @Override - public int hashCode() - { - return accessor.hashCode(); + Assert.notNull(additionalCategories, "additionalCategories"); + this.additionalCategories.addAll(additionalCategories); } /** @@ -587,42 +775,32 @@ public void setCircleStartNode(final DiffNode circleStartNode) */ public enum State { - /** - * The value has been added to the working object. - */ - ADDED, + ADDED("The value has been added to the working object"), + CHANGED("The value exists but differs between the base and working object"), + REMOVED("The value has been removed from the working object"), + UNTOUCHED("The value is identical in the working and base object"), + CIRCULAR("Special state to mark circular references"), + IGNORED("The value has not been looked at and has been ignored"), + INACCESSIBLE("When a comparison was not possible because the underlying value was not accessible"); - /** - * The value has been changed compared to the base object. - */ - CHANGED, + private final String reason; - /** - * The value has been removed from the working object. - */ - REMOVED, - - /** - * The value is identical between working and base - */ - UNTOUCHED, - - /** - * Special state to mark circular references - */ - CIRCULAR, + State(final String reason) + { + this.reason = reason; + } - /** - * The value has not been looked at and has been ignored. - */ - IGNORED + public String getReason() + { + return reason; + } } /** * Visitor to traverse a node graph. */ - public static interface Visitor + public interface Visitor { - void accept(DiffNode node, Visit visit); + void node(DiffNode node, Visit visit); } } diff --git a/src/main/java/de/danielbechler/diff/visitor/NodeHierarchyVisitor.java b/src/main/java/de/danielbechler/diff/node/NodeHierarchyVisitor.java similarity index 88% rename from src/main/java/de/danielbechler/diff/visitor/NodeHierarchyVisitor.java rename to src/main/java/de/danielbechler/diff/node/NodeHierarchyVisitor.java index 28caa6d2..7bf358a5 100644 --- a/src/main/java/de/danielbechler/diff/visitor/NodeHierarchyVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/NodeHierarchyVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,23 +14,22 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.util.Strings; /** * Prints the hierarchy of the object graph in a human-readable form. * * @author Daniel Bechler */ +@SuppressWarnings({"UnusedDeclaration"}) public class NodeHierarchyVisitor implements DiffNode.Visitor { public static final int UNLIMITED = -1; private final int maxDepth; - @SuppressWarnings({"UnusedDeclaration"}) public NodeHierarchyVisitor() { this(UNLIMITED); @@ -41,7 +40,7 @@ public NodeHierarchyVisitor(final int maxDepth) this.maxDepth = maxDepth; } - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { if (maxDepth == 0) { @@ -65,6 +64,18 @@ else if (maxDepth < 0) } } + private static int calculateDepth(final DiffNode node) + { + int count = 0; + DiffNode parentNode = node.getParentNode(); + while (parentNode != null) + { + count++; + parentNode = parentNode.getParentNode(); + } + return count; + } + protected void print(final DiffNode node, final int level) { final String nodeAsString = node.getPath() + " ===> " + node.toString(); @@ -77,16 +88,4 @@ protected void print(final String text) { System.out.println(text); } - - private static int calculateDepth(final DiffNode node) - { - int count = 0; - DiffNode parentNode = node.getParentNode(); - while (parentNode != null) - { - count++; - parentNode = parentNode.getParentNode(); - } - return count; - } } diff --git a/src/main/java/de/danielbechler/diff/visitor/PropertyVisitor.java b/src/main/java/de/danielbechler/diff/node/NodePathVisitor.java similarity index 66% rename from src/main/java/de/danielbechler/diff/visitor/PropertyVisitor.java rename to src/main/java/de/danielbechler/diff/node/NodePathVisitor.java index 502ee654..1ab5f742 100644 --- a/src/main/java/de/danielbechler/diff/visitor/PropertyVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/NodePathVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,28 +14,30 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.util.Assert; -/** @author Daniel Bechler */ -public class PropertyVisitor implements DiffNode.Visitor +/** + * @author Daniel Bechler + */ +public class NodePathVisitor implements DiffNode.Visitor { private final NodePath nodePath; private DiffNode node; - public PropertyVisitor(final NodePath nodePath) + public NodePathVisitor(final NodePath nodePath) { - Assert.notNull(nodePath, "propertyPath"); + Assert.notNull(nodePath, "nodePath"); this.nodePath = nodePath; } - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { final NodePath differencePath = node.getPath(); - if (differencePath.isParentOf(nodePath)) + if (differencePath.matches(nodePath) || differencePath.isParentOf(nodePath)) { if (differencePath.matches(nodePath)) { diff --git a/src/main/java/de/danielbechler/diff/visitor/PrintingVisitor.java b/src/main/java/de/danielbechler/diff/node/PrintingVisitor.java similarity index 85% rename from src/main/java/de/danielbechler/diff/visitor/PrintingVisitor.java rename to src/main/java/de/danielbechler/diff/node/PrintingVisitor.java index 69b07b6f..6d2c1b86 100644 --- a/src/main/java/de/danielbechler/diff/visitor/PrintingVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/PrintingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,13 +14,14 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.diff.path.NodePath; +import de.danielbechler.util.Strings; -/** @author Daniel Bechler */ -@SuppressWarnings({"MethodMayBeStatic"}) +/** + * @author Daniel Bechler + */ public class PrintingVisitor implements DiffNode.Visitor { private final Object working; @@ -32,7 +33,7 @@ public PrintingVisitor(final Object working, final Object base) this.working = working; } - public void accept(final DiffNode node, final Visit visit) + public void node(final DiffNode node, final Visit visit) { if (filter(node)) { @@ -44,12 +45,7 @@ public void accept(final DiffNode node, final Visit visit) protected boolean filter(final DiffNode node) { return (node.isRootNode() && !node.hasChanges()) - || (node.hasChanges() && node.getChildren().isEmpty()); - } - - protected void print(final String text) - { - System.out.println(text); + || (node.hasChanges() && !node.hasChildren()); } protected String differenceToString(final DiffNode node, final Object base, final Object modified) @@ -66,7 +62,12 @@ protected String differenceToString(final DiffNode node, final Object base, fina return stringBuilder.toString(); } - private String translateState(final DiffNode.State state, final Object base, final Object modified) + protected void print(final String text) + { + System.out.println(text); + } + + private static String translateState(final DiffNode.State state, final Object base, final Object modified) { if (state == DiffNode.State.IGNORED) { diff --git a/src/main/java/de/danielbechler/diff/visitor/StateFilteringVisitor.java b/src/main/java/de/danielbechler/diff/node/StateFilteringVisitor.java similarity index 87% rename from src/main/java/de/danielbechler/diff/visitor/StateFilteringVisitor.java rename to src/main/java/de/danielbechler/diff/node/StateFilteringVisitor.java index 02b2b0b3..7696c2f6 100644 --- a/src/main/java/de/danielbechler/diff/visitor/StateFilteringVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/StateFilteringVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,12 +14,13 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.util.Assert; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class StateFilteringVisitor extends AbstractFilteringVisitor { private final DiffNode.State state; diff --git a/src/main/java/de/danielbechler/diff/visitor/StopVisitationException.java b/src/main/java/de/danielbechler/diff/node/StopVisitationException.java similarity index 84% rename from src/main/java/de/danielbechler/diff/visitor/StopVisitationException.java rename to src/main/java/de/danielbechler/diff/node/StopVisitationException.java index e16120b5..9407ee06 100644 --- a/src/main/java/de/danielbechler/diff/visitor/StopVisitationException.java +++ b/src/main/java/de/danielbechler/diff/node/StopVisitationException.java @@ -14,10 +14,12 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -/** @author Daniel Bechler */ -public final class StopVisitationException extends RuntimeException +/** + * @author Daniel Bechler + */ +final class StopVisitationException extends RuntimeException { private static final long serialVersionUID = 1L; diff --git a/src/main/java/de/danielbechler/diff/visitor/ToMapPrintingVisitor.java b/src/main/java/de/danielbechler/diff/node/ToMapPrintingVisitor.java similarity index 92% rename from src/main/java/de/danielbechler/diff/visitor/ToMapPrintingVisitor.java rename to src/main/java/de/danielbechler/diff/node/ToMapPrintingVisitor.java index 08ff6d72..c8a387a8 100644 --- a/src/main/java/de/danielbechler/diff/visitor/ToMapPrintingVisitor.java +++ b/src/main/java/de/danielbechler/diff/node/ToMapPrintingVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,11 +14,12 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -import de.danielbechler.diff.*; +import de.danielbechler.diff.path.NodePath; -import java.util.*; +import java.util.LinkedHashMap; +import java.util.Map; /** * This visitor generates the same output as the {@link PrintingVisitor}, but instead of printing it to diff --git a/src/main/java/de/danielbechler/diff/visitor/Visit.java b/src/main/java/de/danielbechler/diff/node/Visit.java similarity index 93% rename from src/main/java/de/danielbechler/diff/visitor/Visit.java rename to src/main/java/de/danielbechler/diff/node/Visit.java index 136e6225..f5c98ba0 100644 --- a/src/main/java/de/danielbechler/diff/visitor/Visit.java +++ b/src/main/java/de/danielbechler/diff/node/Visit.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package de.danielbechler.diff.visitor; +package de.danielbechler.diff.node; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public final class Visit { private enum State diff --git a/src/main/java/de/danielbechler/diff/path/NodePath.java b/src/main/java/de/danielbechler/diff/path/NodePath.java new file mode 100644 index 00000000..fa2b5e93 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/path/NodePath.java @@ -0,0 +1,262 @@ +/* + * 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.path; + +import de.danielbechler.diff.selector.BeanPropertyElementSelector; +import de.danielbechler.diff.selector.CollectionItemElementSelector; +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.diff.selector.MapKeyElementSelector; +import de.danielbechler.diff.selector.RootElementSelector; +import de.danielbechler.util.Assert; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * @author Daniel Bechler + */ +public final class NodePath implements Comparable +{ + private final List elementSelectors; + + private NodePath(final List elementSelectors) + { + this.elementSelectors = Collections.unmodifiableList(elementSelectors); + } + + public boolean isParentOf(final NodePath nodePath) + { + final List otherElementSelectors = nodePath.getElementSelectors(); + if (elementSelectors.size() < otherElementSelectors.size()) + { + return otherElementSelectors.subList(0, elementSelectors.size()).equals(elementSelectors); + } + return false; + } + + public List getElementSelectors() + { + return elementSelectors; + } + + public boolean isChildOf(final NodePath nodePath) + { + final List otherElementSelectors = nodePath.getElementSelectors(); + if (elementSelectors.size() > otherElementSelectors.size()) + { + return elementSelectors.subList(0, otherElementSelectors.size()).equals(otherElementSelectors); + } + return false; + } + + public ElementSelector getLastElementSelector() + { + return elementSelectors.get(elementSelectors.size() - 1); + } + + @Override + public int hashCode() + { + return elementSelectors.hashCode(); + } + + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + + final NodePath that = (NodePath) o; + + if (!elementSelectors.equals(that.elementSelectors)) + { + return false; + } + + return true; + } + + @Override + public String toString() + { + final StringBuilder sb = new StringBuilder(); + final Iterator iterator = elementSelectors.iterator(); + ElementSelector previousElementSelector = null; + while (iterator.hasNext()) + { + final ElementSelector elementSelector = iterator.next(); + if (elementSelector instanceof RootElementSelector) + { + sb.append("/"); + } + else if (elementSelector instanceof CollectionItemElementSelector || elementSelector instanceof MapKeyElementSelector) + { + sb.append(elementSelector); + } + else if (previousElementSelector instanceof RootElementSelector) + { + sb.append(elementSelector); + } + else + { + sb.append('/'); + sb.append(elementSelector); + } + previousElementSelector = elementSelector; + } + return sb.toString(); + } + + public int compareTo(final NodePath that) + { + final int distance = getElementSelectors().size() - that.getElementSelectors().size(); + if (distance == 0) + { + return matches(that) ? 0 : 1; + } + else if (distance > 0) + { + return 1; + } + else + { + return -1; + } + } + + public boolean matches(final NodePath nodePath) + { + return nodePath.equals(this); + } + + public static AppendableBuilder startBuildingFrom(final NodePath nodePath) + { + Assert.notNull(nodePath, "propertyPath"); + return new AppendableBuilderImpl(new ArrayList(nodePath.getElementSelectors())); + } + + public static NodePath with(final String propertyName, final String... additionalPropertyNames) + { + return startBuilding().propertyName(propertyName, additionalPropertyNames).build(); + } + + public static AppendableBuilder startBuilding() + { + final List elementSelectors1 = new LinkedList(); + elementSelectors1.add(RootElementSelector.getInstance()); + return new AppendableBuilderImpl(elementSelectors1); + } + + public static NodePath withRoot() + { + return startBuilding().build(); + } + + public static interface AppendableBuilder + { + AppendableBuilder element(ElementSelector elementSelector); + + AppendableBuilder propertyName(String name, String... names); + + AppendableBuilder collectionItem(T item); + + AppendableBuilder mapKey(K key); + + NodePath build(); + } + + private static final class AppendableBuilderImpl implements AppendableBuilder + { + private final List elementSelectors; + + public AppendableBuilderImpl(final List elementSelectors) + { + Assert.notEmpty(elementSelectors, "elementSelectors"); + this.elementSelectors = new LinkedList(elementSelectors); + } + + public AppendableBuilder element(final ElementSelector elementSelector) + { + Assert.notNull(elementSelector, "elementSelector"); + elementSelectors.add(elementSelector); + return this; + } + + public AppendableBuilder propertyName(final String name, final String... names) + { + elementSelectors.add(new BeanPropertyElementSelector(name)); + for (final String s : names) + { + elementSelectors.add(new BeanPropertyElementSelector(s)); + } + return this; + } + + public AppendableBuilder collectionItem(final T item) + { + elementSelectors.add(new CollectionItemElementSelector(item)); + return this; + } + + public AppendableBuilder mapKey(final K key) + { + Assert.notNull(key, "key"); + elementSelectors.add(new MapKeyElementSelector(key)); + return this; + } + + public NodePath build() + { + if (elementSelectors.isEmpty()) + { + throw new IllegalStateException("A property path cannot be empty"); + } + else if (!(elementSelectors.get(0) instanceof RootElementSelector)) + { + throw new IllegalStateException("A property path must start with a root element"); + } + else if (elementCount(RootElementSelector.class) > 1) + { + throw new IllegalStateException("A property path cannot contain multiple root elements"); + } + return new NodePath(elementSelectors); + } + + private int elementCount(final Class type) + { + assert type != null : "Type must not be null"; + int count = 0; + for (final ElementSelector elementSelector : elementSelectors) + { + if (type.isAssignableFrom(elementSelector.getClass())) + { + count++; + } + } + return count; + } + } +} diff --git a/src/main/java/de/danielbechler/diff/path/NodePathValueHolder.java b/src/main/java/de/danielbechler/diff/path/NodePathValueHolder.java new file mode 100644 index 00000000..76c7211d --- /dev/null +++ b/src/main/java/de/danielbechler/diff/path/NodePathValueHolder.java @@ -0,0 +1,192 @@ +/* + * 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.path; + +import de.danielbechler.diff.selector.ElementSelector; +import de.danielbechler.diff.selector.RootElementSelector; +import de.danielbechler.util.Assert; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * @author Daniel Bechler + * @see de.danielbechler.diff.inclusion.ValueNode + * @deprecated The ConfigNode provides a much more powerful way to store values for NodePaths. + */ +@Deprecated +public class NodePathValueHolder +{ + private final Map> elementValueHolders = new HashMap>(); + private T value; + + public static NodePathValueHolder of(final Class type) + { + Assert.notNull(type, "type"); + return new NodePathValueHolder(); + } + + public NodePathValueHolder put(final NodePath nodePath, final T value) + { + put(nodePath.getElementSelectors(), value); + return this; + } + + private void put(final List elementSelectors, final T value) + { + if (elementSelectors.isEmpty()) + { + return; + } + final ElementSelector elementSelector = elementSelectors.get(0); + NodePathValueHolder nodePathValueHolder = valueHolderForElementSelector(elementSelector); + if (nodePathValueHolder == null) + { + nodePathValueHolder = new NodePathValueHolder(); + elementValueHolders.put(elementSelector, nodePathValueHolder); + } + if (elementSelectors.size() == 1) + { + nodePathValueHolder.value = value; + } + else + { + final List nodePathElementsTail = new ArrayList(elementSelectors.size() - 1); + nodePathElementsTail.addAll(elementSelectors.subList(1, elementSelectors.size())); + nodePathValueHolder.put(nodePathElementsTail, value); + } + } + + private T visit(final List accumulator, final Iterator elementIterator) + { + if (value != null) + { + accumulator.add(value); + } + if (elementIterator.hasNext()) + { + final ElementSelector selector = elementIterator.next(); + final NodePathValueHolder valueHolder = valueHolderForElementSelector(selector); + if (valueHolder != null) + { + return valueHolder.visit(accumulator, elementIterator); + } + return null; + } + return value; + } + + private NodePathValueHolder valueHolderForElementSelector(final ElementSelector elementSelector) + { + return elementValueHolders.get(elementSelector); + } + + public T valueForNodePath(final NodePath nodePath) + { + return visit(new LinkedList(), nodePath.getElementSelectors().iterator()); + } + + public List accumulatedValuesForNodePath(final NodePath nodePath) + { + final List accumulator = new LinkedList(); + visit(accumulator, nodePath.getElementSelectors().iterator()); + return accumulator; + } + + public boolean containsValue(final T value) + { + if (value == null && this.value == null) + { + return true; + } + else if (value != null && value.equals(this.value)) + { + return true; + } + else + { + for (final NodePathValueHolder valueHolder : elementValueHolders.values()) + { + if (valueHolder.containsValue(value)) + { + return true; + } + } + return false; + } + } + + public void collect(final Collector collector) + { + collect(null, collector); + } + + private void collect(final NodePath nodePath, final Collector collector) + { + if (nodePath != null && value != null) + { + collector.it(nodePath, value); + } + for (final Map.Entry> entry : elementValueHolders.entrySet()) + { + final NodePath childNodePath; + final ElementSelector elementSelector = entry.getKey(); + final NodePathValueHolder valueHolder = entry.getValue(); + if (elementSelector == RootElementSelector.getInstance()) + { + childNodePath = NodePath.withRoot(); + } + else + { + childNodePath = NodePath.startBuildingFrom(nodePath).element(elementSelector).build(); + } + + if (valueHolder != null) + { + valueHolder.collect(childNodePath, collector); + } + } + } + + @Override + public String toString() + { + final StringBuilder stringBuilder = new StringBuilder(); + collect(new Collector() + { + public void it(final NodePath path, final T value) + { + stringBuilder.append(path.toString()).append(" => ").append(value).append('\n'); + } + }); + return stringBuilder.toString(); + } + + public void hasChildMatchingValue(NodePath nodePath, T inclusion) + { + + } + + public static interface Collector + { + void it(NodePath path, T value); + } +} diff --git a/src/main/java/de/danielbechler/diff/bean/NamedPropertyElement.java b/src/main/java/de/danielbechler/diff/selector/BeanPropertyElementSelector.java similarity index 78% rename from src/main/java/de/danielbechler/diff/bean/NamedPropertyElement.java rename to src/main/java/de/danielbechler/diff/selector/BeanPropertyElementSelector.java index 12816a3a..2f103958 100644 --- a/src/main/java/de/danielbechler/diff/bean/NamedPropertyElement.java +++ b/src/main/java/de/danielbechler/diff/selector/BeanPropertyElementSelector.java @@ -14,17 +14,18 @@ * limitations under the License. */ -package de.danielbechler.diff.bean; +package de.danielbechler.diff.selector; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.util.Assert; -/** @author Daniel Bechler */ -public final class NamedPropertyElement extends Element +/** + * @author Daniel Bechler + */ +public final class BeanPropertyElementSelector extends ElementSelector { private final String propertyName; - public NamedPropertyElement(final String propertyName) + public BeanPropertyElementSelector(final String propertyName) { Assert.hasText(propertyName, "propertyName"); this.propertyName = propertyName; @@ -53,7 +54,7 @@ public boolean equals(final Object o) return false; } - final NamedPropertyElement that = (NamedPropertyElement) o; + final BeanPropertyElementSelector that = (BeanPropertyElementSelector) o; if (!propertyName.equals(that.propertyName)) { diff --git a/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java b/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java new file mode 100644 index 00000000..6feff573 --- /dev/null +++ b/src/main/java/de/danielbechler/diff/selector/CollectionItemElementSelector.java @@ -0,0 +1,105 @@ +/* + * 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.selector; + +import de.danielbechler.diff.identity.EqualsIdentityStrategy; +import de.danielbechler.diff.identity.IdentityStrategy; +import de.danielbechler.util.Assert; +import de.danielbechler.util.Strings; + +/** + * @author Daniel Bechler + */ +public final class CollectionItemElementSelector extends ElementSelector +{ + private final Object item; + private final IdentityStrategy identityStrategy; + + /** + * Default implementation uses {@linkplain EqualsIdentityStrategy}. + * + * @param item + */ + public CollectionItemElementSelector(final Object item) + { + this(item, EqualsIdentityStrategy.getInstance()); + } + + /** + * Allows for custom IdentityStrategy. + * + * @param item + * @param identityStrategy + */ + CollectionItemElementSelector(final Object item, final IdentityStrategy identityStrategy) + { + Assert.notNull(identityStrategy, "identityStrategy"); + this.item = item; + this.identityStrategy = identityStrategy; + } + + public CollectionItemElementSelector copyWithIdentityStrategy(final IdentityStrategy identityStrategy) + { + return new CollectionItemElementSelector(item, identityStrategy); + } + + /** + * @deprecated Low-level API. Don't use in production code. May be removed + * in future versions. + */ + @SuppressWarnings({"UnusedDeclaration"}) + @Deprecated + public Object getItem() + { + return item; + } + + @Override + public String toHumanReadableString() + { + return "[" + Strings.toSingleLineString(item) + "]"; + } + + @Override + public boolean equals(final Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + + final CollectionItemElementSelector that = (CollectionItemElementSelector) o; + + if (item != null ? !identityStrategy.equals(item, that.item) : that.item != null) + { + return false; + } + + return true; + } + + @Override + public int hashCode() + { + return 31; + } + +} diff --git a/src/main/java/de/danielbechler/diff/Element.java b/src/main/java/de/danielbechler/diff/selector/ElementSelector.java similarity index 94% rename from src/main/java/de/danielbechler/diff/Element.java rename to src/main/java/de/danielbechler/diff/selector/ElementSelector.java index 7ae58c9a..c0d77a1b 100644 --- a/src/main/java/de/danielbechler/diff/Element.java +++ b/src/main/java/de/danielbechler/diff/selector/ElementSelector.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.selector; /** * Serves mainly as marker class and enforces the proper implementation of hashCode(), equals() and toString() - * for all property path elements. + * for all element selectors. * * @author Daniel Bechler */ -public abstract class Element +public abstract class ElementSelector { public abstract String toHumanReadableString(); @@ -30,7 +30,6 @@ public abstract class Element * Must be implemented in a way so that this element can be distinguished from the other ones. * * @param obj The object to check equality against. - * * @return true is the given object equals this one, otherwise false. */ public abstract boolean equals(Object obj); diff --git a/src/main/java/de/danielbechler/diff/map/MapElement.java b/src/main/java/de/danielbechler/diff/selector/MapKeyElementSelector.java similarity index 75% rename from src/main/java/de/danielbechler/diff/map/MapElement.java rename to src/main/java/de/danielbechler/diff/selector/MapKeyElementSelector.java index d24e8205..f1ac9186 100644 --- a/src/main/java/de/danielbechler/diff/map/MapElement.java +++ b/src/main/java/de/danielbechler/diff/selector/MapKeyElementSelector.java @@ -14,22 +14,25 @@ * limitations under the License. */ -package de.danielbechler.diff.map; +package de.danielbechler.diff.selector; -import de.danielbechler.diff.*; -import de.danielbechler.util.*; +import de.danielbechler.util.Strings; -/** @author Daniel Bechler */ -public final class MapElement extends Element +/** + * @author Daniel Bechler + */ +public final class MapKeyElementSelector extends ElementSelector { private final Object key; - public MapElement(final Object key) + public MapKeyElementSelector(final Object key) { this.key = key; } - /** @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 getKey() @@ -55,7 +58,7 @@ public boolean equals(final Object o) return false; } - final MapElement that = (MapElement) o; + final MapKeyElementSelector that = (MapKeyElementSelector) o; if (key != null ? !key.equals(that.key) : that.key != null) { diff --git a/src/main/java/de/danielbechler/diff/RootElement.java b/src/main/java/de/danielbechler/diff/selector/RootElementSelector.java similarity index 77% rename from src/main/java/de/danielbechler/diff/RootElement.java rename to src/main/java/de/danielbechler/diff/selector/RootElementSelector.java index f6e9539a..f44c2b67 100644 --- a/src/main/java/de/danielbechler/diff/RootElement.java +++ b/src/main/java/de/danielbechler/diff/selector/RootElementSelector.java @@ -14,18 +14,20 @@ * limitations under the License. */ -package de.danielbechler.diff; +package de.danielbechler.diff.selector; -/** @author Daniel Bechler */ -public final class RootElement extends Element +/** + * @author Daniel Bechler + */ +public final class RootElementSelector extends ElementSelector { - private static final RootElement instance = new RootElement(); + private static final RootElementSelector instance = new RootElementSelector(); - RootElement() + RootElementSelector() { } - public static RootElement getInstance() + public static RootElementSelector getInstance() { return instance; } diff --git a/src/main/java/de/danielbechler/util/Classes.java b/src/main/java/de/danielbechler/util/Classes.java index 3ded570b..e0f75f79 100644 --- a/src/main/java/de/danielbechler/util/Classes.java +++ b/src/main/java/de/danielbechler/util/Classes.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,27 +16,73 @@ package de.danielbechler.util; -import org.slf4j.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import java.io.*; -import java.lang.reflect.*; -import java.math.*; -import java.net.*; -import java.util.*; +import java.io.Serializable; +import java.lang.reflect.Constructor; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URI; +import java.net.URL; +import java.util.Calendar; +import java.util.Collection; +import java.util.Comparator; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.TreeSet; +import java.util.UUID; + +import static java.util.Arrays.asList; -/** @author Daniel Bechler */ public final class Classes { private static final Logger logger = LoggerFactory.getLogger(Classes.class); - - private static final Set> WRAPPER_TYPES = getWrapperTypes(); + private static final Set> PRIMITIVE_WRAPPER_TYPES = getPrimitiveWrapperTypes(); private static final Collection> PRIMITIVE_NUMERIC_TYPES = getPrimitiveNumericTypes(); + // private static final List COMPARABLE_TYPES = Arrays.asList( + // // The Extendables + // // since Java 1.8 + // Duration.class, + // Instant.class, + // LocalDate.class, + // LocalDateTime.class, + // LocalTime.class, + // MonthDay.class, + // OffsetDateTime.class, + // OffsetTime.class, + // Year.class, + // YearMonth.class, + // ZonedDateTime.class, + // ZoneOffset.class + // ); + @SuppressWarnings("unchecked") + private static final Collection> EXTENDABLE_SIMPLE_TYPES = asList( + BigDecimal.class, + BigInteger.class, + CharSequence.class, // String, StringBuilder, etc. + Calendar.class, // GregorianCalendar, etc. + Date.class, // java.sql.Date, java.util.Date, java.util.Time, etc. + Enum.class // enums... duh + + ); + @SuppressWarnings("unchecked") + private static final List> FINAL_SIMPLE_TYPES = asList( + Class.class, + URI.class, + URL.class, + Locale.class, + UUID.class + ); private Classes() { } - private static Set> getWrapperTypes() + private static Set> getPrimitiveWrapperTypes() { final Set> wrapperTypes = new HashSet>(); wrapperTypes.add(Boolean.class); @@ -66,60 +112,44 @@ private static Collection> getPrimitiveNumericTypes() numericTypes.add(long.class); numericTypes.add(float.class); numericTypes.add(double.class); +// +// The following are not numeric types and tests fail when they are commented in. Feels weird... +// +// numericTypes.add(boolean.class); +// numericTypes.add(void.class); +// return numericTypes; } - public static boolean isPrimitiveType(final Class clazz) - { - return clazz != null && clazz.isPrimitive(); - } - - public static boolean isPrimitiveWrapperType(final Class clazz) + public static boolean isComparableType(final Class clazz) { - return clazz != null && WRAPPER_TYPES.contains(clazz); + return Comparable.class.isAssignableFrom(clazz); } public static boolean isSimpleType(final Class clazz) { - if (clazz != null) + if (clazz == null) { - if (isPrimitiveType(clazz)) - { - return true; - } - else if (isPrimitiveWrapperType(clazz)) - { - return true; - } - else if (clazz.isEnum()) - { - return true; - } - else if (CharSequence.class.isAssignableFrom(clazz)) - { - return true; - } - else if (Number.class.isAssignableFrom(clazz)) - { - return true; - } - else if (Date.class.isAssignableFrom(clazz)) - { - return true; - } - else if (URI.class.equals(clazz)) - { - return true; - } - else if (URL.class.equals(clazz)) - { - return true; - } - else if (Locale.class.equals(clazz)) + return false; + } + else if (isPrimitiveType(clazz)) + { + return true; + } + else if (isPrimitiveWrapperType(clazz)) + { + return true; + } + for (final Class type : FINAL_SIMPLE_TYPES) + { + if (type.equals(clazz)) { return true; } - else if (Class.class.equals(clazz)) + } + for (final Class type : EXTENDABLE_SIMPLE_TYPES) + { + if (type.isAssignableFrom(clazz)) { return true; } @@ -127,9 +157,14 @@ else if (Class.class.equals(clazz)) return false; } - public static boolean isComparableType(final Class clazz) + public static boolean isPrimitiveType(final Class clazz) + { + return clazz != null && clazz.isPrimitive(); + } + + public static boolean isPrimitiveWrapperType(final Class clazz) { - return BigDecimal.class.equals(clazz); + return clazz != null && PRIMITIVE_WRAPPER_TYPES.contains(clazz); } public static T freshInstanceOf(final Class clazz) @@ -143,7 +178,7 @@ public static T freshInstanceOf(final Class clazz) { constructor = clazz.getDeclaredConstructor(); } - catch (NoSuchMethodException e) + catch (final NoSuchMethodException e) { logger.debug("Missing default constructor for type {}. Assuming standard default values " + "for primitive properties.", clazz.getName()); @@ -155,7 +190,7 @@ public static T freshInstanceOf(final Class clazz) constructor.setAccessible(true); return constructor.newInstance(); } - catch (Exception e) + catch (final Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/de/danielbechler/util/Collections.java b/src/main/java/de/danielbechler/util/Collections.java index b34666fe..a926178e 100644 --- a/src/main/java/de/danielbechler/util/Collections.java +++ b/src/main/java/de/danielbechler/util/Collections.java @@ -25,10 +25,10 @@ private Collections() { } - public static Set setOf(final T... c) - { - return setOf(Arrays.asList(c)); - } +// public static Set setOf(final T... c) +// { +// return setOf(Arrays.asList(c)); +// } public static Set setOf(final Collection c) { @@ -59,31 +59,31 @@ public static boolean containsAny(final Iterable haystack, final Iterable return false; } - public static T get(final Iterable haystack, final T needle) - { - for (final T t : haystack) - { - if (t.equals(needle)) - { - return t; - } - } - return null; - } - - public static int indexOf(final Iterable haystack, final T needle) - { - int index = 0; - for (final T item : haystack) - { - if (item.equals(needle)) - { - return index; - } - index++; - } - return -1; - } +// public static T get(final Iterable haystack, final T needle) +// { +// for (final T t : haystack) +// { +// if (t.equals(needle)) +// { +// return t; +// } +// } +// return null; +// } +// +// public static int indexOf(final Iterable haystack, final T needle) +// { +// int index = 0; +// for (final T item : haystack) +// { +// if (item.equals(needle)) +// { +// return index; +// } +// index++; +// } +// return -1; +// } public static Collection filteredCopyOf(final Collection source, final Collection filter) @@ -104,13 +104,13 @@ public static Collection filteredCopyOf(final Collection Collection maskedCopyOf(final Collection source, - final Collection mask) - { - final Collection copy = new LinkedList(source); - copy.retainAll(new ArrayList(mask)); - return copy; - } +// public static Collection maskedCopyOf(final Collection source, +// final Collection mask) +// { +// final Collection copy = new LinkedList(source); +// copy.retainAll(new ArrayList(mask)); +// return copy; +// } public static T firstElementOf(final Collection items) { @@ -121,29 +121,29 @@ public static T firstElementOf(final Collection items) return null; } - public static T lastElementOf(final List items) - { - if (items != null && !items.isEmpty()) - { - return items.get(items.size() - 1); - } - return null; - } - - public static T lastElementOf(final Collection items) - { - if (items != null && !items.isEmpty()) - { - final Iterator iterator = items.iterator(); - while (iterator.hasNext()) - { - final T t = iterator.next(); - if (!iterator.hasNext()) - { - return t; - } - } - } - return null; - } +// public static T lastElementOf(final List items) +// { +// if (items != null && !items.isEmpty()) +// { +// return items.get(items.size() - 1); +// } +// return null; +// } +// +// public static T lastElementOf(final Collection items) +// { +// if (items != null && !items.isEmpty()) +// { +// final Iterator iterator = items.iterator(); +// while (iterator.hasNext()) +// { +// final T t = iterator.next(); +// if (!iterator.hasNext()) +// { +// return t; +// } +// } +// } +// return null; +// } } diff --git a/src/main/java/de/danielbechler/util/Comparables.java b/src/main/java/de/danielbechler/util/Comparables.java index 04e25185..2dfd852c 100644 --- a/src/main/java/de/danielbechler/util/Comparables.java +++ b/src/main/java/de/danielbechler/util/Comparables.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,7 +16,10 @@ package de.danielbechler.util; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ +@SuppressWarnings("ClassUnconnectedToPackage") public class Comparables { private Comparables() @@ -31,7 +34,13 @@ public static > boolean isEqualByComparison(final T a, f } else if (a != null && b != null) { - return a.compareTo(b) == 0; + // When testing the comparison of java.util.Date and java.sql.Timestamp I noticed, + // that they were never considered equal, because java.util.Date was used as the + // working object. This way only its compareTo method was used, but never the one + // of java.sql.Date, although the subclass added some magic to make the objects + // compatible. To remedy this, this method tests both objects against each other and + // returns true when one of the comparisons returns true. + return a.compareTo(b) == 0 || b.compareTo(a) == 0; } return false; } diff --git a/src/main/java/de/danielbechler/util/Exceptions.java b/src/main/java/de/danielbechler/util/Exceptions.java index fe8c71d2..f2e3571b 100644 --- a/src/main/java/de/danielbechler/util/Exceptions.java +++ b/src/main/java/de/danielbechler/util/Exceptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,7 +16,10 @@ package de.danielbechler.util; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ +@SuppressWarnings("ClassUnconnectedToPackage") public class Exceptions { private Exceptions() diff --git a/src/main/java/de/danielbechler/util/Objects.java b/src/main/java/de/danielbechler/util/Objects.java index cbd50588..b39935e0 100644 --- a/src/main/java/de/danielbechler/util/Objects.java +++ b/src/main/java/de/danielbechler/util/Objects.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,7 +16,10 @@ package de.danielbechler.util; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ +@SuppressWarnings("ClassUnconnectedToPackage") public class Objects { private Objects() @@ -29,7 +32,7 @@ public static boolean isEqual(final Object a, final Object b) { return a.equals(b); } - else if (b != null) + if (b != null) { return b.equals(a); } diff --git a/src/test/java/de/danielbechler/diff/CategoryServiceTest.groovy b/src/test/java/de/danielbechler/diff/CategoryServiceTest.groovy deleted file mode 100644 index 69a626d8..00000000 --- a/src/test/java/de/danielbechler/diff/CategoryServiceTest.groovy +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright 2013 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 - -import de.danielbechler.diff.bean.NamedPropertyElement -import spock.lang.Specification - -import static DiffNode.ROOT - -/** - * @author Daniel Bechler - */ -class CategoryServiceTest extends Specification -{ - def categoryService = new CategoryService() - def accessor = Mock(PropertyAwareAccessor) - def nodePath = NodePath.buildWith("foo") - def nodeType = Alliance - def DiffNode node - def DiffNode rootNode - - def "setup"() - { - accessor.pathElement >> new NamedPropertyElement("foo") - rootNode = new DiffNode(ROOT, RootAccessor.instance, null) - node = new DiffNode(rootNode, accessor, nodeType) - } - - def "resolveCategories: should return categories configured via path"() - { - given: - categoryService.ofNode(nodePath).toBe("Stark", "Lannister") - - expect: - categoryService.resolveCategories(node) == ["Stark", "Lannister"] as Set - } - - def "resolveCategories: should return categories configured via type"() - { - given: - categoryService.ofType(nodeType).toBe("Stark", "Lannister") - - expect: - categoryService.resolveCategories(node) == ["Stark", "Lannister"] as Set - } - - def "resolveCategories: should return categories configured via node"() - { - given: - accessor.categories >> ["Stark", "Lannister"] - - expect: - categoryService.resolveCategories(node) == ["Stark", "Lannister"] as Set - } - - def "resolveCategories: should return combined categories configured via all possible options"() - { - given: - categoryService.ofNode(nodePath).toBe("A") - - and: - categoryService.ofType(nodeType).toBe("B") - - and: - accessor.categories >> ["C"] - - expect: - categoryService.resolveCategories(node) == ["A", "B", "C"] as Set - } - - def "resolveCategories: should not return categories of parent nodes"() - { - given: - accessor.categories >> ["B"] - - and: - categoryService.ofNode(NodePath.buildRootPath()).toBe("A") - - expect: - categoryService.resolveCategories(node) == ["B"] as Set - } - - def "resolveCategories: should return empty Set if no category is defined"() - { - given: - node = new DiffNode() - - expect: - categoryService.resolveCategories(node) == [] as Set - } - - class Alliance - { - } -} diff --git a/src/test/java/de/danielbechler/diff/CircularReferenceDetectorTest.java b/src/test/java/de/danielbechler/diff/CircularReferenceDetectorTest.java deleted file mode 100644 index b5fd41c6..00000000 --- a/src/test/java/de/danielbechler/diff/CircularReferenceDetectorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.diff.mock.*; -import org.fest.assertions.api.*; -import org.testng.annotations.*; - -import static org.fest.assertions.api.Assertions.assertThat; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.*; -import static org.testng.Assert.*; - -/** @author Daniel Bechler */ -public class CircularReferenceDetectorTest -{ - private CircularReferenceDetector circularReferenceDetector; - - @BeforeMethod - public void setUp() - { - circularReferenceDetector = new CircularReferenceDetector(CircularReferenceDetector.ReferenceMatchingMode.EQUALITY_OPERATOR); - } - - @Test - public void testPush_does_nothing_with_null_object() throws Exception - { - circularReferenceDetector.push(null, null); - assertThat(circularReferenceDetector.size(), is(0)); - } - - @Test - public void testPush_adds_unknown_object_to_stack() throws Exception - { - circularReferenceDetector.push("foo", NodePath.buildRootPath()); - assertThat(circularReferenceDetector.size(), is(1)); - } - - @Test - public void testPush_throws_CircularReferenceException_on_known_object() throws Exception - { - circularReferenceDetector.push("foo", NodePath.buildRootPath()); - try - { - circularReferenceDetector.push("foo", NodePath.createBuilder() - .withRoot() - .withPropertyName("test") - .build()); - Assertions.fail("Expected CircularReferenceException wasn't thrown."); - } - catch (CircularReferenceDetector.CircularReferenceException e) - { - final NodePath nodePath = e.getNodePath(); - assertThat(nodePath).isEqualTo(NodePath.buildRootPath()); - } - } - - @Test - public void testRemove_does_nothing_with_null_object() throws Exception - { - assertThat(circularReferenceDetector.size(), is(0)); - circularReferenceDetector.remove(null); - assertThat(circularReferenceDetector.size(), is(0)); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testRemove_throws_IllegalArgumentException_when_trying_to_remove_any_instance_other_than_the_last_pushed_one() throws Exception - { - circularReferenceDetector.push("foo", null); - circularReferenceDetector.remove("bar"); - } - - @Test - public void testRemove_removes_instance_when_it_was_the_last_one_pushed() throws Exception - { - circularReferenceDetector.push("foo", null); - assertThat(circularReferenceDetector.size(), is(1)); - circularReferenceDetector.remove("foo"); - assertThat(circularReferenceDetector.size(), is(0)); - } - - @Test - public void testKnows_returns_true_for_previously_added_instance() - { - circularReferenceDetector.push("foo", null); - assertTrue(circularReferenceDetector.knows("foo")); - } - - @Test - public void testKnows_returns_false_if_instance_has_not_been_pushed() - { - assertFalse(circularReferenceDetector.knows("foo")); - } - - @Test - public void testKnows_returns_false_if_instance_has_been_pushed_but_later_removed() - { - circularReferenceDetector.push("foo", null); - circularReferenceDetector.remove("foo"); - assertFalse(circularReferenceDetector.knows("foo")); - } - - @Test - public void knowsEqualObject_whenReferenceMatchingModeIsSetToEqualsMethod() - { - circularReferenceDetector = new CircularReferenceDetector(CircularReferenceDetector.ReferenceMatchingMode.EQUALS_METHOD); - circularReferenceDetector.push(new ObjectWithString("foo"), NodePath.buildRootPath()); - assertThat(circularReferenceDetector.knows(new ObjectWithString("foo"))).isTrue(); - } - - @Test - public void removesEqualObject_whenReferenceMatchingModeIsSetToEqualsMethod() - { - circularReferenceDetector = new CircularReferenceDetector(CircularReferenceDetector.ReferenceMatchingMode.EQUALS_METHOD); - circularReferenceDetector.push(new ObjectWithString("foo"), NodePath.buildRootPath()); - circularReferenceDetector.remove(new ObjectWithString("foo")); - assertThat(circularReferenceDetector.size()).isEqualTo(0); - } - - @Test(expectedExceptions = CircularReferenceDetector.CircularReferenceException.class) - public void throwsException_onAttemptToPushEqualObject_whenReferenceMatchingModeIsSetToEqualsMethod() - { - circularReferenceDetector = new CircularReferenceDetector(CircularReferenceDetector.ReferenceMatchingMode.EQUALS_METHOD); - circularReferenceDetector.push(new ObjectWithString("foo"), NodePath.buildRootPath()); - circularReferenceDetector.push(new ObjectWithString("foo"), NodePath.buildRootPath()); - } -} diff --git a/src/test/java/de/danielbechler/diff/DefaultDifferProviderShould.java b/src/test/java/de/danielbechler/diff/DefaultDifferProviderShould.java deleted file mode 100644 index b830af90..00000000 --- a/src/test/java/de/danielbechler/diff/DefaultDifferProviderShould.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2012 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; - -import org.fest.assertions.api.*; -import org.fest.assertions.core.*; -import org.testng.annotations.*; - -import java.util.*; - -import static org.mockito.Mockito.*; - -/** @author Daniel Bechler */ -public class DefaultDifferProviderShould -{ - private DifferProvider differProvider; - - @BeforeMethod - public void initDifferProvider() - { - differProvider = new DifferProvider(); - } - - @Test - public void return_differ_that_accepts_given_type() - { - final Differ differ = given_differ_accepting_type(String.class); - - final Differ retrievedDiffer = differProvider.retrieveDifferForType(String.class); - - Assertions.assertThat(retrievedDiffer).is(sameAs(differ)); - } - - @Test - public void return_the_last_pushed_differ_that_accepts_the_given_type() - { - final Differ differ = given_differ_accepting_type(String.class); - final Differ differ2 = given_differ_accepting_type(String.class); - - final Differ retrievedDiffer = differProvider.retrieveDifferForType(String.class); - - Assertions.assertThat(retrievedDiffer).is(sameAs(differ2)); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void throw_IllegalArgumentException_if_no_type_is_given() - { - differProvider.retrieveDifferForType(null); - } - - @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = "Couldn't find a differ for type: java.util.Date") - public void throw_IllegalStateException_if_differ_accepts_the_given_type() - { - given_differ_accepting_type(String.class); - - differProvider.retrieveDifferForType(Date.class); - } - - private Differ given_differ_accepting_type(final Class type) - { - final Differ differ = mock(Differ.class); - when(differ.accepts(type)).thenReturn(true); - differProvider.push(differ); - return differ; - } - - private static Condition sameAs(final Differ retrievedDiffer) - { - return new Condition("same as " + retrievedDiffer) - { - @Override - public boolean matches(final Differ differ) - { - return differ == retrievedDiffer; - } - }; - } -} diff --git a/src/test/java/de/danielbechler/diff/DefaultNodeQueryServiceTest.groovy b/src/test/java/de/danielbechler/diff/DefaultNodeQueryServiceTest.groovy new file mode 100644 index 00000000..6e5a810a --- /dev/null +++ b/src/test/java/de/danielbechler/diff/DefaultNodeQueryServiceTest.groovy @@ -0,0 +1,111 @@ +/* + * Copyright 2016 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 + +import de.danielbechler.diff.category.CategoryResolver +import de.danielbechler.diff.comparison.ComparisonStrategy +import de.danielbechler.diff.comparison.ComparisonStrategyResolver +import de.danielbechler.diff.comparison.PrimitiveDefaultValueMode +import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver +import de.danielbechler.diff.filtering.IsReturnableResolver +import de.danielbechler.diff.inclusion.IsIgnoredResolver +import de.danielbechler.diff.introspection.IsIntrospectableResolver +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Unroll + +class DefaultNodeQueryServiceTest extends Specification { + + DefaultNodeQueryService nodeQueryService + + def node = DiffNode.newRootNode() + def categoryResolver = Mock(CategoryResolver) + def introspectableResolver = Mock(IsIntrospectableResolver) + def ignoredResolver = Mock(IsIgnoredResolver) + def returnableResolver = Mock(IsReturnableResolver) + def comparisonStrategyResolver = Mock(ComparisonStrategyResolver) + def primitiveDefaultValueModeResolver = Mock(PrimitiveDefaultValueModeResolver) + + def setup() { + nodeQueryService = new DefaultNodeQueryService( + categoryResolver, + introspectableResolver, + ignoredResolver, + returnableResolver, + comparisonStrategyResolver, + primitiveDefaultValueModeResolver) + } + + def "resolveCategories delegates to CategoryResolver"() { + def categories = ['foo'] + setup: + 1 * categoryResolver.resolveCategories(node) >> categories + expect: + with(nodeQueryService.resolveCategories(node)) { + assert it.containsAll(categories) + assert it.size() == categories.size() + } + } + + @Unroll + def "isIntrospectable delegates to IsIntrospectableResolver (#introspectable)"() { + setup: + 1 * introspectableResolver.isIntrospectable(node) >> introspectable + expect: + nodeQueryService.isIntrospectable(node) == introspectable + where: + introspectable << [true, false] + } + + @Unroll + def "isIgnored delegates to IsIgnoredResolver (#ignored)"() { + setup: + 1 * ignoredResolver.isIgnored(node) >> ignored + expect: + nodeQueryService.isIgnored(node) == ignored + where: + ignored << [true, false] + } + + @Unroll + def "isReturnable delegates to IsReturnableResolver (#returnable)"() { + setup: + 1 * returnableResolver.isReturnable(node) >> returnable + expect: + nodeQueryService.isReturnable(node) == returnable + where: + returnable << [true, false] + } + + def "resolveComparisonStrategy delegates to ComparisonStrategyResolver"() { + def comparisonStrategy = Mock(ComparisonStrategy) + setup: + 1 * comparisonStrategyResolver.resolveComparisonStrategy(node) >> comparisonStrategy + expect: + nodeQueryService.resolveComparisonStrategy(node) is(comparisonStrategy) + } + + @Unroll + def "resolvePrimitiveDefaultValueMode delegates to PrimitiveDefaultValueModeResolver (#valueMode)"() { + setup: + 1 * primitiveDefaultValueModeResolver.resolvePrimitiveDefaultValueMode(node) >> valueMode + expect: + nodeQueryService.resolvePrimitiveDefaultValueMode(node) == valueMode + where: + valueMode << PrimitiveDefaultValueMode.values() + } +} diff --git a/src/test/java/de/danielbechler/diff/DiffNodeTest.java b/src/test/java/de/danielbechler/diff/DiffNodeTest.java deleted file mode 100644 index d65e6d95..00000000 --- a/src/test/java/de/danielbechler/diff/DiffNodeTest.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.diff.bean.*; -import de.danielbechler.diff.collection.*; -import de.danielbechler.diff.helper.*; -import de.danielbechler.diff.mock.*; -import org.fest.assertions.api.*; -import org.hamcrest.core.*; -import org.testng.annotations.*; - -import java.lang.annotation.*; -import java.util.*; - -import static org.fest.assertions.api.Assertions.assertThat; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.*; - -/** @author Daniel Bechler */ -public class DiffNodeTest -{ - @Mock - private Accessor accessor; - - @BeforeMethod - public void setUp() - { - initMocks(this); - } - - @Test - public void testHasChanges_returns_false_when_untouched() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.UNTOUCHED); - assertThat(node.hasChanges(), Is.is(false)); - } - - @Test - public void testHasChanges_returns_false_when_ignored() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.IGNORED); - assertThat(node.hasChanges(), Is.is(false)); - } - - @Test - public void testHasChanges_returns_false_when_circular() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CIRCULAR); - assertThat(node.hasChanges(), Is.is(false)); - } - - @Test - public void testHasChanges_returns_true_when_changed() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - assertThat(node.hasChanges(), Is.is(true)); - } - - @Test - public void testHasChanges_returns_true_when_removed() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.REMOVED); - assertThat(node.hasChanges(), Is.is(true)); - } - - @Test - public void testHasChanges_returns_true_when_added() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.ADDED); - assertThat(node.hasChanges(), Is.is(true)); - } - - @Test - public void testHasChanges_returns_true_when_child_has_changed() - { - final DiffNode root = new DiffNode(List.class); - final DiffNode child = new DiffNode(root, new CollectionItemAccessor("foo"), String.class); - root.addChild(child); - child.setState(DiffNode.State.ADDED); - assertThat(root.hasChanges(), Is.is(true)); - } - - @Test - public void testGetPropertyPath_with_parent_node_should_return_canonical_path() - { - final DiffNode parentNode = new DiffNode(RootAccessor.getInstance(), String.class); - when(accessor.getPathElement()).thenReturn(new NamedPropertyElement("foo")); - - final DiffNode root = new DiffNode(parentNode, accessor, Object.class); - - assertThat(root.getPath()).isEqualTo(NodePath.buildWith("foo")); - } - - @Test - public void testGetPropertyPath_without_parent_node_should_return_root_path() - { - final DiffNode root = new DiffNode(Object.class); - assertThat(root.getPath()).isEqualTo(NodePath - .createBuilder().withRoot().build()); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testAddChild_throws_exception_when_root_node_is_passed() throws Exception - { - final DiffNode root = new DiffNode(Object.class); - root.addChild(new DiffNode(Object.class)); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testAddChild_throws_exception_when_passed_node_is_already_child_of_another_node() throws Exception - { - final DiffNode node1 = new DiffNode(Object.class); - final DiffNode node2 = new DiffNode(Object.class); - final DiffNode node3 = new DiffNode(accessor, Object.class); - node1.addChild(node3); - node2.addChild(node3); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testAddChild_throws_exception_when_node_is_added_to_itself() throws Exception - { - final DiffNode node = new DiffNode(null, accessor, Object.class); - node.addChild(node); - } - - @Test - public void testAddChild_establishes_parent_child_relationship() throws Exception - { - final DiffNode node1 = new DiffNode(List.class); - final DiffNode node2 = new DiffNode(new CollectionItemAccessor("foo"), String.class); - node1.addChild(node2); - assertThat(node1.getChildren()).containsOnly(node2); - assertThat(node2.getParentNode()).isSameAs(node1); - } - - @Test - public void testAddChild_changes_node_state_to_changed_if_changed_child_node_gets_added() - { - final DiffNode node = new DiffNode(Object.class); - final DiffNode childNode = new DiffNode(node, accessor, String.class); - childNode.setState(DiffNode.State.CHANGED); - - node.addChild(childNode); - - NodeAssertions.assertThat(node).root().hasState(DiffNode.State.CHANGED); - } - - @Test - public void testShould_return_property_annotations_of_property_accessor() throws Exception - { - final BeanPropertyAccessor propertyAccessor = mock(BeanPropertyAccessor.class); - final Annotation annotation = mock(Annotation.class); - when(propertyAccessor.getReadMethodAnnotations()).thenReturn(new LinkedHashSet(Arrays.asList(annotation))); - final DiffNode node = new DiffNode(propertyAccessor, Object.class); - - final Set annotations = node.getPropertyAnnotations(); - - Assertions.assertThat(annotations).containsAll(Arrays.asList(annotation)); - } - - @Test - public void testShould_return_empty_set_of_property_annotations_if_accessor_is_not_property_accessor() throws Exception - { - final BeanPropertyAccessor propertyAccessor = mock(BeanPropertyAccessor.class); - final Annotation annotation = mock(Annotation.class); - when(propertyAccessor.getReadMethodAnnotations()).thenReturn(new LinkedHashSet(Arrays.asList(annotation))); - final DiffNode node = new DiffNode(propertyAccessor, Object.class); - - final Set annotations = node.getPropertyAnnotations(); - - Assertions.assertThat(annotations).containsAll(Arrays.asList(annotation)); - } - - @Test - public void test_get_property_annotation_should_delegate_call_to_property_accessor() - { - final BeanPropertyAccessor propertyAccessor = mock(BeanPropertyAccessor.class); - when(propertyAccessor.getReadMethodAnnotation(ObjectDiffTest.class)).thenReturn(null); - - new DiffNode(propertyAccessor, Object.class).getPropertyAnnotation(ObjectDiffTest.class); - - verify(propertyAccessor, times(1)).getReadMethodAnnotation(ObjectDiffTest.class); - } - - @Test - public void test_get_property_annotation_should_return_null_if_accessor_is_not_property_accessor() - { - final Accessor propertyAccessor = mock(Accessor.class); - - final ObjectDiffTest annotation = new DiffNode(propertyAccessor, Object.class).getPropertyAnnotation(ObjectDiffTest.class); - - assertThat(annotation).isNull(); - } -} diff --git a/src/test/java/de/danielbechler/diff/DifferAcceptTypeDataProvider.java b/src/test/java/de/danielbechler/diff/DifferAcceptTypeDataProvider.java deleted file mode 100644 index 953094a7..00000000 --- a/src/test/java/de/danielbechler/diff/DifferAcceptTypeDataProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2013 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; - -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import java.util.*; - -/** @author Daniel Bechler */ -public class DifferAcceptTypeDataProvider -{ - @DataProvider - public static Object[][] collectionTypes() - { - return new Object[][] { - new Object[] {Collection.class}, - new Object[] {List.class}, - new Object[] {Queue.class}, - new Object[] {Set.class}, - new Object[] {ArrayList.class}, - new Object[] {LinkedList.class}, - }; - } - - @DataProvider - public static Object[][] mapTypes() - { - return new Object[][] { - new Object[] {Map.class}, - new Object[] {HashMap.class}, - new Object[] {TreeMap.class}, - }; - } - - @DataProvider - public static Object[][] beanTypes() - { - return new Object[][] { - new Object[] {Object.class}, - new Object[] {ObjectWithString.class}, - new Object[] {Date.class}, - }; - } - - @DataProvider - public Object[][] primitiveTypes() - { - return new Object[][] { - new Object[] {int.class}, - new Object[] {short.class}, - new Object[] {char.class}, - new Object[] {long.class}, - new Object[] {boolean.class}, - new Object[] {byte.class}, - new Object[] {float.class}, - new Object[] {double.class}, - }; - } -} diff --git a/src/test/java/de/danielbechler/diff/DifferDispatcherShould.java b/src/test/java/de/danielbechler/diff/DifferDispatcherShould.java deleted file mode 100644 index fd1d35d0..00000000 --- a/src/test/java/de/danielbechler/diff/DifferDispatcherShould.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2013 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; - -import de.danielbechler.diff.mock.*; -import org.mockito.Mock; -import org.mockito.invocation.*; -import org.mockito.stubbing.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.CircularReferenceDetector.*; -import static org.fest.assertions.api.Assertions.*; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.*; - -/** @author Daniel Bechler */ -public class DifferDispatcherShould -{ - private DifferDispatcher differDispatcher; - - @Mock - private DifferProvider differProvider; - @Mock - private Accessor accessor; - @Mock - private CircularReferenceDetector circularReferenceDetector; - @Mock - private CircularReferenceDetectorFactory circularReferenceDetectorFactory; - @Mock - private CircularReferenceExceptionHandler circularReferenceExceptionHandler; - @Mock - private IsIgnoredResolver ignoredResolver; - @Mock - private IsReturnableResolver returnableResolver; - @Mock - private Instances instances; - @Mock - private Instances accessedInstances; - - @BeforeMethod - public void setUp() throws Exception - { - initMocks(this); - - when(circularReferenceDetectorFactory.createCircularReferenceDetector()).thenReturn(circularReferenceDetector); - when(instances.access(any(Accessor.class))).thenReturn(accessedInstances); - when(accessedInstances.getSourceAccessor()).thenReturn(accessor); - - differDispatcher = new DifferDispatcher(differProvider, circularReferenceDetectorFactory, circularReferenceExceptionHandler, ignoredResolver, returnableResolver); - } - - @SuppressWarnings("unchecked") - private void given_the_delegated_node_is_circular(final NodePath circularStartPath) - { - doThrow(new CircularReferenceException(circularStartPath)).when(circularReferenceDetector) - .push(any(), any(NodePath.class)); - - when(instances.getSourceAccessor()).thenReturn(RootAccessor.getInstance()); - when(instances.getType()).then(returnType(Object.class)); - when(instances.getWorking()).thenReturn(""); - when(instances.getBase()).thenReturn(""); - when(differProvider.retrieveDifferForType(ObjectWithCircularReference.class)).thenReturn(mock(Differ.class)); - } - - @Test - public void assign_the_circular_start_path_if_the_delegated_node_is_circular() throws Exception - { - final NodePath circularStartPath = NodePath.buildRootPath(); - given_the_delegated_node_is_circular(circularStartPath); - - final DiffNode node = differDispatcher.dispatch(DiffNode.ROOT, instances, accessor); - - assertThat(node.getCircleStartPath()).isEqualTo(circularStartPath); - } - - @Test - public void mark_node_as_circular_if_the_delegated_node_is_circular() throws Exception - { - given_the_delegated_node_is_circular(NodePath.buildRootPath()); - - final DiffNode node = differDispatcher.dispatch(DiffNode.ROOT, instances, accessor); - - assertThat(node.getState()).isEqualTo(DiffNode.State.CIRCULAR); - } - - @Test - public void pass_node_to_onCircularReferenceException_method_of_the_exceptionListener_if_the_delegated_node_is_circular() throws Exception - { - given_the_delegated_node_is_circular(NodePath.buildRootPath()); - - final DiffNode node = differDispatcher.dispatch(DiffNode.ROOT, instances, accessor); - - verify(circularReferenceExceptionHandler).onCircularReferenceException(node); - } - - @Test(expectedExceptions = IllegalStateException.class) - public void throw_exception_if_no_differ_can_be_found_for_instance_type() - { - when(accessedInstances.getType()).thenAnswer(new Answer() - { - public Object answer(final InvocationOnMock invocation) throws Throwable - { - return UnsupportedType.class; - } - }); - - when(differProvider.retrieveDifferForType(eq(UnsupportedType.class))).thenReturn(null); - - differDispatcher.dispatch(DiffNode.ROOT, instances, accessor); - } - - private static Answer> returnType(final Class type) - { - return new Answer>() - { - public Class answer(final InvocationOnMock invocation) throws Throwable - { - return type; - } - }; - } - - private static class UnsupportedType { - } -} diff --git a/src/test/java/de/danielbechler/diff/InclusionServiceSpec.groovy b/src/test/java/de/danielbechler/diff/InclusionServiceSpec.groovy deleted file mode 100644 index 80a4dc45..00000000 --- a/src/test/java/de/danielbechler/diff/InclusionServiceSpec.groovy +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2013 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 - -import de.danielbechler.diff.bean.NamedPropertyElement -import spock.lang.Specification - -/** - * @author Daniel Bechler - */ -@SuppressWarnings("GroovyPointlessBoolean") -class InclusionServiceSpec extends Specification { - def categoryResolver = Mock(CategoryResolver) - def accessor = Mock(PropertyAwareAccessor) - def inclusionService = new InclusionService(categoryResolver) - def NodePath nodePath = NodePath.buildWith("foo") - def DiffNode rootNode - def DiffNode node - - def "setup"() { - accessor.pathElement >> new NamedPropertyElement("foo") - rootNode = new DiffNode(RootAccessor.instance, null) - node = new DiffNode(rootNode, accessor, null) - categoryResolver.resolveCategories(_ as DiffNode) >> [] - } - - def "construction: should fail if no categoryResolver is given"() { - when: - new InclusionService(null) - - then: - thrown(IllegalArgumentException) - } - - def "isIgnored: should return 'true' if node is marked as ignored"() { - when: - def ignored = inclusionService.isIgnored(node) - - then: - accessor.isExcluded() >> true - - and: - ignored == true - } - - def "isIgnored: should return 'false' if no include and exclude rules are defined"() { - expect: - inclusionService.isIgnored(node) == false - } - - def "isIgnored: should return 'true' if node doesn't match defined inclusion rules"() { - setup: - inclusionService.toInclude().categories("unknown-category") - - expect: - inclusionService.isIgnored(node) == true - } - - def "isIgnored: should return 'false' if node doesn't match defined inclusion rules but is root node"() { - given: - inclusionService.toInclude().categories("unknown-category") - - expect: - !inclusionService.isIgnored(rootNode) - } - - def "isIgnored: should return 'false' if node is included via path"() { - setup: - inclusionService.toInclude().nodes(nodePath) - - expect: - inclusionService.isIgnored(node) == false - } - - def "isIgnored: should return 'false' if node is included via category"() { - given: - categoryResolver.resolveCategories(node) >> ["test-category"] - - when: - def ignored = inclusionService.isIgnored(node) - - then: - inclusionService.toInclude().categories("test-category") - - and: - ignored == false - } - - def "isIgnored: should return 'false' if node is included via type"() { - setup: - node.setType(URL) - inclusionService.toInclude().types(URL) - - expect: - inclusionService.isIgnored(node) == false - } - - def "isIgnored: should return 'false' if node doesn't match exclusion rules"() { - setup: - inclusionService.toExclude().categories("unknown-category") - - expect: - inclusionService.isIgnored(node) == false - } - - def "isIgnored: should return 'true' if node is excluded via path"() { - setup: - inclusionService.toExclude().nodes(nodePath) - - expect: - inclusionService.isIgnored(node) == true - } - - def "isIgnored: should return 'true' if node is excluded via type"() { - setup: - node.setType(URL) - inclusionService.toExclude().types(URL) - - expect: - inclusionService.isIgnored(node) - } - - def "isIgnored: should return 'true' if node is excluded via category"() { - given: - inclusionService.toExclude().categories("test-category") - - when: - def ignored = inclusionService.isIgnored(node) - - then: - categoryResolver.resolveCategories(node) >> ["test-category"] - - and: - ignored == true - } - - def "isIgnored: should return 'true' when node is excluded via property name"() { - given: - def accessor = Mock(PropertyAwareAccessor) - def node = new DiffNode(accessor, null) - accessor.pathElement >> new NamedPropertyElement("foo") - - and: - inclusionService.toExclude().propertyNames("foo") - - expect: - inclusionService.isIgnored(node) == true - } - - def "isIgnored: should return 'false' when node is included via property name"() { - given: - def accessor = Mock(PropertyAwareAccessor) - def node = new DiffNode(accessor, null) - accessor.pathElement >> new NamedPropertyElement("foo") - - and: - inclusionService.toInclude().propertyNames("foo") - - expect: - inclusionService.isIgnored(node) == false - } - - def "isIgnored: should return 'false' for children of included nodes"() { - given: - inclusionService.toInclude().nodes(NodePath.buildRootPath()) - - expect: - inclusionService.isIgnored(node) == false - } - - def "isIgnored: should return 'true' for children of excluded nodes"() { - given: - inclusionService.toExclude().nodes(NodePath.buildRootPath()) - - expect: - inclusionService.isIgnored(node) == true - } -} diff --git a/src/test/java/de/danielbechler/diff/InstancesSpec.groovy b/src/test/java/de/danielbechler/diff/InstancesSpec.groovy deleted file mode 100644 index 02daccfb..00000000 --- a/src/test/java/de/danielbechler/diff/InstancesSpec.groovy +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2013 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 - -import spock.lang.Specification -import spock.lang.Unroll - -/** - * @author Daniel Bechler - */ -class InstancesSpec extends Specification -{ - def "getType() throws IllegalArgumentException if base and working have incompatible types"() - { - setup: - def instances = new Instances(RootAccessor.instance, working, base, null) - - when: - instances.getType() - - then: - thrown(IllegalArgumentException) - - where: - base | working - new StringBuilder() | "" - "foo" | 42 - true | new Date() - } - - @Unroll - def "getType() returns '#resultTypeName' for base of type '#baseClassName' and working of type '#workingClassName'"() - { - setup: - def instances = Instances.of(working, base) - - expect: - instances.getType() == resultType - - where: - base | working || resultType - new ArrayList() | new LinkedHashSet() || Collection.class - new HashMap() | new TreeMap() || Map.class - - baseClassName = base.getClass().getSimpleName() - workingClassName = working.getClass().getSimpleName() - resultTypeName = resultType.getSimpleName() - } - - @Unroll - def "getType() returns type of TypeAwareAccessor when it is primitive '#type'"() - { - given: - def typeAwareAccessor = Mock(TypeAwareAccessor); - def instances = Instances.of(typeAwareAccessor, 0, 0L); - - and: - 1 * typeAwareAccessor.getType() >>> type - - expect: - instances.getType() == type - - where: - type << [long, int, float, boolean, char, byte, short] - } - - def "getType() returns most specific shared type for non-primitive objects (except for Map and Collection)"() - { - given: - def instances = Instances.of(accessor, working, base); - - expect: - instances.getType() == resultType - - where: - accessor | working | base | resultType - RootAccessor.instance | BigInteger.ONE | BigDecimal.ONE | Number - RootAccessor.instance | new LineNumberReader(new StringReader("")) | new BufferedReader(new StringReader("")) | BufferedReader - RootAccessor.instance | new StringReader("") | new BufferedReader(new StringReader("")) | Reader - } - - def "getType() uses type of TypeAwareAccessor if no shared type for non-primitive objects could be determined"() - { - given: - def accessor = Mock(TypeAwareAccessor) - def instances = Instances.of(accessor, new StringBuilder(), ""); - - and: - accessor.getType() >>> CharSequence - - expect: - instances.getType() == CharSequence - } -} diff --git a/src/test/java/de/danielbechler/diff/InstancesTest.java b/src/test/java/de/danielbechler/diff/InstancesTest.java deleted file mode 100644 index bbb1a260..00000000 --- a/src/test/java/de/danielbechler/diff/InstancesTest.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.diff.bean.*; -import org.testng.annotations.*; - -import java.lang.reflect.*; - -import static de.danielbechler.diff.helper.MockitoExtensions.*; -import static org.fest.assertions.api.Assertions.*; -import static org.mockito.Mockito.*; - -/** @author Daniel Bechler */ -public class InstancesTest -{ - private static TypeAwareAccessor mockTypeAwareAccessorOfType(final Class clazz) - { - final TypeAwareAccessor typeAwareAccessor = mock(TypeAwareAccessor.class); - when(typeAwareAccessor.getType()).then(returnClass(clazz)); - return typeAwareAccessor; - } - - @Test - public void testAreNull_returns_true_when_base_and_working_are_null() - { - final Instances instances = new Instances(RootAccessor.getInstance(), null, null, null); - assertThat(instances.areNull()).isTrue(); - } - - @Test - public void testAreNull_returns_false_when_base_is_not_null() - { - final Instances instances = new Instances(RootAccessor.getInstance(), null, "", null); - assertThat(instances.areNull()).isFalse(); - } - - @Test - public void testAreNull_returns_false_when_working_is_not_null() - { - final Instances instances = new Instances(RootAccessor.getInstance(), "", null, null); - assertThat(instances.areNull()).isFalse(); - } - - @Test - public void testIsPrimitiveTypeReturnsTrueForPrimitiveType() - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(long.class); - assertThat(new Instances(typeAwareAccessor, 1L, 2L, 0L).isPrimitiveType()).isTrue(); - } - - @Test - public void testIsPrimitiveTypeReturnsFalseForPrimitiveWrapperType() - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(Long.class); - assertThat(new Instances(typeAwareAccessor, 1L, 2L, 0L).isPrimitiveType()).isFalse(); - } - - @Test - public void testGetFreshReturnsZeroForPrimitiveNumericTypeIfUndefined() throws Exception - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(long.class); - assertThat(new Instances(typeAwareAccessor, 0, 0, null).getFresh()).isEqualTo(0); - } - - @Test - public void testGetFreshReturnsDefinedDefaultValueForPrimitiveNumericType() throws Exception - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(long.class); - assertThat(new Instances(typeAwareAccessor, 0, 0, 1337).getFresh()).isEqualTo(1337); - } - - @Test - public void testGetFreshReturnsZeroForPrimitiveBooleanTypeIfUndefined() throws Exception - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(boolean.class); - assertThat(new Instances(typeAwareAccessor, true, true, null).getFresh()).isEqualTo(false); - } - - @Test - public void testGetFreshReturnsNullForDefaultLessPrimitiveWrapperType() throws Exception - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(Long.class); - assertThat(new Instances(typeAwareAccessor, 0L, 0L, null).getFresh()).isNull(); - } - - @Test - public void testGetFreshReturnsDefinedDefaultValueForPrimitiveBooleanType() throws Exception - { - final TypeAwareAccessor typeAwareAccessor = mockTypeAwareAccessorOfType(boolean.class); - assertThat(new Instances(typeAwareAccessor, true, true, true).getFresh()).isEqualTo(true); - } - - @Test - public void testIsPrimitiveTypeReturnsPrimitiveClassForPrimitiveType() throws Exception - { - final Method readMethod = getClass().getDeclaredMethod("getTestValue"); - final BeanPropertyAccessor accessor = new BeanPropertyAccessor("testValue", readMethod, null); - final Instances instances = new Instances(accessor, 1L, 2L, 0L); - assertThat(instances.getType() == long.class).isTrue(); - } - - @Test - public void testIsPrimitiveTypeReturnsFalseForComplexType() - { - assertThat(new Instances(RootAccessor.getInstance(), "1", "2", null).isPrimitiveType()).isFalse(); - } - - @SuppressWarnings({"MethodMayBeStatic", "UnusedDeclaration"}) - public long getTestValue() - { - return 0L; - } -} diff --git a/src/test/java/de/danielbechler/diff/IntrospectionServiceTest.groovy b/src/test/java/de/danielbechler/diff/IntrospectionServiceTest.groovy deleted file mode 100644 index 4c5c5ce0..00000000 --- a/src/test/java/de/danielbechler/diff/IntrospectionServiceTest.groovy +++ /dev/null @@ -1,187 +0,0 @@ -package de.danielbechler.diff - -import de.danielbechler.diff.mock.ObjectWithString -import spock.lang.Shared -import spock.lang.Specification -import spock.lang.Unroll - -/** - * @author Daniel Bechler - */ -class IntrospectionServiceTest extends Specification -{ - @Shared - def primitiveTypes = [null, int, short, long, boolean, char, byte, float, double, float] - - @Shared - def primitiveWrapperTypes = [Integer, Short, Long, Boolean, Character, Byte, Double, Float] - - def introspectionService = new IntrospectionService() - def rootNode = new DiffNode(DiffNode.ROOT, RootAccessor.instance, ObjectWithString) - def childNode - def childAccessor = Mock(TypeAwareAccessor) - def defaultIntrospector = Mock(Introspector) - - def "setup"() - { - introspectionService.setDefaultIntrospector(defaultIntrospector) - } - - @Unroll - def 'introspection should always be disabled for #type'() - { - given: - childAccessor.getType() >> type - childNode = new DiffNode(rootNode, childAccessor, type) - introspectionService.ofType(type).toBeEnabled() - - expect: - !introspectionService.isIntrospectable(childNode) - - where: - type << primitiveTypes + primitiveWrapperTypes - } - - def 'introspection should always be disabled for Arrays'() - { - given: - rootNode = new DiffNode(DiffNode.ROOT, RootAccessor.instance, type); - introspectionService.ofType(type).toBeEnabled() - - expect: - !introspectionService.isIntrospectable(rootNode) - - //noinspection GroovyAssignabilityCheck - where: - type << [int[], String[], Object[]] - } - - def 'introspection should always be disabled for Enums'() - { - given: - rootNode = new DiffNode(DiffNode.ROOT, RootAccessor.instance, type); - introspectionService.ofType(type).toBeEnabled() - - expect: - !introspectionService.isIntrospectable(rootNode) - - where: - type << [Inclusion, CircularReferenceMatchingMode] - } - - def 'introspection should always be disabled for nodes with unknown type (null)'() - { - given: - rootNode = new DiffNode(DiffNode.ROOT, RootAccessor.instance, null); - introspectionService.ofType(null).toBeEnabled() - - expect: - !introspectionService.isIntrospectable(rootNode) - } - - def 'introspection can be enabled via type'() - { - given: - introspectionService.ofType(ObjectWithString).toBeEnabled() - - expect: - introspectionService.isIntrospectable(rootNode) - } - - def 'introspection can be disabled via type'() - { - given: - introspectionService.ofType(ObjectWithString).toBeDisabled() - - expect: - !introspectionService.isIntrospectable(rootNode) - } - - def 'introspection can be re-enabled via type'() - { - given: - introspectionService.ofType(ObjectWithString).toBeDisabled() - introspectionService.ofType(ObjectWithString).toBeEnabled() - - expect: - introspectionService.isIntrospectable(rootNode) - } - - def 'introspection can be enabled via node'() - { - given: - introspectionService.ofNode(rootNode.path).toBeEnabled() - - expect: - introspectionService.isIntrospectable(rootNode) - } - - def 'introspection can be disabled via node'() - { - given: - introspectionService.ofNode(rootNode.path).toBeDisabled() - - expect: - !introspectionService.isIntrospectable(rootNode) - } - - def 'introspection can be re-enabled via node'() - { - given: - introspectionService.ofNode(rootNode.path).toBeDisabled() - introspectionService.ofNode(rootNode.path).toBeEnabled() - - expect: - introspectionService.isIntrospectable(rootNode) - } - - def 'node configuration overrules type configuration'() - { - given: - introspectionService.ofNode(rootNode.path).toBeDisabled() - introspectionService.ofType(ObjectWithString).toBeEnabled() - - expect: - !introspectionService.isIntrospectable(rootNode) - } - - def 'attempting to set default introspector to null should cause IllegalArgumentException'() - { - when: - introspectionService.setDefaultIntrospector(null) - - then: - thrown(IllegalArgumentException) - } - - def 'introspectorForNode returns default introspector if no special configuration exists'() - { - given: - def introspector = Mock(Introspector) - introspectionService.setDefaultIntrospector(introspector) - - expect: - introspectionService.introspectorForNode(rootNode) == introspector - } - - def 'introspectorForNode returns introspector configured via type'() - { - given: - def typeIntrospector = Mock(Introspector) - introspectionService.ofType(String).toUse(typeIntrospector) - rootNode = new DiffNode(String) - - expect: - introspectionService.introspectorForNode(rootNode) == typeIntrospector - } - - def 'introspectorForNode returns introspector configured via node path'() - { - given: - def nodeIntrospector = Mock(Introspector) - introspectionService.ofNode(NodePath.buildRootPath()).toUse(nodeIntrospector) - - expect: - introspectionService.introspectorForNode(rootNode) == nodeIntrospector - } -} diff --git a/src/test/java/de/danielbechler/diff/ListDiffSpec.groovy b/src/test/java/de/danielbechler/diff/ListDiffSpec.groovy deleted file mode 100644 index 7b768ca2..00000000 --- a/src/test/java/de/danielbechler/diff/ListDiffSpec.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2013 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 - -import de.danielbechler.diff.collection.CollectionElement -import de.danielbechler.diff.visitor.PrintingVisitor -import spock.lang.Ignore -import spock.lang.Specification - -/** - * @author Daniel Bechler - */ -class ListDiffSpec extends Specification -{ - def objectDiffer; - - def setup() - { - objectDiffer = ObjectDifferBuilder.buildDefaultObjectDiffer(); - } - - @Ignore - def "detects position switch"() - { - DiffNode node; - - when: - node = objectDiffer.compare(working, base); - - then: - node.visit(new PrintingVisitor(working, base)); - node.getChild(new CollectionElement("a")).getState() == DiffNode.State.CHANGED; - node.getChild(new CollectionElement("b")).getState() == DiffNode.State.CHANGED; - - where: - base | working - ["a", "b"] | ["b", "a"] - } -} diff --git a/src/test/java/de/danielbechler/diff/NodePathBuilderTest.java b/src/test/java/de/danielbechler/diff/NodePathBuilderTest.java deleted file mode 100644 index 47da7fe7..00000000 --- a/src/test/java/de/danielbechler/diff/NodePathBuilderTest.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2012 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; - -import de.danielbechler.diff.bean.*; -import de.danielbechler.diff.collection.*; -import de.danielbechler.diff.map.*; -import org.testng.annotations.*; - -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class NodePathBuilderTest -{ - @Test - public void testWithRoot() - { - final NodePath nodePath = NodePath.createBuilder() - .withRoot() - .build(); - assertThat(nodePath.getElements()).containsOnly(RootElement.getInstance()); - } - - @Test - public void testWithElement() - { - final CollectionElement element = new CollectionElement("foo"); - final NodePath nodePath = NodePath.createBuilder() - .withRoot() - .withElement(element) - .build(); - assertThat(nodePath.getElements()).containsSequence( - RootElement.getInstance(), - element); - } - - @Test - public void testWithPropertyName() - { - final NodePath nodePath = NodePath.createBuilder() - .withRoot() - .withPropertyName("foo", "bar") - .build(); - assertThat(nodePath.getElements()).containsSequence( - RootElement.getInstance(), - new NamedPropertyElement("foo"), - new NamedPropertyElement("bar") - ); - } - - @Test - public void testWithMapKey() - { - final NodePath nodePath = NodePath.createBuilder() - .withRoot() - .withMapKey("foo") - .build(); - assertThat(nodePath.getElements()).containsSequence(RootElement.getInstance(), new MapElement("foo")); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testWithMapKey_throws_exception_when_key_is_null() - { - NodePath.createBuilder().withRoot().withMapKey(null).build(); - } - - @Test - public void testWithCollectionItem() - { - final NodePath nodePath = NodePath.createBuilder() - .withRoot() - .withCollectionItem("foo") - .build(); - assertThat(nodePath.getElements()).containsSequence(RootElement.getInstance(), new CollectionElement("foo")); - } - - @Test - public void testWithPropertyPath() - { - final NodePath nodePath = NodePath.createBuilder() - .withPropertyPath(NodePath - .buildWith("foo")) - .build(); - assertThat(nodePath.getElements()).containsSequence(RootElement.getInstance(), new NamedPropertyElement("foo")); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testWithPropertyPath_throws_exception_when_property_path_is_null() - { - NodePath.createBuilder().withPropertyPath(null).build(); - } - - @Test - public void testBuild_with_one_root_element_should_succeed() throws Exception - { - final NodePath nodePath = NodePath.createBuilder().withRoot().build(); - assertThat(nodePath.getElements()).containsOnly(RootElement.getInstance()); - } -} diff --git a/src/test/java/de/danielbechler/diff/NodePathValueHolderTest.groovy b/src/test/java/de/danielbechler/diff/NodePathValueHolderTest.groovy deleted file mode 100644 index 95cd1208..00000000 --- a/src/test/java/de/danielbechler/diff/NodePathValueHolderTest.groovy +++ /dev/null @@ -1,54 +0,0 @@ -package de.danielbechler.diff - -import spock.lang.Specification -import spock.lang.Unroll - -/** - * @author Daniel Bechler - */ -class NodePathValueHolderTest extends Specification -{ - def "should allow creation of new instances"() - { - expect: - NodePathValueHolder.of(String) instanceof NodePathValueHolder - } - - @Unroll - def "should store value '#value' at node path '#nodePath'"() - { - given: - def valueHolder = NodePathValueHolder.of(String) - valueHolder.put(nodePath, value); - - expect: - valueHolder.valueForNodePath(nodePath) == value; - - where: - nodePath | value - NodePath.buildRootPath() | "foo1" - NodePath.buildWith("a") | "foo2" - NodePath.buildWith("a", "b") | "foo3" - } - - def "should return null for unknown paths"() - { - given: - def valueHolder = NodePathValueHolder.of(String) - - expect: - valueHolder.valueForNodePath(NodePath.buildWith("a")) == null - } - - def "should return accumulated values along path"() - { - given: - def valueHolder = NodePathValueHolder.of(String) - valueHolder.put(NodePath.buildRootPath(), "foo1") - valueHolder.put(NodePath.buildWith("a"), "foo2") - valueHolder.put(NodePath.buildWith("a", "b"), "foo3") - - expect: - valueHolder.accumulatedValuesForNodePath(NodePath.buildWith("a", "b")) == ["foo1", "foo2", "foo3"] - } -} diff --git a/src/test/java/de/danielbechler/diff/ObjectDifferBuilderTest.groovy b/src/test/java/de/danielbechler/diff/ObjectDifferBuilderTest.groovy new file mode 100644 index 00000000..ad079617 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/ObjectDifferBuilderTest.groovy @@ -0,0 +1,108 @@ +/* + * Copyright 2016 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 + +import de.danielbechler.diff.category.CategoryConfigurer +import de.danielbechler.diff.circular.CircularReferenceConfigurer +import de.danielbechler.diff.comparison.ComparisonConfigurer +import de.danielbechler.diff.differ.Differ +import de.danielbechler.diff.differ.DifferConfigurer +import de.danielbechler.diff.differ.DifferFactory +import de.danielbechler.diff.filtering.FilteringConfigurer +import de.danielbechler.diff.identity.IdentityConfigurer +import de.danielbechler.diff.inclusion.InclusionConfigurer +import de.danielbechler.diff.introspection.IntrospectionConfigurer +import spock.lang.Specification + +class ObjectDifferBuilderTest extends Specification { + + def 'startBuilding returns ObjectDifferBuilder instance'() { + expect: + ObjectDifferBuilder.startBuilding() instanceof ObjectDifferBuilder + } + + def 'startBuilding always returns a new ObjectDifferBuilder instance'() { + expect: + !ObjectDifferBuilder.startBuilding().is(ObjectDifferBuilder.startBuilding()) + } + + def 'buildDefault returns ObjectDiffer instance'() { + expect: + ObjectDifferBuilder.buildDefault() instanceof ObjectDiffer + } + + def 'buildDefault always returns a new ObjectDiffer instance'() { + expect: + !ObjectDifferBuilder.buildDefault().is(ObjectDifferBuilder.buildDefault()) + } + + def 'categories returns CategoryConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().categories() instanceof CategoryConfigurer + } + + def 'circularReferenceHandling returns CircularReferenceConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().circularReferenceHandling() instanceof CircularReferenceConfigurer + } + + def 'comparison returns ComparisonConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().comparison() instanceof ComparisonConfigurer + } + + def 'differs returns DifferConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().differs() instanceof DifferConfigurer + } + + def 'filtering returns FilteringConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().filtering() instanceof FilteringConfigurer + } + + def 'identity returns IdentityConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().identity() instanceof IdentityConfigurer + } + + def 'inclusion returns InclusionConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().inclusion() instanceof InclusionConfigurer + } + + def 'introspection returns IntrospectionConfigurer'() { + expect: + ObjectDifferBuilder.startBuilding().introspection() instanceof IntrospectionConfigurer + } + + def 'build returns new ObjectDiffer instance'() { + expect: + ObjectDifferBuilder.startBuilding().build() instanceof ObjectDiffer + } + + def 'build invokes all registered DifferFactories'() { + def builder = ObjectDifferBuilder.startBuilding() + def differFactory = Mock(DifferFactory) + given: + builder.differs().register(differFactory) + when: + builder.build() + then: + 1 * differFactory.createDiffer(_, _) >> Stub(Differ) + } +} diff --git a/src/test/java/de/danielbechler/diff/RootAccessorTest.java b/src/test/java/de/danielbechler/diff/RootAccessorTest.java deleted file mode 100644 index c9c69c81..00000000 --- a/src/test/java/de/danielbechler/diff/RootAccessorTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2012 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; - -import org.testng.annotations.*; - -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.core.Is.*; -import static org.hamcrest.core.IsEqual.*; -import static org.hamcrest.core.IsInstanceOf.*; -import static org.hamcrest.core.IsSame.*; - -/** @author Daniel Bechler */ -public class RootAccessorTest -{ - private Object target; - - @BeforeMethod - public void setUpTargetObject() - { - target = new Object(); - } - - @Test - public void get_should_return_target_object() - { - assertThat(RootAccessor.getInstance().get(target), sameInstance(target)); - } - - @Test(expectedExceptions = UnsupportedOperationException.class) - public void set_should_not_be_supported() - { - RootAccessor.getInstance().set(target, new Object()); - } - - @Test(expectedExceptions = UnsupportedOperationException.class) - public void unset_should_not_be_supported() - { - RootAccessor.getInstance().unset(target); - } - - @Test - public void getPathElement_should_return_RootElement() - { - assertThat(RootAccessor.getInstance().getPathElement(), is(instanceOf(RootElement.class))); - } - - @Test - public void toString_should_return_human_readable_string() - { - assertThat(RootAccessor.getInstance().toString(), is(equalTo("root element"))); - } -} diff --git a/src/test/java/de/danielbechler/diff/RootElementTest.groovy b/src/test/java/de/danielbechler/diff/RootElementTest.groovy deleted file mode 100644 index 3595efa2..00000000 --- a/src/test/java/de/danielbechler/diff/RootElementTest.groovy +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2013 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 - -import spock.lang.Specification - -/** - * @author Daniel Bechler - */ -class RootElementTest extends Specification -{ - def 'equals should be true'() - { - expect: - RootElement.instance.equals(instance) - - where: - instance << [RootElement.instance, new RootElement()] - } - - def 'equals should be false'() - { - expect: - !RootElement.instance.equals(null) - } - - def 'toHumanReadableString should always return empty string'() - { - expect: - RootElement.instance.toHumanReadableString() == '' - } - - def 'getInstance should always return the same instance'() - { - expect: - RootElement.instance == RootElement.instance - } - - def 'hashCode should always be 0'() - { - expect: - RootElement.instance.hashCode() == 0 - } -} diff --git a/src/test/java/de/danielbechler/diff/access/CollectionItemAccessorTest.groovy b/src/test/java/de/danielbechler/diff/access/CollectionItemAccessorTest.groovy new file mode 100644 index 00000000..371ded8d --- /dev/null +++ b/src/test/java/de/danielbechler/diff/access/CollectionItemAccessorTest.groovy @@ -0,0 +1,198 @@ +/* + * Copyright 2012 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.access + +import de.danielbechler.diff.mock.ObjectWithIdentityAndValue +import de.danielbechler.diff.selector.CollectionItemElementSelector +import spock.lang.Specification +import spock.lang.Unroll + +public class CollectionItemAccessorTest extends Specification { + + private static final def ANY_REFERENCE_ITEM = new Object() + + def "construction: should work with reference item"() { + when: + new CollectionItemAccessor(new Object()) != null + then: + notThrown(Throwable) + } + + def "construction: should work with 'null' reference item"() { + when: + new CollectionItemAccessor(null) + then: + notThrown(Throwable) + } + + def "get: should return item equal to reference item from collection"() { + given: + def accessor = new CollectionItemAccessor(new ObjectWithIdentityAndValue("1")) + and: + def collection = [ + new ObjectWithIdentityAndValue("1", "foo") + ] + when: + ObjectWithIdentityAndValue item = accessor.get(collection) as ObjectWithIdentityAndValue + then: + item.id == '1' + item.value == 'foo' + } + + def "get: should return null when no item in the collection matches the reference item"() { + given: + def referenceItemForNonExistingItem = new ObjectWithIdentityAndValue("1") + def accessor = new CollectionItemAccessor(referenceItemForNonExistingItem) + expect: + accessor.get([]) == null + } + + def "get: should return null when reference item is null"() { + given: + def referenceItem = null + def accessor = new CollectionItemAccessor(referenceItem) + expect: + accessor.get(['some-item']) == null + } + + def 'get: should fail with exception if target object is not a collection'() { + given: + def accessor = new CollectionItemAccessor(ANY_REFERENCE_ITEM) + def notACollection = new Object() + when: + accessor.get(notACollection) + then: + thrown(IllegalArgumentException) + } + + def 'set: should fail with exception if target object is not a collection'() { + given: + def accessor = new CollectionItemAccessor(ANY_REFERENCE_ITEM) + def notACollection = new Object() + when: + accessor.get(notACollection) + then: + thrown(IllegalArgumentException) + } + + def 'set: should insert non-existing item into collection'() throws Exception { + given: + def collection = [] + def accessor = new CollectionItemAccessor(new ObjectWithIdentityAndValue("foo")) + when: + accessor.set(collection, new ObjectWithIdentityAndValue("foo", 'bar')) + then: + collection.find { it.id == 'foo' && it.value == 'bar' } != null + } + + def 'set: should do nothing if target object is null'() { + given: + def accessor = new CollectionItemAccessor('foo') + when: + accessor.set(null, 'bar'); + then: + notThrown(Throwable) + } + + @Unroll + def 'set: should replace matching item in collection (#collectionType)'() { + given: + def collection = collectionType.newInstance() + def collectionItem = new ObjectWithIdentityAndValue("foo", 'bar') + collection.add(collectionItem) + and: + def referenceItem = new ObjectWithIdentityAndValue("foo") + def accessor = new CollectionItemAccessor(referenceItem) + when: + accessor.set(collection, 'new value') + then: + collection.size() == 1 + collection.contains 'new value' + where: + collectionType << [ArrayList, HashSet] + } + + @Unroll + def 'unset: should remove matching item from collection (#collectionType)'() { + given: + def collection = collectionType.newInstance() + def collectionItem = new ObjectWithIdentityAndValue("foo", 'bar') + collection.add(collectionItem) + and: + def referenceItem = new ObjectWithIdentityAndValue("foo") + def accessor = new CollectionItemAccessor(referenceItem) + when: + accessor.unset collection + then: + collection.isEmpty() + where: + collectionType << [ArrayList, HashSet] + } + + @Unroll + def 'unset: should not fail if reference item is not in collection (#collectionType)'() { + given: + def emptyCollection = collectionType.newInstance() + and: + def referenceItem = new ObjectWithIdentityAndValue("foo") + def accessor = new CollectionItemAccessor(referenceItem) + when: + accessor.unset emptyCollection + then: + emptyCollection.isEmpty() + where: + collectionType << [ArrayList, HashSet] + } + + def 'unset: should fail with exception if target object is not a collection'() { + given: + def accessor = new CollectionItemAccessor(ANY_REFERENCE_ITEM) + def notACollection = new Object() + when: + accessor.unset(notACollection); + then: + thrown(IllegalArgumentException) + } + + @Unroll + def 'getType: should return type of reference item (#expectedType)'() { + given: + def accessor = new CollectionItemAccessor(referenceItem) + expect: + accessor.type == expectedType + where: + referenceItem || expectedType + null || null + 'foo' || String + new Date() || Date + } + + def 'toString: should return text starting with "collection item"'() { + given: + def accessor = new CollectionItemAccessor(ANY_REFERENCE_ITEM) + expect: + accessor.toString() =~ /^collection\sitem/ + } + + def 'getElementSelector: should return proper element selector'() { + given: + def referenceItem = 'foo' + def accessor = new CollectionItemAccessor(referenceItem) + expect: + accessor.elementSelector == new CollectionItemElementSelector(referenceItem) + } +} diff --git a/src/test/java/de/danielbechler/diff/access/InstancesTest.groovy b/src/test/java/de/danielbechler/diff/access/InstancesTest.groovy new file mode 100644 index 00000000..ffcf45f5 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/access/InstancesTest.groovy @@ -0,0 +1,284 @@ +/* + * 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.access + +import spock.lang.Specification +import spock.lang.Unroll + +class InstancesTest extends Specification { + + def "getType: returns Object.class if base and working have different types"() { + setup: + def instances = new Instances(RootAccessor.instance, working, base, null) + + expect: + instances.getType() == Object.class + + where: + base | working + new StringBuilder() | "" + "foo" | 42 + true | new Date() + } + + @Unroll + def "getType: returns '#resultTypeName' for base of type '#baseClassName' and working of type '#workingClassName'"() { + setup: + def instances = Instances.of(working, base) + + expect: + instances.getType() == resultType + + where: + base | working || resultType + new ArrayList() | new ArrayList() || ArrayList.class + new ArrayList() | new LinkedHashSet() || Collection.class + new HashMap() | new TreeMap() || Map.class + + baseClassName = base.getClass().getSimpleName() + workingClassName = working.getClass().getSimpleName() + resultTypeName = resultType.getSimpleName() + } + + @Unroll + def "getType: returns type of TypeAwareAccessor when it is primitive '#type'"() { + given: + def typeAwareAccessor = Mock(TypeAwareAccessor); + def instances = Instances.of(typeAwareAccessor, 0, 0L); + + and: + 1 * typeAwareAccessor.getType() >>> type + + expect: + instances.getType() == type + + where: + type << [long, int, float, boolean, char, byte, short] + } + + def "getType: returns most specific shared type for non-primitive objects (except for Map and Collection)"() { + given: + def instances = Instances.of(propertyAccessor, working, base); + + expect: + instances.getType() == resultType + + where: + propertyAccessor | working | base | resultType + RootAccessor.instance | BigInteger.ONE | BigDecimal.ONE | Number + RootAccessor.instance | new LineNumberReader(new StringReader("")) | new BufferedReader(new StringReader("")) | BufferedReader + RootAccessor.instance | new StringReader("") | new BufferedReader(new StringReader("")) | Reader + } + + def "getType: uses type of TypeAwareAccessor if no shared type for non-primitive objects could be determined"() { + given: + def accessor = Mock(TypeAwareAccessor) + def instances = Instances.of(accessor, new StringBuilder(), ""); + + and: + accessor.getType() >>> CharSequence + + expect: + instances.getType() == CharSequence + } + + def 'areNull: returns true when base and working are null'() { + given: + Instances instances = new Instances(RootAccessor.instance, null, null, null); + expect: + instances.areNull() + } + + def 'areNull: returns false when base is not null'() { + given: + Instances instances = new Instances(RootAccessor.instance, null, '', null); + expect: + !instances.areNull() + } + + def 'areNull: returns false when working is not null'() { + given: + Instances instances = new Instances(RootAccessor.instance, '', null, null); + expect: + !instances.areNull() + } + + @Unroll + def 'isPrimitiveType: returns true for primitive type (#type)'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> type + and: + def instances = new Instances(typeAwareAccessor, null, null, null) + expect: + instances.isPrimitiveType() + where: + type << [boolean, short, int, char, long, double, float, byte] + } + + @Unroll + def 'isPrimitiveType: returns returns false for primitive wrapper type (#type)'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> type + and: + def instances = new Instances(typeAwareAccessor, null, null, null) + expect: + !instances.isPrimitiveType() + where: + type << [Boolean, Short, Integer, Character, Long, Double, Float, Byte] + } + + @Unroll + def 'isPrimitiveType: returns returns false for complex type'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> type + and: + def instances = new Instances(typeAwareAccessor, null, null, null) + expect: + !instances.isPrimitiveType() + where: + type << [String, Object, Date] + } + + def 'getFresh: returns zero for numeric types without custom initialization'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> type + and: + def instances = new Instances(typeAwareAccessor, null, null, null) + expect: + instances.getFresh() == 0 + where: + type << [short, int, char, long, double, float, byte] + } + + def 'getFresh: returns false for primitive boolean type without custom initialization'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> boolean + and: + def instances = new Instances(typeAwareAccessor, null, null, null) + expect: + instances.getFresh() == false + } + + def 'getFresh: returns fresh value from constructor for non-primitive types'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> Object + and: + def instances = new Instances(typeAwareAccessor, null, null, 'foo') + expect: + instances.getFresh() == 'foo' + } + + @Unroll + def 'getFresh: returns null for primitive wrapper types (#type)'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> type + and: + def instances = Instances.of(typeAwareAccessor, null, null) + expect: + instances.getFresh() == null + where: + type << [Boolean, Short, Integer, Character, Long, Double, Float, Byte] + } + + def 'areSame: should return true when working and base are the same object'() { + given: + def object = new Object() + def instances = new Instances(RootAccessor.instance, object, object, null) + expect: + instances.areSame() + } + + def 'areSame: should return false when working and base are not the same object'() { + given: + def instances = new Instances(RootAccessor.instance, new Object(), new Object(), null) + expect: + !instances.areSame() + } + + @Unroll('hasBeenAdded: should return #result when [working: #working, base: #base, fresh: #fresh]') + def 'hasBeenAdded'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> typeFromAccessor + and: + def instances = new Instances(typeAwareAccessor, working, base, fresh) + expect: + instances.hasBeenAdded() == result + where: + working | base | fresh | typeFromAccessor || result + null | null | null | null || false + 'foo' | null | null | null || true + 'foo' | 'foo' | null | null || false + null | 'foo' | null | null || false + 2 | 0 | 0 | int || true + 2 | 1 | 1 | int || true + 1 | 1 | 1 | int || false + 2 | 0 | 1 | int || false + } + + @Unroll('hasBeenRemoved: should return #result when [working: #working, base: #base, fresh: #fresh]') + def 'hasBeenRemoved'() { + given: + def typeAwareAccessor = Stub TypeAwareAccessor + typeAwareAccessor.type >> typeFromAccessor + and: + def instances = Instances.of(typeAwareAccessor, working, base, fresh) + expect: + instances.hasBeenRemoved() == result + where: + working | base | fresh | typeFromAccessor || result + null | null | null | null || false + 'foo' | null | null | null || false + 'foo' | 'foo' | null | null || false + null | 'foo' | null | null || true + 0 | 0 | 0 | int || false + 1 | 1 | 1 | int || false + 0 | 1 | 1 | int || false + 1 | 2 | 1 | int || true + } + + def 'access: fails with IllegalArgumentException when accessor is null'() { + when: + new Instances(Mock(Accessor), 'a', 'b', 'c').access(null) + then: + thrown IllegalArgumentException + } + + def 'access: returns new instance created by using the given accessor'() { + given: + def instances = new Instances(Mock(Accessor), 'working', 'base', 'fresh') + def accessor = Stub Accessor, { + get('working') >> 'working2' + get('base') >> 'base2' + get('fresh') >> 'fresh2' + } + when: + def accessedInstances = instances.access(accessor) + then: + accessedInstances.working == 'working2' + accessedInstances.base == 'base2' + accessedInstances.fresh == 'fresh2' + accessedInstances.sourceAccessor.is accessor + } +} diff --git a/src/test/java/de/danielbechler/diff/access/MapEntryAccessorTest.groovy b/src/test/java/de/danielbechler/diff/access/MapEntryAccessorTest.groovy new file mode 100644 index 00000000..f0a8a2ba --- /dev/null +++ b/src/test/java/de/danielbechler/diff/access/MapEntryAccessorTest.groovy @@ -0,0 +1,188 @@ +/* + * Copyright 2012 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.access + +import de.danielbechler.diff.mock.ObjectWithIdentityAndValue +import de.danielbechler.diff.selector.MapKeyElementSelector +import spock.lang.Shared +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +class MapEntryAccessorTest extends Specification { + + @Shared + def key1 = new ObjectWithIdentityAndValue("key", "1") + @Shared + def key2 = new ObjectWithIdentityAndValue("key", "2") + def accessor = new MapEntryAccessor("b"); + + def "fail on construction with null referenceKey"() { + when: + new MapEntryAccessor(null); + + then: + thrown(IllegalArgumentException) + } + + def "provide access to its path element"() { + expect: + accessor.getElementSelector() instanceof MapKeyElementSelector + } + + def "provide write access to referenced value in any map"() { + given: + def map = [] as TreeMap + + when: + accessor.set(map, "foo") + + then: + map.get("b") == "foo" + + when: + accessor.set(map, "bar") + + then: + map.get("b") == "bar" + } + + def "provide read access to referenced value in any map"() throws Exception { + given: + def map = [] as TreeMap + + when: + map.put("b", "foo"); + + then: + accessor.get(map) == "foo" + } + + def "throw exception when trying to read from non map object"() { + when: + accessor.get(new Object()) + + then: + thrown(IllegalArgumentException) + } + + def "return null when reading from null object"() { + expect: + accessor.get(null) == null + } + + def "remove referenced entry from any map"() { + given: + def map = [] as TreeMap + + when: + map.put("b", "foo") + + and: + accessor.unset(map) + + then: + map.isEmpty() + } + + def "return the key object of the given map"() { + given: + accessor = new MapEntryAccessor(referenceKey) + + and: + def map = [] as HashMap + map.put actualMapKey, "foo" + + expect: + accessor.getKey(map).is actualMapKey + + where: + referenceKey | actualMapKey + key1 | key2 + key1 | key1 + } + + def "return null as the key object if the target object is null"() { + expect: + accessor.getKey(null) == null + } + + def "return null as the key object if the given map does not contain it"() { + setup: + def map = [] as HashMap + map.put("d", "whatever value") + + expect: + accessor.getKey(map) == null + } + + def "toString should make clear it's a map accessor"() { + expect: + accessor.toString() == "map key {b}" + } + + def "unset should be null-safe"() { + when: + accessor.unset null + + then: + notThrown Throwable + } + + def "set should be null-safe"() { + when: + accessor.set null, 'foo' + + then: + notThrown Throwable + } + + def "get should be null-safe"() { + when: + accessor.get null + + then: + notThrown Throwable + } + + def "equals should work as expected"() { + given: + def accessor = new MapEntryAccessor("a") + + expect: + accessor.equals(otherAccessor) == expectedResult + + and: "make sure the same instance is always equal" + accessor.equals(accessor) + + where: + otherAccessor || expectedResult + null || false + "foo" || false + new MapEntryAccessor("b") || false + new MapEntryAccessor("a") || true + } + + def "hashCode should equal hashCode of referenceKey"() { + setup: + def referenceKey = "foo" + + expect: + new MapEntryAccessor(referenceKey).hashCode() == referenceKey.hashCode() + } +} diff --git a/src/test/java/de/danielbechler/diff/access/RootAccessorTest.groovy b/src/test/java/de/danielbechler/diff/access/RootAccessorTest.groovy new file mode 100644 index 00000000..10068332 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/access/RootAccessorTest.groovy @@ -0,0 +1,58 @@ +/* + * 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.access + +import de.danielbechler.diff.selector.RootElementSelector +import spock.lang.Specification + +class RootAccessorTest extends Specification { + + def 'get: returns target object'() { + given: + def target = new Object() + expect: + RootAccessor.instance.get(target).is target + } + + def 'set: fails with UnsupportedOperationException'() { + given: + def target = new Object() + when: + RootAccessor.instance.set(target, 'anything') + then: + thrown UnsupportedOperationException + } + + def 'unset: fails with UnsupportedOperationException'() { + given: + def target = new Object() + when: + RootAccessor.instance.unset target + then: + thrown UnsupportedOperationException + } + + def 'getPathElement: returns RootElementSelector'() { + expect: + RootAccessor.instance.elementSelector instanceof RootElementSelector + } + + def 'toString: returns "root element"'() { + expect: + RootAccessor.instance.toString() == "root element" + } +} diff --git a/src/test/java/de/danielbechler/diff/bean/BeanDifferShould.java b/src/test/java/de/danielbechler/diff/bean/BeanDifferShould.java deleted file mode 100644 index 8fb04fad..00000000 --- a/src/test/java/de/danielbechler/diff/bean/BeanDifferShould.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.*; -import org.fest.assertions.api.*; -import org.mockito.Mock; -import org.mockito.invocation.*; -import org.mockito.stubbing.*; -import org.testng.annotations.*; - -import java.util.*; - -import static de.danielbechler.diff.NodePath.*; -import static de.danielbechler.diff.helper.NodeAssertions.assertThat; -import static de.danielbechler.diff.helper.NodeMatchers.*; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.*; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.*; - -/** @author Daniel Bechler */ -public class BeanDifferShould -{ - private BeanDiffer beanDiffer; - - @Mock - private DifferDispatcher differDispatcher; - @Mock - private Introspector introspector; - @Mock - private IntrospectorResolver introspectorResolver; - @Mock - private Instances instances; - @Mock - private IsIntrospectableResolver introspectableResolver; - @Mock - private IsReturnableResolver returnableResolver; - @Mock - private ComparisonStrategyResolver comparisonStrategyResolver; - @Mock - private ComparisonStrategy comparisonStrategy; - - @BeforeMethod - public void setUp() - { - initMocks(this); - - when(instances.getSourceAccessor()).thenReturn(RootAccessor.getInstance()); - when(instances.getType()).then(new Answer() - { - public Object answer(final InvocationOnMock invocation) throws Throwable - { - return String.class; - } - }); - when(introspectorResolver.introspectorForNode(any(DiffNode.class))).thenReturn(introspector); - when(introspector.introspect(any(Class.class))).thenReturn(Collections.emptyList()); - - beanDiffer = new BeanDiffer(differDispatcher, introspectableResolver, returnableResolver, comparisonStrategyResolver, introspectorResolver); - } - - @DataProvider - public static Object[][] acceptableTypes() - { - return new Object[][] { - new Class[] {String.class}, - new Class[] {Object.class}, - new Class[] {Number.class}, - new Class[] {Date.class}, - }; - } - - @DataProvider - public static Object[][] rejectableTypes() - { - return new Object[][] { - new Class[] {int.class}, - new Class[] {int[].class}, - new Class[] {boolean.class} - }; - } - - @Test(dataProvider = "acceptableTypes") - public void accept_all_object_types(final Class type) - { - Assertions.assertThat(beanDiffer.accepts(type)).isTrue(); - } - - @Test(dataProvider = "rejectableTypes") - public void reject_primitive_types(final Class type) - { - Assertions.assertThat(beanDiffer.accepts(type)).isFalse(); - } - - @Test - public void return_untouched_node_if_working_and_base_are_null() - { - when(instances.areNull()).thenReturn(true); - - final DiffNode node = beanDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).self().isUntouched(); - } - - @Test - public void return_added_node_if_working_is_not_null_and_base_is() - { - when(instances.hasBeenAdded()).thenReturn(true); - - final DiffNode node = beanDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node.getState(), is(DiffNode.State.ADDED)); - } - - @Test - public void return_removed_node_if_working_is_null_and_base_is_not() - { - when(instances.hasBeenRemoved()).thenReturn(true); - - final DiffNode node = beanDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node.getState(), is(DiffNode.State.REMOVED)); - } - - @Test - public void return_untouched_node_if_working_and_base_are_the_same_instance() - { - when(instances.areNull()).thenReturn(true); - - final DiffNode node = beanDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).self().isUntouched(); - } - - @Test - public void compare_via_comparisonStrategy_if_present() - { - when(comparisonStrategyResolver.resolveComparisonStrategy(node(buildRootPath()))).thenReturn(comparisonStrategy); - - beanDiffer.compare(DiffNode.ROOT, instances); - - verify(comparisonStrategy).compare(node(buildRootPath()), same(instances)); - } - - @Test - public void compare_via_introspector_if_introspectable() - { - when(introspectableResolver.isIntrospectable(node(buildRootPath()))).thenReturn(true); - - beanDiffer.compare(DiffNode.ROOT, instances); - - verify(introspector).introspect(instances.getType()); - } - - @Test - public void delegate_comparison_of_properties_when_comparing_via_introspector() - { - given_root_node_is_introspectable(); - final PropertyAwareAccessor propertyAccessor = given_introspector_returns_PropertyAccessor("foo"); - final DiffNode propertyNode = given_DifferDispatcher_returns_Node_for_PropertyAccessor(propertyAccessor); - given_Node_is_returnable(propertyNode); - - final DiffNode node = beanDiffer.compare(DiffNode.ROOT, instances); - - Assertions.assertThat(node.getChild("foo")).isSameAs(propertyNode); - } - - @Test - public void do_not_add_property_nodes_to_bean_node_if_they_are_not_returnable() - { - given_root_node_is_introspectable(); - final PropertyAwareAccessor propertyAccessor = given_introspector_returns_PropertyAccessor("foo"); - final DiffNode propertyNode = given_DifferDispatcher_returns_Node_for_PropertyAccessor(propertyAccessor); - given_Node_is_not_returnable(propertyNode); - - final DiffNode node = beanDiffer.compare(DiffNode.ROOT, instances); - - Assertions.assertThat(node.getChildren()).isEmpty(); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_construction_without_DifferDispatcher() - { - new BeanDiffer(null, introspectableResolver, returnableResolver, comparisonStrategyResolver, introspectorResolver); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_construction_without_IntrospectorResolver() - { - new BeanDiffer(differDispatcher, introspectableResolver, returnableResolver, comparisonStrategyResolver, null); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_construction_without_introspectableResolver() - { - new BeanDiffer(differDispatcher, null, returnableResolver, comparisonStrategyResolver, introspectorResolver); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_construction_without_returnableResolver() - { - new BeanDiffer(differDispatcher, introspectableResolver, null, comparisonStrategyResolver, introspectorResolver); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_construction_without_comparisonStrategyResolver() - { - new BeanDiffer(differDispatcher, introspectableResolver, returnableResolver, null, introspectorResolver); - } - - private void given_Node_is_returnable(final DiffNode node) - { - doReturn(true).when(returnableResolver).isReturnable(node); - } - - private void given_Node_is_not_returnable(final DiffNode node) - { - doReturn(false).when(returnableResolver).isReturnable(node); - } - - private DiffNode given_DifferDispatcher_returns_Node_for_PropertyAccessor(final PropertyAwareAccessor propertyAccessor) - { - final DiffNode propertyNode = new DiffNode(propertyAccessor, String.class); - doReturn(propertyNode).when(differDispatcher) - .dispatch(any(DiffNode.class), any(Instances.class), same(propertyAccessor)); - return propertyNode; - } - - private void given_root_node_is_introspectable() - { - doReturn(true).when(introspectableResolver).isIntrospectable(node(buildRootPath())); - } - - private PropertyAwareAccessor given_introspector_returns_PropertyAccessor(final String propertyName) - { - final NamedPropertyElement propertyElement = new NamedPropertyElement(propertyName); - final PropertyAwareAccessor propertyAccessor = mock(PropertyAwareAccessor.class); - when(propertyAccessor.getPathElement()).thenReturn(propertyElement); - final Set propertyAccessors = Collections.singleton(propertyAccessor); - when(introspector.introspect(any(Class.class))).thenReturn(propertyAccessors); - return propertyAccessor; - } -} diff --git a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorForCollectionsShould.java b/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorForCollectionsShould.java deleted file mode 100644 index 74c26cca..00000000 --- a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorForCollectionsShould.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import java.lang.reflect.*; -import java.util.*; - -import static java.util.Collections.*; -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class BeanPropertyAccessorForCollectionsShould -{ - private BeanPropertyAccessor accessor; - - private ObjectWithCollection target; - private List targetCollection; - - @BeforeMethod - public void setUp() throws NoSuchMethodException - { - targetCollection = new ArrayList(); - target = new ObjectWithCollection(); - target.setCollection(targetCollection); - accessor = createCollectionPropertyAccessor(false); - } - - private static BeanPropertyAccessor createCollectionPropertyAccessor(final boolean readOnly) throws NoSuchMethodException - { - final Class aClass = ObjectWithCollection.class; - final Method readMethod = aClass.getMethod("getCollection"); - final Method writeMethod; - if (readOnly) - { - writeMethod = null; - } - else - { - writeMethod = aClass.getMethod("setCollection", Collection.class); - } - return new BeanPropertyAccessor("collection", readMethod, writeMethod); - } - - @Test - public void replace_content_of_mutable_target_collection_if_no_setter_is_available() throws Exception - { - final List beforeTargetCollection = targetCollection; - targetCollection.add("bar"); - accessor = createCollectionPropertyAccessor(true); - - accessor.set(target, singletonList("foo")); - - assertThat(targetCollection).containsOnly("foo"); - assertThat(targetCollection).isSameAs(beforeTargetCollection); - } - - @Test - public void assign_nothing_if_target_collection_is_null_and_no_write_method_is_available() throws Exception - { - targetCollection = null; - target.setCollection(targetCollection); - accessor = createCollectionPropertyAccessor(true); - - accessor.set(target, singletonList("foo")); - - assertThat(target.getCollection()).isNull(); - } - - @Test - public void assign_nothing_if_target_collection_is_immutable_and_no_write_method_is_available() throws Exception - { - targetCollection = emptyList(); - target.setCollection(targetCollection); - accessor = createCollectionPropertyAccessor(true); - - accessor.set(target, singletonList("foo")); - - assertThat(target.getCollection()).isEmpty(); - } -} diff --git a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorForMapsShould.java b/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorForMapsShould.java deleted file mode 100644 index 99b54994..00000000 --- a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorForMapsShould.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import java.lang.reflect.*; -import java.util.*; - -import static java.util.Collections.*; -import static org.fest.assertions.api.Assertions.*; -import static org.fest.assertions.data.MapEntry.entry; - -/** @author Daniel Bechler */ -public class BeanPropertyAccessorForMapsShould -{ - private BeanPropertyAccessor accessor; - private Map targetMap; - private ObjectWithMap target; - - @BeforeMethod - public void setUp() throws NoSuchMethodException - { - targetMap = new LinkedHashMap(); - target = new ObjectWithMap(); - target.setMap(targetMap); - accessor = createMapPropertyAccessor(true); - } - - private static BeanPropertyAccessor createMapPropertyAccessor(final boolean readOnly) throws NoSuchMethodException - { - final Method readMethod = ObjectWithMap.class.getMethod("getMap"); - final Method writeMethod; - if (readOnly) - { - writeMethod = null; - } - else - { - writeMethod = ObjectWithMap.class.getMethod("setMap", Map.class); - } - return new BeanPropertyAccessor("map", readMethod, writeMethod); - } - - @Test - public void assign_by_replacing_existing_content_if_no_write_method_is_available() throws Exception - { - accessor.set(target, singletonMap("foo", "bar")); - - assertThat(targetMap).contains(entry("foo", "bar")); - } - - @Test - public void assign_nothing_if_target_map_is_immutable() throws Exception - { - targetMap = Collections.emptyMap(); - target.setMap(targetMap); - - accessor.set(target, singletonMap("foo", "bar")); - - assertThat(targetMap).isEmpty(); - } - - @Test - public void assign_nothing_if_target_map_is_null_and_no_write_method_is_available() throws Exception - { - target.setMap(null); - - accessor.set(target, singletonMap("foo", "bar")); - - assertThat(target.getMap()).isNull(); - } -} diff --git a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorShould.java b/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorShould.java deleted file mode 100644 index 283fc9aa..00000000 --- a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorShould.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.annotation.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import java.lang.annotation.*; -import java.lang.reflect.*; - -import static java.util.Arrays.*; -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class BeanPropertyAccessorShould -{ - private BeanPropertyAccessor accessor; - private ObjectWithHashCodeAndEquals item; -// private Method readMethod; - - @BeforeMethod - public void setUp() throws Exception - { - accessor = BeanPropertyAccessorBuilder.forPropertyOf(ObjectWithHashCodeAndEquals.class) - .property("value", String.class) - .readOnly(false) - .build(); -// readMethod = ObjectWithHashCodeAndEquals.class.getMethod("getValue"); -// final Method writeMethod = ObjectWithHashCodeAndEquals.class.getMethod("setValue", String.class); -// accessor = new PropertyAccessor("value", readMethod, writeMethod); - item = new ObjectWithHashCodeAndEquals("foo"); - } - - @Test - public void assign_new_value_if_property_is_writable() throws Exception - { - assertThat(item.getValue()).isNull(); - - accessor.set(item, "bar"); - - assertThat(item.getValue()).isEqualTo("bar"); - } - - @Test(expectedExceptions = BeanPropertyWriteException.class) - public void fail_on_set_if_no_setter_exists() throws NoSuchMethodException - { - final ObjectWithStringAndUnsupportedWriteMethod target = new ObjectWithStringAndUnsupportedWriteMethod("foo"); - final Method readMethod = target.getClass().getMethod("getValue"); - final Method writeMethod = target.getClass().getMethod("setValue", String.class); - accessor = new BeanPropertyAccessor("value", readMethod, writeMethod); - accessor.set(target, "bar"); - } - - @Test - public void assign_nothing_if_no_write_method_is_available() throws Exception - { - accessor = BeanPropertyAccessorBuilder.forPropertyOf(ObjectWithHashCodeAndEquals.class) - .property("value", String.class) - .readOnly(true) - .build(); - - accessor.set(item, "bar"); - - assertThat(item.getValue()).isNull(); - } - - @Test - public void retrieve_correct_value() throws Exception - { - item.setValue("bar"); - - final String value = (String) accessor.get(item); - - assertThat(value).isEqualTo("bar"); - } - - @Test - public void retrieve_null_if_target_is_null() - { - assertThat(accessor.get(null)).isNull(); - } - - @Test - public void assign_nothing_if_target_is_null() throws Exception - { - accessor.set(null, "bar"); // just to make sure no exception is thrown - } - - @Test(expectedExceptions = BeanPropertyReadException.class) - public void fail_if_target_does_not_have_expected_read_method() throws NoSuchMethodException - { - accessor.get(new Object()); - } - - @Test(expectedExceptions = BeanPropertyWriteException.class) - public void fail_if_target_does_not_have_expected_write_method() throws NoSuchMethodException - { - accessor.set(new Object(), "foo"); - } - - @Test - public void unset_property_value() throws Exception - { - item.setValue("bar"); - - accessor.unset(item); - - assertThat(item.getValue()).isNull(); - } - - @SuppressWarnings({"unchecked"}) - @Test - public void return_property_value_type() throws Exception - { - assertThat((Class) accessor.getType()).isEqualTo(String.class); - } - - @Test - public void return_proper_path_element() throws Exception - { - assertThat(accessor.getPathElement()).isEqualTo(new NamedPropertyElement("value")); - } - - @Test - public void returns_proper_property_name() throws Exception - { - assertThat(accessor.getPropertyName()).isEqualTo("value"); - } - - @Test - public void includes_accessor_type_in_string_representation() - { - assertThat(accessor.toString()).startsWith("property "); - } - - @Test - public void returns_annotations_of_property_getter() throws Exception - { - accessor = BeanPropertyAccessorBuilder.forPropertyOf(ObjectWithAnnotatedProperty.class) - .property("value", String.class) - .readOnly(false) - .build(); - final Annotation[] expectedAnnotations = ObjectWithAnnotatedProperty.class.getMethod("getValue") - .getAnnotations(); - assertThat(expectedAnnotations).hasSize(2); - assertThat(accessor.getReadMethodAnnotations()).containsAll(asList(expectedAnnotations)); - } - - @Test - public void returns_specific_annotation_of_property_getter() throws Exception - { - accessor = BeanPropertyAccessorBuilder.forPropertyOf(ObjectWithAnnotatedProperty.class) - .property("value", String.class) - .readOnly(false) - .build(); - final ObjectDiffProperty expectedAnnotation = ObjectWithAnnotatedProperty.class.getMethod("getValue") - .getAnnotation(ObjectDiffProperty.class); - assertThat(expectedAnnotation).isNotNull(); - final ObjectDiffProperty annotation = accessor.getReadMethodAnnotation(ObjectDiffProperty.class); - assertThat(annotation).isEqualTo(expectedAnnotation); - } - - @Test - public void returns_null_if_specific_annotation_of_property_getter_does_not_exist() throws Exception - { - accessor = BeanPropertyAccessorBuilder.forPropertyOf(ObjectWithAnnotatedProperty.class) - .property("value", String.class) - .readOnly(false) - .build(); - assertThat(accessor.getReadMethodAnnotation(Override.class)).isNull(); - } -} diff --git a/src/test/java/de/danielbechler/diff/bean/StandardBeanIntrospectorTest.java b/src/test/java/de/danielbechler/diff/bean/StandardBeanIntrospectorTest.java deleted file mode 100644 index ed066e7c..00000000 --- a/src/test/java/de/danielbechler/diff/bean/StandardBeanIntrospectorTest.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright 2012 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.bean; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.annotation.*; -import de.danielbechler.diff.mock.*; -import de.danielbechler.util.*; -import org.hamcrest.core.*; -import org.testng.annotations.*; - -import java.beans.*; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.Is.is; -import static org.hamcrest.core.IsInstanceOf.instanceOf; - -/** - * @author Daniel Bechler - */ -public class StandardBeanIntrospectorTest -{ - private StandardBeanIntrospector introspector; - - @BeforeMethod - public void setUp() throws Exception - { - introspector = new StandardBeanIntrospector(); - } - - @Test - public void testIntrospectWithEqualsOnlyPropertyType() throws Exception - { - final Iterable accessors = introspector.introspect(ObjectWithEqualsOnlyPropertyType.class); - assertThat(accessors.iterator().hasNext(), is(true)); - final PropertyAwareAccessor propertyAwareAccessor = accessors.iterator().next(); - assertThat(propertyAwareAccessor.getComparisonStrategy(), instanceOf(EqualsOnlyComparisonStrategy.class)); - } - - @Test - public void testIntrospectWithEqualsOnlyPropertyTypeAndValueProviderMethod() throws Exception - { - final Object object = new Object() - { - public ObjectWithObjectDiffEqualsOnlyTypeAnnotationAndValueProviderMethod getValue() - { - return null; - } - }; - - final Iterable accessors = introspector.introspect(object.getClass()); - assertThat(accessors.iterator().hasNext(), is(true)); - - final PropertyAwareAccessor propertyAwareAccessor = accessors.iterator().next(); - - final ComparisonStrategy comparisonStrategy = propertyAwareAccessor.getComparisonStrategy(); - assertThat(comparisonStrategy, is(instanceOf(EqualsOnlyComparisonStrategy.class))); - - final EqualsOnlyComparisonStrategy equalsOnlyComparisonStrategy = (EqualsOnlyComparisonStrategy) comparisonStrategy; - assertThat(equalsOnlyComparisonStrategy.getEqualsValueProviderMethod(), is(IsEqual.equalTo("foo"))); - } - - @Test - public void testIntrospectWithPropertyAnnotations() - { - final Iterable accessors = introspector.introspect(ObjectWithPropertyAnnotations.class); - for (final PropertyAwareAccessor accessor : accessors) - { - if (accessor.getPathElement().equals(new NamedPropertyElement("ignored"))) - { - assertThat(accessor.isExcluded(), is(true)); - } - else if (accessor.getPathElement().equals(new NamedPropertyElement("equalsOnly"))) - { - assertThat(accessor.getComparisonStrategy(), instanceOf(EqualsOnlyComparisonStrategy.class)); - } - else if (accessor.getPathElement().equals(new NamedPropertyElement("categorized"))) - { - assertThat(accessor.getCategories().size(), is(1)); - assertThat(accessor.getCategories(), IsEqual.equalTo(Collections.setOf("foo"))); - } - else if (accessor.getPathElement().equals(new NamedPropertyElement("item"))) - { - assertThat(accessor.getComparisonStrategy(), IsNull.nullValue()); - assertThat(accessor.isExcluded(), is(false)); - assertThat(accessor.getCategories().isEmpty(), is(true)); - } - else if (accessor.getPathElement().equals(new NamedPropertyElement("key"))) - { - // no op - } - else if (accessor.getPathElement().equals(new NamedPropertyElement("value"))) - { - // no op - } - else - { - org.testng.Assert.fail("Unexpected accessor: " + accessor.getPathElement()); - } - } - } - - @Test - public void testIntrospectWithInheritedPropertyAnnotations() - { - final Iterable accessors = introspector.introspect(ObjectWithInheritedPropertyAnnotation.class); - final PropertyAwareAccessor accessor = accessors.iterator().next(); - assertThat((NamedPropertyElement) accessor.getPathElement(), IsEqual.equalTo(new NamedPropertyElement("value"))); - assertThat(accessor.isExcluded(), is(true)); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testIntrospectWithNullType() throws Exception - { - introspector.introspect(null); - } - - @Test(expectedExceptions = RuntimeException.class) - public void testIntrospectWithSimulatedIntrospectionException() throws Exception - { - introspector = new StandardBeanIntrospector() - { - @Override - protected BeanInfo getBeanInfo(final Class type) throws IntrospectionException - { - throw new IntrospectionException(type.getCanonicalName()); - } - }; - introspector.introspect(ObjectWithString.class); - } - - @ObjectDiffEqualsOnlyType(valueProviderMethod = "foo") - private static class ObjectWithObjectDiffEqualsOnlyTypeAnnotationAndValueProviderMethod - { - } -} diff --git a/src/test/java/de/danielbechler/diff/category/CategoryServiceTest.groovy b/src/test/java/de/danielbechler/diff/category/CategoryServiceTest.groovy new file mode 100644 index 00000000..75861236 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/category/CategoryServiceTest.groovy @@ -0,0 +1,101 @@ +/* + * 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.category + +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +class CategoryServiceTest extends Specification { + def categoryService = new CategoryService() + def accessor = Mock(PropertyAwareAccessor) + def nodePath = NodePath.with("foo") + def nodeType = Alliance + def DiffNode node + def DiffNode rootNode + + def "setup"() { + accessor.elementSelector >> new BeanPropertyElementSelector("foo") + rootNode = DiffNode.newRootNode() + node = new DiffNode(rootNode, accessor, nodeType) + } + + def "resolveCategories: should return categories configured via path"() { + given: + categoryService.ofNode(nodePath).toBe("Stark", "Lannister") + + expect: + categoryService.resolveCategories(node) == ["Stark", "Lannister"] as Set + } + + def "resolveCategories: should return categories configured via type"() { + given: + categoryService.ofType(nodeType).toBe("Stark", "Lannister") + + expect: + categoryService.resolveCategories(node) == ["Stark", "Lannister"] as Set + } + + def "resolveCategories: should return categories configured via node"() { + given: + accessor.categoriesFromAnnotation >> ["Stark", "Lannister"] + + expect: + categoryService.resolveCategories(node) == ["Stark", "Lannister"] as Set + } + + def "resolveCategories: should return combined categories configured via all possible options"() { + given: + categoryService.ofNode(nodePath).toBe("A") + + and: + categoryService.ofType(nodeType).toBe("B") + + and: + accessor.categoriesFromAnnotation >> ["C"] + + expect: + categoryService.resolveCategories(node) == ["A", "B", "C"] as Set + } + + def "resolveCategories: should also return categories of parent nodes"() { + given: + accessor.categoriesFromAnnotation >> ["B"] + + and: + categoryService.ofNode(NodePath.withRoot()).toBe("A") + + expect: + categoryService.resolveCategories(node) == ["A", "B"] as Set + } + + def "resolveCategories: should return empty Set if no category is defined"() { + given: + node = DiffNode.newRootNode() + + expect: + categoryService.resolveCategories(node) == [] as Set + } + + class Alliance { + } +} diff --git a/src/test/java/de/danielbechler/diff/circular/CircularReferenceDetectorTest.groovy b/src/test/java/de/danielbechler/diff/circular/CircularReferenceDetectorTest.groovy new file mode 100644 index 00000000..d6717061 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/circular/CircularReferenceDetectorTest.groovy @@ -0,0 +1,147 @@ +/* + * 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.circular + +import de.danielbechler.diff.mock.ObjectWithString +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +import spock.lang.Unroll + +import static de.danielbechler.diff.circular.CircularReferenceDetector.CircularReferenceException +import static de.danielbechler.diff.circular.CircularReferenceDetector.ReferenceMatchingMode.EQUALITY_OPERATOR +import static de.danielbechler.diff.circular.CircularReferenceDetector.ReferenceMatchingMode.EQUALS_METHOD + +class CircularReferenceDetectorTest extends Specification { + + CircularReferenceDetector circularReferenceDetector = new CircularReferenceDetector(EQUALITY_OPERATOR) + + def 'push: does nothing with null object'() { + when: + circularReferenceDetector.push(null, null) + then: + circularReferenceDetector.size() == 0 + } + + def 'push: adds unknown object to stack'() { + when: + circularReferenceDetector.push("foo", NodePath.withRoot()) + then: + circularReferenceDetector.size() == 1 + } + + def 'push: throws CircularReferenceException when given known object'() { + given: + def rootPath = NodePath.withRoot() + circularReferenceDetector.push("foo", rootPath) + when: + circularReferenceDetector.push("foo", NodePath.with("test")) + then: 'exception should be thrown' + CircularReferenceException ex = thrown CircularReferenceException + ex.nodePath == rootPath + and: 'the known object should not be added again' + circularReferenceDetector.size() == 1 + + } + + def 'remove: does nothing with null object'() { + expect: + circularReferenceDetector.size() == 0 + when: + circularReferenceDetector.remove null + then: + circularReferenceDetector.size() == 0 + } + + def 'remove: throws IllegalArgumentException when instance is removed out of order'() { + given: + circularReferenceDetector.push "foo", null + when: + circularReferenceDetector.remove "bar" + then: + thrown IllegalArgumentException + } + + def 'remove: removes instance when it is removed in order'() { + given: + circularReferenceDetector.push "foo", null + expect: + circularReferenceDetector.size() == 1 + when: + circularReferenceDetector.remove "foo" + then: + circularReferenceDetector.size() == 0 + } + + def 'knows: returns true when instance is known'() { + expect: + !circularReferenceDetector.knows('foo') + when: + circularReferenceDetector.push 'foo', null + then: + circularReferenceDetector.knows 'foo' + } + + def 'knows: returns false for previously added instance which has been removed'() { + given: + circularReferenceDetector.push "foo", null + expect: + circularReferenceDetector.knows("foo") == true + when: + circularReferenceDetector.remove "foo" + then: + circularReferenceDetector.knows("foo") == false + } + + def 'knows: returns false when instance is unknown'() { + when: + circularReferenceDetector.push 'foo', null + then: + circularReferenceDetector.knows('bar') == false + } + + @Unroll + def 'matchingMode #matchingMode'() { + given: + circularReferenceDetector = new CircularReferenceDetector(matchingMode) + + expect: 'sanity check' + internalInstance.is(externalInstance) == equalByOperator + + when: + circularReferenceDetector.push(internalInstance, NodePath.withRoot()); + then: + circularReferenceDetector.knows(externalInstance) + + when: + circularReferenceDetector.push(internalInstance, NodePath.withRoot()); + then: + thrown CircularReferenceException + + when: + circularReferenceDetector.remove(externalInstance) + then: + !circularReferenceDetector.knows(externalInstance) + + expect: + circularReferenceDetector.size() == 0 + + where: + matchingMode | internalInstance | externalInstance || equalByOperator + EQUALS_METHOD | new ObjectWithString("foo") | new ObjectWithString("foo") || false + EQUALITY_OPERATOR | 'foo' | 'foo' || true + } +} diff --git a/src/test/java/de/danielbechler/diff/collection/CollectionDifferShould.java b/src/test/java/de/danielbechler/diff/collection/CollectionDifferShould.java deleted file mode 100644 index aa2e504b..00000000 --- a/src/test/java/de/danielbechler/diff/collection/CollectionDifferShould.java +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Copyright 2012 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.collection; - -import de.danielbechler.diff.*; -import org.mockito.Mock; -import org.testng.annotations.*; - -import java.util.*; - -import static de.danielbechler.diff.helper.NodeMatchers.*; -import static org.fest.assertions.api.Assertions.*; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.*; - -/** @author Daniel Bechler */ -public class CollectionDifferShould -{ - @Mock - private ComparisonStrategyResolver comparisonStrategyResolver; - @Mock - private ComparisonStrategy comparisonStrategy; - @Mock - private DifferDispatcher differDispatcher; - @Mock - private Instances instances; - - private CollectionDiffer collectionDiffer; - private DiffNode node; - private Collection baseCollection; - private Collection workingCollection; - - @BeforeMethod - public void setUp() throws Exception - { - initMocks(this); - collectionDiffer = new CollectionDiffer(differDispatcher, comparisonStrategyResolver); - baseCollection = new HashSet(); - workingCollection = new HashSet(); - when(instances.getSourceAccessor()).thenReturn(RootAccessor.getInstance()); - when(instances.getBase(Collection.class)).thenReturn(baseCollection); - 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(); - } - - @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(); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_if_constructed_without_DifferDispatcher() - { - new CollectionDiffer(null, comparisonStrategyResolver); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_if_constructed_without_ComparisonStrategyResolver() - { - new CollectionDiffer(differDispatcher, null); - } - - @Test - public void return_untouched_node_when_instances_are_same() - { - given_instances_are_same(); - - node = collectionDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).has(state(DiffNode.State.UNTOUCHED)); - } - - @Test - public void return_added_node_when_instance_has_been_added() throws Exception - { - given_instance_has_been_added(); - - node = collectionDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).has(state(DiffNode.State.ADDED)); - } - - @Test - 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.buildRootPath()), same(instances), collectionItemAccessor(addedItem)); - verifyNoMoreInteractions(differDispatcher); - } - - @Test - public void return_removed_node_when_instance_has_been_removed() throws Exception - { - given_instance_has_been_removed(); - - node = collectionDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).has(state(DiffNode.State.REMOVED)); - } - - @Test - 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.buildRootPath()), same(instances), collectionItemAccessor(removedItem)); - verifyNoMoreInteractions(differDispatcher); - } - - @Test - 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.buildRootPath()), same(instances)); - } - - @Test - 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.buildRootPath()), same(instances), collectionItemAccessor(addedItem)); - verifyNoMoreInteractions(differDispatcher); - } - - @Test - 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.buildRootPath()), same(instances), collectionItemAccessor(removedItem)); - verifyNoMoreInteractions(differDispatcher); - } - - @Test - 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.buildRootPath()), same(instances), collectionItemAccessor(knownItem)); - verifyNoMoreInteractions(differDispatcher); - } - - private void given_instances_are_same() - { - when(instances.areSame()).thenReturn(true); - } - - private void given_instance_has_been_added() - { - when(instances.hasBeenAdded()).thenReturn(true); - } - - private void given_instance_has_been_removed() - { - when(instances.hasBeenRemoved()).thenReturn(true); - } - - 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) - { - baseCollection.add(item); - workingCollection.remove(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); - } -} diff --git a/src/test/java/de/danielbechler/diff/collection/CollectionElementTest.groovy b/src/test/java/de/danielbechler/diff/collection/CollectionElementTest.groovy deleted file mode 100644 index 507e9a04..00000000 --- a/src/test/java/de/danielbechler/diff/collection/CollectionElementTest.groovy +++ /dev/null @@ -1,71 +0,0 @@ -package de.danielbechler.diff.collection - -import de.danielbechler.diff.map.MapElement -import spock.lang.Specification - -/** - * @author Daniel Bechler - */ -class CollectionElementTest extends Specification -{ - def 'can be created with "null" item'() - { - expect: - new CollectionElement(null) != null - } - - def 'should equal other CollectionElement if items are equal'() - { - setup: - def element = new CollectionElement("foo") - def equalElement = new CollectionElement("foo") - - expect: - element.equals(equalElement) - } - - def 'should not equal elements of different class'() - { - setup: - def element = new CollectionElement("foo") - def mapElement = new MapElement("foo") - - expect: - !element.equals(mapElement) - } - - def 'should not equal null'() - { - setup: - def element = new CollectionElement("foo") - - expect: - !element.equals(null) - } - - def 'should have same hashCode as item'() - { - setup: - def item = "foo" - def element = new CollectionElement(item) - - expect: - element.hashCode() == item.hashCode() - } - - def 'should provide accessor for item'() - { - setup: - def item = "foo" - def element = new CollectionElement(item) - - expect: - element.getItem() == item - } - - def 'should have proper toString() representation'() - { - expect: "string representation of the item should be converted to single line" - new CollectionElement("foo\nbar").toString() == '[foo \\ bar]' - } -} diff --git a/src/test/java/de/danielbechler/diff/collection/CollectionItemAccessorShould.java b/src/test/java/de/danielbechler/diff/collection/CollectionItemAccessorShould.java deleted file mode 100644 index 9592b83d..00000000 --- a/src/test/java/de/danielbechler/diff/collection/CollectionItemAccessorShould.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright 2012 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.collection; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.mock.*; -import org.fest.assertions.core.*; -import org.testng.annotations.*; - -import java.util.*; - -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class CollectionItemAccessorShould -{ - private CollectionItemAccessor accessor; - private ObjectWithIdentityAndValue referenceItem; - private Collection collection; - - @BeforeMethod - public void setUp() throws Exception - { - referenceItem = new ObjectWithIdentityAndValue("1", "foo"); - accessor = new CollectionItemAccessor(referenceItem); - collection = new ArrayList(); - } - - @Test - public void be_instantiatable() - { - accessor = new CollectionItemAccessor(referenceItem); - - assertThat(accessor).isNotNull(); - } - - @Test - public void be_instantiatable_without_reference_item() - { - accessor = new CollectionItemAccessor(null); - - assertThat(accessor).isNotNull(); - } - - @Test - public void retrieve_item_from_collection() throws Exception - { - collection.add(new ObjectWithIdentityAndValue("1")); - - final Object item = accessor.get(collection); - - assertThat(item).isEqualTo(referenceItem); - } - - @Test - public void retrieve_null_from_collection_if_it_does_not_contain_the_accessed_item() throws Exception - { - final Object item = accessor.get(collection); - - assertThat(item).isNull(); - } - - @Test - public void retrieve_null_if_accessed_object_is_null() throws Exception - { - final Object item = accessor.get(null); - - assertThat(item).isNull(); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_retrieve_if_target_object_is_not_a_collection() throws Exception - { - accessor.get(new Object()); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_insert_if_target_object_is_not_a_collection() throws Exception - { - accessor.set(new Object(), null); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void fail_remove_if_target_object_is_not_a_collection() throws Exception - { - accessor.unset(new Object()); - } - - @Test - public void return_proper_path_element() - { - final CollectionElement expectedPathElement = new CollectionElement(referenceItem); - - final Element actualPathElement = accessor.getPathElement(); - - assertThat(actualPathElement).isEqualTo(expectedPathElement); - } - - @Test - public void insert_item_into_collection() throws Exception - { - assertThat(collection).isEmpty(); - - accessor.set(collection, referenceItem); - - assertThat(collection).containsOnly(referenceItem); - } - - @Test - public void insert_nothing_if_target_object_is_null() throws Exception - { - accessor.set(null, referenceItem); - } - - @Test - public void replace_same_item_in_list() throws Exception - { - collection.add(new ObjectWithIdentityAndValue("1", "foo")); - - accessor.set(collection, new ObjectWithIdentityAndValue("1", "bar")); - - assertThat(collection).hasSize(1); - assertThat(collection.iterator().next().getValue()).isEqualTo("bar"); - } - - @Test - public void replace_same_item_in_set() throws Exception - { - collection = new HashSet(); - collection.add(new ObjectWithIdentityAndValue("1", "foo")); - - accessor.set(collection, new ObjectWithIdentityAndValue("1", "bar")); - - assertThat(collection).hasSize(1); - assertThat(collection.iterator().next().getValue()).isEqualTo("bar"); - } - - @Test - public void remove_item_from_collection() - { - collection.add(referenceItem); - - accessor.unset(collection); - - assertThat(collection).isEmpty(); - } - - @Test - public void not_fail_if_item_to_remove_is_not_in_collection() - { - accessor.unset(collection); - - assertThat(collection).isEmpty(); - } - - @Test - public void return_type_of_reference_item() throws Exception - { - assertThat(accessor.getType()).is(new SameTypeAs(ObjectWithIdentityAndValue.class)); - } - - @Test - public void return_null_type_if_reference_item_is_null() - { - accessor = new CollectionItemAccessor(null); - - assertThat(accessor.getType()).isNull(); - } - - @Test - public void mention_its_type_in_string_representation() throws Exception - { - assertThat(accessor.toString()).startsWith("collection item "); - } - - private static class SameTypeAs extends Condition> - { - private final Class type; - - public SameTypeAs(final Class type) - { - super(type.getCanonicalName()); - this.type = type; - } - - @Override - public boolean matches(final Class value) - { - return value == type; - } - } -} diff --git a/src/test/java/de/danielbechler/diff/comparison/ComparisonServiceTest.groovy b/src/test/java/de/danielbechler/diff/comparison/ComparisonServiceTest.groovy new file mode 100644 index 00000000..5feffcc6 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/comparison/ComparisonServiceTest.groovy @@ -0,0 +1,100 @@ +/* + * 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.comparison + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +import spock.lang.Unroll + +import java.sql.Time +import java.sql.Timestamp + +class ComparisonServiceTest extends Specification { + + def objectDifferBuilder = Stub(ObjectDifferBuilder) + ComparisonService comparisonService = new ComparisonService(objectDifferBuilder) + + @Unroll + def "resolveComparisonStrategy: returns ComparableComparisonStrategy for simple types implementing Comparable (e.g. #type)"() { + given: + def node = Stub(DiffNode) + node.valueType >> type + node.path >> NodePath.with('any') + + expect: + comparisonService.resolveComparisonStrategy(node) instanceof ComparableComparisonStrategy + + where: + type << [ + Boolean, + Byte, + Character, + Double, + Enum, + Float, + Integer, + Long, + Short, + String, + BigDecimal, + BigInteger, + URI, + Date, + Time, + Timestamp, +// Duration, +// Instant, +// LocalDate, +// LocalDateTime, +// LocalTime, +// MonthDay, +// OffsetDateTime, +// OffsetTime, +// Year, +// YearMonth, +// ZonedDateTime, +// ZoneOffset, + Calendar, + Date, + GregorianCalendar, + UUID + ] + } + + def 'resolveComparisonStrategy: should not return ComparableComparisonStrategy for non-simple types implementing Comparable'() { + given: + def node = Stub(DiffNode) + node.valueType >> type + node.path >> NodePath.with('any') + node.getPropertyAnnotation(_ as Class) >> null + + expect: + comparisonService.resolveComparisonStrategy(node) == null + + where: + type << [CustomComparable] + } + + public static class CustomComparable implements Comparable { + @Override + int compareTo(CustomComparable o) { + return 0 + } + } +} diff --git a/src/test/java/de/danielbechler/diff/differ/BeanDifferTest.groovy b/src/test/java/de/danielbechler/diff/differ/BeanDifferTest.groovy new file mode 100644 index 00000000..e81c366d --- /dev/null +++ b/src/test/java/de/danielbechler/diff/differ/BeanDifferTest.groovy @@ -0,0 +1,196 @@ +/* + * 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.differ +import de.danielbechler.diff.access.Instances +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.access.RootAccessor +import de.danielbechler.diff.comparison.ComparisonStrategy +import de.danielbechler.diff.comparison.ComparisonStrategyResolver +import de.danielbechler.diff.filtering.IsReturnableResolver +import de.danielbechler.diff.instantiation.TypeInfo +import de.danielbechler.diff.introspection.IsIntrospectableResolver +import de.danielbechler.diff.introspection.TypeInfoResolver +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import spock.lang.FailsWith +import spock.lang.Specification +import spock.lang.Unroll + +class BeanDifferTest extends Specification { + + DifferDispatcher differDispatcher = Mock(DifferDispatcher) + Instances instances = Mock(Instances) { + getBase() >> 'any' + getWorking() >> 'any' + getFresh() >> 'any' + getSourceAccessor() >> RootAccessor.instance + } + IsIntrospectableResolver introspectableResolver = Mock(IsIntrospectableResolver) + IsReturnableResolver returnableResolver = Mock(IsReturnableResolver) + ComparisonStrategyResolver comparisonStrategyResolver = Mock(ComparisonStrategyResolver) + ComparisonStrategy comparisonStrategy = Mock(ComparisonStrategy) + TypeInfoResolver typeInfoResolver = Mock(TypeInfoResolver) + + BeanDiffer beanDiffer = new BeanDiffer( + differDispatcher, + introspectableResolver, + returnableResolver, + comparisonStrategyResolver, + typeInfoResolver) + + @Unroll + def 'accepts all object types (e.g. #type)'() { + expect: + beanDiffer.accepts(type) + where: + type << [Object, Number, Number, Date] + } + + @Unroll + def 'rejects all primitive types (e.g. #type)'() { + expect: + beanDiffer.accepts(type) == false + where: + type << [int, int[], boolean] + } + + def 'returns untouched node if working and base are null'() { + given: + instances.areNull() >> true + expect: + beanDiffer.compare(DiffNode.ROOT, instances).isUntouched() + } + + def 'returns added node if working is not null and base is'() { + given: + instances.hasBeenAdded() >> true + expect: + beanDiffer.compare(DiffNode.ROOT, instances).isAdded() + } + + def 'returns removed node if working is null and base is not'() { + given: + instances.hasBeenRemoved() >> true + expect: + beanDiffer.compare(DiffNode.ROOT, instances).isRemoved() + } + + def 'returns untouched node if working and base are the same instance'() { + given: + instances.areSame() >> true + expect: + beanDiffer.compare(DiffNode.ROOT, instances).isUntouched() + } + + def 'compares via comparisonStrategy if one exists'() { + given: + comparisonStrategyResolver.resolveComparisonStrategy(_ as DiffNode) >> comparisonStrategy + when: + beanDiffer.compare(DiffNode.ROOT, instances); + then: + 1 * comparisonStrategy.compare(_ as DiffNode, instances.type, instances.working, instances.base) + } + + def 'compares via introspection if object is introspectable'() { + given: + introspectableResolver.isIntrospectable(_ as DiffNode) >> true + when: + beanDiffer.compare(DiffNode.ROOT, instances) + then: + 1 * typeInfoResolver.typeInfoForNode(_ as DiffNode) >> new TypeInfo(Object) + } + + def 'delegate comparison of properties to DifferDispatcher when comparing via introspector'() { + given: 'the root object is introspectable' + introspectableResolver.isIntrospectable({ DiffNode node -> node.isRootNode() }) >> true + and: 'the root object type info contains an accessor' + def accessor = Mock(PropertyAwareAccessor) + def typeInfo = new TypeInfo(Object) + typeInfo.addPropertyAccessor(accessor) + typeInfoResolver.typeInfoForNode(_ as DiffNode) >> typeInfo + and: 'the property node returned by the dispatcher is returnable' + def propertyNode = Mock(DiffNode) { + getElementSelector() >> new BeanPropertyElementSelector('example') + } + returnableResolver.isReturnable(propertyNode) >> true + when: + def rootNode = beanDiffer.compare(DiffNode.ROOT, instances) + then: + 1 * differDispatcher.dispatch(_ as DiffNode, instances, accessor) >> propertyNode + and: + rootNode.childCount() == 1 + rootNode.getChild(propertyNode.elementSelector) == propertyNode + } + + def 'add property nodes only if they are returnable'() { + given: 'the root object is introspectable' + introspectableResolver.isIntrospectable({ DiffNode node -> node.isRootNode() }) >> true + and: 'the root object type info contains an accessor' + def accessor = Mock(PropertyAwareAccessor) + def typeInfo = new TypeInfo(Object) + typeInfo.addPropertyAccessor(accessor) + typeInfoResolver.typeInfoForNode(_ as DiffNode) >> typeInfo + and: 'the property node returned by the dispatcher is NOT returnable' + def propertyNode = Mock(DiffNode) { + getElementSelector() >> new BeanPropertyElementSelector('example') + } + returnableResolver.isReturnable(propertyNode) >> false + when: + def node = beanDiffer.compare(DiffNode.ROOT, instances) + then: + 1 * differDispatcher.dispatch(_ as DiffNode, instances, accessor) >> propertyNode + and: + !node.hasChildren() + } + + def 'assigns type info resolved via type info resolver to bean node when comparing via introspection'() { + given: + def propertyAccessor = Mock(PropertyAwareAccessor) + def typeInfo = new TypeInfo(Date) + typeInfo.addPropertyAccessor(propertyAccessor) + and: + def propertyNode = Mock(DiffNode) { + getElementSelector() >> new BeanPropertyElementSelector('example') + } + when: + def rootNode = beanDiffer.compare(DiffNode.ROOT, instances) + then: + 1 * introspectableResolver.isIntrospectable({ DiffNode node -> node.isRootNode() }) >> true + and: + 1 * typeInfoResolver.typeInfoForNode({ DiffNode node -> node.isRootNode() }) >> typeInfo + and: + 1 * differDispatcher.dispatch({ DiffNode node -> node.isRootNode() }, instances, propertyAccessor) >> propertyNode + and: + 1 * returnableResolver.isReturnable(propertyNode) >> false + and: + rootNode.valueTypeInfo == typeInfo + } + + @Unroll + @FailsWith(IllegalArgumentException) + def 'construction fails with IllegalArgumentException'() { + expect: + new BeanDiffer(differDispatcher, introspectableResolver, returnableResolver, comparisonStrategyResolver, typeInfoResolver) + where: + differDispatcher | introspectableResolver | returnableResolver | comparisonStrategyResolver | typeInfoResolver + null | Mock(IsIntrospectableResolver) | Mock(IsReturnableResolver) | Mock(ComparisonStrategyResolver) | Mock(TypeInfoResolver) + Mock(DifferDispatcher) | null | Mock(IsReturnableResolver) | Mock(ComparisonStrategyResolver) | Mock(TypeInfoResolver) + Mock(DifferDispatcher) | Mock(IsIntrospectableResolver) | null | Mock(ComparisonStrategyResolver) | Mock(TypeInfoResolver) + Mock(DifferDispatcher) | Mock(IsIntrospectableResolver) | Mock(IsReturnableResolver) | null | Mock(TypeInfoResolver) + Mock(DifferDispatcher) | Mock(IsIntrospectableResolver) | Mock(IsReturnableResolver) | Mock(ComparisonStrategyResolver) | null + } +} diff --git a/src/test/java/de/danielbechler/diff/differ/CollectionDifferTest.groovy b/src/test/java/de/danielbechler/diff/differ/CollectionDifferTest.groovy new file mode 100644 index 00000000..deb66e91 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/differ/CollectionDifferTest.groovy @@ -0,0 +1,183 @@ +/* + * 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.differ + +import de.danielbechler.diff.access.CollectionItemAccessor +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.EqualsIdentityStrategy +import de.danielbechler.diff.identity.IdentityStrategyResolver +import de.danielbechler.diff.mock.ObjectWithString +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +public class CollectionDifferTest extends Specification { + + ComparisonStrategyResolver comparisonStrategyResolver = Mock() + ComparisonStrategy comparisonStrategy = Mock() + DifferDispatcher differDispatcher = Mock() + Instances instances = Mock() + IdentityStrategyResolver identityStrategyResolver = Stub() + + CollectionDiffer collectionDiffer + DiffNode node + Collection baseCollection + Collection workingCollection + + def setup() { + collectionDiffer = new CollectionDiffer(differDispatcher, comparisonStrategyResolver, identityStrategyResolver) + baseCollection = new HashSet() + workingCollection = new HashSet() + instances = Stub(Instances) { + getSourceAccessor() >> RootAccessor.getInstance() + getType() >> List + getBase(Collection) >> baseCollection + getWorking(Collection) >> workingCollection + } + } + + def 'accepts all collection types'() { + expect: + collectionDiffer.accepts(type) == true + where: + type << [Collection, List, Queue, Set, ArrayList, LinkedList] + } + + def 'rejects non-collection types'() { + expect: + collectionDiffer.accepts(type) == false + where: + type << [Object, ObjectWithString, Date] + } + + def 'fails instantiation'() { + when: + new CollectionDiffer(differDispatcher, comparisonStrategyResolver, identityStrategyResolver) + then: + thrown(IllegalArgumentException) + where: + differDispatcher | comparisonStrategyResolver | identityStrategyResolver + null | Mock(ComparisonStrategyResolver) | Mock(IdentityStrategyResolver) + Mock(DifferDispatcher) | null | Mock(IdentityStrategyResolver) + Mock(DifferDispatcher) | Mock(ComparisonStrategyResolver) | null + } + + def 'returns untouched node when instances are same'() { + given: + instances.areSame() >> true + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances) + then: + node.state == DiffNode.State.UNTOUCHED + } + + def 'returns added node when instance has been added'() { + given: + instances.hasBeenAdded() >> true + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances) + then: + node.state == DiffNode.State.ADDED + } + + def 'returns removed node when instance has been removed'() { + given: + instances.hasBeenRemoved() >> true + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances); + then: + node.state == DiffNode.State.REMOVED + } + + def 'delegates added items to dispatcher when instance has been added'() { + given: + instances = Mock(Instances) { + getSourceAccessor() >> RootAccessor.instance + hasBeenAdded() >> true + getWorking(Collection) >> ["foo"] + } + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances) + then: + 1 * differDispatcher.dispatch(_, instances, _) >> { parentNode, instances, accessor -> + assert parentNode != null + assert accessor instanceof CollectionItemAccessor + } + and: + 0 * differDispatcher.dispatch(*_) + } + + def 'delegate removed items to dispatcher when instance has been removed'() { + given: + instances = Mock(Instances) { + getSourceAccessor() >> RootAccessor.instance + hasBeenRemoved() >> true + getBase(Collection) >> ["foo"] + } + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances); + then: + 1 * differDispatcher.dispatch(_, instances, _) >> { parentNode, instances, accessor -> + assert parentNode != null + assert accessor instanceof CollectionItemAccessor + } + and: + 0 * differDispatcher.dispatch(*_) + } + + def 'compare using comparison strategy if available'() { + given: + comparisonStrategyResolver.resolveComparisonStrategy(_) >> comparisonStrategy + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances); + then: + 1 * comparisonStrategy.compare(_, _, workingCollection, baseCollection) >> { node, type, working, base -> + assert node.path.matches(NodePath.withRoot()) + assert type == List + } + } + + def 'delegate items to dispatcher when performing deep comparison'() { + given: + instances = Mock(Instances) { + getSourceAccessor() >> RootAccessor.instance + getWorking(Collection) >> working + getBase(Collection) >> base + } + and: + identityStrategyResolver.resolveIdentityStrategy(_) >> { + return EqualsIdentityStrategy.instance + } + when: + node = collectionDiffer.compare(DiffNode.ROOT, instances); + then: + 1 * differDispatcher.dispatch(_, instances, _) >> { parentNode, instances, accessor -> + assert parentNode.path.matches(NodePath.withRoot()) + assert accessor instanceof CollectionItemAccessor + } + and: + 0 * differDispatcher.dispatch(*_) + where: + working | base + ['added'] | [] + ['known'] | ['known'] + [] | ['removed'] + } +} diff --git a/src/test/java/de/danielbechler/diff/differ/DifferDispatcherTest.groovy b/src/test/java/de/danielbechler/diff/differ/DifferDispatcherTest.groovy new file mode 100644 index 00000000..637cb8d7 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/differ/DifferDispatcherTest.groovy @@ -0,0 +1,140 @@ +/* + * 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.differ + +import de.danielbechler.diff.access.Instances +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.access.RootAccessor +import de.danielbechler.diff.category.CategoryResolver +import de.danielbechler.diff.circular.CircularReferenceDetector +import de.danielbechler.diff.circular.CircularReferenceDetectorFactory +import de.danielbechler.diff.circular.CircularReferenceExceptionHandler +import de.danielbechler.diff.filtering.IsReturnableResolver +import de.danielbechler.diff.inclusion.IsIgnoredResolver +import de.danielbechler.diff.introspection.PropertyAccessExceptionHandler +import de.danielbechler.diff.introspection.PropertyAccessExceptionHandlerResolver +import de.danielbechler.diff.introspection.PropertyReadException +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +import static de.danielbechler.diff.circular.CircularReferenceDetector.CircularReferenceException + +public class DifferDispatcherTest extends Specification { + + DifferDispatcher differDispatcher + + DifferProvider differProvider = Mock() + CircularReferenceDetector circularReferenceDetector = Mock() + CircularReferenceDetectorFactory circularReferenceDetectorFactory = Mock() + CircularReferenceExceptionHandler circularReferenceExceptionHandler = Mock() + IsIgnoredResolver ignoredResolver = Mock() + CategoryResolver categoryResolver = Mock() + IsReturnableResolver returnableResolver = Mock() + PropertyAccessExceptionHandlerResolver propertyAccessExceptionHandlerResolver = Mock() + + def setup() { + circularReferenceDetectorFactory.createCircularReferenceDetector() >> circularReferenceDetector + categoryResolver.resolveCategories(_) >> [] + differDispatcher = new DifferDispatcher(differProvider, + circularReferenceDetectorFactory, + circularReferenceExceptionHandler, + ignoredResolver, + returnableResolver, + propertyAccessExceptionHandlerResolver, + categoryResolver); + } + + def 'when circular reference is detected the node should be marked as circular'() throws Exception { + given: + circularReferenceDetector.push(_, _) >> { instance, nodePath -> + throw new CircularReferenceException(nodePath) + } + when: + def node = differDispatcher.dispatch(DiffNode.ROOT, Instances.of('*', '*'), RootAccessor.instance); + then: + node.state == DiffNode.State.CIRCULAR + } + + def 'when circular reference is detected the node should hold the path to the node it circles back to'() throws Exception { + given: + circularReferenceDetector.push(_, _) >> { instance, nodePath -> + throw new CircularReferenceException(nodePath) + } + when: + def node = differDispatcher.dispatch(DiffNode.ROOT, Instances.of('*', '*'), RootAccessor.instance); + then: 'the node should be marked as circular' + node.circleStartPath == NodePath.withRoot() + and: + 1 * circularReferenceExceptionHandler.onCircularReferenceException(_ as DiffNode) + } + + def 'when circular reference is detected the node should be passed to the exception handler before it is returned'() throws Exception { + def handledNode = null + + given: + circularReferenceDetector.push(_, _) >> { instance, nodePath -> + throw new CircularReferenceException(nodePath) + } + when: + def node = differDispatcher.dispatch(DiffNode.ROOT, Instances.of('*', '*'), RootAccessor.instance); + then: + 1 * circularReferenceExceptionHandler.onCircularReferenceException(_ as DiffNode) >> { DiffNode it -> handledNode = it } + expect: + node.is(handledNode) + } + + def 'throw exception if no differ can be found for instance type'() { + given: + differProvider.retrieveDifferForType(_) >> null + when: + differDispatcher.dispatch(DiffNode.ROOT, Instances.of('*', '*'), RootAccessor.instance); + then: + thrown(IllegalStateException) + } + + def 'should delegate property read exception to PropertyAccessExceptionHandler'() { + def propertyExceptionHandler = Mock PropertyAccessExceptionHandler + def propertyAccessor = Mock(PropertyAwareAccessor) { + getPropertyName() >> 'foo' + } + def propertyAccessException = new PropertyReadException('foo', Date, new RuntimeException()) + def instances = Mock Instances, { + access(propertyAccessor) >> { throw propertyAccessException } + getType() >> Date + } + when: + differDispatcher.dispatch(DiffNode.ROOT, instances, propertyAccessor) + then: + 1 * propertyAccessExceptionHandlerResolver.resolvePropertyAccessExceptionHandler(Date, 'foo') >> propertyExceptionHandler + and: + 1 * propertyExceptionHandler.onPropertyReadException(propertyAccessException, _ as DiffNode) + } + + def 'when reading a property value caused an exception the returned node should have state INACESSIBLE'() { + def propertyAccessor = Mock(PropertyAwareAccessor) + def instances = Mock Instances, { + access(propertyAccessor) >> { + throw new PropertyReadException('foo', Date, new RuntimeException()) + } + } + when: + def node = differDispatcher.dispatch(DiffNode.ROOT, instances, propertyAccessor) + then: + node.state == DiffNode.State.INACCESSIBLE + } +} diff --git a/src/test/java/de/danielbechler/diff/differ/DifferProviderTest.groovy b/src/test/java/de/danielbechler/diff/differ/DifferProviderTest.groovy new file mode 100644 index 00000000..a20fb946 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/differ/DifferProviderTest.groovy @@ -0,0 +1,72 @@ +/* + * Copyright 2012 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.differ + +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +public class DifferProviderTest extends Specification { + + def differProvider = new DifferProvider() + + def 'return differ that accepts given type'() { + setup: + def differMock = Mock(Differ) { + accepts(String) >> true + } + differProvider.push(differMock) + expect: + differProvider.retrieveDifferForType(String).is(differMock) + } + + def 'return the last pushed differ that accepts the given type'() { + given: + def differ1 = Mock(Differ) { + accepts(String) >> true + } + differProvider.push(differ1) + and: + def differ2 = Mock(Differ) { + accepts(String) >> true + } + differProvider.push(differ2) + expect: + differProvider.retrieveDifferForType(String).is(differ1) == false + differProvider.retrieveDifferForType(String).is(differ2) == true + } + + def 'throw IllegalArgumentException if no type is given'() { + when: + differProvider.retrieveDifferForType(null) + then: + thrown(IllegalArgumentException) + } + + def 'throw IllegalStateException if no differ exists for given type'() { + given: + differProvider.push(Stub(Differ) { + accepts(String) >> true + }) + when: + differProvider.retrieveDifferForType(Date) + then: + Exception ex = thrown(IllegalStateException) + ex.message == "Couldn't find a differ for type: java.util.Date" + } +} diff --git a/src/test/java/de/danielbechler/diff/differ/MapDifferTest.groovy b/src/test/java/de/danielbechler/diff/differ/MapDifferTest.groovy new file mode 100644 index 00000000..74199625 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/differ/MapDifferTest.groovy @@ -0,0 +1,199 @@ +/* + * 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.differ + +import de.danielbechler.diff.access.Accessor +import de.danielbechler.diff.access.Instances +import de.danielbechler.diff.access.MapEntryAccessor +import de.danielbechler.diff.access.RootAccessor +import de.danielbechler.diff.comparison.ComparisonStrategy +import de.danielbechler.diff.comparison.ComparisonStrategyResolver +import de.danielbechler.diff.filtering.IsReturnableResolver +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification + +import static de.danielbechler.diff.node.DiffNode.State.* + +/** @author Daniel Bechler */ +public class MapDifferTest extends Specification { + + def MapDiffer mapDiffer + def node + def working = [] as HashMap + def base = [] as HashMap + + def childNode = Mock(DiffNode) + def instances = Mock(Instances) + def differDispatcher = Mock(DifferDispatcher) + def comparisonStrategyResolver = Mock(ComparisonStrategyResolver) + def isReturnableResolver = Mock(IsReturnableResolver) + def comparisonStrategy = Mock(ComparisonStrategy) + + def setup() { + differDispatcher.dispatch(_ as DiffNode, _ as Instances, _ as Accessor) >> childNode + mapDiffer = new MapDiffer(differDispatcher, comparisonStrategyResolver) + instances.sourceAccessor >> RootAccessor.instance + instances.getWorking(Map) >> working + instances.getBase(Map) >> base + isReturnableResolver.isReturnable(_ as DiffNode) >> true + } + + def "mark node as added when map was null and has been changed to an instance"() { + setup: + instances.hasBeenAdded() >> true + + expect: + mapDiffer.compare(DiffNode.ROOT, instances).state == ADDED + } + + def "dispatch comparison of each entry of an added map"() { + given: + working.put('1', 'one') + working.put('2', 'two') + instances.hasBeenAdded() >> true + + when: + node = mapDiffer.compare(DiffNode.ROOT, instances) + + then: + 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('1')) + 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('2')) + } + + def "mark node as removed when map was an instance and has been changed to null"() { + setup: + instances.hasBeenRemoved() >> true + + expect: + mapDiffer.compare(DiffNode.ROOT, instances).state == REMOVED + } + + def "mark node as untouched when map instances are identical"() { + setup: + instances.areSame() >> true + + expect: + mapDiffer.compare(DiffNode.ROOT, instances).state == UNTOUCHED + } + + def "do not compare entries of identical map instances"() { + given: + instances.areSame() >> true + + and: + working.put('foo', 'bar') + + when: + mapDiffer.compare(DiffNode.ROOT, instances) + + then: + 0 * differDispatcher._ + } + + def "dispatch comparison of added map entry"() { + given: + working.put("foo", "bar") + + when: + node = mapDiffer.compare(DiffNode.ROOT, instances) + + then: + 1 * differDispatcher.dispatch(_ as DiffNode, instances, new MapEntryAccessor('foo')) >> childNode + } + + def "dispatch comparison of removed map entry"() { + given: + base.put("foo", "bar") + + when: + node = mapDiffer.compare(DiffNode.ROOT, instances) + + then: + 1 * differDispatcher.dispatch(_ as DiffNode, instances, new MapEntryAccessor('foo')) >> childNode + } + + def "delegate to comparison strategy returned by ComparisonStrategyResolver"() { + given: + comparisonStrategyResolver.resolveComparisonStrategy(_ as DiffNode) >> comparisonStrategy + + when: + node = mapDiffer.compare(DiffNode.ROOT, instances) + + then: + 1 * comparisonStrategy.compare(_ as DiffNode, instances.getType(), instances.getWorking(Map), instances.getBase(Map)) + } + + def "do not add unreturnable child nodes"() { + given: + working.put("foo", "bar") + + when: + node = mapDiffer.compare(DiffNode.ROOT, instances) + + then: + isReturnableResolver.isReturnable(childNode) >> false + + and: + node.hasChildren() == false + } + + def "dispatch comparison of each entry of a removed map"() { + base.put('1', 'one') + base.put('2', 'two') + instances.hasBeenRemoved() >> true + + when: + node = mapDiffer.compare(DiffNode.ROOT, instances) + + then: + 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('1')) + 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('2')) + } + + def "fail when constructed without delegator"() { + when: + new MapDiffer(null, comparisonStrategyResolver); + + then: + thrown(IllegalArgumentException) + } + + def "return node with proper parent"() { + given: + def DiffNode parentNode = DiffNode.newRootNode() + + when: + node = mapDiffer.compare(parentNode, instances) + + then: + node.parentNode == parentNode + } + + def "accepts all implementations of java.util.Map"(Class type, def accept) { + expect: + mapDiffer.accepts(type) == accept + + where: + type || accept + Map || true + HashMap || true + TreeMap || true + LinkedList || false + String || false + null || false + } +} diff --git a/src/test/java/de/danielbechler/diff/differ/PrimitiveDifferTest.groovy b/src/test/java/de/danielbechler/diff/differ/PrimitiveDifferTest.groovy new file mode 100644 index 00000000..4ee147e0 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/differ/PrimitiveDifferTest.groovy @@ -0,0 +1,168 @@ +/* + * 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.differ + +import de.danielbechler.diff.access.Instances +import de.danielbechler.diff.access.TypeAwareAccessor +import de.danielbechler.diff.comparison.PrimitiveDefaultValueModeResolver +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Unroll + +import static de.danielbechler.diff.comparison.PrimitiveDefaultValueMode.ASSIGNED +import static de.danielbechler.diff.comparison.PrimitiveDefaultValueMode.UNASSIGNED +import static de.danielbechler.diff.node.DiffNode.State.* + +public class PrimitiveDifferTest extends Specification { + + PrimitiveDefaultValueModeResolver primitiveDefaultValueModeResolver = Mock(PrimitiveDefaultValueModeResolver) + PrimitiveDiffer primitiveDiffer = new PrimitiveDiffer(primitiveDefaultValueModeResolver) + + @Unroll('in #mode mode #type values get marked as #state when #scenarioSummary') + def 'detects state based on the given PrimitiveDefaultValueMode'() { + given: + primitiveDefaultValueModeResolver.resolvePrimitiveDefaultValueMode(_ as DiffNode) >> mode + and: + def typeAwareAccessor = Stub(TypeAwareAccessor, { + getType() >> type + }) + def instances = Instances.of(typeAwareAccessor, working, base, fresh); + when: + def node = primitiveDiffer.compare(DiffNode.ROOT, instances) + then: + node.state == state + where: + mode | type | base | working | fresh || state + UNASSIGNED | int | 0 | 0 | 0 || UNTOUCHED + UNASSIGNED | int | 0 | 1 | 0 || ADDED + UNASSIGNED | int | 1 | 0 | 0 || REMOVED + UNASSIGNED | int | 1 | 2 | 0 || CHANGED + UNASSIGNED | int | 2 | 2 | 0 || UNTOUCHED + + UNASSIGNED | long | 1L | 1L | 0L || UNTOUCHED + UNASSIGNED | long | 0L | 1L | 0L || ADDED + UNASSIGNED | long | 1L | 0L | 0L || REMOVED + UNASSIGNED | long | 1L | 2L | 0L || CHANGED + UNASSIGNED | long | 2L | 2L | 0L || UNTOUCHED + + UNASSIGNED | float | 0.0F | 0.0F | 0.0F || UNTOUCHED + UNASSIGNED | float | 0.0F | 1.0F | 0.0F || ADDED + UNASSIGNED | float | 1.0F | 0.0F | 0.0F || REMOVED + UNASSIGNED | float | 1.0F | 2.0F | 0.0F || CHANGED + UNASSIGNED | float | 2.0F | 2.0F | 0.0F || UNTOUCHED + + UNASSIGNED | double | 0.0D | 0.0D | 0.0D || UNTOUCHED + UNASSIGNED | double | 0.0D | 1.0D | 0.0D || ADDED + UNASSIGNED | double | 1.0D | 0.0D | 0.0D || REMOVED + UNASSIGNED | double | 1.0D | 2.0D | 0.0D || CHANGED + UNASSIGNED | double | 2.0D | 2.0D | 0.0D || UNTOUCHED + + UNASSIGNED | boolean | false | false | false || UNTOUCHED + UNASSIGNED | boolean | false | true | false || ADDED + UNASSIGNED | boolean | true | false | false || REMOVED + UNASSIGNED | boolean | true | true | false || UNTOUCHED + + ASSIGNED | int | 0 | 0 | 0 || UNTOUCHED + ASSIGNED | int | 0 | 1 | 0 || CHANGED + ASSIGNED | int | 1 | 0 | 0 || CHANGED + ASSIGNED | int | 1 | 2 | 0 || CHANGED + ASSIGNED | int | 2 | 2 | 0 || UNTOUCHED + + ASSIGNED | long | 0L | 0L | 0L || UNTOUCHED + ASSIGNED | long | 0L | 1L | 0L || CHANGED + ASSIGNED | long | 1L | 0L | 0L || CHANGED + ASSIGNED | long | 1L | 2L | 0L || CHANGED + ASSIGNED | long | 2L | 2L | 0L || UNTOUCHED + + ASSIGNED | float | 0.0F | 0.0F | 0.0F || UNTOUCHED + ASSIGNED | float | 0.0F | 1.0F | 0.0F || CHANGED + ASSIGNED | float | 1.0F | 0.0F | 0.0F || CHANGED + ASSIGNED | float | 1.0F | 2.0F | 0.0F || CHANGED + ASSIGNED | float | 2.0F | 2.0F | 0.0F || UNTOUCHED + + ASSIGNED | double | 0.0D | 0.0D | 0.0D || UNTOUCHED + ASSIGNED | double | 0.0D | 1.0D | 0.0D || CHANGED + ASSIGNED | double | 1.0D | 0.0D | 0.0D || CHANGED + ASSIGNED | double | 1.0D | 2.0D | 0.0D || CHANGED + ASSIGNED | double | 2.0D | 2.0D | 0.0D || UNTOUCHED + + ASSIGNED | boolean | false | false | false || UNTOUCHED + ASSIGNED | boolean | false | true | false || CHANGED + ASSIGNED | boolean | true | false | false || CHANGED + ASSIGNED | boolean | true | true | false || UNTOUCHED + + scenarioSummary = computeScenarioSummary(base, working, fresh) + } + + String computeScenarioSummary(base, working, fresh) { + if (working == fresh) { + if (base == fresh) { + 'base and working equal default value' + } else { + 'working equals default value but base does not' + } + } else if (base == fresh) { + if (working == fresh) { + 'base and working equal default value' + } else { + 'base equals default value but working does not' + } + } else if (working == base) { + 'base and working are equal but differ from default value' + } else { + 'base and working are not equal but differ from default value' + } + } + + @Unroll + def 'fails with exception when called for non-primitive type (#type)'() { + given: + def instances = Stub(Instances, { + getType() >> type + }) + when: + primitiveDiffer.compare(DiffNode.ROOT, instances) + then: + thrown IllegalArgumentException + where: + type << [Integer, Boolean, Void, Double, Float, Short, Byte, Date, BigDecimal] + } + + @Unroll + def 'accepts only primitive types (#type -> #acceptable)'() { + expect: + primitiveDiffer.accepts(type) == acceptable + where: + type || acceptable + boolean || true + int || true + char || true + long || true + float || true + double || true + short || true + byte || true + Boolean || false + Integer || false + Long || false + Float || false + Double || false + Short || false + Byte || false + } + +} diff --git a/src/test/java/de/danielbechler/diff/helper/MockitoAnswers.java b/src/test/java/de/danielbechler/diff/helper/MockitoAnswers.java deleted file mode 100644 index a0234533..00000000 --- a/src/test/java/de/danielbechler/diff/helper/MockitoAnswers.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.danielbechler.diff.helper; - -import org.mockito.invocation.*; -import org.mockito.stubbing.*; - -/** @author Daniel Bechler */ -public class MockitoAnswers -{ - private MockitoAnswers() - { - } - - public static Answer> withType(final Class type) - { - return new TypeAnswer(type); - } - - private static class TypeAnswer implements Answer> - { - private final Class type; - - public TypeAnswer(final Class type) - { - this.type = type; - } - - public Class answer(final InvocationOnMock invocation) throws Throwable - { - return type; - } - } -} diff --git a/src/test/java/de/danielbechler/diff/helper/NodeAssertions.java b/src/test/java/de/danielbechler/diff/helper/NodeAssertions.java deleted file mode 100644 index 5b085393..00000000 --- a/src/test/java/de/danielbechler/diff/helper/NodeAssertions.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2012 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.helper; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.collection.*; -import de.danielbechler.diff.visitor.*; -import org.fest.assertions.api.*; -import org.fest.assertions.core.*; - -/** @author Daniel Bechler */ -public final class NodeAssertions -{ - public static Syntax.SelectNode assertThat(final DiffNode node) - { - return new NodeAssertionLanguage(node); - } - - public static final class NodeAssertionLanguage implements Syntax.SelectNode, Syntax.AssertNode - { - private final DiffNode rootNode; - - private DiffNode selectedNode; - private NodePath nodePath; - - private NodeAssertionLanguage(final DiffNode rootNode) - { - this.rootNode = rootNode; - } - - public Syntax.AssertNode root() - { - this.selectedNode = rootNode; - this.nodePath = NodePath.createBuilder().withRoot().build(); - return this; - } - - public Syntax.AssertNode self() - { - this.selectedNode = rootNode; - this.nodePath = rootNode.getPath(); - return this; - } - - public Syntax.AssertNode child(final NodePath nodePath) - { - if (rootNode != null) - { - selectedNode = rootNode.getChild(nodePath); - } - this.nodePath = nodePath; - return this; - } - - public Syntax.AssertNode child(final NodePath.AppendableBuilder propertyPathBuilder) - { - return child(propertyPathBuilder.build()); - } - - public Syntax.AssertNode child(final String propertyName, final String... propertyNames) - { - return child(NodePath.buildWith(propertyName, propertyNames)); - } - - public Syntax.AssertNode child(final Element pathElement) - { - return child(NodePath.createBuilder().withRoot().withElement(pathElement)); - } - - public Syntax.AssertNode collectionChild(final Object referenceItem) - { - return child(new CollectionElement(referenceItem)); - } - - public Syntax.AssertNode doesExist() - { - Assertions.assertThat(rootNode).has(childAt(nodePath)); - return this; - } - - public Syntax.AssertNode doesNotExist() - { - Assertions.assertThat(rootNode).has(noChildAt(nodePath)); - return this; - } - - public Syntax.AssertNode hasState(final DiffNode.State state) - { - doesExist(); - Assertions.assertThat(selectedNode).has(state(state)); - return this; - } - - public Syntax.AssertNode hasChildren() - { - doesExist(); - Assertions.assertThat(selectedNode).has(atLeastOneChild()); - return this; - } - - public Syntax.AssertNode hasChildren(final int count) - { - doesExist(); - Assertions.assertThat(selectedNode).has(exactChildCountOf(count)); - return this; - } - - private static Condition childAt(final NodePath nodePath) - { - return new Condition("child at path " + nodePath) - { - @Override - public boolean matches(final DiffNode value) - { - if (value == null) - { - return false; - } - else - { - final PropertyVisitor visitor = new PropertyVisitor(nodePath); - value.visit(visitor); - return visitor.getNode() != null; - } - } - }; - } - - private static Condition state(final DiffNode.State state) - { - return new Condition("state " + state) - { - @Override - public boolean matches(final DiffNode value) - { - if (value == null) - { - return false; - } - else - { - return value.getState() == state; - } - } - }; - } - - private static Condition noChildAt(final NodePath nodePath) - { - return new Condition("no child at path " + nodePath) - { - @Override - public boolean matches(final DiffNode value) - { - if (value == null) - { - return true; - } - else - { - final PropertyVisitor visitor = new PropertyVisitor(nodePath); - value.visit(visitor); - return visitor.getNode() == null; - } - } - }; - } - - private static Condition atLeastOneChild() - { - return new Condition("at least one child") - { - @Override - public boolean matches(final DiffNode value) - { - return value.hasChildren(); - } - }; - } - - private static Condition exactChildCountOf(final int count) - { - if (count < 0) - { - throw new IllegalArgumentException("The number of expected children must be greater or equal to 0."); - } - return new Condition(count + " children") - { - @Override - public boolean matches(final DiffNode value) - { - if (count == 0) - { - return value == null || value.getChildren().isEmpty(); - } - else - { - return value != null && value.getChildren().size() == count; - } - } - }; - } - - public Syntax.AssertNode hasNoChildren() - { - return hasChildren(0); - } - - public Syntax.AssertNode isCircular() - { - doesExist(); - Assertions.assertThat(selectedNode.isCircular()).isTrue(); - return this; - } - - public Syntax.AssertNode isUntouched() - { - doesExist(); - Assertions.assertThat(selectedNode.getState()).isEqualTo(DiffNode.State.UNTOUCHED); - return this; - } - - public Syntax.AssertNode hasChanges() - { - doesExist(); - Assertions.assertThat(selectedNode.hasChanges()).isTrue(); - return this; - } - - public Syntax.AssertNode hasCircularStartPathEqualTo(final NodePath nodePath) - { - doesExist(); - Assertions.assertThat(selectedNode.getCircleStartPath()).isEqualTo(nodePath); - return this; - } - } - - private NodeAssertions() - { - } - - private interface Syntax - { - public interface SelectNode - { - AssertNode root(); - - AssertNode self(); - - AssertNode child(NodePath nodePath); - - AssertNode child(NodePath.AppendableBuilder propertyPathBuilder); - - AssertNode child(String propertyName, String... propertyPathElements); - - AssertNode child(Element pathElement); - - AssertNode collectionChild(Object referenceItem); - } - - public interface AssertNode - { - AssertNode doesExist(); - - AssertNode doesNotExist(); - - AssertNode hasState(DiffNode.State state); - - AssertNode hasChildren(); - - AssertNode hasChildren(int count); - - AssertNode hasNoChildren(); - - AssertNode isCircular(); - - AssertNode isUntouched(); - - AssertNode hasChanges(); - - AssertNode hasCircularStartPathEqualTo(final NodePath nodePath); - } - } -} diff --git a/src/test/java/de/danielbechler/diff/helper/NodeAssertionsTest.java b/src/test/java/de/danielbechler/diff/helper/NodeAssertionsTest.java deleted file mode 100644 index 30c934fe..00000000 --- a/src/test/java/de/danielbechler/diff/helper/NodeAssertionsTest.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright 2012 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.helper; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.bean.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import static de.danielbechler.diff.helper.NodeAssertions.*; -import static org.mockito.Mockito.*; - -/** @author Daniel Bechler */ -public class NodeAssertionsTest -{ - @Test - public void testAssertThat_node_does_exist_succeeds_when_node_exists() throws Exception - { - final DiffNode node = new DiffNode(String.class); - assertThat(node).root().doesExist(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_node_does_exist_fails_when_node_doesnt_exist() throws Exception - { - assertThat(null).root().doesExist(); - } - - @Test - public void testAssertThat_node_does_not_exist_succeeds_when_node_doesnt_exist() throws Exception - { - assertThat(null).root().doesNotExist(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_node_does_not_exist_fails_when_node_exist() throws Exception - { - final DiffNode node = new DiffNode(String.class); - assertThat(node).root().doesNotExist(); - } - - @Test - public void testAssertThat_node_has_children_succeeds_when_children_are_present() throws Exception - { - final DiffNode root = new DiffNode(String.class); - final DiffNode child = new DiffNode(root, mock(Accessor.class), String.class); - root.addChild(child); - assertThat(root).root().hasChildren(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_node_has_children_fails_when_no_children_are_present() throws Exception - { - final DiffNode node = new DiffNode(String.class); - assertThat(node).root().hasChildren(); - } - - @Test - public void testAssertThat_child_at_property_names_does_exist_succeeds_when_child_exists() - { - final Accessor accessor = mock(Accessor.class); - when(accessor.getPathElement()).thenReturn(new NamedPropertyElement("value")); - final DiffNode node = new DiffNode(ObjectWithString.class); - final DiffNode child = new DiffNode(node, accessor, String.class); - node.addChild(child); - assertThat(node).child("value").doesExist(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_child_at_property_names_does_exist_fails_when_child_doesnt_exist() - { - assertThat(null).child("value").doesExist(); - } - - @Test - public void testAssertThat_child_at_property_path_does_exist_succeeds_when_child_exists() - { - final ObjectWithString working = new ObjectWithString("foo"); - final ObjectWithString base = new ObjectWithString("bar"); - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - assertThat(node).child(NodePath.buildWith("value")).doesExist(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_child_at_property_path_does_exist_fails_when_child_doesnt_exist() - { - assertThat(null).child(NodePath.buildWith("value")).doesExist(); - } - - @Test - public void testAssertThat_child_at_property_path_builder_does_exist_succeeds_when_child_exist() - { - final ObjectWithString working = new ObjectWithString("foo"); - final ObjectWithString base = new ObjectWithString("bar"); - final DiffNode node = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(working, base); - assertThat(node).child(NodePath.createBuilder().withRoot().withPropertyName("value")).doesExist(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_child_at_property_path_builder_does_exist_fails_when_child_doesnt_exist() - { - assertThat(null).child(NodePath.createBuilder().withRoot().withPropertyName("value")).doesExist(); - } - - @Test - public void testAssertThat_node_has_no_children_succeeds_when_node_has_no_children() - { - final DiffNode node = new DiffNode(String.class); - assertThat(node).root().hasNoChildren(); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_node_has_no_children_fails_when_node_has_children() - { - final DiffNode root = new DiffNode(String.class); - final DiffNode child = new DiffNode(root, mock(Accessor.class), String.class); - root.addChild(child); - assertThat(root).root().hasNoChildren(); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testAssertThat_node_has_children_with_negative_count_throws_IllegalArgumentException() - { - final DiffNode node = new DiffNode(String.class); - assertThat(node).root().hasChildren(-1); - } - - @Test - public void testAssertThat_node_has_changed_state_succeeds_when_node_has_changed() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - assertThat(node).root().hasState(DiffNode.State.CHANGED); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_node_has_changed_state_fails_when_node_has_different_state() - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.UNTOUCHED); - assertThat(node).root().hasState(DiffNode.State.CHANGED); - } - - @Test(expectedExceptions = AssertionError.class) - public void testAssertThat_node_has_changed_state_fails_when_node_doesnt_exist() - { - assertThat(null).root().hasState(DiffNode.State.CHANGED); - } -} diff --git a/src/test/java/de/danielbechler/diff/helper/NodeMatchers.java b/src/test/java/de/danielbechler/diff/helper/NodeMatchers.java deleted file mode 100644 index 3c4d4bf5..00000000 --- a/src/test/java/de/danielbechler/diff/helper/NodeMatchers.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2013 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.helper; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.collection.*; -import org.fest.assertions.core.*; -import org.mockito.*; - -import static org.mockito.Matchers.*; - -/** @author Daniel Bechler */ -public class NodeMatchers -{ - private NodeMatchers() - { - } - - public static Accessor collectionItemAccessor(final Object item) - { - return argThat(new ArgumentMatcher() - { - @Override - public boolean matches(final Object argument) - { - if (argument != null && argument instanceof CollectionItemAccessor) - { - @SuppressWarnings("TypeMayBeWeakened") - final CollectionItemAccessor accessor = (CollectionItemAccessor) argument; - final CollectionElement collectionElement = new CollectionElement(item); - return accessor.getPathElement().equals(collectionElement); - } - return false; - } - }); - } - - public static DiffNode node(final NodePath nodePath, final Class typeHint) - { - final NodeMatcher node = new NodeMatcher(); - node.expectedNodePath = nodePath; - node.expectedTypeHint = typeHint; - return argThat(node); - } - - public static DiffNode node(final NodePath nodePath) - { - final NodeMatcher node = new NodeMatcher(); - node.expectedNodePath = nodePath; - return argThat(node); - } - - public static Condition state(final DiffNode.State state) - { - return new Condition("state " + state.toString()) - { - @Override - public boolean matches(final DiffNode value) - { - return value.getState() == state; - } - }; - } - - private static class NodeMatcher extends ArgumentMatcher - { - private Class expectedTypeHint; - private NodePath expectedNodePath; - - @Override - public boolean matches(final Object argument) - { - if (argument instanceof DiffNode) - { - final DiffNode node = (DiffNode) argument; - final NodePath path = node.getPath(); - if (expectedNodePath != null && path != null && !path.matches(this.expectedNodePath)) - { - return false; - } - if (expectedTypeHint != null && expectedTypeHint != node.getValueType()) - { - return false; - } - return true; - } - return false; - } - } -} diff --git a/src/test/java/de/danielbechler/diff/identity/EqualsIdentityStrategyTest.groovy b/src/test/java/de/danielbechler/diff/identity/EqualsIdentityStrategyTest.groovy new file mode 100644 index 00000000..78105d0f --- /dev/null +++ b/src/test/java/de/danielbechler/diff/identity/EqualsIdentityStrategyTest.groovy @@ -0,0 +1,43 @@ +/* + * 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 spock.lang.Specification +import spock.lang.Unroll + +class EqualsIdentityStrategyTest extends Specification { + + EqualsIdentityStrategy identityStrategy = EqualsIdentityStrategy.instance + + @Unroll + def "equals(#a, #b) should be #equal"() { + expect: + identityStrategy.equals(a, b) == equal + + where: + a | b || equal + null | null || true + null | 'foo' || false + 'foo' | 'bar' || false + 'foo' | 'foo' || true + } + + def "getInstance"() { + expect: + EqualsIdentityStrategy.instance.is(identityStrategy) + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/CategoryInclusionResolverTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/CategoryInclusionResolverTest.groovy new file mode 100644 index 00000000..94f21cda --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/CategoryInclusionResolverTest.groovy @@ -0,0 +1,99 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.category.CategoryResolver +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Subject + +import static de.danielbechler.diff.inclusion.Inclusion.* + +@Subject(CategoryInclusionResolver) +class CategoryInclusionResolverTest extends Specification { + + CategoryResolver categoryResolver = Mock CategoryResolver + CategoryInclusionResolver inclusionResolver = new CategoryInclusionResolver(categoryResolver) + + def "Constructor: fails without categoryResolver"() { + when: + new CategoryInclusionResolver(null) + then: + thrown(IllegalArgumentException) + } + + def "GetInclusion: returns DEFAULT when no INCLUDE or EXCLUDE has been configured"() { + given: + inclusionResolver.setInclusion('some-category', configuredInclusion) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == DEFAULT + where: + configuredInclusion << [null, DEFAULT] + } + + def "GetInclusion: returns DEFAULT when node has no categories"() { + given: + categoryResolver.resolveCategories(_ as DiffNode) >> [] + and: + inclusionResolver.setInclusion('inclusion-to-activate-the-resolver', INCLUDED) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == DEFAULT + } + + def "GetInclusion: returns DEFAULT when none of the nodes categories has inclusion"() { + given: + categoryResolver.resolveCategories(_ as DiffNode) >> ['foo', 'bar'] + and: + inclusionResolver.setInclusion('inclusion-to-activate-the-resolver', EXCLUDED) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == DEFAULT + } + + def "GetInclusion: returns EXCLUDED when any of the nodes categories is EXCLUDED"() { + given: + categoryResolver.resolveCategories(_ as DiffNode) >> ['foo', 'bar'] + and: + inclusionResolver.setInclusion('foo', INCLUDED) + inclusionResolver.setInclusion('bar', EXCLUDED) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == EXCLUDED + } + + def "GetInclusion: returns INCLUDED when any of the nodes categories is INCLUDED"() { + given: + categoryResolver.resolveCategories(_ as DiffNode) >> ['foo', 'bar'] + and: + inclusionResolver.setInclusion('bar', INCLUDED) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == INCLUDED + } + + def "EnablesStrictIncludeMode: is true when at least one INCLUDE has been configured"() { + expect: + !inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion('category', INCLUDED) + then: + inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion('category', EXCLUDED) + then: + !inclusionResolver.enablesStrictIncludeMode() + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/InclusionServiceOldTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/InclusionServiceOldTest.groovy new file mode 100644 index 00000000..f830fd9a --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/InclusionServiceOldTest.groovy @@ -0,0 +1,213 @@ +/* + * 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.inclusion +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.category.CategoryResolver +import de.danielbechler.diff.introspection.ObjectDiffProperty +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import spock.lang.Specification +/** + * @author Daniel Bechler + */ +class InclusionServiceOldTest extends Specification { + def categoryResolver = Stub(CategoryResolver) + def accessor = Mock(PropertyAwareAccessor) + def builder = Stub(ObjectDifferBuilder) + def inclusionService = new InclusionService(categoryResolver, builder) + def NodePath nodePath = NodePath.with("foo") + def DiffNode rootNode + def DiffNode node + + def "setup"() { + accessor.elementSelector >> new BeanPropertyElementSelector("foo") + rootNode = DiffNode.newRootNode() + node = new DiffNode(rootNode, accessor, null) + categoryResolver.resolveCategories(_ as DiffNode) >> [] + } + + def "isIgnored: should return 'true' if node is marked as ignored"() { + given: + accessor.getReadMethodAnnotation(ObjectDiffProperty) >> annotation + + expect: + inclusionService.isIgnored(node) + + where: + annotation << [ + Stub(ObjectDiffProperty, { excluded() >> true }), + Stub(ObjectDiffProperty, { inclusion() >> Inclusion.EXCLUDED }) + ] + } + + def "isIgnored: should return 'false' if no include and exclude rules are defined"() { + expect: + inclusionService.isIgnored(node) == false + } + + def "isIgnored: should return 'true' if node doesn't match defined inclusion rules"() { + setup: + inclusionService.include().category("unknown-category") + + expect: + inclusionService.isIgnored(node) == true + } + + def "isIgnored: should return 'false' if node doesn't match defined inclusion rules but is root node"() { + given: + inclusionService.include().category("unknown-category") + + expect: + !inclusionService.isIgnored(rootNode) + } + + def "isIgnored: should return 'false' if node is included via path"() { + setup: + inclusionService.include().node(nodePath) + + expect: + inclusionService.isIgnored(node) == false + } + + def "isIgnored: should return 'false' if node is included via category"() { + given: + categoryResolver.resolveCategories(node) >> ["test-category"] + + when: + def ignored = inclusionService.isIgnored(node) + + then: + inclusionService.include().category("test-category") + + and: + ignored == false + } + + def "isIgnored: should return 'false' if node is included via type"() { + setup: + node.setType(URL) + inclusionService.include().type(URL) + + expect: + inclusionService.isIgnored(node) == false + } + + def "isIgnored: should return 'false' if node doesn't match exclusion rules"() { + setup: + inclusionService.exclude().category("unknown-category") + + expect: + inclusionService.isIgnored(node) == false + } + + def "isIgnored: should return 'true' if node is excluded via annotation"() { + given: + accessor.getReadMethodAnnotation(ObjectDiffProperty) >> annotation + + expect: + inclusionService.isIgnored(node) == true + + where: + annotation << [ + Stub(ObjectDiffProperty, { excluded() >> true }), + Stub(ObjectDiffProperty, { inclusion() >> Inclusion.EXCLUDED }) + ] + } + + + def "isIgnored: should return 'true' if node is excluded via path"() { + setup: + inclusionService.exclude().node(nodePath) + + expect: + inclusionService.isIgnored(node) == true + } + + def "isIgnored: should return 'true' if node is excluded via type"() { + setup: + node.setType(URL) + inclusionService.exclude().type(URL) + + expect: + inclusionService.isIgnored(node) + } + + def "isIgnored: should return 'true' if node is excluded via category"() { + given: + inclusionService.exclude().category("test-category") + + when: + def ignored = inclusionService.isIgnored(node) + + then: + categoryResolver.resolveCategories(node) >> ["test-category"] + + and: + ignored == true + } + + def "isIgnored: should return 'true' when node is excluded via property name"() { + given: + def propertyName = "foo" + def propertyAwareAccessor = mockPropertyAwareAccessor(propertyName) + def node = new DiffNode(null, propertyAwareAccessor, null) + + and: + inclusionService.exclude().propertyName(propertyName) + + expect: + inclusionService.isIgnored(node) == true + } + + def "isIgnored: should return 'false' when node is included via property name"() { + given: + def propertyName = "foo" + def propertyAwareAccessor = mockPropertyAwareAccessor(propertyName) + def node = new DiffNode(null, propertyAwareAccessor, null) + + and: + inclusionService.include().propertyName(propertyName) + + expect: + inclusionService.isIgnored(node) == false + } + + def mockPropertyAwareAccessor(String name) { + def propertyAwareAccessor = Mock(PropertyAwareAccessor) + propertyAwareAccessor.elementSelector >> new BeanPropertyElementSelector(name) + propertyAwareAccessor.propertyName >> name + propertyAwareAccessor + } + + def "isIgnored: should return 'false' for children of included nodes"() { + given: + inclusionService.include().node(NodePath.withRoot()) + + expect: + inclusionService.isIgnored(node) == false + } + + def "isIgnored: should return 'true' for children of excluded nodes"() { + given: + inclusionService.exclude().node(NodePath.withRoot()) + + expect: + inclusionService.isIgnored(node) == true + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/InclusionServiceTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/InclusionServiceTest.groovy new file mode 100644 index 00000000..e4592163 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/InclusionServiceTest.groovy @@ -0,0 +1,229 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.category.CategoryResolver +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.* + +@SuppressWarnings("GroovyAssignabilityCheck") +@Subject(InclusionService) +class InclusionServiceTest extends Specification { + + def categoryResolver = Stub CategoryResolver + def rootConfiguration = Stub ObjectDifferBuilder + def categoryInclusionResolver = Mock CategoryInclusionResolver + def typePropertyConfigInclusionResolver = Mock TypePropertyConfigInclusionResolver + def typeInclusionResolver = Mock TypeInclusionResolver + def nodePathInclusionResolver = Mock NodePathInclusionResolver + def propertyNameInclusionResolver = Mock PropertyNameInclusionResolver + + InclusionService inclusionService = new InclusionService(categoryResolver, rootConfiguration) { + @Override + def CategoryInclusionResolver newCategoryInclusionResolver() { + return categoryInclusionResolver + } + + @Override + def TypePropertyConfigInclusionResolver newTypePropertyConfigInclusionResolver() { + return typePropertyConfigInclusionResolver + } + + @Override + def TypeInclusionResolver newTypeInclusionResolver() { + return typeInclusionResolver + } + + @Override + def NodePathInclusionResolver newNodePathInclusionResolver() { + return nodePathInclusionResolver + } + + @Override + def PropertyNameInclusionResolver newPropertyNameInclusionResolver() { + return propertyNameInclusionResolver + } + } + + def 'construction: should fail if categoryResolver is null'() { + when: + new InclusionService(null, rootConfiguration); + then: + thrown(IllegalArgumentException) + } + + def 'construction: should fail if objectDifferBuilder is null'() { + when: + new InclusionService(categoryResolver, null); + then: + thrown(IllegalArgumentException) + } + + def 'isIgnored: should never return true for the root node'() { + given: + def rootNode = DiffNode.newRootNode() + and: + inclusionService.include().node(NodePath.withRoot()) + expect: + inclusionService.isIgnored(rootNode) == false + } + + @Unroll + def 'isIgnored: should return #expectIgnored for nodes with #inclusion inclusion if strict include mode is #strictIncludeModeState'() { + given: + inclusionService.resolveUsing(Stub(InclusionResolver, { + getInclusion(_) >> inclusion + enablesStrictIncludeMode() >> strictMode + })) + expect: + inclusionService.isIgnored(Mock(DiffNode)) == expectIgnored + where: + inclusion | strictMode || expectIgnored + null | true || true + DEFAULT | true || true + INCLUDED | true || false + EXCLUDED | true || true + null | false || false + DEFAULT | false || false + INCLUDED | false || false + EXCLUDED | false || true + + strictIncludeModeState = strictMode ? 'enabled' : 'disabled' + ignoreState = expectIgnored ? 'ignore' : 'not ignore' + } + + @Unroll + def '#name: #inclusionText creates and activates (exactly one) #resolverType'() { + expect: + inclusionService.inclusionResolvers.findAll { resolverType.isInstance(it) }.size() == 0 + when: + stimulus.call(inclusionService) + stimulus.call(inclusionService) + then: + inclusionService.inclusionResolvers.findAll { resolverType.isInstance(it) }.size() == 1 + where: + name | inclusion | resolverType | stimulus + 'propertyName' | INCLUDED | PropertyNameInclusionResolver | { InclusionService service -> service.include().propertyName('foo') } + 'propertyName' | EXCLUDED | PropertyNameInclusionResolver | { InclusionService service -> service.exclude().propertyName('foo') } + 'propertyNameOfType' | INCLUDED | TypePropertyConfigInclusionResolver | { InclusionService service -> service.include().propertyNameOfType(String, 'foo') } + 'propertyNameOfType' | EXCLUDED | TypePropertyConfigInclusionResolver | { InclusionService service -> service.exclude().propertyNameOfType(String, 'foo') } + 'category' | INCLUDED | CategoryInclusionResolver | { InclusionService service -> service.include().category('foo') } + 'category' | EXCLUDED | CategoryInclusionResolver | { InclusionService service -> service.exclude().category('foo') } + 'node' | INCLUDED | NodePathInclusionResolver | { InclusionService service -> service.include().node(NodePath.with('foo')) } + 'node' | EXCLUDED | NodePathInclusionResolver | { InclusionService service -> service.exclude().node(NodePath.with('foo')) } + 'type' | INCLUDED | TypeInclusionResolver | { InclusionService service -> service.include().type(Object) } + 'type' | EXCLUDED | TypeInclusionResolver | { InclusionService service -> service.exclude().type(Object) } + inclusionText = inclusion == INCLUDED ? 'including' : 'excluding' + } + + @Unroll + def 'type: #inclusionText delegates to TypeInclusionResolver'() { + when: + performInclusion.call(inclusionService) + then: + 1 * typeInclusionResolver.setInclusion(type, inclusion) + where: + inclusion | type | performInclusion + INCLUDED | String | { InclusionService service -> service.include().type(String) } + EXCLUDED | Date | { InclusionService service -> service.exclude().type(Date) } + inclusionText = inclusion == INCLUDED ? 'including' : 'excluding' + } + + @Unroll + def 'category: #inclusionText delegates to CategoryInclusionResolver'() { + given: + def expectedCategory = 'some-category' + when: + stimulus.call(inclusionService, expectedCategory) + then: + 1 * categoryInclusionResolver.setInclusion(expectedCategory, inclusion) + where: + inclusion | stimulus + INCLUDED | { InclusionService service, String category -> service.include().category(category) } + EXCLUDED | { InclusionService service, String category -> service.exclude().category(category) } + + inclusionText = inclusion == INCLUDED ? 'including' : 'excluding' + } + + @Unroll + def 'propertyName: #inclusionText delegates to PropertyNameInclusionResolver'() { + when: + performInclusion.call(inclusionService, 'some-property-name') + then: + 1 * propertyNameInclusionResolver.setInclusion('some-property-name', inclusion) + where: + inclusion | performInclusion + INCLUDED | { InclusionService service, String name -> service.include().propertyName(name) } + EXCLUDED | { InclusionService service, String name -> service.exclude().propertyName(name) } + + inclusionText = inclusion == INCLUDED ? 'including' : 'excluding' + } + + @Unroll + def 'node: #inclusionText delegates to NodePathInclusionResolver'() { + def nodePath = NodePath.with('foo', 'bar') + when: + performInclusion.call(inclusionService, nodePath) + then: + 1 * nodePathInclusionResolver.setInclusion(nodePath, inclusion) + where: + inclusion | performInclusion + INCLUDED | { InclusionService service, NodePath path -> service.include().node(path) } + EXCLUDED | { InclusionService service, NodePath path -> service.exclude().node(path) } + + inclusionText = inclusion == INCLUDED ? 'including' : 'excluding' + } + + @Unroll + def 'propertyNameOfType: #inclusionMethodName delegates to TypePropertyConfigInclusionResolver'() { + when: + inclusionService.invokeMethod(inclusionMethodName, null).propertyNameOfType(String, 'foo') + then: + 1 * typePropertyConfigInclusionResolver.setInclusion(String, 'foo', inclusion) + where: + inclusion << [INCLUDED, EXCLUDED] + inclusionMethodName = inclusion == INCLUDED ? 'include' : 'exclude' + } + + def 'construction order of inclusion resolvers should be reflected by the collection'() { + given: + inclusionService = new InclusionService(categoryResolver, rootConfiguration) + when: + inclusionService + .include().propertyNameOfType(String, 'foo').also() + .include().propertyName('foo').also() + .include().type(Date).also() + .include().category('foo').also() + .include().node(NodePath.with('foo')) + then: + inclusionService.with { + inclusionResolvers.size() == 6 + inclusionResolvers[0] instanceof TypePropertyAnnotationInclusionResolver + inclusionResolvers[1] instanceof TypePropertyConfigInclusionResolver + inclusionResolvers[2] instanceof PropertyNameInclusionResolver + inclusionResolvers[3] instanceof TypeInclusionResolver + inclusionResolvers[4] instanceof CategoryInclusionResolver + inclusionResolvers[5] instanceof NodePathInclusionResolver + } + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/NodePathInclusionResolverTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/NodePathInclusionResolverTest.groovy new file mode 100644 index 00000000..695845af --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/NodePathInclusionResolverTest.groovy @@ -0,0 +1,105 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.* + +@Subject(NodePathInclusionResolver) +class NodePathInclusionResolverTest extends Specification { + + NodePathInclusionResolver inclusionResolver = new NodePathInclusionResolver() + + def "GetInclusion: returns DEFAULT when no INCLUDE or EXCLUDE has been configured"() { + given: + inclusionResolver.setInclusion(NodePath.with('foo'), configuredInclusion) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == DEFAULT + where: + configuredInclusion << [null, DEFAULT] + } + + @Unroll + def "GetInclusion: returns #inclusion when the node itself has #inclusion inclusion"() { + given: + def node = Stub(DiffNode) { + getPath() >> NodePath.with('foo') + } + when: + inclusionResolver.setInclusion(node.path, inclusion) + then: + inclusionResolver.getInclusion(node) == inclusion + where: + inclusion << [INCLUDED, EXCLUDED] + } + + @Unroll("GetInclusion: returns #expectedInclusion when the current node inclusion is #nodeInclusion and the closest parent inclusion is #parentInclusion") + def "GetInclusion: determines inclusion by looking at the actual node and its closest parent with inclusion"() { + given: + def parentNodePath = NodePath.with('foo') + def node = Stub(DiffNode) { + getPath() >> NodePath.startBuildingFrom(parentNodePath).propertyName('bar').build() + } + when: + inclusionResolver.setInclusion(NodePath.with('any-other-node'), INCLUDED) + inclusionResolver.setInclusion(parentNodePath, parentInclusion) + inclusionResolver.setInclusion(node.path, nodeInclusion) + then: + inclusionResolver.getInclusion(node) == expectedInclusion + where: + nodeInclusion | parentInclusion || expectedInclusion + null | null || DEFAULT + null | DEFAULT || DEFAULT + null | INCLUDED || INCLUDED + null | EXCLUDED || EXCLUDED + DEFAULT | null || DEFAULT + DEFAULT | DEFAULT || DEFAULT + DEFAULT | INCLUDED || INCLUDED + DEFAULT | EXCLUDED || EXCLUDED + INCLUDED | null || INCLUDED + INCLUDED | DEFAULT || INCLUDED + INCLUDED | INCLUDED || INCLUDED + INCLUDED | EXCLUDED || EXCLUDED + EXCLUDED | null || EXCLUDED + EXCLUDED | DEFAULT || EXCLUDED + EXCLUDED | INCLUDED || EXCLUDED + EXCLUDED | EXCLUDED || EXCLUDED + } + + def "GetInclusion: returns DEFAULT when neither the node itself nor its parents have an inclusion"() { + } + + def "EnablesStrictIncludeMode: is true when at least one INCLUDE has been configured"() { + expect: + !inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion(NodePath.with('foo'), INCLUDED) + then: + inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion(NodePath.with('foo'), EXCLUDED) + then: + !inclusionResolver.enablesStrictIncludeMode() + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/PropertyNameInclusionResolverTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/PropertyNameInclusionResolverTest.groovy new file mode 100644 index 00000000..f063d97f --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/PropertyNameInclusionResolverTest.groovy @@ -0,0 +1,87 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.* + +@Subject(PropertyNameInclusionResolver) +class PropertyNameInclusionResolverTest extends Specification { + + PropertyNameInclusionResolver inclusionResolver = new PropertyNameInclusionResolver() + + def "GetInclusion: returns DEFAULT when node is null"() { + expect: + inclusionResolver.getInclusion(null) == DEFAULT + } + + def "GetInclusion: returns DEFAULT when no INCLUDE or EXCLUDE has been configured"() { + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == DEFAULT + } + + @Unroll + def "GetInclusion: returns #expectedInclusion when configured property name inclusion is #propertyNameInclusion"() { + given: + def node = Mock(DiffNode, { getPropertyName() >> 'property-under-test' }) + when: + inclusionResolver.setInclusion('some-other-property', otherExistingInclusion) + inclusionResolver.setInclusion('property-under-test', propertyNameInclusion) + then: + inclusionResolver.getInclusion(node) == expectedInclusion + where: + propertyNameInclusion | otherExistingInclusion || expectedInclusion + null | INCLUDED || DEFAULT + DEFAULT | EXCLUDED || DEFAULT + INCLUDED | DEFAULT || INCLUDED + EXCLUDED | DEFAULT || EXCLUDED + } + + def "GetInclusion: returns INCLUDED when the node itself is not explicitly included but its parent is"() { + given: + def parentNode = Mock(DiffNode, { + getPropertyName() >> 'parent-property' + }) + def node = Mock(DiffNode, { + getPropertyName() >> 'child-property' + getParentNode() >> parentNode + }) + when: + inclusionResolver.setInclusion(parentNode.getPropertyName(), INCLUDED) + then: + inclusionResolver.getInclusion(node) == INCLUDED + } + + def "EnablesStrictIncludeMode: is true when at least one INCLUDE has been configured"() { + expect: + !inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion('property', INCLUDED) + then: + inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion('property', EXCLUDED) + then: + !inclusionResolver.enablesStrictIncludeMode() + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/TypeInclusionResolverTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/TypeInclusionResolverTest.groovy new file mode 100644 index 00000000..a6e2ca4b --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/TypeInclusionResolverTest.groovy @@ -0,0 +1,91 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.* + +@Subject(TypeInclusionResolver) +class TypeInclusionResolverTest extends Specification { + + TypeInclusionResolver inclusionResolver = new TypeInclusionResolver() + + def "GetInclusion: returns DEFAULT when no INCLUDE or EXCLUDE has been configured"() { + given: + inclusionResolver.setInclusion(Object, configuredInclusion) + expect: + inclusionResolver.getInclusion(Mock(DiffNode)) == DEFAULT + where: + configuredInclusion << [null, DEFAULT] + } + + def "GetInclusion: returns DEFAULT when node has no valueType"() { + given: + def node = Mock(DiffNode, { getValueType() >> null }) + and: + inclusionResolver.setInclusion(String, INCLUDED) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + def "GetInclusion: returns DEFAULT when no inclusion for the nodes valueType has been configured"() { + given: + def node = Mock(DiffNode, { getValueType() >> Date }) + and: + inclusionResolver.setInclusion(String, INCLUDED) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + @Unroll + def "GetInclusion: returns #expectedInclusion when the configured inclusion for the nodes valueType is #configuredInclusion"() { + given: + def valueType = String + def node = Mock(DiffNode, { getValueType() >> valueType }) + and: 'ensure the inclusion resolver is "active"' + inclusionResolver.setInclusion(Object, otherInclusionToActivateResolver) + and: + inclusionResolver.setInclusion(valueType, configuredInclusion) + expect: + inclusionResolver.getInclusion(node) == expectedInclusion + where: + otherInclusionToActivateResolver | configuredInclusion || expectedInclusion + INCLUDED | null || DEFAULT + EXCLUDED | DEFAULT || DEFAULT + DEFAULT | INCLUDED || INCLUDED + DEFAULT | EXCLUDED || EXCLUDED + } + + def "EnablesStrictIncludeMode: is true when at least one INCLUDE has been configured"() { + expect: + !inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion(Object, INCLUDED) + then: + inclusionResolver.enablesStrictIncludeMode() + + when: + inclusionResolver.setInclusion(Object, EXCLUDED) + then: + !inclusionResolver.enablesStrictIncludeMode() + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/TypePropertyAnnotationInclusionResolverTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/TypePropertyAnnotationInclusionResolverTest.groovy new file mode 100644 index 00000000..b7b01ebc --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/TypePropertyAnnotationInclusionResolverTest.groovy @@ -0,0 +1,154 @@ +/* + * 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.inclusion +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.instantiation.TypeInfo +import de.danielbechler.diff.introspection.ObjectDiffProperty +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.* +/** + * Created by Daniel Bechler. + */ +@Subject(TypePropertyAnnotationInclusionResolver) +class TypePropertyAnnotationInclusionResolverTest extends Specification { + def inclusionResolver = new TypePropertyAnnotationInclusionResolver() + def node = Stub DiffNode, { + getPropertyAnnotation(ObjectDiffProperty) >> null + getParentNode() >> Stub(DiffNode, { + getValueTypeInfo() >> Stub(TypeInfo, { + getAccessors() >> [ + Stub(PropertyAwareAccessor, { getReadMethodAnnotation(ObjectDiffProperty) >> null }) + ] + }) + }) + } + + def 'should return DEFAULT when node has no annotated inclusion and no siblings'() { + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + @Unroll + def 'should return #expected when annotated inclusion is #inclusion'() { + def annotation = Mock(ObjectDiffProperty) + annotation.inclusion() >> inclusion + + when: + def result = inclusionResolver.getInclusion(node) + then: + node.getPropertyAnnotation(ObjectDiffProperty) >> annotation + and: + result == expected + where: + inclusion || expected + null || DEFAULT + DEFAULT || DEFAULT + INCLUDED || INCLUDED + EXCLUDED || EXCLUDED + } + + def 'should return EXCLUDED when excluded() is true'() { + when: + def result = inclusionResolver.getInclusion(node) + then: + node.getPropertyAnnotation(ObjectDiffProperty) >> Stub(ObjectDiffProperty, { excluded() >> true }) + and: + result == EXCLUDED + } + + def 'should ignore excluded() = true when inclusion() is set to INCLUDED'() { + when: + def result = inclusionResolver.getInclusion(node) + then: + node.getPropertyAnnotation(ObjectDiffProperty) >> Stub(ObjectDiffProperty, { + excluded() >> true + inclusion() >> INCLUDED + }) + and: + result == INCLUDED + } + + @Unroll + def 'should honor excluded() = true when inclusion() is set to #inclusion'() { + when: + def result = inclusionResolver.getInclusion(node) + then: + node.getPropertyAnnotation(ObjectDiffProperty) >> Stub(ObjectDiffProperty, { + excluded() >> true + inclusion() >> inclusion + }) + and: + result == expected + where: + inclusion || expected + null || EXCLUDED + DEFAULT || EXCLUDED + } + + def 'should return EXCLUDED when node is not explicitly included but any of its siblings is'() { + when: + def result = inclusionResolver.getInclusion(node) + then: + node.parentNode.valueTypeInfo.accessors >> [propertyAccessorWithInclusion(INCLUDED)] + and: + result == EXCLUDED + } + + def 'should return DEFAULT when node is not explicitly included but any of its siblings is excluded'() { + when: + def result = inclusionResolver.getInclusion(node) + then: + node.parentNode.valueTypeInfo.accessors >> [propertyAccessorWithInclusion(EXCLUDED)] + and: + result == DEFAULT + } + + def 'should not throw an exception when no parent node exists'() { + when: + inclusionResolver.getInclusion(node) + then: + node.parentNode >> null + and: + noExceptionThrown() + } + + def 'should not throw an exception when parent node has no TypeInfo'() { + when: + inclusionResolver.getInclusion(node) + then: + node.parentNode.valueTypeInfo >> null + and: + noExceptionThrown() + } + + private propertyAccessorWithInclusion(Inclusion inclusionValue) { + Stub(PropertyAwareAccessor, { + getReadMethodAnnotation(ObjectDiffProperty) >> Stub(ObjectDiffProperty, { + inclusion() >> inclusionValue + }) + }) + } + + def "EnabledStrictIncludeMode: should always be false"() { + expect: + !inclusionResolver.enablesStrictIncludeMode() + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/TypePropertyConfigInclusionResolverTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/TypePropertyConfigInclusionResolverTest.groovy new file mode 100644 index 00000000..b73d1d47 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/TypePropertyConfigInclusionResolverTest.groovy @@ -0,0 +1,123 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.access.Accessor +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.* + +@Subject(TypePropertyConfigInclusionResolver) +class TypePropertyConfigInclusionResolverTest extends Specification { + + def inclusionResolver = new TypePropertyConfigInclusionResolver() + + def 'should return DEFAULT when node is not property aware'() { + given: + def node = new DiffNode(null, Stub(Accessor), null) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + def 'should return DEFAULT when node has no parent'() { + given: + def propertyAwareAccessor = Stub(PropertyAwareAccessor) + def node = new DiffNode(null, propertyAwareAccessor, null) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + def 'should return DEFAULT when parent node has no type'() { + given: + def parentNode = DiffNode.newRootNode() + def node = new DiffNode(parentNode, Stub(PropertyAwareAccessor), null) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + def "should return DEFAULT when node doesn't have a property name"() { + given: + def node = createPropertyNode(ObjectForTesting, null) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + @Unroll + def 'should return #expected when inclusion for node is #inclusion'() { + given: + def node = createPropertyNode(ObjectForTesting, 'foo') + and: + inclusionResolver.setInclusion(ObjectForTesting, 'foo', inclusion) + expect: + inclusionResolver.getInclusion(node) == expected + where: + inclusion || expected + null || DEFAULT + DEFAULT || DEFAULT + INCLUDED || INCLUDED + EXCLUDED || EXCLUDED + } + + def 'should return EXCLUDED when the nodes inclusion is DEFAULT but any of its siblings are INCLUDED'() { + given: + def node = createPropertyNode(ObjectForTesting, 'foo') + and: + inclusionResolver.setInclusion(ObjectForTesting, 'bar', INCLUDED) + expect: + inclusionResolver.getInclusion(node) == EXCLUDED + } + + def 'should return DEFAULT when the node has siblings with inclusions but none if them is INCLUDED'() { + given: + def node = createPropertyNode(ObjectForTesting, 'foo') + and: + inclusionResolver.setInclusion(ObjectForTesting, 'bar', inclusion) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + where: + inclusion << [null, DEFAULT, EXCLUDED] + } + + def 'should not be affected by inclusions from other parent types'() { + given: + def node = createPropertyNode(ObjectForTesting, 'foo') + and: + inclusionResolver.setInclusion(ObjectForTesting, 'foo', DEFAULT) + inclusionResolver.setInclusion(Object, 'foo', EXCLUDED) + expect: + inclusionResolver.getInclusion(node) == DEFAULT + } + + def "EnabledStrictIncludeMode: should always be false"() { + expect: + !inclusionResolver.enablesStrictIncludeMode() + } + + DiffNode createPropertyNode(Class parentType, String propertyName) { + def parentNode = DiffNode.newRootNodeWithType(parentType) + def accessor = Stub(PropertyAwareAccessor, { getPropertyName() >> propertyName }) + return new DiffNode(parentNode, accessor, null) + } + + class ObjectForTesting { + def foo + } +} diff --git a/src/test/java/de/danielbechler/diff/inclusion/ValueNodeTest.groovy b/src/test/java/de/danielbechler/diff/inclusion/ValueNodeTest.groovy new file mode 100644 index 00000000..b37fd8c8 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/inclusion/ValueNodeTest.groovy @@ -0,0 +1,151 @@ +/* + * 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.inclusion + +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import de.danielbechler.diff.selector.RootElementSelector +import spock.lang.Specification +import spock.lang.Unroll + +import static de.danielbechler.diff.inclusion.Inclusion.EXCLUDED +import static de.danielbechler.diff.inclusion.Inclusion.INCLUDED + +class ValueNodeTest extends Specification { + + def node = new ValueNode() + + def 'GetChild: throws an exception when given a RootElementSelector'() { + when: + node.getChild(RootElementSelector.instance) + then: + thrown(IllegalArgumentException) + where: + selector << [ + RootElementSelector.instance, + [RootElementSelector.instance] + ] + } + + def 'GetChild: creates a new child for unknown selector'() { + given: + def childElementSelector = new BeanPropertyElementSelector('foo') + + when: "called with unknown selector" + def childNode = node.getChild(childElementSelector) + + then: "returns new child node" + childNode.elementSelector == childElementSelector + + and: "the next call returns the same child node" + node.getChild(childElementSelector) == childNode + } + + def 'GetChild: returns the same child on consecutive calls with the same selector'() { + setup: + def childElementSelector = new BeanPropertyElementSelector('foo') + + expect: "called with unknown selector" + node.getChild(childElementSelector) is node.getChild(childElementSelector) + + and: + node.getChild(childElementSelector).elementSelector == childElementSelector + } + + def 'GetChild: sets reference to parent when creating new child nodes'() { + when: + def childNode = node.getChild(new BeanPropertyElementSelector('foo')) + + then: + node == childNode.parent + } + + def 'GetChild: creates child node hierarchy for list of ElementSelectors'() { + when: + node.getNodeForPath(NodePath.with('foo', 'bar')) + then: + node.hasChild(new BeanPropertyElementSelector('foo')) + and: + node.getChild(new BeanPropertyElementSelector('foo')).hasChild(new BeanPropertyElementSelector('bar')) + } + + @Unroll + def 'HasValue: returns #expected when value is #valueText'() { + when: + node.setValue(value) + then: + node.hasValue() == expected + where: + value || expected + INCLUDED || true + null || false + valueText = value == null ? 'not set' : 'set' + } + + def 'GetNodeForPath: returns root node for root path'() { + expect: + node.getNodeForPath(NodePath.withRoot()) is node + } + + def 'GetNodeForPath: returns child node for child path'() { + expect: + node.getNodeForPath(NodePath.with('foo')).elementSelector == new BeanPropertyElementSelector('foo') + } + + def 'GetNodeForPath: always starts at the root node'() { + given: + def childNode = node.getNodeForPath(NodePath.with('foo', 'bar')) + + expect: + childNode.getNodeForPath(NodePath.with('foo', 'bar')) is childNode + } + + def 'ContainsValue: is true when the node has the requested value'() { + when: + node.setValue(INCLUDED) + then: + node.containsValue(INCLUDED) + } + + def 'ContainsValue: is true when any child node has the requested value'() { + given: + def childNode = node.getChild(new BeanPropertyElementSelector('foo')) + when: + childNode.setValue(INCLUDED) + then: + node.containsValue(INCLUDED) + } + + def 'ContainsValue: is false when no node has the requested value'() { + given: + def childNode = node.getChild(new BeanPropertyElementSelector('foo')) + childNode.setValue(EXCLUDED) + expect: + !node.containsValue(INCLUDED) + } + + def 'GetValue: returns the assigned value'() { + when: + node.setValue(INCLUDED) + then: + node.getValue() == INCLUDED + when: + node.setValue(EXCLUDED) + then: + node.getValue() == EXCLUDED + } +} diff --git a/src/test/java/de/danielbechler/diff/introspection/DefaultPropertyAccessExceptionHandlerTest.groovy b/src/test/java/de/danielbechler/diff/introspection/DefaultPropertyAccessExceptionHandlerTest.groovy new file mode 100644 index 00000000..67736daa --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/DefaultPropertyAccessExceptionHandlerTest.groovy @@ -0,0 +1,36 @@ +/* + * 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.introspection + +import de.danielbechler.diff.node.DiffNode +import spock.lang.Specification + +class DefaultPropertyAccessExceptionHandlerTest extends Specification { + + def exceptionHandler = new DefaultPropertyAccessExceptionHandler() + def diffNode = Mock DiffNode + + def 'onPropertyReadException should rethrow the PropertyReadException'() { + given: + def readException = new PropertyReadException('foo', Object, null) + when: + exceptionHandler.onPropertyReadException(readException, diffNode) + then: + def thrownException = thrown(PropertyReadException) + thrownException.is readException + } +} diff --git a/src/test/java/de/danielbechler/diff/introspection/IntrospectionServiceTest.groovy b/src/test/java/de/danielbechler/diff/introspection/IntrospectionServiceTest.groovy new file mode 100644 index 00000000..de6ca96e --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/IntrospectionServiceTest.groovy @@ -0,0 +1,201 @@ +/* + * 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.introspection +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.access.TypeAwareAccessor +import de.danielbechler.diff.circular.CircularReferenceMatchingMode +import de.danielbechler.diff.inclusion.Inclusion +import de.danielbechler.diff.instantiation.TypeInfo +import de.danielbechler.diff.mock.ObjectWithString +import de.danielbechler.diff.node.DiffNode +import de.danielbechler.diff.path.NodePath +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll +/** + * @author Daniel Bechler + */ +class IntrospectionServiceTest extends Specification { + @Shared + def primitiveTypes = [null, int, short, long, boolean, char, byte, float, double, float] + + @Shared + def primitiveWrapperTypes = [Integer, Short, Long, Boolean, Character, Byte, Double, Float] + + def objectDifferBuilder = Mock(ObjectDifferBuilder) + def introspectionService = new IntrospectionService(objectDifferBuilder) + def rootNode = DiffNode.newRootNodeWithType(ObjectWithString) + def childNode + def childAccessor = Mock(TypeAwareAccessor) + def defaultIntrospector = Mock(Introspector) + + def "setup"() { + introspectionService.setDefaultIntrospector(defaultIntrospector) + } + + @Unroll + def 'introspection should always be disabled for #type'() { + given: + childAccessor.getType() >> type + childNode = new DiffNode(rootNode, childAccessor, type) + introspectionService.ofType(type).toBeEnabled() + + expect: + !introspectionService.isIntrospectable(childNode) + + where: + type << primitiveTypes + primitiveWrapperTypes + } + + def 'introspection should always be disabled for Arrays'() { + given: + rootNode = DiffNode.newRootNodeWithType(type) + introspectionService.ofType(type).toBeEnabled() + + expect: + !introspectionService.isIntrospectable(rootNode) + + //noinspection GroovyAssignabilityCheck + where: + type << [int[], String[], Object[]] + } + + def 'introspection should always be disabled for Enums'() { + given: + rootNode = DiffNode.newRootNodeWithType(type) + introspectionService.ofType(type).toBeEnabled() + + expect: + !introspectionService.isIntrospectable(rootNode) + + where: + type << [Inclusion, CircularReferenceMatchingMode] + } + + def 'introspection should always be disabled for nodes with unknown type (null)'() { + given: + rootNode = DiffNode.newRootNode() + introspectionService.ofType(null).toBeEnabled() + + expect: + !introspectionService.isIntrospectable(rootNode) + } + + def 'introspection can be enabled via type'() { + given: + introspectionService.ofType(ObjectWithString).toBeEnabled() + + expect: + introspectionService.isIntrospectable(rootNode) + } + + def 'introspection can be disabled via type'() { + given: + introspectionService.ofType(ObjectWithString).toBeDisabled() + + expect: + !introspectionService.isIntrospectable(rootNode) + } + + def 'introspection can be re-enabled via type'() { + given: + introspectionService.ofType(ObjectWithString).toBeDisabled() + introspectionService.ofType(ObjectWithString).toBeEnabled() + + expect: + introspectionService.isIntrospectable(rootNode) + } + + def 'introspection can be enabled via node'() { + given: + introspectionService.ofNode(rootNode.path).toBeEnabled() + + expect: + introspectionService.isIntrospectable(rootNode) + } + + def 'introspection can be disabled via node'() { + given: + introspectionService.ofNode(rootNode.path).toBeDisabled() + + expect: + !introspectionService.isIntrospectable(rootNode) + } + + def 'introspection can be re-enabled via node'() { + given: + introspectionService.ofNode(rootNode.path).toBeDisabled() + introspectionService.ofNode(rootNode.path).toBeEnabled() + + expect: + introspectionService.isIntrospectable(rootNode) + } + + def 'node configuration overrules type configuration'() { + given: + introspectionService.ofNode(rootNode.path).toBeDisabled() + introspectionService.ofType(ObjectWithString).toBeEnabled() + + expect: + !introspectionService.isIntrospectable(rootNode) + } + + def 'attempting to set default introspector to null should cause IllegalArgumentException'() { + when: + introspectionService.setDefaultIntrospector(null) + + then: + thrown(IllegalArgumentException) + } + + def 'introspectorForNode returns default introspector if no special configuration exists'() { + given: + def introspector = Mock(Introspector) + introspectionService.setDefaultIntrospector(introspector) + + expect: + introspectionService.introspectorForNode(rootNode) == introspector + } + + def 'introspectorForNode returns introspector configured via type'() { + given: + def typeIntrospector = Mock(Introspector) + introspectionService.ofType(String).toUse(typeIntrospector) + rootNode = DiffNode.newRootNode() + rootNode.setValueTypeInfo(new TypeInfo(String)) + + expect: + introspectionService.introspectorForNode(rootNode) == typeIntrospector + } + + def 'introspectorForNode returns introspector configured via node path'() { + given: + def nodeIntrospector = Mock(Introspector) + introspectionService.ofNode(NodePath.withRoot()).toUse(nodeIntrospector) + + expect: + introspectionService.introspectorForNode(rootNode) == nodeIntrospector + } + + def 'and() returns original ObjectDifferBuilder'() { + given: + objectDifferBuilder = Mock(ObjectDifferBuilder) + introspectionService = new IntrospectionService(objectDifferBuilder) + expect: + introspectionService.and().is objectDifferBuilder + } +} diff --git a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorBuilder.java b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorBuilder.java similarity index 82% rename from src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorBuilder.java rename to src/test/java/de/danielbechler/diff/introspection/PropertyAccessorBuilder.java index 7806903b..68913d9f 100644 --- a/src/test/java/de/danielbechler/diff/bean/BeanPropertyAccessorBuilder.java +++ b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -14,14 +14,16 @@ * limitations under the License. */ -package de.danielbechler.diff.bean; +package de.danielbechler.diff.introspection; -import java.lang.reflect.*; +import java.lang.reflect.Method; -/** @author Daniel Bechler */ -public final class BeanPropertyAccessorBuilder +/** + * @author Daniel Bechler + */ +final class PropertyAccessorBuilder { - private BeanPropertyAccessorBuilder() + private PropertyAccessorBuilder() { } @@ -32,6 +34,21 @@ public static NameAndType forPropertyOf(final Class targetType) return builder; } + public interface Buildable + { + PropertyAccessor build(); + } + + public interface NameAndType + { + ReadOnly property(String name, Class type); + } + + public interface ReadOnly extends Buildable + { + Buildable readOnly(boolean readOnly); + } + public static class Builder implements NameAndType, ReadOnly, Buildable { private String propertyName; @@ -52,7 +69,7 @@ public Buildable readOnly(final boolean readOnly) return this; } - public BeanPropertyAccessor build() + public PropertyAccessor build() { try { @@ -66,9 +83,9 @@ public BeanPropertyAccessor build() { writeMethod = targetType.getDeclaredMethod(name("set"), propertyType); } - return new BeanPropertyAccessor(propertyName, readMethod, writeMethod); + return new PropertyAccessor(propertyName, readMethod, writeMethod); } - catch (NoSuchMethodException e) + catch (final NoSuchMethodException e) { throw new RuntimeException(e); } @@ -79,19 +96,4 @@ private String name(final String prefix) return prefix + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); } } - - public interface Buildable - { - BeanPropertyAccessor build(); - } - - public interface NameAndType - { - ReadOnly property(String name, Class type); - } - - public interface ReadOnly extends Buildable - { - Buildable readOnly(boolean readOnly); - } } diff --git a/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorCollectionTest.groovy b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorCollectionTest.groovy new file mode 100644 index 00000000..8f192a0a --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorCollectionTest.groovy @@ -0,0 +1,77 @@ +/* + * 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.introspection + +import de.danielbechler.diff.mock.ObjectWithCollection +import spock.lang.Specification + +import java.lang.reflect.Method + +import static java.util.Collections.singletonList + +/** + * @author Daniel Bechler + */ +public class PropertyAccessorCollectionTest extends Specification { + + PropertyAccessor beanPropertyAccessor + + def setup() { + beanPropertyAccessor = createCollectionPropertyAccessor(false) + } + + def 'replace content of mutable target collection if no setter is available'() { + given: + def target = new ObjectWithCollection([]) + def initialCollectionInstance = target.collection + target.collection.add('bar') + beanPropertyAccessor = createCollectionPropertyAccessor(true) + when: + beanPropertyAccessor.set(target, ['foo']) + then: + target.collection == ['foo'] + target.collection.is initialCollectionInstance + } + + def 'assign nothing if target collection is null and no write method is available'() { + given: + def target = new ObjectWithCollection(null) + and: + beanPropertyAccessor = createCollectionPropertyAccessor(true) + when: + beanPropertyAccessor.set(target, singletonList("foo")) + then: + target.collection == null + } + + def 'assign nothing if target collection is immutable and no write method is available'() { + given: + def target = new ObjectWithCollection([].asImmutable()) + beanPropertyAccessor = createCollectionPropertyAccessor(true) + when: + beanPropertyAccessor.set(target, singletonList("foo")) + then: + target.collection.isEmpty() + } + + def PropertyAccessor createCollectionPropertyAccessor(boolean readOnly) { + Method readMethod = ObjectWithCollection.getMethod("getCollection") + Method writeMethod = readOnly ? null : ObjectWithCollection.getMethod("setCollection", Collection) + return new PropertyAccessor("collection", readMethod, writeMethod) + } + +} diff --git a/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorMapTest.groovy b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorMapTest.groovy new file mode 100644 index 00000000..383879ba --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorMapTest.groovy @@ -0,0 +1,72 @@ +/* + * 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.introspection + +import de.danielbechler.diff.mock.ObjectWithMap +import spock.lang.Specification + +import java.lang.reflect.Method + +import static java.util.Collections.singletonMap + +/** + * @author Daniel Bechler + */ +public class PropertyAccessorMapTest extends Specification { + PropertyAccessor accessor + Map targetMap + ObjectWithMap target + + def setup() { + targetMap = [:] + target = new ObjectWithMap() + target.setMap(targetMap) + accessor = createMapPropertyAccessor(true) + } + + def 'assign by replacing existing content if no write method is available'() { + when: + accessor.set(target, singletonMap("foo", "bar")) + then: + targetMap.get('foo') == 'bar' + } + + def 'assign nothing if target map is immutable'() { + given: + targetMap = Collections.emptyMap() + target.setMap(targetMap) + when: + accessor.set(target, singletonMap("foo", "bar")) + then: + targetMap.isEmpty() + } + + def 'assign nothing if target map is null and no write method is available'() { + given: + target.setMap(null) + when: + accessor.set(target, singletonMap("foo", "bar")) + then: + target.map == null + } + + private static PropertyAccessor createMapPropertyAccessor(boolean readOnly) throws NoSuchMethodException { + final Method readMethod = ObjectWithMap.class.getMethod("getMap") + final Method writeMethod = readOnly ? null : ObjectWithMap.class.getMethod("setMap", Map.class) + return new PropertyAccessor("map", readMethod, writeMethod) + } +} diff --git a/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorTest.groovy new file mode 100644 index 00000000..6f871882 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/PropertyAccessorTest.groovy @@ -0,0 +1,227 @@ +/* + * 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.introspection + +import de.danielbechler.diff.mock.ObjectWithAnnotatedProperty +import de.danielbechler.diff.mock.ObjectWithHashCodeAndEquals +import de.danielbechler.diff.mock.ObjectWithStringAndUnsupportedWriteMethod +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import spock.lang.Specification + +import java.lang.annotation.Annotation +import java.lang.reflect.Method + +/** + * @author Daniel Bechler + */ +public class PropertyAccessorTest extends Specification { + PropertyAccessor propertyAccessor + ObjectWithHashCodeAndEquals item + + def 'setup'() { + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(ObjectWithHashCodeAndEquals) + .property("value", String) + .readOnly(false) + .build() + item = new ObjectWithHashCodeAndEquals("foo") + } + + def 'assign new value if property is writable'() { + given: + item.value = null + when: + propertyAccessor.set(item, "bar") + then: + item.value == 'bar' + } + + def 'fail on set if no setter exists'() { + setup: + ObjectWithStringAndUnsupportedWriteMethod target = new ObjectWithStringAndUnsupportedWriteMethod("foo") + Method readMethod = target.getClass().getMethod("getValue") + Method writeMethod = target.getClass().getMethod("setValue", String) + propertyAccessor = new PropertyAccessor("value", readMethod, writeMethod) + when: + propertyAccessor.set(target, "bar") + then: + thrown(PropertyWriteException) + } + + def 'assign nothing if no write method is available'() { + given: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(ObjectWithHashCodeAndEquals) + .property("value", String) + .readOnly(true) + .build() + when: + propertyAccessor.set(item, "bar") + then: + item.value == null + } + + def 'retrieve correct value'() { + given: + item.value = "bar" + when: + final String value = (String) propertyAccessor.get(item) + then: + value == "bar" + } + + def 'retrieve null if target is null'() { + expect: + propertyAccessor.get(null) == null + } + + def 'assign nothing if target is null'() { + expect: + propertyAccessor.set(null, "bar"); // just to make sure no exception is thrown + } + + def 'fail if target does not have expected read method'() { + when: + propertyAccessor.get(new Object()) + then: + thrown(PropertyReadException) + } + + def 'fail if target does not have expected write method'() { + when: + propertyAccessor.set(new Object(), "foo") + then: + thrown(PropertyWriteException) + } + + def 'unset property value'() { + given: + item.value = "bar" + when: + propertyAccessor.unset(item) + then: + item.value == null + } + + def 'return property value type'() { + expect: + propertyAccessor.type == String + } + + def 'return proper path element'() { + expect: + propertyAccessor.elementSelector == new BeanPropertyElementSelector("value") + } + + def 'returns proper property name'() { + expect: + propertyAccessor.propertyName == "value" + } + + def 'toString returns debug friendly string'() { + expect: + propertyAccessor.toString() == 'PropertyAccessor{propertyName=\'value\'' + + ', type=java.lang.String' + + ', source=de.danielbechler.diff.mock.ObjectWithHashCodeAndEquals' + + ', hasWriteMethod=true}' + } + + def 'returns annotations of property getter'() { + setup: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(ObjectWithAnnotatedProperty) + .property("value", String) + .readOnly(false) + .build() + final Annotation[] expectedAnnotations = ObjectWithAnnotatedProperty.class.getMethod("getValue").annotations + expect: + expectedAnnotations.length == 2 + and: + propertyAccessor.readMethodAnnotations.containsAll(expectedAnnotations) + } + + def 'returns specific annotation of property getter'() { + setup: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(ObjectWithAnnotatedProperty) + .property("value", String) + .readOnly(false) + .build() + ObjectDiffProperty expectedAnnotation = ObjectWithAnnotatedProperty.class.getMethod("getValue").getAnnotation(ObjectDiffProperty) + expect: + expectedAnnotation != null + and: + propertyAccessor.getReadMethodAnnotation(ObjectDiffProperty) == expectedAnnotation + } + + def 'returns null if specific annotation of property getter does not exist'() { + given: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(ObjectWithAnnotatedProperty.class) + .property("value", String.class) + .readOnly(false) + .build() + expect: + propertyAccessor.getReadMethodAnnotation(Override.class) == null + } + + def 'getCategoriesFromAnnotation returns empty set when annotation is absent'() { + given: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(AnnotatedType) + .property('unannotatedValue', String) + .readOnly(true) + .build() + expect: + propertyAccessor.categoriesFromAnnotation == [] as Set + } + + def 'getCategoriesFromAnnotation returns set of categories from annotation'() { + given: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(AnnotatedType) + .property('value', String) + .readOnly(true) + .build() + expect: + propertyAccessor.categoriesFromAnnotation == ['A', 'B', 'C'] as Set + } + + class AnnotatedType { + @ObjectDiffProperty(excluded = true, categories = ['A', 'B', 'C']) + public String getValue() { + return 'foo' + } + + public String getUnannotatedValue() { + return 'foo' + } + } + + def 'isExcludedByAnnotation returns value from annotation'() { + given: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(AnnotatedType) + .property('value', String) + .readOnly(true) + .build() + expect: + propertyAccessor.excludedByAnnotation == true + } + + def 'isExcludedByAnnotation returns false when annotation is absent'() { + given: + propertyAccessor = PropertyAccessorBuilder.forPropertyOf(AnnotatedType) + .property('unannotatedValue', String) + .readOnly(true) + .build() + expect: + propertyAccessor.excludedByAnnotation == false + } +} diff --git a/src/test/java/de/danielbechler/diff/introspection/PropertyReadExceptionTest.groovy b/src/test/java/de/danielbechler/diff/introspection/PropertyReadExceptionTest.groovy new file mode 100644 index 00000000..f8b0abe4 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/PropertyReadExceptionTest.groovy @@ -0,0 +1,44 @@ +/* + * 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.introspection + +import spock.lang.Specification + +/** + * Created by Daniel Bechler. + */ +class PropertyReadExceptionTest extends Specification { + def throwable = Mock(Throwable) + def exception = new PropertyReadException('value', ObjectWithValue, throwable) + + def 'should just work'() { + expect: + exception.propertyName == 'value' + exception.targetType == ObjectWithValue + exception.cause.is throwable + } + + def 'getMessage should print useful message'() { + expect: + exception.message == "Failed to read value from property 'value' " + + "of type 'de.danielbechler.diff.introspection.PropertyReadExceptionTest.ObjectWithValue'" + } + + class ObjectWithValue { + def value + } +} diff --git a/src/test/java/de/danielbechler/diff/introspection/PropertyWriteExceptionTest.groovy b/src/test/java/de/danielbechler/diff/introspection/PropertyWriteExceptionTest.groovy new file mode 100644 index 00000000..da6472d2 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/PropertyWriteExceptionTest.groovy @@ -0,0 +1,47 @@ +/* + * 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.introspection + +import spock.lang.Specification + +/** + * Created by Daniel Bechler. + */ +class PropertyWriteExceptionTest extends Specification { + + def throwable = Mock(Throwable) + def exception = new PropertyWriteException('value', ObjectWithValue, 'new-value', throwable) + + def 'should just work'() { + expect: + exception.propertyName == 'value' + exception.targetType == ObjectWithValue + exception.newValue == 'new-value' + exception.cause.is throwable + } + + def 'getMessage should print useful message'() { + expect: + exception.message == "Failed to write new value 'new-value' " + + "to property 'value' " + + "of type 'de.danielbechler.diff.introspection.PropertyWriteExceptionTest.ObjectWithValue'" + } + + class ObjectWithValue { + def value + } +} diff --git a/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy b/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy new file mode 100644 index 00000000..dc204624 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/introspection/StandardIntrospectorTest.groovy @@ -0,0 +1,143 @@ +/* + * 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.introspection + +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.mock.ObjectWithString +import spock.lang.Specification + +import java.beans.BeanInfo +import java.beans.IntrospectionException + +/** + * @author Daniel Bechler + */ +public class StandardIntrospectorTest extends Specification { + + def introspector = new StandardIntrospector() + + private Map introspect(Class type) { + introspector.introspect(type).accessors.collectEntries { + accessor -> [accessor.propertyName, accessor] + } + } + + def 'should return proper accessor for property'() { + when: + def accessor = introspect(TypeWithOnlyOneProperty).get('value') + then: + accessor.propertyName == 'value' + and: + def target = new TypeWithOnlyOneProperty() + accessor.get(target) == null + and: + accessor.set(target, 'bar') + accessor.get(target) == 'bar' + and: + accessor.excludedByAnnotation == false + and: + accessor.categoriesFromAnnotation.isEmpty() + } + + def 'should return PropertyAwareAccessors for each property of the given class'() { + when: + def accessors = introspect(TypeWithTwoProperties) + then: + accessors.size() == 2 + accessors.get('foo') != null + accessors.get('bar') != null + } + + def 'should apply categories of ObjectDiffProperty annotation to accessor'() { + when: + def accessor = introspect(TypeWithPropertyAnnotation).get('value') + then: + accessor.categoriesFromAnnotation.size() == 2 + accessor.categoriesFromAnnotation.containsAll(['category1', 'category2']) + } + + def 'should apply exclusion of ObjectDiffProperty annotation to accessor'() { + when: + def accessor = introspect(TypeWithPropertyAnnotation).get('value') + then: + accessor.excludedByAnnotation == true + } + + def 'should throw exception when invoked without type'() { + when: + introspector.introspect(null) + then: + thrown(IllegalArgumentException) + } + + def 'should skip default class properties'() { + expect: + introspect(TypeWithNothingButDefaultProperties).isEmpty() + } + + def 'should skip properties without getter'() { + expect: + introspect(TypeWithPropertyWithoutGetter).isEmpty() + } + + def 'should wrap IntrospectionException with RuntimeException'() { + given: + introspector = new StandardIntrospector() { + @Override + protected BeanInfo getBeanInfo(final Class type) throws IntrospectionException { + throw new IntrospectionException(type.getCanonicalName()); + } + }; + when: + introspector.introspect(ObjectWithString.class); + then: + thrown(RuntimeException) + } + + private class TypeWithNothingButDefaultProperties { + } + + private class TypeWithPropertyWithoutGetter { + private String value + + void setValue(String value) { + this.value = value + } + } + + private class TypeWithPropertyAnnotation { + private String value + + @ObjectDiffProperty(excluded = true, categories = ['category1', 'category2']) + String getValue() { + return value + } + + void setValue(String value) { + this.value = value + } + } + + private class TypeWithOnlyOneProperty { + def value + } + + private class TypeWithTwoProperties { + def foo + def bar + } +} diff --git a/src/test/java/de/danielbechler/diff/map/MapDifferTest.groovy b/src/test/java/de/danielbechler/diff/map/MapDifferTest.groovy deleted file mode 100644 index 60b7e309..00000000 --- a/src/test/java/de/danielbechler/diff/map/MapDifferTest.groovy +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2012 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.map - -import de.danielbechler.diff.* -import spock.lang.Specification - -import static de.danielbechler.diff.DiffNode.State.* - -/** @author Daniel Bechler */ -public class MapDifferTest extends Specification { - - def MapDiffer mapDiffer - def node - def working = [] as HashMap - def base = [] as HashMap - - def childNode = Mock(DiffNode) - def instances = Mock(Instances) - def differDispatcher = Mock(DifferDispatcher) - def comparisonStrategyResolver = Mock(ComparisonStrategyResolver) - def isReturnableResolver = Mock(IsReturnableResolver) - def comparisonStrategy = Mock(ComparisonStrategy) - - def setup() { - differDispatcher.dispatch(_ as DiffNode, _ as Instances, _ as Accessor) >> childNode - mapDiffer = new MapDiffer(differDispatcher, comparisonStrategyResolver) - instances.sourceAccessor >> RootAccessor.instance - instances.getWorking(Map) >> working - instances.getBase(Map) >> base - isReturnableResolver.isReturnable(_ as DiffNode) >> true - } - - def "mark node as added when map was null and has been changed to an instance"() { - setup: - instances.hasBeenAdded() >> true - - expect: - mapDiffer.compare(DiffNode.ROOT, instances).state == ADDED - } - - def "dispatch comparison of each entry of an added map"() { - given: - working.put('1', 'one') - working.put('2', 'two') - instances.hasBeenAdded() >> true - - when: - node = mapDiffer.compare(DiffNode.ROOT, instances) - - then: - 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('1')) - 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('2')) - } - - def "mark node as removed when map was an instance and has been changed to null"() { - setup: - instances.hasBeenRemoved() >> true - - expect: - mapDiffer.compare(DiffNode.ROOT, instances).state == REMOVED - } - - def "mark node as untouched when map instances are identical"() { - setup: - instances.areSame() >> true - - expect: - mapDiffer.compare(DiffNode.ROOT, instances).state == UNTOUCHED - } - - def "do not compare entries of identical map instances"() { - given: - instances.areSame() >> true - - and: - working.put('foo', 'bar') - - when: - mapDiffer.compare(DiffNode.ROOT, instances) - - then: - 0 * differDispatcher._ - } - - def "dispatch comparison of added map entry"() { - given: - working.put("foo", "bar") - - when: - node = mapDiffer.compare(DiffNode.ROOT, instances) - - then: - 1 * differDispatcher.dispatch(_ as DiffNode, instances, new MapEntryAccessor('foo')) >> childNode - } - - def "dispatch comparison of removed map entry"() { - given: - base.put("foo", "bar") - - when: - node = mapDiffer.compare(DiffNode.ROOT, instances) - - then: - 1 * differDispatcher.dispatch(_ as DiffNode, instances, new MapEntryAccessor('foo')) >> childNode - } - - def "delegate to comparison strategy returned by ComparisonStrategyResolver"() { - given: - comparisonStrategyResolver.resolveComparisonStrategy(_ as DiffNode) >> comparisonStrategy - - when: - node = mapDiffer.compare(DiffNode.ROOT, instances) - - then: - 1 * comparisonStrategy.compare(_ as DiffNode, instances) - } - - def "do not add unreturnable child nodes"() { - given: - working.put("foo", "bar") - - when: - node = mapDiffer.compare(DiffNode.ROOT, instances) - - then: - isReturnableResolver.isReturnable(childNode) >> false - - and: - node.hasChildren() == false - } - - def "dispatch comparison of each entry of a removed map"() { - base.put('1', 'one') - base.put('2', 'two') - instances.hasBeenRemoved() >> true - - when: - node = mapDiffer.compare(DiffNode.ROOT, instances) - - then: - 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('1')) - 1 * differDispatcher.dispatch(_ as DiffNode, _ as Instances, new MapEntryAccessor('2')) - } - - def "fail when constructed without delegator"() { - when: - new MapDiffer(null, comparisonStrategyResolver); - - then: - thrown(IllegalArgumentException) - } - - def "return node with proper parent"() { - given: - def DiffNode parentNode = new DiffNode() - - when: - node = mapDiffer.compare(parentNode, instances) - - then: - node.parentNode == parentNode - } - - def "accepts all implementations of java.util.Map"(Class type, def accept) { - expect: - mapDiffer.accepts(type) == accept - - where: - type || accept - Map || true - HashMap || true - TreeMap || true - LinkedList || false - String || false - null || false - } -} diff --git a/src/test/java/de/danielbechler/diff/map/MapElementTest.groovy b/src/test/java/de/danielbechler/diff/map/MapElementTest.groovy deleted file mode 100644 index 1fb37103..00000000 --- a/src/test/java/de/danielbechler/diff/map/MapElementTest.groovy +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2013 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.map - -import spock.lang.Specification -import spock.lang.Unroll - -class MapElementTest extends Specification { - - def "should be constructable with 'null'"() { - when: - new MapElement(null) - - then: - notThrown(Throwable) - } - - def "should be equal to self"() { - setup: - def element = new MapElement('foo') - - expect: - element.equals(element) - } - - @Unroll - def "equals should be #expected when keys are #key and #anotherKey"() { - given: - def element = new MapElement(key) - def anotherElement = new MapElement(anotherKey) - - expect: - element.equals(anotherElement) == expected - - where: - key | anotherKey || expected - null | null || true - null | 'foo' || false - 'foo' | null || false - 'foo' | 'foo' || true - 'foo' | 'bar' || false - } - - def "should not be equal to 'null'"() { - expect: - !new MapElement('foo').equals(null) - } - - def "should not be equal to different types"() { - expect: - !new MapElement('foo').equals(2) - } - - def "hashCode should equal the hashCode of the key"() { - given: - def key = new Object() - - expect: - new MapElement(key).hashCode() == key.hashCode() - } - - def "hashCode should be null when key is null"() { - expect: - new MapElement(null).hashCode() == 0 - } - - def "toString should return the same as toHumanReadableString()"() { - setup: - def element = new MapElement('foo') - - expect: - element.toString() == element.toHumanReadableString() - } - - def "getKey should return the object passed to the constructor"() { - setup: - def key = new Object() - - expect: - new MapElement(key).key.is(key) - } - - def "toHumanReadableString should return the string representation of the key (without line breaks)"() { - expect: - new MapElement("foo,\nbar").toHumanReadableString() == "{foo, \\ bar}" - } -} diff --git a/src/test/java/de/danielbechler/diff/map/MapEntryAccessorTest.groovy b/src/test/java/de/danielbechler/diff/map/MapEntryAccessorTest.groovy deleted file mode 100644 index 4162d98f..00000000 --- a/src/test/java/de/danielbechler/diff/map/MapEntryAccessorTest.groovy +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright 2012 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.map - -import de.danielbechler.diff.mock.ObjectWithIdentityAndValue -import spock.lang.Shared -import spock.lang.Specification - -/** - * @author Daniel Bechler - */ -class MapEntryAccessorTest extends Specification { - - @Shared - def key1 = new ObjectWithIdentityAndValue("key", "1") - @Shared - def key2 = new ObjectWithIdentityAndValue("key", "2") - def accessor = new MapEntryAccessor("b"); - - def "fail on construction with null referenceKey"() { - when: - new MapEntryAccessor(null); - - then: - thrown(IllegalArgumentException) - } - - def "provide access to its path element"() { - expect: - accessor.getPathElement() instanceof MapElement - } - - def "provide write access to referenced value in any map"() { - given: - def map = [] as TreeMap - - when: - accessor.set(map, "foo") - - then: - map.get("b") == "foo" - - when: - accessor.set(map, "bar") - - then: - map.get("b") == "bar" - } - - def "provide read access to referenced value in any map"() throws Exception { - given: - def map = [] as TreeMap - - when: - map.put("b", "foo"); - - then: - accessor.get(map) == "foo" - } - - def "throw exception when trying to read from non map object"() { - when: - accessor.get(new Object()) - - then: - thrown(IllegalArgumentException) - } - - def "return null when reading from null object"() { - expect: - accessor.get(null) == null - } - - def "remove referenced entry from any map"() { - given: - def map = [] as TreeMap - - when: - map.put("b", "foo") - - and: - accessor.unset(map) - - then: - map.isEmpty() - } - - def "return the key object of the given map"() { - given: - accessor = new MapEntryAccessor(referenceKey) - - and: - def map = [] as HashMap - map.put actualMapKey, "foo" - - expect: - accessor.getKey(map).is actualMapKey - - where: - referenceKey | actualMapKey - key1 | key2 - key1 | key1 - } - - def "return null as the key object if the target object is null"() { - expect: - accessor.getKey(null) == null - } - - def "return null as the key object if the given map does not contain it"() { - setup: - def map = [] as HashMap - map.put("d", "whatever value") - - expect: - accessor.getKey(map) == null - } - - def "toString should make clear it's a map accessor"() { - expect: - accessor.toString() == "map key {b}" - } - - def "unset should be null-safe"() { - when: - accessor.unset null - - then: - notThrown Throwable - } - - def "set should be null-safe"() { - when: - accessor.set null, 'foo' - - then: - notThrown Throwable - } - - def "get should be null-safe"() { - when: - accessor.get null - - then: - notThrown Throwable - } - - def "equals should work as expected"() { - given: - def accessor = new MapEntryAccessor("a") - - expect: - accessor.equals(otherAccessor) == expectedResult - - and: "make sure the same instance is always equal" - accessor.equals(accessor) - - where: - otherAccessor || expectedResult - null || false - "foo" || false - new MapEntryAccessor("b") || false - new MapEntryAccessor("a") || true - } - - def "hashCode should equal hashCode of referenceKey"() { - setup: - def referenceKey = "foo" - - expect: - new MapEntryAccessor(referenceKey).hashCode() == referenceKey.hashCode() - } -} diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectDiffTest.java b/src/test/java/de/danielbechler/diff/mock/ObjectDiffTest.java index ce1fad83..a10e2ced 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectDiffTest.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectDiffTest.java @@ -16,14 +16,18 @@ package de.danielbechler.diff.mock; -import de.danielbechler.diff.annotation.*; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -import java.lang.annotation.*; - -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Inherited -@ObjectDiffAnnotation -public @interface ObjectDiffTest { +public @interface ObjectDiffTest +{ } diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithAccessTrackingIgnoredProperty.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithAccessTrackingIgnoredProperty.java index 3e814f3d..efff5038 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithAccessTrackingIgnoredProperty.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithAccessTrackingIgnoredProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,14 +16,16 @@ package de.danielbechler.diff.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @SuppressWarnings({"MethodMayBeStatic", "UnusedDeclaration"}) public class ObjectWithAccessTrackingIgnoredProperty { - private boolean value; public boolean accessed; + private boolean value; @ObjectDiffProperty(excluded = true) public boolean getValue() diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithAnnotatedProperty.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithAnnotatedProperty.java index 628c8f63..1c598915 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithAnnotatedProperty.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithAnnotatedProperty.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,9 +16,11 @@ package de.danielbechler.diff.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class ObjectWithAnnotatedProperty { private String value; diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithIgnoredMap.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithIgnoredMap.java index ee8ea85b..c17a7dd2 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithIgnoredMap.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithIgnoredMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,11 +16,13 @@ package de.danielbechler.diff.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; -import java.util.*; +import java.util.Map; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ public class ObjectWithIgnoredMap extends ObjectWithMap { @ObjectDiffProperty(excluded = true) diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection.java index ff577d94..b12ef566 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection.java @@ -1,8 +1,24 @@ +/* + * 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.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; -import java.util.*; +import java.util.Collection; public class ObjectWithMethodEqualsOnlyValueProviderMethodOnGetCollection extends ObjectWithCollection { diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap.java index 240e375d..40322f87 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap.java @@ -1,8 +1,24 @@ +/* + * 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.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; -import java.util.*; +import java.util.Map; public class ObjectWithMethodEqualsOnlyValueProviderMethodOnGetMap extends ObjectWithMap { diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject.java index 832741cb..859700b5 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject.java @@ -1,6 +1,22 @@ +/* + * 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.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffProperty; public class ObjectWithMethodEqualsOnlyValueProviderMethodOnGetNestedObject extends ObjectWithNestedObject { diff --git a/src/test/java/de/danielbechler/diff/mock/ObjectWithPropertyAnnotations.java b/src/test/java/de/danielbechler/diff/mock/ObjectWithPropertyAnnotations.java index 36cbb0dc..691d3b24 100644 --- a/src/test/java/de/danielbechler/diff/mock/ObjectWithPropertyAnnotations.java +++ b/src/test/java/de/danielbechler/diff/mock/ObjectWithPropertyAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * 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. @@ -16,14 +16,16 @@ package de.danielbechler.diff.mock; -import de.danielbechler.diff.annotation.*; +import de.danielbechler.diff.introspection.ObjectDiffEqualsOnlyType; +import de.danielbechler.diff.introspection.ObjectDiffProperty; -/** @author Daniel Bechler */ +/** + * @author Daniel Bechler + */ @ObjectDiffEqualsOnlyType public class ObjectWithPropertyAnnotations extends ObjectWithHashCodeAndEquals { private String ignored; - private String equalsOnly; private String categorized; public ObjectWithPropertyAnnotations(final String key) @@ -47,17 +49,6 @@ public void setIgnored(final String ignored) this.ignored = ignored; } - @ObjectDiffProperty(equalsOnly = true) - public String getEqualsOnly() - { - return equalsOnly; - } - - public void setEqualsOnly(final String equalsOnly) - { - this.equalsOnly = equalsOnly; - } - @ObjectDiffProperty(categories = {"foo"}) public String getCategorized() { diff --git a/src/test/java/de/danielbechler/diff/node/CategoryFilteringVisitorTest.groovy b/src/test/java/de/danielbechler/diff/node/CategoryFilteringVisitorTest.groovy new file mode 100644 index 00000000..741a29ae --- /dev/null +++ b/src/test/java/de/danielbechler/diff/node/CategoryFilteringVisitorTest.groovy @@ -0,0 +1,111 @@ +/* + * Copyright 2016 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.node + +import spock.lang.Specification + +class CategoryFilteringVisitorTest extends Specification { + + private interface FilterCallback { + void accept() + + void dismiss() + } + + def node = DiffNode.newRootNode() + def filterCallback = Mock(FilterCallback) + def visitor = new CategoryFilteringVisitor() { + @Override + protected void onAccept(DiffNode node, Visit visit) { + super.onAccept(node, visit) + filterCallback.accept() + } + + @Override + protected void onDismiss(DiffNode node, Visit visit) { + super.onDismiss(node, visit) + filterCallback.dismiss() + } + } + + def setup() { + node.addCategories(['foo']) + } + + def 'dismisses node if category is neither included nor excluded and includeAllNonExcluded is false'() { + given: + visitor.includeAllNonExcluded(false) + when: + visitor.node(node, new Visit()) + then: + 1 * filterCallback.dismiss() + } + + def 'accepts node if category is neither included nor excluded but includeAllNonExcluded is true'() { + given: + visitor.includeAllNonExcluded(true) + when: + visitor.node(node, new Visit()) + then: + 1 * filterCallback.accept() + } + + def 'dismisses node if category is excluded'() { + given: + visitor.includeAllNonExcluded(false) + visitor.exclude('foo') + when: + visitor.node(node, new Visit()) + then: + 1 * filterCallback.dismiss() + } + + def 'accepts node if category is included'() { + given: + visitor.includeAllNonExcluded(false) + visitor.include('foo') + when: + visitor.node(node, new Visit()) + then: + 1 * filterCallback.accept() + } + + def 'on dismiss sets dontGoDeeper flag of Visit'() { + given: + def visit = new Visit() + and: + visitor.includeAllNonExcluded(false) + expect: + visit.allowedToGoDeeper + when: + visitor.node(node, visit) + then: + !visit.allowedToGoDeeper + } + + def 'on accept adds node to matches'() { + given: + visitor.includeAllNonExcluded(false) + visitor.include('foo') + expect: + !visitor.matches.contains(node) + when: + visitor.node(node, new Visit()) + then: + visitor.matches.contains(node) + } +} diff --git a/src/test/java/de/danielbechler/diff/node/DiffNodeFieldAnnotationsTest.groovy b/src/test/java/de/danielbechler/diff/node/DiffNodeFieldAnnotationsTest.groovy new file mode 100644 index 00000000..7f602960 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/node/DiffNodeFieldAnnotationsTest.groovy @@ -0,0 +1,129 @@ +/* + * 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.node + +import de.danielbechler.diff.access.Accessor +import de.danielbechler.diff.introspection.PropertyAccessor +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import spock.lang.Specification + +class DiffNodeFieldAnnotationsTest extends Specification { + + def 'getFieldAnnotation(): returns null if not PropertyAwareAccessor'() { + given: + def accessor = Mock(Accessor) { + getElementSelector() >> new BeanPropertyElementSelector('f') + } + def diffNode = new DiffNode(null, accessor, A) + expect: + diffNode.fieldAnnotations.size() == 0 + diffNode.getFieldAnnotation(SomeFieldAnnotation) == null + } + + def 'getFieldAnnotation(): class has the field and annotated'() { + given: + def accessor = new PropertyAccessor("f", A.getMethod("getF"), A.getMethod("setF", String)) + def diffNode = new DiffNode(null, accessor, A) + expect: + diffNode.fieldAnnotations.size() == 1 + diffNode.getFieldAnnotation(SomeFieldAnnotation) != null + diffNode.getFieldAnnotation(SomeFieldAnnotation).annotationType() == SomeFieldAnnotation + } + + def 'getFieldAnnotation(): class does not have the field, or different name'() { + given: + def accessor = new PropertyAccessor("F", ADiffName.getMethod("getF"), null) + def diffNode = new DiffNode(null, accessor, ADiffName) + expect: + diffNode.fieldAnnotations.size() == 0 + diffNode.getFieldAnnotation(SomeFieldAnnotation) == null + } + + def 'getFieldAnnotation(): inheritance'() { + given: + def accessor = new PropertyAccessor("f", AB.getMethod("getF"), AB.getMethod("setF", String)) + def diffNode = new DiffNode(null, accessor, AB) + expect: + diffNode.fieldAnnotations.size() == 1 + diffNode.getFieldAnnotation(SomeFieldAnnotation) != null + diffNode.getFieldAnnotation(SomeFieldAnnotation).annotationType() == SomeFieldAnnotation + } + + def 'getFieldAnnotation(): inheritance, overridden getter'() { + given: + def accessor = new PropertyAccessor("f", ABGetter.getMethod("getF"), ABGetter.getMethod("setF", String)) + def diffNode = new DiffNode(null, accessor, ABGetter) + expect: + diffNode.fieldAnnotations.size() == 1 + diffNode.getFieldAnnotation(SomeFieldAnnotation) != null + diffNode.getFieldAnnotation(SomeFieldAnnotation).annotationType() == SomeFieldAnnotation + } + + def 'getFieldAnnotation(): inheritance, not annotated'() { + given: + def accessor = new PropertyAccessor("f", NAB.getMethod("getF"), NAB.getMethod("setF", String)) + def diffNode = new DiffNode(null, accessor, NAB) + expect: + diffNode.fieldAnnotations.size() == 0 + diffNode.getFieldAnnotation(SomeFieldAnnotation) == null + } + + def 'getFieldAnnotation(): inheritance, overridden getter, not annotated'() { + given: + def accessor = new PropertyAccessor("f", NABGetter.getMethod("getF"), NABGetter.getMethod("setF", String)) + def diffNode = new DiffNode(null, accessor, NABGetter) + expect: + diffNode.fieldAnnotations.size() == 0 + diffNode.getFieldAnnotation(SomeFieldAnnotation) == null + } + + public static class A { + @SomeFieldAnnotation + String f; + } + + public static class NA { + String f; + } + + public static class ADiffName { + public String getF() { + return null; + } + } + + public static class AB extends A { + } + + public static class ABGetter extends A { + @Override + public String getF() { + return null; + } + } + + public static class NAB extends NA { + } + + public static class NABGetter extends NA { + @Override + public String getF() { + return null; + } + } + +} diff --git a/src/test/java/de/danielbechler/diff/node/DiffNodeTest.groovy b/src/test/java/de/danielbechler/diff/node/DiffNodeTest.groovy new file mode 100644 index 00000000..e23da930 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/node/DiffNodeTest.groovy @@ -0,0 +1,284 @@ +/* + * 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.node + +import de.danielbechler.diff.access.Accessor +import de.danielbechler.diff.access.CollectionItemAccessor +import de.danielbechler.diff.access.PropertyAwareAccessor +import de.danielbechler.diff.mock.ObjectDiffTest +import de.danielbechler.diff.path.NodePath +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import spock.lang.Specification +import spock.lang.Unroll + +import static de.danielbechler.diff.node.DiffNode.State.* + +class DiffNodeTest extends Specification { + + def 'getChild: always starts at root node when called with NodePath'() { + given: + def rootNode = DiffNode.newRootNode() + def childNodeA = new DiffNode(null, new CollectionItemAccessor('A'), null) + def childNodeB = new DiffNode(null, new CollectionItemAccessor('B'), null) + when: + rootNode.addChild(childNodeA) + childNodeA.addChild(childNodeB) + then: + childNodeA.getChild(NodePath.startBuilding().collectionItem('A').collectionItem('B').build()) == childNodeB + } + + @Unroll + def '#stateCheckMethodName: returns true when state is #state'() { + given: + def node = DiffNode.newRootNode() + node.state = state + expect: + node.invokeMethod stateCheckMethodName, null + where: + state << [UNTOUCHED, IGNORED, CIRCULAR, CHANGED, REMOVED, ADDED] + stateCheckMethodName = 'is' + state.name().toLowerCase().capitalize() + } + + @Unroll + def 'hasChanges: returns #expectedHasChangesResult when node state is #state'() { + given: + DiffNode node = DiffNode.newRootNode() + node.state = state + expect: + node.hasChanges() == expectedHasChangesResult + where: + state || expectedHasChangesResult + ADDED || true + REMOVED || true + CHANGED || true + UNTOUCHED || false + IGNORED || false + CIRCULAR || false + } + + def 'hasChanges returns true when any child node has changes'() { + given: + DiffNode rootNode = DiffNode.newRootNode() + expect: + rootNode.hasChanges() == false + when: + rootNode.addChild(Mock(DiffNode, { + 1 * hasChanges() >> true + })) + then: + rootNode.hasChanges() == true + } + + def 'getPropertyPath: returns absolute path for root node'() { + given: + def diffNode = DiffNode.newRootNode() + + expect: + diffNode.path == NodePath.withRoot() + } + + + def 'getPropertyPath: returns absolute path for child node'() { + given: + def parentNode = Mock(DiffNode) { + getPath() >> NodePath.startBuilding().propertyName('a', 'b').build() + } + def accessor = Mock(Accessor) { + getElementSelector() >> new BeanPropertyElementSelector('c') + } + def diffNode = new DiffNode(parentNode, accessor, Object) + + expect: + diffNode.path == NodePath.with('a', 'b', 'c') + } + + def 'addChild: fails with exception when attempting to add root node'() { + given: + def rootNode = DiffNode.newRootNode() + def anotherRootNode = DiffNode.newRootNode() + when: + rootNode.addChild(anotherRootNode) + then: + thrown IllegalArgumentException + } + + def 'addChild: fails with exception when attempting to add node that is already child of another node'() { + given: + def childNode = new DiffNode(DiffNode.newRootNode(), Stub(Accessor), Object) + when: 'adding the child to another node' + DiffNode.newRootNode().addChild(childNode) + then: + thrown IllegalArgumentException + } + + def 'addChild: fails with exception when attempting to add node to itself'() { + given: + def node = DiffNode.newRootNode() + when: + node.addChild node + then: + thrown IllegalArgumentException + } + + def 'addChild: establishes parent-child relationship'() { + given: + def parent = DiffNode.newRootNode() + and: + def childAccessor = Stub Accessor, { + getElementSelector() >> new BeanPropertyElementSelector('foo') + } + def child = new DiffNode(null, childAccessor, String) + when: + parent.addChild(child) + then: 'child has been added to parent' + parent.getChild(child.elementSelector).is child + and: 'parent has been assigned to child' + child.parentNode.is parent + } + + def 'addChild: changes parent node state to CHANGED if child node has changes'() { + given: + def parentNode = DiffNode.newRootNode() + and: + def childNode = new DiffNode(parentNode, Stub(Accessor), String) + childNode.setState(CHANGED) + expect: + parentNode.state == UNTOUCHED + and: + childNode.hasChanges() == true + when: + parentNode.addChild(childNode) + then: + parentNode.state == CHANGED + } + + def 'getPropertyAnnotations: delegates to accessor if it is property aware'() { + given: + ObjectDiffTest annotation = Stub(ObjectDiffTest) + PropertyAwareAccessor accessor = Stub(PropertyAwareAccessor) { + getReadMethodAnnotations() >> [annotation] + } + def node = new DiffNode(null, accessor, Object) + expect: + node.propertyAnnotations.size() == 1 + node.propertyAnnotations.contains(annotation) + } + + def 'getPropertyAnnotations: returns empty set if accessor is not property aware'() { + given: + def node = new DiffNode(null, Stub(Accessor), Object) + expect: + node.propertyAnnotations.isEmpty() + } + + def 'getPropertyAnnotation: should delegate call to property accessor'() { + given: + ObjectDiffTest annotation = Mock(ObjectDiffTest) + PropertyAwareAccessor accessor = Mock(PropertyAwareAccessor) + when: + def node = new DiffNode(DiffNode.newRootNode(), accessor) + node.getPropertyAnnotation(ObjectDiffTest) == annotation + then: + 1 * accessor.getReadMethodAnnotation(ObjectDiffTest) >> annotation + } + + def 'getPropertyAnnotation: should return null if accessor is not a property aware accessor'() { + given: + def accessor = Mock(Accessor) + expect: + def node = new DiffNode(null, accessor, Object) + node.getPropertyAnnotation(ObjectDiffTest) == null + } + + def 'getPropertyName: returns name from PropertyAwareAccessor'() { + given: + def expectedPropertyName = 'foo'; + def nodeWithPropertyName = new DiffNode(null, Stub(PropertyAwareAccessor, { + getPropertyName() >> expectedPropertyName + }), Object) + expect: + nodeWithPropertyName.propertyName == expectedPropertyName + } + + def 'getPropertyName: returns name of parent node if it doesn\'t have one itself'() { + given: + def expectedPropertyName = 'foo' + def parentNodeWithPropertyName = new DiffNode(null, Stub(PropertyAwareAccessor, { + getPropertyName() >> expectedPropertyName + }), Object) + and: + def nodeWithoutPropertyName = new DiffNode(parentNodeWithPropertyName, Stub(Accessor), Object) + expect: + nodeWithoutPropertyName.propertyName == expectedPropertyName + } + + def 'getPropertyName: returns null when property name can not be resolved from accessor'() { + expect: + def node = new DiffNode(null, Mock(Accessor), Object) + node.propertyName == null + } + + @Unroll + def 'isPropertyAware: returns #expectedResult when acessor #doesOrDoesNotImplement PropertyAwareAccessor interface'() { + given: + def node = new DiffNode(null, Stub(accessorType), Object) + expect: + node.isPropertyAware() == expectedResult + where: + accessorType || expectedResult + Accessor || false + PropertyAwareAccessor || true + + doesOrDoesNotImplement = expectedResult ? 'implements' : 'does not implement' + } + + def "should return added categories"() { + given: + def node = new DiffNode(null, Mock(Accessor), Object) + node.addCategories(["addedCategory"] as List) + expect: + node.getCategories() == ["addedCategory"] as Set + } + + def "categories should not be modifiable by a client directly"() { + + when: + def node = new DiffNode(null, Mock(Accessor), Object) + def cats = node.getCategories() + cats.removeAll() + then: + thrown UnsupportedOperationException + } + + def "should throw exception when added a null collection"() { + + when: + def node = new DiffNode(null, Mock(Accessor), Object) + node.addCategories(null) + then: + def ex = thrown(IllegalArgumentException) + ex.message == "'additionalCategories' must not be null" + } + + def 'toString should print a human-readable representation'() { + def accessor = Mock(Accessor) + given: + def node = new DiffNode(null, accessor, TreeSet) + expect: + node.toString() == 'DiffNode(state=UNTOUCHED, type=java.util.TreeSet, no children, accessed via ' + accessor + ')' + } +} diff --git a/src/test/java/de/danielbechler/diff/node/PrintingVisitorTest.groovy b/src/test/java/de/danielbechler/diff/node/PrintingVisitorTest.groovy new file mode 100644 index 00000000..d86023a6 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/node/PrintingVisitorTest.groovy @@ -0,0 +1,76 @@ +/* + * 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.node + +import de.danielbechler.diff.ObjectDifferBuilder +import de.danielbechler.diff.mock.ObjectWithCircularReference +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +public class PrintingVisitorTest extends Specification { + + def 'omits intermediate nodes with changed child nodes'() { + given: + ObjectWithCircularReference a1 = new ObjectWithCircularReference("a") + ObjectWithCircularReference b1 = new ObjectWithCircularReference("b") + ObjectWithCircularReference c1 = new ObjectWithCircularReference("c") + a1.setReference(b1) + b1.setReference(c1) + and: + ObjectWithCircularReference a2 = new ObjectWithCircularReference("a") + ObjectWithCircularReference b2 = new ObjectWithCircularReference("b") + ObjectWithCircularReference d2 = new ObjectWithCircularReference("d") + a2.setReference(b2) + b2.setReference(d2) + when: + DiffNode rootNode = ObjectDifferBuilder.buildDefault().compare(a1, a2) + TestablePrintingVisitor visitor = new TestablePrintingVisitor(a1, a2) + rootNode.visit(visitor) + then: + visitor.output == "Property at path '/reference/reference/id' has changed from [ d ] to [ c ]\n" + } + + def 'prints root node if unchanged and without children'() { + def visitor = new TestablePrintingVisitor("foo", "foo") + + given: + DiffNode rootNode = DiffNode.newRootNodeWithType(String) + when: + rootNode.visit(visitor) + then: + visitor.output == "Property at path '/' has not changed\n" + } + + private static class TestablePrintingVisitor extends PrintingVisitor { + private final StringBuilder sb = new StringBuilder() + + public TestablePrintingVisitor(final Object working, final Object base) { + super(working, base) + } + + @Override + protected void print(final String text) { + sb.append(text).append('\n') + } + + public String getOutput() { + return sb.toString() + } + } +} diff --git a/src/test/java/de/danielbechler/diff/node/SomeFieldAnnotation.java b/src/test/java/de/danielbechler/diff/node/SomeFieldAnnotation.java new file mode 100644 index 00000000..99a32db9 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/node/SomeFieldAnnotation.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013 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.node; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface SomeFieldAnnotation { +} diff --git a/src/test/java/de/danielbechler/diff/node/ToMapPrintingVisitorTest.groovy b/src/test/java/de/danielbechler/diff/node/ToMapPrintingVisitorTest.groovy new file mode 100644 index 00000000..84b2e83b --- /dev/null +++ b/src/test/java/de/danielbechler/diff/node/ToMapPrintingVisitorTest.groovy @@ -0,0 +1,141 @@ +/* + * 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.node + +import de.danielbechler.diff.path.NodePath +import spock.lang.Specification + +public class ToMapPrintingVisitorTest extends Specification { + + ToMapPrintingVisitor visitor + + def setup() { + visitor = new ToMapPrintingVisitor("foo", "bar") + } + + def 'constructor works with null values'() { + when: + new ToMapPrintingVisitor(null, null) + then: + noExceptionThrown() + } + + def 'getMessages returns Map with accepted messages'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.state = DiffNode.State.CHANGED + when: + node.visit(visitor) + then: + def messages = visitor.messages + messages.size() == 1 + messages.containsKey(NodePath.withRoot()) + } + + def 'getMessages returns empty map if no messages have been accepted'() { + expect: + visitor.messages.isEmpty() + } + + def 'getMessages returns modifiable map when messages exist'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.state = DiffNode.State.CHANGED + node.visit(visitor) + when: + def previousSize = visitor.messages.size() + visitor.messages.put(NodePath.with("foo"), "bar") + then: + visitor.messages.size() == previousSize + 1 + } + + def 'getMessages returns modifiable map when no messages exist'() { + when: + visitor.messages.put(NodePath.with("foo"), "bar") + then: + visitor.messages.size() == 1 + } + + def 'getMessage returns message when messages are present'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.state = DiffNode.State.CHANGED + when: + node.visit(visitor) + then: + visitor.getMessage(NodePath.withRoot()).length() > 0 + } + + def 'getMessage_returns_null_when_message_absend'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.setState(DiffNode.State.CHANGED) + when: + node.visit(visitor) + then: + visitor.getMessage(NodePath.with("doesn't-exist")) == null + } + + def 'hasMessages returns true when messages exist'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.state = DiffNode.State.CHANGED + when: + node.visit(visitor) + then: + visitor.hasMessages() == true + } + + def 'hasMessages returns false when no messages exist'() { + expect: + visitor.hasMessages() == false + } + + def 'getMessagesAsString returns line break separated list of messages when messages exist'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.state = DiffNode.State.CHANGED + when: + node.visit(visitor) + then: + visitor.messagesAsString == visitor.getMessage(NodePath.withRoot()) + '\n' + } + + def 'getMessagesAsString returns empty string when no messages exist'() { + expect: + visitor.messagesAsString.isEmpty() + } + + def 'toString is analogous to getMessagesAsString'() { + given: + DiffNode node = DiffNode.newRootNodeWithType(String) + node.state = DiffNode.State.CHANGED + when: + node.visit(visitor) + then: + visitor.toString() == visitor.messagesAsString + } + + def 'clear removes all messages'() { + given: + visitor.messages.put(NodePath.withRoot(), 'foo') + when: + visitor.clear() + then: + visitor.hasMessages() == false + } +} diff --git a/src/main/java/de/danielbechler/diff/bean/BeanPropertyReadException.java b/src/test/java/de/danielbechler/diff/node/VisitTest.groovy similarity index 56% rename from src/main/java/de/danielbechler/diff/bean/BeanPropertyReadException.java rename to src/test/java/de/danielbechler/diff/node/VisitTest.groovy index 2e99b7b6..b4d9ddaf 100644 --- a/src/main/java/de/danielbechler/diff/bean/BeanPropertyReadException.java +++ b/src/test/java/de/danielbechler/diff/node/VisitTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * Copyright 2016 Daniel Bechler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,21 +14,31 @@ * limitations under the License. */ -package de.danielbechler.diff.bean; +package de.danielbechler.diff.node -/** @author Daniel Bechler */ -public class BeanPropertyReadException extends BeanPropertyException -{ - private static final long serialVersionUID = 1L; +import spock.lang.Specification - public BeanPropertyReadException(final Throwable cause) - { - super(cause); +class VisitTest extends Specification { + + def "Stop"() { + given: + final visit = new Visit() + expect: + !visit.stopped + when: + visit.stop() + then: + visit.stopped } - @Override - public String getMessage() - { - return "Failed to invoke read method. " + super.getMessage(); + def "DontGoDeeper"() { + given: + final visit = new Visit() + expect: + visit.allowedToGoDeeper + when: + visit.dontGoDeeper() + then: + !visit.allowedToGoDeeper } } diff --git a/src/test/java/de/danielbechler/diff/path/NodePathTest.groovy b/src/test/java/de/danielbechler/diff/path/NodePathTest.groovy new file mode 100644 index 00000000..783fe6e6 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/path/NodePathTest.groovy @@ -0,0 +1,165 @@ +/* + * 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.path + +import de.danielbechler.diff.selector.BeanPropertyElementSelector +import de.danielbechler.diff.selector.CollectionItemElementSelector +import de.danielbechler.diff.selector.MapKeyElementSelector +import de.danielbechler.diff.selector.RootElementSelector +import spock.lang.Specification +import spock.lang.Unroll + +class NodePathTest extends Specification { + + def 'matches'() { + expect: + path.matches(givenPath) == expected + + where: + path | givenPath || expected + NodePath.with('a') | NodePath.with('a') || true + NodePath.with('a') | NodePath.with('z') || false + } + + @Unroll + def 'isParentOf returns #expected #message'() { + expect: + path.isParentOf(givenPath) == expected + + where: + path | givenPath || expected || message + NodePath.with('a') | NodePath.with('a', 'b') || true || "when given path is child" + NodePath.with('a', 'b') | NodePath.with('a') || false || "when given path is parent" + NodePath.with('a') | NodePath.with('a') || false || "when given path is equal" + NodePath.with('a', 'b') | NodePath.with('z', 'b', 'c') || false || 'when given path doesn\'t share the same ancestry' + } + + @Unroll + def 'isChildOf returns #expected #message'() { + expect: + path.isChildOf(otherPath) == expected + + where: + path | otherPath || expected || message + NodePath.with('a', 'b') | NodePath.with('a') || true || 'when given path is parent' + NodePath.with('a') | NodePath.with('a', 'b') || false || 'when given path is child' + NodePath.with('a') | NodePath.with('a') || false || 'when given path is equal' + NodePath.with('a', 'b') | NodePath.with('z', 'b') || false || 'when given path doesn\'t share the same ancestry' + } + + def 'getLastElementSelector returns last element from path'() { + expect: + path.getLastElementSelector() == expected + + where: + path || expected + NodePath.withRoot() || RootElementSelector.instance + NodePath.with('a', 'b', 'c') || new BeanPropertyElementSelector('c') + } + + def 'equals'() { + expect: + path1.equals(path2) == result + where: + path1 | path2 || result + NodePath.withRoot() | NodePath.withRoot() || true + NodePath.with('a') | NodePath.with('a') || true + NodePath.with('a') | NodePath.with('a', 'b') || false + NodePath.with('a', 'b') | NodePath.with('a') || false + NodePath.with('a') | NodePath.with('b') || false + } + + def 'compareTo'() { + expect: + path1.compareTo(path2) == result + where: + path1 | path2 || result + NodePath.withRoot() | NodePath.withRoot() || 0 + NodePath.with('a') | NodePath.with('a') || 0 + NodePath.with('a') | NodePath.with('a', 'b') || -1 + NodePath.with('a', 'b') | NodePath.with('a') || 1 + NodePath.with('a') | NodePath.with('b') || 1 + } + + def 'startBuilding without elements'() { + expect: + NodePath.startBuilding().build().elementSelectors == [RootElementSelector.instance] + } + + def 'startBuilding with element'() { + def elementSelector = new CollectionItemElementSelector("foo") + expect: + NodePath.startBuilding().element(elementSelector).build().elementSelectors == [ + RootElementSelector.instance, + elementSelector + ] + } + + def 'startBuilding with propertyName'() { + expect: + NodePath.startBuilding().propertyName("foo", "bar").build().elementSelectors == [ + RootElementSelector.getInstance(), + new BeanPropertyElementSelector("foo"), + new BeanPropertyElementSelector("bar") + ] + } + + def 'startBuilding with mapKey'() { + expect: + NodePath.startBuilding().mapKey("foo").build().elementSelectors == [ + RootElementSelector.instance, + new MapKeyElementSelector("foo") + ] + } + + def 'startBuilding with mapKey throws IllegalArgumentException when key is null'() { + when: + NodePath.startBuilding().mapKey(null).build() + then: + thrown IllegalArgumentException + } + + def 'startBuilding with collectionItem'() { + expect: + NodePath.startBuilding().collectionItem("foo").build().elementSelectors == [ + RootElementSelector.instance, + new CollectionItemElementSelector("foo") + ] + } + + def 'startBuildingFrom'() { + expect: + NodePath.startBuildingFrom(NodePath.with("foo")).build().elementSelectors == [ + RootElementSelector.getInstance(), + new BeanPropertyElementSelector("foo") + ] + } + + def 'startBuildingFrom: throws IllegalArgumentException when called with null'() { + when: + NodePath.startBuildingFrom(null).build() + then: + thrown IllegalArgumentException + } + + def 'withRoot: build path with only the root element'() { + expect: + NodePath.withRoot().elementSelectors == [ + RootElementSelector.instance + ] + } +} \ No newline at end of file diff --git a/src/test/java/de/danielbechler/diff/path/NodePathValueHolderTest.groovy b/src/test/java/de/danielbechler/diff/path/NodePathValueHolderTest.groovy new file mode 100644 index 00000000..ccc52c5f --- /dev/null +++ b/src/test/java/de/danielbechler/diff/path/NodePathValueHolderTest.groovy @@ -0,0 +1,49 @@ +package de.danielbechler.diff.path + +import spock.lang.Specification +import spock.lang.Unroll + +/** + * @author Daniel Bechler + */ +class NodePathValueHolderTest extends Specification { + def "should allow creation of new instances"() { + expect: + NodePathValueHolder.of(String) instanceof NodePathValueHolder + } + + @Unroll + def "should store value '#value' at node path '#nodePath'"() { + given: + def valueHolder = NodePathValueHolder.of(String) + valueHolder.put(nodePath, value); + + expect: + valueHolder.valueForNodePath(nodePath) == value; + + where: + nodePath | value + NodePath.withRoot() | "foo1" + NodePath.with("a") | "foo2" + NodePath.with("a", "b") | "foo3" + } + + def "should return null for unknown paths"() { + given: + def valueHolder = NodePathValueHolder.of(String) + + expect: + valueHolder.valueForNodePath(NodePath.with("a")) == null + } + + def "should return accumulated values along path"() { + given: + def valueHolder = NodePathValueHolder.of(String) + valueHolder.put(NodePath.withRoot(), "foo1") + valueHolder.put(NodePath.with("a"), "foo2") + valueHolder.put(NodePath.with("a", "b"), "foo3") + + expect: + valueHolder.accumulatedValuesForNodePath(NodePath.with("a", "b")) == ["foo1", "foo2", "foo3"] + } +} diff --git a/src/test/java/de/danielbechler/diff/primitive/PrimitiveDifferTest.java b/src/test/java/de/danielbechler/diff/primitive/PrimitiveDifferTest.java deleted file mode 100644 index 9b6b249a..00000000 --- a/src/test/java/de/danielbechler/diff/primitive/PrimitiveDifferTest.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2012 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.primitive; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.bean.*; -import org.mockito.Mock; -import org.mockito.invocation.*; -import org.mockito.stubbing.*; -import org.testng.annotations.*; - -import java.util.ArrayList; -import java.util.*; - -import static de.danielbechler.diff.PrimitiveDefaultValueMode.*; -import static de.danielbechler.diff.helper.MockitoAnswers.*; -import static de.danielbechler.diff.helper.MockitoExtensions.*; -import static de.danielbechler.diff.helper.NodeAssertions.*; -import static java.util.Arrays.*; -import static org.mockito.Mockito.*; -import static org.mockito.MockitoAnnotations.*; - -/** @author Daniel Bechler */ -public class PrimitiveDifferTest -{ - private PrimitiveDiffer primitiveDiffer; - - @Mock - private DifferDispatcher differDispatcher; - @Mock - private TypeAwareAccessor accessor; - @Mock - private PrimitiveDefaultValueModeResolver primitiveDefaultValueModeResolver; - @Mock - private Instances instances; - - @BeforeMethod - public void setUp() - { - initMocks(this); - } - - private void given_primitiveDiffer_with_defaultValueMode(final PrimitiveDefaultValueMode mode) - { - when(primitiveDefaultValueModeResolver.resolvePrimitiveDefaultValueMode(any(DiffNode.class))).thenReturn(mode); - primitiveDiffer = new PrimitiveDiffer(primitiveDefaultValueModeResolver); - } - - @Test(dataProvider = "removals") - public void testRemovedWhenPrimitiveDefaultModeIsUnassigned(final Class type, - final Object base, - final Object working, - final Object fresh) throws Exception - { - given_primitiveDiffer_with_defaultValueMode(UNASSIGNED); - given_instances(type, base, working, fresh); - when(instances.hasBeenRemoved()).thenReturn(true); - - final DiffNode node = primitiveDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).self().hasState(DiffNode.State.REMOVED); - - } - - @Test(dataProvider = "changes") - public void testChangedWhenPrimitiveDefaultModeIsUnassigned(final Class type, - final Object base, - final Object working, - final Object fresh) throws Exception - { - given_primitiveDiffer_with_defaultValueMode(UNASSIGNED); - given_instances(type, base, working, fresh); - - final DiffNode node = primitiveDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).self().hasState(DiffNode.State.CHANGED); - } - - @Test(dataProvider = "additions") - public void testAddedWhenPrimitiveDefaultModeIsUnassigned(final Class type, - final Object base, - final Object working, - final Object fresh) throws Exception - { - given_primitiveDiffer_with_defaultValueMode(UNASSIGNED); - given_instances(type, base, working, fresh); - when(instances.hasBeenAdded()).thenReturn(true); - - final DiffNode node = primitiveDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).self().hasState(DiffNode.State.ADDED); - } - - @Test(dataProvider = "changesWhenDefaultModeIsAssigned") - public void testChangedWhenPrimitiveDefaultModeIsAssigned(final Class type, - final Object base, - final Object working, - final Object fresh) throws Exception - { - given_primitiveDiffer_with_defaultValueMode(ASSIGNED); - given_instances(type, base, working, fresh); - - final DiffNode node = primitiveDiffer.compare(DiffNode.ROOT, instances); - - assertThat(node).self().hasState(DiffNode.State.CHANGED); - } - - @Test(dataProvider = "wrapperTypes", expectedExceptions = IllegalArgumentException.class) - public void testThrowsIllegalArgumentExceptionWhenNonPrimitiveTypeIsPassed(final Class wrapperType) throws Exception - { - given_primitiveDiffer_with_defaultValueMode(UNASSIGNED); - instances = mock(Instances.class); - when(instances.getType()).thenAnswer(new Answer() - { - public Object answer(final InvocationOnMock invocation) throws Throwable - { - return String.class; - } - }); -// when(accessor.getType()).then(returnClass(wrapperType)); - - primitiveDiffer.compare(DiffNode.ROOT, instances); - } - - private Instances given_instances(final Class type, - final Object base, - final Object working, - final Object fresh) - { - when(accessor.getType()).then(returnClass(type)); - when(accessor.getPathElement()).thenReturn(new NamedPropertyElement("ignored")); - instances = mock(Instances.class); - when(instances.getWorking()).thenReturn(working); - when(instances.getBase()).thenReturn(base); - when(instances.getFresh()).thenReturn(fresh); - when(instances.getSourceAccessor()).thenReturn(accessor); - when(instances.getType()).thenAnswer(withType(type)); - return instances; - } - - private static Object[] instances(final Class type, final T base, final T working, final T fresh) - { - return new Object[] {type, base, working, fresh}; - } - - @DataProvider - public Object[][] additions() - { - return new Object[][] { - instances(int.class, 0, 1, 0), - instances(long.class, 0L, 1L, 0L), - instances(float.class, 0F, 1F, 0F), - instances(double.class, 0D, 1D, 0D), - instances(boolean.class, false, true, false), - }; - } - - @DataProvider - public Object[][] removals() - { - return new Object[][] { - instances(int.class, 1, 0, 0), - instances(long.class, 1L, 0L, 0L), - instances(float.class, 1F, 0F, 0F), - instances(double.class, 1D, 0D, 0D), - instances(boolean.class, true, false, false), - }; - } - - @DataProvider - public Object[][] changes() - { - return new Object[][] { - instances(int.class, 1, 2, 0), - instances(long.class, 1L, 2L, 0L), - instances(float.class, 1F, 2F, 0F), - instances(double.class, 1D, 2D, 0D), - }; - } - - @DataProvider - public Object[][] wrapperTypes() - { - return new Object[][] { - new Object[] {Integer.class}, - new Object[] {Long.class}, - new Object[] {Float.class}, - new Object[] {Double.class}, - new Object[] {Short.class}, - new Object[] {Byte.class}, - new Object[] {Boolean.class}, - }; - } - - @DataProvider - public Object[][] changesWhenDefaultModeIsAssigned() - { - final List objects = new ArrayList(); - objects.addAll(asList(additions())); - objects.addAll(asList(changes())); - objects.addAll(asList(removals())); - //noinspection ToArrayCallWithZeroLengthArrayArgument - return objects.toArray(new Object[0][0]); - } - -} diff --git a/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy b/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy new file mode 100644 index 00000000..2032ae87 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/selector/CollectionItemElementSelectorTest.groovy @@ -0,0 +1,64 @@ +package de.danielbechler.diff.selector + +import spock.lang.Specification +/** + * @author Daniel Bechler + */ +class CollectionItemElementSelectorTest extends Specification { + def 'can be created with "null" item'() { + expect: + new CollectionItemElementSelector(null) != null + } + + def 'should equal other CollectionElement if items are equal'() { + setup: + def element = new CollectionItemElementSelector("foo") + def equalElement = new CollectionItemElementSelector("foo") + + expect: + element.equals(equalElement) + } + + def 'should not equal elements of different class'() { + setup: + def element = new CollectionItemElementSelector("foo") + def mapElement = new MapKeyElementSelector("foo") + + expect: + !element.equals(mapElement) + } + + def 'should not equal null'() { + setup: + def element = new CollectionItemElementSelector("foo") + + expect: + !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. + + expect: + new CollectionItemElementSelector('foo').hashCode() == 31 + } + + def 'should provide accessor for item'() { + setup: + def item = "foo" + def element = new CollectionItemElementSelector(item) + + expect: + element.getItem() == item + } + + def 'should have proper toString() representation'() { + expect: "string representation of the item should be converted to single line" + new CollectionItemElementSelector("foo\nbar").toString() == '[foo \\ bar]' + } +} diff --git a/src/test/java/de/danielbechler/diff/selector/MapKeyElementSelectorTest.groovy b/src/test/java/de/danielbechler/diff/selector/MapKeyElementSelectorTest.groovy new file mode 100644 index 00000000..590f3934 --- /dev/null +++ b/src/test/java/de/danielbechler/diff/selector/MapKeyElementSelectorTest.groovy @@ -0,0 +1,101 @@ +/* + * Copyright 2013 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.selector + +import spock.lang.Specification +import spock.lang.Unroll + +class MapKeyElementSelectorTest extends Specification { + + def "should be constructable with 'null'"() { + when: + new MapKeyElementSelector(null) + + then: + notThrown(Throwable) + } + + def "should be equal to self"() { + setup: + def element = new MapKeyElementSelector('foo') + + expect: + element.equals(element) + } + + @Unroll + def "equals should be #expected when keys are #key and #anotherKey"() { + given: + def element = new MapKeyElementSelector(key) + def anotherElement = new MapKeyElementSelector(anotherKey) + + expect: + element.equals(anotherElement) == expected + + where: + key | anotherKey || expected + null | null || true + null | 'foo' || false + 'foo' | null || false + 'foo' | 'foo' || true + 'foo' | 'bar' || false + } + + def "should not be equal to 'null'"() { + expect: + !new MapKeyElementSelector('foo').equals(null) + } + + def "should not be equal to different types"() { + expect: + !new MapKeyElementSelector('foo').equals(2) + } + + def "hashCode should equal the hashCode of the key"() { + given: + def key = new Object() + + expect: + new MapKeyElementSelector(key).hashCode() == key.hashCode() + } + + def "hashCode should be null when key is null"() { + expect: + new MapKeyElementSelector(null).hashCode() == 0 + } + + def "toString should return the same as toHumanReadableString()"() { + setup: + def element = new MapKeyElementSelector('foo') + + expect: + element.toString() == element.toHumanReadableString() + } + + def "getKey should return the object passed to the constructor"() { + setup: + def key = new Object() + + expect: + new MapKeyElementSelector(key).key.is(key) + } + + def "toHumanReadableString should return the string representation of the key (without line breaks)"() { + expect: + new MapKeyElementSelector("foo,\nbar").toHumanReadableString() == "{foo, \\ bar}" + } +} diff --git a/src/test/java/de/danielbechler/diff/selector/RootElementSelectorTest.groovy b/src/test/java/de/danielbechler/diff/selector/RootElementSelectorTest.groovy new file mode 100644 index 00000000..7524ce8d --- /dev/null +++ b/src/test/java/de/danielbechler/diff/selector/RootElementSelectorTest.groovy @@ -0,0 +1,52 @@ +/* + * Copyright 2013 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.selector + +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +class RootElementSelectorTest extends Specification { + def 'equals should be true'() { + expect: + RootElementSelector.instance.equals(instance) + + where: + instance << [RootElementSelector.instance, new RootElementSelector()] + } + + def 'equals should be false'() { + expect: + !RootElementSelector.instance.equals(null) + } + + def 'toHumanReadableString should always return empty string'() { + expect: + RootElementSelector.instance.toHumanReadableString() == '' + } + + def 'getInstance should always return the same instance'() { + expect: + RootElementSelector.instance == RootElementSelector.instance + } + + def 'hashCode should always be 0'() { + expect: + RootElementSelector.instance.hashCode() == 0 + } +} diff --git a/src/test/java/de/danielbechler/diff/visitor/PrintingVisitorTest.java b/src/test/java/de/danielbechler/diff/visitor/PrintingVisitorTest.java deleted file mode 100644 index 5e6f10e0..00000000 --- a/src/test/java/de/danielbechler/diff/visitor/PrintingVisitorTest.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2012 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.visitor; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class PrintingVisitorTest -{ - @Test - public void prints_root_node_if_unchanged_and_without_children() - { - final DiffNode rootNode = new DiffNode(String.class); - final TestablePrintingVisitor visitor = new TestablePrintingVisitor("foo", "foo"); - rootNode.visit(visitor); - final String output = visitor.getOutput(); - assertThat(output).isEqualTo("Property at path '/' has not changed\n"); - } - - @Test - public void omits_intermediate_nodes_with_changed_child_nodes() - { - final ObjectWithCircularReference a1 = new ObjectWithCircularReference("a"); - final ObjectWithCircularReference b1 = new ObjectWithCircularReference("b"); - final ObjectWithCircularReference c1 = new ObjectWithCircularReference("c"); - a1.setReference(b1); - b1.setReference(c1); - - final ObjectWithCircularReference a2 = new ObjectWithCircularReference("a"); - final ObjectWithCircularReference b2 = new ObjectWithCircularReference("b"); - final ObjectWithCircularReference d2 = new ObjectWithCircularReference("d"); - a2.setReference(b2); - b2.setReference(d2); - - final DiffNode rootNode = ObjectDifferBuilder.buildDefaultObjectDiffer().compare(a1, a2); - final TestablePrintingVisitor visitor = new TestablePrintingVisitor(a1, a2); - rootNode.visit(visitor); - final String output = visitor.getOutput(); - assertThat(output).isEqualTo("Property at path '/reference/reference/id' has changed from [ d ] to [ c ]\n"); - } - - private static class TestablePrintingVisitor extends PrintingVisitor - { - private final StringBuilder sb = new StringBuilder(); - - public TestablePrintingVisitor(final Object working, final Object base) - { - super(working, base); - } - - @Override - protected void print(final String text) - { - sb.append(text).append('\n'); - } - - public String getOutput() - { - return sb.toString(); - } - } -} diff --git a/src/test/java/de/danielbechler/diff/visitor/ToMapPrintingVisitorTest.java b/src/test/java/de/danielbechler/diff/visitor/ToMapPrintingVisitorTest.java deleted file mode 100644 index e660196b..00000000 --- a/src/test/java/de/danielbechler/diff/visitor/ToMapPrintingVisitorTest.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2012 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.visitor; - -import de.danielbechler.diff.*; -import org.testng.annotations.*; - -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class ToMapPrintingVisitorTest -{ - private ToMapPrintingVisitor visitor; - - @BeforeMethod - public void setUp() throws Exception - { - visitor = new ToMapPrintingVisitor("foo", "bar"); - } - - @Test - public void testConstructor_works_with_null_values() - { - assertThat(new ToMapPrintingVisitor(null, null)).isNotNull(); - } - - @Test - public void testGetMessages_returns_map_with_accepted_messages() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - assertThat(visitor.getMessages()).hasSize(1).containsKey(NodePath.buildRootPath()); - } - - @Test - public void testGetMessages_returns_empty_map_if_no_messages_have_been_accepted() throws Exception - { - assertThat(visitor.getMessages()).isEmpty(); - } - - @Test - public void testGetMessages_returns_modifiable_map_when_messages_exist() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - assertThat(visitor.getMessages()).hasSize(1); - visitor.getMessages().put(NodePath.buildWith("foo"), "bar"); - assertThat(visitor.getMessages()).hasSize(2); - } - - @Test - public void testGetMessages_returns_modifiable_map_when_no_messages_exist() throws Exception - { - assertThat(visitor.getMessages()).hasSize(0); - visitor.getMessages().put(NodePath.buildWith("foo"), "bar"); - assertThat(visitor.getMessages()).hasSize(1); - } - - @Test - public void testGetMessage_returns_message_when_message_present() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - final NodePath path = NodePath.buildRootPath(); - assertThat(visitor.getMessage(path)).isNotEmpty(); - } - - @Test - public void testGetMessage_returns_null_when_message_absend() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - final NodePath path = NodePath.buildWith("doesn't-exist"); - assertThat(visitor.getMessage(path)).isNull(); - } - - @Test - public void testHasMessages_returns_true_when_messages_exist() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - assertThat(visitor.hasMessages()).isTrue(); - } - - @Test - public void testHasMessages_returns_false_when_no_messages_exist() throws Exception - { - assertThat(visitor.hasMessages()).isFalse(); - } - - @Test - public void testGetMessagesAsString_returns_line_break_separated_list_of_messages_when_messages_exist() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - assertThat(visitor.getMessagesAsString()).isEqualTo(visitor.getMessage(NodePath.buildRootPath()) + "\n"); - } - - @Test - public void testGetMessagesAsString_returns_empty_string_when_no_messages_exist() throws Exception - { - assertThat(visitor.getMessagesAsString()).isEmpty(); - } - - @Test - public void testToString_is_analogous_to_getMessagesAsString() throws Exception - { - final DiffNode node = new DiffNode(String.class); - node.setState(DiffNode.State.CHANGED); - node.visit(visitor); - assertThat(visitor.toString()).isEqualTo(visitor.getMessage(NodePath.buildRootPath()) + "\n"); - } - - @Test - public void testClear_removes_all_messages() throws Exception - { - testGetMessages_returns_map_with_accepted_messages(); - visitor.clear(); - assertThat(visitor.hasMessages()).isFalse(); - } -} diff --git a/src/test/java/de/danielbechler/util/ClassesSpec.groovy b/src/test/java/de/danielbechler/util/ClassesSpec.groovy deleted file mode 100644 index bdd9db74..00000000 --- a/src/test/java/de/danielbechler/util/ClassesSpec.groovy +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2013 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.util - -import spock.lang.Specification -import spock.lang.Unroll - -import java.util.concurrent.ConcurrentSkipListMap - -/** - * @author Daniel Bechler - */ -class ClassesSpec extends Specification -{ - @Unroll - def "Classes.mostSpecificSharedType should return #expectedResult for #types"() - { - expect: - Classes.mostSpecificSharedType(types) == expectedResult - - where: - types || expectedResult - [TreeMap, TreeMap] || TreeMap - [ConcurrentSkipListMap, TreeMap] || AbstractMap - [ConcurrentSkipListMap, TreeMap, HashMap] || AbstractMap - [ArrayList, AbstractList] || AbstractList - [CharSequence, String] || CharSequence - [Serializable, Serializable] || Serializable - [String, Map, Date] || null - } -} diff --git a/src/test/java/de/danielbechler/util/ClassesTest.groovy b/src/test/java/de/danielbechler/util/ClassesTest.groovy new file mode 100644 index 00000000..61f6b586 --- /dev/null +++ b/src/test/java/de/danielbechler/util/ClassesTest.groovy @@ -0,0 +1,129 @@ +/* + * Copyright 2013 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.util + +import de.danielbechler.diff.mock.ObjectWithExceptionThrowingDefaultConstructor +import de.danielbechler.diff.mock.ObjectWithPrivateDefaultConstructor +import de.danielbechler.diff.mock.ObjectWithString +import de.danielbechler.diff.mock.ObjectWithoutDefaultConstructor +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +import java.text.Collator +import java.util.concurrent.ConcurrentSkipListMap + +class ClassesTest extends Specification { + + @Shared + static def PRIMITIVE_WRAPPER_TYPE = [Integer, Short, Character, Long, Boolean, Byte, Float, Double] + @Shared + static def COMPLEX_TYPES = [Scanner, Collator, List, Object] + @Shared + static def SIMPLE_TYPES = [ + int, short, char, long, boolean, byte, float, double, + Integer, Short, Character, Long, Boolean, Byte, Float, Double, + CharSequence, String, URL, Locale, Class + ] + + @Unroll + def "mostSpecificSharedType: should return #expectedResult for #types"() { + expect: + Classes.mostSpecificSharedType(types) == expectedResult + + where: + types || expectedResult + [TreeMap, TreeMap] || TreeMap + [ConcurrentSkipListMap, TreeMap] || AbstractMap + [ConcurrentSkipListMap, TreeMap, HashMap] || AbstractMap + [ArrayList, AbstractList] || AbstractList + [CharSequence, String] || CharSequence + [Serializable, Serializable] || Serializable + [String, Map, Date] || null + } + + def 'allAssignableFrom: should return true if all items share the expected type'() { + given: + def types = [ArrayList, LinkedList] + expect: + Classes.allAssignableFrom(List, types) == true; + } + + def 'allAssignableFrom: should return false if not all items share the expected type'() { + given: + def types = [Object, LinkedList] + expect: + Classes.allAssignableFrom(List, types) == false + } + + @Unroll + def 'isPrimitiveWrapperType: should return true for #type'() { + expect: + Classes.isPrimitiveWrapperType(type) == true + where: + type << PRIMITIVE_WRAPPER_TYPE + } + + def 'isPrimitiveWrapperType: should return false for other types'() { + expect: + Classes.isPrimitiveWrapperType(type) == false + where: + type << COMPLEX_TYPES + } + + @Unroll + def 'isSimpleType: should return true for type #type'() { + expect: + Classes.isSimpleType(type) + where: + type << SIMPLE_TYPES + } + + def 'isSimpleType: should return false for other types'() { + expect: + !Classes.isSimpleType(type) + where: + type << COMPLEX_TYPES + } + + def 'freshInstanceOf: should return new instance of desired type'() { + expect: + Classes.freshInstanceOf(ObjectWithString) instanceof ObjectWithString + } + + def 'freshInstanceOf: should return new instance of desired type (even if it has a private default constructor)'() { + expect: + Classes.freshInstanceOf(ObjectWithPrivateDefaultConstructor) instanceof ObjectWithPrivateDefaultConstructor + } + + def 'freshInstanceOf: should return null when desired type has no default constructor'() { + expect: + Classes.freshInstanceOf(ObjectWithoutDefaultConstructor) == null + } + + def 'freshInstanceOf: should return null when desired type is null'() { + expect: + Classes.freshInstanceOf(null) == null + } + + def 'freshInstanceOf: should fail with exception if constructor throws exception'() { + when: + Classes.freshInstanceOf(ObjectWithExceptionThrowingDefaultConstructor) + then: + thrown(RuntimeException) + } +} diff --git a/src/test/java/de/danielbechler/util/ClassesTest.java b/src/test/java/de/danielbechler/util/ClassesTest.java deleted file mode 100644 index f1227c57..00000000 --- a/src/test/java/de/danielbechler/util/ClassesTest.java +++ /dev/null @@ -1,165 +0,0 @@ -/* - * Copyright 2012 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.util; - -import de.danielbechler.diff.*; -import de.danielbechler.diff.example.phonebook.*; -import de.danielbechler.diff.mock.*; -import org.testng.annotations.*; - -import java.net.*; -import java.text.*; -import java.util.*; - -import static org.fest.assertions.api.Assertions.*; - -/** @author Daniel Bechler */ -public class ClassesTest -{ - @Test - public void testAllAssignableFrom_returns_true_when_all_items_share_the_expected_type() throws Exception - { - final Collection> items = new ArrayList>(2); - items.add(ArrayList.class); - items.add(LinkedList.class); - final boolean result = Classes.allAssignableFrom(List.class, items); - assertThat(result).isTrue(); - } - - @Test - public void testAllAssignableFrom_returns_false_if_not_all_items_share_the_expected_type() throws Exception - { - final Collection> items = new ArrayList>(2); - items.add(Object.class); - items.add(LinkedList.class); - final boolean result = Classes.allAssignableFrom(List.class, items); - assertThat(result).isFalse(); - } - - @Test(dataProvider = "wrapperTypes") - public void testIsWrapperType(final Class type) - { - assertThat(Classes.isPrimitiveWrapperType(type)).isTrue(); - } - - @Test(dataProvider = "complexTypes") - public void testIsNotWrapperType(final Class type) - { - assertThat(Classes.isPrimitiveWrapperType(type)).isFalse(); - } - - @Test(dataProvider = "simpleTypes") - public void testIsSimpleType(final Class type) - { - assertThat(Classes.isSimpleType(type)).isTrue(); - } - - @Test(dataProvider = "complexTypes") - public void testIsNotSimpleType(final Class type) - { - assertThat(Classes.isSimpleType(type)).isFalse(); - } - - @Test - public void testFreshInstanceOfClassWithPublicDefaultConstructorReturnsNewInstance() - { - assertThat(Classes.freshInstanceOf(ObjectWithString.class)).isOfAnyClassIn(ObjectWithString.class); - } - - @Test - public void testFreshInstanceOfClassWithPrivateDefaultConstructorReturnsNewInstance() - { - assertThat(Classes.freshInstanceOf(ObjectWithPrivateDefaultConstructor.class)).isOfAnyClassIn(ObjectWithPrivateDefaultConstructor.class); - } - - @Test - public void testFreshInstanceOfClassWithoutDefaultConstructorReturnsNull() - { - assertThat(Classes.freshInstanceOf(ObjectWithoutDefaultConstructor.class)).isNull(); - } - - @Test(expectedExceptions = RuntimeException.class) - public void testFreshInstanceOfThrowsExceptionIfOneIsThrownByDefaultConstructor() - { - Classes.freshInstanceOf(ObjectWithExceptionThrowingDefaultConstructor.class); - } - - @Test - public void testFreshInstanceOfReturnsNullWhenNoClassIsGiven() - { - assertThat(Classes.freshInstanceOf(null)).isNull(); - } - - @DataProvider - public Object[][] wrapperTypes() - { - return new Object[][] { - new Object[] {Integer.class}, - new Object[] {Short.class}, - new Object[] {Character.class}, - new Object[] {Long.class}, - new Object[] {Boolean.class}, - new Object[] {Byte.class}, - new Object[] {Float.class}, - new Object[] {Double.class}, - }; - } - - @DataProvider - public Object[][] complexTypes() - { - return new Object[][] { - new Object[] {Contact.class}, - new Object[] {PhoneBook.class}, - new Object[] {PhoneNumber.class}, - new Object[] {Scanner.class}, - new Object[] {Collator.class}, - }; - } - - @DataProvider - public Object[][] simpleTypes() - { - return new Object[][] { - new Object[] {int.class}, - new Object[] {Integer.class}, - new Object[] {short.class}, - new Object[] {Short.class}, - new Object[] {char.class}, - new Object[] {Character.class}, - new Object[] {long.class}, - new Object[] {Long.class}, - new Object[] {boolean.class}, - new Object[] {Boolean.class}, - new Object[] {byte.class}, - new Object[] {Byte.class}, - new Object[] {float.class}, - new Object[] {Float.class}, - new Object[] {double.class}, - new Object[] {Double.class}, - new Object[] {CharSequence.class}, - new Object[] {String.class}, - new Object[] {Date.class}, - new Object[] {URL.class}, - new Object[] {Locale.class}, - new Object[] {URI.class}, - new Object[] {Number.class}, - new Object[] {DiffNode.State.class}, - new Object[] {Class.class}, - }; - } -} diff --git a/src/test/java/de/danielbechler/util/CollectionsTest.groovy b/src/test/java/de/danielbechler/util/CollectionsTest.groovy new file mode 100644 index 00000000..8a3712e8 --- /dev/null +++ b/src/test/java/de/danielbechler/util/CollectionsTest.groovy @@ -0,0 +1,91 @@ +/* + * 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.util + +import spock.lang.Specification +import spock.lang.Unroll + +class CollectionsTest extends Specification { + + def "setOf: should create a new set from the given input"() { + given: + def input = ['a', 'b', 'c'] + expect: + Collections.setOf(input) == input as Set + and: + !Collections.setOf(input).is(input) + } + + def "setOf: should fail with exception when input is null"() { + when: + Collections.setOf(null) + then: + thrown(NullPointerException) + } + + @Unroll + def "isEmpty: should return #result for collection #collection"() { + expect: + Collections.isEmpty(collection) == result + where: + collection || result + null || true + [] || true + ['with', 'one', 'or', 'more', 'items'] || false + } + + @Unroll + def "containsAny: should return #result for #needles in #haystack"() { + expect: + Collections.containsAny(haystack, needles) == result + where: + haystack | needles || result + null | null || false + [] | [] || false + [] | ['foo'] || false + ['foo'] | [] || false + ['foo'] | ['foo'] || true + ['foo', 'bar'] | ['bar'] || true + + } + + @Unroll + def "filteredCopyOf: should return #result for source #source and filter #filter"() { + expect: + Collections.filteredCopyOf(source, filter) == result + where: + source | filter || result + null | null || [] + [] | null || [] + [] | [] || [] + [] | ['no match'] || [] + ['match'] | ['match'] || [] + ['match', 'no match'] | ['match'] || ['no match'] + ['foo', 'bar'] | [] || ['foo', 'bar'] + } + + @Unroll + def 'firstElementOf: should return #result for collection #collection'() { + expect: + Collections.firstElementOf(collection) == result + where: + collection || result + null || null + [] || null + [1, 2, 3] || 1 + } +} diff --git a/src/test/java/de/danielbechler/util/ComparablesSpec.groovy b/src/test/java/de/danielbechler/util/ComparablesSpec.groovy deleted file mode 100644 index bc3a2f77..00000000 --- a/src/test/java/de/danielbechler/util/ComparablesSpec.groovy +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2013 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.util - -import spock.lang.Specification - -/** - * @author Daniel Bechler - */ -class ComparablesSpec extends Specification -{ - def "isEqualByComparison should return true when both parameters are null"() - { - expect: - Comparables.isEqualByComparison(null, null) - } - - def "isEqualByComparison should return true when both arguments are equal by comparison"() - { - expect: - Comparables.isEqualByComparison(new BigDecimal(10), new BigDecimal(10)) - } - - def "isEqualByComparison should be null-safe even if compareTo method of comparable types is not"() - { - when: - def result = Comparables.isEqualByComparison(a, b) - - then: - !result - - where: - a | b - BigDecimal.ONE | null - null | BigDecimal.TEN - } -} diff --git a/src/test/java/de/danielbechler/util/ComparablesTest.groovy b/src/test/java/de/danielbechler/util/ComparablesTest.groovy new file mode 100644 index 00000000..0c563976 --- /dev/null +++ b/src/test/java/de/danielbechler/util/ComparablesTest.groovy @@ -0,0 +1,78 @@ +/* + * Copyright 2013 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.util + +import spock.lang.Specification + +/** + * @author Daniel Bechler + */ +class ComparablesTest extends Specification { + + def "isEqualByComparison: should return true when both parameters are null"() { + expect: + Comparables.isEqualByComparison(null, null) + } + + def "isEqualByComparison: should return true when both arguments are equal by comparison"() { + expect: + Comparables.isEqualByComparison(new BigDecimal(10), new BigDecimal(10)) + } + + def "isEqualByComparison: should be null-safe even if compareTo method of comparable types is not"() { + when: + def result = Comparables.isEqualByComparison(a, b) + + then: + !result + + where: + a | b + BigDecimal.ONE | null + null | BigDecimal.TEN + } + + def "isEqualByComparison: should return true when either a.compareTo(b) == 0 or b.compareTo(a) == 0"() { + given: + def a = Mock(Comparable) + def b = Mock(Comparable) + + when: + Comparables.isEqualByComparison(a, b) + then: + 1 * a.compareTo(b) >> 0 + 0 * b.compareTo(a) + + when: + Comparables.isEqualByComparison(a, b) + then: + 1 * a.compareTo(b) >> -1 + 1 * b.compareTo(a) >> 0 + } + + def "isEqualByComparison: should return false when neither a.compareTo(b) == 0 or b.compareTo(a) == 0"() { + given: + def a = Mock(Comparable) + def b = Mock(Comparable) + when: + !Comparables.isEqualByComparison(a, b) + then: + 1 * a.compareTo(b) >> -1 + 1 * b.compareTo(a) >> -1 + } + +} diff --git a/src/test/java/de/danielbechler/diff/helper/MockitoExtensions.java b/src/test/java/de/danielbechler/util/StringsTest.groovy similarity index 55% rename from src/test/java/de/danielbechler/diff/helper/MockitoExtensions.java rename to src/test/java/de/danielbechler/util/StringsTest.groovy index c0530cd2..bfd26a60 100644 --- a/src/test/java/de/danielbechler/diff/helper/MockitoExtensions.java +++ b/src/test/java/de/danielbechler/util/StringsTest.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2012 Daniel Bechler + * Copyright 2016 Daniel Bechler * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,24 @@ * limitations under the License. */ -package de.danielbechler.diff.helper; +package de.danielbechler.util -import org.mockito.invocation.*; -import org.mockito.stubbing.*; +import spock.lang.Specification -/** @author Daniel Bechler */ -public class MockitoExtensions -{ - private MockitoExtensions() - { +class StringsTest extends Specification { + + def "join: joins parts with given delimiter"() { + expect: + Strings.join(':', ['a', 'b', 'c']) == 'a:b:c' } - public static Answer> returnClass(final Class clazz) - { - return new Answer>() - { - public Class answer(final InvocationOnMock invocation) throws Throwable - { - return clazz; - } - }; + def "join: ignores null elements"() { + expect: + Strings.join(':', ['a', 'b', null, 'c']) == 'a:b:c' } + def "join: returns empty string when given null"() { + expect: + Strings.join(':', null) == '' + } } diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml index 4c56ff3d..a25ded1d 100644 --- a/src/test/resources/logback-test.xml +++ b/src/test/resources/logback-test.xml @@ -22,7 +22,7 @@ - +