diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..91e9188 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,37 @@ +# Java Gradle CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-java/ for more details +# +version: 2 +jobs: + build: + docker: + # specify the version you desire here + - image: circleci/openjdk:8-jdk + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + working_directory: ~/repo + + steps: + - checkout + + # Download and cache dependencies + - restore_cache: + keys: + - v1-dependencies-{{ checksum "build.gradle" }} + # fallback to using the latest cache if no exact match is found + - v1-dependencies- + + - run: gradle dependencies + + - save_cache: + paths: + - ~/.gradle + key: v1-dependencies-{{ checksum "build.gradle" }} + + # run tests! + - run: gradle test \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..222702d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,4 @@ +# +# Team and People to notify +# +* @amatiushkin \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..920c7d3 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,32 @@ +Open source projects are “living.” Contributions in the form of issues and pull requests are welcomed and encouraged. When you contribute, you explicitly say you are part of the community and abide by its Code of Conduct. + +# The Code + +At Intuit, we foster a kind, respectful, harassment-free cooperative community. Our open source community works to: + +- Be kind and respectful; +- Act as a global community; +- Conduct ourselves professionally. + +As members of this community, we will not tolerate behaviors including, but not limited to: + +- Violent threats or language; +- Discriminatory or derogatory jokes or language; +- Public or private harassment of any kind; +- Other conduct considered inappropriate in a professional setting. + +## Reporting Concerns + +If you see someone violating the Code of Conduct please email TechOpenSource@intuit.com + +## Scope + +This code of conduct applies to: + +All repos and communities for Intuit-managed projects, whether or not the text is included in a Intuit-managed project’s repository; + +Individuals or teams representing projects in official capacity, such as via official social media channels or at in-person meetups. + +## Attribution + +This Code of Conduct is partly inspired by and based on those of Amazon, CocoaPods, GitHub, Microsoft, thoughtbot, and on the Contributor Covenant version 1.4.1. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..3b962e7 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,23 @@ +Thanks for contributing to graphql-java! + + +Please be sure that you read the [Code of Conduct](CODE_OF_CONDUCT.md) before contributing to this project and please +create a new Issue and discuss first what your are planning to do for bigger changes. + + +The overall goal of traverser is to implement support for specific set of use cases in a production ready way. + +In order to achieve that we have a strong focus on maintainability and high test coverage: + +- We expect new or modified unit test for every change (written in [Spock](http://spockframework.org/)) and jUnit. + +- Low dependencies footprint is a must. + +- traverser is dedicated to specific use cases - expect that some suggestions will be out of scope + + +For bug reports or specific code related topics create a new issue. + +Thanks! + + diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5eb2788 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1 @@ +If you are sure you have found a bug, please make sure you follow [contributing](CONTRIBUTING.md) guidelines. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..561e004 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ + + +**1. Issue Link:** + + +**2. Brief explanation of a change:** + + +**3. Will it break existing clients and code in production?** diff --git a/.github/RELEASE_TEMPLATE.md b/.github/RELEASE_TEMPLATE.md new file mode 100644 index 0000000..c6bc359 --- /dev/null +++ b/.github/RELEASE_TEMPLATE.md @@ -0,0 +1,23 @@ +## Location + +Maven: +``` +com.intuit.commons +traverser +NEXT_VERSION +``` + +Gradle: +``` +implementation 'com.intuit.commons:traverser:NEXT_VERSION' +``` + +## Details of what's new + +Release notes: https://github.intuit.com/services-java/traverser/blob/NEXT_VERSION/RELEASE.md + +## Summary of changes + +- add brief one-line explanation of a change +- ??? +- PROFIT diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c178f38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.gradle +gradle +*.iml +.idea +*.class +.DS_Store +.classpath +.project +.settings/ +.nb-gradle/ +*/out/ +*.iws +gradlew +gradlew.bat +build +out +gradle.properties \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..12c253a --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,3 @@ +# +# Traverser Changelog +# \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7a2f164 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2019 Intuit Inc. + + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd9a880 --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# Traverser: java library to walk object graph + + + +Traverser solves a one of the most common tasks to operate on tree or graph data structure: + +- flatten into collection or stream +- perform an action during traversal +- control traversal flow + +It exposes rich and fine level of capabilities like: +- iterators +- both depth- and breadth- first search (DFS/BFS) +- visitors +- local / global context + +It helps in several areas: +- speed up implementation by re-using generic solution (stable, tested, well-performant solution) +- reduce codebase +- expand and adjust use cases with simple changes +- decouples traversing from data structure, which boosts maintenability + +## Getting Started + +Add dependency on this module and traverse any complex data structure. +Gradle: +``` + compile 'com.intuit.commons:traverser:alfa-0.1.0' +``` +Maven: +``` + + com.intuit.commons + traverser + alfa-0.1.0 + pom + +``` + +## Learn by example +Represnting hierarchy of a medium-sized company is good example to illustrate key features of the traverser. +``` +Company + Bussiness Entity + Teams + Members +``` + +Snapshot of a virtual company staff: + +``` +Company + BU-Pacific + Product Development + John Smith + Lucy Gold + Sylvester Stallone + Sales Department + Nick Becker + Ruby Klingemann + Management + Anna Schulze +``` + +## Explore + +### Integration: children provider + +Once of benefits of using generic traversing mechanism is to decouple actual traversing from data structure it is beeing operated on. +Regardles of the way data is organized, core algorithms does not change. But data, root element(s) as well as immediate children for a given elements must be be known to traverser in some way, so the algorithm can move to next iteration. + +Children proivder or simply *\* indicates such function, which feeds traverser with children of current given element. +It could be passed as lamda-function or method reference or any other applicable means. + +### Iterator +Iteration is build on top of graph/tree traversal and shares common API. +There are 2 flavors (go deep or go broad) of direction and 2 flavours (before or after) of invocation. +It give 4 total possible combinations of how iteration can be performed. + + + +Iterators are useful to "flatten" object structure into stream of objects. + +#### Depth-First traversing + +Depth-first always follows reference to child, when next item on current level: + + +##### Perform an action, when move to next: +```java +Company c = new Company(); // +// ... +TraversingIterator i = Traverser.depthFirst().preOrderIterator(c); +// ... +``` + +Output: +``` +Company +BU-Pacific +PD=Product Development +Sylvester Stallone +John Smith +Lucy Gold +SL=Sales Department +Nick Becker +Ruby Klingemann +MG=Management +Anna Schulze +``` + +##### Move to next, perform an action: +```java +Company c = new Company(); // +// ... +TraversingIterator i = Traverser.depthFirst().postOrderIterator(c); +// ... +``` + +Output: +``` +Sylvester Stallone +John Smith +Lucy Gold +PD=Product Development +Nick Becker +Ruby Klingemann +SL=Sales Department +Anna Schulze +MG=Management +BU-Pacific +Company +``` + + +#### Breadth-First traversing + +Breadth-first always follows next item on current level, when goes to reference to child. +##### Perform an action, when move to next: +```java +Company c = new Company(); // +// ... +TraversingIterator i = Traverser.breadthFirst().preOrderIterator(c); +// ... +``` +Output: +``` +Company +BU-Pacific +PD=Product Development +SL=Sales Department +MG=Management +Sylvester Stallone +John Smith +Lucy Gold +Nick Becker +Ruby Klingemann +Anna Schulze +``` + + +##### Move to next, perform an action: +```java +Company c = new Company(); // +// ... +TraversingIterator i = Traverser.breadthFirst().postOrderIterator(c); +// ... +``` +Output +``` +Company +BU-Pacific +PD=Product Development +SL=Sales Department +MG=Management +Sylvester Stallone +John Smith +Lucy Gold +Nick Becker +Ruby Klingemann +Anna Schulze +``` +### Manipulating traversal flow +Traveser accepts [visitors](https://en.wikipedia.org/wiki/Visitor_pattern) which can be used to take action on a given node. + + +#### Configuration + + +It is possible to override `version` and `group` of the artifact: +``` +./gradlew clean -Pproject.version=1.0.0-SNAPSHOT -Pproject.group=com.intuit.commons publishToMavenLocal +``` +where `project.version` and `project.group` are used to control desired group and version. +Artifact name is configured with `rootProject.name` property in *settings.gradle*. + +## Technologies Used + +Minimum required: Java 8 + +This library depends on Apache `commons-collections` and `slf4j`. + +## Contributing Guidelines + +Welcome contributors! + [CONTRIBUTING.md](.github/CONTRIBUTING.md). + +## Local Development + +``` +./gradlew test +``` + + +## Support + +About [opensource](https://opensource.intuit.com) at Intuit. + +## Legal + +Read more about license for this software [License](LICENSE). diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..407cfe0 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,23 @@ +# Release notes +Browse all [releases](https://github.intuit.com/services-java/traverser/releases). + + + + +# alfa-0.1.0 +## Release Notes + +### Extract traverser code from original codebase + +Standalone release of the traverser as oneself artifact. + +---- \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..0124ab7 --- /dev/null +++ b/build.gradle @@ -0,0 +1,77 @@ +plugins { + id "java" + id "org.sonarqube" version "2.7" + id 'maven-publish' +} + + +group = findProperty("project.group") ?: "com.intuit.commons" +version = findProperty("project.version") ?: "${new Date().format('yyyy_MMdd_HHmmss')}-SNAPSHOT" + +def PUBLISH_URI = findProperty("PUBLISH_URL") ?: "" + +allprojects { + repositories { + mavenLocal() + maven { + url "https://oss.sonatype.org/content/repositories/snapshots" + } + + mavenCentral() + } +} + + +dependencies { + + compile 'org.slf4j:slf4j-api:1.7.13' + + // For Collections, Iterators, Predicates, etc. + compile 'org.apache.commons:commons-collections4:4.1' + + testCompile 'org.codehaus.groovy:groovy-all:2.3.7' + testCompile 'org.jmockit:jmockit:1.20' + testCompile 'junit:junit:4.12' + testCompile 'org.slf4j:slf4j-simple:1.7.13' + +} + +sonarqube { + properties { + property "sonar.host.url", System.getenv("SONARQUBE_SERVER_URL") + property "sonar.login", System.getenv("SONARQUBE_USERNAME") + property "sonar.password", System.getenv("SONARQUBE_PASSWORD") + } +} + + +task sourcesJar(type: Jar) { + from sourceSets.main.allJava + classifier = 'sources' +} + +task javadocJar(type: Jar) { + from javadoc + classifier = 'javadoc' +} + +publishing { + publications { + mavenJava(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + } + + } + repositories { + maven { + url(uri(PUBLISH_URI)) + credentials { + username = System.getenv("PUBLISH_REPO_USERNAME") + password = System.getenv("PUBLISH_REPO_PASSWORD") + } + } + } +} + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..eacf489 Binary files /dev/null and b/logo.png differ diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..037cb9f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'traverser' +// Gradle 5.0 compatibility +// enableFeaturePreview('STABLE_PUBLISHING') \ No newline at end of file diff --git a/src/main/java/com/intuit/commons/Comparables.java b/src/main/java/com/intuit/commons/Comparables.java new file mode 100644 index 0000000..d6662fa --- /dev/null +++ b/src/main/java/com/intuit/commons/Comparables.java @@ -0,0 +1,59 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons; + +import java.util.Comparator; +import java.util.Objects; + +/** + * + * @author gkesler + */ +public class Comparables { + // disable instantiation + private Comparables () { + } + + /** + * Compares two enum values and returns one with the highest ordinal value + * + * @param type of Comparable objects in this method + * + * @param left LHS of comparison + * @param right RHS of comparison + * @return left argument if its ordinal value is highest, or right otherwise + */ + public static > E max (E left, E right) { + return Objects.compare(left, right, Comparator.naturalOrder()) > 0 + ? left + : right; + } + + /** + * Compares two enum values and returns one with the lowest ordinal value + * + * @param type of Comparable objects in this method + * + * @param left LHS of comparison + * @param right RHS of comparison + * @return left argument if its ordinal value is lowest, or right otherwise + */ + public static > E min (E left, E right) { + return Objects.compare(left, right, Comparator.naturalOrder()) < 0 + ? left + : right; + } +} diff --git a/src/main/java/com/intuit/commons/TriConsumer.java b/src/main/java/com/intuit/commons/TriConsumer.java new file mode 100644 index 0000000..9d381f1 --- /dev/null +++ b/src/main/java/com/intuit/commons/TriConsumer.java @@ -0,0 +1,36 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons; + +import java.util.Objects; + +/** + * + * @author gkesler + */ +@FunctionalInterface +public interface TriConsumer { + void accept (X x, Y y, Z z); + + default TriConsumer andThen (TriConsumer after) { + Objects.requireNonNull(after); + + return (X x, Y y, Z z) -> { + accept(x, y, z); + after.accept(x, y, z); + }; + } +} diff --git a/src/main/java/com/intuit/commons/package-info.java b/src/main/java/com/intuit/commons/package-info.java new file mode 100644 index 0000000..ce82ad6 --- /dev/null +++ b/src/main/java/com/intuit/commons/package-info.java @@ -0,0 +1,6 @@ +/** + * Commons - a place for generic software components and solutions. + * + * These components are decoupled from any business logic. + */ +package com.intuit.commons; \ No newline at end of file diff --git a/src/main/java/com/intuit/commons/traverser/AbstractTraverseContextQueue.java b/src/main/java/com/intuit/commons/traverser/AbstractTraverseContextQueue.java new file mode 100644 index 0000000..3e38cdf --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/AbstractTraverseContextQueue.java @@ -0,0 +1,45 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import java.util.Objects; +import java.util.Queue; + +/** + * + * @author gkesler + * @param traversable type + * @param implementing type of a memory structure (stack ({@link java.util.Deque}) or queue) + */ + +abstract public class AbstractTraverseContextQueue>> implements TraverseContextQueue { + AbstractTraverseContextQueue(Q impl, Traverser> outer) { + this.impl = Objects.requireNonNull(impl); + this.outer = Objects.requireNonNull((Traverser>)outer); + } + + @Override + public final boolean isEmpty() { + return impl.isEmpty(); + } + + protected final TraverseContext newPostOrderContext(TraverseContext preOrder) { + return outer.newPostOrderContext(preOrder); + } + + protected final Q impl; + protected final Traverser> outer; +} diff --git a/src/main/java/com/intuit/commons/traverser/BreadthFirstQueue.java b/src/main/java/com/intuit/commons/traverser/BreadthFirstQueue.java new file mode 100644 index 0000000..586e5c4 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/BreadthFirstQueue.java @@ -0,0 +1,58 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Queue; +import java.util.stream.Stream; + +/** + * + * @author gkesler + */ +class BreadthFirstQueue extends AbstractTraverseContextQueue>> { + public BreadthFirstQueue(Traverser> outer) { + super(new LinkedList<>(), outer); + } + + @Override + public TraverseContext pop() { + return impl.remove(); + } + + @Override + public TraverseContext peek() { + return impl.peek(); + } + + @Override + public void pushAll(Optional> parent, Stream> contexts) { + // let parent have children: {child1, child2, ..., childN} + // let stack have state: {ctx1, ctx2, ctx3, ..., ctxN} + // BFS algorithm implies that the most recent nodes are visited last + // therefore we need to append children contexts followed by the parent post-order context + // at the tail of the queue and let the older content remain closer to the head of the queue + List> tail = (List>)impl; + contexts + .forEach(tail::add); + parent + .map(this::newPostOrderContext) + .ifPresent(tail::add); + } +} diff --git a/src/main/java/com/intuit/commons/traverser/DepthFirstQueue.java b/src/main/java/com/intuit/commons/traverser/DepthFirstQueue.java new file mode 100644 index 0000000..e485eb2 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/DepthFirstQueue.java @@ -0,0 +1,57 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import java.util.Deque; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Stream; + +/** + * + * @author gkesler + */ +class DepthFirstQueue extends AbstractTraverseContextQueue>> { + public DepthFirstQueue(Traverser> outer) { + super(new LinkedList<>(), outer); + } + + @Override + public TraverseContext pop() { + return impl.pop(); + } + + @Override + public TraverseContext peek() { + return impl.peek(); + } + + @Override + public void pushAll(Optional> parent, Stream> contexts) { + // let parent have children: {child1, child2, ..., childN} + // let stack have state: {ctx1, ctx2, ctx3, ..., ctxN} + // DFS algorithm implies that the most recent nodes are visited first + // therefore we need to insert children contexts followed by the parent post-order context + // at the head of the queue and push the older content further to the end of the queue + List> head = ((List>)impl).subList(0, 0); + contexts + .forEach(head::add); + parent + .map(this::newPostOrderContext) + .ifPresent(head::add); + } +} diff --git a/src/main/java/com/intuit/commons/traverser/Markers.java b/src/main/java/com/intuit/commons/traverser/Markers.java new file mode 100644 index 0000000..b4859d1 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/Markers.java @@ -0,0 +1,31 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +/** + * Mark special results from Visitor + */ +public enum Markers { + + /** + * Tells the traversal process to skip children + */ + SKIP, + /** + * Tells the traversal process to quit traversing + */ + QUIT +} diff --git a/src/main/java/com/intuit/commons/traverser/TraverseContext.java b/src/main/java/com/intuit/commons/traverser/TraverseContext.java new file mode 100644 index 0000000..90c27e1 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/TraverseContext.java @@ -0,0 +1,261 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Spliterators; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Encapsulates current state of traversal. + * Context wraps the current graph element being entered (pre-order) or left (post-order) + * along with the convenient methods to navigate through the parent contexts all the way + * to the root/source graph node, store traversal result, maintain traversal internal variables and more. + * + * @param type of the graph node being wrapped + * + * @author gkesler + */ +public interface TraverseContext { + /** + * Returns the graph node associated with this Context instance + * + * @return wrapped graph node + */ + T thisNode(); + /** + * Returns previous context. + * Contexts are organized as a linked lists that start from the Context for + * wrapped the graph element used to start traversing all the way down to the + * current Context. + * + * @return parent Context + */ + TraverseContext parentContext(); + /** + * A shortcut to obtain result of immediate parent. + * For performance and usability reasons, traverser guarantees that for every + * Context being traversed there always be a parent, so null check for parent Context is not needed. + * + * @param expected type of result + * + * @return parent result + */ + default U getParentResult() { + return parentContext().getResult(); + } + /** + * A shortcut to set result of immediate parent. + * For performance and usability reasons, traverser guarantees that for every + * Context being traversed there always be a parent, so null check for parent Context is not needed. + * + * @param expected type of result + * @param o parent result value + */ + default void setParentResult(U o) { + parentContext().setResult(o); + } + /** + * Sets result for the current context. + * Depending on the Context implementation, results could be shared among all + * Contexts in a single traversal or they could be stored per each Context/graph element + * + * Default: all Contexts share the same result {@link TraverseContextBuilder#ROOT_STRATEGY}. + * + * @see TraverseContextBuilder.ContextStrategy + * + * @param expected type of result + * @param o result value + */ + void setResult(U o); + /** + * Obtains result of this Context + * + * @param expected type of result + * @return result value + */ + U getResult(); + /** + * Retrieves local result stored in this context. + * Difference between this method and {@link #getResult() } is that the latter + * is not guaranteed to return locally stored result. It rather represents the + * overall centralized result acquired during entire graph traversal. + * + * @param expected type of result + * @return result value + */ + U getContextResult (); + /** + * Obtains initial data provided when traversal was requested. + * Serves as the default result value in case it wasn't overwritten + * during traversal. + * + * @return initial result value provided by the client + */ + Object initialData(); + /** + * Context local variables used during traversal. + * Some visitors can declare their own local variables, while others can + * leverage this common variables map. + * + * @return variables map. + */ + Map, Object> getContextVars(); + /** + * A fluent API method to set parent result + * + * @param expected type of result + * @param o parent result value + * @return this instance to allow method chaining + */ + default TraverseContext parentResult(U o) { + setParentResult(o); + return this; + } + /** + * A fluent API method to set context result + * + * @param expected type of result + * @param o result to save in context + * @return this instance to allow method chaining + */ + default TraverseContext result(U o) { + setResult(o); + return this; + } + /** + * Shortcut to read a variable associated with the provided key + * + * @param expected type of result + * @param valueClass used as a key to access the variable + * @return variable value + */ + default U getVar (Class valueClass) { + return (U)getContextVars().get(valueClass); + } + /** + * Shortcut to associate a variable the provided key + * + * @param expected type of result + * @param valueClass used as a key to access the variable + * @param value variable value + * @return this instance to allow method chaining + */ + default U setVar(Class valueClass, U value) { + return (U)getContextVars().put(valueClass, value); + } + /** + * A fluent API method to set a variable value + * + * @param type of a variable + * @param valueClass implementing class + * @param value value + * @return current context + */ + default TraverseContext var (Class valueClass, U value) { + setVar(valueClass, value); + return this; + } + /** + * Checks if this context wraps one of the roots passed to traverse method + * + * @see Traverser + * + * @return {@code true} in case of root context + */ + default boolean isRoot () { + return !Optional + .ofNullable(parentContext()) + .flatMap(ctx -> Optional.ofNullable(ctx.thisNode())) + .isPresent(); + } + /** + * Indicates that this Context is accessed after all its immediate children + * have been visited. This allows for post-order operations on the associated + * graph-element. + * + * @return {@code true} if the Context is a post-order context + */ + boolean isPostOrder(); + /** + * Indicates that this Context wraps a graph node that had been already seen + * during traversal process. This allows the traversal to prevent infinite + * cyclic traversals and inform the client code about detected cycle. + * + * @param visitedTracker lookup method to check if this context has been already seen + * @return {@code true} is the graph node had been already seen + */ + boolean isBackRef(Function, ? extends Optional> visitedTracker); + /** + * Obtains result value associated with the first occurrence of the graph node. + * @param expected type of result + * @return back reference result value + */ + U getBackRefResult(); + /** + * Convenience method to iterate over the parents chain of this Context + * + * @return iterator to iterate over the parents list starting from this object + */ + default Iterator> parentsIterator () { + return new Iterator>() { + @Override + public boolean hasNext() { + return current != null; + } + + @Override + public TraverseContext next() { + if (current == null) + throw new NoSuchElementException("no more parents"); + + TraverseContext result = current; + current = current.parentContext(); + return result; + } + + TraverseContext current = TraverseContext.this; + }; + } + /** + * Convenience method to treat the parents of this context as a stream/sequence + * of Context objects with all goodies from Stream API + * + * @return stream of parent contexts starting from this object + */ + default Stream> parentsStream () { + return StreamSupport + .stream(Spliterators.spliteratorUnknownSize(parentsIterator(), 0), false); + } + /** + * Utility method for supporting type-safety + * @param a target type + * @param castTo an implementing class + * @return the object after casting into target type + * + * @throws ClassCastException if object is not assignable to the type U + */ + @SuppressWarnings("unchecked") + default > U as (Class castTo) { + return (U)Objects.requireNonNull(castTo).cast(this); + } +} diff --git a/src/main/java/com/intuit/commons/traverser/TraverseContextBuilder.java b/src/main/java/com/intuit/commons/traverser/TraverseContextBuilder.java new file mode 100644 index 0000000..14b8a1e --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/TraverseContextBuilder.java @@ -0,0 +1,237 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +/** + * + * @author gkesler + * @param type + * @param context + * @param builder + */ +public abstract class TraverseContextBuilder, B extends TraverseContextBuilder> + implements TraverseContext { + protected TraverseContextBuilder(Traverser.ContextType contextType) { + this.contextType = Objects.requireNonNull(contextType); + } + + public B from (TraverseContext other) { + Objects.requireNonNull(other); + + return this + .thisNode(other.thisNode()) + .parentContext(other.parentContext()) + .initialData(other.initialData()) + .vars(other.getContextVars()); + } + + @Override + public T thisNode() { + return node; + } + + public B thisNode(T node) { + this.node = node; + return (B)this; + } + + @Override + public TraverseContext parentContext() { + return parentContext; + } + + public B parentContext(TraverseContext parentContext) { + this.parentContext = (B)parentContext; + return (B)this; + } + + @Override + public void setResult(U o) { + contextStrategy.setResult(this, o); + } + + @Override + public U getResult() { + return contextStrategy.getResult(this); + } + + @Override + public U getBackRefResult() { + return (U)backRefResult + .orElse(null); + } + + @Override + public U getContextResult() { + return (U)result; + } + + @Override + public Object initialData() { + return initialData; + } + + public B initialData(Object initialData) { + this.result = this.initialData = initialData; + return (B)this; + } + + @Override + public Map, Object> getContextVars() { + return vars; + } + + public B vars(Map, ?> vars) { + this.vars = Objects.requireNonNull((Map, Object>) vars); + return (B)this; + } + + @Override + public U getVar(Class valueClass) { + return contextStrategy.getVar(this, valueClass); + } + + @Override + public U setVar(Class valueClass, U newValue) { + return contextStrategy.setVar(this, valueClass, newValue); + } + + @Override + public boolean isPostOrder() { + return contextType == Traverser.ContextType.POST_ORDER; + } + + @Override + public boolean isBackRef(Function, ? extends Optional> visitedTracker) { + this.backRefResult = Objects.requireNonNull(visitedTracker).apply(this); + return (Object)backRefResult != null; + } + + public abstract C build (); + + protected C build(Function newContext) { + return Objects.requireNonNull(newContext) + .compose(B::preConstruct) + .andThen(this::postConstruct) + .apply((B)this); + } + + protected B preConstruct () { + // mandatory pre-construct + return this + .contextStrategy( + Optional + .ofNullable(parentContext) + .map(o -> NORMAL_STRATEGY) + .orElse(ROOT_STRATEGY) + ) + .vars(initVars(vars)); + } + + protected C postConstruct (C result) { + return result; + } + + protected Map, Object> initVars (Map, Object> vars) { + switch (contextType) { + case PRE_ORDER: + if (parentContext != null) + return new HashMap<>(); + // intentional fall through + case POST_ORDER: + return vars; + }; + + throw new IllegalStateException("Unknown contextType: " + contextType); + } + + public B contextStrategy (ContextStrategy strategy) { + this.contextStrategy = Objects.requireNonNull(strategy); + return (B)this; + } + + public interface ContextStrategy { + void setResult (TraverseContextBuilder outer, U result); + U getResult (TraverseContextBuilder outer); + U getVar (TraverseContextBuilder outer, Class key); + U setVar (TraverseContextBuilder outer, Class key, U newValue); + } + + protected final Traverser.ContextType contextType; + protected T node; + protected B parentContext; + protected Object result; + protected Optional backRefResult; + protected Object initialData; + protected Map, Object> vars = Collections.emptyMap(); + protected ContextStrategy contextStrategy = ROOT_STRATEGY; + + private static final ContextStrategy ROOT_STRATEGY = new ContextStrategy() { + @Override + public void setResult(TraverseContextBuilder outer, U result) { + outer.result = result; + } + + @Override + public U getResult(TraverseContextBuilder outer) { + return (U)outer.result; + } + + @Override + public U getVar(TraverseContextBuilder outer, Class key) { + return (U)outer.vars.get(key); + } + + @Override + public U setVar(TraverseContextBuilder outer, Class key, U newValue) { + return (U)outer.vars.put(key, newValue); + } + }; + private static final ContextStrategy NORMAL_STRATEGY = new ContextStrategy() { + @Override + public void setResult(TraverseContextBuilder outer, U result) { + outer.result = result; + outer.parentContext.setResult(result); + } + + @Override + public U getResult(TraverseContextBuilder outer) { + return outer.parentContext.getResult(); + } + + @Override + public U getVar(TraverseContextBuilder outer, Class key) { + U value; + return ((value = (U)outer.vars.get(key)) != null || outer.vars.containsKey(key)) + ? value + : outer.parentContext.getVar(key); + } + + @Override + public U setVar(TraverseContextBuilder outer, Class key, U newValue) { + return outer.vars.containsKey(key) + ? (U)outer.vars.put(key, newValue) + : outer.parentContext.setVar(key, newValue); + } + }; +} diff --git a/src/main/java/com/intuit/commons/traverser/TraverseContextQueue.java b/src/main/java/com/intuit/commons/traverser/TraverseContextQueue.java new file mode 100644 index 0000000..d005f04 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/TraverseContextQueue.java @@ -0,0 +1,70 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import java.util.Optional; +import java.util.stream.Stream; + +/** + * Collection of pending Contexts to visit while traversing. + * + * Most important queues are: + *
    + *
  • LIFO, i.e. Stack - for DFS traversal
  • + *
  • FIFO, i.e. Queue - for BFS traversal
  • + *
+ * + * @author gkesler + */ +public interface TraverseContextQueue { + /** + * Verifies if traversal is finished. + * Traversal can finish naturally, when there are no more pending elements + * in the traversal queue or is traversal was aborted by the client code. + * + * @param action client provided action to control traversal loop. + * @return {@code true} if traversal is finished. + */ + default boolean isDone(Traverser.Action action) { + return action == Traverser.Action.QUIT || isEmpty(); + } + /** + * Verifies if traversal queue is empty. + * + * @return {@code true} if the queue is empty. + */ + boolean isEmpty(); + /** + * Reads and removes the next element from the traversal queue. + * + * @return the next element from the queue. + */ + TraverseContext pop(); + /** + * Reads the next element in the queue without removing. + * + * @return the next element from the queue. + */ + TraverseContext peek (); + /** + * Offers provided contexts into the traversal queue. + * If parent context is specified, creates and enqueues POST-ORDER context for it. + * + * @param parent parent context to enqueue for post order processing + * @param contexts stream of child contexts + */ + void pushAll(Optional> parent, Stream> contexts); +} diff --git a/src/main/java/com/intuit/commons/traverser/TraverseVisitor.java b/src/main/java/com/intuit/commons/traverser/TraverseVisitor.java new file mode 100644 index 0000000..18f625b --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/TraverseVisitor.java @@ -0,0 +1,208 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import com.intuit.commons.Comparables; + +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import org.apache.commons.collections4.map.Flat3Map; + +/** + * GoF Visitor @see visitor-pattern that + * allows the Traverser to execute clients code during graph traversal. + * + * @param type of nodes in the graph + * + * @author gkesler + */ +public interface TraverseVisitor> { + /** + * Signals the Visitor that a new graph node is being visited (pre-order) + * + * @param context traversal context around graph node + * @return signal to Traverser on the next traversal step. + * + * @see Traverser.Action + */ + Traverser.Action enter(C context); + /** + * Signals the Visitor that a graph node is being exited (post-order) + * + * @param context traversal context around graph node + * @return signal to Traverser on the next traversal step. + * + * @see Traverser.Action + */ + Traverser.Action leave(C context); + /** + * Signals the Visitor that an already visited graph node is being visited again (cycle prevention) + * + * @param context traversal context around graph node + * @return signal to Traverser on the next traversal step. + * + * @see Traverser.Action + */ + Traverser.Action onBackRef(C context); + + /** + * Returns new builder + * @param type + * @param context + * @return builder with visitor, which does nothing + */ + static > Builder newBuilder () { + return new Builder<>(); + } + + /** + * Creates new builder based on current visitors actions + * @param type + * @param context + * @param visitor a visitor + * @return new builder + */ + static > Builder of (TraverseVisitor visitor) { + return new Builder(visitor); + } + + /** + * Chain visitor invocation after current one + * @param visitor visitor + * @return returns new visitor + */ + default TraverseVisitor andThen (TraverseVisitor visitor) { + return of(this) + .thenApply(visitor) + .build(); + } + + /** + * Builds new visitor based on current + * @param visitor visitor to combine visiting with + * @return new visitor + */ + default TraverseVisitor compose (TraverseVisitor visitor) { + return of(this) + .compose(visitor) + .build(); + } + + static Traverser.Action adapt (TraverseContext context, BiFunction delegateTo) { + Object result = delegateTo.apply((T)context.thisNode(), context.getResult()); + context.setResult(result); + + return RESULT_TO_ACTION.getOrDefault(result, Traverser.Action.CONTINUE); + } + + static final Map RESULT_TO_ACTION = new Flat3Map() {{ + put(Markers.QUIT, Traverser.Action.QUIT); + put(Markers.SKIP, Traverser.Action.SKIP); + }}; + + /** + * + * @param type + * @param context + */ + static class Builder> { + private Builder (Function onEnter, + Function onLeave, + Function onBackRef) { + this.onEnter = Objects.requireNonNull((Function)onEnter); + this.onLeave = Objects.requireNonNull((Function)onLeave); + this.onBackRef = Objects.requireNonNull((Function)onBackRef); + } + + public Builder () { + this(noop(), noop(), noop()); + } + + public Builder (TraverseVisitor visitor) { + this(Objects.requireNonNull(visitor)::enter, visitor::leave, visitor::onBackRef); + } + + public Builder onEnter (Function func) { + this.onEnter = Objects.requireNonNull((Function)func); + return this; + } + + public Builder onLeave (Function func) { + this.onLeave = Objects.requireNonNull((Function)func); + return this; + } + + public Builder onBackRef (Function func) { + this.onBackRef = Objects.requireNonNull((Function)func); + return this; + } + + public Builder thenApply (TraverseVisitor visitor) { + Objects.requireNonNull(visitor); + + return this + .onEnter(combine(onEnter, visitor::enter)) + .onLeave(combine(onLeave, visitor::leave)) + .onBackRef(combine(onBackRef, visitor::onBackRef)); + } + + public Builder compose (TraverseVisitor visitor) { + Objects.requireNonNull(visitor); + + return this + .onEnter(combine(visitor::enter, onEnter)) + .onLeave(combine(visitor::leave, onLeave)) + .onBackRef(combine(visitor::onBackRef, onBackRef)); + } + + private Function combine (Function first, Function next) { + return u -> Comparables.max(first.apply(u), next.apply(u)); + } + + public TraverseVisitor build () { + return new TraverseVisitor() { + @Override + public Traverser.Action enter(C context) { + return onEnter.apply(context); + } + + @Override + public Traverser.Action leave(C context) { + return onLeave.apply(context); + } + + @Override + public Traverser.Action onBackRef(C context) { + return onBackRef.apply(context); + } + }; + } + + private static > Function noop () { + return (Function)NOOP_FUNC; + } + + private Function onEnter; + private Function onLeave; + private Function onBackRef; + } + + static final Function, ? extends Traverser.Action> NOOP_FUNC = + c -> Traverser.Action.CONTINUE; + static final TraverseVisitor NOOP_VISITOR = new TraverseVisitorStub<>(); +} diff --git a/src/main/java/com/intuit/commons/traverser/TraverseVisitorStub.java b/src/main/java/com/intuit/commons/traverser/TraverseVisitorStub.java new file mode 100644 index 0000000..7d29498 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/TraverseVisitorStub.java @@ -0,0 +1,39 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +/** + * + * @author gkesler + * @param type + * @param context + */ +public class TraverseVisitorStub> implements TraverseVisitor { + @Override + public Traverser.Action enter(C context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(C context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(C context) { + return Traverser.Action.CONTINUE; + } +} diff --git a/src/main/java/com/intuit/commons/traverser/Traverser.java b/src/main/java/com/intuit/commons/traverser/Traverser.java new file mode 100644 index 0000000..3c1eaf9 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/Traverser.java @@ -0,0 +1,702 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import com.intuit.commons.TriConsumer; + +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Optional; +import java.util.Spliterators; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Generic purpose tree traverse mechanism. + * + * This class greatly simplifies [possibly cyclic] graph traversal by + * decoupling traversing the graph from the processing code executed when + * a graph node is visited. + * Unifies depth-first and bread-first traversal processes, so use of each of them + * becomes a routine rather than challenge. + * + * Example usage: + * + * Let's assume we need to depth-first traverse a tree composed of nodes: + * {@code + * class Node { + * Node (T data) { + * this.data = data; + * } + * + * Node child (Node child) { + * children.add(child); + * } + * + * T data; + * Collection> children = new ArrayList<>(); + * } + * } + * in order to collect all their data into a list. + * Depth First traversal could be performed as shown below: + * {@code + * Node root = new Node("root") + * .child(new Node("left") + * .child("left-left") + * .child("left-right")) + * .child(new Node("right")); + * + * Traverser> traverser = Traverser.depthFirst(node -> node.children); + * List allData = traverser.traverse(root, new ArrayList<>(), new Visitor>>() { + * Traverser.Action enter (Context> context) { + * Node node = context.thisNode(); + * List allData = context.getResult(); + * allData.add(node.data); + * return Traverser.Action.CONTINUE; + * } + * Traverser.Action leave (Context> context) { + * return Traverser.Action.CONTINUE; + * } + * Traverser.Action onBackRef (Context> context) { + * return Traverser.Action.CONTINUE; + * } + * }); + * } + * And that yields result: + * {@code + * "root", "left", "left-left", "left-right", "right" + * } + * @param graph node type + * @param + * + * see @linkplain https://www.oodesign.com/visitor-pattern.html to learn about Visitor Pattern. + * + * @author gkesler + */ +public final class Traverser> { + private Traverser (Function>, ? extends TraverseContextQueue> queueSupplier, + BiFunction>, ? super TraverseContext, ? extends Stream>> childrenProvider, + Function, ?>> builderFactory) { + this.contextQueueFactory = Objects.requireNonNull(queueSupplier); + this.childrenProvider = Objects.requireNonNull(childrenProvider); + this.contextBuilderFactory = Objects.requireNonNull(builderFactory); + } + + private Traverser (Function>, ? extends TraverseContextQueue> queueSupplier, + BiFunction>, ? super TraverseContext, ? extends Stream>> childrenProvider) { + this(queueSupplier, childrenProvider, Traverser::newBuilder); + } + + private Traverser (Function>, ? extends TraverseContextQueue> queueSupplier, + Function> childrenProvider) { + this(queueSupplier, adapt(childrenProvider)); + } + + private static BiFunction>, TraverseContext, Stream>> adapt (Function> func) { + return (traverser, context) -> Objects.requireNonNull(func) + .apply(context.thisNode()) + .stream() + .map(o -> traverser.newContext(context, o)); + } + + /** + * Creates a Traverser suitable to perform DFS traversal. + * + * @param graph node type + * + * @param childrenProvider function that obtains children to recurse down + * @return Traverser instance configured to perform DFS traversal + * @throws NullPointerException if {@code childrenProvider} is {@code null}; + */ + public static Traverser> depthFirst ( + BiFunction>, ? super TraverseContext, ? extends Stream>> childrenProvider) { + return new Traverser<>(DepthFirstQueue::new, (BiFunction>, TraverseContext, Stream>>)childrenProvider); + } + /** + * Creates a Traverser suitable to perform DFS traversal. + * + * @param graph node type + * @param traverse context type + * @param builder + * + * @param builderFactory function that creates {@link TraverseContext} instances + * @param childrenProvider function that obtains children to recurse down + * @return Traverser instance configured to perform DFS traversal + * @throws NullPointerException if {@code childrenProvider} is {@code null}; + */ + public static , B extends TraverseContextBuilder> Traverser depthFirst ( + Function builderFactory, + BiFunction, ? super C, ? extends Stream> childrenProvider) { + return depthFirst((BiFunction>, ? super TraverseContext, ? extends Stream>>)childrenProvider) + .contextBuilderFactory(builderFactory); + } + /** + * Creates a Traverser suitable to perform DFS traversal. + * + * @param graph node type + * + * @param childrenProvider function that obtains children to recurse down + * @return Traverser instance configured to perform DFS traversal + * @throws NullPointerException if {@code childrenProvider} is {@code null}; + */ + public static Traverser> depthFirst (Function> childrenProvider) { + return depthFirst(adapt(childrenProvider)); + } + /** + * Creates a Traverser suitable to perform BFS traversal. + * + * @param graph node type + * + * @param childrenProvider function that obtains children to recurse down + * @return this instance configured to perform BFS traversal + * @throws NullPointerException if {@code childrenProvider} is {@code null}; + */ + public static Traverser> breadthFirst ( + BiFunction>, ? super TraverseContext, ? extends Stream>> childrenProvider) { + return new Traverser<>(BreadthFirstQueue::new, (BiFunction>, TraverseContext, Stream>>)childrenProvider); + } + /** + /** + * Creates a Traverser suitable to perform BFS traversal. + * + * @param graph node type + * @param traverse context type + * @param builder + * + * @param builderFactory function that creates {@link TraverseContext} instances + * @param childrenProvider function that obtains children to recurse down + * @return Traverser instance configured to perform BFS traversal + * @throws NullPointerException if {@code childrenProvider} is {@code null}; + */ + public static , B extends TraverseContextBuilder> Traverser breadthFirst ( + Function builderFactory, + BiFunction, ? super C, ? extends Stream> childrenProvider) { + return breadthFirst((BiFunction>, ? super TraverseContext, ? extends Stream>>)childrenProvider) + .contextBuilderFactory(builderFactory); + } + /** + * Creates a Traverser suitable to perform BFS traversal. + * + * @param graph node type + * + * @param childrenProvider {lambda expression that obtains children to recurse down} + * @return this instance configured to perform BFS traversal + * @throws NullPointerException if {@code childrenProvider} is {@code null}; + */ + public static Traverser> breadthFirst (Function> childrenProvider) { + return breadthFirst(adapt(childrenProvider)); + } + + /** + * Creates a new Traverser that uses alternative type of traverse context queue. + * + * @param queueSupplier traverse context queue factory + * @return a new instance of Traverser that uses provided traverse context queue factory. + */ + public Traverser contextQueueFactory (Function>, ? extends TraverseContextQueue> queueSupplier) { + return new Traverser<>(queueSupplier, childrenProvider, contextBuilderFactory); + } + + /** + * Creates a new Traverser that uses alternative context builder + * + * @param target type of TraverseContext used by the new Traverser + * + * @param builderFactory functor that creates a TraverseContextBuilder producing TraverseContext objects of type U + * @return a new Traverser instance that uses new type of TraverseContext + */ + public > Traverser contextBuilderFactory (Function> builderFactory) { + return new Traverser<>(contextQueueFactory, childrenProvider, (Function, ?>>)builderFactory); + } + /** + * Traverses the graph starting from the only root. + * + * @param type of result to accumulate during the traversal + * @param root element (node) in the graph to start traversing + * @param seed initial result value + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (T root, U seed, TraverseVisitor visitor) { + return traverse(root, seed, Collections.emptyMap(), visitor); + } + /** + * Traverses the graph starting from the only root. + * + * @param type of result to accumulate during the traversal + * @param root element (node) in the graph to start traversing + * @param seed initial result value + * @param vars a map of variables with their names to be used while traversing + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (T root, U seed, Map, ?> vars, TraverseVisitor visitor) { + return traverse(root, seed, vars, new IdentityHashMap<>(), visitor); + } + /** + * Traverses the graph starting from the only root. + * + * @param type of result to accumulate during the traversal + * @param root element (node) in the graph to start traversing + * @param seed initial result value + * @param vars a map of variables with their names to be used while traversing + * @param visitedMap a map to keep track of visited nodes and the result at the moment of recording + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (T root, U seed, Map, ?> vars, Map visitedMap, TraverseVisitor visitor) { + return traverse(root, seed, vars, newVisitTracker(visitedMap), visitor); + } + /** + * Traverses the graph starting from the only root. + * + * @param type of result to accumulate during the traversal + * @param root element (node) in the graph to start traversing + * @param seed initial result value + * @param vars a map of variables with their names to be used while traversing + * @param visitTracker a functor to lookup visited node + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (T root, U seed, Map, ?> vars, Function, ? extends Optional> visitTracker, TraverseVisitor visitor) { + return traverse(Collections.singleton(root), seed, vars, visitTracker, visitor); + } + /** + * Traverses the graph starting from multiple roots. + * + * @param type of result to accumulate during the traversal + * @param roots elements (nodes) in the graph to start traversing + * @param seed initial result value + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (Iterable roots, U seed, TraverseVisitor visitor) { + return traverse(roots, seed, Collections.emptyMap(), visitor); + } + + /** + * Traverses the graph starting from multiple roots. + * + * @param type of result to accumulate during the traversal + * @param roots elements (nodes) in the graph to start traversing + * @param seed initial result value + * @param vars a map of variables with their names to be used while traversing + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (Iterable roots, U seed, Map, ?> vars, TraverseVisitor visitor) { + return traverse(roots, seed, vars, new IdentityHashMap<>(), visitor); + } + + /** + * Traverses the graph starting from multiple roots. + * + * @param type of result to accumulate during the traversal + * @param roots elements (nodes) in the graph to start traversing + * @param seed initial result value + * @param vars a map of variables with their names to be used while traversing + * @param visitedMap a map to keep track of visited nodes and the result at the moment of recording + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (Iterable roots, U seed, Map, ?> vars, Map visitedMap, TraverseVisitor visitor) { + return traverse(roots, seed, vars, newVisitTracker(visitedMap), visitor); + } + + /** + * Traverses the graph starting from multiple roots. + * + * @param type of result to accumulate during the traversal + * @param roots elements (nodes) in the graph to start traversing + * @param seed initial result value + * @param vars a map of variables with their names to be used while traversing + * @param visitTracker a functor to lookup visited node + * @param visitor GoF Visitor to be called when a graph element (node) + * is being entered (pre-order) or left (post-order). + * @return accumulated result + */ + public U traverse (Iterable roots, U seed, Map, ?> vars, Function, ? extends Optional> visitTracker, TraverseVisitor visitor) { + Objects.requireNonNull(roots); + Objects.requireNonNull(vars); + Objects.requireNonNull(visitTracker); + Objects.requireNonNull(visitor); + + C rootContext = newRootContext(seed, vars); + TraverseContextQueue contextQueue = newContextQueue(rootContext, (Iterable)roots); + + Action action = Action.CONTINUE; + while (!contextQueue.isDone(action)) { + action = traverseOne(contextQueue, visitTracker, visitor); + } + + return (U)Optional + .ofNullable(contextQueue.peek()) + .orElse(rootContext) + .getResult(); + } + + private Action traverseOne (TraverseContextQueue contextQueue, Function, ? extends Optional> visitTracker, TraverseVisitor visitor) { + C context = pop(contextQueue); + + if (context.isPostOrder()) { + return visitor + .leave(context); + } else if (context.isBackRef(visitTracker)) { + return visitor + .onBackRef(context); + } else { + return visitor + .enter(context) + .prepareNext(this, contextQueue, context); + } + } + + /** + * Creates a new Iterator for the currently configured traversal type + * + * @param roots root objects to begin traversal with + * @param visitor a Visitor object that helps to select nodes to return in {@link java.util.Iterator#next() } call + * @return new iterator instance + */ + public TraversingIterator newIterator (Iterable roots, TraverseVisitor visitor) { + return newIterator(roots, newVisitTracker(new IdentityHashMap<>()), visitor); + } + + /** + * Creates a new Iterator for the currently configured traversal type + * + * @param roots root objects to begin traversal with + * @param visitedMap a map to keep track of visited nodes and the result at the moment of recording + * @param visitor a Visitor object that helps to select nodes to return in {@link java.util.Iterator#next() } call + * @return new iterator instance + */ + public TraversingIterator newIterator (Iterable roots, Map visitedMap, TraverseVisitor visitor) { + return newIterator(roots, newVisitTracker(visitedMap), visitor); + } + + /** + * Creates a new Iterator for the currently configured traversal type + * + * @param roots root objects to begin traversal with + * @param visitTracker a functor to lookup visited node + * @param visitor a Visitor object that helps to select nodes to return in {@link java.util.Iterator#next() } call + * @return new iterator instance + */ + public TraversingIterator newIterator (Iterable roots, Function, ? extends Optional> visitTracker, TraverseVisitor visitor) { + Objects.requireNonNull(roots); + Objects.requireNonNull(visitTracker); + Objects.requireNonNull(visitor); + + C rootContext = newRootContext(null, Collections.emptyMap()); + TraverseContextQueue contextQueue = newContextQueue(rootContext, (Iterable)roots); + + return new TraversingIterator() { + @Override + public Stream path(Function, E> func) { + Objects.requireNonNull(func); + + return last + .orElseThrow(() -> new IllegalStateException("no results")) + .parentsStream() + .filter(context -> context.thisNode() != null) + .map(func); + } + + Optional getNext(Optional next) { + Action action = Action.CONTINUE; + while (!(next.isPresent() || contextQueue.isDone(action))) { + TraverseContext current = contextQueue.peek(); + action = traverseOne(contextQueue, visitTracker, visitor); + next = Optional.ofNullable(current.getResult()); + } + + return next; + } + + @Override + public boolean hasNext() { + return (next = next + .map(o -> next) + .orElseGet(() -> getNext(next))) + .isPresent(); + } + + @Override + public T next() { + T result = (last = next) + .flatMap(ctx -> Optional.ofNullable(ctx.thisNode())) + .orElseThrow(() -> new NoSuchElementException("no more results")); + + next = last + .map(ctx -> ctx.result(null)) + .flatMap(ctx -> Optional.empty()); + return result; + } + + @Override + public R replace(Object newValue, BiFunction, ? super Object, ? extends R> replaceFunction) { + return Objects.requireNonNull(replaceFunction).apply(last.orElseThrow(() -> new IllegalArgumentException("no results")), newValue); + } + + Optional next = Optional.empty(); + Optional last = next; + }; + } + /** + * Creates a pre-order iterator that iterates over the graph elements before + * recursing to their children. + * + * @param roots a collection of graph elements to start traversing + * @param delegate a visitor to be called when an element is being entered + * @return new TraversingIterator suitable for pre-order graph traversal. + */ + public TraversingIterator preOrderIterator (Iterable roots, TraverseVisitor delegate) { + Objects.requireNonNull(delegate); + + return newIterator(roots, new TraverseVisitor.Builder() + .onEnter(context -> ((TraverseVisitor)delegate).enter((C)context.result(context))) + .build()); + } + /** + * Creates a pre-order iterator that iterates over the graph elements before + * recursing to their children. + * + * @param root a graph elements to start traversing + * @param delegate a visitor to be called when an element is being entered + * @return new TraversingIterator suitable for pre-order graph traversal. + */ + public TraversingIterator preOrderIterator (T root, TraverseVisitor delegate) { + return preOrderIterator(Collections.singleton(root), delegate); + } + /** + * Creates a pre-order iterator that iterates over the graph elements before + * recursing to their children. + * + * @param roots a collection of graph elements to start traversing + * @return new TraversingIterator suitable for pre-order graph traversal. + */ + public TraversingIterator preOrderIterator (Iterable roots) { + return preOrderIterator(roots, (TraverseVisitor)TraverseVisitor.NOOP_VISITOR); + } + /** + * Creates a pre-order iterator that iterates over the graph elements before + * recursing to their children. + * + * @param root a graph element to start traversing + * @return new TraversingIterator suitable for pre-order graph traversal. + */ + public TraversingIterator preOrderIterator (T root) { + return preOrderIterator(Collections.singleton(root)); + } + /** + * Creates a post-order iterator that iterates over the graph elements after + * recursing to their children. + * + * @param roots a collection of graph elements to start traversing + * @param delegate a visitor to be called when an element is being entered + * @return new TraversingIterator suitable for post-order graph traversal. + */ + public TraversingIterator postOrderIterator (Iterable roots, TraverseVisitor delegate) { + Objects.requireNonNull(delegate); + + return newIterator(roots, new TraverseVisitor.Builder() + .onLeave(context -> ((TraverseVisitor)delegate).enter((C)context.result(context))) + .build()); + } + /** + * Creates a post-order iterator that iterates over the graph elements after + * recursing to their children. + * + * @param root a graph element to start traversing + * @param delegate a visitor to be called when an element is being entered + * @return new TraversingIterator suitable for post-order graph traversal. + */ + public TraversingIterator postOrderIterator (T root, TraverseVisitor delegate) { + return postOrderIterator(Collections.singleton(root), delegate); + } + /** + * Creates a post-order iterator that iterates over the graph elements after + * recursing to their children. + * + * @param roots a collection of graph elements to start traversing + * @return new TraversingIterator suitable for post-order graph traversal. + */ + public TraversingIterator postOrderIterator (Iterable roots) { + return postOrderIterator(roots, (TraverseVisitor)TraverseVisitor.NOOP_VISITOR); + } + /** + * Creates a post-order iterator that iterates over the graph elements after + * recursing to their children. + * + * @param root graph elements to start traversing + * @return new TraversingIterator suitable for post-order graph traversal. + */ + public TraversingIterator postOrderIterator (T root) { + return postOrderIterator(Collections.singleton(root)); + } + + private C pop (TraverseContextQueue queue) { + return (C)queue.pop(); + } + + private void pushChildren (TraverseContextQueue queue, C parent) { + enqueWithChildren(parent, childrenProvider, (factory) -> queue, Optional.of(parent)); + } + + private TraverseContextQueue newContextQueue (C rootContext, Iterable roots) { + return enqueWithChildren(rootContext, + (builder, context) -> StreamSupport + .stream(Spliterators.spliteratorUnknownSize(roots.iterator(), 0), false) + .map(o -> builder.newContext(context, o)), + contextQueueFactory, + Optional.empty()); + } + + private TraverseContextQueue enqueWithChildren (C parent, + BiFunction>, ? super TraverseContext, ? extends Stream>> childrenProvider, + Function>, ? extends TraverseContextQueue> queueFactory, + Optional> listParent) { + TraverseContextQueue contextQueue = queueFactory.apply((Traverser>)this); + + contextQueue.pushAll( + listParent, + childrenProvider.apply((Traverser>)this, parent) + ); + + return contextQueue; + } + + private static Function, Optional> newVisitTracker (Map visitedMap) { + return context -> (Optional)Optional + .ofNullable(context.thisNode()) + .map(node -> lookupBackRefResult(visitedMap, node, context.getResult())) + .orElse(null); + } + + private static Object lookupBackRefResult (Map visitedMap, T node, Object result) { + return visitedMap.containsKey(node) + ? Optional.ofNullable(visitedMap.get(node)) + : visitedMap.put(node, result); + } + + private C newRootContext (Object seed, Map, ?> vars) { + return newPreOrderContext(null, null, seed, vars); + } + + public C newContext (TraverseContext parent, T child) { + return newPreOrderContext(Objects.requireNonNull(parent), child, parent.initialData(), parent.getContextVars()); + } + + protected C newPreOrderContext (TraverseContext context, T node, Object initialData, Map, ?> vars) { + C parent = (C)context; + + return (C)contextBuilderFactory.apply(ContextType.PRE_ORDER) + .thisNode(node) + .parentContext(parent) + .initialData(initialData) + .vars(vars) + .build(); + } + + protected C newPostOrderContext (TraverseContext context) { + C preOrder = (C)Objects.requireNonNull(context); + + return (C)contextBuilderFactory.apply(ContextType.POST_ORDER) + .from(preOrder) + .build(); + } + + private static class ContextBuilder + extends TraverseContextBuilder, ContextBuilder> { + public ContextBuilder(ContextType contextType) { + super(contextType); + } + + @Override + public TraverseContext build() { + return build(UnaryOperator.identity()); + } + } + + private static TraverseContextBuilder, ?> newBuilder (ContextType contextType) { + return new ContextBuilder<>(contextType); + } + + /** + * Enumerates actions a client visitor can return from its functions to + * control overarching traversal loop. + */ + public enum Action { + /** + * indicates Visitor's desire to continue current traversal + */ + CONTINUE(Traverser::pushChildren), + /** + * indicates Visitor's desire to skip traversing children of the current element + */ + SKIP(Action::noop), + /** + * indicates Visitor's desire to stop traversing + */ + QUIT(Action::noop); + + private > Action (TriConsumer, ? super TraverseContextQueue, ? super C> delegate) { + this.delegate = Objects.requireNonNull((TriConsumer, TraverseContextQueue, TraverseContext>)delegate); + } + + private > Action prepareNext (Traverser outer, TraverseContextQueue contextQueue, C context) { + delegate.accept(outer, contextQueue, context); + return this; + } + + private static > void noop (Traverser outer, TraverseContextQueue state, C parent) { + } + + private final TriConsumer, TraverseContextQueue, TraverseContext> delegate; + } + + /** + * Enumerates possibly context types to be created by the ContextFactory + */ + public enum ContextType { + /** + * Indicates the requested Context MUST be a regular Context + */ + PRE_ORDER, + /** + * Indicates the requested Context MUST be an end-of-list Context + */ + POST_ORDER + } + + private final Function>, ? extends TraverseContextQueue> contextQueueFactory; + private final BiFunction>, ? super TraverseContext, ? extends Stream>> childrenProvider; + private final Function, ?>> contextBuilderFactory; +} diff --git a/src/main/java/com/intuit/commons/traverser/TraversingIterator.java b/src/main/java/com/intuit/commons/traverser/TraversingIterator.java new file mode 100644 index 0000000..17757a1 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/TraversingIterator.java @@ -0,0 +1,56 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + + +import java.util.Iterator; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Standard Iterator extension that also providers traversal specific API. + * + * @param type of nodes in the graph. + * + * @author gkesler + */ +public interface TraversingIterator extends Iterator { + /** + * Streams graph nodes that belong to the path from the current node to the traversal root. + * + * @return stream of graph nodes on the path + */ + default Stream path () { + return path(TraverseContext::thisNode); + } + /** + * + * @param element type of a path + * @param func function to build path + * @return stream of path elements + */ + Stream path (Function, E> func); + /** + * + * @param newValue new value + * @param replaceFunction function which performs replacement + * @param result + * + * @return result of a replacement + */ + R replace (Object newValue, BiFunction, ? super Object, ? extends R> replaceFunction); +} diff --git a/src/main/java/com/intuit/commons/traverser/package-info.java b/src/main/java/com/intuit/commons/traverser/package-info.java new file mode 100644 index 0000000..11476a9 --- /dev/null +++ b/src/main/java/com/intuit/commons/traverser/package-info.java @@ -0,0 +1,14 @@ +/** + * Generic traverser helps to perform BFS/DFS traversals + * on a data structure of any complexity (tree, graph etc). + * + * Supports: + * - both depth- and breadth- first search (DFS/BFS) + * - manipulating traversal flow (skip, quit) + * - iterators + * - local / global context + * - builders + * - type safety + * + */ +package com.intuit.commons.traverser; \ No newline at end of file diff --git a/src/test/java/com/intuit/commons/traverser/GraphIterationsTest.java b/src/test/java/com/intuit/commons/traverser/GraphIterationsTest.java new file mode 100644 index 0000000..ba7f529 --- /dev/null +++ b/src/test/java/com/intuit/commons/traverser/GraphIterationsTest.java @@ -0,0 +1,113 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.*; + +/** + * Iterating over complete graph + * + * https://en.wikipedia.org/wiki/Complete_graph + */ + +public class GraphIterationsTest { + + private interface TestVisitor extends TraverseVisitor> { + } + + static class Node { + T getData () { + return data; + } + + List> getChildren () { + return children; + } + + Node data (T data) { + this.data = data; + return this; + } + + Node children (List> children) { + this.children = children; + return this; + } + + Node child (Node child) { + children.add(child); + return this; + } + + T data; + List> children = new ArrayList<>(); + } + + @Test(expected = NullPointerException.class) + public void testNullGraph() { + + TraversingIterator> preOrder = preOrder((Node)null); + assertFalse(preOrder.hasNext()); + preOrder.next(); + } + + @Test + public void testKn1() { + Node v = new Node().data("vertexK1"); + assertOrder(preOrder(v), "vertexK1"); + } + + @Test + public void testKn2() { + Node v1 = new Node().data("vertexK1"); + Node v2 = new Node().data("vertexK2"); + v1.child(v2); + v2.child(v1); + + assertOrder(preOrder(v1), "vertexK1", "vertexK2"); + assertOrder(preOrder(v2), "vertexK2", "vertexK1"); + assertOrder(preOrder(Arrays.asList(v1,v2)), "vertexK1", "vertexK2"); + assertOrder(preOrder(Arrays.asList(v2,v1)), "vertexK2", "vertexK1"); + } + + + + private TraversingIterator> preOrder(Node node) { + return Traverser + .>breadthFirst(Node::getChildren) + .preOrderIterator(node); + } + + private TraversingIterator> preOrder(List> list) { + return Traverser + .>breadthFirst(Node::getChildren) + .preOrderIterator(list); + } + + private void assertOrder(TraversingIterator> preOrder, String... data) { + for(String datum : data) { + assertTrue(preOrder.hasNext()); + assertEquals(datum, preOrder.next().getData()); + } + assertFalse(preOrder.hasNext()); + } +} \ No newline at end of file diff --git a/src/test/java/com/intuit/commons/traverser/TraverserTest.java b/src/test/java/com/intuit/commons/traverser/TraverserTest.java new file mode 100644 index 0000000..aeebb71 --- /dev/null +++ b/src/test/java/com/intuit/commons/traverser/TraverserTest.java @@ -0,0 +1,621 @@ +/** + * Copyright 2019 Intuit Inc. + * + * 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 com.intuit.commons.traverser; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * + * @author gkesler + */ +public class TraverserTest { + + public TraverserTest() { + } + + @BeforeClass + public static void setUpClass() { + } + + @AfterClass + public static void tearDownClass() { + } + + @Before + public void setUp() { + } + + @After + public void tearDown() { + } + + private interface TestVisitor extends TraverseVisitor> { + } + + static class Node { + T getData () { + return data; + } + + List> getChildren () { + return children; + } + + Node data (T data) { + this.data = data; + return this; + } + + Node children (List> children) { + this.children = children; + return this; + } + + Node child (Node child) { + children.add(child); + return this; + } + + T data; + List> children = new ArrayList<>(); + } + + @Test + public void testSingleDepthFirst () { + Node root = new Node() + .data("Hello, world"); + + String result = Traverser + .>depthFirst(Node::getChildren) + .traverse(root, null, new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + context.setResult(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + }); + + assertEquals(result, root.getData()); + } + + @Test + public void testTreeDepthFirst () { + Node root = new Node() + .data("root") + .child(new Node() + .data("left")) + .child(new Node() + .data("right")); + + String result = Traverser + .>depthFirst(Node::getChildren) + .traverse(root, null, new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + context.setResult(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + }); + + assertEquals("right", result); + } + + @Test + public void testTreeWithCycleDepthFirst () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + List result = Traverser + .>depthFirst(Node::getChildren) + .traverse(root, new ArrayList<>(), new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + }); + + assertEquals(Arrays.asList("root", "left", "root", "right", "left"), result); + } + + @Test + public void testQuitAction () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + List result = Traverser + .>depthFirst(Node::getChildren) + .traverse(root, new ArrayList<>(), new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return result.contains("left") + ? Traverser.Action.QUIT + : Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + }); + + assertEquals(Arrays.asList("root", "left"), result); + } + + @Test + public void testSkipAction () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + List result = Traverser + .>depthFirst(Node::getChildren) + .traverse(root, new ArrayList<>(), new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return result.contains("left") || result.contains("right") + ? Traverser.Action.SKIP + : Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + }); + + assertEquals(Arrays.asList("root", "left", "right"), result); + } + + @Test + public void testPreOrderIterator () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + Iterator> preOrder = Traverser + .>depthFirst(Node::getChildren) + .preOrderIterator(root); + + List result = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(preOrder, 0), false) + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("root", "left", "right"), result); + } + + @Test + public void testPostOrderIterator () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + Iterator> postOrder = Traverser + .>depthFirst(Node::getChildren) + .postOrderIterator(root); + + List result = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(postOrder, 0), false) + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("left", "right", "root"), result); + } + + @Test + public void testPreOrderPath () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(new Node() + .data("left-left") + .child(root))) + .child(right = new Node() + .data("right") + .child(left)); + + TraversingIterator> preOrder = Traverser + .>depthFirst(Node::getChildren) + .preOrderIterator(root); + + while (preOrder.hasNext()) { + Node node = preOrder.next(); + if ("left-left".equals(node.getData())) { + List path = preOrder + .path() + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("left-left", "left", "root"), path); + } + } + } + + @Test + public void testPostOrderPath () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(new Node() + .data("left-left") + .child(root))) + .child(right = new Node() + .data("right") + .child(left)); + + TraversingIterator> postOrder = Traverser + .>depthFirst(Node::getChildren) + .postOrderIterator(root); + + while (postOrder.hasNext()) { + Node node = postOrder.next(); + if ("left-left".equals(node.getData())) { + List path = postOrder + .path() + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("left-left", "left", "root"), path); + } + } + } + + @Test + public void testSingleBreadthFirst () { + Node root = new Node() + .data("Hello, world"); + + String result = Traverser + .>breadthFirst(Node::getChildren) + .traverse(root, null, new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + context.setResult(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + }); + + assertEquals(result, root.getData()); + } + + @Test + public void testTreeBreadthFirst () { + Node root = new Node() + .data("root") + .child(new Node() + .data("left")) + .child(new Node() + .data("right")); + + String result = Traverser + .>breadthFirst(Node::getChildren) + .traverse(root, null, new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + context.setResult(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + }); + + assertEquals("right", result); + } + + @Test + public void testTreeWithCycleBreadthFirst () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + List result = Traverser + .>breadthFirst(Node::getChildren) + .traverse(root, new ArrayList<>(), new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + List result = context.getResult(); + result.add(context.thisNode().getData()); + return Traverser.Action.CONTINUE; + } + }); + + assertEquals(Arrays.asList("root", "left", "right", "root", "left"), result); + } + + @Test + public void testBreadthFirstIterator () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(root)) + .child(right = new Node() + .data("right") + .child(left)); + + Iterator> preOrder = Traverser + .>breadthFirst(Node::getChildren) + .preOrderIterator(root); + + List result = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(preOrder, 0), false) + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("root", "left", "right"), result); + } + + @Test + public void testBreadthFirstPreOrderPath () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(new Node() + .data("left-left") + .child(root))) + .child(right = new Node() + .data("right") + .child(left)); + + TraversingIterator> preOrder = Traverser + .>breadthFirst(Node::getChildren) + .preOrderIterator(root); + + while (preOrder.hasNext()) { + Node node = preOrder.next(); + if ("left-left".equals(node.getData())) { + List path = preOrder + .path() + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("left-left", "left", "root"), path); + } + } + } + + @Test + public void testBreadthFirstPostOrderPath () { + Node root = new Node() + .data("root"); + + Node left, right; + root.child(left = new Node() + .data("left") + .child(new Node() + .data("left-left") + .child(root))) + .child(right = new Node() + .data("right") + .child(left)); + + TraversingIterator> postOrder = Traverser + .>breadthFirst(Node::getChildren) + .postOrderIterator(root); + + while (postOrder.hasNext()) { + Node node = postOrder.next(); + if ("left-left".equals(node.getData())) { + List path = postOrder + .path() + .map(Node::getData) + .collect(Collectors.toList()); + + assertEquals(Arrays.asList("left-left", "left", "root"), path); + } + } + } + + @Test + public void testSetVar () { + Node root = new Node() + .data("root") + .child(new Node() + .data("left")) + .child(new Node() + .data("right")); + + List result = Traverser + .>depthFirst(Node::getChildren) + .traverse(root, new ArrayList<>(), new TestVisitor>() { + @Override + public Traverser.Action enter(TraverseContext> context) { + Node node = context.thisNode(); + if ("root".equals(node.getData())) { + // define local var + context.getContextVars().put(String.class, null); + // set "String" var value + context.setVar(String.class, "rootVar"); + } else { + // verify we have a Var propagated up from the parent + String var = context.getVar(String.class); + assertEquals(var, "rootVar"); + // verify local vars don't contain this variable + assertNull(context.getContextVars().get(String.class)); + } + + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action leave(TraverseContext> context) { + // Make sure the var defined in our context is available + Node node = context.thisNode(); + if ("root".equals(node.getData())) { + assertEquals(context.getVar(String.class), "rootVar"); + assertEquals(context.getContextVars().get(String.class), "rootVar"); + } + + return Traverser.Action.CONTINUE; + } + + @Override + public Traverser.Action onBackRef(TraverseContext> context) { + return Traverser.Action.CONTINUE; + } + }); + } +}