diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c3845fcd --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + target-branch: develop + + - package-ecosystem: maven + directory: / + schedule: + interval: daily + target-branch: develop diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..2e11d264 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,43 @@ +name-template: '$RESOLVED_VERSION 🌈' +tag-template: '$RESOLVED_VERSION' +version-resolver: + minor: + labels: + - 'feature' + patch: + labels: + - 'fix' + - 'refactoring' + - 'chore' + default: patch +autolabeler: + - label: 'feature' + branch: + - '/feature\/.+/' + - '/feat\/.+/' + - label: 'fix' + branch: + - '/fix\/.+/' + - label: 'refactoring' + branch: + - '/refactor\/.+/' + - label: 'chore' + branch: + - '/chore\/.+/' + - '/dependabot\/.+/' +categories: + - title: '🚀 Features' + label: 'feature' + - title: '🐛 Bug Fixes' + label: 'fix' + - title: '🔀 Refactoring' + label: 'refactoring' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +template: | + ## Changes + + $CHANGES + + $CONTRIBUTORS diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..b554ae02 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven + +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +name: CI + +on: + push: + branches: [ "develop", "main" ] + pull_request: + branches: [ "develop", "main" ] + +permissions: + contents: read + pull-requests: write + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + java: [ '17', '21' ] + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Set up JDK + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: ${{ matrix.java }} + distribution: 'temurin' + cache: 'maven' + - name: Run tests with Maven + run: mvn -B test --file pom.xml + - name: Upload coverage to Codecov + if: ${{ matrix.java == '17' }} + uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..570f2dd0 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,78 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["develop", "main"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["develop", "main"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["java"] + # CodeQL supports [ $supported-codeql-languages ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3.29.5 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3.29.5 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v3.29.5 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..bd2d544b --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,27 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Request, +# surfacing known-vulnerable versions of the packages declared or updated in the PR. +# Once installed, if the workflow run is marked as required, +# PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - name: 'Checkout Repository' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: 'Dependency Review' + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 diff --git a/.github/workflows/post-release.yml b/.github/workflows/post-release.yml new file mode 100644 index 00000000..67f1e898 --- /dev/null +++ b/.github/workflows/post-release.yml @@ -0,0 +1,33 @@ +name: Post-Release Sync + +on: + workflow_dispatch: # Manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + main-to-develop-sync: + runs-on: ubuntu-latest + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - name: Checkout main branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + + - name: Create PR from main to develop + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "Merge main into develop" + body: "This PR merges changes from main into develop." + base: develop + branch: sync-main-to-develop + delete-branch: true diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml new file mode 100644 index 00000000..73a007cc --- /dev/null +++ b/.github/workflows/pre-release.yml @@ -0,0 +1,33 @@ +name: Pre-Release Sync + +on: + workflow_dispatch: # Manual trigger + +permissions: + contents: write + pull-requests: write + +jobs: + develop-to-main-sync: + runs-on: ubuntu-latest + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - name: Checkout develop branch + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: develop + + - name: Create PR from develop to main + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + title: "Merge develop into main" + body: "This PR merges changes from develop into main." + base: main + branch: sync-develop-to-main + delete-branch: true diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..d61b73bf --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,31 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - main + - develop + +permissions: + contents: write + +jobs: + update_release_draft: + permissions: + # write permission is required to create a GitHub release + contents: write + # write permission is required for autolabeler + # otherwise, read permission is required at least + pull-requests: write + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into main + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..dc748f8e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Auto Publish to Maven Central + +on: + release: + types: [published] # Trigger on release publish + +permissions: + contents: read # Required for reading the repository contents + packages: write # Required for publishing packages + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Cache OWASP Dependency-Check data + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: ~/.m2/repository/org/owasp/dependency-check-data + key: dependency-check-data-${{ runner.os }}-${{ hashFiles('**/pom.xml') }} + restore-keys: | + dependency-check-data-${{ runner.os }}- + - name: Set up Maven Central Repository + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: '17' + distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Publish package + run: mvn -P release --batch-mode deploy -DskipTests -DperformRelease=true -Dnvd.api.key=${{ secrets.NVD_API_KEY }} + env: + MAVEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSWORD }} + NVD_API_KEY: ${{ secrets.NVD_API_KEY }} # Optional, if needed for NVD checks diff --git a/.github/workflows/update-version.yml b/.github/workflows/update-version.yml new file mode 100644 index 00000000..cd24dd21 --- /dev/null +++ b/.github/workflows/update-version.yml @@ -0,0 +1,41 @@ +name: Update version to pom.xml + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to set in pom.xml (e.g., 1.2.3)' + required: true + type: string + +permissions: + contents: write # Required for pushing changes and tags + pull-requests: write # Required for creating PRs + packages: write # Required for publishing packages + +jobs: + update-version: + runs-on: ubuntu-latest + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@8d3c67de8e2fe68ef647c8db1e6a09f647780f40 # v2.19.0 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Update version in pom.xml + run: | + VERSION=${{ github.event.inputs.version }} + echo "VERSION=$VERSION" >> $GITHUB_ENV + mvn versions:set -DnewVersion="$VERSION" -DprocessAllModules=true -DgenerateBackupPoms=false + + - name: Create PR to update version + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update-version-to-${{ env.VERSION }} + commit-message: "chore: update version to ${{ env.VERSION }}" + title: "Update version to ${{ env.VERSION }}" + body: "This PR updates the version in pom.xml to match the release version." + delete-branch: true diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b179f30c..00000000 --- a/.travis.yml +++ /dev/null @@ -1,3 +0,0 @@ -language: java -jdk: - - openjdk8 diff --git a/LICENSE b/LICENSE index 0db03a67..850576a9 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,8 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2013-2014 Bazaarvoice, Inc. + Copyright 2013-2023 Bazaarvoice, Inc. + Copyright 2025 Jolt Community Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 3a54f870..dee7acc3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,13 @@ -Jolt +JOLT (Community Edition) ======== +[![CI](https://github.com/jolt-community/jolt-community/actions/workflows/ci.yml/badge.svg)](https://github.com/jolt-community/jolt-community/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/jolt-community/jolt-community/graph/badge.svg?token=ZH2ZCH1J8Y)](https://codecov.io/gh/jolt-community/jolt-community) JSON to JSON transformation library written in Java where the "specification" for the transform is itself a JSON document. +### Community Edition +This repository is a community-maintained fork of JOLT. For the original version, please visit the [bazaarvoice/jolt](https://github.com/bazaarvoice/jolt) repository. + ### Useful For 1. Transforming JSON data from ElasticSearch, MongoDb, Cassandra, etc before sending it off to the world @@ -20,8 +25,7 @@ JSON to JSON transformation library written in Java where the "specification" fo 8. [Alternatives](#Alternatives) 9. [Performance](#Performance) 10. [CLI](#CLI) - 11. [Code Coverage](#Code_Coverage) - 12. [Release Notes](#Release_Notes) + 11. [Release Notes](#Release_Notes) ## Overview @@ -56,19 +60,19 @@ Jolt [Slide Deck](https://docs.google.com/presentation/d/1sAiuiFC4Lzz4-064sg1p8E Javadoc explaining each transform DSL : -* [shift](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java) -* [default](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Defaultr.java) -* [remove](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Removr.java) -* [cardinality](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/CardinalityTransform.java) -* [sort](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Sortr.java) +* [shift](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Shiftr.java) +* [default](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Defaultr.java) +* [remove](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Removr.java) +* [cardinality](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/CardinalityTransform.java) +* [sort](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Sortr.java) * full qualified Java ClassName : Class implements the Transform or ContextualTransform interfaces, and can optionally be SpecDriven (marker interface) - * [Transform](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Transform.java) interface - * [SpecDriven](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/SpecDriven.java) + * [Transform](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Transform.java) interface + * [SpecDriven](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/SpecDriven.java) * where the "input" is "hydrated" Java version of your JSON Data -Running a Jolt transform means creating an instance of [Chainr](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Chainr.java) with a list of transforms. +Running a Jolt transform means creating an instance of [Chainr](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Chainr.java) with a list of transforms. -The JSON spec for Chainr looks like : [unit test](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/test/resources/json/chainr/integration/firstSample.json). +The JSON spec for Chainr looks like : [unit test](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/test/resources/json/chainr/integration/firstSample.json). The Java side looks like : @@ -85,7 +89,7 @@ return output; ### Shiftr Transform DSL The Shiftr transform generally does most of the "heavy lifting" in the transform chain. -To see the Shiftr DSL in action, please look at our unit tests ([shiftr tests](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/resources/json/shiftr)) for nice bite sized transform examples, and read the extensive Shiftr [javadoc](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java). +To see the Shiftr DSL in action, please look at our unit tests ([shiftr tests](https://github.com/jolt-community/jolt-community/tree/main/jolt-core/src/test/resources/json/shiftr)) for nice bite sized transform examples, and read the extensive Shiftr [javadoc](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Shiftr.java). Our unit tests follow the pattern : @@ -105,11 +109,11 @@ Our unit tests follow the pattern : } ``` -We read in "input", apply the "spec", and [Diffy](https://github.com/bazaarvoice/jolt/blob/master/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java) it against the "expected". +We read in "input", apply the "spec", and [Diffy](https://github.com/jolt-community/jolt-community/blob/main/json-utils/src/main/java/io/joltcommunity/jolt/Diffy.java) it against the "expected". To learn the Shiftr DSL, examine "input" and "output" json, get an understanding of how data is moving, and *then* look at the transform spec to see how it facilitates the transform. -For reference, [this](https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/test/resources/json/shiftr/firstSample.json) was the very first test we wrote. +For reference, [this](https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/test/resources/json/shiftr/firstSample.json) was the very first test we wrote. ## Demo @@ -175,19 +179,6 @@ Two things to be aware of : Jolt Transforms and tools can be run from the command line. Command line interface doc [here](cli/README.md). -## Code Coverage - -[![Build Status](https://secure.travis-ci.org/bazaarvoice/jolt.png)](http://travis-ci.org/bazaarvoice/jolt) - -For the moment we have Cobertura configured in our poms. - -``` sh -mvn cobertura:cobertura -open jolt-core/target/site/cobertura/index.html -``` - -Currently, for the jolt-core artifact, code coverage is at 89% line, and 83% branch. - ## Release Notes -[Versions and Release Notes available here](https://github.com/bazaarvoice/jolt/releases). +[Versions and Release Notes available here](https://github.com/jolt-community/jolt-community/releases). diff --git a/cli/pom.xml b/cli/pom.xml index 8996669b..e1bbc8ca 100644 --- a/cli/pom.xml +++ b/cli/pom.xml @@ -1,34 +1,44 @@ - + 4.0.0 - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 ../parent/pom.xml - jolt-cli + jolt-community-cli Jolt Command Line Tools + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + net.sourceforge.argparse4j argparse4j + ${argparse4j.version} - com.bazaarvoice.jolt - jolt-complete + io.github.jolt-community.jolt + jolt-community-complete ${project.version} org.testng testng + ${testng.version} test @@ -39,7 +49,7 @@ org.apache.maven.plugins maven-shade-plugin - 1.6 + ${maven-shade-plugin.version} true @@ -61,9 +71,11 @@ - - - com.bazaarvoice.jolt.JoltCli + + + io.joltcommunity.jolt.JoltCli @@ -73,4 +85,4 @@ - \ No newline at end of file + diff --git a/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java b/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java deleted file mode 100644 index 1ff3e7c2..00000000 --- a/cli/src/main/java/com/bazaarvoice/jolt/DiffyCliProcessor.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.Namespace; -import net.sourceforge.argparse4j.inf.Subparser; -import net.sourceforge.argparse4j.inf.Subparsers; - -import java.io.File; - -/** - * The JoltCliProcessor for Diffy. See https://github.com/bazaarvoice/jolt/blob/master/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java - * for documentation on Diffy. - */ -public class DiffyCliProcessor implements JoltCliProcessor { - - /** - * Initialize the arg parser for the Diffy sub command - * - * @param subparsers The Subparsers object to attach the new Subparser to - */ - @Override - public void intializeSubCommand( Subparsers subparsers ) { - Subparser diffyParser = subparsers.addParser( "diffy" ) - .description( "Jolt CLI Diffy Tool. This tool will ingest two JSON inputs (from files or standard input) and " + - "perform the Jolt Diffy operation to detect any differences. The program will return an exit code of " + - "0 if no differences are found or a 1 if a difference is found or an error is encountered." ) - .defaultHelp( true ); - - diffyParser.addArgument( "filePath1" ).help( "File path to feed to Input #1 for the Diffy operation. " + - "This file should contain valid JSON." ) - .type( Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead() ); - diffyParser.addArgument( "filePath2" ).help( "File path to feed to Input #2 for the Diffy operation. " + - "This file should contain valid JSON. " + - "If this argument is not specified then standard input will be used." ) - .type( Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead() ) - .nargs( "?" ).setDefault( (File) null ); // these last two method calls make filePath2 optional - - diffyParser.addArgument( "-s" ).help( "Diffy will suppress output and run silently." ) - .action( Arguments.storeTrue() ); - diffyParser.addArgument( "-a" ).help( "Diffy will not consider array order when detecting differences" ) - .action( Arguments.storeTrue() ); - } - - /** - * Process the Diffy Subcommand - * - * @param ns Namespace which contains parsed commandline arguments - * @return true if no differences are found, false if a difference is found or an error occurs - */ - @Override - public boolean process( Namespace ns ) { - boolean suppressOutput = ns.getBoolean( "s" ); - - Object jsonObject1 = JoltCliUtilities.createJsonObjectFromFile( (File) ns.get( "filePath1" ), suppressOutput ); - File file = ns.get( "filePath2" ); - Object jsonObject2 = JoltCliUtilities.readJsonInput( file, suppressOutput ); - - Diffy diffy; - if ( ns.getBoolean( "a" ) ) { - diffy = new ArrayOrderObliviousDiffy(); - } else { - diffy = new Diffy(); - } - Diffy.Result result = diffy.diff( jsonObject1, jsonObject2 ); - - if ( result.isEmpty() ) { - JoltCliUtilities.printToStandardOut( "Diffy found no differences", suppressOutput ); - return true; - } else { - try { - JoltCliUtilities.printToStandardOut( "Differences found. Input #1 contained this:\n" + - JsonUtils.toPrettyJsonString( result.expected ) + "\n" + - "Input #2 contained this:\n" + - JsonUtils.toPrettyJsonString( result.actual ), suppressOutput ); - - } - catch ( Exception e ) { - JoltCliUtilities.printToStandardOut( "Differences found, but diffy encountered an error while writing the result.", suppressOutput ); - } - return false; - } - } -} diff --git a/cli/src/main/java/com/bazaarvoice/jolt/TransformCliProcessor.java b/cli/src/main/java/com/bazaarvoice/jolt/TransformCliProcessor.java deleted file mode 100644 index 8d752dce..00000000 --- a/cli/src/main/java/com/bazaarvoice/jolt/TransformCliProcessor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import net.sourceforge.argparse4j.impl.Arguments; -import net.sourceforge.argparse4j.inf.Namespace; -import net.sourceforge.argparse4j.inf.Subparser; -import net.sourceforge.argparse4j.inf.Subparsers; - -import java.io.File; - -/** - * The JoltCliProcessor for Chainr. See https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Chainr.java - * for documentation on Chainr. - */ -public class TransformCliProcessor implements JoltCliProcessor { - - private static final boolean SUPPRESS_OUTPUT = false; - - /** - * Initialize the arg parser for the Transform sub command - * - * @param subparsers The Subparsers object to attach the new Subparser to - */ - @Override - public void intializeSubCommand( Subparsers subparsers ) { - Subparser transformParser = subparsers.addParser( "transform" ) - .description( "Jolt CLI Transform Tool. This tool will ingest a JSON spec file and an JSON input (from a file or " + - "standard input) and run the transforms specified in the spec file on the input. The program will return an " + - "exit code of 0 if the input is transformed successfully or a 1 if an error is encountered" ) - .defaultHelp( true ); - - File nullFile = null; - transformParser.addArgument( "spec" ).help( "File path to Jolt Transform Spec to execute on the input. " + - "This file should contain valid JSON." ) - .type( Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead() ); - transformParser.addArgument( "input" ).help( "File path to the input JSON for the Jolt Transform operation. " + - "This file should contain valid JSON. " + - "If this argument is not specified then standard input will be used." ) - .type( Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead() ) - .nargs( "?" ).setDefault( nullFile ); // these last two method calls make input optional - - transformParser.addArgument( "-u" ).help( "Turns off pretty print for the output. Output will be raw json with no formatting." ) - .action( Arguments.storeTrue() ); - } - - /** - * Process the transform sub command - * - * @param ns Namespace which contains parsed commandline arguments - * @return true if the transform is successful, false if an error occured - */ - @Override - public boolean process( Namespace ns ) { - - Chainr chainr; - try { - chainr = ChainrFactory.fromFile((File) ns.get("spec")); - } catch ( Exception e ) { - JoltCliUtilities.printToStandardOut( "Chainr failed to load spec file.", SUPPRESS_OUTPUT ); - e.printStackTrace( System.out ); - return false; - } - - File file = ns.get( "input" ); - Object input = JoltCliUtilities.readJsonInput( file, SUPPRESS_OUTPUT ); - - Object output; - try { - output = chainr.transform( input ); - } catch ( Exception e ) { - JoltCliUtilities.printToStandardOut( "Chainr failed to run spec file.", SUPPRESS_OUTPUT ); - return false; - } - - Boolean uglyPrint = ns.getBoolean( "u" ); - return JoltCliUtilities.printJsonObject( output, uglyPrint, SUPPRESS_OUTPUT ); - } - -} diff --git a/cli/src/main/java/io/joltcommunity/jolt/DiffyCliProcessor.java b/cli/src/main/java/io/joltcommunity/jolt/DiffyCliProcessor.java new file mode 100644 index 00000000..b1443d8f --- /dev/null +++ b/cli/src/main/java/io/joltcommunity/jolt/DiffyCliProcessor.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import net.sourceforge.argparse4j.inf.Subparsers; + +import java.io.File; + +/** + * The JoltCliProcessor for Diffy. See https://github.com/jolt-community/jolt-community/blob/main/json-utils/src/main/java/io/joltcommunity/jolt/Diffy.java + * for documentation on Diffy. + */ +public class DiffyCliProcessor implements JoltCliProcessor { + + /** + * Initialize the arg parser for the Diffy sub command + * + * @param subparsers The Subparsers object to attach the new Subparser to + */ + @Override + public void intializeSubCommand(Subparsers subparsers) { + Subparser diffyParser = subparsers.addParser("diffy") + .description("Jolt CLI Diffy Tool. This tool will ingest two JSON inputs (from files or standard input) and " + + "perform the Jolt Diffy operation to detect any differences. The program will return an exit code of " + + "0 if no differences are found or a 1 if a difference is found or an error is encountered.") + .defaultHelp(true); + + diffyParser.addArgument("filePath1").help("File path to feed to Input #1 for the Diffy operation. " + + "This file should contain valid JSON.") + .type(Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead()); + diffyParser.addArgument("filePath2").help("File path to feed to Input #2 for the Diffy operation. " + + "This file should contain valid JSON. " + + "If this argument is not specified then standard input will be used.") + .type(Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead()) + .nargs("?").setDefault((File) null); // these last two method calls make filePath2 optional + + diffyParser.addArgument("-s").help("Diffy will suppress output and run silently.") + .action(Arguments.storeTrue()); + diffyParser.addArgument("-a").help("Diffy will not consider array order when detecting differences") + .action(Arguments.storeTrue()); + } + + /** + * Process the Diffy Subcommand + * + * @param ns Namespace which contains parsed commandline arguments + * @return true if no differences are found, false if a difference is found or an error occurs + */ + @Override + public boolean process(Namespace ns) { + boolean suppressOutput = ns.getBoolean("s"); + + Object jsonObject1 = JoltCliUtilities.createJsonObjectFromFile((File) ns.get("filePath1"), suppressOutput); + File file = ns.get("filePath2"); + Object jsonObject2 = JoltCliUtilities.readJsonInput(file, suppressOutput); + + Diffy diffy; + if (ns.getBoolean("a")) { + diffy = new ArrayOrderObliviousDiffy(); + } else { + diffy = new Diffy(); + } + Diffy.Result result = diffy.diff(jsonObject1, jsonObject2); + + if (result.isEmpty()) { + JoltCliUtilities.printToStandardOut("Diffy found no differences", suppressOutput); + return true; + } else { + try { + JoltCliUtilities.printToStandardOut("Differences found. Input #1 contained this:\n" + + JsonUtils.toPrettyJsonString(result.expected) + "\n" + + "Input #2 contained this:\n" + + JsonUtils.toPrettyJsonString(result.actual), suppressOutput); + + } catch (Exception e) { + JoltCliUtilities.printToStandardOut("Differences found, but diffy encountered an error while writing the result.", suppressOutput); + } + return false; + } + } +} diff --git a/cli/src/main/java/com/bazaarvoice/jolt/JoltCli.java b/cli/src/main/java/io/joltcommunity/jolt/JoltCli.java similarity index 54% rename from cli/src/main/java/com/bazaarvoice/jolt/JoltCli.java rename to cli/src/main/java/io/joltcommunity/jolt/JoltCli.java index 485f841d..13f7a207 100644 --- a/cli/src/main/java/com/bazaarvoice/jolt/JoltCli.java +++ b/cli/src/main/java/io/joltcommunity/jolt/JoltCli.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import net.sourceforge.argparse4j.ArgumentParsers; import net.sourceforge.argparse4j.inf.ArgumentParser; @@ -21,8 +22,6 @@ import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparsers; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; public class JoltCli { @@ -30,16 +29,15 @@ public class JoltCli { private static final Map JOLT_CLI_PROCESSOR_MAP; static { - Map temp = new HashMap<>(); - temp.put( JoltCliUtilities.DIFFY_COMMAND_IDENTIFIER, new DiffyCliProcessor() ); - temp.put( JoltCliUtilities.SORT_COMMAND_IDENTIFIER, new SortCliProcessor() ); - temp.put( JoltCliUtilities.TRANSFORM_COMMAND_IDENTIFIER, new TransformCliProcessor() ); - - JOLT_CLI_PROCESSOR_MAP = Collections.unmodifiableMap( temp ); + JOLT_CLI_PROCESSOR_MAP = Map.of( + JoltCliUtilities.DIFFY_COMMAND_IDENTIFIER, new DiffyCliProcessor(), + JoltCliUtilities.SORT_COMMAND_IDENTIFIER, new SortCliProcessor(), + JoltCliUtilities.TRANSFORM_COMMAND_IDENTIFIER, new TransformCliProcessor() + ); } - public static void main( String[] args ) { - System.exit( runJolt( args ) ? 0 : 1 ); + public static void main(String[] args) { + System.exit(runJolt(args) ? 0 : 1); } /** @@ -49,27 +47,31 @@ public static void main( String[] args ) { * @param args the arguments from the command line input * @return true if two inputs were read with no differences, false if differences were found or an error was encountered */ - protected static boolean runJolt( String[] args ) { - ArgumentParser parser = ArgumentParsers.newArgumentParser( "jolt" ); - Subparsers subparsers = parser.addSubparsers().help( "transform: given a Jolt transform spec, runs the specified transforms on the input data.\n" + - "diffy: diff two JSON documents.\n" + - "sort: sort a JSON document alphabetically for human readability." ); + protected static boolean runJolt(String[] args) { + ArgumentParser parser = ArgumentParsers.newFor("jolt").build(); + Subparsers subparsers = parser.addSubparsers().help( + """ + transform: given a Jolt transform spec, runs the specified transforms on the input data. + diffy: diff two JSON documents. + sort: sort a JSON document alphabetically for human readability. + """ + ); - for ( Map.Entry entry : JOLT_CLI_PROCESSOR_MAP.entrySet() ) { - entry.getValue().intializeSubCommand( subparsers ); + for (Map.Entry entry : JOLT_CLI_PROCESSOR_MAP.entrySet()) { + entry.getValue().intializeSubCommand(subparsers); } Namespace ns; try { - ns = parser.parseArgs( args ); - } catch ( ArgumentParserException e ) { - parser.handleError( e ); + ns = parser.parseArgs(args); + } catch (ArgumentParserException e) { + parser.handleError(e); return false; } - JoltCliProcessor joltToolProcessor = JOLT_CLI_PROCESSOR_MAP.get( args[0] ); - if ( joltToolProcessor != null ) { - return joltToolProcessor.process( ns ); + JoltCliProcessor joltToolProcessor = JOLT_CLI_PROCESSOR_MAP.get(args[0]); + if (joltToolProcessor != null) { + return joltToolProcessor.process(ns); } else { // TODO: error message, print usage. although I don't think it will ever get to this point. return false; diff --git a/cli/src/main/java/com/bazaarvoice/jolt/JoltCliProcessor.java b/cli/src/main/java/io/joltcommunity/jolt/JoltCliProcessor.java similarity index 84% rename from cli/src/main/java/com/bazaarvoice/jolt/JoltCliProcessor.java rename to cli/src/main/java/io/joltcommunity/jolt/JoltCliProcessor.java index a6d89983..f01db04a 100644 --- a/cli/src/main/java/com/bazaarvoice/jolt/JoltCliProcessor.java +++ b/cli/src/main/java/io/joltcommunity/jolt/JoltCliProcessor.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import net.sourceforge.argparse4j.inf.Namespace; import net.sourceforge.argparse4j.inf.Subparsers; @@ -28,7 +29,7 @@ public interface JoltCliProcessor { * * @param subparsers The Subparsers object to attach the new Subparser to */ - public void intializeSubCommand( Subparsers subparsers ); + public void intializeSubCommand(Subparsers subparsers); /** * This method does the processing of the input which is provided via the Namespace @@ -36,6 +37,6 @@ public interface JoltCliProcessor { * @param ns Namespace which contains parsed commandline arguments * @return true if processing was successful */ - public boolean process( Namespace ns ); + public boolean process(Namespace ns); } diff --git a/cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java b/cli/src/main/java/io/joltcommunity/jolt/JoltCliUtilities.java similarity index 52% rename from cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java rename to cli/src/main/java/io/joltcommunity/jolt/JoltCliUtilities.java index 16a9cee3..145dabc2 100644 --- a/cli/src/main/java/com/bazaarvoice/jolt/JoltCliUtilities.java +++ b/cli/src/main/java/io/joltcommunity/jolt/JoltCliUtilities.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import com.fasterxml.jackson.core.JsonParseException; import java.io.File; import java.io.FileInputStream; import java.io.IOException; +import tools.jackson.core.JacksonException; + /** * A utility class for the Jolt CLI tool. */ @@ -33,9 +35,9 @@ public class JoltCliUtilities { /** * Prints the given string to standard out, or doesn't, based on the suppressOutput flag */ - public static void printToStandardOut( String output, boolean suppressOutput ) { - if ( !suppressOutput ) { - System.out.println( output ); + public static void printToStandardOut(String output, boolean suppressOutput) { + if (!suppressOutput) { + System.out.println(output); } } @@ -46,40 +48,36 @@ public static void printToStandardOut( String output, boolean suppressOutput ) { * * @return the Map containing the JSON data */ - public static Object createJsonObjectFromFile( File file, boolean suppressOutput ) { - Object jsonObject = null; - try { - FileInputStream inputStream = new FileInputStream( file ); - jsonObject = JsonUtils.jsonToObject( inputStream ); - inputStream.close(); - } catch ( IOException e ) { - if ( e instanceof JsonParseException ) { - printToStandardOut( "File " + file.getAbsolutePath() + " did not contain properly formatted JSON.", suppressOutput ); - } else { - printToStandardOut( "Failed to open file: " + file.getAbsolutePath(), suppressOutput ); - } - System.exit( 1 ); + public static Object createJsonObjectFromFile(File file, boolean suppressOutput) { + try (FileInputStream inputStream = new FileInputStream(file)) { + return JsonUtils.jsonToObject(inputStream); + } catch (JacksonException e) { + printToStandardOut("File " + file.getAbsolutePath() + " did not contain properly formatted JSON.", suppressOutput); + System.exit(1); + } catch (IOException e) { + printToStandardOut("Failed to open file: " + file.getAbsolutePath(), suppressOutput); + System.exit(1); } - return jsonObject; + return null; // Unreachable, but required for compilation } /** * Prints the given json object to standard out, accounting for pretty printing and suppressed output. * - * @param output The object to print. This method will fail if this object is not well formed JSON. - * @param uglyPrint ignore pretty print + * @param output The object to print. This method will fail if this object is not well formed JSON. + * @param uglyPrint ignore pretty print * @param suppressOutput suppress output to standard out * @return true if printing operation was successful */ - public static boolean printJsonObject( Object output, Boolean uglyPrint, boolean suppressOutput ) { + public static boolean printJsonObject(Object output, Boolean uglyPrint, boolean suppressOutput) { try { - if ( uglyPrint ) { - printToStandardOut( JsonUtils.toJsonString( output ), suppressOutput ); + if (uglyPrint) { + printToStandardOut(JsonUtils.toJsonString(output), suppressOutput); } else { - printToStandardOut( JsonUtils.toPrettyJsonString( output ), suppressOutput ); + printToStandardOut(JsonUtils.toPrettyJsonString(output), suppressOutput); } - } catch ( Exception e ) { - printToStandardOut( "An error occured while attempting to print the output.", suppressOutput ); + } catch (Exception e) { + printToStandardOut("An error occured while attempting to print the output.", suppressOutput); return false; } return true; @@ -89,21 +87,21 @@ public static boolean printJsonObject( Object output, Boolean uglyPrint, boolean * This method will read in JSON, either from the given file or from standard in * if the file is null. An object contain the ingested input is returned. * - * @param file the file to read the input from, or null to use standard in + * @param file the file to read the input from, or null to use standard in * @param suppressOutput suppress output of error messages to standard out * @return Object containing input if successful or null if an error occured */ - public static Object readJsonInput( File file, boolean suppressOutput ) { + public static Object readJsonInput(File file, boolean suppressOutput) { Object jsonObject; - if ( file == null ) { + if (file == null) { try { - jsonObject = JsonUtils.jsonToMap( System.in ); - } catch ( Exception e ) { - printToStandardOut( "Failed to process standard input.", suppressOutput ); + jsonObject = JsonUtils.jsonToMap(System.in); + } catch (Exception e) { + printToStandardOut("Failed to process standard input.", suppressOutput); return null; } } else { - jsonObject = createJsonObjectFromFile( file, suppressOutput ); + jsonObject = createJsonObjectFromFile(file, suppressOutput); } return jsonObject; } diff --git a/cli/src/main/java/com/bazaarvoice/jolt/SortCliProcessor.java b/cli/src/main/java/io/joltcommunity/jolt/SortCliProcessor.java similarity index 50% rename from cli/src/main/java/com/bazaarvoice/jolt/SortCliProcessor.java rename to cli/src/main/java/io/joltcommunity/jolt/SortCliProcessor.java index 6942d721..d2f88dc5 100644 --- a/cli/src/main/java/com/bazaarvoice/jolt/SortCliProcessor.java +++ b/cli/src/main/java/io/joltcommunity/jolt/SortCliProcessor.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import net.sourceforge.argparse4j.impl.Arguments; import net.sourceforge.argparse4j.inf.Namespace; @@ -23,7 +24,7 @@ import java.io.File; /** - * The JoltCliProcessor for Sortr. See https://github.com/bazaarvoice/jolt/blob/master/jolt-core/src/main/java/com/bazaarvoice/jolt/Sortr.java + * The JoltCliProcessor for Sortr. See https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Sortr.java * for documentation on Sortr. */ public class SortCliProcessor implements JoltCliProcessor { @@ -36,42 +37,41 @@ public class SortCliProcessor implements JoltCliProcessor { * @param subparsers The Subparsers object to attach the new Subparser to */ @Override - public void intializeSubCommand( Subparsers subparsers ) { - Subparser sortParser = subparsers.addParser( "sort" ) - .description( "Jolt CLI Sort Tool. This tool will ingest one JSON input (from a file or standard input) and " + + public void intializeSubCommand(Subparsers subparsers) { + Subparser sortParser = subparsers.addParser("sort") + .description("Jolt CLI Sort Tool. This tool will ingest one JSON input (from a file or standard input) and " + "perform the Jolt sort operation on it. The sort order is standard alphabetical ascending, with a " + "special case for \"~\" prefixed keys to be bumped to the top. The program will return an exit code " + - "of 0 if the sort operation is performed successfully or a 1 if an error is encountered." ) - .defaultHelp( true ); + "of 0 if the sort operation is performed successfully or a 1 if an error is encountered.") + .defaultHelp(true); - sortParser.addArgument( "input" ).help( "File path to the input JSON that the sort operation should be performed on. " + - "This file should contain valid JSON. " + - "If this argument is not specified then standard input will be used." ) - .type( Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead() ) - .nargs( "?" ).setDefault( (File) null ).required( false ); // these last two method calls make input optional + sortParser.addArgument("input").help("File path to the input JSON that the sort operation should be performed on. " + + "This file should contain valid JSON. " + + "If this argument is not specified then standard input will be used.") + .type(Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead()) + .nargs("?").setDefault((File) null).required(false); // these last two method calls make input optional - sortParser.addArgument( "-u" ).help( "Turns off pretty print for the output. Output will be raw json with no formatting." ) - .action( Arguments.storeTrue() ); + sortParser.addArgument("-u").help("Turns off pretty print for the output. Output will be raw json with no formatting.") + .action(Arguments.storeTrue()); } /** - * * @param ns Namespace which contains parsed commandline arguments * @return true if the sort was successful, false if an error occurred */ @Override - public boolean process( Namespace ns ) { + public boolean process(Namespace ns) { - File file = ns.get( "input" ); - Object jsonObject = JoltCliUtilities.readJsonInput( file, SUPPRESS_OUTPUT ); - if ( jsonObject == null ) { + File file = ns.get("input"); + Object jsonObject = JoltCliUtilities.readJsonInput(file, SUPPRESS_OUTPUT); + if (jsonObject == null) { return false; } Sortr sortr = new Sortr(); - Object output = sortr.transform( jsonObject ); - Boolean uglyPrint = ns.getBoolean( "u" ); - return JoltCliUtilities.printJsonObject( output, uglyPrint, SUPPRESS_OUTPUT ); + Object output = sortr.transform(jsonObject); + Boolean uglyPrint = ns.getBoolean("u"); + return JoltCliUtilities.printJsonObject(output, uglyPrint, SUPPRESS_OUTPUT); } } diff --git a/cli/src/main/java/io/joltcommunity/jolt/TransformCliProcessor.java b/cli/src/main/java/io/joltcommunity/jolt/TransformCliProcessor.java new file mode 100644 index 00000000..b99ed663 --- /dev/null +++ b/cli/src/main/java/io/joltcommunity/jolt/TransformCliProcessor.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import net.sourceforge.argparse4j.impl.Arguments; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import net.sourceforge.argparse4j.inf.Subparsers; + +import java.io.File; + +/** + * The JoltCliProcessor for Chainr. See https://github.com/jolt-community/jolt-community/blob/main/jolt-core/src/main/java/io/joltcommunity/jolt/Chainr.java + * for documentation on Chainr. + */ +public class TransformCliProcessor implements JoltCliProcessor { + + private static final boolean SUPPRESS_OUTPUT = false; + + /** + * Initialize the arg parser for the Transform sub command + * + * @param subparsers The Subparsers object to attach the new Subparser to + */ + @Override + public void intializeSubCommand(Subparsers subparsers) { + Subparser transformParser = subparsers.addParser("transform") + .description("Jolt CLI Transform Tool. This tool will ingest a JSON spec file and an JSON input (from a file or " + + "standard input) and run the transforms specified in the spec file on the input. The program will return an " + + "exit code of 0 if the input is transformed successfully or a 1 if an error is encountered") + .defaultHelp(true); + + File nullFile = null; + transformParser.addArgument("spec").help("File path to Jolt Transform Spec to execute on the input. " + + "This file should contain valid JSON.") + .type(Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead()); + transformParser.addArgument("input").help("File path to the input JSON for the Jolt Transform operation. " + + "This file should contain valid JSON. " + + "If this argument is not specified then standard input will be used.") + .type(Arguments.fileType().verifyExists().verifyIsFile().verifyCanRead()) + .nargs("?").setDefault(nullFile); // these last two method calls make input optional + + transformParser.addArgument("-u").help("Turns off pretty print for the output. Output will be raw json with no formatting.") + .action(Arguments.storeTrue()); + } + + /** + * Process the transform sub command + * + * @param ns Namespace which contains parsed commandline arguments + * @return true if the transform is successful, false if an error occured + */ + @Override + public boolean process(Namespace ns) { + + Chainr chainr; + try { + chainr = ChainrFactory.fromFile((File) ns.get("spec")); + } catch (Exception e) { + JoltCliUtilities.printToStandardOut("Chainr failed to load spec file.", SUPPRESS_OUTPUT); + e.printStackTrace(System.out); + return false; + } + + File file = ns.get("input"); + Object input = JoltCliUtilities.readJsonInput(file, SUPPRESS_OUTPUT); + + Object output; + try { + output = chainr.transform(input); + } catch (Exception e) { + JoltCliUtilities.printToStandardOut("Chainr failed to run spec file.", SUPPRESS_OUTPUT); + return false; + } + + Boolean uglyPrint = ns.getBoolean("u"); + return JoltCliUtilities.printJsonObject(output, uglyPrint, SUPPRESS_OUTPUT); + } + +} diff --git a/cli/src/test/java/com/bazaarvoice/jolt/JoltCliTest.java b/cli/src/test/java/io/joltcommunity/jolt/JoltCliTest.java similarity index 72% rename from cli/src/test/java/com/bazaarvoice/jolt/JoltCliTest.java rename to cli/src/test/java/io/joltcommunity/jolt/JoltCliTest.java index 40b88b9b..202ca800 100644 --- a/cli/src/test/java/com/bazaarvoice/jolt/JoltCliTest.java +++ b/cli/src/test/java/io/joltcommunity/jolt/JoltCliTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import org.testng.Assert; import org.testng.annotations.Test; @@ -31,26 +32,25 @@ public void testRunJolt() // chooses the path to the resource files copied by maven into the target/ directory. Obviously, this assumes // that you did not name $JOLT_CHECKOUT 'cli'. If that check fails then the path is chosen with the assumption // that the test is running in an IDE (Intellij IDEA in my case). Your mileage with other IDE's may very. - String path = System.getProperty( "user.dir" ); - if ( path.endsWith( "cli" ) ) { + String path = System.getProperty("user.dir"); + if (path.endsWith("cli")) { // This test is being run by maven path += "//target//test-classes//json//"; - } - else { + } else { // This test is being run in an IDE (IntelliJ IDEA) - path += "//cli//src//test//resources//json//"; + path += "//cli//src//test//resources//json//"; } // diffy: Input with no differences should return true - Assert.assertTrue( JoltCli.runJolt( new String[] {"diffy", path + "input1.json", path + "input1.json", "-s"} ) ); + Assert.assertTrue(JoltCli.runJolt(new String[]{"diffy", path + "input1.json", path + "input1.json", "-s"})); // diffy: Input with differences should return false - Assert.assertFalse( JoltCli.runJolt( new String[] {"diffy", path + "input1.json", path + "input2.json", "-s"} ) ); + Assert.assertFalse(JoltCli.runJolt(new String[]{"diffy", path + "input1.json", path + "input2.json", "-s"})); // sort: well formed input should return true - Assert.assertTrue( JoltCli.runJolt( new String[] {"sort", path + "input1.json"} ) ); + Assert.assertTrue(JoltCli.runJolt(new String[]{"sort", path + "input1.json"})); // transform: well formed input should return true - Assert.assertTrue( JoltCli.runJolt( new String[] {"transform", path + "spec.json", path + "transformInput.json"} ) ); + Assert.assertTrue(JoltCli.runJolt(new String[]{"transform", path + "spec.json", path + "transformInput.json"})); } } diff --git a/cli/src/test/resources/json/input1.json b/cli/src/test/resources/json/input1.json index ef90a564..9de3e40e 100644 --- a/cli/src/test/resources/json/input1.json +++ b/cli/src/test/resources/json/input1.json @@ -1,21 +1,25 @@ { - "input" : { - "rating-primary" : [ 5, 4 ], - "rating-quality" : [ 4, 5 ], - - "rating-multi" : 3 + "input": { + "rating-primary": [ + 5, + 4 + ], + "rating-quality": [ + 4, + 5 + ], + "rating-multi": 3 }, - - "spec" : { - "rating-*" : "ONE", - - "rating-multi" : "MANY" // here's a comment! + "spec": { + "rating-*": "ONE", + "rating-multi": "MANY" + // here's a comment! }, - - "expected" : { - "rating-primary" : 5, - "rating-quality" : 4, - - "rating-multi" : [ 3 ] + "expected": { + "rating-primary": 5, + "rating-quality": 4, + "rating-multi": [ + 3 + ] } -} \ No newline at end of file +} diff --git a/cli/src/test/resources/json/input2.json b/cli/src/test/resources/json/input2.json index 140b9fb4..c7feab69 100644 --- a/cli/src/test/resources/json/input2.json +++ b/cli/src/test/resources/json/input2.json @@ -1,22 +1,26 @@ { - "input" : { - "rating-primary" : [ 5, 4 ], - "rating-quality" : [ 4, 5 ], - "extra-stuff" : "whatever", - - "rating-multi" : 3 + "input": { + "rating-primary": [ + 5, + 4 + ], + "rating-quality": [ + 4, + 5 + ], + "extra-stuff": "whatever", + "rating-multi": 3 }, - - "spec" : { - "rating-*" : "ONE", - - "rating-multi" : "MANY" // here's a comment! + "spec": { + "rating-*": "ONE", + "rating-multi": "MANY" + // here's a comment! }, - - "expected" : { - "rating-primary" : 5, - "rating-quality" : 4, - - "rating-multi" : [ 3 ] + "expected": { + "rating-primary": 5, + "rating-quality": 4, + "rating-multi": [ + 3 + ] } -} \ No newline at end of file +} diff --git a/cli/src/test/resources/json/spec.json b/cli/src/test/resources/json/spec.json index 5eba1ee9..073f75d1 100644 --- a/cli/src/test/resources/json/spec.json +++ b/cli/src/test/resources/json/spec.json @@ -4,7 +4,7 @@ "spec": { "facets": { "statistics": { - "_type":"MANY" + "_type": "MANY" } } } @@ -14,9 +14,9 @@ "spec": { "facets": { "statistics": { - "id":"abc123" + "id": "abc123" } } } } -] \ No newline at end of file +] diff --git a/cli/src/test/resources/json/transformInput.json b/cli/src/test/resources/json/transformInput.json index 96c86c56..e18dab74 100644 --- a/cli/src/test/resources/json/transformInput.json +++ b/cli/src/test/resources/json/transformInput.json @@ -12,4 +12,4 @@ "variance": 1.8875 } } -} \ No newline at end of file +} diff --git a/complete/pom.xml b/complete/pom.xml index 1317c331..e770dd90 100644 --- a/complete/pom.xml +++ b/complete/pom.xml @@ -1,35 +1,44 @@ - + 4.0.0 - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 ../parent/pom.xml - jolt-complete + jolt-community-complete Jolt Complete jar + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + - com.bazaarvoice.jolt - jolt-core + io.github.jolt-community.jolt + jolt-community-core ${project.version} - com.bazaarvoice.jolt - json-utils + io.github.jolt-community.jolt + json-community-utils ${project.version} org.testng testng + ${testng.version} test - \ No newline at end of file + diff --git a/complete/src/main/java/com/bazaarvoice/jolt/ChainrFactory.java b/complete/src/main/java/io/joltcommunity/jolt/ChainrFactory.java similarity index 56% rename from complete/src/main/java/com/bazaarvoice/jolt/ChainrFactory.java rename to complete/src/main/java/io/joltcommunity/jolt/ChainrFactory.java index ff5e7c77..abdbfe3b 100644 --- a/complete/src/main/java/com/bazaarvoice/jolt/ChainrFactory.java +++ b/complete/src/main/java/io/joltcommunity/jolt/ChainrFactory.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import com.bazaarvoice.jolt.chainr.instantiator.ChainrInstantiator; +import io.joltcommunity.jolt.chainr.instantiator.ChainrInstantiator; import java.io.File; import java.io.FileInputStream; +import java.io.IOException; /** * A factory class with various static methods that return instances of Chainr. @@ -31,20 +33,20 @@ public class ChainrFactory { * @param chainrSpecClassPath The class path that points to the chainr spec. * @return a Chainr instance */ - public static Chainr fromClassPath( String chainrSpecClassPath ) { - return fromClassPath( chainrSpecClassPath, null ); + public static Chainr fromClassPath(String chainrSpecClassPath) { + return fromClassPath(chainrSpecClassPath, null); } /** * Builds a Chainr instance using the spec described in the data via the class path that is passed in. * * @param chainrSpecClassPath The class path that points to the chainr spec. - * @param chainrInstantiator the ChainrInstantiator to use to initialze the Chainr instance + * @param chainrInstantiator the ChainrInstantiator to use to initialze the Chainr instance * @return a Chainr instance */ - public static Chainr fromClassPath( String chainrSpecClassPath, ChainrInstantiator chainrInstantiator ) { - Object chainrSpec = JsonUtils.classpathToObject( chainrSpecClassPath ); - return getChainr( chainrInstantiator, chainrSpec ); + public static Chainr fromClassPath(String chainrSpecClassPath, ChainrInstantiator chainrInstantiator) { + Object chainrSpec = JsonUtils.classpathToObject(chainrSpecClassPath); + return getChainr(chainrInstantiator, chainrSpec); } /** @@ -53,8 +55,8 @@ public static Chainr fromClassPath( String chainrSpecClassPath, ChainrInstantiat * @param chainrSpecFilePath The file path that points to the chainr spec. * @return a Chainr instance */ - public static Chainr fromFileSystem( String chainrSpecFilePath ) { - return fromFileSystem( chainrSpecFilePath, null ); + public static Chainr fromFileSystem(String chainrSpecFilePath) { + return fromFileSystem(chainrSpecFilePath, null); } /** @@ -64,9 +66,9 @@ public static Chainr fromFileSystem( String chainrSpecFilePath ) { * @param chainrInstantiator the ChainrInstantiator to use to initialze the Chainr instance * @return a Chainr instance */ - public static Chainr fromFileSystem( String chainrSpecFilePath, ChainrInstantiator chainrInstantiator ) { - Object chainrSpec = JsonUtils.filepathToObject( chainrSpecFilePath ); - return getChainr( chainrInstantiator, chainrSpec ); + public static Chainr fromFileSystem(String chainrSpecFilePath, ChainrInstantiator chainrInstantiator) { + Object chainrSpec = JsonUtils.filepathToObject(chainrSpecFilePath); + return getChainr(chainrInstantiator, chainrSpec); } /** @@ -75,42 +77,40 @@ public static Chainr fromFileSystem( String chainrSpecFilePath, ChainrInstantiat * @param chainrSpecFile The File which contains the chainr spec. * @return a Chainr instance */ - public static Chainr fromFile( File chainrSpecFile ) { - return fromFile( chainrSpecFile, null ); + public static Chainr fromFile(File chainrSpecFile) { + return fromFile(chainrSpecFile, null); } /** * Builds a Chainr instance using the spec described in the File that is passed in. * - * @param chainrSpecFile The File which contains the chainr spec. + * @param chainrSpecFile The File which contains the chainr spec. * @param chainrInstantiator the ChainrInstantiator to use to initialze the Chainr instance * @return a Chainr instance */ - public static Chainr fromFile( File chainrSpecFile, ChainrInstantiator chainrInstantiator ) { + public static Chainr fromFile(File chainrSpecFile, ChainrInstantiator chainrInstantiator) { Object chainrSpec; - try { - FileInputStream fileInputStream = new FileInputStream( chainrSpecFile ); - chainrSpec = JsonUtils.jsonToObject( fileInputStream ); - } catch ( Exception e ) { - throw new RuntimeException( "Unable to load chainr spec file " + chainrSpecFile.getAbsolutePath() ); + try (FileInputStream fileInputStream = new FileInputStream(chainrSpecFile)) { + chainrSpec = JsonUtils.jsonToObject(fileInputStream); + } catch (IOException e) { + throw new RuntimeException("Unable to load chainr spec file " + chainrSpecFile.getAbsolutePath(), e); } - return getChainr( chainrInstantiator, chainrSpec ); + return getChainr(chainrInstantiator, chainrSpec); } /** * The main engine in ChainrFactory for building a Chainr Instance. * * @param chainrInstantiator The ChainrInstantiator to use. If null it will not be used. - * @param chainrSpec The json spec for the chainr transformation + * @param chainrSpec The json spec for the chainr transformation * @return the Chainr instance created from the chainrInstantiator and inputStream */ - private static Chainr getChainr( ChainrInstantiator chainrInstantiator, Object chainrSpec ) { + private static Chainr getChainr(ChainrInstantiator chainrInstantiator, Object chainrSpec) { Chainr chainr; - if (chainrInstantiator == null ) { - chainr = Chainr.fromSpec( chainrSpec ); - } - else { - chainr = Chainr.fromSpec( chainrSpec, chainrInstantiator ); + if (chainrInstantiator == null) { + chainr = Chainr.fromSpec(chainrSpec); + } else { + chainr = Chainr.fromSpec(chainrSpec, chainrInstantiator); } return chainr; } diff --git a/complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java b/complete/src/test/java/io/joltcommunity/jolt/ChainrFactoryTest.java similarity index 57% rename from complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java rename to complete/src/test/java/io/joltcommunity/jolt/ChainrFactoryTest.java index 2d74517b..292a3c30 100644 --- a/complete/src/test/java/com/bazaarvoice/jolt/ChainrFactoryTest.java +++ b/complete/src/test/java/io/joltcommunity/jolt/ChainrFactoryTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.chainr.instantiator.DefaultChainrInstantiator; -import com.bazaarvoice.jolt.exception.JsonUnmarshalException; +package io.joltcommunity.jolt; +import io.joltcommunity.jolt.chainr.instantiator.DefaultChainrInstantiator; +import io.joltcommunity.jolt.exception.JsonUnmarshalException; import org.testng.Assert; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -37,86 +37,86 @@ public class ChainrFactoryTest { @BeforeClass public void setup() { fileSystemPath = getFileSystemPath(); - wellformedFile = new File( fileSystemPath + WELLFORMED_INPUT_FILENAME ); - malformedFile = new File( fileSystemPath + MALFORMED_INPUT_FILENAME ); + wellformedFile = new File(fileSystemPath + WELLFORMED_INPUT_FILENAME); + malformedFile = new File(fileSystemPath + MALFORMED_INPUT_FILENAME); } @Test public void testGetChainrInstanceFromClassPath_success() throws Exception { - Chainr result = ChainrFactory.fromClassPath( CLASSPATH + WELLFORMED_INPUT_FILENAME ); - Assert.assertNotNull( result, "ChainrFactory did not return an instance of Chainr." ); + Chainr result = ChainrFactory.fromClassPath(CLASSPATH + WELLFORMED_INPUT_FILENAME); + Assert.assertNotNull(result, "ChainrFactory did not return an instance of Chainr."); } - @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*" ) + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*") public void testGetChainrInstanceFromClassPath_error() throws Exception { - ChainrFactory.fromClassPath( CLASSPATH + MALFORMED_INPUT_FILENAME ); + ChainrFactory.fromClassPath(CLASSPATH + MALFORMED_INPUT_FILENAME); } @Test public void testGetChainrInstanceFromClassPathWithInstantiator_success() throws Exception { - Chainr result = ChainrFactory.fromClassPath( CLASSPATH + WELLFORMED_INPUT_FILENAME, new DefaultChainrInstantiator() ); - Assert.assertNotNull( result, "ChainrFactory did not return an instance of Chainr." ); + Chainr result = ChainrFactory.fromClassPath(CLASSPATH + WELLFORMED_INPUT_FILENAME, new DefaultChainrInstantiator()); + Assert.assertNotNull(result, "ChainrFactory did not return an instance of Chainr."); } - @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*" ) + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load JSON.*") public void testGetChainrInstanceFromClassPathWithInstantiator_error() throws Exception { - ChainrFactory.fromClassPath( CLASSPATH + MALFORMED_INPUT_FILENAME, new DefaultChainrInstantiator() ); + ChainrFactory.fromClassPath(CLASSPATH + MALFORMED_INPUT_FILENAME, new DefaultChainrInstantiator()); } @Test public void testGetChainrInstanceFromFileSystem_success() throws Exception { - Chainr result = ChainrFactory.fromFileSystem( fileSystemPath + WELLFORMED_INPUT_FILENAME ); - Assert.assertNotNull( result, "ChainrFactory did not return an instance of Chainr." ); + Chainr result = ChainrFactory.fromFileSystem(fileSystemPath + WELLFORMED_INPUT_FILENAME); + Assert.assertNotNull(result, "ChainrFactory did not return an instance of Chainr."); } - @Test( expectedExceptions = JsonUnmarshalException.class, expectedExceptionsMessageRegExp = "Unable to unmarshal JSON.*" ) + @Test(expectedExceptions = JsonUnmarshalException.class, expectedExceptionsMessageRegExp = "Unable to unmarshal JSON.*") public void testGetChainrInstanceFromFileSystem_error() throws Exception { - ChainrFactory.fromFileSystem( fileSystemPath + MALFORMED_INPUT_FILENAME ); + ChainrFactory.fromFileSystem(fileSystemPath + MALFORMED_INPUT_FILENAME); } @Test public void testGetChainrInstanceFromFileSystemWithInstantiator_success() throws Exception { - Chainr result = ChainrFactory.fromFileSystem( fileSystemPath + WELLFORMED_INPUT_FILENAME, new DefaultChainrInstantiator() ); - Assert.assertNotNull( result, "ChainrFactory did not return an instance of Chainr." ); + Chainr result = ChainrFactory.fromFileSystem(fileSystemPath + WELLFORMED_INPUT_FILENAME, new DefaultChainrInstantiator()); + Assert.assertNotNull(result, "ChainrFactory did not return an instance of Chainr."); } @Test(expectedExceptions = JsonUnmarshalException.class, expectedExceptionsMessageRegExp = "Unable to unmarshal JSON.*") public void testGetChainrInstanceFromFileSystemWithInstantiator_error() throws Exception { - ChainrFactory.fromFileSystem( fileSystemPath + MALFORMED_INPUT_FILENAME, new DefaultChainrInstantiator() ); + ChainrFactory.fromFileSystem(fileSystemPath + MALFORMED_INPUT_FILENAME, new DefaultChainrInstantiator()); } @Test public void testGetChainrInstanceFromFile_success() throws Exception { - Chainr result = ChainrFactory.fromFile( wellformedFile ); - Assert.assertNotNull( result, "ChainrFactory did not return an instance of Chainr." ); + Chainr result = ChainrFactory.fromFile(wellformedFile); + Assert.assertNotNull(result, "ChainrFactory did not return an instance of Chainr."); } - @Test( expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load chainr spec file.*" ) + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to unmarshal JSON to an Object.*") public void testGetChainrInstanceFromFile_error() throws Exception { - ChainrFactory.fromFile( malformedFile ); + ChainrFactory.fromFile(malformedFile); } @Test public void testGetChainrInstanceFromFileWithInstantiator_success() throws Exception { - Chainr result = ChainrFactory.fromFile( wellformedFile, new DefaultChainrInstantiator() ); - Assert.assertNotNull( result, "ChainrFactory did not return an instance of Chainr." ); + Chainr result = ChainrFactory.fromFile(wellformedFile, new DefaultChainrInstantiator()); + Assert.assertNotNull(result, "ChainrFactory did not return an instance of Chainr."); } - @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to load chainr spec file.*") + @Test(expectedExceptions = RuntimeException.class, expectedExceptionsMessageRegExp = "Unable to unmarshal JSON to an Object.*") public void testGetChainrInstanceFromFileWithInstantiator_error() throws Exception { - ChainrFactory.fromFile( malformedFile, new DefaultChainrInstantiator() ); + ChainrFactory.fromFile(malformedFile, new DefaultChainrInstantiator()); } private String getFileSystemPath() { @@ -126,8 +126,8 @@ private String getFileSystemPath() { // chooses the path to the resource files copied by maven into the target/ directory. Obviously, this assumes // that you did not name $JOLT_CHECKOUT 'tools'. If that check fails then the path is chosen with the assumption // that the test is running in an IDE (Intellij IDEA in my case). Your mileage with other IDE's may very. - String path = System.getProperty( "user.dir" ); - if ( path.endsWith( "complete" ) ) { + String path = System.getProperty("user.dir"); + if (path.endsWith("complete")) { // This test is being run by maven path += "//target//test-classes//json//"; } else { diff --git a/complete/src/test/resources/json/malformed-input.json b/complete/src/test/resources/json/malformed-input.json index 6a570d20..c1814a76 100644 --- a/complete/src/test/resources/json/malformed-input.json +++ b/complete/src/test/resources/json/malformed-input.json @@ -1,10 +1,11 @@ [ { - "operation" "cardinality", + "operation" + "cardinality", "spec": { "facets": { "statistics": { - "_type":"MANY" + "_type": "MANY" } } } @@ -14,9 +15,9 @@ "spec": { "facets": { "statistics": { - "id":"abc123" + "id": "abc123" } } } } -] \ No newline at end of file +] diff --git a/complete/src/test/resources/json/wellformed-input.json b/complete/src/test/resources/json/wellformed-input.json index 5eba1ee9..073f75d1 100644 --- a/complete/src/test/resources/json/wellformed-input.json +++ b/complete/src/test/resources/json/wellformed-input.json @@ -4,7 +4,7 @@ "spec": { "facets": { "statistics": { - "_type":"MANY" + "_type": "MANY" } } } @@ -14,9 +14,9 @@ "spec": { "facets": { "statistics": { - "id":"abc123" + "id": "abc123" } } } } -] \ No newline at end of file +] diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..77381558 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,1981 @@ +# JOLT Community Edition + +## Table of Contents + +- [Introduction](#introduction) +- [Getting Started](#getting-started) +- [Learning JOLT](#learning-jolt) + - [JOLT Demo](#jolt-demo) + - [LLM Support](#llm-support) + - [Terminology](#terminology) +- [Operations](#operations) + - [Specification](#specification) + - [JOLT Standard Syntax](#jolt-standard-syntax) + - [The `shift` Operation](#the-shift-operation) + - [Shifting Nested JSON: LHS vs RHS](#shifting-nested-json-lhs-vs-rhs) + - [Wildcard-free `shift` Examples](#wildcard-free-shift-examples) + - [`shift` Wildcards](#shift-wildcards) + - [Essential Wildcard Expressions](#essential-wildcard-expressions) + - [`*` Wildcard](#-wildcard) + - [`&` Wildcard](#-wildcard-1) + - [`$` Wildcard](#-wildcard-2) + - [`#` Wildcard](#-wildcard-3) + - [`|` Wildcard](#-wildcard-4) + - [`@` Wildcard](#-wildcard-5) + - [JSON Arrays](#json-arrays) + - [The `default` Operation](#the-default-operation) + - [The `remove` Operation](#the-remove-operation) + - [`remove` Wildcards](#remove-wildcards) + - [The `modify` Operations](#the-modify-operations) + - [Modifier Variants](#modifier-variants) + - [Functions Reference](#functions-reference) + - [The `enrich` Operation](#the-enrich-operation) + - [The `cardinality` Operation](#the-cardinality-operation) + - [The `sort` Operation](#the-sort-operation) + +[↑ Back to top](#jolt-community-edition) + +## Introduction + +JOLT Community Edition is a community-maintained edition of JOLT, a JSON to JSON transformation library written in Java. +For the original version, please visit the [bazaarvoice/jolt](https://github.com/bazaarvoice/jolt) repository. + +--- + +## Getting Started + +**TODO** + +[↑ Back to top](#jolt-community-edition) + +--- + +## Learning JOLT + +### JOLT Demo + +An interactive JOLT (v0.1.1) demo site is available +at [jolt-demo.appspot.com](https://jolt-demo.appspot.com/#inception). Version 0.1.1 is a very early version of JOLT, so +not all features are supported. + +### LLM Support + +Large Language Models struggle to reliably generate non-trivial (and sometimes even trivial) JOLT specs. LLMs such as +OpenAI's ChatGPT-4o and Anthropic's Claude frequently generate invalid JOLT syntax, hallucinate nonexistent functions, +and even imagine entire capabilities that do not exist in JOLT. They also tend to "forget" in conversation that certain +suggestions are invalid, especially while using search capabilities. Like many niche domain-specific languages (DSLs), +JOLT does not have a wide dataset of examples to train on. Furthermore, official JOLT documentation has been fairly +sparse. If LLM support is a must, you may have better luck with a traditional scripting language or a more popular JSON +transformation DSL. + +### Terminology + +This documentation follows the terminology set out by RFC 8259, with one notable exception. To reduce confusion, the +term "key" will be used in place of the more traditional terms "name" or "member name". When used, the term "name" +exclusively refers to the actual value of the string which is being used as a key. + +| JSON Term | Definition | Example | +|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------| +| String | A sequence of zero or more Unicode characters in double quotes, supporting backslash escapes (`\"`, `\\`, `\uXXXX`). | `"hello world"` | +| Number | A base-10 signed decimal literal: optional minus; integer part (no leading zeros unless zero); optional fraction; optional exponent (`E`/`e` plus digits); `NaN` and `Infinity` are disallowed. (RFC 8259 §6) | `0.0001`, `1234` | +| Boolean | Exactly one of the literals: `true` or `false`. | `true`,`false` | +| Null | The literal `null`, representing an explicit empty value. | `null` | +| Value | Any valid JSON type: string, number, boolean, null, array, or object. | | +| Array | An ordered, comma-separated sequence of zero or more values, enclosed in square brackets `[...]`. | `[0, "abc", {}]` | +| Element | A single value within an array. | `"abc"` in `[0, "abc", {}]` | +| Key | A string serving as the identifier for a value. | `"id"`, `"Label"`, `"settings"` | +| Attribute | A key, followed by `:`, followed by a value. Sometimes called a key/value pair. | `"key":"value"` | +| Object | An unordered set of zero or more attributes, enclosed in `{...}`. Keys should be unique. | `{"a":"b"}` | + +In addition to these "traditional" terms, we also define several "applied" terms, which may appear infrequently. + +| Extended JSON Term | Definition | +|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Index | A number, starting with 0, representing the position (left-to-right) of an element within an array. When an array is cast to an object, the index is used as the key for the given value of the element (after being cast to string). | +| Path | An ordered sequence of keys and/or indices which can be traversed in order to arrive at a desired value. | +| Root | The outermost value, i.e. the entire JSON object itself. Typically an array or object. Often denoted as `$`, especially in paths. | +| Dot Notation | A representation format for a path where keys are delimited by the character `.` in-between names. E.g. `$.settings.users.display_name`. Use is discouraged if any of the names contains the character `.`. | +| Bracket Notation | A representation format for a path where keys and indices are wrapped in square brackets. E.g. `$[0]["settings"]["users"]["display_name"]` | + +[↑ Back to top](#jolt-community-edition) + +--- + +## Operations + +In JOLT, an operation is a certain (narrow) type of data transformation. By default, JOLT comes with several core +operations: + +1. [shift](#the-shift-operation): move data from one path to another +2. [default](#the-default-operation): provide attributes if they do not already exist +3. [remove](#the-remove-operation): remove attributes from an object, or elements from an array +4. [modify-overwrite](#the-modify-overwrite-operation): modify values using built-in functions +5. [enrich](#the-enrich-operation): invoke Java methods or context-supplied beans to enrich values +6. [cardinality](#the-cardinality-operation): ensure that values are either arrays or not arrays +7. [sort](#the-sort-operation): order the keys of a JSON object deterministically. + +Operations are extensible, and other types of transforms may be provided in certain platforms, such as `chain`, which +allows for executing other operations in sequence. + +### Specification + +A specification (or "spec") is a JSON-based representation of where and how each operation should be performed. Each +operation's spec follows its own domain-specific language. + +### JOLT Standard Syntax + +Unless noted otherwise, all specs will be written in this format, for clarity: + +```json +{ + "operation": "operation-name", + "spec": { + ... + } +} +``` + +Some platforms may ask for the spec and operation separately. Here, we include both in the same object for convenience. +The JOLT standard syntax may include other arbitrary attributes as well, which are usually ignored by most platforms +providing JOLT. We can use these attributes to provide comments and representative data to make our spec easier to read. +Below is an example of some common types of arbitrary attributes in practice. + +```json +{ + "operation": "operation-name", + "comments": "in production settings, a comment should indicate not how, but WHY the operation is being done", + "description": [ + "In the case of advanced syntax or inexperienced audiences, a description may contain a description of what the spec does.", + "Pseudo-syntax such as {'a':[...], ...} -> [...] will also do, in a pinch." + ], + "spec": { + ... + }, + "input": { + ... + "what_goes_in_here": "Sample inputs, usually trimmed versions of production data.", + "guidelines": [ + "1. Keep it short. Long inputs make the spec hard to find, especially when multiple specs are in the same file.", + "2. Keep it focused. Only include relevant keys and values, so others can understand your intentions.", + "3. This data can and should be used for informally testing your transform as you develop it.", + "4. Keep more formal and extensive tests in a separate directory." + ] + }, + "output": { + ... + "what_goes_in_here": "The output of the transformation on the sample input." + } +} +``` + +--- + +### The `shift` Operation + +> **Summary:** Moves data from one path to another. Any data not shifted will disappear from the output. + +`shift` is a kind of JOLT transform that specifies where "data" from the input JSON should be placed in the output JSON. +At a base level, a single `shift` operation maps data from an input path to an output path. + +The spec syntax tends to follow this format, where keys describe existing paths, and values describe new paths. + +```json +{ + "operation": "shift", + "spec": { + "original_key": "new_key", + ... + } +} +``` + +Aside: The `shift` operation supports shifting in nested JSON objects. Sub-objects can have keys and are values too. To +avoid confusion about which value we are referencing, when we want to refer to a key as an existing data path, we use +the term left-hand side (LHS), and when we want to refer to the value as the destination of the data, we use the term +right-hand side (RHS). + +There are several important facts to know about the `shift` operation: + +- More advanced syntax for `shift` often differs between the LHS and RHS. +- The `shift` operation provides a wide number of wildcard symbols which make it flexible and powerful. +- Any data not shifted in the `shift` spec will disappear. To keep unshifted data as-is, we must shift all "unmentioned" + data to its current location. This can be done easily with the use of wildcards. +- If a key on the LHS does not exist within a JSON input, that key is ignored, and no error is raised. + +#### Shifting Nested JSON: LHS vs RHS + +In `shift`, a nested input path is specified via a JSON tree structure, and the output path is specified via a +flattened "dot notation" path. + +```json +{ + "operation": "shift", + "description": "CORRECT SYNTAX for shifting from nested objects: LHS nested, RHS dot notation", + "spec": { + "keep": { + "old": "keep.new" + } + }, + "input": { + "keep": { + "old": "shift me to keep.new" + } + }, + "output": { + "keep": { + "new": "shift me to keep.new" + } + } +} +``` + +While counter-intuitive, the nested key syntax on the LHS disambiguates nested and dot-flattened input keys. For +example, in the below spec, if we used dot notation for the LHS, the key `"keep.old"` would match on multiple locations, +causing confusion and ambiguity. Instead, now we know which key it will go to. + +```json +{ + "operation": "shift", + "description": "INCORRECT SYNTAX for shifting from a nested object", + "spec": { + "keep.old": "keep.new" + }, + "input": { + "keep": { + "old": "shift me to keep.new" + }, + "keep.old": "do not shift this value to keep.new" + }, + "output": { + "keep": { + "new": "do not shift this value to keep.new" + } + } +} +``` + +Aside: Forgetting to include the dot notation on the RHS is a common mistake and results in shifting data to a key in +the root object. + +```json +{ + "operation": "shift", + "description": "common mistake while shifting a key within a nested object is forgetting to provide the full path on the RHS.", + "spec": { + "a": { + "b": "c" + } + }, + "input": { + "a": { + "b": "keep me nested in a" + } + }, + "intended_output": { + "a": { + "c": "keep me nested in a" + } + }, + "actual_output": { + "c": "keep me nested in a" + } +} +``` + +#### Wildcard-free `shift` Examples + +```json +{ + "operation": "shift", + "description": "shift a value from one key to a new key in the object root", + "spec": { + "original": "new" + }, + "input": { + "original": 1, + "deleteme": 2 + }, + "output": { + "new": 1 + } +} +``` + +```json +{ + "operation": "shift", + "description": "shift a value into an array", + "spec": { + "a": "a[]" + }, + "input": { + "a": 1 + }, + "output": { + "a": [ + 1 + ] + } +} +``` + +```json +{ + "operation": "shift", + "description": "map first element of an array (index 0) into the object root.", + "spec": { + "0": "" + }, + "input": [ + { + "a": 1 + }, + { + "b": 2 + } + ], + "output": { + "a": 1 + } +} +``` + +```json +{ + "operation": "shift", + "description": "Escape wildcard symbols with a \\", + "spec": { + "\\@": "\\&" + }, + "input": { + "@": 1 + }, + "output": { + "&": 1 + } +} +``` + +#### `shift` Wildcards + +As shown above, `shift` specs can be entirely made up of literal string values, but its real power comes from symbolic +wildcards which provide elegant access to nested keys, indexes, existing values, and more. Wildcard symbols are used +_within the string_ on the LHS or RHS. Some wildcard symbols can be used on both the LHS and RHS, and some are only +valid on one side only. + +| Symbol | Wildcard Name | LHS | RHS | +|--------|-------------------------|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------| +| `*` | Name | Non-greedy wildcard matching of key names | Not Valid on RHS | +| `\|` | ANY/OR | Used as delimiter in the LHS string to indicate matches on one of several arbitrary keys | Not Valid on RHS | +| `&` | Path as Key | Use a key in a nearby location | Copies elements of the current path in the output path | +| `$` | Key as Value | Use a key as the value in the output | Not Valid on RHS. `"my_subobject":""` will make a sub-object the new root. | +| `@` | Value as Key | Use a key as the value in the output | Not Valid on RHS | +| `#` | Synthetic (Value/Index) | Synthetic value: use whatever follows afterwards as a literal value | Synthetic Index: Reference the index value of a match on a different array | + +##### Essential Wildcard Expressions + +Some wildcard expressions are so important, they are worth mentioning here, before we go into depth about each symbol. + +###### Keep Unshifted Data With The `"*":"&"` Idiom + +Recall one of the most important facts about `shift`: + +> Any data not shifted in the `shift` spec will disappear. To keep unshifted data as-is, we must shift all "unmentioned" +> data to its current location. + +This spec matches all key names in the root level of the JSON and maps them to their current key. + +```json +{ + "operation": "shift", + "description": "Map each current key onto the current key.", + "spec": { + "*": "&" + }, + "input": { + "a": 1, + "b": 2 + }, + "output": { + "a": 1, + "b": 2 + } +} +``` + +This is effectively a no-op, but shifting the key back to itself prevents the key from being removed. + +There are a few sharp edges to watch out for, however. For starters, the `"*":"&"` idiom is often used multiple times in +a spec. The `*` wildcard is non-greedy, which means explicitly shifting a key within a spec excludes it from being +matched by the `*` wildcard. Furthermore, if one of a sub-object's attributes is explicitly shifted within the spec, any +unshifted attributes within that sub-object will not be kept. Therefore, you may see the `"*":"&"` idiom more than once +within a spec, particularly when sub-objects are being manipulated and unmentioned sub-attributes need to remain as-is. +However, otherwise untouched nested objects kept with a `"*":"&"` will remain intact. + +For example, take the following spec, where the input has three sub-objects. + +```json +{ + "operation": "shift", + "description": "", + "spec": { + "*": "&", + "root_shift": "SHIFTED_root_shift", + "subobject_shift": { + "a": "subobject_shift.SHIFTED_a" + } + }, + "input": { + "untouched": { + "a": true, + "b": { + "c": true + } + }, + "root_shift": { + "a": true, + "b": { + "c": true + } + }, + "subobject_shift": { + "a": true, + "b": { + "c": true + } + } + }, + "output": { + "untouched": { + "a": true, + "b": { + "c": true + } + }, + "SHIFTED_root_shift": { + "a": true, + "b": { + "c": true + } + }, + "subobject_shift": { + "SHIFTED_a": true + } + } +} +``` + +Which demonstrates the following: + +1. The `"untouched"` sub-object kept via the `"*":"&"` idiom keeps all sub-attributes. +2. The explicitly shifted sub-object `"root_shift"` mapped to a new key keeps it's sub-attributes. +3. The sub-object `"subobject_shift"` is now missing the attribute `"b":{"c":true}`, however, because it did have a + different sub-attribute shifted, and `"b":{"c":true}` was unshifted. `"b":{"c":true}` was not kept in place by the + `"*":"&"` idiom because `"subobject_shift"` is explicitly shifted, and explicitly shifting a key excludes it from the + `*` wildcard. + +To keep `"b":{"c":true}` within `"subobject_shift"`, we must use a second `"*":"&"` idiom, within `"subobject_shift"`: + +```json +{ + "operation": "shift", + "description": "", + "spec": { + "*": "&", + "root_shift": "SHIFTED_root_shift", + "subobject_shift": { + "*": "subobject_shift.&", + "a": "subobject_shift.SHIFTED_a" + } + }, + "input": { + "untouched": { + "a": true, + "b": { + "c": true + } + }, + "root_shift": { + "a": true, + "b": { + "c": true + } + }, + "subobject_shift": { + "a": true, + "b": { + "c": true + } + } + }, + "output": { + "untouched": { + "a": true, + "b": { + "c": true + } + }, + "SHIFTED_root_shift": { + "a": true, + "b": { + "c": true + } + }, + "subobject_shift": { + "SHIFTED_a": true, + "b": { + "c": true + } + } + } +} +``` + +Aside: It is worth noting, however, that this has many "magic strings" that will cause issues if the input schema were +to change. The `&` wildcard allows us to write this spec more concisely: + +```json +{ + "operation": "shift", + "description": "", + "spec": { + "*": "&", + "root_shift": "SHIFTED_&", + "subobject_shift": { + "*": "&1.&", + "a": "&1.SHIFTED_&" + } + }, + "input": { + "untouched": { + "a": true, + "b": { + "c": true + } + }, + "root_shift": { + "a": true, + "b": { + "c": true + } + }, + "subobject_shift": { + "a": true, + "b": { + "c": true + } + } + }, + "output": { + "untouched": { + "a": true, + "b": { + "c": true + } + }, + "SHIFTED_root_shift": { + "a": true, + "b": { + "c": true + } + }, + "subobject_shift": { + "SHIFTED_a": true, + "b": { + "c": true + } + } + } +} +``` + +##### `*` Wildcard + +Valid only on the LHS (input JSON keys) side of a `shift` Spec. +The `*` wildcard can be used by itself or to match part of a key. + +`*` wildcard by itself: +As illustrated in the example above, the `*` wildcard by itself is useful for "templating" JSON maps, +where each key / value has the same "format". + +In the example below, "rating.quality" and "rating.sharpness" both have the same structure/format, and thus we can use the +`*` to allow us to write more compact rules and avoid having to explicitly write very similar rules for both "quality" +and "sharpness". + +```json +{ + "rating": { + "quality": { + "value": 3, + "max": 5 + }, + "sharpness": { + "value": 7, + "max": 10 + } + } +} +``` + +`*` wildcard as part of a key: +This is useful for working with input JSON with keys that are "prefixed". +Ex: if you had an input document like: + +```json +{ + "tag-Pro": "Awesome", + "tag-Con": "Bogus" +} +``` + +A "tag-\*" would match both keys and make the whole key and "matched" part of the key available. +Ex, input key of "tag-Pro" with LHS spec "tag-\*", would make "tag-Pro" and "Pro" available to reference. +Note the `*` wildcard is as non-greedy as possible, hence you can use more than one `*` in a key. +For example, "tag-*-*" would match "tag-Foo-Bar", making "tag-Foo-Bar", "Foo", and "Bar" all available to reference. + +##### `&` Wildcard + +Valid on the LHS (left hand side - input JSON keys) and RHS (output data path) + +Means, dereference against a "path" to get a value and use that value as if it were a literal key. +The canonical form of the wildcard is "&(0,0)". +The first parameter is where in the input path to look for a value, and the second parameter is which part of the key to +use (used with a key). +There are syntactic sugar versions of the wildcard, all of the following mean the same thing. +Sugar : `&` = `&0` = `&(0)` = `&(0,0)` +The syntactic sugar versions are nice, as there are a set of data transforms that do not need to use the canonical form, +e.g. if your input data does not have any "prefixed" keys. + +###### `&` Path lookup + +As `shift` processes data and walks down the spec, it maintains a data structure describing the path it has walked. +The `&` wildcard can access data from that path in a 0 major, upward oriented way. + +Example: + +```json +{ + "foo": { + "bar": { + "baz": + // &0 = baz, &1 = bar, &2 = foo + } + } +} +``` + +###### `&` Subkey lookup + +`&` subkey lookup allows us to reference the values captured by the `*` wildcard. + +Example, "tag-\*-\*" would match "tag-Foo-Bar", making &(0,0) = "tag-Foo-Bar", &(0,1) = "Foo", &(0,2) = "Bar" + +##### `$` Wildcard + +Valid only on the LHS of the spec. +The existence of this wildcard is a reflection of the fact that the "data" of the input JSON can be both in the "values" +and the "keys" of the input JSON + +The base case operation of `shift` is to copy input JSON "values"; thus we need a way to specify that we want to copy +the input JSON "key" instead. + +Thus `$` specifies that we want to use an input key, or input key derived value, as the data to be placed in the output +JSON. +`$` has the same syntax as the `&` wildcard, and can be read as, dereference to get a value, and then use that value as +the data to be output. + +There are two cases where this is useful: + +1) when a "key" in the input JSON needs to be an "id" value in the output JSON (e.g. `"$": "SecondaryRatings.&1.Id"`) +2) you want to make a list of all the input keys. + +Example of "a list of the input keys": + +```json +// input +{ + "rating": { + "primary": { + "value": 3, + "max": 5 + }, + "quality": { + "value": 3, + "max": 7 + } + } +} + +// desired output +{ + "ratings": [ + "primary", + "quality" + ] + // Aside: this is an example of implicit JSON array creation in the output which is detailed further down. + // For now just observe that the input keys "primary" and "quality" have both made it to the output. +} + +// spec +{ + "rating": { + "*": { + // match all keys below "rating" + "$": "ratings" + // output each of the "keys" to "ratings" in the output + } + } +} + ``` + +##### `#` Wildcard + +Valid both on the LHS and RHS, but has different behavior / format on either side. +The way to think of it is that it allows you to specify a "synthetic" value, i.e. a value not found in the input data. + +On the RHS of the spec, `#` is only valid in the context of an array, like "[#2]". +What "[#2]" means is, go up the three levels and ask that node how many matches it has, and then use that as an +index in the arrays. +This means that, while `shift` is doing its parallel tree walk of the input data and the spec, it tracks how many +matches it has processed at each level of the spec tree. + +This is useful if you want to take a JSON map and turn it into a JSON array, and you do not care about the order of the +array. + +On the LHS of the spec, `#` allows you to specify a hard coded string to be placed as a value in the output. + +The initial use-case for this feature was to be able to process a Boolean input value, and if the value is +boolean true, write out the string "enabled". Note, this was possible before, but it required two `shift` steps. + +```json +{ + "hidden" : { + "true": { + // if the value of "hidden" is true + "#disabled": "clients.clientId" // write the word "disabled" to the path "clients.clientId" + } + } +} +``` + +##### `|` Wildcard + +Valid only on the LHS of the spec. +This 'or' wildcard allows you to match multiple input keys. Useful if you don't always know exactly what your input data +will be. Example spec: + +```json +{ + "rating|Rating": "rating-primary" + // match "rating" or "Rating" copy the data to "rating-primary" +} +``` + +This is really just syntactic sugar, as the implementation really just treats the key "rating|Rating" as two keys when +processing. + +##### `@` Wildcard + +Valid on both sides of the spec. + +The basic `@` on the LHS. + +This wildcard is necessary if you want to put both the input value and the input key somewhere in the output JSON. + +Example `@` wildcard usage: + + ```json +// Say we have a spec that just operates on the value of the input key "rating" +{ + "foo": "place.to.put.value" + // leveraging the implicit operation of `shift` which is to operate on input JSON values +} + +// if we want to do something with the "key" as well as the value +{ + "foo": { + "$": "place.to.put.key", + "@": "place.to.put.value" + // `@` explicitly tell `shift` to operate on the input JSON value of the parent key "foo" + } +} +``` + +Thus, the `@` wildcard means "copy the value of the data at this level in the tree, to the output". + +Advanced `@` sign wildcard. +The format looks like "@(3,title)", where +"3" means go up the tree 3 levels and then look up the key +"title" and use the value at that key. + +See the *filter*.json* and *transpose*.json* unit test fixtures. + +#### JSON Arrays + +Reading from (input) and writing to (output) JSON Arrays is fully supported. + +1) Handling Arrays in the input JSON + +`shift` treats JSON arrays in the input data as Maps with numeric keys. Example : + +```json +// input +{ + "Photos": [ + "AAA.jpg", + "BBB.jpg" + ] +} + +// spec +{ + "Photos": { + "1": "photo-&-url" + // Specify that we only want to operate on the 1-th index of the "Photos" input array + } +} + +// output +{ + "photo-1-url": "BBB.jpg" +} +``` + +2) Handling Arrays in the output JSON + +Traditional array brackets ([]) are used to specify array index in the output JSON. []'s are only valid on the RHS +of the `shift` spec. + +Example: + +```json +// input +{ + "photo-1-id": "327704", + "photo-1-url": "http://bob.com/0001/327704/photo.jpg" +} + +// spec +{ + "photo-1-id": "Photos[1].Id", + // Declare the "Photos" in the output to be an array, + "photo-1-url": "Photos[1].Url" + // that the 1-th array location should have data + + // same as above but more powerful + // note `&` logic can be used inside the '[ ]' notation + "photo-*-url": "Photos[&(0,1)].Url" +} + +// output +{ + "Photos": [ + null, + // note Photos[0] is null, because no data was pushed to it + { + "Id": "327704", + "Url": "http://bob.com/0001/327704/photo.jpg" + } + ] +} +``` + +3) JSON arrays in the spec file + +JSON Arrays in `shift` spec are used to specify that a piece of input data should be copied to two places in the output JSON. + +Example : + +```json +// input +{ + "foo": 3 +} + +// spec +{ + "foo": [ + "bar", + "baz" + ] +} // push the 3, to both the output paths + +// output +{ + "bar": 3, + "baz": 3 +} +``` + +4) Implicit Array creation in the output JSON + +If a spec file is configured to output multiple pieces of data to the same output location, the output location +will be turned into a JSON array. + +Example: + +```json +// input +{ + "foo": "bar", + "tuna": "marlin" +} + +// spec +{ + "foo": "baz", + "tuna": "baz" +} + +// output +{ + "baz": [ + "bar", + "marlin" + ] + // Note the order of this Array should not be relied upon +} +``` + +Algorithm High Level + +Walk the input data, and `shift` spec simultaneously, and execute the `shift` command/mapping each time +there is a match. + +Algorithm Low Level + +- Simultaneously walk the spec and input JSON and maintain a walked "input" path data structure. +- Determine a match between input JSON key and LHS spec by matching LHS spec keys in the following order +(note that `|` keys are split into their subkeys, e.g. "literal", `*`, or `&` LHS keys): + +1) Try to match the input key with "literal" spec key values +2) If no literal match is found, try to match against LHS `&` computed values. + - For deterministic behaviour, if there is more than one `&` LHS key, they are applied/matched in alphabetical + order, after the `&` syntactic sugar is replaced with its canonical form. +3) If no match is found, try to match against LHS keys with `*` wildcard values. + - For deterministic behaviour, `*` wildcard keys are sorted and applied/matched in alphabetical order. + +Note, processing of the `@` and `$` LHS keys always occur if their parents match, and do not block any other matching. + +Implementation + +Instances of this class execute `shift` transformations given a transform spec of Jackson-style maps of maps +and a Jackson-style map-of-maps input. + +[↑ Back to top](#jolt-community-edition) + +--- + +### The `default` Operation + +> **Summary:** Adds default values to the output in a non-destructive way. Existing values are preserved. + +`default` is a kind of JOLT transform that applies default values in a non-destructive way. + +For comparison : +- `shift` walks the input data and asks its spec "Where should this go?" +- `default` walks the spec and asks: "Does this exist in the data? If not, add it." + +Example: Given input JSON like: + +```json + { + "Rating": 3, + "SecondaryRatings": { + "quality": { + "Range": 7, + "Value": 3, + "Id": "quality" + }, + "sharpness": { + "Value": 4, + "Id": "sharpness" + } + } +} +``` + +With the desired output being: + +```json + { + "Rating": 3, + "RatingRange": 5, + "SecondaryRatings": { + "quality": { + "Range": 7, + "Value": 3, + "Id": "quality", + "ValueLabel": null, + "Label": null, + "MaxLabel": "Great", + "MinLabel": "Terrible", + "DisplayType": "NORMAL" + }, + "sharpness": { + "Range": 5, + "Value": 4, + "Id": "sharpness", + "ValueLabel": null, + "Label": null, + "MaxLabel": "High", + "MinLabel": "Low", + "DisplayType": "NORMAL" + } + } +} +``` + +This is what the `default` Spec would look like: + +```json + { + "RatingRange": 5, + "SecondaryRatings": { + "quality|value": { + "ValueLabel": null, + "Label": null, + "MaxLabel": "Great", + "MinLabel": "Terrible", + "DisplayType": "NORMAL" + } + "*": { + "Range": 5, + "ValueLabel": null, + "Label": null, + "MaxLabel": "High", + "MinLabel": "Low", + "DisplayType": "NORMAL" + } + } +} +``` + +The Spec file format for `default` are tree Map objects. `default` handles outputting +of JSON Arrays via special wildcard in the Spec. + +`default` Spec wildcards and flag: +- "*" aka STAR: Apply these defaults to all input keys at this level +- "|" aka OR: Apply these defaults to input keys, if they exist +- "[]" aka: Signal to `default` that the data for this key should be an array. +This means all `default` keys below this entry have to be "integers". + +Valid Array Specification: + + ```json + { + "photos[]": { + "2": { + "url": "http://www.bazaarvoice.com", + "caption": "" + } + } +} + ``` + +An Invalid Array Specification would be: + + ```json + { + "photos[]": { + "photo-id-1234": { + "url": "http://www.bazaarvoice.com", + "caption": "" + } + } +} + ``` + +Algorithm + +`default` walks its Spec in a depth first way. +At each level in the Spec tree, `default` works from most specific to least specific Spec key: +- Literals key values +- "|", sub-sorted by how many or values there, then alphabetically (for deterministic behavior) +- "*" + +At a given level in the `default` Spec tree, only literal keys force `default` to create new entries +in the input data: either as a single literal value or adding new nested Array or Map objects. +The wildcard operators are applied after the literal keys and will not cause those keys to be +added if they are not already present in the input document (either naturally or having been defaulted +in from literal spec keys). + +Detailed algorithm -: + +1) Walk the spec +2) for each literal key in the spec (specKey) + - if the specKey is a map or array, and the input is null, default an empty Map or Array into the output + - re-curse on the literal spec + - if the specKey is a map or array, and the input is not null, but of the "wrong" type, skip and do not + recurse + - if the specKey, is a literal value, default the literal and value into the output and do not recurse +3) for each wildcard in the spec + - find all keys from the defaultee that match the wildcard + - treat each key as a literal speckey + +Corner Cases: + +Due to `default` array syntax, we can't actually express that we expect the top level of the input to be an Array. +The workaround for this is that we check the type of the object that is at the root level of the input: +- If it is a map, no problem. +- If it is an array, we treat the "root" level of the `default` spec, as if it were the child of an Array type `default` +entry. + +To force unambiguity, `default` throws an Exception if the input is null. + +[↑ Back to top](#jolt-community-edition) + +--- + +### The `remove` Operation + +> **Summary:** Removes specified keys and values from the input JSON. + +`remove` is a kind of JOLT transform that removes content from the input JSON. + +For comparison: +- `shift` walks the input data and asks its spec "Where should this go?" +- `default` walks the spec and asks "Does this exist in the data? If not, add it." +- `remove` walks the spec and asks "If this exists, remove it." + +Example: given input JSON like: + + ```json + { + "~emVersion": "2", + "id": "123124", + "productId": "31231231", + "submissionId": "34343", + "this": "stays", + "configured": { + "a": "b", + "c": "d" + } +} + ``` + +With the desired output being: + + ```json + { + "id": "123124", + "this": "stays", + "configured": { + "a": "b" + } +} + ``` + +This is what the `remove` Spec would look like: + + ```json + { + "~emVersion": "", + "productId": "", + "submissionId": "", + "configured": { + "c": "" + } +} + ``` + +#### `remove` Wildcards + +##### `*` Wildcard + +Valid only on the LHS (input JSON keys) side of a `remove` Spec. +The `*` wildcard can be used by itself or to match part of a key. + +`*` wildcard by itself: +To remove "all" keys under an input, use the `*` by itself on the LHS. + +```json +// example input +{ + "ratings": { + "Set1": { + "a": "a", + "b": "b" + }, + "Set2": { + "c": "c", + "b": "b" + } + } +} +//desired output +{ + "ratings": { + "Set1": { + "a": "a" + }, + "Set2": { + "c": "c" + } + } +} + +//Spec would be +{ + "ratings": { + "*": { + "b": "" + } + } +} +``` +In this example, "Set1" and "Set2" under rating both have the same structure, and thus we can use the `*` +to allow us to write more compact rules to remove "b" from all children under ratings. This is especially useful when we don't know +how many children will be under ratings, but we would like to nuke certain parts of it across. + +`*` wildcard as part of a key + +This is useful for working with input JSON with keys that are "prefixed". + +Ex: if you had an input document like: + +```json +{ + "ratings_legacy": { + "Set1":{ + "a": "a", + "b": "b" + }, + "Set2":{ + "a": "a", + "b": "b" + } + }, + "ratings_new":{ + "Set1":{ + "a": "a", + "b": "b" + }, + "Set2":{ + "a": "a", + "b": "b" + } + } +} +``` + +A `rating_*` would match both keys. As in `shift` wildcard matching, `*` wildcard is as non-greedy as possible, +which enables us to give more than one `*` in a key. + +For an output that removed Set1 from all `ratings_*` keys, the spec would be: + +```json +{ + "ratings_*": { + "Set1": "" + } +} +``` + +##### Arrays + +`remove` can also handle data in Arrays. + +It can walk through all the elements of an array with the `*` wildcard. + +Additionally, it can remove individual array indices. To do this, the LHS key +must be a number but in string format. + +Example: + +```json +{ + "spec": { + "array": { + "0": "" + } + } +} +``` + +In this case, `remove` will remove the zeroth item from the input "array", which will cause data at +index "1" to become the new "0". Because of this, `remove` matches all the literal/explicit +indices first, sorts them from biggest to smallest, then does the removing. + +[↑ Back to top](#jolt-community-edition) + +--- + +### The `modify` Operations + +> **Summary:** Modifies values in place using built-in functions. Available in three variants: overwrite, define, and default. + +The `modify` operations allow you to compute and modify values in your JSON using built-in functions. +Unlike `shift` which moves data, or `default` which only adds missing values, modifier operations apply functions +to transform existing values or create new ones. + +**Key Characteristics:** +- Modifies data in place without restructuring +- Supports function chaining and composition +- Can reference values from elsewhere in the document +- Works with both literal values and dynamic lookups + +#### Modifier Variants + +There are three variants of the modifier operation, each with different behaviour for handling existing values: + +##### 1. `modify-overwrite` (or `modify-overwrite-beta`) + +Writes the computed value whether the key exists or not. If the key exists, its value is overwritten. + +```json +{ + "operation": "modify-overwrite", + "spec": { + "fullName": "=concat(@(1,firstName),' ',@(1,lastName))" + } +} +``` + +##### 2. `modify-define` (or `modify-define-beta`) + +Only writes the computed value if the key does not exist. If the key exists (even with a `null` value), it is left unchanged. + +```json +{ + "operation": "modify-define", + "spec": { + "status": "=defaultValue('active')" + } +} +``` + +##### 3. `modify-default` (or `modify-default-beta`) + +Only writes the computed value if the key does not exist OR if its value is `null`. Existing non-null values are preserved. + +```json +{ + "operation": "modify-default", + "spec": { + "timestamp": "=now()" + } +} +``` + +##### Comparison with the `default` operation + +Compared to the `default` operation, `modify-default` and `modify-define` are more powerful and flexible: +- They can add computed/dynamic values using functions (`default` only adds static values) +- They can perform transformations (concat, toLower, calculations, etc.) +- They can reference other values using `@(levels,key)` lookups + +#### Spec Syntax + +The modifier spec follows these conventions: + +**Literal Values:** +```json +{ + "key": "literal value" +} +``` + +**Function Calls:** +Functions are prefixed with `=`: +```json +{ + "key": "=functionName(arg1, arg2, ...)" +} +``` + +**Lookups:** +Use `@(levels,key)` to reference values elsewhere in the document: +```json +{ + "derived": "=concat(@(1,field1), @(1,field2))" +} +``` + +**Context References:** +Use `^` to reference context values: +```json +{ + "contextValue": "^some.context.path" +} +``` + +**Passthrough:** +Use `@` alone to explicitly pass through the current value: +```json +{ + "unchanged": "@" +} +``` + +#### Functions Reference + +##### String Functions + +| Function | Description | Example | Result | +|--------------|-------------------------------------|----------------------------------|-------------------| +| `toLower` | Converts string to lowercase | `=toLower('HELLO')` | `"hello"` | +| `toUpper` | Converts string to uppercase | `=toUpper('hello')` | `"HELLO"` | +| `concat` | Concatenates multiple values | `=concat('Hello', ' ', 'World')` | `"Hello World"` | +| `join` | Joins values with a delimiter | `=join('-', 'a', 'b', 'c')` | `"a-b-c"` | +| `split` | Splits string by delimiter | `=split('-', 'a-b-c')` | `["a", "b", "c"]` | +| `substring` | Extracts substring | `=substring('Hello', 0, 3)` | `"Hel"` | +| `trim` | Removes leading/trailing whitespace | `=trim(' hello ')` | `"hello"` | +| `leftPad` | Pads string on the left | `=leftPad('5', 3, '0')` | `"005"` | +| `rightPad` | Pads string on the right | `=rightPad('5', 3, '0')` | `"500"` | +| `replace` | Replaces first occurrence | `=replace('hello', 'l', 'L')` | `"heLlo"` | +| `replaceAll` | Replaces all occurrences (regex) | `=replaceAll('hello', 'l', 'L')` | `"heLLo"` | + +##### Mathematical Functions + +| Function | Description | Example | Result | +|--------------------|------------------------------|--------------------------------|--------| +| `min` | Returns minimum value | `=min(5, 3, 9)` | `3` | +| `max` | Returns maximum value | `=max(5, 3, 9)` | `9` | +| `abs` | Absolute value | `=abs(-5)` | `5` | +| `avg` | Average of values | `=avg(2, 4, 6)` | `4.0` | +| `intSum` | Sum as integer | `=intSum(1, 2, 3)` | `6` | +| `doubleSum` | Sum as double | `=doubleSum(1.5, 2.5)` | `4.0` | +| `longSum` | Sum as long | `=longSum(100, 200)` | `300` | +| `intSubtract` | Subtract as integer | `=intSubtract(10, 3)` | `7` | +| `doubleSubtract` | Subtract as double | `=doubleSubtract(10.5, 3.2)` | `7.3` | +| `longSubtract` | Subtract as long | `=longSubtract(1000, 300)` | `700` | +| `divide` | Division | `=divide(10, 2)` | `5.0` | +| `divideAndRound` | Division with rounding | `=divideAndRound(10, 3, 0)` | `3` | +| `multiply` | Multiplication | `=multiply(5, 3)` | `15.0` | +| `multiplyAndRound` | Multiplication with rounding | `=multiplyAndRound(5.7, 3, 0)` | `17` | + +##### Type Conversion Functions + +| Function | Description | Example | Result | +|-------------|-----------------------------------|----------------------|--------| +| `toInteger` | Converts to integer | `=toInteger('42')` | `42` | +| `toDouble` | Converts to double | `=toDouble('3.14')` | `3.14` | +| `toLong` | Converts to long | `=toLong('9999')` | `9999` | +| `toBoolean` | Converts to boolean | `=toBoolean('true')` | `true` | +| `toString` | Converts to string | `=toString(42)` | `"42"` | +| `size` | Returns size of collection/string | `=size([1,2,3])` | `3` | + +##### List Functions + +| Function | Description | Example | Result | +|----------------|-----------------------------|--------------------------|-----------| +| `firstElement` | Gets first element of array | `=firstElement([1,2,3])` | `1` | +| `lastElement` | Gets last element of array | `=lastElement([1,2,3])` | `3` | +| `elementAt` | Gets element at index | `=elementAt([1,2,3], 1)` | `2` | +| `toList` | Converts value to list | `=toList(5)` | `[5]` | +| `sort` | Sorts list | `=sort([3,1,2])` | `[1,2,3]` | + +##### Object Functions + +| Function | Description | Example | +|--------------------------|---------------------------------|-----------------------------| +| `squashNulls` | Removes null values from object | `=squashNulls()` | +| `recursivelySquashNulls` | Recursively removes nulls | `=recursivelySquashNulls()` | +| `squashDuplicates` | Removes duplicate values | `=squashDuplicates()` | + +##### Date Functions + +| Function | Description | Example | +|------------------|----------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `now` | Returns current date/time string | `=now()` | +| `nowEpochMillis` | Returns current epoch milliseconds | `=nowEpochMillis()` | +| `fromEpochMilli` | Converts epoch millis to date | `=fromEpochMilli(1609459200000)` | +| `toEpochMilli` | Converts date to epoch millis | `=toEpochMilli('2021-01-01')` | +| `dateAdd` | Adds duration to date | `=dateAdd(date, amount, unit)` | +| `dateSubstract` | Subtracts duration from date | `=dateSubstract(date, amount, unit)` | +| `formatDate` | Change date from one format to another | `=formatDate('20210101', yyyyMMdd, yyyy-MM-dd)`
`=formatDate('202101011200', yyyyMMddHHmm, yyyy-MM-dd'T'HH:mm:ssXXX, Europe/Paris)`
`=formatDate('202101011200', yyyyMMddHHmm, yyyy-MM-dd'T'HH:mm:ss'Z', Europe/Paris, UTC)` | + +##### Utility Functions + +| Function | Description | Example | Result | +|-------------|-----------------------------|------------------------|----------------------| +| `noop` | Returns input unchanged | `=noop(value)` | `value` | +| `isPresent` | Checks if value exists | `=isPresent(@(1,key))` | `true/false` | +| `notNull` | Checks if value is not null | `=notNull(@(1,key))` | `true/false` | +| `isNull` | Checks if value is null | `=isNull(@(1,key))` | `true/false` | +| `uuid` | Generates a UUID | `=uuid()` | `"550e8400-e29b..."` | + +#### Example + +```json +{ + "operation": "modify-overwrite", + "spec": { + "person": { + "fullName": "=concat(@(1,firstName),' ',@(1,lastName))", + "age": "=toInteger(@(1,ageString))", + "email": "=toLower(@(1,email))", + "status": "=defaultValue('active')", + "createdAt": "=now()", + "id": "=uuid()" + } + } +} +``` + +**Input:** +```json +{ + "person": { + "firstName": "John", + "lastName": "Doe", + "ageString": "30", + "email": "JOHN.DOE@EXAMPLE.COM" + } +} +``` + +**Output:** +```json +{ + "person": { + "firstName": "John", + "lastName": "Doe", + "ageString": "30", + "email": "john.doe@example.com", + "fullName": "John Doe", + "age": 30, + "status": "active", + "createdAt": "2025-03-02T10:30:00Z", + "id": "550e8400-e29b-41d4-a716-446655440000" + } +} +``` + +[↑ Back to top](#jolt-community-edition) + +--- + +### The `enrich` Operation + +> **Summary:** Enriches a value by invoking a user-supplied Java method or a bean supplied in transform context. + +`enrich` is intended for cases where the built-in `modify-*` function DSL is not enough. Instead of using JOLT's +stock modifier functions, `enrich` resolves a Java method and writes its result back into the document. This is useful +for service lookups, application beans, and async/reactive integrations such as a Spring WebFlux `WebClient`. + +Each enrichment rule reads a value from `path`, invokes the target method, and writes the returned value to +`outputPath`. If `outputPath` is omitted, the original field at `path` is overwritten. + +```json +{ + "operation": "enrich", + "spec": { + "executionMode": "async", + "enrichments": [ + { + "path": "customer.id", + "outputPath": "customer.profile", + "contextKey": "customerLookup", + "method": "lookup" + } + ] + } +} +``` + +#### Spec Fields + +- `executionMode`: optional. `sync` (default) applies enrichments one by one. `async` starts all enrichments first, + then waits for all results before returning the transformed document. +- `enrichments`: required array of enrichment rules. +- `path`: required source path to read from the input document. Supports fixed object keys, explicit array indices such + as `[0]`, and array wildcards such as `[*]`. +- `outputPath`: optional destination path. Defaults to `path`. Supports fixed paths, explicit array indices, matching + `[*]` placeholders, and `[]` append semantics. +- `method`: required public method name to invoke. +- `className`: optional fully qualified class name to load with reflection. +- `contextKey`: optional key used to resolve the target object from the Chainr transform context. +- Exactly one of `className` or `contextKey` must be supplied. + +When `path` uses `[*]`, `outputPath` must either: +- use the same number of `[*]` segments so each match writes back to its corresponding array location, or +- use `[]` append semantics to collect results into a list. + +`[]` is not valid in `path`; it is output-only. + +#### Supported Method Signatures + +- `Object method(Object value)` +- `Object method(Object value, Object input)` +- `Object method(Object value, Object input, Map context)` + +The first argument is always the value found at `path`. The optional second argument is the full in-flight document, +and the optional third argument is the Chainr transform context map. + +#### Supported Return Types + +- `Object` +- `CompletionStage` +- `Publisher` such as a Reactor `Mono` + +When a `Publisher` is returned, it must emit at most one value. `async` parallelizes enrichment execution, but the +overall transform still waits for all enrichments to complete before returning a final document. + +#### Array Paths + +`enrich` can fan out over array elements by using `[*]` in `path`. Each match invokes the configured method once. + +```json +{ + "operation": "enrich", + "spec": { + "enrichments": [ + { + "path": "customers.[*].id", + "outputPath": "customers.[*].profile", + "className": "com.example.CustomerLookup", + "method": "lookup" + } + ] + } +} +``` + +For an input like: + +```json +{ + "customers": [ + { "id": "cust-101" }, + { "id": "cust-202" } + ] +} +``` + +the transform invokes `lookup(...)` twice and writes each result back to the matching array element: + +```json +{ + "customers": [ + { + "id": "cust-101", + "profile": { + "customerId": "cust-101" + } + }, + { + "id": "cust-202", + "profile": { + "customerId": "cust-202" + } + } + ] +} +``` + +#### `enrich` vs `modify-*` + +Use `modify-*` when the transformation can be expressed with built-in functions such as `concat`, `toLower`, or +`@(levels,key)` lookups. Use `enrich` when the value must come from arbitrary Java code, a bean supplied in the +transform context, or an async/reactive API call. + +#### Example + +```json +{ + "operation": "enrich", + "spec": { + "enrichments": [ + { + "path": "customer.id", + "outputPath": "customer.profile", + "className": "com.example.CustomerLookup", + "method": "lookup" + } + ] + }, + "input": { + "customer": { + "id": "cust-123" + } + }, + "output": { + "customer": { + "id": "cust-123", + "profile": { + "customerId": "cust-123", + "segment": "gold" + } + } + } +} +``` + +#### External API Example + +The following example shows `enrich` calling an external API through a context-supplied Spring bean. The JOLT spec +uses `contextKey` so the application can supply the already-configured client object at runtime. + +```json +{ + "operation": "enrich", + "spec": { + "executionMode": "async", + "enrichments": [ + { + "path": "customer.id", + "outputPath": "customer.profile", + "contextKey": "customerLookupClient", + "method": "lookupProfile" + } + ] + } +} +``` + +Example Spring WebFlux bean: + +```java +@Component +public class CustomerLookupClient { + + private final WebClient webClient; + + public CustomerLookupClient(WebClient webClient) { + this.webClient = webClient; + } + + public Mono lookupProfile(Object value, Object input, Map context) { + return webClient.get() + .uri("/customers/{id}", value) + .retrieve() + .bodyToMono(Object.class); + } +} +``` + +At runtime, place the bean in the Chainr context map under `customerLookupClient`. JOLT will resolve it through +`contextKey`, invoke `lookupProfile`, wait for the returned `Mono`, and store the response at `customer.profile`. + +[↑ Back to top](#jolt-community-edition) + +--- + +### The `cardinality` Operation + +> **Summary:** Ensures that values are either singular (ONE) or arrays (MANY). + +The CardinalityTransform changes the cardinality of input JSON data elements. +The impetus for the CardinalityTransform was to deal with data sources that are inconsistent with +respect to the cardinality of their returned data. + +For example, say you know that there will be a "photos" element in a document. If your underlying data +source is trying to be nice, it may adjust the "type" of the photos element, depending on how many +photos there actually are. + +Single photo : + +```json +{ + "photos" : {"url": "pants.com/1.jpg"} // photos element is a "single" map entry +} +``` + +Or multiple photos : + +```json +{ + "photos" : [ + {"url": "pants.com/1.jpg"}, + {"url": "pants.com/2.jpg"} + ] +} +``` + +The `shift` and `default` transforms can't handle that variability, so the CardinalityTransform was +created to "fix" document, so that the rest of the transforms can _assume_ "photos" will be an Array. + +At a base level, a single Cardinality "command" maps data into a "ONE" or "MANY" state. + +The idea is that you can start with a copy of your JSON input and modify it into a Cardinality spec by +specifying a "cardinality" for each piece of data that you care about changing in the output. +Input data that are not called out in the spec will remain in the output unchanged. + +For example, given this simple input JSON : + + ```json + { + "review": { + "rating": [ + 5, + 4 + ] + } +} + ``` + +A simple Cardinality spec could be constructed by specifying that the "rating" should be a single value: + + ```json + { + "review": { + "rating": "ONE" + } +} + ``` + +would produce the following output JSON : + + ```json + { + "review": { + "rating": 5 + } +} + ``` + +In this case, we turn the array "[ 5, 4 ]" into a single value by pulling the first index of the array. +Hence, the output has "rating: 5". + +Valid Cardinality Values (RHS: right hand side) + +- 'ONE': +If the input value is a List, grab the first element in that list and set it as the data for that element. +For all other input value types, no-op. + +- 'MANY': +If the input is not a List, make a list and set the first element to be the input value. +If the input is "null", make it be an empty list. +If the input is a list, no-op. + +Cardinality Wildcards + +As shown above, Cardinality specs can be entirely made up of literal string values, but wildcards similar +to some of those used by `shift` can be used. + +`*` Wildcard + +Valid only on the LHS (input JSON keys) side of a Cardinality Spec. +Unlike `shift`, the `*` wildcard can only be used by itself. It can be used to achieve a for/each manner of processing +input. + +Let's say we have the following input : + + ```json + { + "photosArray": [ + { + "url": [ + "http://pants.com/123-normal.jpg", + "http://pants.com/123-thumbnail.jpg" + ], + "caption": "Nice pants" + }, + { + "url": [ + "http://pants.com/123-thumbnail.jpg", + "http://pants.com/123-normal.jpg" + ], + "caption": "Nice pants" + } + ] +} + ``` + +And we'd like a spec that says "for each item 'url', convert to ONE": + + ```json + { + "photosArray": { + "*": { + // for each item in the array + "url": "ONE" + // url should be singular + } + } +} + ``` + +Which would yield the following output : + + ```json + { + "photosArray": [ + { + "url": "http://pants.com/123-normal.jpg", + "caption": "Nice pants" + }, + { + "url": "http://pants.com/123-thumbnail.jpg", + "caption": "Nice pants" + } + ] +} + ``` + +`@` Wildcard + +Valid only on the LHS of the spec. +This wildcard should be used when content nested within modified content needs to be modified as well. + +Let's say we have the following input: + + ```json + { + "views": [ + { + "count": 1024 + }, + { + "count": 2048 + } + ] +} + ``` + +The following spec would convert "views" to a ONE and "count" to a MANY : + + ```json + { + "views": { + "@": "ONE", + "count": "MANY" + } +} + ``` + +Yielding the following output: + + ```json + { + "views": { + "count": [ + 1024 + ] + } +} + ``` + +Cardinality Logic Table + +| INPUT | CARDINALITY | OUTPUT | NOTE | +|---------|-------------|--------|----------------------------------------------| +| String | ONE | String | no-op | +| Number | ONE | Number | no-op | +| Boolean | ONE | Map | no-op | +| Map | ONE | Map | no-op | +| List | ONE | [0] | use whatever the first item in the list was | +| String | MANY | List | make the input String, be [0] in a new list | +| Number | MANY | List | make the input Number, be [0] in a new list | +| Boolean | MANY | List | make the input Boolean, be [0] in a new list | +| Map | MANY | List | make the input Map, be [0] in a new list | +| List | MANY | List | no-op | + +[↑ Back to top](#jolt-community-edition) + +--- + +### The `sort` Operation + +> **Summary:** Recursively sorts all object keys alphabetically for deterministic output. + +Recursively sorts all maps within a JSON object into new sorted LinkedHashMaps so that serialised +representations are deterministic. Useful for debugging and making test fixtures. + +Note this will make a copy of the input Map and List objects. + +The sort order is standard alphabetical ascending, with a special case for "~" prefixed keys to be bumped to the top. + +[↑ Back to top](#jolt-community-edition) diff --git a/gettingStarted.md b/gettingStarted.md index 4ca24d59..7f2268c1 100644 --- a/gettingStarted.md +++ b/gettingStarted.md @@ -5,24 +5,24 @@ Maven Dependency to Add to your pom file ``` xml - com.bazaarvoice.jolt - jolt-core + io.github.jolt-community.jolt + jolt-community-core ${latest.jolt.version} - com.bazaarvoice.jolt - json-utils + io.github.jolt-community.jolt + json-community-utils ${latest.jolt.version} ``` -Where `latest.jolt.version` looks like `0.0.16`, and can be found by looking at the [project's releases](https://github.com/bazaarvoice/jolt/releases). +Where `latest.jolt.version` looks like `1.0.0`, and can be found by looking at the [project's releases](https://github.com/jolt-community/jolt-community/releases). The two maven artifacts are: -1. `jolt-core` : only one dependency on apache.commons for StringUtils - * The goal is for the `jolt-core` artifact to be pure Java, so that it does not cause any dependency issues. -2. `json-utils` : Jackson wrapper and testing utilities. Used by jolt-core as a test dependency. +1. `jolt-community-core` : only one dependency on apache.commons for StringUtils + * The goal is for the `jolt-community-coree` artifact to be pure Java, so that it does not cause any dependency issues. +2. `json-community-utils` : Jackson wrapper and testing utilities. Used by jolt-community-core as a test dependency. * If you are willing to pull in Jackson 2, this artifact provides nice utility methods. @@ -34,13 +34,13 @@ The two maven artifacts are: ### JoltSample.java -Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java). +Available [here](https://github.com/jolt-community/jolt-community/tree/main/jolt-core/src/test/java/io/joltcommunity/jolt/sample/JoltSample.java). ``` java -package com.bazaarvoice.jolt.sample; +package io.joltcommunity.jolt.sample; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JsonUtils; import java.io.IOException; import java.util.List; @@ -65,7 +65,7 @@ public class JoltSample { ``` ### /json/sample/input.json -Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/resources/json/sample/input.json). +Available [here](https://github.com/jolt-community/jolt-community/tree/main/jolt-core/src/test/resources/json/sample/input.json). ``` json { @@ -81,7 +81,7 @@ Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/t ``` ### /json/sample/spec.json -Available [here](https://github.com/bazaarvoice/jolt/tree/master/jolt-core/src/test/resources/json/sample/spec.json). +Available [here](https://github.com/jolt-community/jolt-community/tree/main/jolt-core/src/test/resources/json/sample/spec.json). ``` json [ diff --git a/guice/pom.xml b/guice/pom.xml index c337b10a..2544d394 100644 --- a/guice/pom.xml +++ b/guice/pom.xml @@ -1,53 +1,92 @@ - + 4.0.0 - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 ../parent/pom.xml - jolt-guice + jolt-community-guice Jolt Guice Integration jar + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + - javax.inject - javax.inject + jakarta.inject + jakarta.inject-api + ${jakarta-inject.version} com.google.inject guice + ${guice.version} - com.bazaarvoice.jolt - json-utils + io.github.jolt-community.jolt + json-community-utils ${project.version} - com.bazaarvoice.jolt - jolt-core + io.github.jolt-community.jolt + jolt-community-core ${project.version} org.testng testng + ${testng.version} test com.google.guava guava + ${guava.version} test - \ No newline at end of file + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.base/java.security=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadoc + none + + + + + + diff --git a/guice/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/GuiceChainrInstantiator.java b/guice/src/main/java/io/joltcommunity/jolt/chainr/instantiator/GuiceChainrInstantiator.java similarity index 70% rename from guice/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/GuiceChainrInstantiator.java rename to guice/src/main/java/io/joltcommunity/jolt/chainr/instantiator/GuiceChainrInstantiator.java index 8ebf0616..8e9a521e 100644 --- a/guice/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/GuiceChainrInstantiator.java +++ b/guice/src/main/java/io/joltcommunity/jolt/chainr/instantiator/GuiceChainrInstantiator.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.instantiator; +package io.joltcommunity.jolt.chainr.instantiator; -import com.bazaarvoice.jolt.JoltTransform; -import com.bazaarvoice.jolt.chainr.spec.ChainrEntry; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.JoltTransform; +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.exception.SpecException; import com.google.inject.AbstractModule; import com.google.inject.Guice; import com.google.inject.Injector; @@ -44,45 +45,44 @@ public class GuiceChainrInstantiator implements ChainrInstantiator { /** * @param parentModule Guice module that will be used to create an injector to instantiate Transform classes */ - public GuiceChainrInstantiator( Module parentModule ) { + public GuiceChainrInstantiator(Module parentModule) { this.parentModule = parentModule; - this.nonSpecInjector = Guice.createInjector( parentModule ); + this.nonSpecInjector = Guice.createInjector(parentModule); } @Override - public JoltTransform hydrateTransform( ChainrEntry entry ) { + public JoltTransform hydrateTransform(ChainrEntry entry) { final Class transformClass = entry.getJoltTransformClass(); final Object transformSpec = entry.getSpec(); try { - if ( entry.isSpecDriven() ) { + if (entry.isSpecDriven()) { // In order to inject an "Object" into the constructor of a SpecTransform, we create an Injector just for this class. Injector injector; - injector = Guice.createInjector( new AbstractModule() { + injector = Guice.createInjector(new AbstractModule() { @Override protected void configure() { // install the parent module so that Custom Java Transforms or Templates can have @Injected properties filled in - install( parentModule ); + install(parentModule); // Bind the "spec" for the transform - bind( Object.class ).toInstance( transformSpec ); + bind(Object.class).toInstance(transformSpec); } - } ); + }); - return injector.getInstance( transformClass ); + return injector.getInstance(transformClass); } else { // else normal no-op constructor OR non-spec constructor with @Inject annotation - return nonSpecInjector.getInstance( transformClass ); + return nonSpecInjector.getInstance(transformClass); } - } - catch ( Exception creationException ) { - throw new SpecException( "Exception using Guice to initialize class:" + transformClass.getCanonicalName() + entry.getErrorMessageIndexSuffix(), creationException ); + } catch (Exception creationException) { + throw new SpecException("Exception using Guice to initialize class:" + transformClass.getCanonicalName() + entry.getErrorMessageIndexSuffix(), creationException); } } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/GuicedChainrContextTest.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/GuicedChainrContextTest.java similarity index 54% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/GuicedChainrContextTest.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/GuicedChainrContextTest.java index 8d790794..bafb2bb8 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/GuicedChainrContextTest.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/GuicedChainrContextTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; +package io.joltcommunity.jolt.chainr; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.chainr.instantiator.GuiceChainrInstantiator; -import com.bazaarvoice.jolt.chainr.transforms.GuiceContextDrivenTransform; -import com.bazaarvoice.jolt.chainr.transforms.GuiceSpecAndContextDrivenTransform; +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.chainr.instantiator.GuiceChainrInstantiator; +import io.joltcommunity.jolt.chainr.transforms.GuiceContextDrivenTransform; +import io.joltcommunity.jolt.chainr.transforms.GuiceSpecAndContextDrivenTransform; import com.google.common.collect.Lists; import com.google.inject.AbstractModule; import com.google.inject.Module; @@ -39,28 +40,28 @@ public class GuicedChainrContextTest { public Iterator getCases() throws IOException { String testPath = "/json/chainr/guice_spec_with_context.json"; - Map testSuite = JsonUtils.classpathToMap( testPath ); + Map testSuite = JsonUtils.classpathToMap(testPath); - Object spec = testSuite.get( "spec" ); - List tests = (List) testSuite.get( "tests" ); + Object spec = testSuite.get("spec"); + List tests = (List) testSuite.get("tests"); List accum = Lists.newLinkedList(); - for ( Map testCase : tests ) { + for (Map testCase : tests) { - String testCaseName = (String) testCase.get( "testCaseName" ); - Object input = testCase.get( "input" ); - Map context = (Map) testCase.get( "context" ); - Object expected = testCase.get( "expected" ); + String testCaseName = (String) testCase.get("testCaseName"); + Object input = testCase.get("input"); + Map context = (Map) testCase.get("context"); + Object expected = testCase.get("expected"); - accum.add( new Object[] { testCaseName, spec, input, context, expected } ); + accum.add(new Object[]{testCaseName, spec, input, context, expected}); } return accum.iterator(); } - @Test( dataProvider = "getCases") - public void successCases( String testCaseName, Object spec, Object input, Map context, Object expected ) throws IOException { + @Test(dataProvider = "getCases") + public void successCases(String testCaseName, Object spec, Object input, Map context, Object expected) throws IOException { Module parentModule = new AbstractModule() { @Override @@ -69,22 +70,22 @@ protected void configure() { @Provides public GuiceContextDrivenTransform.GuiceConfig getConfigC() { - return new GuiceContextDrivenTransform.GuiceConfig( "c", "cc" ); + return new GuiceContextDrivenTransform.GuiceConfig("c", "cc"); } @Provides public GuiceSpecAndContextDrivenTransform.GuiceConfig getConfigD() { - return new GuiceSpecAndContextDrivenTransform.GuiceConfig( "dd" ); + return new GuiceSpecAndContextDrivenTransform.GuiceConfig("dd"); } }; - Chainr unit = Chainr.fromSpec( spec, new GuiceChainrInstantiator( parentModule ) ); + Chainr unit = Chainr.fromSpec(spec, new GuiceChainrInstantiator(parentModule)); - Assert.assertTrue( unit.hasContextualTransforms() ); - Assert.assertEquals( unit.getContextualTransforms().size(), 2 ); + Assert.assertTrue(unit.hasContextualTransforms()); + Assert.assertEquals(unit.getContextualTransforms().size(), 2); - Object actual = unit.transform( input, context ); + Object actual = unit.transform(input, context); - JoltTestUtil.runDiffy( "failed case " + testCaseName, expected, actual ); + JoltTestUtil.runDiffy("failed case " + testCaseName, expected, actual); } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/GuicedChainrTest.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/GuicedChainrTest.java similarity index 59% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/GuicedChainrTest.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/GuicedChainrTest.java index 40796b2a..3ec30ecf 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/GuicedChainrTest.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/GuicedChainrTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; - -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.chainr.instantiator.GuiceChainrInstantiator; -import com.bazaarvoice.jolt.chainr.transforms.GuiceSpecDrivenTransform; -import com.bazaarvoice.jolt.chainr.transforms.GuiceTransform; -import com.bazaarvoice.jolt.chainr.transforms.GuiceTransformMissingInjectAnnotation; -import com.bazaarvoice.jolt.exception.SpecException; +package io.joltcommunity.jolt.chainr; + +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.chainr.instantiator.GuiceChainrInstantiator; +import io.joltcommunity.jolt.chainr.transforms.GuiceSpecDrivenTransform; +import io.joltcommunity.jolt.chainr.transforms.GuiceTransform; +import io.joltcommunity.jolt.chainr.transforms.GuiceTransformMissingInjectAnnotation; +import io.joltcommunity.jolt.exception.SpecException; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.inject.AbstractModule; @@ -39,11 +40,11 @@ public class GuicedChainrTest { public void successTestCase() throws IOException { String testPath = "/json/chainr/guice_spec.json"; - Map testUnit = JsonUtils.classpathToMap( testPath ); + Map testUnit = JsonUtils.classpathToMap(testPath); - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); Module parentModule = new AbstractModule() { @Override @@ -52,33 +53,32 @@ protected void configure() { @Provides public GuiceTransform.GuiceConfig getConfigC() { - return new GuiceTransform.GuiceConfig( "c", "cc" ); + return new GuiceTransform.GuiceConfig("c", "cc"); } @Provides public GuiceSpecDrivenTransform.GuiceConfig getConfigD() { - return new GuiceSpecDrivenTransform.GuiceConfig( "dd" ); + return new GuiceSpecDrivenTransform.GuiceConfig("dd"); } }; - Chainr unit = Chainr.fromSpec( spec, new GuiceChainrInstantiator( parentModule ) ); + Chainr unit = Chainr.fromSpec(spec, new GuiceChainrInstantiator(parentModule)); - Assert.assertFalse( unit.hasContextualTransforms() ); - Assert.assertEquals( unit.getContextualTransforms().size(), 0 ); + Assert.assertFalse(unit.hasContextualTransforms()); + Assert.assertEquals(unit.getContextualTransforms().size(), 0); - Object actual = unit.transform( input, null ); + Object actual = unit.transform(input, null); - JoltTestUtil.runDiffy( "failed case " + testPath, expected, actual ); + JoltTestUtil.runDiffy("failed case " + testPath, expected, actual); } - @Test( expectedExceptions = SpecException.class ) - public void itBlowsUpForMissingProviderStockTransform() throws IOException - { + @Test(expectedExceptions = SpecException.class) + public void itBlowsUpForMissingProviderStockTransform() throws IOException { String testPath = "/json/chainr/guice_spec.json"; - Map testUnit = JsonUtils.classpathToMap( testPath ); + Map testUnit = JsonUtils.classpathToMap(testPath); - Object spec = testUnit.get( "spec" ); + Object spec = testUnit.get("spec"); Module parentModule = new AbstractModule() { @Override @@ -87,20 +87,19 @@ protected void configure() { @Provides public GuiceSpecDrivenTransform.GuiceConfig getConfigD() { - return new GuiceSpecDrivenTransform.GuiceConfig( "dd" ); + return new GuiceSpecDrivenTransform.GuiceConfig("dd"); } }; - Chainr.fromSpec( spec, new GuiceChainrInstantiator( parentModule ) ); + Chainr.fromSpec(spec, new GuiceChainrInstantiator(parentModule)); } - @Test( expectedExceptions = SpecException.class ) - public void itBlowsUpForMissingProviderSpecTransform() throws IOException - { + @Test(expectedExceptions = SpecException.class) + public void itBlowsUpForMissingProviderSpecTransform() throws IOException { String testPath = "/json/chainr/guice_spec.json"; - Map testUnit = JsonUtils.classpathToMap( testPath ); + Map testUnit = JsonUtils.classpathToMap(testPath); - Object spec = testUnit.get( "spec" ); + Object spec = testUnit.get("spec"); Module parentModule = new AbstractModule() { @Override @@ -109,25 +108,25 @@ protected void configure() { @Provides public GuiceTransform.GuiceConfig getConfigC() { - return new GuiceTransform.GuiceConfig( "c", "cc" ); + return new GuiceTransform.GuiceConfig("c", "cc"); } }; - Chainr.fromSpec( spec, new GuiceChainrInstantiator( parentModule ) ); + Chainr.fromSpec(spec, new GuiceChainrInstantiator(parentModule)); } - @Test( expectedExceptions = SpecException.class ) + @Test(expectedExceptions = SpecException.class) public void itBlowsUpForBadGuiceTransform() { - Chainr.fromSpec( ImmutableList.of( ImmutableMap.of( "operator", "com.bazaarvoice.jolt.chainr.transforms.GuiceTransformMissingInjectAnnotation" ) ), - new GuiceChainrInstantiator( new AbstractModule() { + Chainr.fromSpec(ImmutableList.of(ImmutableMap.of("operator", "io.joltcommunity.jolt.chainr.transforms.GuiceTransformMissingInjectAnnotation")), + new GuiceChainrInstantiator(new AbstractModule() { @Override protected void configure() { } @Provides public GuiceTransformMissingInjectAnnotation.BadGuiceConfig getConfigC() { - return new GuiceTransformMissingInjectAnnotation.BadGuiceConfig( "b:", "bad" ); + return new GuiceTransformMissingInjectAnnotation.BadGuiceConfig("b:", "bad"); } - } ) ); + })); } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/JoltTestUtil.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/JoltTestUtil.java similarity index 58% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/JoltTestUtil.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/JoltTestUtil.java index d7f30dc6..d72dff2d 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/JoltTestUtil.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/JoltTestUtil.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,21 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; +package io.joltcommunity.jolt.chainr; -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtils; +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtils; import org.testng.Assert; public class JoltTestUtil { private static final Diffy diffy = new Diffy(); - public static void runDiffy( String failureMessage, Object expected, Object actual ) { + public static void runDiffy(String failureMessage, Object expected, Object actual) { - Diffy.Result result = diffy.diff( expected, actual ); + Diffy.Result result = diffy.diff(expected, actual); if (!result.isEmpty()) { - Assert.fail( failureMessage + ".\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + Assert.fail(failureMessage + ".\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); } } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceContextDrivenTransform.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceContextDrivenTransform.java similarity index 66% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceContextDrivenTransform.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceContextDrivenTransform.java index de22ac19..27e8a68e 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceContextDrivenTransform.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceContextDrivenTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.ContextualTransform; +import io.joltcommunity.jolt.ContextualTransform; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.Map; public class GuiceContextDrivenTransform implements ContextualTransform { @@ -26,27 +27,27 @@ public class GuiceContextDrivenTransform implements ContextualTransform { private final GuiceConfig guiceConfig; // Value we get form Guice - public static class GuiceConfig { - private final String key, value; - - public GuiceConfig( String key, String value ) { - this.key = key; - this.value = value; - } - } - @Inject - public GuiceContextDrivenTransform( GuiceConfig guiceConfig ) { + public GuiceContextDrivenTransform(GuiceConfig guiceConfig) { this.guiceConfig = guiceConfig; } @Override - public Object transform( Object input, Map context ) { + public Object transform(Object input, Map context) { - String suffix = (String) context.get( CONTEXT_KEY ); + String suffix = (String) context.get(CONTEXT_KEY); - ( (Map) input ).put( guiceConfig.key, guiceConfig.value + suffix ); + ((Map) input).put(guiceConfig.key, guiceConfig.value + suffix); return input; } + + public static class GuiceConfig { + private final String key, value; + + public GuiceConfig(String key, String value) { + this.key = key; + this.value = value; + } + } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceSpecAndContextDrivenTransform.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceSpecAndContextDrivenTransform.java similarity index 64% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceSpecAndContextDrivenTransform.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceSpecAndContextDrivenTransform.java index 2a0dd4a0..64e8c165 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceSpecAndContextDrivenTransform.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceSpecAndContextDrivenTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.ContextualTransform; -import com.bazaarvoice.jolt.SpecDriven; +import io.joltcommunity.jolt.ContextualTransform; +import io.joltcommunity.jolt.SpecDriven; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.Map; public class GuiceSpecAndContextDrivenTransform implements SpecDriven, ContextualTransform { @@ -30,27 +31,27 @@ public class GuiceSpecAndContextDrivenTransform implements SpecDriven, Contextua private final String specKeyValue; // Value we get from the spec private final GuiceConfig guiceConfig; // Value we get form Guice - public static class GuiceConfig { - private final String value; - - public GuiceConfig( String value ) { - this.value = value; - } - } - @Inject - public GuiceSpecAndContextDrivenTransform( GuiceConfig guiceConfig, Object spec ) { + public GuiceSpecAndContextDrivenTransform(GuiceConfig guiceConfig, Object spec) { this.guiceConfig = guiceConfig; - specKeyValue = (String) ( (Map) spec ).get( SPEC_DRIVEN_KEY ); + specKeyValue = (String) ((Map) spec).get(SPEC_DRIVEN_KEY); } @Override - public Object transform( Object input, Map context ) { + public Object transform(Object input, Map context) { - String suffix = (String) context.get( CONTEXT_KEY ); + String suffix = (String) context.get(CONTEXT_KEY); - ( (Map) input ).put( specKeyValue, guiceConfig.value + suffix ); + ((Map) input).put(specKeyValue, guiceConfig.value + suffix); return input; } + + public static class GuiceConfig { + private final String value; + + public GuiceConfig(String value) { + this.value = value; + } + } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceSpecDrivenTransform.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceSpecDrivenTransform.java similarity index 66% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceSpecDrivenTransform.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceSpecDrivenTransform.java index 51f72408..3bc75f4b 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceSpecDrivenTransform.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceSpecDrivenTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,40 +14,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.SpecDriven; -import com.bazaarvoice.jolt.Transform; +import io.joltcommunity.jolt.SpecDriven; +import io.joltcommunity.jolt.Transform; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.Map; public class GuiceSpecDrivenTransform implements SpecDriven, Transform { private static final String SPEC_DRIVEN_KEY = "KEY_TO_ADD"; - - public static class GuiceConfig { - private final String value; - - public GuiceConfig( String value ) { - this.value = value; - } - } - private final GuiceConfig guiceConfig; private final String specKeyValue; @Inject - public GuiceSpecDrivenTransform( GuiceConfig guiceConfig, Object spec ) { + public GuiceSpecDrivenTransform(GuiceConfig guiceConfig, Object spec) { this.guiceConfig = guiceConfig; - specKeyValue = (String) ((Map) spec).get( SPEC_DRIVEN_KEY ); + specKeyValue = (String) ((Map) spec).get(SPEC_DRIVEN_KEY); } @Override - public Object transform( Object input ) { + public Object transform(Object input) { - ((Map) input).put( specKeyValue, guiceConfig.value ); + ((Map) input).put(specKeyValue, guiceConfig.value); return input; } + + public static class GuiceConfig { + private final String value; + + public GuiceConfig(String value) { + this.value = value; + } + } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceTransform.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceTransform.java similarity index 70% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceTransform.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceTransform.java index 95c055b2..3bcd47f3 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceTransform.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,35 +14,35 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.Transform; +import io.joltcommunity.jolt.Transform; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.Map; public class GuiceTransform implements Transform { - public static class GuiceConfig { - private final String key, value; - - public GuiceConfig( String key, String value ) { - this.key = key; - this.value = value; - } - } - private final GuiceConfig config; @Inject - public GuiceTransform( GuiceConfig config ) { + public GuiceTransform(GuiceConfig config) { this.config = config; } @Override - public Object transform( Object input ) { + public Object transform(Object input) { - ((Map) input).put( config.key, config.value ); + ((Map) input).put(config.key, config.value); return input; } + + public static class GuiceConfig { + private final String key, value; + + public GuiceConfig(String key, String value) { + this.key = key; + this.value = value; + } + } } diff --git a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceTransformMissingInjectAnnotation.java b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceTransformMissingInjectAnnotation.java similarity index 72% rename from guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceTransformMissingInjectAnnotation.java rename to guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceTransformMissingInjectAnnotation.java index f60bbe0b..5aea2f9b 100644 --- a/guice/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GuiceTransformMissingInjectAnnotation.java +++ b/guice/src/test/java/io/joltcommunity/jolt/chainr/transforms/GuiceTransformMissingInjectAnnotation.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.Transform; +import io.joltcommunity.jolt.Transform; import java.util.Map; @@ -24,25 +25,25 @@ */ public class GuiceTransformMissingInjectAnnotation implements Transform { - public static class BadGuiceConfig { - private final String key, value; - - public BadGuiceConfig( String key, String value ) { - this.key = key; - this.value = value; - } - } - private final BadGuiceConfig config; - public GuiceTransformMissingInjectAnnotation( BadGuiceConfig config ) { + public GuiceTransformMissingInjectAnnotation(BadGuiceConfig config) { this.config = config; } @Override - public Object transform( Object input ) { + public Object transform(Object input) { - ((Map) input).put( config.key, config.value ); + ((Map) input).put(config.key, config.value); return input; } + + public static class BadGuiceConfig { + private final String key, value; + + public BadGuiceConfig(String key, String value) { + this.key = key; + this.value = value; + } + } } diff --git a/guice/src/test/resources/json/chainr/guice_spec.json b/guice/src/test/resources/json/chainr/guice_spec.json index 360040fa..f73a2a2c 100644 --- a/guice/src/test/resources/json/chainr/guice_spec.json +++ b/guice/src/test/resources/json/chainr/guice_spec.json @@ -1,9 +1,8 @@ { // Input data for the unit test data. Will verify that this data passes all the way thru. "input": { - "a" : "aa" + "a": "aa" }, - "spec": [ { // Verify that the GuiceInstantiator correctly loads stock transforms @@ -14,17 +13,16 @@ }, { // This guy will add ("c" : "cc"), where the Guice config tells it to output ("c" : "cc") - "operation": "com.bazaarvoice.jolt.chainr.transforms.GuiceTransform" + "operation": "io.joltcommunity.jolt.chainr.transforms.GuiceTransform" }, { // This guy will add "d" : "dd", where the "d" comes from the spec, and the "dd" comes from Guice - "operation": "com.bazaarvoice.jolt.chainr.transforms.GuiceSpecDrivenTransform", - "spec" : { - "KEY_TO_ADD" : "d" + "operation": "io.joltcommunity.jolt.chainr.transforms.GuiceSpecDrivenTransform", + "spec": { + "KEY_TO_ADD": "d" } } ], - "expected": { "a": "aa", "b": "bb", diff --git a/guice/src/test/resources/json/chainr/guice_spec_with_context.json b/guice/src/test/resources/json/chainr/guice_spec_with_context.json index 855fad30..f80e41eb 100644 --- a/guice/src/test/resources/json/chainr/guice_spec_with_context.json +++ b/guice/src/test/resources/json/chainr/guice_spec_with_context.json @@ -12,32 +12,29 @@ // "c" is hardcoded // "cc" comes from guice injection // suffix comes from context - "operation": "com.bazaarvoice.jolt.chainr.transforms.GuiceContextDrivenTransform" + "operation": "io.joltcommunity.jolt.chainr.transforms.GuiceContextDrivenTransform" }, { // This guy will add "d" : "dd" + suffix // "d" is from the spec KEY_TO_ADD // "dd" comes from guice injection // suffix comes from context - "operation": "com.bazaarvoice.jolt.chainr.transforms.GuiceSpecAndContextDrivenTransform", - "spec" : { - "KEY_TO_ADD" : "d" + "operation": "io.joltcommunity.jolt.chainr.transforms.GuiceSpecAndContextDrivenTransform", + "spec": { + "KEY_TO_ADD": "d" } } ], - - "tests" : [ + "tests": [ { - "testCaseName" : "cc, dd, and -guice", - + "testCaseName": "cc, dd, and -guice", // Input data for the unit test data. Will verify that this data passes all the way thru. "input": { - "a" : "aa" + "a": "aa" }, - "context" : { - "suffix" : "-guice" + "context": { + "suffix": "-guice" }, - "expected": { "a": "aa", "b": "bb", @@ -46,16 +43,14 @@ } }, { - "testCaseName" : "cc, dd, and -tuna", - + "testCaseName": "cc, dd, and -tuna", // Input data for the unit test data. Will verify that this data passes all the way thru. "input": { - "a" : "aa" + "a": "aa" }, - "context" : { - "suffix" : "-tuna" + "context": { + "suffix": "-tuna" }, - "expected": { "a": "aa", "b": "bb", @@ -64,5 +59,4 @@ } } ] - } diff --git a/jolt-core/pom.xml b/jolt-core/pom.xml index 4dff57ad..16c150f2 100644 --- a/jolt-core/pom.xml +++ b/jolt-core/pom.xml @@ -1,24 +1,38 @@ - + 4.0.0 - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 ../parent/pom.xml - jolt-core + jolt-community-core Jolt Core jar + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + - javax.inject - javax.inject + jakarta.inject + jakarta.inject-api + ${jakarta-inject.version} + + + + org.reactivestreams + reactive-streams @@ -29,8 +43,8 @@ - com.bazaarvoice.jolt - json-utils + io.github.jolt-community.jolt + json-community-utils ${project.version} test @@ -38,11 +52,13 @@ com.google.guava guava + ${guava.version} test org.testng testng + ${testng.version} test @@ -50,18 +66,34 @@ - org.codehaus.mojo - cobertura-maven-plugin - 2.7 + org.apache.maven.plugins + maven-surefire-plugin - - - html - xml - + @{argLine} -Dfile.encoding=UTF-8 -Djava.awt.headless=true + + test/integration/** + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + - diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Modifier.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/Modifier.java deleted file mode 100644 index 7b70d25c..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Modifier.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.modifier.OpMode; -import com.bazaarvoice.jolt.modifier.TemplatrSpecBuilder; -import com.bazaarvoice.jolt.modifier.function.Function; -import com.bazaarvoice.jolt.modifier.function.Lists; -import com.bazaarvoice.jolt.modifier.function.Math; -import com.bazaarvoice.jolt.modifier.function.Objects; -import com.bazaarvoice.jolt.modifier.function.Strings; -import com.bazaarvoice.jolt.modifier.spec.ModifierCompositeSpec; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Base Templatr transform that to behave differently based on provided opMode - */ -public abstract class Modifier implements SpecDriven, ContextualTransform { - - private static final Map STOCK_FUNCTIONS = new HashMap<>( ); - - static { - STOCK_FUNCTIONS.put( "toLower", new Strings.toLowerCase() ); - STOCK_FUNCTIONS.put( "toUpper", new Strings.toUpperCase() ); - STOCK_FUNCTIONS.put( "concat", new Strings.concat() ); - STOCK_FUNCTIONS.put( "join", new Strings.join() ); - STOCK_FUNCTIONS.put( "split", new Strings.split() ); - STOCK_FUNCTIONS.put( "substring", new Strings.substring() ); - STOCK_FUNCTIONS.put( "trim", new Strings.trim() ); - STOCK_FUNCTIONS.put( "leftPad", new Strings.leftPad() ); - STOCK_FUNCTIONS.put( "rightPad", new Strings.rightPad() ); - - STOCK_FUNCTIONS.put( "min", new Math.min() ); - STOCK_FUNCTIONS.put( "max", new Math.max() ); - STOCK_FUNCTIONS.put( "abs", new Math.abs() ); - STOCK_FUNCTIONS.put( "avg", new Math.avg() ); - STOCK_FUNCTIONS.put( "intSum", new Math.intSum() ); - STOCK_FUNCTIONS.put( "doubleSum", new Math.doubleSum() ); - STOCK_FUNCTIONS.put( "longSum", new Math.longSum() ); - STOCK_FUNCTIONS.put( "intSubtract", new Math.intSubtract() ); - STOCK_FUNCTIONS.put( "doubleSubtract", new Math.doubleSubtract() ); - STOCK_FUNCTIONS.put( "longSubtract", new Math.longSubtract() ); - STOCK_FUNCTIONS.put( "divide", new Math.divide() ); - STOCK_FUNCTIONS.put( "divideAndRound", new Math.divideAndRound() ); - - - STOCK_FUNCTIONS.put( "toInteger", new Objects.toInteger() ); - STOCK_FUNCTIONS.put( "toDouble", new Objects.toDouble() ); - STOCK_FUNCTIONS.put( "toLong", new Objects.toLong() ); - STOCK_FUNCTIONS.put( "toBoolean", new Objects.toBoolean() ); - STOCK_FUNCTIONS.put( "toString", new Objects.toString() ); - STOCK_FUNCTIONS.put( "size", new Objects.size() ); - - STOCK_FUNCTIONS.put( "squashNulls", new Objects.squashNulls() ); - STOCK_FUNCTIONS.put( "recursivelySquashNulls", new Objects.recursivelySquashNulls() ); - STOCK_FUNCTIONS.put( "squashDuplicates", new Objects.squashDuplicates() ); - - STOCK_FUNCTIONS.put( "noop", Function.noop ); - STOCK_FUNCTIONS.put( "isPresent", Function.isPresent ); - STOCK_FUNCTIONS.put( "notNull", Function.notNull ); - STOCK_FUNCTIONS.put( "isNull", Function.isNull ); - - STOCK_FUNCTIONS.put( "firstElement", new Lists.firstElement() ); - STOCK_FUNCTIONS.put( "lastElement", new Lists.lastElement() ); - STOCK_FUNCTIONS.put( "elementAt", new Lists.elementAt() ); - STOCK_FUNCTIONS.put( "toList", new Lists.toList() ); - STOCK_FUNCTIONS.put( "sort", new Lists.sort() ); - } - - private final ModifierCompositeSpec rootSpec; - - @SuppressWarnings( "unchecked" ) - private Modifier( Object spec, OpMode opMode, Map functionsMap ) { - if ( spec == null ){ - throw new SpecException( opMode.name() + " expected a spec of Map type, got 'null'." ); - } - if ( ! ( spec instanceof Map ) ) { - throw new SpecException( opMode.name() + " expected a spec of Map type, got " + spec.getClass().getSimpleName() ); - } - - if(functionsMap == null || functionsMap.isEmpty()) { - throw new SpecException( opMode.name() + " expected a populated functions' map type, got " + (functionsMap == null?"null":"empty") ); - } - - functionsMap = Collections.unmodifiableMap( functionsMap ); - TemplatrSpecBuilder templatrSpecBuilder = new TemplatrSpecBuilder( opMode, functionsMap ); - rootSpec = new ModifierCompositeSpec( ROOT_KEY, (Map) spec, opMode, templatrSpecBuilder ); - } - - @Override - public Object transform( final Object input, final Map context ) { - - Map contextWrapper = new HashMap<>( ); - contextWrapper.put( ROOT_KEY, context ); - - MatchedElement rootLpe = new MatchedElement( ROOT_KEY ); - WalkedPath walkedPath = new WalkedPath(); - walkedPath.add( input, rootLpe ); - - rootSpec.apply( ROOT_KEY, Optional.of( input), walkedPath, null, contextWrapper ); - return input; - } - - /** - * This variant of modifier creates the key/index is missing, - * and overwrites the value if present - */ - public static final class Overwritr extends Modifier { - - public Overwritr( Object spec ) { - this( spec, STOCK_FUNCTIONS ); - } - - public Overwritr( Object spec, Map functionsMap ) { - super( spec, OpMode.OVERWRITR, functionsMap ); - } - } - - /** - * This variant of modifier only writes when the key/index is missing - */ - public static final class Definr extends Modifier { - - public Definr( final Object spec ) { - this( spec, STOCK_FUNCTIONS ); - } - - public Definr( Object spec, Map functionsMap ) { - super( spec, OpMode.DEFINER, functionsMap ); - } - } - - /** - * This variant of modifier only writes when the key/index is missing or the value is null - */ - public static class Defaultr extends Modifier { - - public Defaultr( final Object spec ) { - this( spec, STOCK_FUNCTIONS ); - } - - public Defaultr( Object spec, Map functionsMap ) { - super( spec, OpMode.DEFAULTR, functionsMap ); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java deleted file mode 100644 index e75e0aa8..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityCompositeSpec.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.cardinality; - -import com.bazaarvoice.jolt.common.ComputedKeysComparator; -import com.bazaarvoice.jolt.common.pathelement.AmpPathElement; -import com.bazaarvoice.jolt.common.pathelement.AtPathElement; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarPathElement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * CardinalitySpec that has children, which it builds and then manages during Transforms. - */ -public class CardinalityCompositeSpec extends CardinalitySpec { - - private static final HashMap orderMap; - private static final ComputedKeysComparator computedKeysComparator; - - static { - orderMap = new HashMap<>(); - orderMap.put( AmpPathElement.class, 1 ); - orderMap.put( StarPathElement.class, 2 ); - computedKeysComparator = ComputedKeysComparator.fromOrder(orderMap); - } - // Three different buckets for the children of this CardinalityCompositeSpec - private CardinalityLeafSpec specialChild; // children that aren't actually triggered off the input data - private final Map literalChildren; // children that are simple exact matches against the input data - private final List computedChildren; // children that are regex matches against the input data - - public CardinalityCompositeSpec( String rawKey, Map spec ) { - super( rawKey ); - - Map literals = new HashMap<>(); - ArrayList computed = new ArrayList<>(); - - specialChild = null; - - // self check - if ( pathElement instanceof AtPathElement ) { - throw new SpecException( "@ CardinalityTransform key, can not have children." ); - } - - List children = createChildren( spec ); - - if ( children.isEmpty() ) { - throw new SpecException( "Shift CardinalitySpec format error : CardinalitySpec line with empty {} as value is not valid." ); - } - - for ( CardinalitySpec child : children ) { - literals.put( child.pathElement.getRawKey(), child ); - - if ( child.pathElement instanceof LiteralPathElement ) { - literals.put( child.pathElement.getRawKey(), child ); - } - // special is it is "@" - else if ( child.pathElement instanceof AtPathElement ) { - if ( child instanceof CardinalityLeafSpec ) { - specialChild = (CardinalityLeafSpec) child; - } else { - throw new SpecException( "@ CardinalityTransform key, can not have children." ); - } - } else { // star - computed.add( child ); - } - } - - // Only the computed children need to be sorted - Collections.sort( computed, computedKeysComparator ); - - computed.trimToSize(); - literalChildren = Collections.unmodifiableMap( literals ); - computedChildren = Collections.unmodifiableList( computed ); - } - - - /** - * Recursively walk the spec input tree. - */ - private static List createChildren( Map rawSpec ) { - - List children = new ArrayList<>(); - Set actualKeys = new HashSet<>(); - - for ( String keyString : rawSpec.keySet() ) { - - Object rawRhs = rawSpec.get( keyString ); - - CardinalitySpec childSpec; - if ( rawRhs instanceof Map ) { - childSpec = new CardinalityCompositeSpec( keyString, (Map) rawRhs ); - } else { - childSpec = new CardinalityLeafSpec( keyString, rawRhs ); - } - - String childCanonicalString = childSpec.pathElement.getCanonicalForm(); - - if ( actualKeys.contains( childCanonicalString ) ) { - throw new IllegalArgumentException( "Duplicate canonical CardinalityTransform key found : " + childCanonicalString ); - } - - actualKeys.add( childCanonicalString ); - - children.add( childSpec ); - } - - return children; - } - - /** - * If this Spec matches the inputkey, then perform one step in the parallel treewalk. - *

- * Step one level down the input "tree" by carefully handling the List/Map nature the input to - * get the "one level down" data. - *

- * Step one level down the Spec tree by carefully and efficiently applying our children to the - * "one level down" data. - * - * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it - */ - @Override - public boolean applyCardinality( String inputKey, Object input, WalkedPath walkedPath, Object parentContainer ) { - MatchedElement thisLevel = pathElement.match( inputKey, walkedPath ); - if ( thisLevel == null ) { - return false; - } - - walkedPath.add( input, thisLevel ); - - // The specialChild can change the data object that I point to. - // Aka, my key had a value that was a List, and that gets changed so that my key points to a ONE value - if (specialChild != null) { - input = specialChild.applyToParentContainer( inputKey, input, walkedPath, parentContainer ); - } - - // Handle the rest of the children - process( input, walkedPath ); - - walkedPath.removeLast(); - return true; - } - - @SuppressWarnings( "unchecked" ) - private void process( Object input, WalkedPath walkedPath ) { - - if ( input instanceof Map ) { - - // Iterate over the whole entrySet rather than the keyset with follow on gets of the values - Set> entrySet = new HashSet<>( ( (Map) input ).entrySet() ); - for ( Map.Entry inputEntry : entrySet ) { - applyKeyToLiteralAndComputed( this, inputEntry.getKey(), inputEntry.getValue(), walkedPath, input ); - } - } else if ( input instanceof List ) { - - for ( int index = 0; index < ( (List) input ).size(); index++ ) { - Object subInput = ( (List) input ).get( index ); - String subKeyStr = Integer.toString( index ); - - applyKeyToLiteralAndComputed( this, subKeyStr, subInput, walkedPath, input ); - } - } else if ( input != null ) { - - // if not a map or list, must be a scalar - String scalarInput = input.toString(); - applyKeyToLiteralAndComputed( this, scalarInput, null, walkedPath, scalarInput ); - } - } - - /** - * This method implements the Cardinality matching behavior - * when we have both literal and computed children. - *

- * For each input key, we see if it matches a literal, and it not, try to match the key with every computed child. - */ - private static void applyKeyToLiteralAndComputed( CardinalityCompositeSpec spec, String subKeyStr, Object subInput, WalkedPath walkedPath, Object input ) { - - CardinalitySpec literalChild = spec.literalChildren.get( subKeyStr ); - - // if the subKeyStr found a literalChild, then we do not have to try to match any of the computed ones - if ( literalChild != null ) { - literalChild.applyCardinality( subKeyStr, subInput, walkedPath, input ); - } else { - // If no literal spec key matched, iterate through all the computedChildren - - // Iterate through all the computedChildren until we find a match - // This relies upon the computedChildren having already been sorted in priority order - for ( CardinalitySpec computedChild : spec.computedChildren ) { - // if the computed key does not match it will quickly return false - if ( computedChild.applyCardinality( subKeyStr, subInput, walkedPath, input ) ) { - break; - } - } - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java deleted file mode 100644 index 4577c664..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalityLeafSpec.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.cardinality; - -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -/** - * Leaf level CardinalitySpec object. - *

- * If this CardinalitySpec's PathElement matches the input (successful parallel tree walk) - * this CardinalitySpec has the information needed to write the given data to the output object. - */ -public class CardinalityLeafSpec extends CardinalitySpec { - - public enum CardinalityRelationship { - ONE, - MANY - } - - private CardinalityRelationship cardinalityRelationship; - - public CardinalityLeafSpec( String rawKey, Object rhs ) { - super( rawKey ); - - try { - cardinalityRelationship = CardinalityRelationship.valueOf( rhs.toString() ); - } - catch( Exception e ) { - throw new SpecException( "Invalid Cardinality type :" + rhs.toString(), e ); - } - } - - /** - * If this CardinalitySpec matches the inputkey, then do the work of modifying the data and return true. - * - * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it - */ - @Override - public boolean applyCardinality( String inputKey, Object input, WalkedPath walkedPath, Object parentContainer ) { - - MatchedElement thisLevel = getMatch( inputKey, walkedPath ); - if ( thisLevel == null ) { - return false; - } - performCardinalityAdjustment( inputKey, input, walkedPath, (Map) parentContainer, thisLevel ); - return true; - } - - /** - * This should only be used by composite specs with an '@' child - * - * @return null if no work was done, otherwise returns the re-parented data - */ - public Object applyToParentContainer ( String inputKey, Object input, WalkedPath walkedPath, Object parentContainer ) { - - MatchedElement thisLevel = getMatch( inputKey, walkedPath ); - if ( thisLevel == null ) { - return null; - } - return performCardinalityAdjustment( inputKey, input, walkedPath, (Map) parentContainer, thisLevel ); - } - - /** - * - * @return null if no work was done, otherwise returns the re-parented data - */ - private Object performCardinalityAdjustment( String inputKey, Object input, WalkedPath walkedPath, Map parentContainer, MatchedElement thisLevel ) { - - // Add our the LiteralPathElement for this level, so that write path References can use it as &(0,0) - walkedPath.add( input, thisLevel ); - - Object returnValue = null; - if ( cardinalityRelationship == CardinalityRelationship.MANY ) { - if ( input instanceof List ) { - returnValue = input; - } - else if ( input instanceof Object[] ) { - returnValue = Arrays.asList(((Object[]) input)); - } - else if ( input instanceof Map || input instanceof String || input instanceof Number || input instanceof Boolean ) { - Object one = parentContainer.remove( inputKey ); - List tempList = new ArrayList<>(); - tempList.add( one ); - returnValue = tempList; - - } - else if ( input == null ) { - returnValue = Collections.emptyList(); - } - parentContainer.put( inputKey, returnValue ); - } - else if ( cardinalityRelationship == CardinalityRelationship.ONE ) { - if ( input instanceof List ) { - if (!( (List) input ).isEmpty()) { - returnValue = ( (List) input ).get( 0 ); - } - parentContainer.put( inputKey, returnValue ); - } else if ( input instanceof Object[] ) { - returnValue = ((Object[]) input)[0]; - parentContainer.put(inputKey, returnValue); - } - } - - walkedPath.removeLast(); - - return returnValue; - } - - private MatchedElement getMatch( String inputKey, WalkedPath walkedPath ) { - return pathElement.match( inputKey, walkedPath ); - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalitySpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalitySpec.java deleted file mode 100644 index de597501..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/cardinality/CardinalitySpec.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.cardinality; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.pathelement.AtPathElement; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.MatchablePathElement; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarRegexPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarSinglePathElement; -import com.bazaarvoice.jolt.common.spec.BaseSpec; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -/** - * A Spec Object represents a single line from the JSON Cardinality Spec. - * - * At a minimum a single Spec has : - * Raw LHS spec value - * Some kind of PathElement (based off that raw LHS value) - * - * Additionally there are 2 distinct subclasses of the base Spec - * CardinalityLeafSpec : where the RHS is either "ONE" or "MANY" - * CardinalityCompositeSpec : where the RHS is a map of children Specs - * - * The tree structure of formed by the CompositeSpecs is what is used during the transform - * to do the parallel tree walk with the input data tree. - * - * During the parallel tree walk, a Path is maintained, and used when - * a tree walk encounters a leaf spec. - */ -public abstract class CardinalitySpec implements BaseSpec { - - private static final String STAR = "*"; - private static final String AT = "@"; - - // The processed key from the JSON config - protected final MatchablePathElement pathElement; - - public CardinalitySpec( String rawJsonKey ) { - List pathElements = parse( rawJsonKey ); - - if ( pathElements.size() != 1 ){ - throw new SpecException( "CardinalityTransform invalid LHS:" + rawJsonKey + " can not contain '.'" ); - } - - PathElement pe = pathElements.get( 0 ); - if ( ! ( pe instanceof MatchablePathElement ) ) { - throw new SpecException( "Spec LHS key=" + rawJsonKey + " is not a valid LHS key." ); - } - - this.pathElement = (MatchablePathElement) pe; - } - - // once all the cardinalitytransform specific logic is extracted. - public static List parse( String key ) { - - if ( key.contains(AT) ) { - return Arrays.asList( new AtPathElement( key ) ); - } - else if ( STAR.equals(key) ) { - return Arrays.asList( new StarAllPathElement( key ) ); - } - else if ( key.contains(STAR) ) { - if ( StringTools.countMatches(key, STAR) == 1 ) { - return Arrays.asList( new StarSinglePathElement( key ) ); - } - else { - return Arrays.asList( new StarRegexPathElement( key ) ); - } - } - else { - return Arrays.asList( new LiteralPathElement( key ) ); - } - } - - /** - * This is the main recursive method of the CardinalityTransform parallel "spec" and "input" tree walk. - * - * It should return true if this Spec object was able to successfully apply itself given the - * inputKey and input object. - * - * In the context of the CardinalityTransform parallel treewalk, if this method returns a non-null Object, - * the assumption is that no other sibling Cardinality specs need to look at this particular input key. - * - * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it - */ - public abstract boolean applyCardinality( String inputKey, Object input, WalkedPath walkedPath, Object parentContainer ); - - @Override - public boolean apply( final String inputKey, final Optional inputOptional, final WalkedPath walkedPath, final Map output, final Map context ) { - return applyCardinality( inputKey, inputOptional.get(), walkedPath, output ); - } - - @Override - public MatchablePathElement getPathElement() { - return pathElement; - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/spec/ChainrEntry.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/spec/ChainrEntry.java deleted file mode 100644 index ab2df7c4..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/spec/ChainrEntry.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.chainr.spec; - -import com.bazaarvoice.jolt.CardinalityTransform; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.Defaultr; -import com.bazaarvoice.jolt.JoltTransform; -import com.bazaarvoice.jolt.Modifier; -import com.bazaarvoice.jolt.Removr; -import com.bazaarvoice.jolt.Shiftr; -import com.bazaarvoice.jolt.Sortr; -import com.bazaarvoice.jolt.SpecDriven; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Helper class that encapsulates the information one of the individual transform entries in - * the Chainr spec's list. - */ -public class ChainrEntry { - - /** - * Map transform "operation" names to the classes that handle them - */ - public static final Map STOCK_TRANSFORMS; - - /** - * getName() returns fqdn$path compared to humanReadablePath from getCanonicalPath() - * to make internal classes available/loadable at runtime it is imperative that we use fqdn - */ - static { - HashMap temp = new HashMap<>(); - temp.put( "shift", Shiftr.class.getName() ); - temp.put( "default", Defaultr.class.getName() ); - temp.put( "modify-overwrite-beta", Modifier.Overwritr.class.getName() ); - temp.put( "modify-default-beta", Modifier.Defaultr.class.getName() ); - temp.put( "modify-define-beta", Modifier.Definr.class.getName() ); - temp.put( "remove", Removr.class.getName() ); - temp.put( "sort", Sortr.class.getName() ); - temp.put( "cardinality", CardinalityTransform.class.getName() ); - STOCK_TRANSFORMS = Collections.unmodifiableMap( temp ); - } - - public static final String OPERATION_KEY = "operation"; - public static final String SPEC_KEY = "spec"; - - private final int index; - private final Object spec; - private final String operationClassName; - - private final Class joltTransformClass; - private final boolean isSpecDriven; - - /** - * Process an element from the Chainr Spec into a ChainrEntry class. - * This method tries to validate the syntax of the Chainr spec, whereas - * the ChainrInstantiator deals with loading the Transform classes. - * - * @param chainrEntryObj the unknown Object from the Chainr list - * @param index the index of the chainrEntryObj, used in reporting errors - */ - public ChainrEntry( int index, Object chainrEntryObj, ClassLoader classLoader ) { - - if ( ! (chainrEntryObj instanceof Map ) ) { - throw new SpecException( "JOLT ChainrEntry expects a JSON map - Malformed spec" + getErrorMessageIndexSuffix() ); - } - - @SuppressWarnings( "unchecked" ) // We know it is a Map due to the check above - Map chainrEntryMap = (Map) chainrEntryObj; - - this.index = index; - - String opString = extractOperationString( chainrEntryMap ); - - if ( opString == null ) { - throw new SpecException( "JOLT Chainr 'operation' must implement Transform or ContextualTransform" + getErrorMessageIndexSuffix() ); - } - - if ( STOCK_TRANSFORMS.containsKey( opString ) ) { - operationClassName = STOCK_TRANSFORMS.get( opString ); - } - else { - operationClassName = opString; - } - - joltTransformClass = loadJoltTransformClass( classLoader ); - - spec = chainrEntryMap.get( ChainrEntry.SPEC_KEY ); - - isSpecDriven = SpecDriven.class.isAssignableFrom( joltTransformClass ); - if ( isSpecDriven && ! chainrEntryMap.containsKey( SPEC_KEY ) ) { - throw new SpecException( "JOLT Chainr - Transform className:" + joltTransformClass.getName() + " requires a spec" + getErrorMessageIndexSuffix() ); - } - } - - private String extractOperationString( Map chainrEntryMap ) { - - Object operationNameObj = chainrEntryMap.get( ChainrEntry.OPERATION_KEY ); - if ( operationNameObj == null ) { - return null; - } - else if ( operationNameObj instanceof String) { - if ( StringTools.isBlank((String) operationNameObj) ) { - throw new SpecException( "JOLT Chainr '" + ChainrEntry.OPERATION_KEY + "' should not be blank" + getErrorMessageIndexSuffix() ); - } - return (String) operationNameObj; - } - else { - throw new SpecException( "JOLT Chainr needs a '" + ChainrEntry.OPERATION_KEY + "' of type String" + getErrorMessageIndexSuffix() ); - } - } - - private Class loadJoltTransformClass(ClassLoader classLoader) { - - try { - Class opClass = classLoader.loadClass( operationClassName ); - - if ( Chainr.class.isAssignableFrom( opClass ) ) { - throw new SpecException( "Attempt to nest Chainr inside itself" + getErrorMessageIndexSuffix() ); - } - - if ( ! JoltTransform.class.isAssignableFrom( opClass ) ) - { - throw new SpecException( "JOLT Chainr class:" + operationClassName + " does not implement the JoltTransform interface" + getErrorMessageIndexSuffix() ); - } - - @SuppressWarnings( "unchecked" ) // We know it is some type of Transform due to the check above - Class transformClass = (Class) opClass; - - return transformClass; - - } catch ( ClassNotFoundException e ) { - throw new SpecException( "JOLT Chainr could not find transform class:" + operationClassName + getErrorMessageIndexSuffix(), e ); - } - } - - - /** - * Generate an error message suffix what lists the index of the ChainrEntry in the overall ChainrSpec. - */ - public String getErrorMessageIndexSuffix() { - return " at index:" + index + "."; - } - - /** - * @return Spec for the transform, can be null - */ - public Object getSpec() { - return spec; - } - - /** - * @return Class instance specified by this ChainrEntry - */ - public Class getJoltTransformClass() { - return joltTransformClass; - } - - /** - * @return true if the Jolt Transform specified by this ChainrEntry implements the SpecTransform interface - */ - public boolean isSpecDriven() { - return isSpecDriven; - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/DeepCopy.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/DeepCopy.java deleted file mode 100644 index 0f447b1b..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/DeepCopy.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; - -public class DeepCopy { - - /** - * Simple deep copy, that leverages Java Serialization. - * Supplied object is serialized to an in memory buffer (byte array), - * and then a new object is reconstituted from that byte array. - * - * This is meant for copying small objects or object graphs, and will - * probably do nasty things if asked to copy a large graph. - * - * @param object object to deep copy - * @return deep copy of the object - */ - public static Object simpleDeepCopy( Object object ) { - - try { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(bos); - oos.writeObject(object); - oos.flush(); - oos.close(); - bos.close(); - - byte [] byteData = bos.toByteArray(); - ByteArrayInputStream bais = new ByteArrayInputStream(byteData); - - return new ObjectInputStream(bais).readObject(); - } - catch ( IOException ioe ) { - throw new RuntimeException( "DeepCopy IOException", ioe ); - } - catch ( ClassNotFoundException cnf ) { - throw new RuntimeException( "DeepCopy ClassNotFoundException", cnf ); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/ExecutionStrategy.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/ExecutionStrategy.java deleted file mode 100644 index 29594336..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/ExecutionStrategy.java +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common; - -import com.bazaarvoice.jolt.common.spec.BaseSpec; -import com.bazaarvoice.jolt.common.spec.OrderedCompositeSpec; -import com.bazaarvoice.jolt.common.tree.WalkedPath; - -import java.util.List; -import java.util.Map; - -public enum ExecutionStrategy { - - /** - * The performance assumption built into this code is that the literal values in the spec, are generally smaller - * than the number of potential keys to check in the input. - * - * More specifically, the assumption here is that the set of literalChildren is smaller than the input "keyset". - */ - AVAILABLE_LITERALS { - @Override - void processMap( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ) { - - for( String key : spec.getLiteralChildren().keySet() ) { - - // Do not work if the value is missing in the input map - if ( inputMap.containsKey( key ) ) { - - Optional subInputOptional = Optional.of( inputMap.get( key ) ); - spec.getLiteralChildren().get( key ).apply( key, subInputOptional, walkedPath, output, context ); - } - } - } - - @Override - void processList( OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context ) { - - Integer originalSize = walkedPath.lastElement().getOrigSize().get(); - for( String key : spec.getLiteralChildren().keySet() ) { - - int keyInt = Integer.MAX_VALUE; - - try { - keyInt = Integer.parseInt( key ); - } - catch( NumberFormatException nfe ) { - // If the data is an Array, but the spec keys are Non-Integer Strings, - // we are annoyed, but we don't stop the whole transform. - // Just this part of the Transform won't work. - } - - // Do not work if the index is outside of the input list - if ( keyInt < inputList.size() ) { - - Object subInput = inputList.get( keyInt ); - Optional subInputOptional; - if ( subInput == null && originalSize != null && keyInt >= originalSize ) { - subInputOptional = Optional.empty(); - } - else { - subInputOptional = Optional.of( subInput ); - } - - // we know the .get(key) will not return null, because we are iterating over its keys - spec.getLiteralChildren().get( key ).apply( key, subInputOptional, walkedPath, output, context ); - } - } - } - - @Override - void processScalar( OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context ) { - - BaseSpec literalChild = spec.getLiteralChildren().get( scalarInput ); - if ( literalChild != null ) { - literalChild.apply( scalarInput, Optional.empty(), walkedPath, output, context ); - } - } - }, - - /** - * This is identical to AVAILABLE_LITERALS, except for the fact that it does not skip keys if its missing in the input, like literal does - * Given this works like defaultr, a missing key is our point of entry to insert a default value, either from a passed context or a - * hardcoded value. - */ - ALL_LITERALS { - - @Override - void processMap( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ) { - - for( String key : spec.getLiteralChildren().keySet() ) { - - // if the input in not available in the map us null or else get value, - // then lookup and place a defined value from spec there - Optional subInputOptional = Optional.empty(); - if ( inputMap.containsKey( key ) ) { - subInputOptional = Optional.of( inputMap.get( key )); - } - spec.getLiteralChildren().get( key ).apply( key, subInputOptional, walkedPath, output, context ); - } - } - - @Override - void processList( OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context ) { - - Integer originalSize = walkedPath.lastElement().getOrigSize().get(); - for( String key : spec.getLiteralChildren().keySet() ) { - - int keyInt = Integer.MAX_VALUE; - - try { - keyInt = Integer.parseInt( key ); - } - catch( NumberFormatException nfe ) { - // If the data is an Array, but the spec keys are Non-Integer Strings, - // we are annoyed, but we don't stop the whole transform. - // Just this part of the Transform won't work. - } - - // if the input in not available in the list use null or else get value, - // then lookup and place a default value as defined in spec there - Optional subInputOptional = Optional.empty(); - if ( keyInt < inputList.size() ) { - Object subInput = inputList.get( keyInt ); - if ( subInput != null || originalSize == null || keyInt < originalSize ) { - subInputOptional = Optional.of( subInput ); - } - } - // we know the .get(key) will not return null, because we are iterating over its keys - spec.getLiteralChildren().get( key ).apply( key, subInputOptional, walkedPath, output, context ); - } - } - - @Override - void processScalar( OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context ) { - - AVAILABLE_LITERALS.processScalar( spec, scalarInput, walkedPath, output, context ); - } - }, - - /** - * If the CompositeSpec only has computed children, we can avoid checking the getLiteralChildren() altogether, and - * we can do a slightly better iteration (HashSet.entrySet) across the input. - */ - COMPUTED { - @Override - void processMap( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ) { - - // Iterate over the whole entrySet rather than the keyset with follow on gets of the values - for( Map.Entry inputEntry : inputMap.entrySet() ) { - applyKeyToComputed( spec.getComputedChildren(), walkedPath, output, inputEntry.getKey(), Optional.of( inputEntry.getValue() ), context ); - } - } - - @Override - void processList( OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context ) { - - Integer originalSize = walkedPath.lastElement().getOrigSize().get(); - for (int index = 0; index < inputList.size(); index++) { - Object subInput = inputList.get( index ); - String subKeyStr = Integer.toString( index ); - Optional subInputOptional; - if ( subInput == null && originalSize != null && index >= originalSize ) { - subInputOptional = Optional.empty(); - } - else { - subInputOptional = Optional.of( subInput ); - } - - applyKeyToComputed( spec.getComputedChildren(), walkedPath, output, subKeyStr, subInputOptional, context ); - } - } - - @Override - void processScalar( OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context ) { - applyKeyToComputed( spec.getComputedChildren(), walkedPath, output, scalarInput, Optional.empty(), context ); - } - }, - - /** - * In order to implement the key precedence order, we have to process each input "key", first to - * see if it matches any literals, and if it does not, check against each of the computed - */ - CONFLICT { - @Override - void processMap( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ) { - - // Iterate over the whole entrySet rather than the keyset with follow on gets of the values - for( Map.Entry inputEntry : inputMap.entrySet() ) { - applyKeyToLiteralAndComputed( spec, inputEntry.getKey(), Optional.of( inputEntry.getValue() ), walkedPath, output, context ); - } - } - - @Override - void processList( OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context ) { - - Integer originalSize = walkedPath.lastElement().getOrigSize().get(); - for (int index = 0; index < inputList.size(); index++) { - Object subInput = inputList.get( index ); - String subKeyStr = Integer.toString( index ); - Optional subInputOptional; - if ( subInput == null && originalSize != null && index >= originalSize ) { - subInputOptional = Optional.empty(); - } - else { - subInputOptional = Optional.of( subInput ); - } - - applyKeyToLiteralAndComputed( spec, subKeyStr, subInputOptional, walkedPath, output, context ); - } - } - - @Override - void processScalar( OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context ) { - applyKeyToLiteralAndComputed( spec, scalarInput, Optional.empty(), walkedPath, output, context ); - } - }, - - /** - * We have both literal and computed children, but we have determined that there is no way an input key - * could match one of our literal and computed children. Hence we can safely run each one. - */ - AVAILABLE_LITERALS_WITH_COMPUTED { - - @Override - void processMap( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ) { - AVAILABLE_LITERALS.processMap( spec, inputMap, walkedPath, output, context ); - COMPUTED.processMap( spec, inputMap, walkedPath, output, context ); - } - - @Override - void processList( OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context ) { - AVAILABLE_LITERALS.processList( spec, inputList, walkedPath, output, context ); - COMPUTED.processList( spec, inputList, walkedPath, output, context ); - } - - @Override - void processScalar( OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context ) { - AVAILABLE_LITERALS.processScalar( spec, scalarInput, walkedPath, output, context ); - COMPUTED.processScalar( spec, scalarInput, walkedPath, output, context ); - } - }, - - ALL_LITERALS_WITH_COMPUTED { - @Override - void processMap( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ) { - ALL_LITERALS.processMap( spec, inputMap, walkedPath, output, context ); - COMPUTED.processMap( spec, inputMap, walkedPath, output, context ); - } - - @Override - void processList( OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context ) { - ALL_LITERALS.processList( spec, inputList, walkedPath, output, context ); - COMPUTED.processList( spec, inputList, walkedPath, output, context ); - } - - @Override - void processScalar( OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context ) { - ALL_LITERALS.processScalar( spec, scalarInput, walkedPath, output, context ); - COMPUTED.processScalar( spec, scalarInput, walkedPath, output, context ); - } - }; - - @SuppressWarnings( "unchecked" ) - public void process( OrderedCompositeSpec spec, Optional inputOptional, WalkedPath walkedPath, Map output, Map context ) { - Object input = inputOptional.get(); - if ( input instanceof Map) { - processMap( spec, (Map) input, walkedPath, output, context ); - } - else if ( input instanceof List ) { - processList( spec, (List) input, walkedPath, output, context ); - } - else if ( input != null ) { - // if not a map or list, must be a scalar - processScalar( spec, input.toString(), walkedPath, output, context ); - } - } - - abstract void processMap ( OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context ); - abstract void processList ( OrderedCompositeSpec spec, List inputList , WalkedPath walkedPath, Map output, Map context ); - abstract void processScalar( OrderedCompositeSpec spec, String scalarInput , WalkedPath walkedPath, Map output, Map context ); - - - /** - * This is the method we are trying to avoid calling. It implements the matching behavior - * when we have both literal and computed children. - * - * For each input key, we see if it matches a literal, and it not, try to match the key with every computed child. - * - * Worse case : n + n * c, where - * n is number of input keys - * c is number of computed children - */ - private static void applyKeyToLiteralAndComputed( T spec, String subKeyStr, Optional subInputOptional, WalkedPath walkedPath, Map output, Map context ) { - - BaseSpec literalChild = spec.getLiteralChildren().get( subKeyStr ); - - // if the subKeyStr found a literalChild, then we do not have to try to match any of the computed ones - if ( literalChild != null ) { - literalChild.apply( subKeyStr, subInputOptional, walkedPath, output, context ); - } - else { - // If no literal spec key matched, iterate through all the getComputedChildren() - applyKeyToComputed( spec.getComputedChildren(), walkedPath, output, subKeyStr, subInputOptional, context ); - } - } - - private static void applyKeyToComputed( List computedChildren, WalkedPath walkedPath, Map output, String subKeyStr, Optional subInputOptional, Map context ) { - - // Iterate through all the getComputedChildren() until we find a match - // This relies upon the getComputedChildren() having already been sorted in priority order - for ( BaseSpec computedChild : computedChildren ) { - // if the computed key does not match it will quickly return false - if ( computedChild.apply( subKeyStr, subInputOptional, walkedPath, output, context ) ) { - break; - } - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathElementBuilder.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathElementBuilder.java deleted file mode 100644 index ae046d6b..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathElementBuilder.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common; - -import com.bazaarvoice.jolt.common.pathelement.AmpPathElement; -import com.bazaarvoice.jolt.common.pathelement.ArrayPathElement; -import com.bazaarvoice.jolt.common.pathelement.AtPathElement; -import com.bazaarvoice.jolt.common.pathelement.DollarPathElement; -import com.bazaarvoice.jolt.common.pathelement.HashPathElement; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.MatchablePathElement; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarDoublePathElement; -import com.bazaarvoice.jolt.common.pathelement.StarRegexPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarSinglePathElement; -import com.bazaarvoice.jolt.common.pathelement.TransposePathElement; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; - -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import static com.bazaarvoice.jolt.common.SpecStringParser.fixLeadingBracketSugar; -import static com.bazaarvoice.jolt.common.SpecStringParser.parseDotNotation; -import static com.bazaarvoice.jolt.common.SpecStringParser.removeEscapeChars; -import static com.bazaarvoice.jolt.common.SpecStringParser.removeEscapedValues; -import static com.bazaarvoice.jolt.common.SpecStringParser.stringIterator; - -/** - * Static utility class that creates PathElement(s) given a string key from a json spec document - */ -public class PathElementBuilder { - - private PathElementBuilder() {} - - /** - * Create a path element and ensures it is a Matchable Path Element - */ - public static MatchablePathElement buildMatchablePathElement(String rawJsonKey) { - PathElement pe = PathElementBuilder.parseSingleKeyLHS( rawJsonKey ); - - if ( ! ( pe instanceof MatchablePathElement ) ) { - throw new SpecException( "Spec LHS key=" + rawJsonKey + " is not a valid LHS key." ); - } - - return (MatchablePathElement) pe; - } - - /** - * Visible for Testing. - * - * Inspects the key in a particular order to determine the correct sublass of - * PathElement to create. - * - * @param origKey String that should represent a single PathElement - * @return a concrete implementation of PathElement - */ - public static PathElement parseSingleKeyLHS( String origKey ) { - - String elementKey; // the String to use to actually make Elements - String keyToInspect; // the String to use to determine which kind of Element to create - - if ( origKey.contains( "\\" ) ) { - // only do the extra work of processing for escaped chars, if there is one. - keyToInspect = removeEscapedValues( origKey ); - elementKey = removeEscapeChars( origKey ); - } - else { - keyToInspect = origKey; - elementKey = origKey; - } - - //// LHS single values - if ( "@".equals( keyToInspect ) ) { - return new AtPathElement( elementKey ); - } - else if ( "*".equals( keyToInspect ) ) { - return new StarAllPathElement( elementKey ); - } - else if ( keyToInspect.startsWith( "[" ) ) { - - if ( StringTools.countMatches( keyToInspect, "[" ) != 1 || StringTools.countMatches(keyToInspect, "]") != 1 ) { - throw new SpecException( "Invalid key:" + origKey + " has too many [] references."); - } - - return new ArrayPathElement( elementKey ); - } - //// LHS multiple values - else if ( keyToInspect.startsWith("@") || keyToInspect.contains( "@(" ) ) { - // The traspose path element gets the origKey so that it has it's escapes. - return TransposePathElement.parse( origKey ); - } - else if ( keyToInspect.contains( "@" ) ) { - throw new SpecException( "Invalid key:" + origKey + " can not have an @ other than at the front." ); - } - else if ( keyToInspect.contains("$") ) { - return new DollarPathElement( elementKey ); - } - else if ( keyToInspect.contains("[") ) { - - if ( StringTools.countMatches(keyToInspect, "[") != 1 || StringTools.countMatches(keyToInspect, "]") != 1 ) { - throw new SpecException( "Invalid key:" + origKey + " has too many [] references."); - } - - return new ArrayPathElement( elementKey ); - } - else if ( keyToInspect.contains( "&" ) ) { - - if ( keyToInspect.contains("*") ) - { - throw new SpecException( "Invalid key:" + origKey + ", Can't mix * with & ) "); - } - return new AmpPathElement( elementKey ); - } - else if ( keyToInspect.contains("*" ) ) { - - int numOfStars = StringTools.countMatches(keyToInspect, "*"); - - if(numOfStars == 1){ - return new StarSinglePathElement( elementKey ); - } - else if(numOfStars == 2){ - return new StarDoublePathElement( elementKey ); - } - else { - return new StarRegexPathElement( elementKey ); - } - } - else if ( keyToInspect.contains("#" ) ) { - return new HashPathElement( elementKey ); - } - else { - return new LiteralPathElement( elementKey ); - } - } - - /** - * Parse the dotNotation of the RHS. - */ - public static List parseDotNotationRHS( String dotNotation ) { - String fixedNotation = fixLeadingBracketSugar( dotNotation ); - List pathStrs = parseDotNotation( new LinkedList(), stringIterator( fixedNotation ), dotNotation ); - - return parseList( pathStrs, dotNotation ); - } - - /** - * @param refDotNotation the original dotNotation string used for error messages - * @return List of PathElements based on the provided List keys - */ - public static List parseList( List keys, String refDotNotation ) { - ArrayList paths = new ArrayList<>(); - - for( String key: keys ) { - PathElement path = parseSingleKeyLHS( key ); - if ( path instanceof AtPathElement ) { - throw new SpecException( "'.@.' is not valid on the RHS: " + refDotNotation ); - } - paths.add( path ); - } - - return paths; - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathEvaluatingTraversal.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathEvaluatingTraversal.java deleted file mode 100644 index 7e9e16c0..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/PathEvaluatingTraversal.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.common; - -import com.bazaarvoice.jolt.common.pathelement.EvaluatablePathElement; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.traversr.Traversr; -import com.bazaarvoice.jolt.utils.StringTools; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static com.bazaarvoice.jolt.common.PathElementBuilder.parseDotNotationRHS; - -/** - * Combines a Traversr with the ability to evaluate References against a WalkedPath. - * - * Convenience class for path based off a single dot notation String, - * like "rating.&1(2).&.value". - * - * This processes the dot notation path into internal data structures, so - * that the String processing only happens once. - */ -public abstract class PathEvaluatingTraversal { - - private final List elements; - private final Traversr traversr; - - public PathEvaluatingTraversal( String dotNotation ) { - - if ( ( dotNotation.contains("*") && ! dotNotation.contains( "\\*" ) ) || - ( dotNotation.contains("$") && ! dotNotation.contains( "\\$" ) ) ) { - throw new SpecException("DotNotation (write key) can not contain '*' or '$' : write key: " + dotNotation ); - } - - List paths; - Traversr trav; - - if ( StringTools.isNotBlank( dotNotation ) ) { - - // Compute the path elements. - paths = parseDotNotationRHS( dotNotation ); - - // Use the canonical versions of the path elements to create the Traversr - List traversrPaths = new ArrayList<>( paths.size() ); - for ( PathElement pe : paths ) { - traversrPaths.add( pe.getCanonicalForm() ); - } - trav = createTraversr( traversrPaths ); - } - else { - paths = Collections.emptyList(); - trav = createTraversr( Arrays.asList( "" ) ); - } - - List evalPaths = new ArrayList<>( paths.size() ); - for( PathElement pe : paths ) { - if ( ! ( pe instanceof EvaluatablePathElement ) ) { - throw new SpecException( "RHS key=" + pe.getRawKey() + " is not a valid RHS key." ); - } - - evalPaths.add( (EvaluatablePathElement) pe ); - } - - this.elements = Collections.unmodifiableList( evalPaths ); - this.traversr = trav; - } - - protected abstract Traversr createTraversr(List paths); - - /** - * Use the supplied WalkedPath, in the evaluation of each of our PathElements to - * build a concrete output path. Then use that output path to write the given - * data to the output. - * - * @param data data to write - * @param output data structure we are going to write the data to - * @param walkedPath reference used to lookup reference values like "&1(2)" - */ - public void write( Object data, Map output, WalkedPath walkedPath ) { - List evaledPaths = evaluate( walkedPath ); - if ( evaledPaths != null ) { - traversr.set( output, evaledPaths, data ); - } - } - - public Optional read( Object data, WalkedPath walkedPath ) { - List evaledPaths = evaluate( walkedPath ); - if ( evaledPaths == null ) { - return Optional.empty(); - } - - return traversr.get( data, evaledPaths ); - } - - /** - * Use the supplied WalkedPath, in the evaluation of each of our PathElements. - * - * If our PathElements contained a TransposePathElement, we may return null. - * - * @param walkedPath used to lookup/evaluate PathElement references values like "&1(2)" - * @return null or fully evaluated Strings, possibly with concrete array references like "photos.[3]" - */ - // Visible for testing - public List evaluate( WalkedPath walkedPath ) { - - List strings = new ArrayList<>( elements.size() ); - for ( EvaluatablePathElement pathElement : elements ) { - - String evaledLeafOutput = pathElement.evaluate( walkedPath ); - if ( evaledLeafOutput == null ) { - // If this output path contains a TransposePathElement, and when evaluated, - // return null, then bail - return null; - } - strings.add( evaledLeafOutput ); - } - - return strings; - } - - public int size() { - return elements.size(); - } - - public PathElement get( int index ) { - return elements.get( index ); - } - - /** - * Testing method. - */ - public String getCanonicalForm() { - StringBuilder buf = new StringBuilder(); - - for ( PathElement pe : elements ) { - buf.append( "." ).append( pe.getCanonicalForm() ); - } - - return buf.substring( 1 ); // strip the leading "." - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java deleted file mode 100644 index e2148802..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/ArrayPathElement.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.reference.AmpReference; -import com.bazaarvoice.jolt.common.reference.HashReference; -import com.bazaarvoice.jolt.common.reference.PathAndGroupReference; -import com.bazaarvoice.jolt.common.reference.PathReference; -import com.bazaarvoice.jolt.common.tree.ArrayMatchedElement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; - -public class ArrayPathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { - - public enum ArrayPathType { AUTO_EXPAND, REFERENCE, HASH, TRANSPOSE, EXPLICIT_INDEX } - - private final ArrayPathType arrayPathType; - private final PathReference ref; - private final TransposePathElement transposePathElement; - - private final String canonicalForm; - private final String arrayIndex; - - public ArrayPathElement( String key ) { - super(key); - - if ( key.charAt( 0 ) != '[' || key.charAt( key.length() - 1 ) != ']') { - throw new SpecException( "Invalid ArrayPathElement key:" + key ); - } - - ArrayPathType apt; - PathReference r = null; - TransposePathElement tpe = null; - String aI = ""; - - if ( key.length() == 2 ) { - apt = ArrayPathType.AUTO_EXPAND; - canonicalForm = "[]"; - } - else { - String meat = key.substring( 1, key.length() - 1 ); // trim the [ ] - char firstChar = meat.charAt( 0 ); - - if ( AmpReference.TOKEN.equals( firstChar ) ) { - r = new AmpReference( meat ); - apt = ArrayPathType.REFERENCE; - canonicalForm = "[" + r.getCanonicalForm() + "]"; - } - else if ( HashReference.TOKEN.equals( firstChar ) ) { - r = new HashReference( meat ); - apt = ArrayPathType.HASH; - - canonicalForm = "[" + r.getCanonicalForm() + "]"; - } - else if( '@' == firstChar ) { - apt = ArrayPathType.TRANSPOSE; - - tpe = TransposePathElement.parse( meat ); - canonicalForm = "[" + tpe.getCanonicalForm() + "]"; - } - else { - aI = verifyStringIsNonNegativeInteger(meat); - if ( aI != null ) { - apt = ArrayPathType.EXPLICIT_INDEX; - canonicalForm = "[" + aI + "]"; - } - else { - throw new SpecException( "Bad explict array index:" + meat + " from key:" + key ); - } - } - } - - transposePathElement = tpe; - arrayPathType = apt; - ref = r; - arrayIndex = aI; - } - - - @Override - public String getCanonicalForm() { - return canonicalForm; - } - - @Override - public String evaluate( WalkedPath walkedPath ) { - - switch ( arrayPathType ) { - case AUTO_EXPAND: - return canonicalForm; - - case EXPLICIT_INDEX: - return arrayIndex; - - case HASH: - MatchedElement element = walkedPath.elementFromEnd( ref.getPathIndex() ).getMatchedElement(); - Integer index = element.getHashCount(); - return index.toString(); - - case TRANSPOSE: - String key = transposePathElement.evaluate( walkedPath ); - return verifyStringIsNonNegativeInteger( key ); - - case REFERENCE: - MatchedElement lpe = walkedPath.elementFromEnd( ref.getPathIndex() ).getMatchedElement(); - String keyPart; - - if ( ref instanceof PathAndGroupReference ) { - keyPart = lpe.getSubKeyRef( ( (PathAndGroupReference) ref).getKeyGroup() ); - } - else { - keyPart = lpe.getSubKeyRef( 0 ); - } - - return verifyStringIsNonNegativeInteger( keyPart ); - default: - throw new IllegalStateException( "ArrayPathType enum added two without updating this switch statement." ); - } - } - - /** - * @return the String version of a non-Negative integer, else null - */ - private static String verifyStringIsNonNegativeInteger( String key ) { - try - { - int number = Integer.parseInt( key ); - if ( number >= 0 ) { - return key; - } - else { - return null; - } - } - catch ( NumberFormatException nfe ) { - // Jolt should not throw any exceptions just because the input data does not match what is expected. - // Thus the exception is being swallowed. - return null; - } - } - - public Integer getExplicitArrayIndex() { - try { - return Integer.parseInt( arrayIndex ); - } - catch ( Exception ignored ) { - return null; - } - } - - public boolean isExplicitArrayIndex() { - return arrayPathType.equals( ArrayPathType.EXPLICIT_INDEX ); - } - - @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { - String evaled = evaluate( walkedPath ); - if ( evaled.equals( dataKey ) ) { - Optional origSizeOptional = walkedPath.lastElement().getOrigSize(); - if(origSizeOptional.isPresent()) { - return new ArrayMatchedElement( evaled, origSizeOptional.get()); - } - else { - return null; - } - } - return null; - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java deleted file mode 100644 index 72fb4438..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/DollarPathElement.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.reference.DollarReference; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; - -public class DollarPathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { - - private final DollarReference dRef; - - public DollarPathElement( String key ) { - super(key); - - dRef = new DollarReference( key ); - } - - @Override - public String getCanonicalForm() { - return dRef.getCanonicalForm(); - } - - @Override - public String evaluate( WalkedPath walkedPath ) { - MatchedElement pe = walkedPath.elementFromEnd( dRef.getPathIndex() ).getMatchedElement(); - return pe.getSubKeyRef( dRef.getKeyGroup() ); - } - - @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { - String evaled = evaluate( walkedPath ); - return new MatchedElement( evaled ); - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/HashPathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/HashPathElement.java deleted file mode 100644 index 6cdc0fff..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/HashPathElement.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; - -/** - * For use on the LHS, allows the user to specify an explicit string to write out. - * Aka given a input that is boolean, would want to write something out other than "true" / "false". - */ -public class HashPathElement extends BasePathElement implements MatchablePathElement { - - private final String keyValue; - - public HashPathElement( String key ) { - super(key); - - if ( StringTools.isBlank( key ) ) { - throw new SpecException( "HashPathElement cannot have empty String as input." ); - } - - if ( ! key.startsWith( "#" ) ) { - throw new SpecException( "LHS # should start with a # : " + key ); - } - - if ( key.length() <= 1 ) { - throw new SpecException( "HashPathElement input is too short : " + key ); - } - - - if ( key.charAt( 1 ) == '(' ) { - if ( key.charAt( key.length() -1 ) == ')' ) { - keyValue = key.substring( 2, key.length() -1 ); - } - else { - throw new SpecException( "HashPathElement, mismatched parens : " + key ); - } - } - else { - keyValue = key.substring( 1 ); - } - } - - @Override - public String getCanonicalForm() { - return "#(" + keyValue + ")"; - } - - @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { - return new MatchedElement( keyValue ); - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/TransposePathElement.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/TransposePathElement.java deleted file mode 100644 index 46ef7898..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/TransposePathElement.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.TransposeReader; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.PathStep; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; - -/** - * This PathElement is used by Shiftr to Transpose data. - * - * It can be used on the Left and Right hand sides of the spec. - * - * Input - * { - * "author" : "Stephen Hawking", - * "book" : "A Brief History of Time" - * } - * - * Wanted - * { - * "Stephen Hawking" : "A Brief History of Time" - * } - * - * The first part of the process is to allow a CompositeShiftr node to look down the input JSON tree. - * - * Spec - * { - * "@author" : "@book" - * } - * - * - * Secondly, we can look up the tree, and come down a different path to locate data. - * - * For example of this see the following ShiftrUnit tests : - * LHS Lookup : json/shiftr/filterParents.json - * RHS Lookup : json/shiftr/transposeComplex6_rhs-complex-at.json - * - * - * CanonicalForm Expansion - * Sugar - * "@2 -> "@(2,) - * "@(2) -> "@(2,) - * "@author" -> "@(0,author)" - * "@(author)" -> "@(0,author)" - * - * Splenda - * "@(a.b)" -> "@(0,a.b)" - * "@(a.&2.c)" -> "@(0,a.&(2,0).c)" - */ -public class TransposePathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { - - private final int upLevel; - private final TransposeReader subPathReader; - private final String canonicalForm; - - /** - * Parse a text value from a Spec, into a TransposePathElement. - * - * @param key rawKey from a Jolt Spec file - * @return a TransposePathElement - */ - public static TransposePathElement parse( String key ) { - - if ( key == null || key.length() < 2 ) { - throw new SpecException( "'Transpose Input' key '@', can not be null or of length 1. Offending key : " + key ); - } - if ( '@' != key.charAt( 0 ) ) { - throw new SpecException( "'Transpose Input' key must start with an '@'. Offending key : " + key ); - } - - // Strip off the leading '@' as we don't need it anymore. - String meat = key.substring( 1 ); - - if ( meat.contains( "@" ) ) { - throw new SpecException( "@ pathElement can not contain a nested @. Was: " + meat ); - } - if ( meat.contains( "*" ) || meat.contains( "[]" ) ) { - throw new SpecException( "'Transpose Input' can not contain expansion wildcards (* and []). Offending key : " + key ); - } - - // Check to see if the key is wrapped by parens - if ( meat.startsWith( "(" ) ) { - if ( meat.endsWith( ")" ) ) { - meat = meat.substring( 1, meat.length() - 1 ); - } - else { - throw new SpecException( "@ path element that starts with '(' must have a matching ')'. Offending key : " + key ); - } - } - - return innerParse( key, meat ); - } - - /** - * Parse the core of the TransposePathElement key, once basic errors have been checked and - * syntax has been handled. - * - * @param originalKey The original text for reference. - * @param meat The string to actually parse into a TransposePathElement - * @return TransposePathElement - */ - private static TransposePathElement innerParse( String originalKey, String meat ) { - - char first = meat.charAt( 0 ); - if ( Character.isDigit( first ) ) { - // loop until we find a comma or end of string - StringBuilder sb = new StringBuilder().append( first ); - for ( int index = 1; index < meat.length(); index++ ) { - char c = meat.charAt( index ); - - // when we find a / the first comma, stop looking for integers, and just assume the rest is a String path - if( ',' == c ) { - - int upLevel; - try { - upLevel = Integer.valueOf( sb.toString() ); - } - catch ( NumberFormatException nfe ) { - // I don't know how this exception would get thrown, as all the chars were checked by isDigit, but oh well - throw new SpecException( "@ path element with non/mixed numeric key is not valid, key=" + originalKey ); - } - - return new TransposePathElement( originalKey, upLevel, meat.substring( index + 1 ) ); - } - else if ( Character.isDigit( c ) ) { - sb.append( c ); - } - else { - throw new SpecException( "@ path element with non/mixed numeric key is not valid, key=" + originalKey ); - } - } - - // if we got out of the for loop, then the whole thing was a number. - return new TransposePathElement( originalKey, Integer.valueOf( sb.toString() ), null ); - } - else { - return new TransposePathElement( originalKey, 0, meat ); - } - } - - /** - * Private constructor used after parsing is done. - * - * @param originalKey for reference - * @param upLevel How far up the tree to go - * @param subPath Where to go down the tree - */ - private TransposePathElement( String originalKey, int upLevel, String subPath ) { - super(originalKey); - this.upLevel = upLevel; - if ( StringTools.isEmpty( subPath ) ) { - this.subPathReader = null; - canonicalForm = "@(" + upLevel + ",)"; - } - else { - subPathReader = new TransposeReader(subPath); - canonicalForm = "@(" + upLevel + "," + subPathReader.getCanonicalForm() + ")"; - } - } - - /** - * This method is used when the TransposePathElement is used on the LFH as data. - * - * Aka, normal "evaluate" returns either a Number or a String. - * - * @param walkedPath WalkedPath to evaluate against - * @return The data specified by this TransposePathElement. - */ - public Optional objectEvaluate( WalkedPath walkedPath ) { - // Grap the data we need from however far up the tree we are supposed to go - PathStep pathStep = walkedPath.elementFromEnd( upLevel ); - - if ( pathStep == null ) { - return Optional.empty(); - } - - Object treeRef = pathStep.getTreeRef(); - - // Now walk down from that level using the subPathReader - if ( subPathReader == null ) { - return Optional.of( treeRef ); - } - else { - return subPathReader.read( treeRef, walkedPath ); - } - } - - @Override - public String evaluate( WalkedPath walkedPath ) { - - Optional dataFromTranspose = objectEvaluate( walkedPath ); - - if ( dataFromTranspose.isPresent() ) { - - Object data = dataFromTranspose.get(); - - // Coerce a number into a String - if ( data instanceof Number ) { - // the idea here being we are looking for an array index value - int val = ((Number) data).intValue(); - return Integer.toString( val ); - } - - // Coerce a boolean into a String - if ( data instanceof Boolean ) { - return Boolean.toString( (Boolean) data ); - } - - if ( data == null || ! ( data instanceof String ) ) { - - // If this output path has a TransposePathElement, and when we evaluate it - // it does not resolve to a String, then return null - return null; - } - - return (String) data; - } - else { - return null; - } - } - - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { - return walkedPath.lastElement().getMatchedElement(); // copy what our parent was so that write keys of &0 and &1 both work. - } - - @Override - public String getCanonicalForm() { - return canonicalForm; - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/BasePathAndGroupReference.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/BasePathAndGroupReference.java deleted file mode 100644 index 50db8471..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/BasePathAndGroupReference.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.reference; - -import com.bazaarvoice.jolt.exception.SpecException; - -/** - * All "References" extend this class and support three level of syntactic sugar - * Example with the AmpReference - * 1 "&" - * 2 "&0" - * 3 "&(0,0)" - * all three mean the same thing. - * - * References are used to look up values in a WalkedPath. - * In the CanonicalForm the first entry is how far up the WalkedPath to look for a LiteralPathElement, - * and the second entry is which part of that LiteralPathElement to ask for. - */ -public abstract class BasePathAndGroupReference implements PathAndGroupReference { - - private final int keyGroup; // equals 0 for "&" "&0" and "&(x,0)" - private final int pathIndex; // equals 0 for "&" "&0" and "&(0,x)" - - protected abstract char getToken(); - - public BasePathAndGroupReference( String refStr ) { - - if ( refStr == null || refStr.length() == 0 || getToken() != refStr.charAt( 0 ) ) { - throw new SpecException( "Invalid reference key=" + refStr + " either blank or doesn't start with correct character=" + getToken() ); - } - - int pI = 0; - int kG = 0; - - try { - if ( refStr.length() > 1 ) { - - String meat = refStr.substring( 1 ); - - if( meat.length() >= 3 && meat.startsWith( "(" ) && meat.endsWith( ")" ) ) { - - // "&(1,2)" -> "1,2".split( "," ) -> String[] { "1", "2" } OR - // "&(3)" -> "3".split( "," ) -> String[] { "3" } - - String parenMeat = meat.substring( 1, meat.length() -1 ); - String[] intStrs = parenMeat.split( "," ); - if ( intStrs.length > 2 ) { - throw new SpecException( "Invalid Reference=" + refStr ); - } - - pI = Integer.parseInt( intStrs[0] ); - if ( intStrs.length == 2 ) { - kG = Integer.parseInt( intStrs[1] ); - } - } - else { // &2 - pI = Integer.parseInt( meat ); - } - } - } - catch( NumberFormatException nfe ) { - throw new SpecException( "Unable to parse '" + getToken() + "' reference key:" + refStr, nfe ); - } - - if ( pI < 0 || kG < 0 ) { - throw new SpecException( "Reference:" + refStr + " can not have a negative value." ); - } - - pathIndex = pI; - keyGroup = kG; - } - - public int getPathIndex() { - return pathIndex; - } - - public int getKeyGroup() { - return keyGroup; - } - - /** - * Builds the non-syntactic sugar / maximally expanded and unique form of this reference. - * @return canonical form : aka "&" -> "&(0,0) - */ - public String getCanonicalForm() { - return getToken() + "(" + pathIndex + "," + keyGroup + ")"; - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/ArrayKey.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/ArrayKey.java deleted file mode 100644 index d6a87710..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/ArrayKey.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.defaultr; - -import com.bazaarvoice.jolt.common.DeepCopy; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -public class ArrayKey extends Key { - - private Collection keyInts; - private int keyInt = -1; - - public ArrayKey( String jsonKey, Object spec ) { - super( jsonKey, spec ); - - // Handle ArrayKey specific stuff - switch( getOp() ){ - case OR : - keyInts = new ArrayList<>(); - for( String orLiteral : keyStrings ) { - int orInt = Integer.parseInt( orLiteral ); - keyInts.add( orInt ); - } - break; - case LITERAL: - keyInt = Integer.parseInt( rawKey ); - keyInts = Arrays.asList( keyInt ); - break; - case STAR: - keyInts = Collections.emptyList(); - break; - default : - throw new IllegalStateException( "Someone has added an op type without changing this method." ); - } - } - - @Override - protected int getLiteralIntKey() { - return keyInt; - } - - @Override - protected void applyChild( Object container ) { - - if ( container instanceof List ) { - @SuppressWarnings( "unchecked" ) - List defaultList = (List) container; - - // Find all defaultee keys that match the childKey spec. Simple for Literal keys, more work for * and |. - for ( Integer literalKey : determineMatchingContainerKeys( defaultList ) ) { - applyLiteralKeyToContainer( literalKey, defaultList ); - } - } - // Else there is disagreement (with respect to Array vs Map) between the data in - // the Container vs the Defaultr Spec type for this key. Container wins, so do nothing. - } - - private void applyLiteralKeyToContainer( Integer literalIndex, List container ) { - - Object defaulteeValue = container.get( literalIndex ); - - if ( children == null ) { - if ( defaulteeValue == null ) { - container.set( literalIndex, DeepCopy.simpleDeepCopy( literalValue ) ); // apply a copy of the default value into a List, assumes the list as already been expanded if needed. - } - } - else { - if ( defaulteeValue == null ) { - defaulteeValue = createOutputContainerObject(); - container.set( literalIndex, defaulteeValue ); // push a new sub-container into this list - } - - // recurse by applying my children to this known valid container - applyChildren( defaulteeValue ); - } - } - - private Collection determineMatchingContainerKeys( List container ) { - - switch ( getOp() ) { - case LITERAL: - // Container it should get these literal values added to it - return keyInts; - case STAR: - // Identify all its keys - // this assumes the container list has already been expanded to the right size - List defaultList = (List) container; - List allIndexes = new ArrayList<>( defaultList.size() ); - for ( int index = 0; index < defaultList.size(); index++ ) { - allIndexes.add( index ); - } - - return allIndexes; - case OR: - // Identify the intersection between the container "keys" and the OR values - List indexesInRange = new ArrayList<>(); - - for ( Integer orValue : keyInts ) { - if ( orValue < ((List) container ).size() ) { - indexesInRange.add( orValue ); - } - } - return indexesInRange; - default : - throw new IllegalStateException( "Someone has added an op type without changing this method." ); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/OPS.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/OPS.java deleted file mode 100644 index 4695f01c..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/OPS.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.defaultr; - -import com.bazaarvoice.jolt.Defaultr; -import com.bazaarvoice.jolt.exception.SpecException; - -import java.util.Comparator; - -public enum OPS { - - STAR, OR, LITERAL; - - public static OPS parse( String key ) { - if ( key.contains( Defaultr.WildCards.STAR ) ){ - - if ( ! Defaultr.WildCards.STAR.equals( key ) ) { - throw new SpecException("Defaultr key " + key + " is invalid. * keys can only contain *, and no other characters." ); - } - - return STAR; - } - if ( key.contains( Defaultr.WildCards.OR ) ) { - return OR; - } - return LITERAL; - } - - public static class OpsPrecedenceComparator implements Comparator { - /** - * The order we want to apply Defaultr logic is Literals, Or, and then Star. - * Since we walk the sorted data from 0 to n, that means Literals need to low, and Star should be high. - */ - @Override - public int compare(OPS ops, OPS ops1) { - - // a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. - // s < s1 -> -1 - // s = s1 -> 0 - // s > s1 -> 1 - - if ( ops == ops1 ) { - return 0; - } - - if ( STAR == ops ) { - return 1; - } - if ( LITERAL == ops ) { - return -1; - } - - // if we get here, "ops" has to equal OR - if ( STAR == ops1) { - return -1; - } - if ( LITERAL == ops1 ) { - return 1; - } - - // both are ORs, should never get here - throw new IllegalStateException( "Someone has added an op type without changing this method." ); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Function.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Function.java deleted file mode 100644 index 638ab096..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Function.java +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; - -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Modifier supports a Function on RHS that accepts jolt path expressions as arguments and evaluates - * them at runtime before calling it. Function always returns an Optional, and the value is written - * only if the optional is not empty. - * - * function spec is defined by "key": "=functionName(args...)" - * - * - * input: - * { "num": -1.0 } - * spec: - * { "num": "=abs(@(1,&0))" } - * will call the stock function Math.abs() and will pass the matching value at "num" - * - * spec: - * { "num": "=abs" } - * an alternative shortcut will do the same thing - * - * output: - * { "num": 1.0 } - * - * - * - * input: - * { "value": -1.0 } - * - * spec: - * { "absValue": "=abs(@(1,value))" } - * will evaluate the jolt path expression @(1,value) and pass the output to stock function Math.abs() - * - * output: - * { "value": -1.0, "absValue": 1.0 } - * - * - * - * Currently defined stock functions are: - * - * toLower - returns toLower value of toString() value of first arg, rest is ignored - * toUpper - returns toUpper value of toString() value of first arg, rest is ignored - * concat - concatenate all given arguments' toString() values - * - * min - returns the min of all numbers provided in the arguments, non-numbers are ignored - * max - returns the max of all numbers provided in the arguments, non-numbers are ignored - * abs - returns the absolute value of first argument, rest is ignored - * toInteger - returns the intValue() value of first argument if its numeric, rest is ignored - * toDouble - returns the doubleValue() value of first argument if its numeric, rest is ignored - * toLong - returns the longValue() value of first argument if its numeric, rest is ignored - * - * All of these functions returns Optional.EMPTY if unsuccessful, which results in a no-op when performing - * the actual write in the json doc. - * - * i.e. - * input: - * { "value1": "xyz" } --- note: string, not number - * { "value1": "1.0" } --- note: string, not number - * - * spec: - * { "value1": "=abs" } --- fails silently - * { "value2": "=abs" } - * - * output: - * { "value1": "xyz", "value2": "1" } --- note: "absValue": null is not inserted - * - * - * This is work in progress, and probably will be changed in future releases. Hence it is marked for - * removal as it'll eventually be moved to a different package as the Function feature is baked into - * other transforms as well. In short this interface is not yet ready to be implemented outside jolt! - * - */ - -@Deprecated -public interface Function { - - Optional apply(Object... args); - - /** - * Does nothing - * - * spec - "key": "=noop" - * - * will cause the key to remain unchanged - */ - Function noop = new Function() { - @Override - public Optional apply( final Object... args ) { - return Optional.empty(); - } - }; - - /** - * Returns the first argument, null or otherwise - * - * spec - "key": [ "=isPresent", "otherValue" ] - * - * input - "key": null - * output - "key": null - * - * input - "key": "value" - * output - "key": "value" - * - * input - key is missing - * output - "key": "otherValue" - * - */ - Function isPresent = new Function() { - @Override - public Optional apply( final Object... args ) { - if (args.length == 0) { - return Optional.empty(); - } - return Optional.of( args[0] ); - } - }; - - /** - * Returns the first argument if in not null - * - * spec - "key": ["=notNull", "otherValue" ] - * - * input - "key": null - * output - "key": "otherValue" - * - * input - "key": "value" - * output - "key": "value" - * - */ - Function notNull = new Function() { - @Override - public Optional apply( final Object... args ) { - if (args.length == 0 || args[0] == null) { - return Optional.empty(); - } - return Optional.of( args[0] ); - } - }; - - /** - * Returns the first argument if it is null - * - * spec - "key": ["=inNull", "otherValue" ] - * - * input - "key": null - * output - "key": null - * - * input - "key": "value" - * output - "key": "otherValue" - * - */ - Function isNull = new Function() { - @Override - public Optional apply( final Object... args ) { - if (args.length == 0 || args[0] != null) { - return Optional.empty(); - } - return Optional.of( args[0] ); - } - }; - - /** - * Abstract class that processes var-args and calls two abstract methods - * - * If its single list arg, or many args, calls applyList() - * else calls applySingle() - * - * @param type of return value - */ - @SuppressWarnings( "unchecked" ) - abstract class BaseFunction implements Function { - - public final Optional apply( final Object... args ) { - if(args.length == 0) { - return Optional.empty(); - } - else if(args.length == 1) { - if(args[0] instanceof List ) { - if(((List) args[0]).isEmpty()) { - return Optional.empty(); - } - else { - return applyList((List) args[0]); - } - } - else if( args[0] instanceof Object[] ) { - if(((Object[]) args[0]).length == 0) { - return Optional.empty(); - } - else { - return applyList(Arrays.asList(((Object[]) args[0]))); - } - } - else if(args[0] == null) { - return Optional.empty(); - } - else { - return (Optional) applySingle( args[0] ); - } - } - else { - return applyList( Arrays.asList( args ) ); - } - } - - protected abstract Optional applyList( final List input ); - - protected abstract Optional applySingle( final Object arg ); - } - - /** - * Abstract class that provides rudimentary abstraction to quickly implement - * a function that works on an single value input - * - * i.e. toUpperCase a string - * - * @param type of return value - */ - @SuppressWarnings( "unchecked" ) - abstract class SingleFunction extends BaseFunction { - - protected final Optional applyList( final List input ) { - List ret = new ArrayList<>( input.size() ); - for(Object o: input) { - Optional optional = applySingle( o ); - ret.add(optional.isPresent()?optional.get():o); - } - return Optional.of( ret ); - } - - protected abstract Optional applySingle( final Object arg ); - } - - /** - * Abstract class that provides rudimentary abstraction to quickly implement - * a function that works on an List of input - * - * i.e. find the max item from a list, etc. - * - */ - @SuppressWarnings( "unchecked" ) - abstract class ListFunction extends BaseFunction { - - protected abstract Optional applyList( final List argList ); - - protected final Optional applySingle( final Object arg ) { - return Optional.empty(); - } - } - - /** - * Abstract class that provides rudimentary abstraction to quickly implement - * a function that classifies first arg as special input and rest as regular - * input. - * - * @param type of special argument - * @param type of return value - */ - @SuppressWarnings( "unchecked" ) - abstract class ArgDrivenFunction implements Function { - - private final Class specialArgType; - - private ArgDrivenFunction() { - /** - * inspired from {@link com.google.common.reflect.TypeCapture#capture()} - * copied, coz jolt-core is designed to have no dependency - * modified, coz the instanceof check and subsequently throwing exception - * is unnecessary as we already know this class has genericSuperClass of - * Parametrized type. In worst case if an implementation does not specify - * the generics, we fall back to Object.class, and that's ok. - */ - Type superclass = getClass().getGenericSuperclass(); - if(superclass instanceof ParameterizedType) { - specialArgType = (Class) ((ParameterizedType) superclass).getActualTypeArguments()[0]; - } - else { - specialArgType = (Class) Object.class; - } - } - - private Optional getSpecialArg( Object[] args) { - if ( (args.length >= 2) && specialArgType.isInstance( args[0]) ) { - SOURCE specialArg = (SOURCE) args[0]; - return Optional.of( specialArg ); - } - return Optional.empty(); - } - - @Override - public final Optional apply( Object... args ) { - - if(args.length == 1 && args[0] instanceof List) { - args = ((List) args[0]).toArray(); - } - - Optional specialArgOptional = getSpecialArg( args ); - if ( specialArgOptional.isPresent() ) { - SOURCE specialArg = specialArgOptional.get(); - if ( args.length == 2) { - if(args[1] instanceof List) { - return (Optional) applyList( specialArg, (List) args[1] ); - } - else { - return (Optional) applySingle( specialArg, args[1] ); - } - } - else { - List input = Arrays.asList( Arrays.copyOfRange(args, 1, args.length) ); - return applyList( specialArg, input ); - } - } - else { - return Optional.empty(); - } - } - - protected abstract Optional applyList( SOURCE specialArg, List args ); - - protected abstract Optional applySingle( SOURCE specialArg, Object arg ); - } - - /** - * Extends ArgDrivenConverter to provide rudimentary abstraction to quickly - * implement a function that works on a single input - * - * i.e. increment(1, value) - * - * @param type of special argument - * @param type of return value - */ - @SuppressWarnings( "unchecked" ) - abstract class ArgDrivenSingleFunction extends ArgDrivenFunction { - - protected final Optional applyList( S specialArg, List input ) { - List ret = new ArrayList<>( input.size() ); - for(Object o: input) { - Optional optional = applySingle( specialArg, o ); - ret.add(optional.isPresent()?optional.get():o); - } - return (Optional) Optional.of( ret ); - } - - protected abstract Optional applySingle( S specialArg, Object arg ); - } - - /** - * Extends ArgDrivenConverter to provide rudimentary abstraction to quickly - * implement a function that works on an input list|array - * - * i.e. join('-', ...) - * - * @param type of special argument - */ - @SuppressWarnings( "unchecked" ) - abstract class ArgDrivenListFunction extends ArgDrivenFunction { - - protected abstract Optional applyList( S specialArg, List args ); - - protected final Optional applySingle( S specialArg, Object arg ) { - return Optional.empty(); - } - } - - /** - * squashNull is a special kind of null processing,the input is always a list or map as a singleton - * - * @param type of return value - */ - abstract class SquashFunction implements Function { - - public final Optional apply( final Object... args ) { - if(args.length == 0) { - return Optional.empty(); - } - else if(args.length == 1) { - if(args[0] instanceof List ) { - if(((List) args[0]).isEmpty()) { - return Optional.empty(); - } - else { - return (Optional)applySingle((List) args[0]); - } - } - else if( args[0] instanceof Object[] ) { - if(((Object[]) args[0]).length == 0) { - return Optional.empty(); - } - else { - return (Optional)applySingle(Arrays.asList(((Object[]) args[0]))); - } - } - else if(args[0] == null) { - return Optional.empty(); - } - else { - return (Optional) applySingle( args[0] ); - } - } - else { - return (Optional)applySingle( Arrays.asList( args ) ); - } - } - - protected abstract Optional applySingle( final Object arg ); - } - -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/FunctionArg.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/FunctionArg.java deleted file mode 100644 index 39535423..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/FunctionArg.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.PathEvaluatingTraversal; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.pathelement.TransposePathElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; - -import java.util.Map; - -public abstract class FunctionArg { - - public static FunctionArg forSelf(PathEvaluatingTraversal traversal) { - return new SelfLookupArg( traversal ); - } - - private static final class SelfLookupArg extends FunctionArg { - private final TransposePathElement pathElement; - - private SelfLookupArg( PathEvaluatingTraversal traversal ) { - PathElement pathElement = traversal.get( traversal.size() - 1 ); - if(pathElement instanceof TransposePathElement ) { - this.pathElement = (TransposePathElement) pathElement; - } - else { - throw new SpecException( "Expected @ path element here" ); - } - } - - @Override - public Optional evaluateArg( final WalkedPath walkedPath, final Map context ) { - return pathElement.objectEvaluate( walkedPath ); - } - } - - public static FunctionArg forContext(PathEvaluatingTraversal traversal) { - return new ContextLookupArg( traversal ); - } - - private static final class ContextLookupArg extends FunctionArg { - private final PathEvaluatingTraversal traversal; - - private ContextLookupArg( PathEvaluatingTraversal traversal ) { - this.traversal = traversal; - } - - @Override - public Optional evaluateArg( final WalkedPath walkedPath, final Map context ) { - return traversal.read( context, walkedPath ); - } - } - - public static FunctionArg forLiteral( Object obj, boolean parseArg ) { - if(parseArg) { - if ( obj instanceof String ) { - String arg = (String) obj; - if ( arg.length() == 0 ) { - return new LiteralArg( null ); - } - else if ( arg.startsWith( "'" ) && arg.endsWith( "'" ) ) { - return new LiteralArg( arg.substring( 1, arg.length() - 1 ) ); - } - else if ( arg.equalsIgnoreCase( "true" ) || arg.equalsIgnoreCase( "false" ) ) { - return new LiteralArg( Boolean.parseBoolean( arg ) ); - } - else { - Optional optional = Objects.toNumber( arg ); - if(optional.isPresent()) { - return new LiteralArg( optional.get() ); - } - return new LiteralArg( arg ); - } - } - else { - return new LiteralArg( obj ); - } - } - else { - return new LiteralArg( obj ); - } - } - - private static final class LiteralArg extends FunctionArg { - - private final Optional returnValue; - - private LiteralArg( final Object object ) { - this.returnValue = Optional.of( object ); - } - - @Override - public Optional evaluateArg( final WalkedPath walkedPath, final Map context ) { - return returnValue; - } - } - - public abstract Optional evaluateArg(WalkedPath walkedPath, Map context); -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Math.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Math.java deleted file mode 100644 index adec651d..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Math.java +++ /dev/null @@ -1,448 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.List; - -@SuppressWarnings( "deprecated" ) -public class Math { - - /** - * Given a list of objects, returns the max value in its appropriate type - * also, interprets String as Number and returns appropriately - * - * max(1,2l,3d) == Optional.of(3d) - * max(1,2l,"3.0") == Optional.of(3.0) - * max("a", "b", "c") == Optional.empty() - * max([]) == Optional.empty() - */ - public static Optional max( List args ) { - if(args == null || args.size() == 0) { - return Optional.empty(); - } - - Integer maxInt = Integer.MIN_VALUE; - Double maxDouble = -(Double.MAX_VALUE); - Long maxLong = Long.MIN_VALUE; - boolean found = false; - - for(Object arg: args) { - if(arg instanceof Integer) { - maxInt = java.lang.Math.max( maxInt, (Integer) arg ); - found = true; - } - else if(arg instanceof Double) { - maxDouble = java.lang.Math.max( maxDouble, (Double) arg ); - found = true; - } - else if(arg instanceof Long) { - maxLong = java.lang.Math.max(maxLong, (Long) arg); - found = true; - } - else if(arg instanceof String) { - Optional optional = Objects.toNumber( arg ); - if(optional.isPresent()) { - arg = optional.get(); - if(arg instanceof Integer) { - maxInt = java.lang.Math.max( maxInt, (Integer) arg ); - found = true; - } - else if(arg instanceof Double) { - maxDouble = java.lang.Math.max( maxDouble, (Double) arg ); - found = true; - } - else if(arg instanceof Long) { - maxLong = java.lang.Math.max(maxLong, (Long) arg); - found = true; - } - } - } - } - if(!found) { - return Optional.empty(); - } - - // explicit getter method calls to avoid runtime autoboxing - // autoBoxing will cause it to return the different type - // check MathTest#testAutoBoxingIssue for example - if(maxInt.longValue() >= maxDouble.longValue() && maxInt.longValue() >= maxLong) { - return Optional.of(maxInt); - } - else if(maxLong >= maxDouble.longValue()) { - return Optional.of(maxLong); - } - else { - return Optional.of(maxDouble); - } - } - - /** - * Given a list of objects, returns the min value in its appropriate type - * also, interprets String as Number and returns appropriately - * - * min(1d,2l,3) == Optional.of(1d) - * min("1.0",2l,d) == Optional.of(1.0) - * min("a", "b", "c") == Optional.empty() - * min([]) == Optional.empty() - */ - public static Optional min( List args ) { - if(args == null || args.size() == 0) { - return Optional.empty(); - } - Integer minInt = Integer.MAX_VALUE; - Double minDouble = Double.MAX_VALUE; - Long minLong = Long.MAX_VALUE; - boolean found = false; - - for(Object arg: args) { - if(arg instanceof Integer) { - minInt = java.lang.Math.min( minInt, (Integer) arg ); - found = true; - } - else if(arg instanceof Double) { - minDouble = java.lang.Math.min( minDouble, (Double) arg ); - found = true; - } - else if(arg instanceof Long) { - minLong = java.lang.Math.min( minLong, (Long) arg ); - found = true; - } - else if(arg instanceof String) { - Optional optional = Objects.toNumber( arg ); - if(optional.isPresent()) { - arg = optional.get(); - if(arg instanceof Integer) { - minInt = java.lang.Math.min( minInt, (Integer) arg ); - found = true; - } - else if(arg instanceof Double) { - minDouble = java.lang.Math.min( minDouble, (Double) arg ); - found = true; - } - else if(arg instanceof Long) { - minLong = java.lang.Math.min(minLong, (Long) arg); - found = true; - } - } - } - } - if(!found) { - return Optional.empty(); - } - // explicit getter method calls to avoid runtime autoboxing - if(minInt.longValue() <= minDouble.longValue() && minInt.longValue() <= minLong) { - return Optional.of(minInt); - } - else if(minLong <= minDouble.longValue()) { - return Optional.of(minLong); - } - else { - return Optional.of(minDouble); - } - } - - /** - * Given any object, returns, if possible. its absolute value wrapped in Optional - * Interprets String as Number - * - * abs("-123") == Optional.of(123) - * abs("123") == Optional.of(123) - * abs("12.3") == Optional.of(12.3) - * - * abs("abc") == Optional.empty() - * abs(null) == Optional.empty() - * - */ - public static Optional abs( Object arg ) { - if(arg instanceof Integer) { - return Optional.of( java.lang.Math.abs( (Integer) arg )); - } - else if(arg instanceof Double) { - return Optional.of( java.lang.Math.abs( (Double) arg )); - } - else if(arg instanceof Long) { - return Optional.of( java.lang.Math.abs( (Long) arg )); - } - else if(arg instanceof String) { - return abs( Objects.toNumber( arg ).get() ); - } - return Optional.empty(); - } - - /** - * Given a list of numbers, returns their avg as double - * any value in the list that is not a valid number is ignored - * - * avg(2,"2","abc") == Optional.of(2.0) - */ - public static Optional avg (List args) { - double sum = 0d; - int count = 0; - for(Object arg: args) { - Optional numberOptional = Objects.toNumber( arg ); - if(numberOptional.isPresent()) { - sum = sum + numberOptional.get().doubleValue(); - count = count + 1; - } - } - return count == 0 ? Optional.empty() : Optional.of( sum / count ); - } - - public static Optional intSum(List args) { - Integer sum = 0; - for(Object arg: args) { - Optional numberOptional = Objects.toInteger(arg); - if(numberOptional.isPresent()) { - sum = sum + numberOptional.get(); - } - } - return Optional.of(sum); - } - - public static Optional doubleSum(List args) { - Double sum = 0.0; - for(Object arg: args) { - Optional numberOptional = Objects.toDouble(arg); - if(numberOptional.isPresent()) { - sum = sum + numberOptional.get(); - } - } - return Optional.of(sum); - } - - public static Optional longSum(List args) { - Long sum = 0l; - for(Object arg: args) { - Optional numberOptional = Objects.toLong(arg); - if(numberOptional.isPresent()) { - sum = sum + numberOptional.get(); - } - } - return Optional.of(sum); - } - - public static Optional intSubtract(List argList) { - - if ( argList == null || argList.size() != 2 ) { - return Optional.empty(); - } - - if ( ! ( argList.get(0) instanceof Integer && argList.get(1) instanceof Integer ) ) { - return Optional.empty(); - } - - int a = (Integer) argList.get(0); - int b = (Integer) argList.get(1); - - return Optional.of( a - b ); - } - - public static Optional doubleSubtract(List argList) { - - if ( argList == null || argList.size() != 2 ) { - return Optional.empty(); - } - - if ( ! ( argList.get(0) instanceof Double && argList.get(1) instanceof Double ) ) { - return Optional.empty(); - } - - double a = (Double) argList.get(0); - double b = (Double) argList.get(1); - - return Optional.of( a - b ); - } - - public static Optional longSubtract(List argList) { - - if ( argList == null || argList.size() != 2 ) { - return Optional.empty(); - } - - if ( ! ( argList.get(0) instanceof Long && argList.get(1) instanceof Long ) ) { - return Optional.empty(); - } - - long a = (Long) argList.get(0); - long b = (Long) argList.get(1); - - return Optional.of( a - b ); - } - - - public static Optional divide(List argList) { - - if ( argList == null || argList.size() != 2 ) { - return Optional.empty(); - } - - Optional numerator = Objects.toNumber(argList.get(0)); - Optional denominator = Objects.toNumber(argList.get(1)); - - if(numerator.isPresent() && denominator.isPresent()) { - - Double drDoubleValue = denominator.get().doubleValue(); - if(drDoubleValue == 0) { - return Optional.empty(); - } - - Double nrDoubleValue = numerator.get().doubleValue(); - Double result = nrDoubleValue/drDoubleValue; - return Optional.of(result); - } - - return Optional.empty(); - } - - public static Optional divideAndRound(List argList, int digitsAfterDecimalPoint ) { - - Optional divideResult = divide(argList); - - if(divideResult.isPresent()){ - Double divResult = divideResult.get(); - BigDecimal bigDecimal = new BigDecimal(divResult).setScale(digitsAfterDecimalPoint, RoundingMode.HALF_UP); - return Optional.of(bigDecimal.doubleValue()); - } - - return Optional.empty(); - } - - @SuppressWarnings( "unchecked" ) - public static final class max extends Function.BaseFunction { - @Override - protected Optional applyList( final List argList ) { - return (Optional) max( argList ); - } - - @Override - protected Optional applySingle( final Object arg ) { - if(arg instanceof Number) { - return Optional.of(arg); - } - else { - return Optional.empty(); - } - } - } - - @SuppressWarnings( "unchecked" ) - public static final class min extends Function.BaseFunction { - - @Override - protected Optional applyList( final List argList ) { - return (Optional) min( argList ); - } - - @Override - protected Optional applySingle(Object arg) { - if(arg instanceof Number) { - return Optional.of(arg); - } - else { - return Optional.empty(); - } - } - } - - @SuppressWarnings( "unchecked" ) - public static final class abs extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - return abs( arg ); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class divide extends Function.ListFunction { - - @Override - protected Optional applyList(List argList) { - return (Optional)divide(argList); - } - - } - - @SuppressWarnings( "unchecked" ) - public static final class divideAndRound extends Function.ArgDrivenListFunction { - - - @Override - protected Optional applyList(Integer digitsAfterDecimalPoint, List args) { - return (Optional)divideAndRound(args, digitsAfterDecimalPoint); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class avg extends Function.ListFunction { - @Override - protected Optional applyList( final List argList ) { - return (Optional) avg( argList ); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class intSum extends Function.ListFunction { - @Override - protected Optional applyList( final List argIntList ) { - return (Optional) intSum(argIntList); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class doubleSum extends Function.ListFunction { - @Override - protected Optional applyList( final List argDoubleList ) { - return (Optional) doubleSum(argDoubleList); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class longSum extends Function.ListFunction { - @Override - protected Optional applyList( final List argLongList ) { - return (Optional) longSum(argLongList); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class intSubtract extends Function.ListFunction { - @Override - protected Optional applyList( final List argIntList ) { - return (Optional) intSubtract(argIntList); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class doubleSubtract extends Function.ListFunction { - @Override - protected Optional applyList( final List argDoubleList ) { - return (Optional) doubleSubtract(argDoubleList); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class longSubtract extends Function.ListFunction { - @Override - protected Optional applyList( final List argLongList ) { - return (Optional) longSubtract(argLongList); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Objects.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Objects.java deleted file mode 100644 index 0408b128..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Objects.java +++ /dev/null @@ -1,322 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -public class Objects { - - /** - * Given any object, returns, if possible. its Java number equivalent wrapped in Optional - * Interprets String as Number - * - * toNumber("123") == Optional.of(123) - * toNumber("-123") == Optional.of(-123) - * toNumber("12.3") == Optional.of(12.3) - * - * toNumber("abc") == Optional.empty() - * toNumber(null) == Optional.empty() - * - * also, see: MathTest#testNitPicks - * - */ - public static Optional toNumber(Object arg) { - if ( arg instanceof Number ) { - return Optional.of( ( (Number) arg )); - } - else if(arg instanceof String) { - try { - return Optional.of( (Number) Integer.parseInt( (String) arg ) ); - } - catch(Exception ignored) {} - try { - return Optional.of( (Number) Long.parseLong( (String) arg ) ); - } - catch(Exception ignored) {} - try { - return Optional.of( (Number) Double.parseDouble( (String) arg ) ); - } - catch(Exception ignored) {} - return Optional.empty(); - } - else { - return Optional.empty(); - } - } - - /** - * Returns int value of argument, if possible, wrapped in Optional - * Interprets String as Number - */ - public static Optional toInteger(Object arg) { - if ( arg instanceof Number ) { - return Optional.of( ( (Number) arg ).intValue() ); - } - else if(arg instanceof String) { - Optional optional = toNumber( arg ); - if ( optional.isPresent() ) { - return Optional.of( optional.get().intValue() ); - } - else { - return Optional.empty(); - } - } - else { - return Optional.empty(); - } - } - - /** - * Returns long value of argument, if possible, wrapped in Optional - * Interprets String as Number - */ - public static Optional toLong(Object arg) { - if ( arg instanceof Number ) { - return Optional.of( ( (Number) arg ).longValue() ); - } - else if(arg instanceof String) { - Optional optional = toNumber( arg ); - if ( optional.isPresent() ) { - return Optional.of( optional.get().longValue() ); - } - else { - return Optional.empty(); - } - } - else { - return Optional.empty(); - } - } - - /** - * Returns double value of argument, if possible, wrapped in Optional - * Interprets String as Number - */ - public static Optional toDouble(Object arg) { - if ( arg instanceof Number ) { - return Optional.of( ( (Number) arg ).doubleValue() ); - } - else if(arg instanceof String) { - Optional optional = toNumber( arg ); - if ( optional.isPresent() ) { - return Optional.of( optional.get().doubleValue() ); - } - else { - return Optional.empty(); - } - } - else { - return Optional.empty(); - } - } - - /** - * Returns boolean value of argument, if possible, wrapped in Optional - * Interprets Strings "true" & "false" as boolean - */ - public static Optional toBoolean(Object arg) { - if ( arg instanceof Boolean ) { - return Optional.of( (Boolean) arg ); - } - else if(arg instanceof String) { - if("true".equalsIgnoreCase( (String)arg )) { - return Optional.of( Boolean.TRUE ); - } - else if("false".equalsIgnoreCase( (String)arg )) { - return Optional.of( Boolean.FALSE ); - } - } - return Optional.empty(); - } - - /** - * Returns String representation of argument, wrapped in Optional - * - * for array argument, returns Arrays.toString() - * for others, returns Objects.toString() - * - * Note: this method does not return Optional.empty() - */ - public static Optional toString(Object arg) { - if ( arg instanceof String ) { - return Optional.of( (String) arg ); - } - else if ( arg instanceof Object[] ) { - return Optional.of( Arrays.toString( (Object[] )arg ) ); - } - else { - return Optional.of( java.util.Objects.toString( arg ) ); - } - } - - /** - * Squashes nulls in a list or map. - * - * Modifies the data. - */ - public static void squashNulls( Object input ) { - if ( input instanceof List ) { - List inputList = (List) input; - inputList.removeIf( java.util.Objects::isNull ); - } - else if ( input instanceof Map ) { - Map inputMap = (Map) input; - - List keysToNuke = new ArrayList<>(); - for (Map.Entry entry : inputMap.entrySet()) { - if ( entry.getValue() == null ) { - keysToNuke.add( entry.getKey() ); - } - } - - inputMap.keySet().removeAll( keysToNuke ); - } - } - - /** - * Recursively squash nulls in maps and lists. - * - * Modifies the data. - */ - public static void recursivelySquashNulls(Object input) { - - // Makes two passes thru the data. - Objects.squashNulls( input ); - - if ( input instanceof List ) { - List inputList = (List) input; - inputList.forEach( i -> recursivelySquashNulls( i ) ); - } - else if ( input instanceof Map ) { - Map inputMap = (Map) input; - - for (Map.Entry entry : inputMap.entrySet()) { - recursivelySquashNulls( entry.getValue() ); - } - } - } - - /** - * Squashes/Deletes duplicates in lists. - * - * Modifies the data. - */ - public static Optional squashDuplicates( Object input ) { - if ( input instanceof List ) { - List inputList = (List) input; - return Optional.of(inputList.stream().distinct().collect(Collectors.toList())); - } - return Optional.of(input); - } - - public static final class toInteger extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - return toInteger( arg ); - } - } - - public static final class toLong extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - return toLong( arg ); - } - } - - public static final class toDouble extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - return toDouble( arg ); - } - } - - public static final class toBoolean extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - return toBoolean( arg ); - } - } - - public static final class toString extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - return Objects.toString( arg ); - } - } - - public static final class squashNulls extends Function.SquashFunction { - @Override - protected Optional applySingle( final Object arg ) { - Objects.squashNulls( arg ); - return Optional.of( arg ); - } - } - - public static final class recursivelySquashNulls extends Function.SquashFunction { - @Override - protected Optional applySingle( final Object arg ) { - Objects.recursivelySquashNulls( arg ); - return Optional.of( arg ); - } - } - - public static final class squashDuplicates extends Function.SquashFunction { - @Override - protected Optional applySingle( final Object arg ) { - return Objects.squashDuplicates( arg ); - } - } - - /** - * Size is a special snowflake and needs specific care - */ - public static final class size implements Function { - - @Override - public Optional apply(Object... args) { - if(args.length == 0) { - return Optional.empty(); - } - else if(args.length == 1) { - if(args[0] == null) { - return Optional.empty(); - } - else if(args[0] instanceof List ) { - return Optional.of(((List) args[0]).size()); - } - else if(args[0] instanceof String) { - return Optional.of( ((String) args[0]).length() ); - } - else if(args[0] instanceof Map) { - return Optional.of( ((Map) args[0]).size() ); - } - else { - return Optional.empty(); - } - } - else { - return Optional.of(args.length); - } - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Strings.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Strings.java deleted file mode 100644 index 3af56224..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Strings.java +++ /dev/null @@ -1,236 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; - -import java.util.Arrays; -import java.util.List; - -@SuppressWarnings( "deprecated" ) -public class Strings { - - public static final class toLowerCase extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - - if ( ! (arg instanceof String) ) { - return Optional.empty(); - } - - String argString = (String) arg; - - return Optional.of( argString.toLowerCase() ); - } - } - - public static final class toUpperCase extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - - if ( ! (arg instanceof String) ) { - return Optional.empty(); - } - - String argString = (String) arg; - - return Optional.of( argString.toUpperCase() ); - } - } - - public static final class trim extends Function.SingleFunction { - @Override - protected Optional applySingle( final Object arg ) { - - if ( ! (arg instanceof String) ) { - return Optional.empty(); - } - - String argString = (String) arg; - - return Optional.of( argString.trim() ); - } - } - - public static final class concat extends Function.ListFunction { - @Override - protected Optional applyList( final List argList ) { - StringBuilder sb = new StringBuilder( ); - for(Object arg: argList ) { - if ( arg != null ) { - sb.append(arg.toString() ); - } - } - return Optional.of(sb.toString()); - } - } - - public static final class substring extends Function.ListFunction { - - @Override - protected Optional applyList(List argList) { - - // There is only one path that leads to success and many - // ways for this to fail. So using a do/while loop - // to make the bailing easy. - do { - - // if argList is null or not the right size; bail - if(argList == null || argList.size() != 3 ) { - break; - } - - if ( ! ( argList.get(0) instanceof String && - argList.get(1) instanceof Integer && - argList.get(2) instanceof Integer ) ) { - break; - } - - // If we get here, then all these casts should work. - String tuna = (String) argList.get(0); - int start = (Integer) argList.get(1); - int end = (Integer) argList.get(2); - - // do start and end make sense? - if ( start >= end || start < 0 || end < 1 || end > tuna.length() ) { - break; - } - - return Optional.of(tuna.substring(start, end)); - - } while( false ); - - // if we got here, then return an Optional.empty. - return Optional.empty(); - } - } - - @SuppressWarnings( "unchecked" ) - public static final class join extends Function.ArgDrivenListFunction { - - @Override - protected Optional applyList( final String specialArg, final List args ) { - StringBuilder sb = new StringBuilder( ); - for(int i=0; i < args.size(); i++) { - Object arg = args.get(i); - if (arg != null ) { - String argString = arg.toString(); - if( !("".equals( argString ))) { - sb.append( argString ); - if ( i < args.size() - 1 ) { - sb.append( specialArg ); - } - } - } - } - return Optional.of( sb.toString() ); - } - } - - public static final class split extends Function.ArgDrivenSingleFunction { - @Override - protected Optional applySingle(final String separator, final Object source) { - if (source == null || separator == null) { - return Optional.empty(); - } - else if ( source instanceof String ) { - // only try to split input strings - String inputString = (String) source; - return Optional.of( Arrays.asList(inputString.split(separator)) ); - } - else { - return Optional.empty(); - } - } - } - - - public static final class leftPad extends Function.ArgDrivenListFunction { - @Override - protected Optional applyList(String source, List args) { - - return padString( true, source, args ); - } - } - - public static final class rightPad extends Function.ArgDrivenListFunction { - @Override - protected Optional applyList(String source, List args) { - - return padString( false, source, args ); - } - } - - private static Optional padString( boolean leftPad, String source, List args ) { - - // There is only one path that leads to success and many - // ways for this to fail. So using a do/while loop - // to make the bailing easy. - do { - - if(source == null || args == null ) { - break; - } - - if ( ! ( args.get(0) instanceof Integer && - args.get(1) instanceof String ) ) { - break; - } - - Integer width = (Integer) args.get(0); - - // if the width param is stupid; bail - if ( width <= 0 || width > 500 ) { - break; - } - - String filler = (String) args.get(1); - - // filler can only be a single char - // otherwise the math becomes hard - if ( filler.length() != 1 ) { - break; - } - - char fillerChar = filler.charAt( 0 ); - - // if the desired width of the overall padding is smaller than - // the source string, then just return the source string. - if( width <= source.length() ) { - return Optional.of( source ); - } - - int padLength = width - source.length(); - char[] padArray = new char[padLength]; - - Arrays.fill( padArray, fillerChar ); - - StringBuilder sb = new StringBuilder(); - - if ( leftPad ) { - sb.append( padArray ).append( source ); - } - else { - sb.append( source ).append( padArray ); - } - - return Optional.of( sb.toString() ); - - } while ( false ); - - return Optional.empty(); - } -} \ No newline at end of file diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/spec/ModifierCompositeSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/spec/ModifierCompositeSpec.java deleted file mode 100644 index 4ae7f120..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/spec/ModifierCompositeSpec.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.spec; - -import com.bazaarvoice.jolt.common.ComputedKeysComparator; -import com.bazaarvoice.jolt.common.ExecutionStrategy; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.pathelement.ArrayPathElement; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarDoublePathElement; -import com.bazaarvoice.jolt.common.pathelement.StarRegexPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarSinglePathElement; -import com.bazaarvoice.jolt.common.spec.BaseSpec; -import com.bazaarvoice.jolt.common.spec.OrderedCompositeSpec; -import com.bazaarvoice.jolt.common.tree.ArrayMatchedElement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.modifier.DataType; -import com.bazaarvoice.jolt.modifier.OpMode; -import com.bazaarvoice.jolt.modifier.TemplatrSpecBuilder; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Composite spec is non-leaf level spec that contains one or many child specs and processes - * them based on a pre-determined execution strategy - */ -public class ModifierCompositeSpec extends ModifierSpec implements OrderedCompositeSpec { - private static final HashMap orderMap; - private static final ComputedKeysComparator computedKeysComparator; - - static { - orderMap = new HashMap<>(); - orderMap.put( ArrayPathElement.class, 1 ); - orderMap.put( StarRegexPathElement.class, 2 ); - orderMap.put( StarDoublePathElement.class, 3 ); - orderMap.put( StarSinglePathElement.class, 4 ); - orderMap.put( StarAllPathElement.class, 5 ); - computedKeysComparator = ComputedKeysComparator.fromOrder(orderMap); - } - - private final Map literalChildren; - private final List computedChildren; - private final ExecutionStrategy executionStrategy; - private final DataType specDataType; - - public ModifierCompositeSpec( final String key, final Map spec, final OpMode opMode, TemplatrSpecBuilder specBuilder ) { - super(key, opMode); - - Map literals = new LinkedHashMap<>(); - ArrayList computed = new ArrayList<>(); - - List children = specBuilder.createSpec( spec ); - - // remember max explicit index from spec to expand input array at runtime - // need to validate spec such that it does not specify both array and literal path element - int maxExplicitIndexFromSpec = -1, confirmedMapAtIndex = -1, confirmedArrayAtIndex = -1; - - for(int i=0; i -1 && confirmedArrayAtIndex > -1) { - throw new SpecException( opMode.name() + " RHS cannot mix int array index and string map key, defined spec for " + key + " contains: " + children.get( confirmedMapAtIndex ).pathElement.getCanonicalForm() + " conflicting " + children.get( confirmedArrayAtIndex ).pathElement.getCanonicalForm() ); - } - } - - // set the dataType from calculated indexes - specDataType = DataType.determineDataType( confirmedArrayAtIndex, confirmedMapAtIndex, maxExplicitIndexFromSpec ); - - // Only the computed children need to be sorted - Collections.sort( computed, computedKeysComparator ); - - computed.trimToSize(); - - literalChildren = Collections.unmodifiableMap( literals ); - computedChildren = Collections.unmodifiableList( computed ); - - // extract generic execution strategy - executionStrategy = determineExecutionStrategy(); - - } - - @Override - @SuppressWarnings( "unchecked" ) - public void applyElement( final String inputKey, Optional inputOptional, MatchedElement thisLevel, final WalkedPath walkedPath, final Map context ) { - - Object input = inputOptional.get(); - // sanity checks, cannot work on a list spec with map input and vice versa, and runtime with null input - if(!specDataType.isCompatible( input )) { - return; - } - - // create input if it is null - if( input == null ) { - input = specDataType.create( inputKey, walkedPath, opMode ); - // if input has changed, wrap - if ( input != null ) { - inputOptional = Optional.of( input ); - } - } - - // if input is List, create special ArrayMatchedElement, which tracks the original size of the input array - if(input instanceof List) { - // LIST means spec had array index explicitly specified, hence expand if needed - if( specDataType instanceof DataType.LIST ) { - int origSize = specDataType.expand( input ); - thisLevel = new ArrayMatchedElement( thisLevel.getRawKey(), origSize ); - } - else { - // specDataType is RUNTIME, so spec had no array index explicitly specified, no need to expand - thisLevel = new ArrayMatchedElement( thisLevel.getRawKey(), ((List) input).size() ); - } - } - - // add self to walked path - walkedPath.add( input, thisLevel ); - // Handle the rest of the children - executionStrategy.process( this, inputOptional, walkedPath, null, context ); - // We are done, so remove ourselves from the walkedPath - walkedPath.removeLast(); - } - - @Override - public Map getLiteralChildren() { - return literalChildren; - } - - @Override - public List getComputedChildren() { - return computedChildren; - } - - @Override - public ExecutionStrategy determineExecutionStrategy() { - - if ( computedChildren.isEmpty() ) { - return ExecutionStrategy.ALL_LITERALS; - } - else if ( literalChildren.isEmpty() ) { - return ExecutionStrategy.COMPUTED; - } - else if(opMode.equals( OpMode.DEFINER ) && specDataType instanceof DataType.LIST ) { - return ExecutionStrategy.CONFLICT; - } - else { - return ExecutionStrategy.ALL_LITERALS_WITH_COMPUTED; - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/spec/ModifierLeafSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/spec/ModifierLeafSpec.java deleted file mode 100644 index e4740530..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/spec/ModifierLeafSpec.java +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.spec; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.SpecStringParser; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.modifier.OpMode; -import com.bazaarvoice.jolt.modifier.TemplatrSpecBuilder; -import com.bazaarvoice.jolt.modifier.function.Function; -import com.bazaarvoice.jolt.modifier.function.FunctionArg; -import com.bazaarvoice.jolt.modifier.function.FunctionEvaluator; - -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -@SuppressWarnings( "deprecated" ) -public class ModifierLeafSpec extends ModifierSpec { - - private final List functionEvaluatorList; - - @SuppressWarnings( "unchecked" ) - public ModifierLeafSpec( final String rawJsonKey, Object rhsObj, final OpMode opMode, final Map functionsMap ) { - super(rawJsonKey, opMode); - functionEvaluatorList = new LinkedList<>( ); - - FunctionEvaluator functionEvaluator; - - // "key": "expression1" - if ( (rhsObj instanceof String) ) { - functionEvaluator = buildFunctionEvaluator( (String) rhsObj, functionsMap ); - functionEvaluatorList.add( functionEvaluator ); - } - // "key": ["expression1", "expression2", "expression3"] - else if(rhsObj instanceof List && ((List)rhsObj).size() > 0) { - List rhsList = (List) rhsObj; - for(Object rhs: rhsList) { - if(rhs instanceof String) { - functionEvaluator = buildFunctionEvaluator( rhs.toString(), functionsMap ); - functionEvaluatorList.add( functionEvaluator ); - } - else { - functionEvaluator = FunctionEvaluator.forArgEvaluation( FunctionArg.forLiteral( rhs, false ) ); - functionEvaluatorList.add( functionEvaluator ); - } - } - } - // "key": anyObjectOrLiteral --- just set as-is - else { - functionEvaluator = FunctionEvaluator.forArgEvaluation( FunctionArg.forLiteral( rhsObj, false ) ); - functionEvaluatorList.add( functionEvaluator ); - } - } - - @Override - public void applyElement( final String inputKey, final Optional inputOptional, final MatchedElement thisLevel, final WalkedPath walkedPath, final Map context ) { - - Object parent = walkedPath.lastElement().getTreeRef(); - - walkedPath.add( inputOptional.get(), thisLevel ); - - Optional valueOptional = getFirstAvailable( functionEvaluatorList, inputOptional, walkedPath, context ); - - if(valueOptional.isPresent()) { - setData( parent, thisLevel, valueOptional.get(), opMode ); - } - - walkedPath.removeLast(); - } - - private static FunctionEvaluator buildFunctionEvaluator( final String rhs, final Map functionsMap ) { - final FunctionEvaluator functionEvaluator; - // "key": "@0" --- evaluate expression then set - if(!rhs.startsWith( TemplatrSpecBuilder.FUNCTION )) { - return FunctionEvaluator.forArgEvaluation( constructSingleArg( rhs, false ) ); - } - else { - String functionName; - // "key": "=abs" --- call function with current value then set output if present - if ( !rhs.contains( "(" ) && !rhs.endsWith( ")" ) ) { - functionName = rhs.substring( TemplatrSpecBuilder.FUNCTION.length() ); - return FunctionEvaluator.forFunctionEvaluation( functionsMap.get( functionName ) ); - } - // "key": "=abs(@(1,&0))" --- evaluate expression then call function with - // expression-output, then set output if present - else { - String fnString = rhs.substring( TemplatrSpecBuilder.FUNCTION.length() ); - List fnArgs = SpecStringParser.parseFunctionArgs( fnString ); - functionName = fnArgs.remove( 0 ); - functionEvaluator = FunctionEvaluator.forFunctionEvaluation( functionsMap.get( functionName ), constructArgs( fnArgs ) ); - } - } - return functionEvaluator; - } - - private static Optional getFirstAvailable(List functionEvaluatorList, Optional inputOptional, WalkedPath walkedPath, Map context) { - Optional valueOptional = Optional.empty(); - for(FunctionEvaluator functionEvaluator: functionEvaluatorList) { - try { - valueOptional = functionEvaluator.evaluate( inputOptional, walkedPath, context ); - if(valueOptional.isPresent()) { - return valueOptional; - } - } - catch(Exception ignored) {} - } - return valueOptional; - } - - private static FunctionArg[] constructArgs( List argsList ) { - FunctionArg[] argsArray = new FunctionArg[argsList.size()]; - for(int i=0; i T buildFromPath( final String path ) { - return (T) new TransposeReader( path ); - } - }; - - protected final OpMode opMode; - protected final MatchablePathElement pathElement; - protected final boolean checkValue; - - /** - * Builds LHS pathElement and validates to specification - */ - protected ModifierSpec( String rawJsonKey, OpMode opMode ) { - String prefix = rawJsonKey.substring( 0, 1 ); - String suffix = rawJsonKey.length() > 1 ? rawJsonKey.substring( rawJsonKey.length() - 1 ) : null; - - if(OpMode.isValid( prefix )) { - this.opMode = OpMode.from( prefix ); - rawJsonKey = rawJsonKey.substring( 1 ); - } - else { - this.opMode = opMode; - } - - if ( suffix != null && suffix.equals( "?" ) && !( rawJsonKey.endsWith( "\\?" ) ) ) { - checkValue = true; - rawJsonKey = rawJsonKey.substring( 0, rawJsonKey.length() - 1 ); - } - else { - checkValue = false; - } - - this.pathElement = buildMatchablePathElement( rawJsonKey ); - if ( !( pathElement instanceof StarPathElement ) && !( pathElement instanceof LiteralPathElement ) && !( pathElement instanceof ArrayPathElement ) ) { - throw new SpecException( opMode.name() + " cannot have " + pathElement.getClass().getSimpleName() + " RHS" ); - } - } - - @Override - public MatchablePathElement getPathElement() { - return pathElement; - } - - @Override - public boolean apply( final String inputKey, final Optional inputOptional, final WalkedPath walkedPath, final Map output, final Map context ) { - if ( output != null ) { - throw new TransformException( "Expected a null output" ); - } - - MatchedElement thisLevel = pathElement.match( inputKey, walkedPath ); - if ( thisLevel == null ) { - return false; - } - - if ( !checkValue ) { // there was no trailing "?" so no check is necessary - applyElement( inputKey, inputOptional, thisLevel, walkedPath, context ); - } - else if ( inputOptional.isPresent() ) { - applyElement( inputKey, inputOptional, thisLevel, walkedPath, context ); - } - return true; - } - - /** - * Templatr specific override that is used in BaseSpec#apply(...) - * The name is changed for easy identification during debugging - */ - protected abstract void applyElement( final String key, final Optional inputOptional, final MatchedElement thisLevel, final WalkedPath walkedPath, final Map context ); - - /** - * Static utility method for facilitating writes on input object - * - * @param parent the source object - * @param matchedElement the current spec (leaf) element that was matched with input - * @param value to write - * @param opMode to determine if write is applicable - */ - @SuppressWarnings( "unchecked" ) - protected static void setData(Object parent, MatchedElement matchedElement, Object value, OpMode opMode) { - if(parent instanceof Map) { - Map source = (Map) parent; - String key = matchedElement.getRawKey(); - if(opMode.isApplicable( source, key )) { - source.put( key, value ); - } - } - else if (parent instanceof List && matchedElement instanceof ArrayMatchedElement ) { - List source = (List) parent; - int origSize = ( (ArrayMatchedElement) matchedElement ).getOrigSize(); - int reqIndex = ( (ArrayMatchedElement) matchedElement ).getRawIndex(); - if(opMode.isApplicable( source, reqIndex, origSize )) { - source.set( reqIndex, value ); - } - } - else { - throw new RuntimeException( "Should not come here!" ); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrCompositeSpec.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrCompositeSpec.java deleted file mode 100644 index 23eb6621..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrCompositeSpec.java +++ /dev/null @@ -1,178 +0,0 @@ -/* -* Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.removr.spec; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarPathElement; -import com.bazaarvoice.jolt.exception.SpecException; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/* - Sample Spec - "spec": { - "ineedtoberemoved":"" //literal leaf element - "TAG-*$*": "", //Leaf Computed element - "TAG-*#*": "", - - "*pants*" : "", - - "buckets": { //composite literal Path element - "a$*": "" //Computed Leaf element - }, - "rating*":{ //composite computed path element - "*":{ //composite computed path element - "a":"" //literal leaf element - } - } - } -*/ - -/** - * Removr Spec that has children. In a removr spec, whenever the RHS is a Map, we build a RemovrCompositeSpec - */ -public class RemovrCompositeSpec extends RemovrSpec { - - private final List allChildNodes; - - public RemovrCompositeSpec(String rawKey, Map spec ) { - super( rawKey ); - List all = new ArrayList<>(); - - for ( String rawLhsStr : spec.keySet() ) { - Object rawRhs = spec.get( rawLhsStr ); - String[] keyStrings = rawLhsStr.split( "\\|" ); - for ( String keyString : keyStrings ) { - RemovrSpec childSpec; - if( rawRhs instanceof Map ) { - childSpec = new RemovrCompositeSpec(keyString, (Map) rawRhs ); - } - else if (rawRhs instanceof String && ((String)rawRhs).trim().length() == 0) { - childSpec = new RemovrLeafSpec(keyString); - } - else{ - throw new SpecException("Invalid Removr spec RHS. Should be an empty string or Map"); - } - all.add(childSpec); - } - } - allChildNodes = Collections.unmodifiableList( all ); - } - - @Override - public List applyToMap( Map inputMap ) { - - if ( pathElement instanceof LiteralPathElement ) { - Object subInput = inputMap.get( pathElement.getRawKey() ); - processChildren( allChildNodes, subInput ); - } - else if ( pathElement instanceof StarPathElement ) { - - StarPathElement star = (StarPathElement) pathElement; - - // Compare my pathElement with each key from the input. - // If it matches, recursively call process the child nodes. - for( Map.Entry entry : inputMap.entrySet() ) { - - if ( star.stringMatch( entry.getKey() ) ) { - processChildren( allChildNodes, entry.getValue() ); - } - } - } - - // Composite Nodes always return an empty list, as they dont actually remove anything. - return Collections.emptyList(); - } - - @Override - public List applyToList( List inputList ) { - - // IF the input is a List, the only thing that will match is a Literal or a "*" - if ( pathElement instanceof LiteralPathElement ) { - - Integer pathElementInt = getNonNegativeIntegerFromLiteralPathElement(); - - if ( pathElementInt != null && pathElementInt < inputList.size() ) { - Object subObj = inputList.get( pathElementInt ); - processChildren( allChildNodes, subObj ); - } - } - else if ( pathElement instanceof StarAllPathElement ) { - for( Object entry : inputList ) { - processChildren( allChildNodes, entry ); - } - } - - // Composite Nodes always return an empty list, as they dont actually remove anything. - return Collections.emptyList(); - } - - /** - * Call our child nodes, build up the set of keys or indices to actually remove, and then - * remove them. - */ - private void processChildren( List children, Object subInput ) { - - if (subInput != null ) { - - if( subInput instanceof List ) { - - List subList = (List) subInput; - Set indiciesToRemove = new HashSet<>(); - - // build a list of all indicies to remove - for(RemovrSpec childSpec : children) { - indiciesToRemove.addAll( childSpec.applyToList( subList ) ); - } - - List uniqueIndiciesToRemove = new ArrayList<>( indiciesToRemove ); - // Sort the list from Biggest to Smallest, so that when we remove items from the input - // list we don't muck up the order. - // Aka removing 0 _then_ 3 would be bad, because we would have actually removed - // 0 and 4 from the "original" list. - Collections.sort( uniqueIndiciesToRemove, new Comparator() { - @Override - public int compare( Integer o1, Integer o2 ) { - return o2.compareTo( o1 ); - } - } ); - - for ( int index : uniqueIndiciesToRemove ) { - subList.remove( index ); - } - } - else if (subInput instanceof Map ) { - - Map subInputMap = (Map) subInput; - - List keysToRemove = new LinkedList<>(); - - for(RemovrSpec childSpec : children) { - keysToRemove.addAll( childSpec.applyToMap( subInputMap ) ); - } - - subInputMap.keySet().removeAll( keysToRemove ); - } - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java deleted file mode 100644 index 097509df..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversr.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.shiftr; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.SimpleTraversr; -import com.bazaarvoice.jolt.traversr.traversal.TraversalStep; - -import java.util.ArrayList; -import java.util.List; - -/** - * Traverser that does not overwrite data. - */ -public class ShiftrTraversr extends SimpleTraversr { - - public ShiftrTraversr( String humanPath ) { - super( humanPath ); - } - - public ShiftrTraversr( List paths ) { - super( paths ); - } - - /** - * Do a Shift style insert : - * 1) if there is no data "there", then just set it - * 2) if there is already a list "there", just add the data to the list - * 3) if there something other than a list there, grab it and stuff it and the data into a list - * and overwrite what is there with a list. - */ - public Optional handleFinalSet( TraversalStep traversalStep, Object tree, String key, DataType data ) { - - Optional optSub = traversalStep.get( tree, key ); - - if ( !optSub.isPresent() || optSub.get() == null ) { - // nothing is here so just set the data - traversalStep.overwriteSet( tree, key, data ); - } - else if ( optSub.get() instanceof List ) { - // there is a list here, so we just add to it - ((List) optSub.get()).add( data ); - } - else { - // take whatever is there and make it the first element in an Array - List temp = new ArrayList<>(); - temp.add( optSub.get() ); - temp.add( data ); - - traversalStep.overwriteSet( tree, key, temp ); - } - - return Optional.of( data ); - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/ArrayTraversalStep.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/ArrayTraversalStep.java deleted file mode 100644 index 3a5973cb..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/ArrayTraversalStep.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.traversr.traversal; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.Traversr; - -import java.util.ArrayList; -import java.util.List; - -/** - * TraversalStep that expects to handle List objects. - */ -public class ArrayTraversalStep extends BaseTraversalStep, DataType> { - - public ArrayTraversalStep( Traversr traversr, TraversalStep child ) { - super( traversr, child ); - } - - public Class getStepType() { - return List.class; - } - - public List newContainer() { - return new ArrayList<>(); - } - - @Override - public Optional get( List list, String key ) { - - int arrayIndex = Integer.parseInt( key ); - if ( arrayIndex < list.size() ) { - return Optional.of( (DataType) list.get( arrayIndex ) ); - } - - return Optional.empty(); - } - - @Override - public Optional remove( List list, String key ) { - - int arrayIndex = Integer.parseInt( key ); - if ( arrayIndex < list.size() ) { - return Optional.of( (DataType) list.remove( arrayIndex ) ); - } - - return Optional.empty(); - } - - @Override - public Optional overwriteSet( List list, String key, DataType data ) { - - int arrayIndex = Integer.parseInt( key ); - ensureArraySize( list, arrayIndex ); // make sure it is big enough - list.set( arrayIndex, data ); - return Optional.of( data ); - } - - private static void ensureArraySize( List list, Integer upperIndex ) { - for ( int sizing = list.size(); sizing <= upperIndex; sizing++ ) { - list.add( null ); - } - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/AutoExpandArrayTraversalStep.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/AutoExpandArrayTraversalStep.java deleted file mode 100644 index 0ca95c62..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/AutoExpandArrayTraversalStep.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.traversr.traversal; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.Traversr; -import com.bazaarvoice.jolt.traversr.TraversrException; - -import java.util.List; - -/** - * Subclass of ArrayTraversalStep that does not care about array index numbers. - * Instead it will just do an array add on any set. - * - * Consequently, get and remove are rather meaningless. - * - * This exists, because we need a way in the human readable path, so say that we - * always want a list value. - * - * Example : "tuna.marlin.[]" - * We want the value of marlin to always be a list, and anytime we set data - * to marlin, it should just be added to the list. - */ -public class AutoExpandArrayTraversalStep extends ArrayTraversalStep { - - public AutoExpandArrayTraversalStep( Traversr traversr, TraversalStep child ) { - super( traversr, child ); - } - - @Override - public Optional get( List list, String key ) { - - if ( ! "[]".equals( key ) ) { - throw new TraversrException( "AutoExpandArrayTraversal expects a '[]' key. Was: " + key ); - } - - return Optional.empty(); - } - - @Override - public Optional remove( List list, String key ) { - - if ( ! "[]".equals( key ) ) { - throw new TraversrException( "AutoExpandArrayTraversal expects a '[]' key. Was: " + key ); - } - - return Optional.empty(); - } - - @Override - public Optional overwriteSet( List list, String key, DataType data ) { - - if ( ! "[]".equals( key ) ) { - throw new TraversrException( "AutoExpandArrayTraversal expects a '[]' key. Was: " + key ); - } - - list.add( data ); - return Optional.of( data ); - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java b/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java deleted file mode 100644 index 831db94a..00000000 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/BaseTraversalStep.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.traversr.traversal; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.Traversr; - -import java.util.Iterator; - - -public abstract class BaseTraversalStep implements TraversalStep { - - protected final TraversalStep child; - protected final Traversr traversr; - - public BaseTraversalStep( Traversr traversr, TraversalStep child ) { - this.traversr = traversr; - this.child = child; - } - - public TraversalStep getChild() { - return child; - } - - public final Optional traverse( StepType tree, Operation op, Iterator keys, DataType data ) { - - if ( tree == null ) { - return Optional.empty(); - } - - if ( getStepType().isAssignableFrom( tree.getClass() ) ) { - - String key = keys.next(); - - if ( child == null ) { - // End of the Traversal so do the set or get - switch (op) { - case GET : - return this.get( tree, key ); - case SET : - return (Optional) traversr.handleFinalSet( this, tree, key, data ); - case REMOVE: - return this.remove( tree, key ); - default : - throw new IllegalStateException( "Invalid op:" + op.toString() ); - } - } - else { - - // We just an intermediate step, so traverse and then hand over control to our child - Optional optSub = traversr.handleIntermediateGet( this, tree, key, op ); - - if ( optSub.isPresent() ) { - return child.traverse( optSub.get(), op, keys, data ); - } - } - } - - return Optional.empty(); - } -} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/CardinalityTransform.java b/jolt-core/src/main/java/io/joltcommunity/jolt/CardinalityTransform.java similarity index 66% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/CardinalityTransform.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/CardinalityTransform.java index 02945dce..3a043477 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/CardinalityTransform.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/CardinalityTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,31 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import com.bazaarvoice.jolt.cardinality.CardinalityCompositeSpec; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.cardinality.CardinalityCompositeSpec; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.Map; /** - * * The CardinalityTransform changes the cardinality of input JSON data elements. * The impetus for the CardinalityTransform, was to deal with data sources that are inconsistent with - * respect to the cardinality of their returned data. - * - * For example, say you know that there will be a "photos" element in a document. If your underlying data - * source is trying to be nice, it may adjust the "type" of the photos element, depending on how many - * photos there actually are. - * + * respect to the cardinality of their returned data. + *

+ * For example, say you know that there will be a "photos" element in a document. If your underlying data + * source is trying to be nice, it may adjust the "type" of the photos element, depending on how many + * photos there actually are. + *

* Single photo : *

  *     "photos" : { "url" : "pants.com/1.jpg" }  // photos element is a "single" map entry
  * 
- * + *

* Or multiple photos : *

  *     "photos" : [
@@ -45,17 +45,17 @@
  *        { "url" : "pants.com/2.jpg" }
  *     ]
  * 
- * + *

* The Shiftr and Defaultr transforms can't handle that variability, so the CardinalityTransform was - * created to "fix" document, so that the rest of the transforms can _assume_ "photos" will be an Array. - * - * + * created to "fix" document, so that the rest of the transforms can _assume_ "photos" will be an Array. + *

+ *

* At a base level, a single Cardinality "command" maps data into a "ONE" or "MANY" state. - * + *

* The idea is that you can start with a copy your JSON input and modify it into a Cardinality spec by - * specifying a "cardinality" for each piece of data that you care about changing in the output. + * specifying a "cardinality" for each piece of data that you care about changing in the output. * Input data that are not called out in the spec will remain in the output unchanged. - * + *

* For example, given this simple input JSON : *

  * {
@@ -80,32 +80,32 @@
  *   }
  * }
  * 
- * + *

* In this case, we turn the array "[ 5, 4 ]" into a single value by pulling the first index of the array. * Hence, the output has "rating : 5". - * + *

* Valid Cardinality Values (RHS : right hand side) - * + *

* 'ONE' - * If the input value is a List, grab the first element in that list, and set it as the data for that element - * For all other input value types, no-op. - * + * If the input value is a List, grab the first element in that list, and set it as the data for that element + * For all other input value types, no-op. + *

* 'MANY' - * If the input is not a List, make a list and set the first element to be the input value. - * If the input is "null", make it be an empty list. - * If the input is a list, no-op - * - * + * If the input is not a List, make a list and set the first element to be the input value. + * If the input is "null", make it be an empty list. + * If the input is a list, no-op + *

+ *

* Cardinality Wildcards - * + *

* As shown above, Cardinality specs can be entirely made up of literal string values, but wildcards similar * to some of those used by Shiftr can be used. - * + *

* '*' Wildcard - * Valid only on the LHS ( input JSON keys ) side of a Cardinality Spec - * Unlike shiftr, the '*' wildcard can only be used by itself. It can be used - * achieve a for/each manner of processing input. - * + * Valid only on the LHS ( input JSON keys ) side of a Cardinality Spec + * Unlike shiftr, the '*' wildcard can only be used by itself. It can be used + * achieve a for/each manner of processing input. + *

* Let's say we have the following input : *

  * {
@@ -146,11 +146,11 @@
  *   ]
  * }
  * 
- * + *

* '@' Wildcard - * Valid only on the LHS of the spec. - * This wildcard should be used when content nested within modified content needs to be modified as well. - * + * Valid only on the LHS of the spec. + * This wildcard should be used when content nested within modified content needs to be modified as well. + *

* Let's say we have the following input: *

  * {
@@ -177,8 +177,8 @@
  *   }
  * }
  * 
- * - * + *

+ *

* Cardinality Logic Table * *

@@ -197,25 +197,24 @@
  */
 public class CardinalityTransform implements SpecDriven, Transform {
 
-    protected static final String ROOT_KEY = "root";
     private final CardinalityCompositeSpec rootSpec;
 
     /**
      * Initialize a Cardinality transform with a CardinalityCompositeSpec.
      *
-     * @throws com.bazaarvoice.jolt.exception.SpecException for a malformed spec
+     * @throws io.joltcommunity.jolt.exception.SpecException for a malformed spec
      */
     @Inject
-    public CardinalityTransform( Object spec ) {
+    public CardinalityTransform(Object spec) {
 
-        if ( spec == null ){
-            throw new SpecException( "CardinalityTransform expected a spec of Map type, got 'null'." );
+        if (spec == null) {
+            throw new SpecException("CardinalityTransform expected a spec of Map type, got 'null'.");
         }
-        if ( ! ( spec instanceof Map) ) {
-            throw new SpecException( "CardinalityTransform expected a spec of Map type, got " + spec.getClass().getSimpleName() );
+        if (!(spec instanceof Map)) {
+            throw new SpecException("CardinalityTransform expected a spec of Map type, got " + spec.getClass().getSimpleName());
         }
 
-        rootSpec = new CardinalityCompositeSpec( ROOT_KEY, (Map) spec );
+        rootSpec = new CardinalityCompositeSpec(ROOT_KEY, (Map) spec);
     }
 
 
@@ -224,13 +223,13 @@ public CardinalityTransform( Object spec ) {
      *
      * @param input the JSON object to transform
      * @return the output object with data shifted to it
-     * @throws com.bazaarvoice.jolt.exception.TransformException for a malformed spec or if there are issues during
-     * the transform
+     * @throws io.joltcommunity.jolt.exception.TransformException for a malformed spec or if there are issues during
+     *                                                           the transform
      */
     @Override
-    public Object transform( Object input ) {
+    public Object transform(Object input) {
 
-        rootSpec.apply( ROOT_KEY, Optional.of( input ), new WalkedPath(), null, null );
+        rootSpec.apply(ROOT_KEY, Optional.of(input), new WalkedPath(), null, null);
 
         return input;
     }
diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Chainr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Chainr.java
similarity index 51%
rename from jolt-core/src/main/java/com/bazaarvoice/jolt/Chainr.java
rename to jolt-core/src/main/java/io/joltcommunity/jolt/Chainr.java
index 0aaca45b..b4df94a6 100644
--- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Chainr.java
+++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Chainr.java
@@ -1,5 +1,6 @@
 /*
- * Copyright 2013 Bazaarvoice, Inc.
+ * Copyright 2013-2023 Bazaarvoice, Inc.
+ * Copyright 2025 Jolt Community
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -13,71 +14,71 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.bazaarvoice.jolt;
+package io.joltcommunity.jolt;
 
-import com.bazaarvoice.jolt.chainr.ChainrBuilder;
-import com.bazaarvoice.jolt.chainr.instantiator.ChainrInstantiator;
-import com.bazaarvoice.jolt.exception.SpecException;
-import com.bazaarvoice.jolt.exception.TransformException;
+import io.joltcommunity.jolt.chainr.ChainrBuilder;
+import io.joltcommunity.jolt.chainr.instantiator.ChainrInstantiator;
+import io.joltcommunity.jolt.exception.SpecException;
+import io.joltcommunity.jolt.exception.TransformException;
+import io.joltcommunity.jolt.removr.Removr;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 /**
  * Chainr is the JOLT mechanism for chaining {@link JoltTransform}s together. Any of the built-in JOLT
  * transform types can be called directly from Chainr. Any custom-written Java transforms
  * can be adapted in by implementing the {@link Transform} or {@link SpecDriven} interfaces.
- *
+ * 

* A Chainr spec should be an array of objects in order that look like this: - * + *

  * [
- *     {
- *         "operation": "[operation-name]",
- *         // stuff that the specific transform needs go here
- *     },
- *     ...
+ *   {
+ *     "operation": "[operation-name]",
+ *     // stuff that the specific transform needs go here
+ *   },
+ *   ...
  * ]
- *
+ * 
+ *

* Each operation is called in the order that it is specified within the array. The original * input to Chainr is passed into the first operation, with its output passed into the next, * and so on. The output of the final operation is returned from Chainr. - * + *

* Currently, [operation-name] can be any of the following: - * + *

  * - shift: ({@link Shiftr}) a tool for moving parts of an input JSON document to a new output document
  * - default: ({@link Defaultr}) a tool for applying default values to the provided JSON document
  * - remove: ({@link Removr}) a tool for removing specific values from the provided JSON document
  * - sort: ({@link Sortr}) sort the JSON document
  * - java: passes control to whatever Java class you specify as long as it implements the {@link Transform} interface
- *
+ * 
+ *

* Shift, default, and remove operation all require a "spec", while sort does not. - * + *

  * [
- *     {
- *         "operation": "shift",
- *         "spec" : { // shiftr spec }
- *     },
- *     {
- *         "operation": "sort"  // sort does not need a spec
- *     },
- *     ...
+ *   {
+ *     "operation": "shift",
+ *     "spec" : { // shiftr spec }
+ *   },
+ *   {
+ *     "operation": "sort"  // sort does not need a spec
+ *   },
+ *   ...
  * ]
- *
+ * 
+ *

* Custom Java classes that implement {@link Transform} and/or {@link SpecDriven} can be loaded by specifying the full - * className to load. Additionally, if upon reflection of the class we see that it is an instance of a - * {@link SpecDriven}, then we will construct it with a the supplied "spec" object. - * + * className to load. Additionally, if upon reflection of the class we see that it is an instance of a + * {@link SpecDriven}, then we will construct it with a the supplied "spec" object. + *

  * [
- *     {
- *         "operation": "com.bazaarvoice.tuna.CustomTransform",
- *
- *         "spec" : { ... } // optional spec to use to construct a custom {@link Transform} if it has the {@link SpecDriven} marker interface.
- *     },
- *     ...
+ *   {
+ *     "operation": "io.joltcommunity.tuna.CustomTransform",
+ *     "spec" : { ... } // optional spec to use to construct a custom {@link Transform} if it has the {@link SpecDriven} marker interface.
+ *   },
+ *   ...
  * ]
+ * 
*/ public class Chainr implements Transform, ContextualTransform { @@ -88,159 +89,139 @@ public class Chainr implements Transform, ContextualTransform { // The list of actual ContextualTransforms, for clients that specifically care. private final List actualContextualTransforms; - public static Chainr fromSpec( Object input ) { - return new ChainrBuilder( input ).build(); - } + public Chainr(List joltTransforms) { - public static Chainr fromSpec( Object input, ChainrInstantiator instantiator ) { - return new ChainrBuilder( input ).loader( instantiator ).build(); - } - - /** - * Adapt "normal" Transforms to look like ContextualTransforms, so that - * Chainr can just maintain a single list of "JoltTransforms" to run. - */ - private static class ContextualTransformAdapter implements ContextualTransform { - - private final Transform transform; - - private ContextualTransformAdapter( Transform transform ) { - this.transform = transform; - } - - @Override - public Object transform( Object input, Map context ) { - return transform.transform( input ); - } - } - - public Chainr( List joltTransforms ) { - - if ( joltTransforms == null ) { - throw new IllegalArgumentException( "Chainr requires a list of JoltTransforms." ); + if (joltTransforms == null) { + throw new IllegalArgumentException("Chainr requires a list of JoltTransforms."); } - transformsList = new ArrayList<>( joltTransforms.size() ); + transformsList = new ArrayList<>(joltTransforms.size()); List realContextualTransforms = new LinkedList<>(); - for ( JoltTransform joltTransform : joltTransforms ) { + for (JoltTransform joltTransform : joltTransforms) { // Do one pass of "instanceof" checks at construction time, rather than repeatedly at "runtime". boolean isTransform = joltTransform instanceof Transform; boolean isContextual = joltTransform instanceof ContextualTransform; - if ( isContextual && isTransform ) { - throw new SpecException( "JOLT Chainr - JoltTransform className:" + joltTransform.getClass().getCanonicalName() + - " implements both Transform and ContextualTransform, should only implement one of those interfaces." ); + if (isContextual && isTransform) { + throw new SpecException("JOLT Chainr - JoltTransform className:" + joltTransform.getClass().getCanonicalName() + + " implements both Transform and ContextualTransform, should only implement one of those interfaces."); } - if ( ! isContextual && ! isTransform ) { - throw new SpecException( "JOLT Chainr - Transform className:" + joltTransform.getClass().getCanonicalName() + - " should implement Transform or ContextualTransform." ); + if (!isContextual && !isTransform) { + throw new SpecException("JOLT Chainr - Transform className:" + joltTransform.getClass().getCanonicalName() + + " should implement Transform or ContextualTransform."); } // We are optimizing given the assumption that Chainr objects will be built and then reused many times. // We want to have a single list of "transforms" that we can just blindly march through. // In order to accomplish this, we adapt Transforms to look like ContextualTransforms and just maintain // a list of type ContextualTransform. - if ( isContextual ) { - transformsList.add( (ContextualTransform) joltTransform ); - realContextualTransforms.add( (ContextualTransform) joltTransform ); - } - else - { - transformsList.add( new ContextualTransformAdapter( (Transform) joltTransform ) ); + if (isContextual) { + transformsList.add((ContextualTransform) joltTransform); + realContextualTransforms.add((ContextualTransform) joltTransform); + } else { + transformsList.add(new ContextualTransformAdapter((Transform) joltTransform)); } } - actualContextualTransforms = Collections.unmodifiableList( realContextualTransforms ); + actualContextualTransforms = Collections.unmodifiableList(realContextualTransforms); + } + + public static Chainr fromSpec(Object input) { + return new ChainrBuilder(input).build(); + } + + public static Chainr fromSpec(Object input, ChainrInstantiator instantiator) { + return new ChainrBuilder(input).loader(instantiator).build(); + } + + private static Object doTransform(List transforms, Object input, Map context) { + + Object intermediate = input; + for (ContextualTransform transform : transforms) { + intermediate = transform.transform(intermediate, context); + } + + return intermediate; } /** * Runs a series of Transforms on the input, piping the inputs and outputs of the Transforms together. - * + *

* Chainr instances are meant to be immutable once they are created so that they can be * used many times. - * + *

* The notion of passing "context" to the transforms allows chainr instances to be * reused, even in situations were you need to slightly vary. * - * @param input a JSON (Jackson-parsed) maps-of-maps object to transform + * @param input a JSON (Jackson-parsed) maps-of-maps object to transform * @param context optional tweaks that the consumer of the transform would like * @return an object representing the JSON resulting from the transform - * @throws com.bazaarvoice.jolt.exception.TransformException if the specification is malformed, an operation is not - * found, or if one of the specified transforms throws an exception. + * @throws io.joltcommunity.jolt.exception.TransformException if the specification is malformed, an operation is not + * found, or if one of the specified transforms throws an exception. */ @Override - public Object transform( Object input, Map context ) { - return doTransform( transformsList, input, context ); + public Object transform(Object input, Map context) { + return doTransform(transformsList, input, context); } @Override - public Object transform( Object input ) { - return doTransform( transformsList, input, null ); + public Object transform(Object input) { + return doTransform(transformsList, input, null); } /** * Have Chainr run a subset of the transforms in it's spec. - * + *

* Useful for testing and debugging. * * @param input the input data to transform - * @param to transform from the chainrSpec to end with: 0 based index exclusive + * @param to transform from the chainrSpec to end with: 0 based index exclusive */ - public Object transform( int to, Object input ) { - return transform( 0, to, input, null ); + public Object transform(int to, Object input) { + return transform(0, to, input, null); } /** * Useful for testing and debugging. * - * @param input the input data to transform - * @param to transform from the chainrSpec to end with: 0 based index exclusive + * @param input the input data to transform + * @param to transform from the chainrSpec to end with: 0 based index exclusive * @param context optional tweaks that the consumer of the transform would like */ - public Object transform( int to, Object input, Map context ) { - return transform( 0, to, input, context ); + public Object transform(int to, Object input, Map context) { + return transform(0, to, input, context); } /** * Useful for testing and debugging. * * @param input the input data to transform - * @param from transform from the chainrSpec to start with: 0 based index - * @param to transform from the chainrSpec to end with: 0 based index exclusive + * @param from transform from the chainrSpec to start with: 0 based index + * @param to transform from the chainrSpec to end with: 0 based index exclusive */ - public Object transform( int from, int to, Object input ) { - return transform( from, to, input, null ); + public Object transform(int from, int to, Object input) { + return transform(from, to, input, null); } /** * Have Chainr run a subset of the transforms in it's spec. - * + *

* Useful for testing and debugging. * - * @param input the input data to transform - * @param from transform from the chainrSpec to start with: 0 based index - * @param to transform from the chainrSpec to end with: 0 based index exclusive + * @param input the input data to transform + * @param from transform from the chainrSpec to start with: 0 based index + * @param to transform from the chainrSpec to end with: 0 based index exclusive * @param context optional tweaks that the consumer of the transform would like */ - public Object transform( int from, int to, Object input, Map context ) { + public Object transform(int from, int to, Object input, Map context) { - if ( from < 0 || to > transformsList.size() || to <= from ) { - throw new TransformException( "JOLT Chainr : invalid from and to parameters : from=" + from + " to=" + to ); + if (from < 0 || to > transformsList.size() || to <= from) { + throw new TransformException("JOLT Chainr : invalid from and to parameters : from=" + from + " to=" + to); } - return doTransform( transformsList.subList( from, to ), input, context ); - } - - private static Object doTransform( List transforms, Object input, Map context ) { - - Object intermediate = input; - for ( ContextualTransform transform : transforms ) { - intermediate = transform.transform( intermediate, context ); - } - - return intermediate; + return doTransform(transformsList.subList(from, to), input, context); } /** @@ -259,4 +240,16 @@ public boolean hasContextualTransforms() { public List getContextualTransforms() { return actualContextualTransforms; } + + /** + * Adapt "normal" Transforms to look like ContextualTransforms, so that + * Chainr can just maintain a single list of "JoltTransforms" to run. + */ + private record ContextualTransformAdapter(Transform transform) implements ContextualTransform { + + @Override + public Object transform(Object input, Map context) { + return transform.transform(input); + } + } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/ContextualTransform.java b/jolt-core/src/main/java/io/joltcommunity/jolt/ContextualTransform.java similarity index 83% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/ContextualTransform.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/ContextualTransform.java index d8437347..8628c339 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/ContextualTransform.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/ContextualTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import java.util.Map; /** * Interface for Jolt Transforms that can incorporate context information along with the * source input JSON. - * + *

* These Jolt Transforms should be stateless, thus allowing multiple threads to * call the transform method simultaneously. */ @@ -28,24 +29,24 @@ public interface ContextualTransform extends JoltTransform { /** * Execute a transform on some input JSON with optionally provided "context" and return the result. - * + *

* The "context" allows transforms to tweak their behavior based upon criteria outside of the input JSON object. - * + *

* The canonical example for the need to have Transforms consider "context" is a Transform that creates * urls based upon input data. Should it generate "http" or "https" urls? - * + *

* Most likely the input JSON data does not provide any guidance. This is what the "context" is for. * It allows the consumer of the Transform to specialize itself based on data outside the scope of the input JSON. - * + *

* Without the "context" notion you would instead create a HttpUrlTransform and a HttpsUrlTransform. * This creates problems when you want to used them as part of a larger Chainr Transform, as you * would need to create two Chainrs that are almost the same. The number of Chainrs needed grows * exponentially as you add other context sensitive transforms. * - * @param input the JSON object to transform in plain vanilla Jackson Map style + * @param input the JSON object to transform in plain vanilla Jackson Map style * @param context information outside of the input JSON that needs to be taken into account when doing the transform * @return the results of the transformation - * @throws com.bazaarvoice.jolt.exception.TransformException if there are issues with the transform + * @throws io.joltcommunity.jolt.exception.TransformException if there are issues with the transform */ - Object transform( Object input, Map context ); + Object transform(Object input, Map context); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Defaultr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Defaultr.java similarity index 79% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/Defaultr.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/Defaultr.java index 3ce6aa2e..40ca3132 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Defaultr.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Defaultr.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import com.bazaarvoice.jolt.defaultr.Key; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.exception.TransformException; +import io.joltcommunity.jolt.defaultr.Key; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.TransformException; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -27,11 +28,11 @@ /** * Defaultr is a kind of JOLT transform that applies default values in a non-destructive way. - * + *

* For comparison : * Shitr walks the input data and asks its spec "Where should this go?" * Defaultr walks the spec and asks "Does this exist in the data? If not, add it." - * + *

* Example : Given input JSON like *

  * {
@@ -102,16 +103,16 @@
  *   }
  * }
  * 
- * + *

* The Spec file format for Defaulr a tree Map objects. Defaultr handles outputting - * of JSON Arrays via special wildcard in the Spec. - * + * of JSON Arrays via special wildcard in the Spec. + *

* Defaltr Spec WildCards and Flag : * "*" aka STAR : Apply these defaults to all input keys at this level * "|" aka OR : Apply these defaults to input keys, if they exist * "[]" aka : Signal to Defaultr that the data for this key should be an array. - * This means all defaultr keys below this entry have to be "integers". - * + * This means all defaultr keys below this entry have to be "integers". + *

* Valid Array Specification : *

  * {
@@ -123,7 +124,7 @@
  *   }
  * }
  * 
- * + *

* An Invalid Array Specification would be : *

  * {
@@ -135,21 +136,21 @@
  *   }
  * }
  * 
- * + *

* Algorithm * Defaultr walks its Spec in a depth first way. * At each level in the Spec tree, Defaultr, works from most specific to least specific Spec key: - * Literals key values - * "|", sub-sorted by how many or values there, then alphabetically (for deterministic behavior) - * "*" - * + * Literals key values + * "|", sub-sorted by how many or values there, then alphabetically (for deterministic behavior) + * "*" + *

* At a given level in the Defaultr Spec tree, only literal keys force Defaultr to create new entries - * in the input data: either as a single literal value or adding new nested Array or Map objects. + * in the input data: either as a single literal value or adding new nested Array or Map objects. * The wildcard operators, are applied after the literal keys, and will not cause the those keys to be - * added if they are not already present in the input document (either naturally or having been defaulted - * in from literal spec keys). - * - * + * added if they are not already present in the input document (either naturally or having been defaulted + * in from literal spec keys). + *

+ *

  * Algorithm :
  * 1) Walk the spec
  * 2) for each literal key in the spec (specKey)
@@ -160,9 +161,10 @@
  * 3) for each wildcard in the spec
  * 3.1) find all keys from the defaultee that match the wildcard
  * 3.2) treat each key as a literal speckey
- *
+ * 
+ *

* Corner Cases : - * + *

* Due to Defaultr's array syntax, we can't actually express that we expect the top level of the input to be an Array. * The workaround for this is that we check the type of the object that is at the root level of the input. * If it is a map, no problem. @@ -171,12 +173,6 @@ */ public class Defaultr implements SpecDriven, Transform { - public interface WildCards { - public static final String STAR = "*"; - public static final String OR = "|"; - public static final String ARRAY = "[]"; - } - private final Key mapRoot; private final Key arrayRoot; @@ -186,7 +182,7 @@ public interface WildCards { * @throws SpecException for a malformed spec or if there are issues */ @Inject - public Defaultr( Object spec ) { + public Defaultr(Object spec) { String rootString = "root"; @@ -197,19 +193,18 @@ public Defaultr( Object spec ) { { Map rootSpec = new LinkedHashMap<>(); - rootSpec.put( rootString, spec ); - mapRoot = Key.parseSpec( rootSpec ).iterator().next(); + rootSpec.put(rootString, spec); + mapRoot = Key.parseSpec(rootSpec).iterator().next(); } // Thus we check the top level type of the input. { Map rootSpec = new LinkedHashMap<>(); - rootSpec.put( rootString + WildCards.ARRAY, spec ); + rootSpec.put(rootString + WildCards.ARRAY, spec); Key tempKey = null; try { - tempKey = Key.parseSpec( rootSpec ).iterator().next(); - } - catch ( NumberFormatException nfe ) { + tempKey = Key.parseSpec(rootSpec).iterator().next(); + } catch (NumberFormatException nfe) { // this is fine, it means the top level spec has non numeric keys // if someone passes a top level array as input later we will error then } @@ -224,24 +219,33 @@ public Defaultr( Object spec ) { * @return the modified input */ @Override - public Object transform( Object input ) { + public Object transform(Object input) { - if ( input == null ) { + if (input == null) { // if null, assume HashMap - input = new HashMap(); + input = new HashMap<>(); } // TODO : Make copy of the defaultee or like shiftr create a new output object - if ( input instanceof List ) { - if ( arrayRoot == null ) { - throw new TransformException( "The Spec provided can not handle input that is a top level Json Array." ); + if (input instanceof List) { + if (arrayRoot == null) { + throw new TransformException("The Spec provided can not handle input that is a top level Json Array."); } - arrayRoot.applyChildren( input ); - } - else { - mapRoot.applyChildren( input ); + arrayRoot.applyChildren(input); + } else { + mapRoot.applyChildren(input); } return input; } + + public static final class WildCards { + public static final String STAR = "*"; + public static final String OR = "|"; + public static final String ARRAY = "[]"; + + private WildCards() { + // Prevent instantiation + } + } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/Enrichr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Enrichr.java new file mode 100644 index 00000000..cd4d3081 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Enrichr.java @@ -0,0 +1,135 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.enrich.EnrichrExecutionMode; +import io.joltcommunity.jolt.enrich.EnrichrManager; +import io.joltcommunity.jolt.enrich.EnrichrPendingEnrichment; +import io.joltcommunity.jolt.exception.SpecException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Enrich fields in a JSON document by invoking user supplied Java methods or context supplied beans. + * + * Spec shape: + * + * { + * "executionMode" : "sync", // optional, defaults to sync. "async" runs all enrichments concurrently. + * "enrichments" : [ + * { + * "path" : "customer.id", + * "className" : "com.acme.CustomerLookup", + * "contextKey" : "customerLookup", // optional alternative to className, resolved from transform context + * "method" : "enrich", + * "outputPath" : "customer.details" // optional, defaults to path + * } + * ] + * } + * + * Supported method signatures are: + * - Object method( Object fieldValue ) + * - Object method( Object fieldValue, Object input ) + * - Object method( Object fieldValue, Object input, Map context ) + * + * Supported return types are: + * - Object + * - CompletionStage + * - Publisher such as a Reactor Mono returned by Spring WebFlux WebClient + * + * When className is used, methods may be static or instance methods with a public no-arg constructor. + * When contextKey is used, the target instance is pulled from the supplied transform context. + */ +public class Enrichr implements SpecDriven, ContextualTransform { + + private static final String ENRICHMENTS_KEY = "enrichments"; + private static final String EXECUTION_MODE_KEY = "executionMode"; + private final List enrichments; + private final EnrichrExecutionMode executionMode; + + /** + * Build an enrich transform from the supplied spec. + * + * @param spec enrich spec containing {@code executionMode} and {@code enrichments} + */ + @SuppressWarnings( "unchecked" ) + public Enrichr( Object spec ) { + if ( spec == null ) { + throw new SpecException( "Enrichr expected a spec of Map type, got 'null'." ); + } + if ( ! ( spec instanceof Map ) ) { + throw new SpecException( "Enrichr expected a spec of Map type, got " + spec.getClass().getSimpleName() ); + } + + Map enrichrSpec = (Map) spec; + executionMode = EnrichrExecutionMode.fromSpec( enrichrSpec.get( EXECUTION_MODE_KEY ) ); + + Object enrichmentsObj = enrichrSpec.get( ENRICHMENTS_KEY ); + if ( ! ( enrichmentsObj instanceof List ) ) { + throw new SpecException( "Enrichr expected '" + ENRICHMENTS_KEY + "' to be a List." ); + } + + List parsed = new ArrayList<>(); + List specs = (List) enrichmentsObj; + for ( int i = 0; i < specs.size(); i++ ) { + parsed.add( new EnrichrManager( specs.get( i ), i ) ); + } + if ( parsed.isEmpty() ) { + throw new SpecException( "Enrichr requires at least one enrichment rule." ); + } + + enrichments = Collections.unmodifiableList( parsed ); + } + + /** + * Apply all configured enrichment rules to the supplied input document. + *

+ * In {@code sync} mode each matched enrichment is resolved and applied before the next one starts. + * In {@code async} mode all matching enrichments are started first and their results are written back + * after every invocation has been scheduled. + * + * @param input document being transformed + * @param context optional runtime context used to resolve {@code contextKey} targets and provide + * method arguments + * @return the same mutated input document instance + */ + @Override + public Object transform( Object input, Map context ) { + if ( executionMode == EnrichrExecutionMode.ASYNC ) { + List pendingEnrichments = new ArrayList<>(); + for ( EnrichrManager enrichment : enrichments ) { + for ( io.joltcommunity.jolt.enrich.EnrichrPathMatch inputMatch : enrichment.match( input ) ) { + pendingEnrichments.add( enrichment.prepare( inputMatch, input, context ) ); + } + } + + for ( EnrichrPendingEnrichment pendingEnrichment : pendingEnrichments ) { + pendingEnrichment.apply(); + } + return input; + } + + for ( EnrichrManager enrichment : enrichments ) { + for ( io.joltcommunity.jolt.enrich.EnrichrPathMatch inputMatch : enrichment.match( input ) ) { + enrichment.prepare( inputMatch, input, context ).apply(); + } + } + return input; + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/JoltTransform.java b/jolt-core/src/main/java/io/joltcommunity/jolt/JoltTransform.java similarity index 89% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/JoltTransform.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/JoltTransform.java index d976d3e2..43cdf42a 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/JoltTransform.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/JoltTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; /** * Marker interface for all Jolt Transforms. - * + *

* Jolt Transforms should not actually implement this interface. Instead they should * implement either the Transform interface or the ContextualTransform interface. - * + *

* This interface exists because the Transform and ContextualTransform interfaces do not * share any methods, but we need a need a way to flag a class being a JoltTransform. */ diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/Modifier.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Modifier.java new file mode 100644 index 00000000..bc75eed5 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Modifier.java @@ -0,0 +1,178 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.modifier.OpMode; +import io.joltcommunity.jolt.modifier.ModifierSpecBuilder; +import io.joltcommunity.jolt.modifier.function.*; +import io.joltcommunity.jolt.modifier.function.Math; +import io.joltcommunity.jolt.modifier.spec.ModifierCompositeSpec; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Base Modifier transform that to behave differently based on provided opMode + */ +public abstract class Modifier implements SpecDriven, ContextualTransform { + + private static final Map STOCK_FUNCTIONS = new HashMap<>(); + + static { + STOCK_FUNCTIONS.put("toLower", new Strings.toLowerCase()); + STOCK_FUNCTIONS.put("toUpper", new Strings.toUpperCase()); + STOCK_FUNCTIONS.put("concat", new Strings.concat()); + STOCK_FUNCTIONS.put("join", new Strings.join()); + STOCK_FUNCTIONS.put("split", new Strings.split()); + STOCK_FUNCTIONS.put("substring", new Strings.substring()); + STOCK_FUNCTIONS.put("trim", new Strings.trim()); + STOCK_FUNCTIONS.put("leftPad", new Strings.leftPad()); + STOCK_FUNCTIONS.put("rightPad", new Strings.rightPad()); + STOCK_FUNCTIONS.put("replace", new Strings.replace()); + STOCK_FUNCTIONS.put("replaceAll", new Strings.replaceAll()); + + STOCK_FUNCTIONS.put("min", new Math.min()); + STOCK_FUNCTIONS.put("max", new Math.max()); + STOCK_FUNCTIONS.put("abs", new Math.abs()); + STOCK_FUNCTIONS.put("avg", new Math.avg()); + STOCK_FUNCTIONS.put("intSum", new Math.intSum()); + STOCK_FUNCTIONS.put("doubleSum", new Math.doubleSum()); + STOCK_FUNCTIONS.put("longSum", new Math.longSum()); + STOCK_FUNCTIONS.put("intSubtract", new Math.intSubtract()); + STOCK_FUNCTIONS.put("doubleSubtract", new Math.doubleSubtract()); + STOCK_FUNCTIONS.put("longSubtract", new Math.longSubtract()); + STOCK_FUNCTIONS.put("divide", new Math.divide()); + STOCK_FUNCTIONS.put("divideAndRound", new Math.divideAndRound()); + STOCK_FUNCTIONS.put("multiply", new Math.multiply()); + STOCK_FUNCTIONS.put("multiplyAndRound", new Math.multiplyAndRound()); + + STOCK_FUNCTIONS.put("toInteger", new Objects.toInteger()); + STOCK_FUNCTIONS.put("toDouble", new Objects.toDouble()); + STOCK_FUNCTIONS.put("toLong", new Objects.toLong()); + STOCK_FUNCTIONS.put("toBoolean", new Objects.toBoolean()); + STOCK_FUNCTIONS.put("toString", new Objects.toString()); + STOCK_FUNCTIONS.put("size", new Objects.size()); + + STOCK_FUNCTIONS.put("squashNulls", new Objects.squashNulls()); + STOCK_FUNCTIONS.put("recursivelySquashNulls", new Objects.recursivelySquashNulls()); + STOCK_FUNCTIONS.put("squashDuplicates", new Objects.squashDuplicates()); + + STOCK_FUNCTIONS.put("noop", Function.noop); + STOCK_FUNCTIONS.put("isPresent", Function.isPresent); + STOCK_FUNCTIONS.put("notNull", Function.notNull); + STOCK_FUNCTIONS.put("isNull", Function.isNull); + STOCK_FUNCTIONS.put("uuid", Function.uuid); + + STOCK_FUNCTIONS.put("firstElement", new Lists.firstElement()); + STOCK_FUNCTIONS.put("lastElement", new Lists.lastElement()); + STOCK_FUNCTIONS.put("elementAt", new Lists.elementAt()); + STOCK_FUNCTIONS.put("toList", new Lists.toList()); + STOCK_FUNCTIONS.put("sort", new Lists.sort()); + + STOCK_FUNCTIONS.put("fromEpochMilli", new Dates.fromEpochMilli()); + STOCK_FUNCTIONS.put("toEpochMilli", new Dates.toEpochMilli()); + STOCK_FUNCTIONS.put("now", new Dates.now()); + STOCK_FUNCTIONS.put("nowEpochMillis", Dates.now); + STOCK_FUNCTIONS.put("dateAdd", new Dates.dateAdd()); + STOCK_FUNCTIONS.put("dateSubstract", new Dates.dateSubstract()); + STOCK_FUNCTIONS.put("formatDate", new Dates.formatDate()); + + } + + private final ModifierCompositeSpec rootSpec; + + @SuppressWarnings("unchecked") + private Modifier(Object spec, OpMode opMode, Map functionsMap) { + if (spec == null) { + throw new SpecException(opMode.name() + " expected a spec of Map type, got 'null'."); + } + if (!(spec instanceof Map)) { + throw new SpecException(opMode.name() + " expected a spec of Map type, got " + spec.getClass().getSimpleName()); + } + + if (functionsMap == null || functionsMap.isEmpty()) { + throw new SpecException(opMode.name() + " expected a populated functions' map type, got " + (functionsMap == null ? "null" : "empty")); + } + + functionsMap = Collections.unmodifiableMap(functionsMap); + ModifierSpecBuilder modifierSpecBuilder = new ModifierSpecBuilder(opMode, functionsMap); + rootSpec = new ModifierCompositeSpec(ROOT_KEY, (Map) spec, opMode, modifierSpecBuilder); + } + + @Override + public Object transform(final Object input, final Map context) { + + Map contextWrapper = new HashMap<>(); + contextWrapper.put(ROOT_KEY, context); + + MatchedElement rootLpe = new MatchedElement(ROOT_KEY); + WalkedPath walkedPath = new WalkedPath(); + walkedPath.add(input, rootLpe); + + rootSpec.apply(ROOT_KEY, Optional.of(input), walkedPath, null, contextWrapper); + return input; + } + + /** + * This variant of modifier creates the key/index is missing, + * and overwrites the value if present + */ + public static final class Overwritr extends Modifier { + + public Overwritr(Object spec) { + this(spec, STOCK_FUNCTIONS); + } + + public Overwritr(Object spec, Map functionsMap) { + super(spec, OpMode.OVERWRITR, functionsMap); + } + } + + /** + * This variant of modifier only writes when the key/index is missing + */ + public static final class Definr extends Modifier { + + public Definr(final Object spec) { + this(spec, STOCK_FUNCTIONS); + } + + public Definr(Object spec, Map functionsMap) { + super(spec, OpMode.DEFINER, functionsMap); + } + } + + /** + * This variant of modifier only writes when the key/index is missing or the value is null + */ + public static class Defaultr extends Modifier { + + public Defaultr(final Object spec) { + this(spec, STOCK_FUNCTIONS); + } + + public Defaultr(Object spec, Map functionsMap) { + super(spec, OpMode.DEFAULTR, functionsMap); + } + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Shiftr.java similarity index 58% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/Shiftr.java index 8e2b1e3c..f324b472 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Shiftr.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Shiftr.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,31 +14,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.shiftr.spec.ShiftrCompositeSpec; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.shiftr.spec.ShiftrCompositeSpec; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.HashMap; import java.util.Map; /** - * * Shiftr is a kind of JOLT transform that specifies where "data" from the input JSON should be placed in the * output JSON, aka how the input JSON/data should be shifted around to make the output JSON/data. - * + *

* At a base level, a single Shiftr "command" is a mapping from an input path to an output path, - * similar to the "mv" command in Unix, "mv /var/data/mysql/data /media/backup/mysql". - * + * similar to the "mv" command in Unix, "mv /var/data/mysql/data /media/backup/mysql". + *

* In Shiftr, the input path is a JSON tree structure, and the output path is flattened "dot notation" path notation. - * + *

* The idea is that you can start with a copy of your JSON input data and modify it into a Shiftr spec by - * supplying a "dot notation" output path for each piece of data that you care about. - * + * supplying a "dot notation" output path for each piece of data that you care about. + *

* For example, given this simple input JSON: *

  * {
@@ -70,11 +70,11 @@
  *   }
  * }
  * 
- * + *

* As shown above, Shiftr specs can be entirely made up of literal string values, but its real power comes from its wildcards. * Using wildcards, you can leverage the fact that you know, not just the data and its immediate key, but the whole input - * path to that data. - * + * path to that data. + *

* Expanding the example above, say we have the following expanded Input JSON: *

  * {
@@ -139,18 +139,18 @@
  *   }
  * }
  * 
- * - * + *

+ *

* Shiftr Wildcards - * + *

* '*' Wildcard - * Valid only on the LHS ( input JSON keys ) side of a Shiftr Spec - * The '*' wildcard can be used by itself or to match part of a key. - * - * '*' wildcard by itself: - * As illustrated in the example above, the '*' wildcard by itself is useful for "templating" JSON maps, - * where each key / value has the same "format". - *

+ * Valid only on the LHS ( input JSON keys ) side of a Shiftr Spec
+ * The '*' wildcard can be used by itself or to match part of a key.
+ * 

+ * '*' wildcard by itself: + * As illustrated in the example above, the '*' wildcard by itself is useful for "templating" JSON maps, + * where each key / value has the same "format". + *

  *    // example input
  *    {
  *      "rating" : {
@@ -164,38 +164,38 @@
  *        }
  *    }
  *    
- * In this example, "rating.quality" and "rating.sharpness" both have the same structure/format, and thus we can use the '*' - * to allow us to write more compact rules and avoid having to explicitly write very similar rules for both "quality" and "sharpness". - * - * '*' wildcard as part of a key: - * This is useful for working with input JSON with keys that are "prefixed". - * Ex: if you had an input document like - *
+ * In this example, "rating.quality" and "rating.sharpness" both have the same structure/format, and thus we can use the '*'
+ * to allow us to write more compact rules and avoid having to explicitly write very similar rules for both "quality" and "sharpness".
+ * 

+ * '*' wildcard as part of a key: + * This is useful for working with input JSON with keys that are "prefixed". + * Ex: if you had an input document like + *

  *    {
  *       "tag-Pro": "Awesome",
  *       "tag-Con": "Bogus"
  *    }
  *    
- * A 'tag-*' would match both keys, and make the whole key and "matched" part of the key available. - * Ex, input key of "tag-Pro" with LHS spec "tag-*", would "tag-Pro" and "Pro" available to reference. - * Note the '*' wildcard is as non-greedy as possible, hence you can use more than one '*' in a key. - * For example, "tag-*-*" would match "tag-Foo-Bar", making "tag-Foo-Bar", "Foo", and "Bar" all available to reference. - * + * A 'tag-*' would match both keys, and make the whole key and "matched" part of the key available. + * Ex, input key of "tag-Pro" with LHS spec "tag-*", would "tag-Pro" and "Pro" available to reference. + * Note the '*' wildcard is as non-greedy as possible, hence you can use more than one '*' in a key. + * For example, "tag-*-*" would match "tag-Foo-Bar", making "tag-Foo-Bar", "Foo", and "Bar" all available to reference. + *

* '&' Wildcard - * Valid on the LHS (left hand side - input JSON keys) and RHS (output data path) - * Means, dereference against a "path" to get a value and use that value as if were a literal key. - * The canonical form of the wildcard is "&(0,0)". - * The first parameter is where in the input path to look for a value, and the second parameter is which part of the key to use (used with * key). - * There are syntactic sugar versions of the wildcard, all of the following mean the same thing. - * Sugar : '&' = '&0' = '&(0)' = '&(0,0) - * The syntactic sugar versions are nice, as there are a set of data transforms that do not need to use the canonical form, - * eg if your input data does not have any "prefixed" keys. - * - * '&' Path lookup - * As Shiftr processes data and walks down the spec, it maintains a data structure describing the path it has walked. - * The '&' wildcard can access data from that path in a 0 major, upward oriented way. - * Example: - *

+ * Valid on the LHS (left hand side - input JSON keys) and RHS (output data path)
+ * Means, dereference against a "path" to get a value and use that value as if were a literal key.
+ * The canonical form of the wildcard is "&(0,0)".
+ * The first parameter is where in the input path to look for a value, and the second parameter is which part of the key to use (used with * key).
+ * There are syntactic sugar versions of the wildcard, all of the following mean the same thing.
+ * Sugar : '&' = '&0' = '&(0)' = '&(0,0)
+ * The syntactic sugar versions are nice, as there are a set of data transforms that do not need to use the canonical form,
+ * eg if your input data does not have any "prefixed" keys.
+ * 

+ * '&' Path lookup + * As Shiftr processes data and walks down the spec, it maintains a data structure describing the path it has walked. + * The '&' wildcard can access data from that path in a 0 major, upward oriented way. + * Example: + *

  *    {
  *        "foo" : {
  *            "bar": {
@@ -204,30 +204,30 @@
  *        }
  *    }
  *    
- * - * '&' Subkey lookup - * '&' subkey lookup allows us to referece the values captured by the '*' wildcard. - * Example, "tag-*-*" would match "tag-Foo-Bar", making - * &(0,0) = "tag-Foo-Bar" - * &(0,1) = "Foo" - * &(0,2) = "Bar" - * + *

+ * '&' Subkey lookup + * '&' subkey lookup allows us to reference the values captured by the '*' wildcard. + * Example, "tag-*-*" would match "tag-Foo-Bar", making + * &(0,0) = "tag-Foo-Bar" + * &(0,1) = "Foo" + * &(0,2) = "Bar" + *

* '$' Wildcard - * Valid only on the LHS of the spec. - * The existence of this wildcard is a reflection of the fact that the "data" of the input JSON, can be both in the "values" - * and the "keys" of the input JSON - * - * The base case operation of Shiftr is to copy input JSON "values", thus we need a way to specify that we want to copy the input JSON "key" instead. - * - * Thus '$' specifies that we want to use an input key, or input key derived value, as the data to be placed in the output JSON. - * '$' has the same syntax as the '&' wildcard, and can be read as, dereference to get a value, and then use that value as the data to be output. - * - * There are two cases where this is useful - * 1) when a "key" in the input JSON needs to be a "id" value in the output JSON, see the ' "$": "SecondaryRatings.&1.Id" ' example above. - * 2) you want to make a list of all the input keys. - * - * Example of "a list of the input keys": - *

+ * Valid only on the LHS of the spec.
+ * The existence of this wildcard is a reflection of the fact that the "data" of the input JSON, can be both in the "values"
+ * and the "keys" of the input JSON
+ * 

+ * The base case operation of Shiftr is to copy input JSON "values", thus we need a way to specify that we want to copy the input JSON "key" instead. + *

+ * Thus '$' specifies that we want to use an input key, or input key derived value, as the data to be placed in the output JSON. + * '$' has the same syntax as the '&' wildcard, and can be read as, dereference to get a value, and then use that value as the data to be output. + *

+ * There are two cases where this is useful + * 1) when a "key" in the input JSON needs to be a "id" value in the output JSON, see the ' "$": "SecondaryRatings.&1.Id" ' example above. + * 2) you want to make a list of all the input keys. + *

+ * Example of "a list of the input keys": + *

  *   // input
  *   {
  *     "rating": {
@@ -257,25 +257,25 @@
  *     }
  *   }
  *   
- * + *

* '#' Wildcard - * Valid both on the LHS and RHS, but has different behavior / format on either side. - * The way to think of it, is that it allows you to specify a "synthentic" value, aka a value not found in the input data. - * - * On the RHS of the spec, # is only valid in the the context of an array, like "[#2]". - * What "[#2]" means is, go up the three levels and ask that node how many matches it has had, and then use that as an index - * in the arrays. - * This means that, while Shiftr is doing its parallel tree walk of the input data and the spec, it tracks how many matches it - * has processed at each level of the spec tree. - * - * This useful if you want to take a JSON map and turn it into a JSON array, and you do not care about the order of the array. - * - * On the LHS of the spec, # allows you to specify a hard coded String to be place as a value in the output. + * Valid both on the LHS and RHS, but has different behavior / format on either side. + * The way to think of it, is that it allows you to specify a "synthetic" value, aka a value not found in the input data. + *

+ * On the RHS of the spec, # is only valid in the context of an array, like "[#2]". + * What "[#2]" means is, go up the three levels and ask that node how many matches it has had, and then use that as an index + * in the arrays. + * This means that, while Shiftr is doing its parallel tree walk of the input data and the spec, it tracks how many matches it + * has processed at each level of the spec tree. + *

+ * This useful if you want to take a JSON map and turn it into a JSON array, and you do not care about the order of the array. + *

+ * On the LHS of the spec, # allows you to specify a hard coded String to be place as a value in the output. + *

+ * The initial use-case for this feature was to be able to process a Boolean input value, and if the value is + * boolean true write out the string "enabled". Note, this was possible before, but it required two Shiftr steps. * - * The initial use-case for this feature was to be able to process a Boolean input value, and if the value is - * boolean true write out the string "enabled". Note, this was possible before, but it required two Shiftr steps. - * - *

+ * 
  *      Example
  *      "hidden" : {
  *          "true" : {                             // if the value of "hidden" is true
@@ -283,29 +283,29 @@
  *          }
  *      }
  *   
- * - * + *

+ *

* '|' Wildcard - * Valid only on the LHS of the spec. - * This 'or' wildcard allows you to match multiple input keys. Useful if you don't always know exactly what your input data will be. - * Example Spec : - *

+ * Valid only on the LHS of the spec.
+ * This 'or' wildcard allows you to match multiple input keys. Useful if you don't always know exactly what your input data will be.
+ * Example Spec :
+ * 
  *   {
  *     "rating|Rating" : "rating-primary"   // match "rating" or "Rating" copy the data to "rating-primary"
  *   }
- *   
- * This is really just syntactic sugar, as the implementation really just treats the key "rating|Rating" as two keys when processing. - * - * + *
+ * This is really just syntactic sugar, as the implementation really just treats the key "rating|Rating" as two keys when processing. + *

+ *

* '@' Wildcard - * Valid on both sides of the spec. - * - * The basic '@' on the LHS. - * - * This wildcard is necessary if you want to put both the input value and the input key somewhere in the output JSON. - * - * Example '@' wildcard usage : - *

+ * Valid on both sides of the spec.
+ * 

+ * The basic '@' on the LHS. + *

+ * This wildcard is necessary if you want to put both the input value and the input key somewhere in the output JSON. + *

+ * Example '@' wildcard usage : + *

  *  // Say we have a spec that just operates on the value of the input key "rating"
  *  {
  *     "foo" : "place.to.put.value",  // leveraging the implicit operation of Shiftr which is to operate on input JSON values
@@ -319,24 +319,24 @@
  *     }
  *  }
  *  
- * Thus the '@' wildcard is the mean "copy the value of the data at this level in the tree, to the output". - * - * Advanced '@' sign wildcard. - * The format is lools like "@(3,title)", where - * "3" means go up the tree 3 levels and then lookup the key - * "title" and use the value at that key. - * - * See the filter*.json and transpose*.json Unit Test fixtures. - * - * + * Thus the '@' wildcard is the mean "copy the value of the data at this level in the tree, to the output". + *

+ * Advanced '@' sign wildcard. + * The format is looks like "@(3,title)", where + * "3" means go up the tree 3 levels and then lookup the key + * "title" and use the value at that key. + *

+ * See the filter*.json and transpose*.json Unit Test fixtures. + *

+ *

* JSON Arrays : - * - * Reading from (input) and writing to (output) JSON Arrays is fully supported. - * + *

+ * Reading from (input) and writing to (output) JSON Arrays is fully supported. + *

* 1) Handling Arrays in the input JSON - * Shiftr treats JSON arrays in the input data as Maps with numeric keys. - * Example : - *

+ * Shiftr treats JSON arrays in the input data as Maps with numeric keys.
+ * Example :
+ * 
  *    // input
  *    {
  *       "Photos": [ "AAA.jpg", "BBB.jpg" ]
@@ -355,14 +355,14 @@
  *       "photo-1-url": "BBB.jpg"
  *   }
  *  
- * - * + *

+ *

* 2) Handling Arrays in the output JSON - * Traditional array brackets, [ ], are used to specify array index in the output JSON. - * []'s are only valid on the RHS of the Shiftr spec. - * - * Example : - *

+ * Traditional array brackets, [ ], are used to specify array index in the output JSON.
+ * []'s are only valid on the RHS of the Shiftr spec.
+ * 

+ * Example : + *

  *    // input
  *    {
  *      "photo-1-id": "327704",
@@ -390,8 +390,8 @@
  *      ]
  *    }
  *  
- * - * + *

+ *

* 3) JSON arrays in the spec file * JSON Arrays in Shiftr spec are used to to specify that piece of input data should be copied to two places in the output JSON. * Example : @@ -408,13 +408,13 @@ * "baz" : 3 * } *

- * - * + *

+ *

* 4) Implicit Array creation in the output JSON - * If a spec file is configured to output multiple pieces of data to the same output location, the - * output location will be turned into a JSON array. - * Example : - *

+ * If a spec file is configured to output multiple pieces of data to the same output location, the
+ * output location will be turned into a JSON array.
+ * Example :
+ * 
  *    // input
  *    {
  *        "foo" : "bar",
@@ -432,32 +432,32 @@
  *        "baz" : [ "bar", "marlin" ]     // Note the order of this Array should not be relied upon
  *    }
  *  
- * - * - * - * - * + *

+ *

+ *

+ *

+ *

* Algorithm High Level - * Walk the input data, and Shiftr spec simultaneously, and execute the Shiftr command/mapping each time - * there is a match. - * + * Walk the input data, and Shiftr spec simultaneously, and execute the Shiftr command/mapping each time + * there is a match. + *

  * Algorithm Low Level
  * - Simultaneously walk of the spec and input JSon, and maintain a walked "input" path data structure.
  * - Determine a match between input JSON key and LHS spec, by matching LHS spec keys in the following order :
  * -- Note that '|' keys are are split into their subkeys, eg "literal", '*', or '&' LHS keys
- *
  * 1) Try to match the input key with "literal" spec key values
  * 2) If no literal match is found, try to match against LHS '&' computed values.
  * 2.1) For deterministic behavior, if there is more than one '&' LHS key, they are applied/matched in alphabetical order,
- *   after the '&' syntactic sugar is replaced with its canonical form.
+ * after the '&' syntactic sugar is replaced with its canonical form.
  * 3) If no match is found, try to match against LHS keys with '*' wildcard values.
  * 3.1) For deterministic behavior, '*' wildcard keys are sorted and applied/matched in alphabetical order.
- *
+ * 
+ *

* Note, processing of the '@' and '$' LHS keys always occur if their parent's match, and do not block any other matching. - * - * + *

+ *

* Implementation - * + *

* Instances of this class execute Shiftr transformations given a transform spec of Jackson-style maps of maps * and a Jackson-style map-of-maps input. */ @@ -468,19 +468,19 @@ public class Shiftr implements SpecDriven, Transform { /** * Initialize a Shiftr transform with a Spec. * - * @throws com.bazaarvoice.jolt.exception.SpecException for a malformed spec + * @throws io.joltcommunity.jolt.exception.SpecException for a malformed spec */ @Inject - public Shiftr( Object spec ) { + public Shiftr(Object spec) { - if ( spec == null ){ - throw new SpecException( "Shiftr expected a spec of Map type, got 'null'." ); + if (spec == null) { + throw new SpecException("Shiftr expected a spec of Map type, got 'null'."); } - if ( ! ( spec instanceof Map ) ) { - throw new SpecException( "Shiftr expected a spec of Map type, got " + spec.getClass().getSimpleName() ); + if (!(spec instanceof Map)) { + throw new SpecException("Shiftr expected a spec of Map type, got " + spec.getClass().getSimpleName()); } - rootSpec = new ShiftrCompositeSpec( ROOT_KEY, (Map) spec ); + rootSpec = new ShiftrCompositeSpec(ROOT_KEY, (Map) spec); } @@ -489,21 +489,21 @@ public Shiftr( Object spec ) { * * @param input the JSON object to transform * @return the output object with data shifted to it - * @throws com.bazaarvoice.jolt.exception.TransformException for a malformed spec or if there are issues during - * the transform + * @throws io.joltcommunity.jolt.exception.TransformException for a malformed spec or if there are issues during + * the transform */ @Override - public Object transform( Object input ) { + public Object transform(Object input) { - Map output = new HashMap<>(); + Map output = new HashMap<>(); // Create a root LiteralPathElement so that # is useful at the root level - MatchedElement rootLpe = new MatchedElement( ROOT_KEY ); + MatchedElement rootLpe = new MatchedElement(ROOT_KEY); WalkedPath walkedPath = new WalkedPath(); - walkedPath.add( input, rootLpe ); + walkedPath.add(input, rootLpe); - rootSpec.apply( ROOT_KEY, Optional.of( input ), walkedPath, output, null ); + rootSpec.apply(ROOT_KEY, Optional.of(input), walkedPath, output, null); - return output.get( ROOT_KEY ); + return output.get(ROOT_KEY); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Sortr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Sortr.java similarity index 58% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/Sortr.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/Sortr.java index 2a15456b..eb9d7e6d 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Sortr.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Sortr.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,68 +14,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Recursively sorts all maps within a JSON object into new sorted LinkedHashMaps so that serialized * representations are deterministic. Useful for debugging and making test fixtures. - * + *

* Note this will make a copy of the input Map and List objects. - * + *

* The sort order is standard alphabetical ascending, with a special case for "~" prefixed keys to be bumped to the top. */ public class Sortr implements Transform { - /** - * Makes a "sorted" copy of the input JSON for human readability. - * - * @param input the JSON object to transform, in plain vanilla Jackson Map style - */ - @Override - public Object transform( Object input ) { - return sortJson( input ); - } + private final static JsonKeyComparator jsonKeyComparator = new JsonKeyComparator(); - @SuppressWarnings( "unchecked" ) - public static Object sortJson( Object obj ) { - if ( obj instanceof Map ) { - return sortMap( (Map) obj ); - } else if ( obj instanceof List ) { - return ordered( (List) obj ); + @SuppressWarnings("unchecked") + public static Object sortJson(Object obj) { + if (obj instanceof Map) { + return sortMap((Map) obj); + } else if (obj instanceof List) { + return ordered((List) obj); } else { return obj; } } - private static Map sortMap( Map map ) { - List keys = new ArrayList<>( map.keySet() ); - Collections.sort( keys, jsonKeyComparator ); + private static Map sortMap(Map map) { + List keys = new ArrayList<>(map.keySet()); + keys.sort(jsonKeyComparator); - LinkedHashMap orderedMap = new LinkedHashMap<>( map.size() ); - for ( String key : keys ) { - orderedMap.put( key, sortJson( map.get(key) ) ); + LinkedHashMap orderedMap = new LinkedHashMap<>(map.size()); + for (String key : keys) { + orderedMap.put(key, sortJson(map.get(key))); } return orderedMap; } - private static List ordered( List list ) { + private static List ordered(List list) { // Don't sort the list because that would change intent, but sort its components // Additionally, make a copy of the List in-case the provided list is Immutable / Unmodifiable - List newList = new ArrayList<>( list.size() ); - for ( Object obj : list ) { - newList.add( sortJson( obj ) ); + List newList = new ArrayList<>(list.size()); + for (Object obj : list) { + newList.add(sortJson(obj)); } return newList; } - private final static JsonKeyComparator jsonKeyComparator = new JsonKeyComparator(); + /** + * Makes a "sorted" copy of the input JSON for human readability. + * + * @param input the JSON object to transform, in plain vanilla Jackson Map style + */ + @Override + public Object transform(Object input) { + return sortJson(input); + } /** * Standard alphabetical sort, with a special case for keys beginning with "~". @@ -84,17 +80,17 @@ private static class JsonKeyComparator implements Comparator { @Override public int compare(String a, String b) { - boolean aTilde = ( a.length() > 0 && a.charAt(0) == '~' ); - boolean bTilde = ( b.length() > 0 && b.charAt(0) == '~' ); + boolean aTilde = (!a.isEmpty() && a.charAt(0) == '~'); + boolean bTilde = (!b.isEmpty() && b.charAt(0) == '~'); - if ( aTilde && ! bTilde ) { + if (aTilde && !bTilde) { return -1; } - if ( ! aTilde && bTilde ) { + if (!aTilde && bTilde) { return 1; } - return a.compareTo( b ); + return a.compareTo(b); } } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/SpecDriven.java b/jolt-core/src/main/java/io/joltcommunity/jolt/SpecDriven.java similarity index 82% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/SpecDriven.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/SpecDriven.java index 1849bfcc..6cb5bc86 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/SpecDriven.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/SpecDriven.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,21 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; /** * Marker interface for Jolt Transforms that are based off a "spec". - * + *

* Implementations of this interface are expected to have a single arg constructor, * which takes an {@link Object} that is the spec for the constructed instance. * Chainr leverages this to instantiate these objects correctly. - * + *

* Additionally, all {@link SpecDriven} implementations should mark their constructor - * with the {@link javax.inject.Inject} annotation, so that they can be loaded via + * with the {@link jakarta.inject.Inject} annotation, so that they can be loaded via * Dependency Injection systems. - * - * All of the "stock" Jolt {@link SpecDriven} transforms are marked with {@link javax.inject.Inject}. - * + *

+ * All of the "stock" Jolt {@link SpecDriven} transforms are marked with {@link jakarta.inject.Inject}. + *

* Ideally, calls to the transform method are expected to be stateless and multi-thread safe. */ public interface SpecDriven { diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Transform.java b/jolt-core/src/main/java/io/joltcommunity/jolt/Transform.java similarity index 78% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/Transform.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/Transform.java index eea43b92..ba249bf5 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Transform.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/Transform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; public interface Transform extends JoltTransform { @@ -22,7 +23,7 @@ public interface Transform extends JoltTransform { * * @param input the JSON object to transform in plain vanilla Jackson Map style * @return the results of the transformation - * @throws com.bazaarvoice.jolt.exception.TransformException if there are issues with the transform + * @throws io.joltcommunity.jolt.exception.TransformException if there are issues with the transform */ - Object transform( Object input ); + Object transform(Object input); } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/annotation/Experimental.java b/jolt-core/src/main/java/io/joltcommunity/jolt/annotation/Experimental.java new file mode 100644 index 00000000..3a5e18c0 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/annotation/Experimental.java @@ -0,0 +1,25 @@ +/* + * 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 io.joltcommunity.jolt.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.TYPE}) +public @interface Experimental { + String value() default ""; +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalityCompositeSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalityCompositeSpec.java new file mode 100644 index 00000000..7c93cbe7 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalityCompositeSpec.java @@ -0,0 +1,207 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.cardinality; + +import io.joltcommunity.jolt.common.ComputedKeysComparator; +import io.joltcommunity.jolt.common.pathelement.AmpPathElement; +import io.joltcommunity.jolt.common.pathelement.AtPathElement; +import io.joltcommunity.jolt.common.pathelement.LiteralPathElement; +import io.joltcommunity.jolt.common.pathelement.StarPathElement; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; + +import java.util.*; + +/** + * CardinalitySpec that has children, which it builds and then manages during Transforms. + */ +public class CardinalityCompositeSpec extends CardinalitySpec { + + private static final HashMap orderMap; + private static final ComputedKeysComparator computedKeysComparator; + + static { + orderMap = new HashMap<>(); + orderMap.put(AmpPathElement.class, 1); + orderMap.put(StarPathElement.class, 2); + computedKeysComparator = ComputedKeysComparator.fromOrder(orderMap); + } + + private final Map literalChildren; // children that are simple exact matches against the input data + private final List computedChildren; // children that are regex matches against the input data + // Three different buckets for the children of this CardinalityCompositeSpec + private CardinalityLeafSpec specialChild; // children that aren't actually triggered off the input data + + public CardinalityCompositeSpec(String rawKey, Map spec) { + super(rawKey); + + Map literals = new HashMap<>(); + ArrayList computed = new ArrayList<>(); + + specialChild = null; + + // self check + if (pathElement instanceof AtPathElement) { + throw new SpecException("@ CardinalityTransform key, can not have children."); + } + + List children = createChildren(spec); + + if (children.isEmpty()) { + throw new SpecException("Shift CardinalitySpec format error : CardinalitySpec line with empty {} as value is not valid."); + } + + for (CardinalitySpec child : children) { + if (child.pathElement instanceof LiteralPathElement) { + literals.put(child.pathElement.getRawKey(), child); + } + // special is it is "@" + else if (child.pathElement instanceof AtPathElement) { + if (child instanceof CardinalityLeafSpec) { + specialChild = (CardinalityLeafSpec) child; + } else { + throw new SpecException("@ CardinalityTransform key, can not have children."); + } + } else { // star + computed.add(child); + } + } + + // Only the computed children need to be sorted + computed.sort(computedKeysComparator); + + computed.trimToSize(); + literalChildren = Collections.unmodifiableMap(literals); + computedChildren = Collections.unmodifiableList(computed); + } + + + /** + * Recursively walk the spec input tree. + */ + private static List createChildren(Map rawSpec) { + + List children = new ArrayList<>(); + Set actualKeys = new HashSet<>(); + + for (String keyString : rawSpec.keySet()) { + + Object rawRhs = rawSpec.get(keyString); + + CardinalitySpec childSpec; + if (rawRhs instanceof Map) { + childSpec = new CardinalityCompositeSpec(keyString, (Map) rawRhs); + } else { + childSpec = new CardinalityLeafSpec(keyString, rawRhs); + } + + String childCanonicalString = childSpec.pathElement.getCanonicalForm(); + + if (actualKeys.contains(childCanonicalString)) { + throw new IllegalArgumentException("Duplicate canonical CardinalityTransform key found : " + childCanonicalString); + } + + actualKeys.add(childCanonicalString); + + children.add(childSpec); + } + + return children; + } + + /** + * This method implements the Cardinality matching behavior + * when we have both literal and computed children. + *

+ * For each input key, we see if it matches a literal, and it not, try to match the key with every computed child. + */ + private static void applyKeyToLiteralAndComputed(CardinalityCompositeSpec spec, String subKeyStr, Object subInput, WalkedPath walkedPath, Object input) { + + CardinalitySpec literalChild = spec.literalChildren.get(subKeyStr); + + // if the subKeyStr found a literalChild, then we do not have to try to match any of the computed ones + if (literalChild != null) { + literalChild.applyCardinality(subKeyStr, subInput, walkedPath, input); + } else { + // If no literal spec key matched, iterate through all the computedChildren + + // Iterate through all the computedChildren until we find a match + // This relies upon the computedChildren having already been sorted in priority order + for (CardinalitySpec computedChild : spec.computedChildren) { + // if the computed key does not match it will quickly return false + if (computedChild.applyCardinality(subKeyStr, subInput, walkedPath, input)) { + break; + } + } + } + } + + /** + * If this Spec matches the inputkey, then perform one step in the parallel treewalk. + *

+ * Step one level down the input "tree" by carefully handling the List/Map nature the input to + * get the "one level down" data. + *

+ * Step one level down the Spec tree by carefully and efficiently applying our children to the + * "one level down" data. + * + * @return true if this spec "handles" the inputkey such that no sibling specs need to see it + */ + @Override + protected boolean applyCardinality(String inputKey, Object input, WalkedPath walkedPath, Object parentContainer) { + MatchedElement thisLevel = pathElement.match(inputKey, walkedPath); + if (thisLevel == null) { + return false; + } + + walkedPath.add(input, thisLevel); + + // The specialChild can change the data object that I point to. + // Aka, my key had a value that was a List, and that gets changed so that my key points to a ONE value + if (specialChild != null) { + input = specialChild.applyToParentContainer(inputKey, input, walkedPath, parentContainer); + } + + // Handle the rest of the children + process(input, walkedPath); + + walkedPath.removeLastElement(); + return true; + } + + @SuppressWarnings("unchecked") + private void process(Object input, WalkedPath walkedPath) { + + if (input instanceof Map) { + + // Iterate over the whole entrySet rather than the keyset with follow on gets of the values + Set> entrySet = new HashSet<>(((Map) input).entrySet()); + for (Map.Entry inputEntry : entrySet) { + applyKeyToLiteralAndComputed(this, inputEntry.getKey(), inputEntry.getValue(), walkedPath, input); + } + } else if (input instanceof List) { + + for (int index = 0; index < ((List) input).size(); index++) { + Object subInput = ((List) input).get(index); + String subKeyStr = Integer.toString(index); + + applyKeyToLiteralAndComputed(this, subKeyStr, subInput, walkedPath, input); + } + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalityLeafSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalityLeafSpec.java new file mode 100644 index 00000000..24c5546b --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalityLeafSpec.java @@ -0,0 +1,124 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.cardinality; + +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; + +import java.util.*; + +/** + * Leaf level CardinalitySpec object. + *

+ * If this CardinalitySpec's PathElement matches the input (successful parallel tree walk) + * this CardinalitySpec has the information needed to write the given data to the output object. + */ +public class CardinalityLeafSpec extends CardinalitySpec { + + private final CardinalityRelationship cardinalityRelationship; + + public CardinalityLeafSpec(String rawKey, Object rhs) { + super(rawKey); + + try { + cardinalityRelationship = CardinalityRelationship.valueOf(rhs.toString()); + } catch (Exception e) { + throw new SpecException("Invalid Cardinality type :" + rhs.toString(), e); + } + } + + /** + * If this CardinalitySpec matches the inputkey, then do the work of modifying the data and return true. + * + * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it + */ + @Override + protected boolean applyCardinality(String inputKey, Object input, WalkedPath walkedPath, Object parentContainer) { + + MatchedElement thisLevel = getMatch(inputKey, walkedPath); + if (thisLevel == null) { + return false; + } + performCardinalityAdjustment(inputKey, input, walkedPath, (Map) parentContainer, thisLevel); + return true; + } + + /** + * This should only be used by composite specs with an '@' child + * + * @return null if no work was done, otherwise returns the re-parented data + */ + public Object applyToParentContainer(String inputKey, Object input, WalkedPath walkedPath, Object parentContainer) { + + MatchedElement thisLevel = getMatch(inputKey, walkedPath); + if (thisLevel == null) { + return null; + } + return performCardinalityAdjustment(inputKey, input, walkedPath, (Map) parentContainer, thisLevel); + } + + /** + * @return null if no work was done, otherwise returns the re-parented data + */ + private Object performCardinalityAdjustment(String inputKey, Object input, WalkedPath walkedPath, Map parentContainer, MatchedElement thisLevel) { + + // Add our the LiteralPathElement for this level, so that write path References can use it as &(0,0) + walkedPath.add(input, thisLevel); + + Object returnValue = null; + if (cardinalityRelationship == CardinalityRelationship.MANY) { + if (input instanceof List) { + returnValue = input; + } else if (input instanceof Object[]) { + returnValue = Arrays.asList(((Object[]) input)); + } else if (input instanceof Map || input instanceof String || input instanceof Number || input instanceof Boolean) { + Object one = parentContainer.remove(inputKey); + List tempList = new ArrayList<>(); + tempList.add(one); + returnValue = tempList; + + } else if (input == null) { + returnValue = Collections.emptyList(); + } + parentContainer.put(inputKey, returnValue); + } else if (cardinalityRelationship == CardinalityRelationship.ONE) { + if (input instanceof List) { + if (!((List) input).isEmpty()) { + returnValue = ((List) input).get(0); + } + parentContainer.put(inputKey, returnValue); + } else if (input instanceof Object[]) { + returnValue = ((Object[]) input)[0]; + parentContainer.put(inputKey, returnValue); + } + } + + walkedPath.removeLastElement(); + + return returnValue; + } + + private MatchedElement getMatch(String inputKey, WalkedPath walkedPath) { + return pathElement.match(inputKey, walkedPath); + } + + public enum CardinalityRelationship { + ONE, + MANY + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalitySpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalitySpec.java new file mode 100644 index 00000000..fa170e66 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/cardinality/CardinalitySpec.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.cardinality; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.pathelement.*; +import io.joltcommunity.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + * A Spec Object represents a single line from the JSON Cardinality Spec. + *

+ * At a minimum a single Spec has : + * Raw LHS spec value + * Some kind of PathElement (based off that raw LHS value) + *

+ * Additionally there are 2 distinct subclasses of the base Spec + * CardinalityLeafSpec : where the RHS is either "ONE" or "MANY" + * CardinalityCompositeSpec : where the RHS is a map of children Specs + *

+ * The tree structure of formed by the CompositeSpecs is what is used during the transform + * to do the parallel tree walk with the input data tree. + *

+ * During the parallel tree walk, a Path is maintained, and used when + * a tree walk encounters a leaf spec. + */ +public abstract class CardinalitySpec implements BaseSpec { + + private static final String STAR = "*"; + private static final String AT = "@"; + + // The processed key from the JSON config + protected final MatchablePathElement pathElement; + + protected CardinalitySpec(String rawJsonKey) { + this.pathElement = parse(rawJsonKey); + } + + private static MatchablePathElement parse(String key) { + if (key.contains(AT)) { + return new AtPathElement(key); + } else if (STAR.equals(key)) { + return new StarAllPathElement(key); + } else if (key.contains(STAR)) { + if (StringTools.countMatches(key, STAR) == 1) { + return new StarSinglePathElement(key); + } else { + return new StarRegexPathElement(key); + } + } else { + return new LiteralPathElement(key); + } + } + + /** + * This is the main recursive method of the CardinalityTransform parallel "spec" and "input" tree walk. + *

+ * It should return true if this Spec object was able to successfully apply itself given the + * inputKey and input object. + *

+ * In the context of the CardinalityTransform parallel treewalk, if this method returns a non-null Object, + * the assumption is that no other sibling Cardinality specs need to look at this particular input key. + * + * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it + */ + protected abstract boolean applyCardinality(String inputKey, Object input, WalkedPath walkedPath, Object parentContainer); + + @Override + public boolean apply(final String inputKey, final Optional inputOptional, final WalkedPath walkedPath, final Map output, final Map context) { + return applyCardinality(inputKey, inputOptional.get(), walkedPath, output); + } + + @Override + public MatchablePathElement getPathElement() { + return pathElement; + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/ChainrBuilder.java b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/ChainrBuilder.java similarity index 57% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/ChainrBuilder.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/chainr/ChainrBuilder.java index e504b0d5..487c46d5 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/ChainrBuilder.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/ChainrBuilder.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; +package io.joltcommunity.jolt.chainr; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JoltTransform; -import com.bazaarvoice.jolt.chainr.instantiator.ChainrInstantiator; -import com.bazaarvoice.jolt.chainr.instantiator.DefaultChainrInstantiator; -import com.bazaarvoice.jolt.chainr.spec.ChainrEntry; -import com.bazaarvoice.jolt.chainr.spec.ChainrSpec; +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JoltTransform; +import io.joltcommunity.jolt.chainr.instantiator.ChainrInstantiator; +import io.joltcommunity.jolt.chainr.instantiator.DefaultChainrInstantiator; +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.chainr.spec.ChainrSpec; import java.util.ArrayList; import java.util.List; @@ -37,7 +38,7 @@ public class ChainrBuilder { * * @param chainrSpecObj List of transforms to run */ - public ChainrBuilder( Object chainrSpecObj ) { + public ChainrBuilder(Object chainrSpecObj) { this.chainrSpecObj = chainrSpecObj; } @@ -47,33 +48,33 @@ public ChainrBuilder( Object chainrSpecObj ) { * * @param loader ChainrInstantiator to use load Transforms */ - public ChainrBuilder loader( ChainrInstantiator loader ) { + public ChainrBuilder loader(ChainrInstantiator loader) { - if ( loader == null ) { - throw new IllegalArgumentException( "ChainrBuilder requires a non-null loader." ); + if (loader == null) { + throw new IllegalArgumentException("ChainrBuilder requires a non-null loader."); } this.chainrInstantiator = loader; return this; } - public ChainrBuilder withClassLoader( ClassLoader classLoader ) { - if ( classLoader == null ) { - throw new IllegalArgumentException( "ChainrBuilder requires a non-null classLoader." ); + public ChainrBuilder withClassLoader(ClassLoader classLoader) { + if (classLoader == null) { + throw new IllegalArgumentException("ChainrBuilder requires a non-null classLoader."); } this.classLoader = classLoader; return this; } public Chainr build() { - ChainrSpec chainrSpec = new ChainrSpec( chainrSpecObj, classLoader ); - List transforms = new ArrayList<>( chainrSpec.getChainrEntries().size() ); - for ( ChainrEntry entry : chainrSpec.getChainrEntries() ) { + ChainrSpec chainrSpec = new ChainrSpec(chainrSpecObj, classLoader); + List transforms = new ArrayList<>(chainrSpec.getChainrEntries().size()); + for (ChainrEntry entry : chainrSpec.getChainrEntries()) { - JoltTransform transform = chainrInstantiator.hydrateTransform( entry ); - transforms.add( transform ); + JoltTransform transform = chainrInstantiator.hydrateTransform(entry); + transforms.add(transform); } - return new Chainr( transforms ); + return new Chainr(transforms); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/ChainrInstantiator.java b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/instantiator/ChainrInstantiator.java similarity index 72% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/ChainrInstantiator.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/chainr/instantiator/ChainrInstantiator.java index c5ff6e6a..a3fabfcd 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/ChainrInstantiator.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/instantiator/ChainrInstantiator.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +14,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.instantiator; +package io.joltcommunity.jolt.chainr.instantiator; -import com.bazaarvoice.jolt.JoltTransform; -import com.bazaarvoice.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.JoltTransform; +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; /** * Interface to allow the guts of the Transform class loading logic to be swapped out. * This primarily exists to allow clients of Jolt to load their own custom Java Transforms - * via Guice or other dependency injection systems. + * via Guice or other dependency injection systems. */ public interface ChainrInstantiator { /** * Instantiate the Transform class specified by the ChainrEntry. */ - public JoltTransform hydrateTransform( ChainrEntry entry ); + public JoltTransform hydrateTransform(ChainrEntry entry); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/DefaultChainrInstantiator.java b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/instantiator/DefaultChainrInstantiator.java similarity index 61% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/DefaultChainrInstantiator.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/chainr/instantiator/DefaultChainrInstantiator.java index 8583f2e8..1d88ddab 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/instantiator/DefaultChainrInstantiator.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/instantiator/DefaultChainrInstantiator.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.instantiator; +package io.joltcommunity.jolt.chainr.instantiator; -import com.bazaarvoice.jolt.JoltTransform; -import com.bazaarvoice.jolt.chainr.spec.ChainrEntry; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.JoltTransform; +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.exception.SpecException; import java.lang.reflect.Constructor; @@ -27,36 +28,35 @@ public class DefaultChainrInstantiator implements ChainrInstantiator { @Override - public JoltTransform hydrateTransform( ChainrEntry entry ) { + public JoltTransform hydrateTransform(ChainrEntry entry) { Object spec = entry.getSpec(); Class transformClass = entry.getJoltTransformClass(); try { // If the transform class is a SpecTransform, we try to construct it with the provided spec. - if ( entry.isSpecDriven() ) { + if (entry.isSpecDriven()) { try { // Lookup a Constructor with a Single "Object" arg. - Constructor constructor = transformClass.getConstructor( Object.class ); + Constructor constructor = transformClass.getConstructor(Object.class); - return (JoltTransform) constructor.newInstance( spec ); + return (JoltTransform) constructor.newInstance(spec); - } catch ( NoSuchMethodException nsme ) { + } catch (NoSuchMethodException nsme) { // This means the transform class "violated" the SpecTransform marker interface - throw new SpecException( "JOLT Chainr encountered an exception constructing SpecTransform className:" + transformClass.getCanonicalName() + - ". Specifically, no single arg constructor found" + entry.getErrorMessageIndexSuffix(), nsme ); + throw new SpecException("JOLT Chainr encountered an exception constructing SpecTransform className:" + transformClass.getCanonicalName() + + ". Specifically, no single arg constructor found" + entry.getErrorMessageIndexSuffix(), nsme); } - } - else { + } else { // The opClass is just a Transform, so just create a newInstance of it. - return transformClass.newInstance(); + return transformClass.getDeclaredConstructor().newInstance(); } - } catch ( Exception e ) { + } catch (Exception e) { // FYI 3 exceptions are known to be thrown here // IllegalAccessException, InvocationTargetException, InstantiationException - throw new SpecException( "JOLT Chainr encountered an exception constructing Transform className:" - + transformClass.getCanonicalName() + entry.getErrorMessageIndexSuffix(), e ); + throw new SpecException("JOLT Chainr encountered an exception constructing Transform className:" + + transformClass.getCanonicalName() + entry.getErrorMessageIndexSuffix(), e); } } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/spec/ChainrEntry.java b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/spec/ChainrEntry.java new file mode 100644 index 00000000..5186921e --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/spec/ChainrEntry.java @@ -0,0 +1,172 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.chainr.spec; + +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.removr.Removr; +import io.joltcommunity.jolt.utils.StringTools; +import io.joltcommunity.jolt.*; + +import java.util.Map; + +import static java.util.Map.entry; + +/** + * Helper class that encapsulates the information one of the individual transform entries in + * the Chainr spec's list. + */ +public class ChainrEntry { + + /** + * Map transform "operation" names to the classes that handle them + */ + public static final Map STOCK_TRANSFORMS; + public static final String OPERATION_KEY = "operation"; + public static final String SPEC_KEY = "spec"; + + /** + * getName() returns fqdn$path compared to humanReadablePath from getCanonicalPath() + * to make internal classes available/loadable at runtime it is imperative that we use fqdn + */ + static { + STOCK_TRANSFORMS = Map.ofEntries( + entry("shift", Shiftr.class.getName()), + entry("default", Defaultr.class.getName()), + entry("modify-overwrite-beta", Modifier.Overwritr.class.getName()), + entry("modify-overwrite", Modifier.Overwritr.class.getName()), + entry("modify-default-beta", Modifier.Defaultr.class.getName()), + entry("modify-default", Modifier.Defaultr.class.getName()), + entry("modify-define-beta", Modifier.Definr.class.getName()), + entry("modify-define", Modifier.Definr.class.getName()), + entry("remove", Removr.class.getName()), + entry("sort", Sortr.class.getName()), + entry("cardinality", CardinalityTransform.class.getName()), + entry("enrich", Enrichr.class.getName()) + ); + } + + private final int index; + private final Object spec; + private final String operationClassName; + + private final Class joltTransformClass; + private final boolean isSpecDriven; + + /** + * Process an element from the Chainr Spec into a ChainrEntry class. + * This method tries to validate the syntax of the Chainr spec, whereas + * the ChainrInstantiator deals with loading the Transform classes. + * + * @param chainrEntryObj the unknown Object from the Chainr list + * @param index the index of the chainrEntryObj, used in reporting errors + */ + public ChainrEntry(int index, Object chainrEntryObj, ClassLoader classLoader) { + + if (!(chainrEntryObj instanceof Map)) { + throw new SpecException("JOLT ChainrEntry expects a JSON map - Malformed spec" + getErrorMessageIndexSuffix()); + } + + @SuppressWarnings("unchecked") // We know it is a Map due to the check above + Map chainrEntryMap = (Map) chainrEntryObj; + + this.index = index; + + String opString = extractOperationString(chainrEntryMap); + + if (opString == null) { + throw new SpecException("JOLT Chainr 'operation' must implement Transform or ContextualTransform" + getErrorMessageIndexSuffix()); + } + + operationClassName = STOCK_TRANSFORMS.getOrDefault(opString, opString); + + joltTransformClass = loadJoltTransformClass(classLoader); + + spec = chainrEntryMap.get(ChainrEntry.SPEC_KEY); + + isSpecDriven = SpecDriven.class.isAssignableFrom(joltTransformClass); + if (isSpecDriven && !chainrEntryMap.containsKey(SPEC_KEY)) { + throw new SpecException("JOLT Chainr - Transform className:" + joltTransformClass.getName() + " requires a spec" + getErrorMessageIndexSuffix()); + } + } + + private String extractOperationString(Map chainrEntryMap) { + + Object operationNameObj = chainrEntryMap.get(ChainrEntry.OPERATION_KEY); + if (operationNameObj == null) { + return null; + } else if (operationNameObj instanceof String) { + if (StringTools.isBlank((String) operationNameObj)) { + throw new SpecException("JOLT Chainr '" + ChainrEntry.OPERATION_KEY + "' should not be blank" + getErrorMessageIndexSuffix()); + } + return (String) operationNameObj; + } else { + throw new SpecException("JOLT Chainr needs a '" + ChainrEntry.OPERATION_KEY + "' of type String" + getErrorMessageIndexSuffix()); + } + } + + private Class loadJoltTransformClass(ClassLoader classLoader) { + + try { + Class opClass = classLoader.loadClass(operationClassName); + + if (Chainr.class.isAssignableFrom(opClass)) { + throw new SpecException("Attempt to nest Chainr inside itself" + getErrorMessageIndexSuffix()); + } + + if (!JoltTransform.class.isAssignableFrom(opClass)) { + throw new SpecException("JOLT Chainr class:" + operationClassName + " does not implement the JoltTransform interface" + getErrorMessageIndexSuffix()); + } + + @SuppressWarnings("unchecked") // We know it is some type of Transform due to the check above + Class transformClass = (Class) opClass; + + return transformClass; + + } catch (ClassNotFoundException e) { + throw new SpecException("JOLT Chainr could not find transform class:" + operationClassName + getErrorMessageIndexSuffix(), e); + } + } + + + /** + * Generate an error message suffix what lists the index of the ChainrEntry in the overall ChainrSpec. + */ + public String getErrorMessageIndexSuffix() { + return " at index:" + index + "."; + } + + /** + * @return Spec for the transform, can be null + */ + public Object getSpec() { + return spec; + } + + /** + * @return Class instance specified by this ChainrEntry + */ + public Class getJoltTransformClass() { + return joltTransformClass; + } + + /** + * @return true if the Jolt Transform specified by this ChainrEntry implements the SpecTransform interface + */ + public boolean isSpecDriven() { + return isSpecDriven; + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/spec/ChainrSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/spec/ChainrSpec.java similarity index 62% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/spec/ChainrSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/chainr/spec/ChainrSpec.java index 5dac88a9..1b4f959f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/chainr/spec/ChainrSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/chainr/spec/ChainrSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.spec; +package io.joltcommunity.jolt.chainr.spec; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.SpecException; import java.util.ArrayList; import java.util.Collections; @@ -23,7 +24,7 @@ /** * Helper class that encapsulates the Chainr spec's list. - * + *

* For reference : a Chainr spec should be an array of objects in order that look like this: * *

@@ -35,7 +36,7 @@
  *     ...
  * ]
  * 
- * + *

* This class represents the Array, while the ChainrEntry class encompass the individual elements * of the array. */ @@ -46,35 +47,35 @@ public class ChainrSpec { /** * @param chainrSpec Plain vanilla hydrated JSON representation of a Chainr spec .json file. */ - public ChainrSpec( Object chainrSpec ) { - this( chainrSpec, ChainrSpec.class.getClassLoader() ); + public ChainrSpec(Object chainrSpec) { + this(chainrSpec, ChainrSpec.class.getClassLoader()); } - public ChainrSpec( Object chainrSpec, ClassLoader classLoader ) { + public ChainrSpec(Object chainrSpec, ClassLoader classLoader) { - if ( !( chainrSpec instanceof List ) ) { - throw new SpecException( "JOLT Chainr expects a JSON array of objects - Malformed spec." ); + if (!(chainrSpec instanceof List)) { + throw new SpecException("JOLT Chainr expects a JSON array of objects - Malformed spec."); } - @SuppressWarnings( "unchecked" ) // We know its a list due to the check above + @SuppressWarnings("unchecked") // We know its a list due to the check above List operations = (List) chainrSpec; - if ( operations.isEmpty() ) { - throw new SpecException( "JOLT Chainr passed an empty JSON array."); + if (operations.isEmpty()) { + throw new SpecException("JOLT Chainr passed an empty JSON array."); } List entries = new ArrayList<>(operations.size()); - for ( int index = 0; index < operations.size(); index++ ) { + for (int index = 0; index < operations.size(); index++) { - Object chainrEntryObj = operations.get( index ); + Object chainrEntryObj = operations.get(index); - ChainrEntry entry = new ChainrEntry( index, chainrEntryObj, classLoader ); + ChainrEntry entry = new ChainrEntry(index, chainrEntryObj, classLoader); - entries.add( entry ); + entries.add(entry); } - chainrEntries = Collections.unmodifiableList( entries ); + chainrEntries = Collections.unmodifiableList(entries); } /** diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/ComputedKeysComparator.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/ComputedKeysComparator.java similarity index 77% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/ComputedKeysComparator.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/ComputedKeysComparator.java index e85a8836..ad3cacc0 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/ComputedKeysComparator.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/ComputedKeysComparator.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,48 +15,49 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common; +package io.joltcommunity.jolt.common; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.pathelement.PathElement; +import io.joltcommunity.jolt.common.spec.BaseSpec; import java.util.Comparator; import java.util.HashMap; /** * This Comparator is used for determining the execution order of childSpecs.apply(...) - * + *

* Argument Map of Class: integer is used to determine precedence */ public class ComputedKeysComparator implements Comparator { + private final HashMap orderMap; + + private ComputedKeysComparator(HashMap orderMap) { + this.orderMap = orderMap; + } + /** * Static factory method to get an Comparator instance for a given order map + * * @param orderMap of precedence * @return Comparator that uses the given order map to determine precedence */ public static ComputedKeysComparator fromOrder(HashMap orderMap) { - return new ComputedKeysComparator( orderMap ); - } - - private final HashMap orderMap; - - private ComputedKeysComparator(HashMap orderMap) { - this.orderMap = orderMap; + return new ComputedKeysComparator(orderMap); } @Override - public int compare( BaseSpec a, BaseSpec b ) { + public int compare(BaseSpec a, BaseSpec b) { PathElement ape = a.getPathElement(); PathElement bpe = b.getPathElement(); - int aa = orderMap.get( ape.getClass() ); - int bb = orderMap.get( bpe.getClass() ); + int aa = orderMap.get(ape.getClass()); + int bb = orderMap.get(bpe.getClass()); - int elementsEqual = aa < bb ? -1 : aa == bb ? 0 : 1; + int elementsEqual = Integer.compare(aa, bb); - if ( elementsEqual != 0 ) { + if (elementsEqual != 0) { return elementsEqual; } @@ -69,6 +71,6 @@ public int compare( BaseSpec a, BaseSpec b ) { // Sort them by length, with the longest (most specific) being first // aka "rating-range-*" needs to be evaluated before "rating-*", or else "rating-*" will catch too much // If the lengths are equal, sort alphabetically as the last ditch deterministic behavior - return alen > blen ? -1 : alen == blen ? acf.compareTo( bcf ) : 1; + return alen > blen ? -1 : alen == blen ? acf.compareTo(bcf) : 1; } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/DeepCopy.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/DeepCopy.java new file mode 100644 index 00000000..614cd1a7 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/DeepCopy.java @@ -0,0 +1,50 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import java.io.*; + +public class DeepCopy { + + /** + * Simple deep copy, that leverages Java Serialization. + * Supplied object is serialized to an in memory buffer (byte array), + * and then a new object is reconstituted from that byte array. + *

+ * This is meant for copying small objects or object graphs, and will + * probably do nasty things if asked to copy a large graph. + * + * @param object object to deep copy + * @return deep copy of the object + */ + public static Object simpleDeepCopy(Object object) { + try ( + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(bos) + ) { + oos.writeObject(object); + oos.flush(); + byte[] byteData = bos.toByteArray(); + try (ByteArrayInputStream bais = new ByteArrayInputStream(byteData); + ObjectInputStream ois = new ObjectInputStream(bais)) { + return ois.readObject(); + } + } catch (IOException | ClassNotFoundException ex) { + throw new RuntimeException("DeepCopy failed", ex); + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/ExecutionStrategy.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/ExecutionStrategy.java new file mode 100644 index 00000000..96d80e80 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/ExecutionStrategy.java @@ -0,0 +1,324 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import io.joltcommunity.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.spec.OrderedCompositeSpec; +import io.joltcommunity.jolt.common.tree.WalkedPath; + +import java.util.List; +import java.util.Map; + +public enum ExecutionStrategy { + + /** + * The performance assumption built into this code is that the literal values in the spec, are generally smaller + * than the number of potential keys to check in the input. + *

+ * More specifically, the assumption here is that the set of literalChildren is smaller than the input "keyset". + */ + AVAILABLE_LITERALS { + @Override + void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context) { + + for (String key : spec.getLiteralChildren().keySet()) { + + // Do not work if the value is missing in the input map + if (inputMap.containsKey(key)) { + + Optional subInputOptional = Optional.of(inputMap.get(key)); + spec.getLiteralChildren().get(key).apply(key, subInputOptional, walkedPath, output, context); + } + } + } + + @Override + void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context) { + + Integer originalSize = walkedPath.lastElement().getOrigSize().get(); + for (String key : spec.getLiteralChildren().keySet()) { + + int keyInt = Integer.MAX_VALUE; + + try { + keyInt = Integer.parseInt(key); + } catch (NumberFormatException nfe) { + // If the data is an Array, but the spec keys are Non-Integer Strings, + // we are annoyed, but we don't stop the whole transform. + // Just this part of the Transform won't work. + } + + // Do not work if the index is outside of the input list + if (keyInt < inputList.size()) { + + Object subInput = inputList.get(keyInt); + Optional subInputOptional; + if (subInput == null && originalSize != null && keyInt >= originalSize) { + subInputOptional = Optional.empty(); + } else { + subInputOptional = Optional.of(subInput); + } + + // we know the .get(key) will not return null, because we are iterating over its keys + spec.getLiteralChildren().get(key).apply(key, subInputOptional, walkedPath, output, context); + } + } + } + + @Override + void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context) { + + BaseSpec literalChild = spec.getLiteralChildren().get(scalarInput); + if (literalChild != null) { + literalChild.apply(scalarInput, Optional.empty(), walkedPath, output, context); + } + } + }, + + /** + * This is identical to AVAILABLE_LITERALS, except for the fact that it does not skip keys if its missing in the input, like literal does + * Given this works like defaultr, a missing key is our point of entry to insert a default value, either from a passed context or a + * hardcoded value. + */ + ALL_LITERALS { + @Override + void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context) { + + for (String key : spec.getLiteralChildren().keySet()) { + + // if the input in not available in the map us null or else get value, + // then lookup and place a defined value from spec there + Optional subInputOptional = Optional.empty(); + if (inputMap.containsKey(key)) { + subInputOptional = Optional.of(inputMap.get(key)); + } + spec.getLiteralChildren().get(key).apply(key, subInputOptional, walkedPath, output, context); + } + } + + @Override + void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context) { + + Integer originalSize = walkedPath.lastElement().getOrigSize().get(); + for (String key : spec.getLiteralChildren().keySet()) { + + int keyInt = Integer.MAX_VALUE; + + try { + keyInt = Integer.parseInt(key); + } catch (NumberFormatException nfe) { + // If the data is an Array, but the spec keys are Non-Integer Strings, + // we are annoyed, but we don't stop the whole transform. + // Just this part of the Transform won't work. + } + + // if the input in not available in the list use null or else get value, + // then lookup and place a default value as defined in spec there + Optional subInputOptional = Optional.empty(); + if (keyInt < inputList.size()) { + Object subInput = inputList.get(keyInt); + if (subInput != null || originalSize == null || keyInt < originalSize) { + subInputOptional = Optional.of(subInput); + } + } + // we know the .get(key) will not return null, because we are iterating over its keys + spec.getLiteralChildren().get(key).apply(key, subInputOptional, walkedPath, output, context); + } + } + + @Override + void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context) { + + AVAILABLE_LITERALS.processScalar(spec, scalarInput, walkedPath, output, context); + } + }, + + /** + * If the CompositeSpec only has computed children, we can avoid checking the getLiteralChildren() altogether, and + * we can do a slightly better iteration (HashSet.entrySet) across the input. + */ + COMPUTED { + @Override + void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context) { + + // Iterate over the whole entrySet rather than the keyset with follow on gets of the values + for (Map.Entry inputEntry : inputMap.entrySet()) { + applyKeyToComputed(spec.getComputedChildren(), walkedPath, output, inputEntry.getKey(), Optional.of(inputEntry.getValue()), context); + } + } + + @Override + void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context) { + + Integer originalSize = walkedPath.lastElement().getOrigSize().get(); + for (int index = 0; index < inputList.size(); index++) { + Object subInput = inputList.get(index); + String subKeyStr = Integer.toString(index); + Optional subInputOptional; + if (subInput == null && originalSize != null && index >= originalSize) { + subInputOptional = Optional.empty(); + } else { + subInputOptional = Optional.of(subInput); + } + + applyKeyToComputed(spec.getComputedChildren(), walkedPath, output, subKeyStr, subInputOptional, context); + } + } + + @Override + void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context) { + applyKeyToComputed(spec.getComputedChildren(), walkedPath, output, scalarInput, Optional.empty(), context); + } + }, + + /** + * In order to implement the key precedence order, we have to process each input "key", first to + * see if it matches any literals, and if it does not, check against each of the computed + */ + CONFLICT { + @Override + void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context) { + + // Iterate over the whole entrySet rather than the keyset with follow on gets of the values + for (Map.Entry inputEntry : inputMap.entrySet()) { + applyKeyToLiteralAndComputed(spec, inputEntry.getKey(), Optional.of(inputEntry.getValue()), walkedPath, output, context); + } + } + + @Override + void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context) { + + Integer originalSize = walkedPath.lastElement().getOrigSize().get(); + for (int index = 0; index < inputList.size(); index++) { + Object subInput = inputList.get(index); + String subKeyStr = Integer.toString(index); + Optional subInputOptional; + if (subInput == null && originalSize != null && index >= originalSize) { + subInputOptional = Optional.empty(); + } else { + subInputOptional = Optional.of(subInput); + } + + applyKeyToLiteralAndComputed(spec, subKeyStr, subInputOptional, walkedPath, output, context); + } + } + + @Override + void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context) { + applyKeyToLiteralAndComputed(spec, scalarInput, Optional.empty(), walkedPath, output, context); + } + }, + + /** + * We have both literal and computed children, but we have determined that there is no way an input key + * could match one of our literal and computed children. Hence we can safely run each one. + */ + AVAILABLE_LITERALS_WITH_COMPUTED { + @Override + void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context) { + AVAILABLE_LITERALS.processMap(spec, inputMap, walkedPath, output, context); + COMPUTED.processMap(spec, inputMap, walkedPath, output, context); + } + + @Override + void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context) { + AVAILABLE_LITERALS.processList(spec, inputList, walkedPath, output, context); + COMPUTED.processList(spec, inputList, walkedPath, output, context); + } + + @Override + void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context) { + AVAILABLE_LITERALS.processScalar(spec, scalarInput, walkedPath, output, context); + COMPUTED.processScalar(spec, scalarInput, walkedPath, output, context); + } + }, + + ALL_LITERALS_WITH_COMPUTED { + @Override + void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context) { + ALL_LITERALS.processMap(spec, inputMap, walkedPath, output, context); + COMPUTED.processMap(spec, inputMap, walkedPath, output, context); + } + + @Override + void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context) { + ALL_LITERALS.processList(spec, inputList, walkedPath, output, context); + COMPUTED.processList(spec, inputList, walkedPath, output, context); + } + + @Override + void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context) { + ALL_LITERALS.processScalar(spec, scalarInput, walkedPath, output, context); + COMPUTED.processScalar(spec, scalarInput, walkedPath, output, context); + } + }; + + /** + * This is the method we are trying to avoid calling. It implements the matching behavior + * when we have both literal and computed children. + *

+ * For each input key, we see if it matches a literal, and it not, try to match the key with every computed child. + *

+ * Worse case : n + n * c, where + * n is number of input keys + * c is number of computed children + */ + private static void applyKeyToLiteralAndComputed(T spec, String subKeyStr, Optional subInputOptional, WalkedPath walkedPath, Map output, Map context) { + + BaseSpec literalChild = spec.getLiteralChildren().get(subKeyStr); + + // if the subKeyStr found a literalChild, then we do not have to try to match any of the computed ones + if (literalChild != null) { + literalChild.apply(subKeyStr, subInputOptional, walkedPath, output, context); + } else { + // If no literal spec key matched, iterate through all the getComputedChildren() + applyKeyToComputed(spec.getComputedChildren(), walkedPath, output, subKeyStr, subInputOptional, context); + } + } + + private static void applyKeyToComputed(List computedChildren, WalkedPath walkedPath, Map output, String subKeyStr, Optional subInputOptional, Map context) { + + // Iterate through all the getComputedChildren() until we find a match + // This relies upon the getComputedChildren() having already been sorted in priority order + for (BaseSpec computedChild : computedChildren) { + // if the computed key does not match it will quickly return false + if (computedChild.apply(subKeyStr, subInputOptional, walkedPath, output, context)) { + break; + } + } + } + + @SuppressWarnings("unchecked") + public void process(OrderedCompositeSpec spec, Optional inputOptional, WalkedPath walkedPath, Map output, Map context) { + Object input = inputOptional.get(); + if (input instanceof Map) { + processMap(spec, (Map) input, walkedPath, output, context); + } else if (input instanceof List) { + processList(spec, (List) input, walkedPath, output, context); + } else if (input != null) { + // if not a map or list, must be a scalar + processScalar(spec, input.toString(), walkedPath, output, context); + } + } + + abstract void processMap(OrderedCompositeSpec spec, Map inputMap, WalkedPath walkedPath, Map output, Map context); + + abstract void processList(OrderedCompositeSpec spec, List inputList, WalkedPath walkedPath, Map output, Map context); + + abstract void processScalar(OrderedCompositeSpec spec, String scalarInput, WalkedPath walkedPath, Map output, Map context); +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/Optional.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/Optional.java similarity index 75% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/Optional.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/Optional.java index 6a0f8995..e04bbe06 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/Optional.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/Optional.java @@ -1,5 +1,6 @@ /* - * Copyright 2016 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common; +package io.joltcommunity.jolt.common; /** * We cannot go away from this Optional to java 8 Optional because, this Optional gives as three states! @@ -22,12 +23,21 @@ */ public class Optional { + private static final Optional EMPTY = new Optional<>(); private final T obj; private final boolean abs; - private static final Optional EMPTY = new Optional<>(); + private Optional() { + obj = null; + abs = true; + } + + private Optional(T obj) { + this.obj = obj; + abs = false; + } - public static Optional empty() { + public static Optional empty() { @SuppressWarnings("unchecked") Optional t = (Optional) EMPTY; return t; @@ -37,36 +47,25 @@ public static Optional of(T value) { return new Optional<>(value); } - private Optional() { - obj = null; - abs = true; - } - - private Optional( T obj ) { - this.obj = obj; - abs = false; - } - public T get() { return obj; } public boolean isPresent() { - return ! abs; + return !abs; } @Override - public boolean equals( final Object obj ) { - if(!(obj instanceof Optional)) { + public boolean equals(final Object obj) { + if (!(obj instanceof Optional that)) { return false; } - Optional that = (Optional) obj; return that == EMPTY || ( this.abs == that.abs && ( (this.obj == null && that.obj == null) || ( this.obj != null && - that.obj != null && - this.obj.equals( that.obj ) + that.obj != null && + this.obj.equals(that.obj) ) ) ); @@ -74,6 +73,6 @@ public boolean equals( final Object obj ) { @Override public String toString() { - return "Optional<" + (abs?"?":obj==null?"?":obj.getClass().getSimpleName()) + ">: present=" + !abs + ", value=(" + obj + ")"; + return "Optional<" + (abs ? "?" : obj == null ? "?" : obj.getClass().getSimpleName()) + ">: present=" + !abs + ", value=(" + obj + ")"; } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/PathElementBuilder.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/PathElementBuilder.java new file mode 100644 index 00000000..e74f8dba --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/PathElementBuilder.java @@ -0,0 +1,146 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import io.joltcommunity.jolt.common.pathelement.*; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import static io.joltcommunity.jolt.common.SpecStringParser.*; + +/** + * Static utility class that creates PathElement(s) given a string key from a json spec document + */ +public class PathElementBuilder { + + private PathElementBuilder() { + } + + /** + * Create a path element and ensures it is a Matchable Path Element + */ + public static MatchablePathElement buildMatchablePathElement(String rawJsonKey) { + PathElement pe = PathElementBuilder.parseSingleKeyLHS(rawJsonKey); + + if (!(pe instanceof MatchablePathElement)) { + throw new SpecException("Spec LHS key=" + rawJsonKey + " is not a valid LHS key."); + } + + return (MatchablePathElement) pe; + } + + /** + * Visible for Testing. + *

+ * Inspects the key in a particular order to determine the correct subclass of + * PathElement to create. + * + * @param origKey String that should represent a single PathElement + * @return a concrete implementation of PathElement + */ + public static PathElement parseSingleKeyLHS(String origKey) { + return parseKeyToPathElement(origKey); + } + + /** + * Parse the dotNotation of the RHS. + */ + public static List parseDotNotationRHS(String dotNotation) { + String fixedNotation = fixLeadingBracketSugar(dotNotation); + List pathStrs = parseDotNotation(new LinkedList<>(), stringIterator(fixedNotation), dotNotation); + + return parseKeysToPathElements(pathStrs, dotNotation); + } + + /** + * @param refDotNotation the original dotNotation string used for error messages + * @return List of PathElements based on the provided List keys + */ + public static List parseKeysToPathElements(List keys, String refDotNotation) { + ArrayList paths = new ArrayList<>(); + + for (String key : keys) { + PathElement path = parseKeyToPathElement(key); + if (path instanceof AtPathElement) { + throw new SpecException("'.@.' is not valid on the RHS: " + refDotNotation); + } + paths.add(path); + } + + return paths; + } + + private static PathElement parseKeyToPathElement(String origKey) { + String elementKey = origKey; // the String to use to actually make Elements + String keyToInspect = origKey; // the String to use to determine which kind of Element to create + + if (origKey.contains("\\")) { + // only do the extra work of processing for escaped chars, if there is one. + keyToInspect = removeEscapedValues(origKey); + elementKey = removeEscapeChars(origKey); + } + + if ("@".equals(keyToInspect)) { + return new AtPathElement(elementKey); + } else if ("*".equals(keyToInspect)) { + return new StarAllPathElement(elementKey); + } else if (keyToInspect.startsWith("$")) { + return new DollarPathElement(elementKey); + } else if (keyToInspect.startsWith("#")) { + return new HashPathElement(elementKey); + } else if (keyToInspect.startsWith("[")) { + + if (StringTools.countMatches(keyToInspect, "[") != 1 || StringTools.countMatches(keyToInspect, "]") != 1) { + throw new SpecException("Invalid key:" + origKey + " has too many [] references."); + } + + return new ArrayPathElement(elementKey); + } + else if (keyToInspect.startsWith("@")) { + // The transpose path element gets the origKey so that it has it's escapes. + return TransposePathElement.parse(origKey); + } else if (keyToInspect.contains("@")) { + throw new SpecException("Invalid key:" + origKey + " can not have an @ other than at the front."); + } else if (keyToInspect.contains("&")) { + + if (keyToInspect.contains("*")) { + throw new SpecException("Invalid key:" + origKey + ", Can't mix * with & ) "); + } + + return new AmpPathElement(elementKey); + } else if (keyToInspect.contains("*")) { + return parseStarPathElement(keyToInspect, elementKey); + } + return new LiteralPathElement(elementKey); + } + + private static StarPathElement parseStarPathElement(String keyToInspect, String elementKey) { + int numOfStars = StringTools.countMatches(keyToInspect, "*"); + + if (numOfStars == 1) { + return new StarSinglePathElement(elementKey); + } else if (numOfStars == 2) { + return new StarDoublePathElement(elementKey); + } + return new StarRegexPathElement(elementKey); + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/PathEvaluatingTraversal.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/PathEvaluatingTraversal.java new file mode 100644 index 00000000..5627acf9 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/PathEvaluatingTraversal.java @@ -0,0 +1,156 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import io.joltcommunity.jolt.common.pathelement.EvaluatablePathElement; +import io.joltcommunity.jolt.common.pathelement.PathElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.traversr.Traversr; +import io.joltcommunity.jolt.utils.StringTools; + +import java.util.*; + +import static io.joltcommunity.jolt.common.PathElementBuilder.parseDotNotationRHS; + +/** + * Combines a Traversr with the ability to evaluate References against a WalkedPath. + *

+ * Convenience class for path based off a single dot notation String, + * like "rating.&1(2).&.value". + *

+ * This processes the dot notation path into internal data structures, so + * that the String processing only happens once. + */ +public abstract class PathEvaluatingTraversal { + + private final List elements; + private final Traversr traversr; + + public PathEvaluatingTraversal(String dotNotation) { + + if ((dotNotation.contains("*") && !dotNotation.contains("\\*")) || + (dotNotation.contains("$") && !dotNotation.contains("\\$"))) { + throw new SpecException("DotNotation (write key) can not contain '*' or '$' : write key: " + dotNotation); + } + + List paths; + Traversr trav; + + if (StringTools.isNotBlank(dotNotation)) { + + // Compute the path elements. + paths = parseDotNotationRHS(dotNotation); + + // Use the canonical versions of the path elements to create the Traversr + List traversrPaths = new ArrayList<>(paths.size()); + for (PathElement pe : paths) { + traversrPaths.add(pe.getCanonicalForm()); + } + trav = createTraversr(traversrPaths); + } else { + paths = Collections.emptyList(); + trav = createTraversr(Arrays.asList("")); + } + + List evalPaths = new ArrayList<>(paths.size()); + for (PathElement pe : paths) { + if (!(pe instanceof EvaluatablePathElement)) { + throw new SpecException("RHS key=" + pe.getRawKey() + " is not a valid RHS key."); + } + + evalPaths.add((EvaluatablePathElement) pe); + } + + this.elements = Collections.unmodifiableList(evalPaths); + this.traversr = trav; + } + + protected abstract Traversr createTraversr(List paths); + + /** + * Use the supplied WalkedPath, in the evaluation of each of our PathElements to + * build a concrete output path. Then use that output path to write the given + * data to the output. + * + * @param data data to write + * @param output data structure we are going to write the data to + * @param walkedPath reference used to lookup reference values like "&1(2)" + */ + public void write(Object data, Map output, WalkedPath walkedPath) { + List evaledPaths = evaluate(walkedPath); + if (evaledPaths != null) { + traversr.set(output, evaledPaths, data); + } + } + + public Optional read(Object data, WalkedPath walkedPath) { + List evaledPaths = evaluate(walkedPath); + if (evaledPaths == null) { + return Optional.empty(); + } + + return traversr.get(data, evaledPaths); + } + + /** + * Use the supplied WalkedPath, in the evaluation of each of our PathElements. + *

+ * If our PathElements contained a TransposePathElement, we may return null. + * + * @param walkedPath used to lookup/evaluate PathElement references values like "&1(2)" + * @return null or fully evaluated Strings, possibly with concrete array references like "photos.[3]" + */ + // Visible for testing + public List evaluate(WalkedPath walkedPath) { + + List strings = new ArrayList<>(elements.size()); + for (EvaluatablePathElement pathElement : elements) { + + String evaledLeafOutput = pathElement.evaluate(walkedPath); + if (evaledLeafOutput == null) { + // If this output path contains a TransposePathElement, and when evaluated, + // return null, then bail + return null; + } + strings.add(evaledLeafOutput); + } + + return strings; + } + + public int size() { + return elements.size(); + } + + public PathElement get(int index) { + return elements.get(index); + } + + /** + * Testing method. + */ + public String getCanonicalForm() { + StringBuilder buf = new StringBuilder(); + + for (PathElement pe : elements) { + buf.append(".").append(pe.getCanonicalForm()); + } + + return buf.substring(1); // strip the leading "." + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/SpecStringParser.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/SpecStringParser.java similarity index 57% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/SpecStringParser.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/SpecStringParser.java index f8ce8bc6..81bf6a1b 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/SpecStringParser.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/SpecStringParser.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +15,9 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common; +package io.joltcommunity.jolt.common; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.SpecException; import java.util.Iterator; import java.util.LinkedList; @@ -30,22 +31,23 @@ public class SpecStringParser { - private SpecStringParser() {} + private SpecStringParser() { + } /** * Method that recursively parses a dotNotation String based on an iterator. - * + *

* This method will call out to parseAtPathElement * - * @param pathStrings List to store parsed Strings that each represent a PathElement - * @param iter the iterator to pull characters from + * @param pathStrings List to store parsed Strings that each represent a PathElement + * @param iter the iterator to pull characters from * @param dotNotationRef the original dotNotation string used for error messages * @return evaluated List from dot notation string spec */ - public static List parseDotNotation( List pathStrings, Iterator iter, - String dotNotationRef ) { + public static List parseDotNotation(List pathStrings, Iterator iter, + String dotNotationRef) { - if ( ! iter.hasNext() ) { + if (!iter.hasNext()) { return pathStrings; } @@ -57,53 +59,45 @@ public static List parseDotNotation( List pathStrings, Iterator< StringBuilder sb = new StringBuilder(); char c; - while( iter.hasNext() ) { + while (iter.hasNext()) { c = iter.next(); + // current is Escape only if the char is escape, or + // it is an Escape and the prior char was, then don't consider this one an escape + currIsEscape = c == '\\' && !prevIsEscape; - currIsEscape = false; - if ( c == '\\' && ! prevIsEscape ) { - // current is Escape only if the char is escape, or - // it is an Escape and the prior char was, then don't consider this one an escape - currIsEscape = true; - } - - if ( prevIsEscape && c != '.' && c != '\\') { - sb.append( '\\' ); - sb.append( c ); - } - else if( c == '@' ) { - sb.append( '@' ); - sb.append( parseAtPathElement( iter, dotNotationRef ) ); + if (prevIsEscape && c != '.' && c != '\\') { + sb.append('\\'); + sb.append(c); + } else if (c == '@') { + sb.append('@'); + sb.append(parseAtPathElement(iter, dotNotationRef)); // there was a "[" seen but no "]" - boolean isPartOfArray = sb.indexOf( "[" ) != -1 && sb.indexOf( "]" ) == -1; - if ( ! isPartOfArray ) { - pathStrings.add( sb.toString() ); + boolean isPartOfArray = sb.indexOf("[") != -1 && sb.indexOf("]") == -1; + if (!isPartOfArray) { + pathStrings.add(sb.toString()); sb = new StringBuilder(); } - } - else if ( c == '.' ) { + } else if (c == '.') { - if ( prevIsEscape ) { - sb.append( '.' ); - } - else { - if ( sb.length() != 0 ) { - pathStrings.add( sb.toString() ); + if (prevIsEscape) { + sb.append('.'); + } else { + if (!sb.isEmpty()) { + pathStrings.add(sb.toString()); } - return parseDotNotation( pathStrings, iter, dotNotationRef ); + return parseDotNotation(pathStrings, iter, dotNotationRef); } - } - else if ( ! currIsEscape ) { - sb.append( c ); + } else if (!currIsEscape) { + sb.append(c); } prevIsEscape = currIsEscape; } - if ( sb.length() != 0 ) { - pathStrings.add( sb.toString() ); + if (!sb.isEmpty()) { + pathStrings.add(sb.toString()); } return pathStrings; } @@ -116,7 +110,7 @@ public static Iterator stringIterator(final String string) { if (string == null) throw new NullPointerException(); - return new Iterator() { + return new Iterator<>() { private int index = 0; public boolean hasNext() { @@ -141,7 +135,7 @@ public void remove() { /** * Given a dotNotation style outputPath like "data[2].&(1,1)", this method fixes the syntactic sugar * of "data[2]" --> "data.[2]" - * + *

* This makes all the rest of the String processing easier once we know that we can always * split on the '.' character. * @@ -149,29 +143,28 @@ public void remove() { * @return */ // TODO Unit Test this - public static String fixLeadingBracketSugar( String dotNotaton ) { + public static String fixLeadingBracketSugar(String dotNotaton) { - if ( dotNotaton == null || dotNotaton.length() == 0 ) { + if (dotNotaton == null || dotNotaton.length() == 0) { return ""; } - char prev = dotNotaton.charAt( 0 ); + char prev = dotNotaton.charAt(0); StringBuilder sb = new StringBuilder(); - sb.append( prev ); + sb.append(prev); - for ( int index = 1; index < dotNotaton.length(); index++ ) { - char curr = dotNotaton.charAt( index ); + for (int index = 1; index < dotNotaton.length(); index++) { + char curr = dotNotaton.charAt(index); - if ( curr == '[' && prev != '\\') { - if ( prev == '@' || prev == '.' ) { + if (curr == '[' && prev != '\\') { + if (prev == '@' || prev == '.') { // no need to add an extra '.' - } - else { - sb.append( '.' ); + } else { + sb.append('.'); } } - sb.append( curr ); + sb.append(curr); prev = curr; } @@ -182,16 +175,16 @@ public static String fixLeadingBracketSugar( String dotNotaton ) { * Parse RHS Transpose @ logic. * "@(a.b)" --> pulls "(a.b)" off the iterator * "@a.b" --> pulls just "a" off the iterator - * + *

* This method expects that the the '@' character has already been seen. * - * @param iter iterator to pull data from + * @param iter iterator to pull data from * @param dotNotationRef the original dotNotation string used for error messages */ // TODO Unit Test this - public static String parseAtPathElement( Iterator iter, String dotNotationRef ) { + public static String parseAtPathElement(Iterator iter, String dotNotationRef) { - if ( ! iter.hasNext() ) { + if (!iter.hasNext()) { return ""; } @@ -204,45 +197,42 @@ public static String parseAtPathElement( Iterator iter, String dotNot int atParensCount = 0; char c = iter.next(); - if ( c == '(' ) { + if (c == '(') { isParensAt = true; atParensCount++; - } - else if ( c == '.' ) { - throw new SpecException( "Unable to parse dotNotation, invalid TransposePathElement : " + dotNotationRef ); + } else if (c == '.') { + throw new SpecException("Unable to parse dotNotation, invalid TransposePathElement : " + dotNotationRef); } - sb.append( c ); + sb.append(c); - while( iter.hasNext() ) { + while (iter.hasNext()) { c = iter.next(); - sb.append( c ); + sb.append(c); // Parsing "@(a.b.[&2])" - if ( isParensAt ) { - if ( c == '(' ) { - throw new SpecException( "Unable to parse dotNotation, too many open parens '(' : " + dotNotationRef ); - } - else if ( c == ')' ) { + if (isParensAt) { + if (c == '(') { + throw new SpecException("Unable to parse dotNotation, too many open parens '(' : " + dotNotationRef); + } else if (c == ')') { atParensCount--; } - if ( atParensCount == 0 ) { + if (atParensCount == 0) { return sb.toString(); - } - else if ( atParensCount < 0 ) { - throw new SpecException( "Unable to parse dotNotation, specifically the '@()' part : " + dotNotationRef ); + } else if (atParensCount < 0) { + throw new SpecException("Unable to parse dotNotation, specifically the '@()' part : " + dotNotationRef); } } // Parsing "@abc.def, return a canonical form of "@(abc)" and leave the "def" in the iterator - else if ( c == '.' ) { - return "(" + sb.toString().substring( 0, sb.length() - 1 ) + ")"; + else if (c == '.') { + return "(" + sb.toString().substring(0, sb.length() - 1) + ")"; } } // if we got to the end of the String and we have mismatched parenthesis throw an exception. - if ( isParensAt && atParensCount != 0 ) { - throw new SpecException( "Invalid @() pathElement from : " + dotNotationRef ); + if (isParensAt && atParensCount != 0) { + throw new SpecException("Invalid @() pathElement from : " + dotNotationRef); } // Parsing "@abc" return sb.toString(); @@ -256,18 +246,16 @@ public static String removeEscapedValues(String origKey) { StringBuilder sb = new StringBuilder(); boolean prevWasEscape = false; - for ( char c : origKey.toCharArray() ) { - if ( '\\' == c ) { - if ( prevWasEscape ) { + for (char c : origKey.toCharArray()) { + if ('\\' == c) { + if (prevWasEscape) { prevWasEscape = false; - } - else { + } else { prevWasEscape = true; } - } - else { - if ( ! prevWasEscape ) { - sb.append( c ); + } else { + if (!prevWasEscape) { + sb.append(c); } prevWasEscape = false; } @@ -280,22 +268,20 @@ public static String removeEscapedValues(String origKey) { // given "\@pants" -> "@pants" starts with escape // given "rating-\&pants" -> "rating-&pants" escape in the middle // given "rating\\pants" -> "rating\pants" escape the escape char - public static String removeEscapeChars( String origKey ) { + public static String removeEscapeChars(String origKey) { StringBuilder sb = new StringBuilder(); boolean prevWasEscape = false; - for ( char c : origKey.toCharArray() ) { - if ( '\\' == c ) { - if ( prevWasEscape ) { - sb.append( c ); + for (char c : origKey.toCharArray()) { + if ('\\' == c) { + if (prevWasEscape) { + sb.append(c); prevWasEscape = false; - } - else { + } else { prevWasEscape = true; } - } - else { - sb.append( c ); + } else { + sb.append(c); prevWasEscape = false; } } @@ -304,50 +290,50 @@ public static String removeEscapeChars( String origKey ) { } public static List parseFunctionArgs(String argString) { - List argsList = new LinkedList<>( ); - int firstBracket = argString.indexOf( '(' ); + List argsList = new LinkedList<>(); + int firstBracket = argString.indexOf('('); - String className = argString.substring( 0, firstBracket ); - argsList.add( className ); + String className = argString.substring(0, firstBracket); + argsList.add(className); // drop the first and last ( ) - argString = argString.substring( firstBracket + 1, argString.length() - 1 ); + argString = argString.substring(firstBracket + 1, argString.length() - 1); - StringBuilder sb = new StringBuilder( ); + StringBuilder sb = new StringBuilder(); boolean inBetweenBrackets = false; boolean inBetweenQuotes = false; - for (int i = 0; i < argString.length(); i++){ + for (int i = 0; i < argString.length(); i++) { char c = argString.charAt(i); - switch ( c ) { + switch (c) { case '(': if (!inBetweenQuotes) { inBetweenBrackets = true; } - sb.append( c ); + sb.append(c); break; case ')': if (!inBetweenQuotes) { inBetweenBrackets = false; } - sb.append( c ); + sb.append(c); break; case '\'': inBetweenQuotes = !inBetweenQuotes; - sb.append( c ); + sb.append(c); break; case ',': - if ( !inBetweenBrackets && !inBetweenQuotes ) { - argsList.add( sb.toString().trim() ); + if (!inBetweenBrackets && !inBetweenQuotes) { + argsList.add(sb.toString().trim()); sb = new StringBuilder(); break; } default: - sb.append( c ); + sb.append(c); break; } } - argsList.add( sb.toString().trim() ); + argsList.add(sb.toString().trim()); return argsList; } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/TransposeReader.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/TransposeReader.java similarity index 70% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/TransposeReader.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/TransposeReader.java index 22168f52..b046e716 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/TransposeReader.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/TransposeReader.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,28 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common; +package io.joltcommunity.jolt.common; -import com.bazaarvoice.jolt.traversr.SimpleTraversr; -import com.bazaarvoice.jolt.traversr.Traversr; +import io.joltcommunity.jolt.traversr.SimpleTraversr; +import io.joltcommunity.jolt.traversr.Traversr; import java.util.List; /** * The TransposeReader uses a PathEvaluatingTraversal with a SimpleTraversr. - * + *

* This means that as it walks a path in a tree structure (PathEvaluatingTraversal), * it uses the behavior of the SimpleTraversr for tree traversal operations like * get, set, and final set. */ public class TransposeReader extends PathEvaluatingTraversal { - public TransposeReader( String dotNotation ) { - super( dotNotation ); + public TransposeReader(String dotNotation) { + super(dotNotation); } @Override - protected Traversr createTraversr( List paths ) { - return new SimpleTraversr( paths ); + protected Traversr createTraversr(List paths) { + return new SimpleTraversr(paths); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/TraversalBuilder.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/TraversalBuilder.java similarity index 68% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/TraversalBuilder.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/TraversalBuilder.java index a9b03071..350d01d9 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/TraversalBuilder.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/TraversalBuilder.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +15,11 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common; +package io.joltcommunity.jolt.common; -import com.bazaarvoice.jolt.SpecDriven; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; +import io.joltcommunity.jolt.SpecDriven; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; /** * Builds Traversal based on specific implementation of build(String path) @@ -28,32 +29,31 @@ */ public abstract class TraversalBuilder { - public T build( Object rawObj ) { + public T build(Object rawObj) { - if ( ! ( rawObj instanceof String ) ) { - throw new SpecException( "Invalid spec, RHS should be a String or array of Strings. Value in question : " + rawObj ); + if (!(rawObj instanceof String outputPathStr)) { + throw new SpecException("Invalid spec, RHS should be a String or array of Strings. Value in question : " + rawObj); } // Prepend "root" to each output path. // This is needed for the "identity" transform, eg if we are just supposed to put the input into the output // what key do we put it under? - String outputPathStr = (String) rawObj; - if ( StringTools.isBlank( outputPathStr ) ) { + if (StringTools.isBlank(outputPathStr)) { outputPathStr = SpecDriven.ROOT_KEY; - } - else { + } else { outputPathStr = SpecDriven.ROOT_KEY + "." + outputPathStr; } - return buildFromPath( outputPathStr ); + return buildFromPath(outputPathStr); } /** * Given a path to traverse, and based on what Type T of traverser requested, * build and appropriate traversr + * * @param path to trvarse - * @param Type of Traversr required + * @param Type of Traversr required * @return a Traversr of type T that con traverse given path */ - public abstract T buildFromPath( String path ); + public abstract T buildFromPath(String path); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/AmpPathElement.java similarity index 52% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/AmpPathElement.java index 8d3e86a3..1aafbef6 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AmpPathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/AmpPathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,27 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.reference.AmpReference; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.reference.AmpReference; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** - * PathElement class that handles keys with & values, like input: "photos-&(1,1)"" - * It breaks down the string into a series of String or Reference tokens, that can be used to + * PathElement class that handles keys with & values, like input: "photos-&(1,1)" + * It breaks down the string into a series of String or Reference tokens, that can be used to. + *

* 1) match input like "photos-5" where "&(1,1)" evaluated to 5 + *

+ * Extends {@link BasePathElement} and implements {@link MatchablePathElement} and {@link EvaluatablePathElement}. + *

+ * It can be used on the Left and Right hand sides of the spec, + * but most of the cases are on the Right hand side (path as key). */ public class AmpPathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { private final List tokens; private final String canonicalForm; - public AmpPathElement( String key ) { + public AmpPathElement(String key) { super(key); StringBuilder literal = new StringBuilder(); @@ -41,52 +48,51 @@ public AmpPathElement( String key ) { ArrayList tok = new ArrayList<>(); int index = 0; - while( index < key.length() ) { + while (index < key.length()) { - char c = key.charAt( index ); + char c = key.charAt(index); // beginning of reference - if ( c == '&' ) { + if (c == '&') { // store off any literal text captured thus far - if ( literal.length() > 0 ) { - tok.add( literal.toString() ); - canonicalBuilder.append( literal ); + if (!literal.isEmpty()) { + tok.add(literal.toString()); + canonicalBuilder.append(literal); literal = new StringBuilder(); } - int refEnd = findEndOfReference( key.substring( index + 1 ) ); - AmpReference ref = new AmpReference(key.substring(index, index + refEnd + 1) ); - canonicalBuilder.append( ref.getCanonicalForm() ); + int refEnd = findEndOfReference(key.substring(index + 1)); + AmpReference ref = new AmpReference(key.substring(index, index + refEnd + 1)); + canonicalBuilder.append(ref.getCanonicalForm()); - tok.add( ref ); + tok.add(ref); index += refEnd; - } - else { - literal.append( c ); + } else { + literal.append(c); } index++; } - if ( literal.length() > 0 ) { - tok.add( literal.toString() ); - canonicalBuilder.append( literal.toString() ); + if (!literal.isEmpty()) { + tok.add(literal.toString()); + canonicalBuilder.append(literal.toString()); } tok.trimToSize(); - tokens = Collections.unmodifiableList( tok ); + tokens = Collections.unmodifiableList(tok); canonicalForm = canonicalBuilder.toString(); } - private static int findEndOfReference( String key ) { - if( "".equals( key ) ) { + private static int findEndOfReference(String key) { + if ("".equals(key)) { return 0; } - for( int index = 0; index < key.length(); index++ ){ - char c = key.charAt( index ); + for (int index = 0; index < key.length(); index++) { + char c = key.charAt(index); // keep going till we see something other than a digit, parens, or comma - if( ! Character.isDigit( c ) && c != '(' && c != ')' && c != ',') { + if (!Character.isDigit(c) && c != '(' && c != ')' && c != ',') { return index; } } @@ -104,21 +110,20 @@ public List getTokens() { } @Override - public String evaluate( WalkedPath walkedPath ) { + public String evaluate(WalkedPath walkedPath) { - // Walk thru our tokens and build up a string + // Walk through our tokens and build up a string // Use the supplied Path to fill in our token References StringBuilder output = new StringBuilder(); - for ( Object token : tokens ) { - if ( token instanceof String ) { - output.append( token ); - } - else { + for (Object token : tokens) { + if (token instanceof String) { + output.append(token); + } else { AmpReference ref = (AmpReference) token; - MatchedElement matchedElement = walkedPath.elementFromEnd( ref.getPathIndex() ).getMatchedElement(); - String value = matchedElement.getSubKeyRef( ref.getKeyGroup() ); - output.append( value ); + MatchedElement matchedElement = walkedPath.elementFromEnd(ref.getPathIndex()).getMatchedElement(); + String value = matchedElement.getSubKeyRef(ref.getKeyGroup()); + output.append(value); } } @@ -126,10 +131,10 @@ public String evaluate( WalkedPath walkedPath ) { } @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { - String evaled = evaluate( walkedPath ); - if ( evaled.equals( dataKey ) ) { - return new MatchedElement( evaled ); + public MatchedElement match(String dataKey, WalkedPath walkedPath) { + String evaled = evaluate(walkedPath); + if (evaled.equals(dataKey)) { + return new MatchedElement(evaled); } return null; } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/ArrayPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/ArrayPathElement.java new file mode 100644 index 00000000..a250b4f2 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/ArrayPathElement.java @@ -0,0 +1,178 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.reference.AmpReference; +import io.joltcommunity.jolt.common.reference.HashReference; +import io.joltcommunity.jolt.common.reference.PathAndGroupReference; +import io.joltcommunity.jolt.common.reference.PathReference; +import io.joltcommunity.jolt.common.tree.ArrayMatchedElement; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; + +/** + * Represents a path element for array access in a Jolt transformation specification. + * Handles different array path types such as auto-expand, explicit index, references, hash, and transpose. + * Provides evaluation and matching logic for array path elements. + *

+ * It can be used on the Right hand sides of the spec only. + */ +public class ArrayPathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { + + private final ArrayPathType arrayPathType; + private final PathReference ref; + private final TransposePathElement transposePathElement; + private final String canonicalForm; + private final String arrayIndex; + + public ArrayPathElement(String key) { + super(key); + + if (key.charAt(0) != '[' || key.charAt(key.length() - 1) != ']') { + throw new SpecException("Invalid ArrayPathElement key:" + key); + } + + ArrayPathType apt; + PathReference r = null; + TransposePathElement tpe = null; + String aI = ""; + + if (key.length() == 2) { + apt = ArrayPathType.AUTO_EXPAND; + canonicalForm = "[]"; + } else { + String meat = key.substring(1, key.length() - 1); // trim the [ ] + char firstChar = meat.charAt(0); + + if (AmpReference.TOKEN.equals(firstChar)) { + r = new AmpReference(meat); + apt = ArrayPathType.REFERENCE; + canonicalForm = "[" + r.getCanonicalForm() + "]"; + } else if (HashReference.TOKEN.equals(firstChar)) { + r = new HashReference(meat); + apt = ArrayPathType.HASH; + + canonicalForm = "[" + r.getCanonicalForm() + "]"; + } else if ('@' == firstChar) { + apt = ArrayPathType.TRANSPOSE; + + tpe = TransposePathElement.parse(meat); + canonicalForm = "[" + tpe.getCanonicalForm() + "]"; + } else { + aI = verifyStringIsNonNegativeInteger(meat); + if (aI != null) { + apt = ArrayPathType.EXPLICIT_INDEX; + canonicalForm = "[" + aI + "]"; + } else { + throw new SpecException("Bad explicit array index:" + meat + " from key:" + key); + } + } + } + + transposePathElement = tpe; + arrayPathType = apt; + ref = r; + arrayIndex = aI; + } + + /** + * @return the String version of a non-Negative integer, else null + */ + private static String verifyStringIsNonNegativeInteger(String key) { + try { + int number = Integer.parseInt(key); + if (number >= 0) { + return key; + } else { + return null; + } + } catch (NumberFormatException nfe) { + // Jolt should not throw any exceptions just because the input data does not match what is expected. + // Thus the exception is being swallowed. + return null; + } + } + + @Override + public String getCanonicalForm() { + return canonicalForm; + } + + @Override + public String evaluate(WalkedPath walkedPath) { + + switch (arrayPathType) { + case AUTO_EXPAND: + return canonicalForm; + + case EXPLICIT_INDEX: + return arrayIndex; + + case HASH: + MatchedElement element = walkedPath.elementFromEnd(ref.getPathIndex()).getMatchedElement(); + return Integer.toString(element.getHashCount()); + + case TRANSPOSE: + String key = transposePathElement.evaluate(walkedPath); + return verifyStringIsNonNegativeInteger(key); + + case REFERENCE: + MatchedElement lpe = walkedPath.elementFromEnd(ref.getPathIndex()).getMatchedElement(); + String keyPart; + + if (ref instanceof PathAndGroupReference) { + keyPart = lpe.getSubKeyRef(((PathAndGroupReference) ref).getKeyGroup()); + } else { + keyPart = lpe.getSubKeyRef(0); + } + + return verifyStringIsNonNegativeInteger(keyPart); + default: + throw new IllegalStateException("ArrayPathType enum added two without updating this switch statement."); + } + } + + public Integer getExplicitArrayIndex() { + try { + return Integer.parseInt(arrayIndex); + } catch (Exception ignored) { + return null; + } + } + + public boolean isExplicitArrayIndex() { + return arrayPathType.equals(ArrayPathType.EXPLICIT_INDEX); + } + + @Override + public MatchedElement match(String dataKey, WalkedPath walkedPath) { + String evaled = evaluate(walkedPath); + if (evaled.equals(dataKey)) { + Optional origSizeOptional = walkedPath.lastElement().getOrigSize(); + if (origSizeOptional.isPresent()) { + return new ArrayMatchedElement(evaled, origSizeOptional.get()); + } else { + return null; + } + } + return null; + } + + public enum ArrayPathType {AUTO_EXPAND, REFERENCE, HASH, TRANSPOSE, EXPLICIT_INDEX} +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/AtPathElement.java similarity index 51% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/AtPathElement.java index 2a73c0ca..96871a16 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/AtPathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/AtPathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,22 +14,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +/** + * Path element representing the "@" key, which references the input in a Jolt transformation path. + * Only a single "@" is allowed as the key. + * Extends {@link BasePathElement} and implements {@link MatchablePathElement}. + *

+ * It can be used on the Left hand sides of the spec only. + */ public class AtPathElement extends BasePathElement implements MatchablePathElement { - public AtPathElement( String key ) { + public AtPathElement(String key) { super(key); - if ( ! "@".equals( key ) ) { - throw new SpecException( "'References Input' key '@', can only be a single '@'. Offending key : " + key ); + if (!"@".equals(key)) { + throw new SpecException("'References Input' key '@', can only be a single '@'. Offending key : " + key); } } - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { + public MatchedElement match(String dataKey, WalkedPath walkedPath) { return walkedPath.lastElement().getMatchedElement(); // copy what our parent was so that write keys of &0 and &1 both work. } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/BasePathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/BasePathElement.java similarity index 83% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/BasePathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/BasePathElement.java index e1e2c71e..e37ef905 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/BasePathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/BasePathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; public abstract class BasePathElement implements PathElement { private final String rawKey; - public BasePathElement( String key ) { + public BasePathElement(String key) { rawKey = key; } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/DollarPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/DollarPathElement.java new file mode 100644 index 00000000..5eb2c415 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/DollarPathElement.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.reference.DollarReference; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; + +/** + * Represents a path element that uses dollar-style references for dynamic key resolution + * within a Jolt transformation path. This element evaluates its value based on the + * {@link DollarReference} provided in the key, allowing for flexible referencing of + * previously matched elements in the {@link WalkedPath}. + * + *

Example usage: $ or $(0,1) in a Jolt spec.

+ * + * Implements: + *
    + *
  • {@link MatchablePathElement} for matching path elements
  • + *
  • {@link EvaluatablePathElement} for evaluating dynamic references
  • + *
+ * + *

+ * It can be used on the Left hand sides of the spec only. + */ +public class DollarPathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { + + private final DollarReference dRef; + + public DollarPathElement(String key) { + super(key); + + dRef = new DollarReference(key); + } + + @Override + public String getCanonicalForm() { + return dRef.getCanonicalForm(); + } + + @Override + public String evaluate(WalkedPath walkedPath) { + MatchedElement pe = walkedPath.elementFromEnd(dRef.getPathIndex()).getMatchedElement(); + return pe.getSubKeyRef(dRef.getKeyGroup()); + } + + @Override + public MatchedElement match(String dataKey, WalkedPath walkedPath) { + String evaled = evaluate(walkedPath); + return new MatchedElement(evaled); + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/EvaluatablePathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/EvaluatablePathElement.java similarity index 74% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/EvaluatablePathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/EvaluatablePathElement.java index 62e36bb3..d9bf2898 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/EvaluatablePathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/EvaluatablePathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,16 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.tree.WalkedPath; public interface EvaluatablePathElement extends PathElement { /** - * Evaluate this key as if it is an write path element. + * Evaluate this key as if it is a write path element. + * * @param walkedPath "up the tree" list of LiteralPathElements, that may be used by this key as it is computing * @return String path element to use for write tree building */ - String evaluate( WalkedPath walkedPath ); + String evaluate(WalkedPath walkedPath); } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/HashPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/HashPathElement.java new file mode 100644 index 00000000..d4d4a4e0 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/HashPathElement.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; + +/** + * For use on the LHS, allows the user to specify an explicit string to write out. + * Aka given a input that is boolean, would want to write something out other than "true" / "false". + *

+ * It can be used on the Left hand sides of the spec only. + */ +public class HashPathElement extends BasePathElement implements MatchablePathElement { + + private final String keyValue; + + public HashPathElement(String key) { + super(key); + + if (StringTools.isBlank(key)) { + throw new SpecException("HashPathElement cannot have empty String as input."); + } + + if (!key.startsWith("#")) { + throw new SpecException("LHS # should start with a # : " + key); + } + + if (key.length() < 2) { + throw new SpecException("HashPathElement input is too short : " + key); + } + + if (key.charAt(1) == '(') { + if (key.charAt(key.length() - 1) == ')') { + keyValue = key.substring(2, key.length() - 1); + } else { + throw new SpecException("HashPathElement, mismatched parens : " + key); + } + } else { + keyValue = key.substring(1); + } + } + + @Override + public String getCanonicalForm() { + return "#(" + keyValue + ")"; + } + + @Override + public MatchedElement match(String dataKey, WalkedPath walkedPath) { + return new MatchedElement(keyValue); + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/LiteralPathElement.java similarity index 62% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/LiteralPathElement.java index 1db1570a..8ca915b5 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/LiteralPathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/LiteralPathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,34 +14,34 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; /** * Meant to be an immutable PathElement from a Spec, and therefore shareable across - * threads running multiple transforms using the same spec. + * threads running multiple transforms using the same spec. */ public class LiteralPathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { private final String canonicalForm; - public LiteralPathElement( String key ) { + public LiteralPathElement(String key) { super(key); - this.canonicalForm = key.replace( ".", "\\." ); + this.canonicalForm = key.replace(".", "\\."); } @Override - public String evaluate( WalkedPath walkedPath ) { + public String evaluate(WalkedPath walkedPath) { return getRawKey(); } @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { - if ( getRawKey().equals( dataKey ) ) { - return new MatchedElement( getRawKey() ); + public MatchedElement match(String dataKey, WalkedPath walkedPath) { + if (getRawKey().equals(dataKey)) { + return new MatchedElement(getRawKey()); } return null; } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/MatchablePathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/MatchablePathElement.java similarity index 73% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/MatchablePathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/MatchablePathElement.java index 469796f6..30ee30c1 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/MatchablePathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/MatchablePathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,21 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; public interface MatchablePathElement extends PathElement { /** * See if this PathElement matches the given dataKey. If it does not match, this method returns null. - * + *

* If this PathElement does match, it returns a LiteralPathElement with subKeys filled in. * - * @param dataKey String key value from the input data + * @param dataKey String key value from the input data * @param walkedPath "up the tree" list of LiteralPathElements, that may be used by this key as it is computing its match * @return null or a matched LiteralPathElement */ - MatchedElement match( String dataKey, WalkedPath walkedPath ); + MatchedElement match(String dataKey, WalkedPath walkedPath); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/PathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/PathElement.java similarity index 83% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/PathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/PathElement.java index 49a3f0a0..a793cebf 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/PathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/PathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; public interface PathElement { @@ -21,7 +22,8 @@ public interface PathElement { /** * Get the canonical form of this PathElement. Really only interesting for the Reference Path element, where - * it will expand "&" to "&0(0)". + * it will expand "&" to "&0(0)". + * * @return canonical String version of this PathElement */ String getCanonicalForm(); diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarAllPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarAllPathElement.java similarity index 56% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarAllPathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarAllPathElement.java index 65998fb2..fda9da94 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarAllPathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarAllPathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,22 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.tree.ArrayMatchedElement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.tree.ArrayMatchedElement; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; /** * PathElement for the lone "*" wildcard. In this case we can avoid doing any - * regex or string comparison work at all. + * regex or string comparison work at all. */ public class StarAllPathElement implements StarPathElement { - public StarAllPathElement( String key ) { - if ( ! "*".equals( key ) ) { - throw new IllegalArgumentException( "StarAllPathElement key should just be a single '*'. Was: " + key ); + public StarAllPathElement(String key) { + if (!"*".equals(key)) { + throw new SpecException("StarAllPathElement key should just be a single '*'. Was: " + key); } } @@ -37,18 +39,17 @@ public StarAllPathElement( String key ) { * @return true if the provided literal will match this Element's regex */ @Override - public boolean stringMatch( String literal ) { + public boolean stringMatch(String literal) { return true; } @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { + public MatchedElement match(String dataKey, WalkedPath walkedPath) { Optional origSizeOptional = walkedPath.lastElement().getOrigSize(); - if(origSizeOptional.isPresent()) { - return new ArrayMatchedElement( dataKey, origSizeOptional.get() ); - } - else { - return new MatchedElement( dataKey ); + if (origSizeOptional.isPresent()) { + return new ArrayMatchedElement(dataKey, origSizeOptional.get()); + } else { + return new MatchedElement(dataKey); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarDoublePathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarDoublePathElement.java similarity index 66% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarDoublePathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarDoublePathElement.java index cfaf6e7f..33d16e82 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarDoublePathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarDoublePathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,58 +14,58 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.utils.StringTools; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; import java.util.ArrayList; import java.util.List; /** - * PathElement for the a double "*" wildcard such as tag-*-*. In this case we can avoid doing any - * regex work by doing String begins, ends and mid element exists. + * PathElement for the a double "*" wildcard such as tag-*-*. In this case we can avoid doing any + * regex work by doing String begins, ends and mid element exists. */ public class StarDoublePathElement extends BasePathElement implements StarPathElement { - private final String prefix,suffix, mid; + private final String prefix, suffix, mid; - /**+ + /** + * + * * @param key : should be a String with two "*" elements. */ public StarDoublePathElement(String key) { super(key); - if ( StringTools.countMatches(key, "*") != 2 ) { - throw new IllegalArgumentException( "StarDoublePathElement should have two '*' in its key. Was: " + key ); + if (StringTools.countMatches(key, "*") != 2) { + throw new SpecException("StarDoublePathElement should have two '*' in its key. Was: " + key); } String[] split = key.split("\\*"); - boolean startsWithStar = key.startsWith( "*" ); + boolean startsWithStar = key.startsWith("*"); boolean endsWithStar = key.endsWith("*"); - if ( startsWithStar && endsWithStar) { + if (startsWithStar && endsWithStar) { prefix = ""; mid = split[1]; suffix = ""; - } - else if ( endsWithStar ) { + } else if (endsWithStar) { prefix = split[0]; mid = split[1]; suffix = ""; - } - else if ( startsWithStar ) { + } else if (startsWithStar) { prefix = ""; mid = split[1]; suffix = split[2]; - } - else{ - prefix=split[0]; - mid=split[1]; - suffix=split[2]; + } else { + prefix = split[0]; + mid = split[1]; + suffix = split[2]; } } + /** * @param literal test to see if the provided string will match this Element's regex * @return true if the provided literal will match this Element's regex @@ -72,7 +73,7 @@ else if ( startsWithStar ) { @Override public boolean stringMatch(String literal) { boolean isMatch = false; - if(literal.startsWith(prefix) && literal.endsWith(suffix)){ + if (literal.startsWith(prefix) && literal.endsWith(suffix)) { isMatch = finMidIndex(literal) > 0; } @@ -84,7 +85,7 @@ public boolean stringMatch(String literal) { * starts, we have found a mid match. Also, it will be the first occurrence of the mid in the literal, so we are not 'greedy' to capture as much as * in the '*' */ - private int finMidIndex(String literal){ + private int finMidIndex(String literal) { int startOffset = prefix.length() + 1; int endOffset = literal.length() - suffix.length() - 1; @@ -98,16 +99,16 @@ private int finMidIndex(String literal){ * endoffset -> 5 - 0 - 1 = 4 * We are left with no substring to search for the mid. Bail out! */ - if(startOffset >= endOffset) { + if (startOffset >= endOffset) { return -1; } int midIndex = literal.substring(startOffset, endOffset).indexOf(mid); - if(midIndex >= 0) { + if (midIndex >= 0) { - return midIndex + startOffset; + return midIndex + startOffset; } return -1; } @@ -115,17 +116,17 @@ private int finMidIndex(String literal){ @Override public MatchedElement match(String dataKey, WalkedPath walkedPath) { - if ( stringMatch( dataKey ) ) { + if (stringMatch(dataKey)) { List subKeys = new ArrayList<>(2); int midStart = finMidIndex(dataKey); int midEnd = midStart + mid.length(); - String firstStarPart = dataKey.substring( prefix.length(), midStart); - subKeys.add( firstStarPart ); + String firstStarPart = dataKey.substring(prefix.length(), midStart); + subKeys.add(firstStarPart); - String secondStarPart = dataKey.substring( midEnd, dataKey.length() - suffix.length() ); - subKeys.add( secondStarPart ); + String secondStarPart = dataKey.substring(midEnd, dataKey.length() - suffix.length()); + subKeys.add(secondStarPart); return new MatchedElement(dataKey, subKeys); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarPathElement.java similarity index 83% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarPathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarPathElement.java index 71a98b96..fb0bf99f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarPathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarPathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; /** * Marker interface for PathElements that contain the "*" wildcard. - * + *

* Three subclasses were created for performance reasons. */ public interface StarPathElement extends MatchablePathElement { @@ -27,5 +28,5 @@ public interface StarPathElement extends MatchablePathElement { * * @return true if the provided literal will match this Element's regex */ - public boolean stringMatch( String literal ); + public boolean stringMatch(String literal); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarRegexPathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarRegexPathElement.java similarity index 77% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarRegexPathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarRegexPathElement.java index 2977cc9f..dc5c95c6 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarRegexPathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarRegexPathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; import java.util.ArrayList; import java.util.HashSet; @@ -32,18 +33,18 @@ public class StarRegexPathElement extends BasePathElement implements StarPathEle private final Pattern pattern; - public StarRegexPathElement( String key ) { + public StarRegexPathElement(String key) { super(key); - pattern = makePattern( key ); + pattern = makePattern(key); } - private static Pattern makePattern( String key ) { + private static Pattern makePattern(String key) { // "rating-*-*" -> "^rating-(.+?)-(.+?)$" aka the '*' must match something in a non-greedy way key = escapeMetacharsIfAny(key); - String regex = "^" + key.replace("*", "(.+?)") + "$"; + String regex = "^" + key.replace("*", "(.+?)") + "$"; /* wtf does "(.+?)" mean @@ -56,20 +57,21 @@ private static Pattern makePattern( String key ) { Differences Among Greedy, Reluctant, and Possessive Quantifiers section */ - return Pattern.compile( regex); + return Pattern.compile(regex); } // Metachars to escape .^$|*+?()[{\ in a regex - /** + + /** + * + * * @param key : String key that needs to be escaped before compiling into regex. * @return : Metachar escaped key. - * + *

* Regex has some special meaning for the metachars [ .^$|*+?()[{\ ].If any of these metachars is present in the pattern key that was passed, it needs to be escaped so that * it can be matched against literal. */ - private static String escapeMetacharsIfAny(String key){ + private static String escapeMetacharsIfAny(String key) { char[] keyChars = key.toCharArray(); @@ -79,7 +81,7 @@ private static String escapeMetacharsIfAny(String key){ Set charsAlreadySeen = new HashSet<>(); - for(char keychar: keyChars) { + for (char keychar : keyChars) { switch (keychar) { @@ -95,7 +97,7 @@ private static String escapeMetacharsIfAny(String key){ case '+': case '.': - if(!charsAlreadySeen.contains( keychar )){ + if (!charsAlreadySeen.contains(keychar)) { key = key.replace(String.valueOf(keychar), "\\" + keychar); @@ -115,26 +117,26 @@ private static String escapeMetacharsIfAny(String key){ * @return true if the provided literal will match this Element's regex */ @Override - public boolean stringMatch( String literal ) { + public boolean stringMatch(String literal) { - Matcher matcher = pattern.matcher( literal ); + Matcher matcher = pattern.matcher(literal); return matcher.find(); } @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { + public MatchedElement match(String dataKey, WalkedPath walkedPath) { - Matcher matcher = pattern.matcher( dataKey ); - if ( ! matcher.find() ) { + Matcher matcher = pattern.matcher(dataKey); + if (!matcher.find()) { return null; } int groupCount = matcher.groupCount(); List subKeys = new ArrayList<>(groupCount); - for ( int index = 1; index <= groupCount; index++) { - subKeys.add( matcher.group( index ) ); + for (int index = 1; index <= groupCount; index++) { + subKeys.add(matcher.group(index)); } return new MatchedElement(dataKey, subKeys); diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarSinglePathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarSinglePathElement.java similarity index 52% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarSinglePathElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarSinglePathElement.java index 7cbe3bf5..48ebd9c4 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/pathelement/StarSinglePathElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/StarSinglePathElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,44 +14,41 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.pathelement; +package io.joltcommunity.jolt.common.pathelement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.utils.StringTools; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; import java.util.ArrayList; import java.util.List; /** * PathElement for the a single "*" wildcard such as tag-*. In this case we can avoid doing any - * regex work by doing String begins and ends with comparisons. + * regex work by doing String begins and ends with comparisons. */ public class StarSinglePathElement extends BasePathElement implements StarPathElement { - private final String prefix,suffix; + private final String prefix, suffix; - public StarSinglePathElement( String key ) { + public StarSinglePathElement(String key) { super(key); - if ( StringTools.countMatches(key, "*") != 1 ) { - throw new IllegalArgumentException( "StarSinglePathElement should only have one '*' in its key. Was: " + key ); - } - else if ( "*".equals( key ) ) { - throw new IllegalArgumentException( "StarSinglePathElement should have a key that is just '*'. Was: " + key ); + if (StringTools.countMatches(key, "*") != 1) { + throw new SpecException("StarSinglePathElement should only have one '*' in its key. Was: " + key); + } else if ("*".equals(key)) { + throw new SpecException("StarSinglePathElement should have a key that is just '*'. Was: " + key); } - if ( key.startsWith( "*" ) ) { + if (key.startsWith("*")) { prefix = ""; - suffix = key.substring( 1 ); - } - else if ( key.endsWith( "*" ) ) { - prefix = key.substring( 0, key.length() -1 ); + suffix = key.substring(1); + } else if (key.endsWith("*")) { + prefix = key.substring(0, key.length() - 1); suffix = ""; - } - else - { - String[] split = key.split( "\\*" ); + } else { + String[] split = key.split("\\*"); prefix = split[0]; suffix = split[1]; } @@ -61,19 +59,19 @@ else if ( key.endsWith( "*" ) ) { * @return true if the provided literal will match this Element's regex */ @Override - public boolean stringMatch( String literal ) { - return literal.startsWith( prefix ) && literal.endsWith( suffix ) // the ends match + public boolean stringMatch(String literal) { + return literal.startsWith(prefix) && literal.endsWith(suffix) // the ends match && literal.length() > prefix.length() + suffix.length(); // and the * captures something } @Override - public MatchedElement match( String dataKey, WalkedPath walkedPath ) { + public MatchedElement match(String dataKey, WalkedPath walkedPath) { - if ( stringMatch( dataKey ) ) { + if (stringMatch(dataKey)) { List subKeys = new ArrayList<>(1); - String starPart = dataKey.substring( prefix.length(), dataKey.length() - suffix.length() ); - subKeys.add( starPart ); + String starPart = dataKey.substring(prefix.length(), dataKey.length() - suffix.length()); + subKeys.add(starPart); return new MatchedElement(dataKey, subKeys); } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/TransposePathElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/TransposePathElement.java new file mode 100644 index 00000000..a640b01c --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/pathelement/TransposePathElement.java @@ -0,0 +1,248 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.TransposeReader; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.PathStep; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; + +/** + * This PathElement is used by Shiftr to Transpose data. + *

+ * It can be used on the Left and Right hand sides of the spec. + *

+ * Input + *

+ * {
+ *   "author" : "Stephen Hawking",
+ *   "book" : "A Brief History of Time"
+ * }
+ * 
+ * Wanted + *
+ * {
+ *   "Stephen Hawking" : "A Brief History of Time"
+ * }
+ * 
+ *

+ * The first part of the process is to allow a CompositeShiftr node to look down the input JSON tree. + *

+ * Spec + *

+ * {
+ *   "@book" : "@author"
+ * }
+ * 
+ *

+ * Secondly, we can look up the tree, and come down a different path to locate data. + * For example of this see the following ShiftrUnit tests : + *

+ * LHS Lookup : json/shiftr/filterParents.json
+ * RHS Lookup : json/shiftr/transposeComplex6_rhs-complex-at.json
+ * 
+ *

+ * CanonicalForm Expansion + *

+ * Sugar
+ * "@2         -> "@(2,)
+ * "@(2)       -> "@(2,)
+ * "@author"   -> "@(0,author)"
+ * "@(author)" -> "@(0,author)"
+ * 
+ * Splenda
+ * "@(a.b)"    -> "@(0,a.b)"
+ * "@(a.&2.c)" -> "@(0,a.&(2,0).c)"
+ * 
+ */ +public class TransposePathElement extends BasePathElement implements MatchablePathElement, EvaluatablePathElement { + + private final int upLevel; + private final TransposeReader subPathReader; + private final String canonicalForm; + + /** + * Private constructor used after parsing is done. + * + * @param originalKey for reference + * @param upLevel How far up the tree to go + * @param subPath Where to go down the tree + */ + private TransposePathElement(String originalKey, int upLevel, String subPath) { + super(originalKey); + this.upLevel = upLevel; + if (StringTools.isEmpty(subPath)) { + this.subPathReader = null; + canonicalForm = "@(" + upLevel + ",)"; + } else { + subPathReader = new TransposeReader(subPath); + canonicalForm = "@(" + upLevel + "," + subPathReader.getCanonicalForm() + ")"; + } + } + + /** + * Parse a text value from a Spec, into a TransposePathElement. + * + * @param key rawKey from a Jolt Spec file + * @return a TransposePathElement + */ + public static TransposePathElement parse(String key) { + + if (key == null || key.length() < 2) { + throw new SpecException("'Transpose Input' key '@', can not be null or of length 1. Offending key : " + key); + } + if ('@' != key.charAt(0)) { + throw new SpecException("'Transpose Input' key must start with an '@'. Offending key : " + key); + } + + // Strip off the leading '@' as we don't need it anymore. + String meat = key.substring(1); + + if (meat.contains("@")) { + throw new SpecException("@ pathElement can not contain a nested @. Was: " + meat); + } + if (meat.contains("*") || meat.contains("[]")) { + throw new SpecException("'Transpose Input' can not contain expansion wildcards (* and []). Offending key : " + key); + } + + // Check to see if the key is wrapped by parens + if (meat.startsWith("(")) { + if (meat.endsWith(")")) { + meat = meat.substring(1, meat.length() - 1); + } else { + throw new SpecException("@ path element that starts with '(' must have a matching ')'. Offending key : " + key); + } + } + + return innerParse(key, meat); + } + + /** + * Parse the core of the TransposePathElement key, once basic errors have been checked and + * syntax has been handled. + * + * @param originalKey The original text for reference. + * @param meat The string to actually parse into a TransposePathElement + * @return TransposePathElement + */ + private static TransposePathElement innerParse(String originalKey, String meat) { + + char first = meat.charAt(0); + if (Character.isDigit(first)) { + // loop until we find a comma or end of string + StringBuilder sb = new StringBuilder().append(first); + for (int index = 1; index < meat.length(); index++) { + char c = meat.charAt(index); + + // when we find a / the first comma, stop looking for integers, and just assume the rest is a String path + if (',' == c) { + + int upLevel; + try { + upLevel = Integer.parseInt(sb.toString()); + } catch (NumberFormatException nfe) { + // I don't know how this exception would get thrown, as all the chars were checked by isDigit, but oh well + throw new SpecException("@ path element with non/mixed numeric key is not valid, key=" + originalKey); + } + + return new TransposePathElement(originalKey, upLevel, meat.substring(index + 1)); + } else if (Character.isDigit(c)) { + sb.append(c); + } else { + throw new SpecException("@ path element with non/mixed numeric key is not valid, key=" + originalKey); + } + } + + // if we got out of the for loop, then the whole thing was a number. + return new TransposePathElement(originalKey, Integer.parseInt(sb.toString()), null); + } else { + return new TransposePathElement(originalKey, 0, meat); + } + } + + /** + * This method is used when the TransposePathElement is used on the LFH as data. + *

+ * Aka, normal "evaluate" returns either a Number or a String. + * + * @param walkedPath WalkedPath to evaluate against + * @return The data specified by this TransposePathElement. + */ + public Optional objectEvaluate(WalkedPath walkedPath) { + // Grap the data we need from however far up the tree we are supposed to go + PathStep pathStep = walkedPath.elementFromEnd(upLevel); + + if (pathStep == null) { + return Optional.empty(); + } + + Object treeRef = pathStep.getTreeRef(); + + // Now walk down from that level using the subPathReader + if (subPathReader == null) { + return Optional.of(treeRef); + } else { + return subPathReader.read(treeRef, walkedPath); + } + } + + @Override + public String evaluate(WalkedPath walkedPath) { + + Optional dataFromTranspose = objectEvaluate(walkedPath); + + if (dataFromTranspose.isPresent()) { + + Object data = dataFromTranspose.get(); + + // Coerce a number into a String + if (data instanceof Number) { + // use long instead of int, as we want to support larger numbers + long val = ((Number) data).longValue(); + return Long.toString(val); + } + + // Coerce a boolean into a String + if (data instanceof Boolean) { + return Boolean.toString((Boolean) data); + } + + if (!(data instanceof String)) { + + // If this output path has a TransposePathElement, and when we evaluate it + // it does not resolve to a String, then return null + return null; + } + + return (String) data; + } else { + return null; + } + } + + public MatchedElement match(String dataKey, WalkedPath walkedPath) { + return walkedPath.lastElement().getMatchedElement(); // copy what our parent was so that write keys of &0 and &1 both work. + } + + @Override + public String getCanonicalForm() { + return canonicalForm; + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/AmpReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/AmpReference.java similarity index 84% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/AmpReference.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/AmpReference.java index a19360e0..92e4be42 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/AmpReference.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/AmpReference.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.reference; +package io.joltcommunity.jolt.common.reference; /** * This class parses the Jolt & syntax into useful programmatic constructs. - * + *

* Valid Syntax is : & &1 &(1) &(1,1) */ public class AmpReference extends BasePathAndGroupReference { public static final Character TOKEN = '&'; - public AmpReference( String refStr ) { + public AmpReference(String refStr) { super(refStr); } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/BasePathAndGroupReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/BasePathAndGroupReference.java new file mode 100644 index 00000000..51eb2d3d --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/BasePathAndGroupReference.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.reference; + +import io.joltcommunity.jolt.exception.SpecException; + +/** + * All "References" extend this class and support three level of syntactic sugar + *

+ * Example with the AmpReference + *

+ * 1   "&"
+ * 2   "&0"
+ * 3   "&(0,0)"
+ * all three mean the same thing.
+ * 
+ * References are used to look up values in a WalkedPath. + * In the CanonicalForm the first entry is how far up the WalkedPath to look for a LiteralPathElement, + * and the second entry is which part of that LiteralPathElement to ask for. + */ +public abstract class BasePathAndGroupReference implements PathAndGroupReference { + + private final int keyGroup; // equals 0 for "&" "&0" and "&(x,0)" + private final int pathIndex; // equals 0 for "&" "&0" and "&(0,x)" + + public BasePathAndGroupReference(String refStr) { + + if (refStr == null || refStr.isEmpty() || getToken() != refStr.charAt(0)) { + throw new SpecException("Invalid reference key=" + refStr + " either blank or doesn't start with correct character=" + getToken()); + } + + int pI = 0; + int kG = 0; + + try { + if (refStr.length() > 1) { + + String meat = refStr.substring(1); + + if (meat.length() >= 3 && meat.startsWith("(") && meat.endsWith(")")) { + + // "&(1,2)" -> "1,2".split( "," ) -> String[] { "1", "2" } OR + // "&(3)" -> "3".split( "," ) -> String[] { "3" } + + String parenMeat = meat.substring(1, meat.length() - 1); + String[] intStrs = parenMeat.split(","); + if (intStrs.length > 2) { + throw new SpecException("Invalid Reference=" + refStr); + } + + pI = Integer.parseInt(intStrs[0]); + if (intStrs.length == 2) { + kG = Integer.parseInt(intStrs[1]); + } + } else { // &2 + pI = Integer.parseInt(meat); + } + } + } catch (NumberFormatException nfe) { + throw new SpecException("Unable to parse '" + getToken() + "' reference key:" + refStr, nfe); + } + + if (pI < 0 || kG < 0) { + throw new SpecException("Reference:" + refStr + " can not have a negative value."); + } + + pathIndex = pI; + keyGroup = kG; + } + + protected abstract char getToken(); + + public int getPathIndex() { + return pathIndex; + } + + public int getKeyGroup() { + return keyGroup; + } + + /** + * Builds the non-syntactic sugar / maximally expanded and unique form of this reference. + * + * @return canonical form : aka "&" -> "&(0,0) + */ + public String getCanonicalForm() { + return getToken() + "(" + pathIndex + "," + keyGroup + ")"; + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/BasePathReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/BasePathReference.java similarity index 55% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/BasePathReference.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/BasePathReference.java index ef64f9db..13d2cfd8 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/BasePathReference.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/BasePathReference.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,43 +14,42 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.reference; +package io.joltcommunity.jolt.common.reference; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.SpecException; public abstract class BasePathReference implements PathReference { private final int pathIndex; // equals 0 for "&" "&0" and "&(0,x)" - protected abstract char getToken(); - - public BasePathReference( String refStr ) { + public BasePathReference(String refStr) { - if ( refStr == null || refStr.length() == 0 || getToken() != refStr.charAt( 0 ) ) { - throw new SpecException( "Invalid reference key=" + refStr + " either blank or doesn't start with correct character=" + getToken() ); + if (refStr == null || refStr.isEmpty() || getToken() != refStr.charAt(0)) { + throw new SpecException("Invalid reference key=" + refStr + " either blank or doesn't start with correct character=" + getToken()); } int pathIndex = 0; try { - if ( refStr.length() > 1 ) { + if (refStr.length() > 1) { - String meat = refStr.substring( 1 ); + String meat = refStr.substring(1); - pathIndex = Integer.parseInt( meat ); + pathIndex = Integer.parseInt(meat); } - } - catch( NumberFormatException nfe ) { - throw new SpecException( "Unable to parse '" + getToken() + "' reference key:" + refStr, nfe ); + } catch (NumberFormatException nfe) { + throw new SpecException("Unable to parse '" + getToken() + "' reference key:" + refStr, nfe); } - if ( pathIndex < 0 ) { - throw new SpecException( "Reference:" + refStr + " can not have a negative value." ); + if (pathIndex < 0) { + throw new SpecException("Reference:" + refStr + " can not have a negative value."); } this.pathIndex = pathIndex; } + protected abstract char getToken(); + @Override public int getPathIndex() { return pathIndex; @@ -57,9 +57,10 @@ public int getPathIndex() { /** * Builds the non-syntactic sugar / maximally expanded and unique form of this reference. + * * @return canonical form : aka "#" -> "#0 */ public String getCanonicalForm() { - return getToken() + Integer.toString( pathIndex ); + return getToken() + Integer.toString(pathIndex); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/DollarReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/DollarReference.java similarity index 82% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/DollarReference.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/DollarReference.java index 487b565b..da5ba310 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/DollarReference.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/DollarReference.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.reference; +package io.joltcommunity.jolt.common.reference; public class DollarReference extends BasePathAndGroupReference { public static final Character TOKEN = '$'; - public DollarReference( String refStr ) { + public DollarReference(String refStr) { super(refStr); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/HashReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/HashReference.java similarity index 84% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/HashReference.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/HashReference.java index 5b2244a8..72767564 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/HashReference.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/HashReference.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.reference; +package io.joltcommunity.jolt.common.reference; /** * TODO : Refactor the out to it's own class, as it really isn't a "Reference" @@ -23,7 +24,7 @@ public class HashReference extends BasePathReference { public static final Character TOKEN = '#'; - public HashReference( String refStr ) { + public HashReference(String refStr) { super(refStr); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/PathAndGroupReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/PathAndGroupReference.java similarity index 53% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/PathAndGroupReference.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/PathAndGroupReference.java index af192c74..663c09af 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/PathAndGroupReference.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/PathAndGroupReference.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.reference; +package io.joltcommunity.jolt.common.reference; /** * Reference is used by Shiftr when lookup up values from a WalkedPath (list of LiteralPathElements). - * + *

* Ex given a WalkedPath like : - * WalkedPath : [ - * LiteralPathElement : [ "cdv-Tuna", "Tuna" ], // This LiteralPathElement would be generated by a match of "cdv-*" and the key "cdv-Tuna" - * LiteralPahtElement : [ "Delicious" ] - * ] - * - * &, &0, &(0,0) would all evaluate to "Delicious" - * &1, &1, &(1,0) would all evaluate to "cdv-Tuna" - * &(1,1) would evaluate to "Tuna" + *

+ * WalkedPath : [
+ *   LiteralPathElement : [ "cdv-Tuna", "Tuna" ],   // This LiteralPathElement would be generated by a match of "cdv-*" and the key "cdv-Tuna"
+ *   LiteralPahtElement : [ "Delicious" ]
+ * ]
  *
+ * &,  &0, &(0,0) would all evaluate to "Delicious"
+ * &1, &1, &(1,0) would all evaluate to "cdv-Tuna"
+ * &(1,1) would evaluate to "Tuna"
+ * 
* The "canonical form" is "C(x,y)", where : - * C : the character used to determine the type of Reference - * x : pathIndex : which is how far up the walkedPath the look - * y : keyGroup : where 0 is the whole key, and 1 thru n smaller captured parts of the key - * + *
+ * C : the character used to determine the type of Reference
+ * x : pathIndex : which is how far up the walkedPath the look
+ * y : keyGroup : where 0 is the whole key, and 1 thru n smaller captured parts of the key
+ * 
*/ public interface PathAndGroupReference extends PathReference { diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/PathReference.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/PathReference.java similarity index 69% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/PathReference.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/PathReference.java index 597b609f..b99048e1 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/reference/PathReference.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/reference/PathReference.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,30 +14,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.reference; +package io.joltcommunity.jolt.common.reference; /** * Reference is used by Shiftr when lookup up values from a WalkedPath (list of LiteralPathElements). - * - * #, #0 are the same - * + *

+ * #, #0 are the same + *

* The "canonical form" is "Cx", where : - * C : the character used to determine the type of Reference - * x : pathIndex : which is how far up the walkedPath the look - * + *

+ * C : the character used to determine the type of Reference
+ * x : pathIndex : which is how far up the walkedPath the look
+ * 
*/ public interface PathReference { - public int getPathIndex(); + int getPathIndex(); /** * Get the canonical form of this Reference. - * + *

* One of the uses of this method is to ensure that spec, does not contain "duplicate" keys, aka - * two keys that when you unroll the syntactic sugar, are the same thing. + * two keys that when you unroll the syntactic sugar, are the same thing. * * @return fully expanded String representation of this Reference */ - public String getCanonicalForm(); + String getCanonicalForm(); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/BaseSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/BaseSpec.java similarity index 69% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/BaseSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/BaseSpec.java index e768ff21..eaef2a5c 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/BaseSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/BaseSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +15,11 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common.spec; +package io.joltcommunity.jolt.common.spec; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.pathelement.MatchablePathElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.pathelement.MatchablePathElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; import java.util.Map; @@ -30,21 +31,22 @@ public interface BaseSpec { /** * Gimme the LHS path element + * * @return LHS path element for comparison */ MatchablePathElement getPathElement(); /** * This is the main recursive method of the Shiftr/Templatr/Cardinality parallel "spec" and "input" tree walk. - * + *

* It should return true if this Spec object was able to successfully apply itself given the - * inputKey and input object. - * + * inputKey and input object. + *

* In the context of the Shiftr parallel treewalk, if this method returns true, the assumption - * is that no other sibling Shiftr specs need to look at this particular input key. + * is that no other sibling Shiftr specs need to look at this particular input key. * * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it */ - boolean apply( final String inputKey, final Optional inputOptional, final WalkedPath walkedPath, final Map output, final Map context ); + boolean apply(final String inputKey, final Optional inputOptional, final WalkedPath walkedPath, final Map output, final Map context); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/OrderedCompositeSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/OrderedCompositeSpec.java similarity index 87% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/OrderedCompositeSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/OrderedCompositeSpec.java index 61ee8073..056aeea4 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/OrderedCompositeSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/OrderedCompositeSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +15,9 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common.spec; +package io.joltcommunity.jolt.common.spec; -import com.bazaarvoice.jolt.common.ExecutionStrategy; +import io.joltcommunity.jolt.common.ExecutionStrategy; import java.util.List; import java.util.Map; @@ -25,10 +26,10 @@ * An ordered composite spec denotes the spec will have Literal and Computed children that * must be Ordered Spec, which should be subject to sorting to before applying any of the * determined execution strategies! - * + *

* This is not enforced directly, but these interface methods ensure the executionStrategy * gets the literal and computed children lists to process its exec strategy - * + *

* The order is provided by a Map and then ordering is achieved using a comparator */ public interface OrderedCompositeSpec extends BaseSpec { diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/SpecBuilder.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/SpecBuilder.java similarity index 61% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/SpecBuilder.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/SpecBuilder.java index e02a172a..1f6f2966 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/spec/SpecBuilder.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/spec/SpecBuilder.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,13 +15,9 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common.spec; +package io.joltcommunity.jolt.common.spec; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * Factory class that provides a factory method create(...) that takes itself @@ -37,21 +34,21 @@ public List createSpec(Map rawSpec) { List result = new ArrayList<>(); Set actualKeys = new HashSet<>(); - for ( String rawLhsStr : rawSpec.keySet() ) { + for (String rawLhsStr : rawSpec.keySet()) { - Object rawRhs = rawSpec.get( rawLhsStr ); - String[] keyStrings = rawLhsStr.split( "\\|" ); // unwrap the syntactic sugar of the OR - for ( String keyString : keyStrings ) { + Object rawRhs = rawSpec.get(rawLhsStr); + String[] keyStrings = rawLhsStr.split("\\|"); // unwrap the syntactic sugar of the OR + for (String keyString : keyStrings) { - T childSpec = createSpec( keyString, rawRhs ); + T childSpec = createSpec(keyString, rawRhs); String childCanonicalString = childSpec.getPathElement().getCanonicalForm(); - if ( actualKeys.contains( childCanonicalString ) ) { - throw new IllegalArgumentException( "Duplicate canonical key found : " + childCanonicalString ); + if (actualKeys.contains(childCanonicalString)) { + throw new IllegalArgumentException("Duplicate canonical key found : " + childCanonicalString); } - actualKeys.add( childCanonicalString ); + actualKeys.add(childCanonicalString); result.add(childSpec); } @@ -62,9 +59,10 @@ public List createSpec(Map rawSpec) { /** * Given a lhs key and rhs spec object, determine, create and return appropriate spec - * @param lhsKey lhs key + * + * @param lhsKey lhs key * @param rhsSpec rhs Spec * @return Spec object */ - public abstract T createSpec( String lhsKey, Object rhsSpec ); + public abstract T createSpec(String lhsKey, Object rhsSpec); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/ArrayMatchedElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/ArrayMatchedElement.java similarity index 81% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/ArrayMatchedElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/ArrayMatchedElement.java index 27bd0600..c3c3629b 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/ArrayMatchedElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/ArrayMatchedElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2016 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +15,14 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.common.tree; +package io.joltcommunity.jolt.common.tree; public class ArrayMatchedElement extends MatchedElement { private final int origSize; - public ArrayMatchedElement( String key, int origSize) { - super( key ); + public ArrayMatchedElement(String key, int origSize) { + super(key); this.origSize = origSize; } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/MatchedElement.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/MatchedElement.java similarity index 55% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/MatchedElement.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/MatchedElement.java index 9703700b..431a1662 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/MatchedElement.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/MatchedElement.java @@ -1,5 +1,6 @@ /* - * Copyright 2016 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.tree; +package io.joltcommunity.jolt.common.tree; -import com.bazaarvoice.jolt.common.pathelement.BasePathElement; -import com.bazaarvoice.jolt.common.pathelement.EvaluatablePathElement; +import io.joltcommunity.jolt.common.pathelement.BasePathElement; +import io.joltcommunity.jolt.common.pathelement.EvaluatablePathElement; import java.util.ArrayList; import java.util.Collections; @@ -24,11 +25,11 @@ /** * MatchedElement is the result of a "match" between a spec PathElement and some input data. - * + *

* MatchedElements are not thread safe, and should instead be stack / single Thread/Transform specific. - * - * This mutability was specifically added for the the HashCount functionality, which allows Shiftr - * to transform data form maps to lists. + *

+ * This mutability was specifically added for the HashCount functionality, which allows Shiftr + * to transform data form maps to lists. */ public class MatchedElement extends BasePathElement implements EvaluatablePathElement { @@ -36,31 +37,31 @@ public class MatchedElement extends BasePathElement implements EvaluatablePathEl private int hashCount = 0; - public MatchedElement( String key ) { + public MatchedElement(String key) { super(key); List keys = new ArrayList<>(1); - keys.add( key ); // always add the full key to index 0 + keys.add(key); // always add the full key to index 0 - this.subKeys = Collections.unmodifiableList( keys ); + this.subKeys = Collections.unmodifiableList(keys); } - public MatchedElement( String key, List subKeys ) { + public MatchedElement(String key, List subKeys) { super(key); - if ( subKeys == null ) { - throw new IllegalArgumentException( "MatchedElement for key:" + key + " got null list of subKeys" ); + if (subKeys == null) { + throw new IllegalArgumentException("MatchedElement for key:" + key + " got null list of subKeys"); } - List keys = new ArrayList<>( 1 + subKeys.size() ); - keys.add( key ); // always add the full key to index 0 - keys.addAll( subKeys ); + List keys = new ArrayList<>(1 + subKeys.size()); + keys.add(key); // always add the full key to index 0 + keys.addAll(subKeys); - this.subKeys = Collections.unmodifiableList( keys ); + this.subKeys = Collections.unmodifiableList(keys); } @Override - public String evaluate( WalkedPath walkedPath ) { + public String evaluate(WalkedPath walkedPath) { return getRawKey(); } @@ -69,14 +70,14 @@ public String getCanonicalForm() { return getRawKey(); } - public String getSubKeyRef( int index ) { + public String getSubKeyRef(int index) { if ((index < 0) || (index >= this.subKeys.size())) { - throw new IndexOutOfBoundsException( "MatchedElement "+ this.subKeys +" cannot be indexed with index "+index ); + throw new IndexOutOfBoundsException("MatchedElement " + this.subKeys + " cannot be indexed with index " + index); } - return subKeys.get( index ); + return subKeys.get(index); } - public int getSubKeyCount(){ + public int getSubKeyCount() { return subKeys.size(); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/PathStep.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/PathStep.java similarity index 76% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/PathStep.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/PathStep.java index ab7b5dc3..6b23e89f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/PathStep.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/PathStep.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.tree; +package io.joltcommunity.jolt.common.tree; -import com.bazaarvoice.jolt.common.Optional; +import io.joltcommunity.jolt.common.Optional; /** * A tuple class that contains the data for one level of a - * tree walk, aka a reference to the input for that level, and - * the LiteralPathElement that was matched at that level. + * tree walk, aka a reference to the input for that level, and + * the LiteralPathElement that was matched at that level. */ public final class PathStep { @@ -28,13 +29,12 @@ public final class PathStep { private final MatchedElement matchedElement; private final Optional origSize; - public PathStep(Object treeRef, MatchedElement matchedElement ) { + public PathStep(Object treeRef, MatchedElement matchedElement) { this.treeRef = treeRef; this.matchedElement = matchedElement; if (matchedElement instanceof ArrayMatchedElement) { - origSize = Optional.of( ( (ArrayMatchedElement) matchedElement ).getOrigSize() ); - } - else { + origSize = Optional.of(((ArrayMatchedElement) matchedElement).getOrigSize()); + } else { origSize = Optional.empty(); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/WalkedPath.java b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/WalkedPath.java similarity index 75% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/WalkedPath.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/WalkedPath.java index 6a2971e6..3ef545a7 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/common/tree/WalkedPath.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/common/tree/WalkedPath.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,24 +14,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.common.tree; +package io.joltcommunity.jolt.common.tree; import java.util.ArrayList; import java.util.Collection; /** * DataStructure used by a SpecTransform during it's parallel tree walk. - * + *

* Basically this is Stack that records the steps down the tree that have been taken. * For each level, there is a PathStep, which contains a pointer the data of that level, - * and a pointer to the LiteralPathElement matched at that level. - * + * and a pointer to the LiteralPathElement matched at that level. + *

* At any given point in time, it represents where in the tree walk a Spec is operating. * It is primarily used to by the ShiftrLeafSpec and CardinalityLeafSpec as a reference * to lookup real values for output "&(1,1)" references. - * + *

* It is expected that as the SpecTransform navigates down the tree, MatchedElements will be added and then - * removed when that subtree has been walked. + * removed when that subtree has been walked. */ public class WalkedPath extends ArrayList { @@ -42,20 +43,20 @@ public WalkedPath(Collection c) { super(c); } - public WalkedPath( Object treeRef, MatchedElement matchedElement ) { + public WalkedPath(Object treeRef, MatchedElement matchedElement) { super(); - this.add( new PathStep( treeRef, matchedElement ) ); + this.add(new PathStep(treeRef, matchedElement)); } /** * Convenience method */ - public boolean add( Object treeRef, MatchedElement matchedElement ) { - return super.add( new PathStep( treeRef, matchedElement ) ); + public boolean add(Object treeRef, MatchedElement matchedElement) { + return super.add(new PathStep(treeRef, matchedElement)); } - public void removeLast() { - remove(size() - 1); + public PathStep removeLastElement() { + return remove(size() - 1); } /** diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/ArrayKey.java b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/ArrayKey.java new file mode 100644 index 00000000..69015dd4 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/ArrayKey.java @@ -0,0 +1,121 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.defaultr; + +import io.joltcommunity.jolt.common.DeepCopy; + +import java.util.*; + +public class ArrayKey extends Key { + + private final Collection keyInts; + private int keyInt = -1; + + public ArrayKey(String jsonKey, Object spec) { + super(jsonKey, spec); + + // Handle ArrayKey specific stuff + switch (getOp()) { + case OR: + keyInts = new ArrayList<>(); + for (String orLiteral : keyStrings) { + int orInt = Integer.parseInt(orLiteral); + keyInts.add(orInt); + } + break; + case LITERAL: + keyInt = Integer.parseInt(rawKey); + keyInts = List.of(keyInt); + break; + case STAR: + keyInts = Collections.emptyList(); + break; + default: + throw new IllegalStateException("Someone has added an op type without changing this method."); + } + } + + @Override + protected int getLiteralIntKey() { + return keyInt; + } + + @Override + protected void applyChild(Object container) { + + if (container instanceof List) { + @SuppressWarnings("unchecked") + List defaultList = (List) container; + + // Find all defaultee keys that match the childKey spec. Simple for Literal keys, more work for * and |. + for (Integer literalKey : determineMatchingContainerKeys(defaultList)) { + applyLiteralKeyToContainer(literalKey, defaultList); + } + } + // Else there is disagreement (with respect to Array vs Map) between the data in + // the Container vs the Defaultr Spec type for this key. Container wins, so do nothing. + } + + private void applyLiteralKeyToContainer(Integer literalIndex, List container) { + + Object defaulteeValue = container.get(literalIndex); + + if (children == null) { + if (defaulteeValue == null) { + container.set(literalIndex, DeepCopy.simpleDeepCopy(literalValue)); // apply a copy of the default value into a List, assumes the list as already been expanded if needed. + } + } else { + if (defaulteeValue == null) { + defaulteeValue = createOutputContainerObject(); + container.set(literalIndex, defaulteeValue); // push a new sub-container into this list + } + + // recurse by applying my children to this known valid container + applyChildren(defaulteeValue); + } + } + + private Collection determineMatchingContainerKeys(List container) { + + switch (getOp()) { + case LITERAL: + // Container it should get these literal values added to it + return keyInts; + case STAR: + // Identify all its keys + // this assumes the container list has already been expanded to the right size + List allIndexes = new ArrayList<>(container.size()); + for (int index = 0; index < container.size(); index++) { + allIndexes.add(index); + } + + return allIndexes; + case OR: + // Identify the intersection between the container "keys" and the OR values + List indexesInRange = new ArrayList<>(); + + for (Integer orValue : keyInts) { + if (orValue < ((List) container).size()) { + indexesInRange.add(orValue); + } + } + return indexesInRange; + default: + throw new IllegalStateException("Someone has added an op type without changing this method."); + } + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/Key.java b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/Key.java similarity index 56% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/Key.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/Key.java index fd5780aa..5ffe0682 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/Key.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/Key.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,62 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.defaultr; +package io.joltcommunity.jolt.defaultr; -import com.bazaarvoice.jolt.Defaultr; -import com.bazaarvoice.jolt.exception.TransformException; +import io.joltcommunity.jolt.Defaultr; +import io.joltcommunity.jolt.exception.TransformException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; -import static com.bazaarvoice.jolt.defaultr.OPS.OR; +import static io.joltcommunity.jolt.defaultr.OPS.OR; public abstract class Key { - /** - * Factory-ish method that recursively processes a Map into a Set objects. - * - * @param spec Simple Jackson default Map input - * @return Set of Keys from this level in the spec - */ - public static Set parseSpec( Map spec ) { - return processSpec( false, spec ); - } - - /** - * Recursively walk the spec input tree. Handle arrays by telling DefaultrKeys if they need to be ArrayKeys, and - * to find the max default array length. - */ - private static Set processSpec( boolean parentIsArray, Map spec ) { - - // TODO switch to List and sort before returning - - Set result = new HashSet<>(); - - for ( String key : spec.keySet() ) { - Object subSpec = spec.get( key ); - if ( parentIsArray ) { - result.add( new ArrayKey( key, subSpec ) ); // this will recursively call processSpec if needed - } - else { - result.add( new MapKey( key, subSpec ) ); // this will recursively call processSpec if needed - } - } - - return result; - } - - private static final String OR_INPUT_REGEX = "\\" + Defaultr.WildCards.OR; private static final Key.KeyPrecedenceComparator keyComparator = new Key.KeyPrecedenceComparator(); - + protected Set children = null; + protected Object literalValue = null; + protected String rawKey; + protected List keyStrings; // Am I supposed to be parent of an array? If so I need to make sure that I inform // my children they need to be ArrayKeys, and I need to make sure that the output array // I will write to is big enough. @@ -77,89 +39,112 @@ private static Set processSpec( boolean parentIsArray, Map private int orCount = 0; private int outputArraySize = -1; - protected Set children = null; - protected Object literalValue = null; - - protected String rawKey; - protected List keyStrings; - - public Key( String rawJsonKey, Object spec ) { - + protected Key(String rawJsonKey, Object spec) { rawKey = rawJsonKey; - if ( rawJsonKey.endsWith( Defaultr.WildCards.ARRAY ) ) { + if (rawJsonKey.endsWith(Defaultr.WildCards.ARRAY)) { isArrayOutput = true; - rawKey = rawKey.replace( Defaultr.WildCards.ARRAY, "" ); + rawKey = rawKey.replace(Defaultr.WildCards.ARRAY, ""); } - op = OPS.parse( rawKey ); + op = OPS.parse(rawKey); - switch( op ){ - case OR : - keyStrings = Arrays.asList( rawKey.split( Key.OR_INPUT_REGEX ) ); + switch (op) { + case OR: + keyStrings = Arrays.asList(rawKey.split(Key.OR_INPUT_REGEX)); orCount = keyStrings.size(); break; case LITERAL: - keyStrings = Arrays.asList( rawKey ); + keyStrings = List.of(rawKey); break; case STAR: keyStrings = Collections.emptyList(); break; - default : - throw new IllegalStateException( "Someone has added an op type without changing this method." ); + default: + throw new IllegalStateException("Someone has added an op type without changing this method."); } // Spec is String -> Map or String -> Literal only - if ( spec instanceof Map ) { - children = processSpec( isArrayOutput(), (Map) spec ); + if (spec instanceof Map) { + children = processSpec(isArrayOutput(), (Map) spec); - if ( isArrayOutput() ) { + if (isArrayOutput()) { // loop over children and find the max literal value - for( Key childKey : children ) { + for (Key childKey : children) { int childValue = childKey.getLiteralIntKey(); - if ( childValue > outputArraySize ) { + if (childValue > outputArraySize) { outputArraySize = childValue; } } } - } - else { + } else { // literal such as String, number, or JSON array literalValue = spec; } } + /** + * Factory-ish method that recursively processes a Map into a Set objects. + * + * @param spec Simple Jackson default Map input + * @return Set of Keys from this level in the spec + */ + public static Set parseSpec(Map spec) { + return processSpec(false, spec); + } + + /** + * Recursively walk the spec input tree. Handle arrays by telling DefaultrKeys if they need to be ArrayKeys, and + * to find the max default array length. + */ + private static Set processSpec(boolean parentIsArray, Map spec) { + + // TODO switch to List and sort before returning + + Set result = new HashSet<>(); + + for (String key : spec.keySet()) { + Object subSpec = spec.get(key); + if (parentIsArray) { + result.add(new ArrayKey(key, subSpec)); // this will recursively call processSpec if needed + } else { + result.add(new MapKey(key, subSpec)); // this will recursively call processSpec if needed + } + } + + return result; + } + /** * This is the main "recursive" method. The defaultee should never be null, because - * the defaultee wasn't null, it was null and we created it, OR there was - * a mismatch between the Defaultr Spec and the input, and we didn't recurse. + * the defaultee wasn't null, it was null and we created it, OR there was + * a mismatch between the Defaultr Spec and the input, and we didn't recurse. */ - public void applyChildren( Object defaultee ) { + public void applyChildren(Object defaultee) { - if ( defaultee == null ) { - throw new TransformException( "Defaultee should never be null when " + - "passed to the applyChildren method." ); + if (defaultee == null) { + throw new TransformException("Defaultee should never be null when " + + "passed to the applyChildren method."); } // This has nothing to do with this being an ArrayKey or MapKey, instead this is about // this key being the parent of an Array in the output. - if ( isArrayOutput() && defaultee instanceof List) { + if (isArrayOutput() && defaultee instanceof List) { - @SuppressWarnings( "unchecked" ) + @SuppressWarnings("unchecked") List defaultList = (List) defaultee; // Extend the defaultee list if needed - for ( int index = defaultList.size() - 1; index < getOutputArraySize(); index++ ) { - defaultList.add( null ); + for (int index = defaultList.size() - 1; index < getOutputArraySize(); index++) { + defaultList.add(null); } } // Find and sort the children DefaultrKeys by precedence: literals, |, then * - ArrayList sortedChildren = new ArrayList<>(); - sortedChildren.addAll( children ); - Collections.sort( sortedChildren, keyComparator ); + List sortedChildren = new ArrayList<>(children); + sortedChildren.sort(keyComparator); - for ( Key childKey : sortedChildren ) { - childKey.applyChild( defaultee ); + for (Key childKey : sortedChildren) { + childKey.applyChild(defaultee); } } @@ -167,29 +152,29 @@ public void applyChildren( Object defaultee ) { /** * Apply this Key to the defaultee. - * + *

* If this Key is a WildCard key, this may apply to many entries in the container. */ - protected abstract void applyChild( Object container ); + protected abstract void applyChild(Object container); - public int getOrCount() { - return orCount; + private int getOrCount() { + return orCount; } - public boolean isArrayOutput() { + private boolean isArrayOutput() { return isArrayOutput; } - public OPS getOp() { - return op; + private int getOutputArraySize() { + return outputArraySize; } - public int getOutputArraySize() { - return outputArraySize; + protected OPS getOp() { + return op; } - public Object createOutputContainerObject() { - if ( isArrayOutput() ) { + protected Object createOutputContainerObject() { + if (isArrayOutput()) { return new ArrayList<>(); } else { return new LinkedHashMap(); @@ -203,18 +188,16 @@ public static class KeyPrecedenceComparator implements Comparator { @Override public int compare(Key a, Key b) { - int opsEqual = opsComparator.compare(a.getOp(), b.getOp() ); + int opsEqual = opsComparator.compare(a.getOp(), b.getOp()); - if ( opsEqual == 0 && OR == a.getOp() && OR == b.getOp() ) - { + if (opsEqual == 0 && OR == a.getOp() && OR == b.getOp()) { // For deterministic behavior, sub sort on the specificity of the OR and then alphabetically on the rawKey // For the Or, the more star like, the higher your value // If the or count matches, fall back to alphabetical on the rawKey from the spec file - return (a.getOrCount() < b.getOrCount() ? -1 : (a.getOrCount() == b.getOrCount() ? a.rawKey.compareTo( b.rawKey ) : 1 ) ); + return (a.getOrCount() < b.getOrCount() ? -1 : (a.getOrCount() == b.getOrCount() ? a.rawKey.compareTo(b.rawKey) : 1)); } return opsEqual; } } - } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/MapKey.java b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/MapKey.java similarity index 53% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/MapKey.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/MapKey.java index b1d72c6a..a9455388 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/defaultr/MapKey.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/MapKey.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.defaultr; +package io.joltcommunity.jolt.defaultr; -import com.bazaarvoice.jolt.common.DeepCopy; +import io.joltcommunity.jolt.common.DeepCopy; import java.util.Collection; import java.util.HashSet; @@ -24,53 +25,52 @@ public class MapKey extends Key { - public MapKey( String jsonKey, Object spec ) { - super( jsonKey, spec ); + public MapKey(String jsonKey, Object spec) { + super(jsonKey, spec); } @Override protected int getLiteralIntKey() { - throw new UnsupportedOperationException( "Shouldn't be be asking a MapKey for int getLiteralIntKey()." ); + throw new UnsupportedOperationException("Shouldn't be be asking a MapKey for int getLiteralIntKey()."); } @Override - protected void applyChild( Object container ) { + protected void applyChild(Object container) { - if ( container instanceof Map ) { + if (container instanceof Map) { Map defaulteeMap = (Map) container; // Find all defaultee keys that match the childKey spec. Simple for Literal keys, more work for * and |. - for ( String literalKey : determineMatchingContainerKeys( defaulteeMap ) ) { - applyLiteralKeyToContainer( literalKey, defaulteeMap ); + for (String literalKey : determineMatchingContainerKeys(defaulteeMap)) { + applyLiteralKeyToContainer(literalKey, defaulteeMap); } } // Else there is disagreement (with respect to Array vs Map) between the data in // the Container vs the Defaultr Spec type for this key. Container wins, so do nothing. } - private void applyLiteralKeyToContainer( String literalKey, Map container ) { + private void applyLiteralKeyToContainer(String literalKey, Map container) { - Object defaulteeValue = container.get( literalKey ); + Object defaulteeValue = container.get(literalKey); - if ( children == null ) { - if ( defaulteeValue == null ) { - container.put( literalKey, DeepCopy.simpleDeepCopy( literalValue ) ); // apply a copy of the default value into a map + if (children == null) { + if (defaulteeValue == null) { + container.put(literalKey, DeepCopy.simpleDeepCopy(literalValue)); // apply a copy of the default value into a map } - } - else { - if ( defaulteeValue == null ) { + } else { + if (defaulteeValue == null) { defaulteeValue = createOutputContainerObject(); - container.put( literalKey, defaulteeValue ); // push a new sub-container into this map + container.put(literalKey, defaulteeValue); // push a new sub-container into this map } // recurse by applying my children to this known valid container - applyChildren( defaulteeValue ); + applyChildren(defaulteeValue); } } - private Collection determineMatchingContainerKeys( Map container ) { + private Collection determineMatchingContainerKeys(Map container) { - switch ( getOp() ) { + switch (getOp()) { case LITERAL: // the container should get these literal values added to it return keyStrings; @@ -79,11 +79,11 @@ private Collection determineMatchingContainerKeys( Map c return container.keySet(); case OR: // Identify the intersection between its keys and the OR values - Set intersection = new HashSet<>( container.keySet() ); - intersection.retainAll( keyStrings ); + Set intersection = new HashSet<>(container.keySet()); + intersection.retainAll(keyStrings); return intersection; - default : - throw new IllegalStateException( "Someone has added an op type without changing this method." ); + default: + throw new IllegalStateException("Someone has added an op type without changing this method."); } } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/OPS.java b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/OPS.java new file mode 100644 index 00000000..ff138acf --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/defaultr/OPS.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.defaultr; + +import io.joltcommunity.jolt.Defaultr; +import io.joltcommunity.jolt.exception.SpecException; + +import java.util.Comparator; +import java.util.EnumMap; + +public enum OPS { + + STAR, OR, LITERAL; + + private static final EnumMap precedenceMap = new EnumMap<>(OPS.class); + + static { + precedenceMap.put(LITERAL, 1); + precedenceMap.put(OR, 2); + precedenceMap.put(STAR, 3); + } + + public static OPS parse(String key) { + if (key.contains(Defaultr.WildCards.STAR)) { + + if (!Defaultr.WildCards.STAR.equals(key)) { + throw new SpecException("Defaultr key " + key + " is invalid. * keys can only contain *, and no other characters."); + } + + return STAR; + } + if (key.contains(Defaultr.WildCards.OR)) { + return OR; + } + return LITERAL; + } + + public static class OpsPrecedenceComparator implements Comparator { + @Override + public int compare(OPS ops1, OPS ops2) { + return Integer.compare(precedenceMap.get(ops1), precedenceMap.get(ops2)); + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrExecutionMode.java b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrExecutionMode.java new file mode 100644 index 00000000..857360a0 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrExecutionMode.java @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.exception.SpecException; + +/** + * Execution strategy for {@link io.joltcommunity.jolt.Enrichr}. + *

+ * {@code SYNC} applies each enrichment immediately after invocation. + * {@code ASYNC} allows all invocations to start first and then blocks only when writing results back. + */ +public enum EnrichrExecutionMode { + SYNC, + ASYNC; + + private static final String EXECUTION_MODE_KEY = "executionMode"; + + /** + * Parse the optional {@code executionMode} spec field. + * + * @param rawValue raw value from the enrich spec + * @return resolved execution mode, defaulting to {@code SYNC} when omitted + */ + public static EnrichrExecutionMode fromSpec( Object rawValue ) { + if ( rawValue == null ) { + return SYNC; + } + if ( ! ( rawValue instanceof String ) ) { + throw new SpecException( "Enrichr optional '" + EXECUTION_MODE_KEY + "' must be a String when provided." ); + } + + String normalizedValue = ( (String) rawValue ).trim(); + if ( normalizedValue.isEmpty() ) { + throw new SpecException( "Enrichr optional '" + EXECUTION_MODE_KEY + "' must not be blank when provided." ); + } + if ( "sync".equalsIgnoreCase( normalizedValue ) ) { + return SYNC; + } + if ( "async".equalsIgnoreCase( normalizedValue ) ) { + return ASYNC; + } + + throw new SpecException( "Enrichr optional '" + EXECUTION_MODE_KEY + "' must be either 'sync' or 'async'." ); + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrManager.java b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrManager.java new file mode 100644 index 00000000..609c35ac --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrManager.java @@ -0,0 +1,153 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.traversr.SimpleTraversr; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +/** + * Parses one enrich spec entry and coordinates the work required to execute it. + *

+ * A manager owns the input path matcher, output path resolver, and Java method invoker for a single + * enrichment rule such as: + *

+ * {
+ *   "path" : "customers.[*].id",
+ *   "outputPath" : "customers.[*].profile",
+ *   "contextKey" : "customerLookup",
+ *   "method" : "enrich"
+ * }
+ * 
+ */ +public class EnrichrManager { + + private final EnrichrMethodInvoker invoker; + private final EnrichrPathTemplate inputPathTemplate; + private final EnrichrPathTemplate outputPathTemplate; + private final SimpleTraversr outputTraversr; + + /** + * Parse one enrichment rule from the {@code enrichments} array. + * + * @param spec raw rule object + * @param index position of the rule inside the spec, used for error messages + */ + @SuppressWarnings( "unchecked" ) + public EnrichrManager( Object spec, int index ) { + if ( ! ( spec instanceof Map ) ) { + throw new SpecException( "Enrichr enrichment at index:" + index + " must be a Map." ); + } + + Map rule = (Map) spec; + String path = requiredString( rule, "path", index ); + String outputPath = optionalString( rule, "outputPath", path ); + + String methodName = requiredString( rule, "method", index ); + String contextKey = optionalString( rule, "contextKey", null ); + String className = optionalString( rule, "className", null ); + + inputPathTemplate = EnrichrPathTemplate.parseInput( path, index ); + outputPathTemplate = EnrichrPathTemplate.parseOutput( outputPath, index ); + validateOutputPath( index ); + outputTraversr = outputPathTemplate.getTraversr(); + invoker = new EnrichrMethodInvoker( methodName, contextKey, className, index ); + } + + /** + * Find every input value matched by this rule. + *

+ * Fixed paths return at most one match, while wildcard array paths such as {@code customers.[*].id} + * return one match per resolved array element. + * + * @param input document currently being transformed + * @return resolved path matches for this rule + */ + public List match( Object input ) { + return inputPathTemplate.match( input ); + } + + /** + * Create a pending enrichment for one already-resolved input match. + *

+ * The method invocation is started immediately. The returned object is responsible for waiting on the + * result and writing it back to the resolved output path later. + * + * @param inputMatch one resolved source value plus any wildcard bindings captured from the input path + * @param input full input document + * @param context optional transform context + * @return pending enrichment ready to be applied + */ + public EnrichrPendingEnrichment prepare( EnrichrPathMatch inputMatch, Object input, Map context ) { + CompletionStage enrichedValueStage = invoker.invokeAsync( inputMatch.getValue(), input, context ); + List outputKeys = outputPathTemplate.resolveKeys( inputMatch.getWildcardBindings() ); + String resolvedOutputPath = outputPathTemplate.resolvePath( inputMatch.getWildcardBindings() ); + return new EnrichrPendingEnrichment( input, outputTraversr, outputKeys, enrichedValueStage, resolvedOutputPath ); + } + + /** + * Read a required non-blank string property from one enrichment rule. + */ + static String requiredString( Map spec, String key, int index ) { + Object value = spec.get( key ); + if ( ! ( value instanceof String ) || ( (String) value ).trim().isEmpty() ) { + throw new SpecException( "Enrichr enrichment at index:" + index + " requires a non-blank '" + key + "'." ); + } + return ( (String) value ).trim(); + } + + /** + * Read an optional non-blank string property from one enrichment rule. + */ + static String optionalString( Map spec, String key, String defaultValue ) { + Object value = spec.get( key ); + if ( value == null ) { + return defaultValue; + } + if ( ! ( value instanceof String ) || ( (String) value ).trim().isEmpty() ) { + throw new SpecException( "Enrichr optional '" + key + "' must be a non-blank String when provided." ); + } + return ( (String) value ).trim(); + } + + /** + * Ensure the output path can be resolved for every wildcard captured from the input path. + *

+ * Wildcard input paths must either bind the same number of {@code [*]} segments in the output path or + * write to an append location using {@code []}. + */ + private void validateOutputPath( int index ) { + int inputWildcardCount = inputPathTemplate.getWildcardCount(); + int outputWildcardCount = outputPathTemplate.getWildcardCount(); + + if ( outputWildcardCount > 0 && outputWildcardCount != inputWildcardCount ) { + throw new SpecException( + "Enrichr enrichment at index:" + index + + " requires 'outputPath' to use the same number of '[*]' segments as 'path'." + ); + } + + if ( inputWildcardCount > 0 && outputWildcardCount == 0 && ! outputPathTemplate.hasAppendSegment() ) { + throw new SpecException( + "Enrichr enrichment at index:" + index + + " with wildcard 'path' requires 'outputPath' to either include matching '[*]' segments or use '[]' append semantics." + ); + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrMethodInvoker.java b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrMethodInvoker.java new file mode 100644 index 00000000..86e175bd --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrMethodInvoker.java @@ -0,0 +1,253 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.TransformException; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Resolves and invokes the Java method configured for one enrich rule. + *

+ * Targets can come either from a declared {@code className} or from a runtime {@code contextKey}. Return + * values are normalized to {@link CompletionStage} so the rest of the enrich flow can treat sync, + * {@link CompletionStage}, and reactive {@link Publisher} methods uniformly. + */ +final class EnrichrMethodInvoker { + + private final Method method; + private final Object target; + private final String methodName; + private final String contextKey; + private final int index; + private final Map, Method> contextMethodCache; + + /** + * Create an invoker for one enrich rule. + * + * @param methodName public method to invoke + * @param contextKey optional context map key that supplies the target instance + * @param className optional fully qualified class name used to resolve the target up front + * @param index enrich rule index used in validation messages + */ + EnrichrMethodInvoker( String methodName, String contextKey, String className, int index ) { + this.methodName = methodName; + this.index = index; + this.contextKey = contextKey; + this.contextMethodCache = new ConcurrentHashMap<>(); + + if ( className == null && contextKey == null ) { + throw new SpecException( "Enrichr enrichment at index:" + index + " requires either 'className' or 'contextKey'." ); + } + if ( className != null && contextKey != null ) { + throw new SpecException( "Enrichr enrichment at index:" + index + " supports only one of 'className' or 'contextKey'." ); + } + + if ( className == null ) { + method = null; + target = null; + return; + } + + try { + Class clazz = Class.forName( className ); + method = findMethod( clazz, this.methodName, index ); + + if ( java.lang.reflect.Modifier.isStatic( method.getModifiers() ) ) { + target = null; + } + else { + target = clazz.getDeclaredConstructor().newInstance(); + } + } + catch ( SpecException e ) { + throw e; + } + catch ( Exception e ) { + throw new SpecException( "Enrichr could not initialize enrichment at index:" + index + ".", e ); + } + } + + /** + * Invoke the configured method and normalize the result to a completion stage. + * + * @param value matched input value + * @param input full input document + * @param context optional transform context + * @return a completion stage representing the final enriched value + */ + CompletionStage invokeAsync( Object value, Object input, Map context ) { + try { + InvocationTarget invocationTarget = resolveInvocationTarget( context ); + Class[] parameterTypes = invocationTarget.method.getParameterTypes(); + Object invocationResult; + + if ( parameterTypes.length == 1 ) { + invocationResult = invocationTarget.method.invoke( invocationTarget.target, value ); + } + else if ( parameterTypes.length == 2 ) { + invocationResult = invocationTarget.method.invoke( invocationTarget.target, value, input ); + } + else { + invocationResult = invocationTarget.method.invoke( invocationTarget.target, value, input, context ); + } + + return toCompletionStage( invocationResult ); + } + catch ( IllegalAccessException | InvocationTargetException e ) { + Throwable cause = e instanceof InvocationTargetException ? ( (InvocationTargetException) e ).getCause() : e; + throw new TransformException( "Enrichr failed invoking " + methodName + ".", cause ); + } + } + + /** + * Resolve the concrete method target for this invocation. + *

+ * Class-based rules are resolved once in the constructor. Context-based rules are resolved at runtime so + * the context map can supply request-scoped collaborators such as Spring beans. + */ + private InvocationTarget resolveInvocationTarget( Map context ) { + if ( method != null ) { + return new InvocationTarget( method, target ); + } + + if ( context == null ) { + throw new TransformException( "Enrichr could not resolve contextKey '" + contextKey + "' because transform context is null." ); + } + + Object contextTarget = context.get( contextKey ); + if ( contextTarget == null ) { + throw new TransformException( "Enrichr could not resolve contextKey '" + contextKey + "' at index:" + index + "." ); + } + + Method contextMethod = contextMethodCache.get( contextTarget.getClass() ); + if ( contextMethod == null ) { + contextMethod = findMethod( contextTarget.getClass(), methodName, index ); + contextMethodCache.put( contextTarget.getClass(), contextMethod ); + } + + return new InvocationTarget( contextMethod, contextTarget ); + } + + /** + * Convert supported return types into a completion stage. + */ + @SuppressWarnings( "unchecked" ) + private static CompletionStage toCompletionStage( Object invocationResult ) { + if ( invocationResult == null ) { + return CompletableFuture.completedFuture( null ); + } + if ( invocationResult instanceof CompletionStage ) { + return (CompletionStage) invocationResult; + } + if ( invocationResult instanceof Publisher ) { + return publisherToCompletionStage( (Publisher) invocationResult ); + } + return CompletableFuture.completedFuture( invocationResult ); + } + + /** + * Bridge a single-value reactive publisher into a completion stage. + */ + private static CompletionStage publisherToCompletionStage( Publisher publisher ) { + CompletableFuture future = new CompletableFuture<>(); + publisher.subscribe( new Subscriber() { + private Subscription subscription; + private boolean hasValue; + private Object value; + + @Override + public void onSubscribe( Subscription subscription ) { + this.subscription = subscription; + subscription.request( Long.MAX_VALUE ); + } + + @Override + public void onNext( Object nextValue ) { + if ( hasValue ) { + subscription.cancel(); + future.completeExceptionally( new TransformException( "Enrichr reactive enrichments must emit at most one value." ) ); + return; + } + + hasValue = true; + value = nextValue; + } + + @Override + public void onError( Throwable throwable ) { + future.completeExceptionally( throwable ); + } + + @Override + public void onComplete() { + future.complete( value ); + } + } ); + return future; + } + + /** + * Find the first compatible public enrich method on the supplied class. + */ + private static Method findMethod( Class clazz, String methodName, int index ) { + for ( Method candidate : clazz.getMethods() ) { + if ( ! candidate.getName().equals( methodName ) ) { + continue; + } + + Class[] parameterTypes = candidate.getParameterTypes(); + if ( parameterTypes.length < 1 || parameterTypes.length > 3 ) { + continue; + } + if ( parameterTypes.length >= 2 && ! Object.class.isAssignableFrom( parameterTypes[1] ) ) { + continue; + } + if ( parameterTypes.length == 3 && ! Map.class.isAssignableFrom( parameterTypes[2] ) ) { + continue; + } + + return candidate; + } + + throw new SpecException( + "Enrichr could not find a public method named '" + methodName + "' on " + clazz.getName() + + " with signature (Object), (Object,Object), or (Object,Object,Map) at index:" + index + "." + ); + } + + /** + * Concrete method plus object instance used for one invocation. + */ + private static final class InvocationTarget { + private final Method method; + private final Object target; + + private InvocationTarget( Method method, Object target ) { + this.method = method; + this.target = target; + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPathMatch.java b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPathMatch.java new file mode 100644 index 00000000..294bb740 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPathMatch.java @@ -0,0 +1,77 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Concrete result of resolving one enrich input path against the current document. + *

+ * For a fixed path this represents a single value. For wildcard array paths it represents one resolved + * array element plus the wildcard bindings needed to compute the corresponding output path. + */ +public final class EnrichrPathMatch { + + private final Object value; + private final List resolvedInputKeys; + private final List wildcardBindings; + private final String resolvedInputPath; + + /** + * Capture one resolved input match. + * + * @param value matched input value + * @param resolvedInputKeys traversr-friendly keys for the matched input path + * @param wildcardBindings array indices captured from {@code [*]} segments + * @param resolvedInputPath human-readable resolved path + */ + EnrichrPathMatch( Object value, List resolvedInputKeys, List wildcardBindings, String resolvedInputPath ) { + this.value = value; + this.resolvedInputKeys = Collections.unmodifiableList( new ArrayList<>( resolvedInputKeys ) ); + this.wildcardBindings = Collections.unmodifiableList( new ArrayList<>( wildcardBindings ) ); + this.resolvedInputPath = resolvedInputPath; + } + + /** + * Return the value currently stored at the resolved input path. + */ + Object getValue() { + return value; + } + + /** + * Return the concrete input keys used to reach this match. + */ + List getResolvedInputKeys() { + return resolvedInputKeys; + } + + /** + * Return the array indices captured from wildcard segments in the input path. + */ + List getWildcardBindings() { + return wildcardBindings; + } + + /** + * Return the concrete input path in human-readable form. + */ + String getResolvedInputPath() { + return resolvedInputPath; + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPathTemplate.java b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPathTemplate.java new file mode 100644 index 00000000..a0ee58ae --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPathTemplate.java @@ -0,0 +1,385 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.traversr.SimpleTraversr; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Parses and resolves enrich input and output paths. + *

+ * Supported path forms are: + *

    + *
  • Map keys such as {@code customer.id}
  • + *
  • Explicit array indices such as {@code customers.[0].id}
  • + *
  • Array wildcards such as {@code customers.[*].id}
  • + *
  • Output-only append segments such as {@code profiles.[]}
  • + *
+ * The template can both match input values and resolve output keys by substituting wildcard bindings + * captured from an input match. + */ +final class EnrichrPathTemplate { + + private final String configuredPath; + private final List segments; + private final int wildcardCount; + private final boolean hasAppendSegment; + private final SimpleTraversr traversr; + + /** + * Create an immutable parsed path template. + * + * @param configuredPath original path string taken from the enrich spec + * @param segments parsed path segments + * @param wildcardCount number of {@code [*]} segments in the template + * @param hasAppendSegment whether the template contains output append semantics via {@code []} + */ + private EnrichrPathTemplate( String configuredPath, List segments, int wildcardCount, boolean hasAppendSegment ) { + this.configuredPath = configuredPath; + this.segments = Collections.unmodifiableList( new ArrayList<>( segments ) ); + this.wildcardCount = wildcardCount; + this.hasAppendSegment = hasAppendSegment; + this.traversr = new SimpleTraversr<>( configuredPath ); + } + + /** + * Parse a source path used to read values from the input document. + *

+ * Input paths support fixed keys, explicit indices, and {@code [*]} wildcards. They intentionally do not + * support {@code []} append semantics because append only makes sense on the write side. + * + * @param configuredPath source path declared in the enrich spec + * @param index enrich rule index used in validation messages + * @return parsed input path template + */ + static EnrichrPathTemplate parseInput( String configuredPath, int index ) { + EnrichrPathTemplate template = parse( configuredPath, index, "path" ); + if ( template.hasAppendSegment ) { + throw new SpecException( + "Enrichr enrichment at index:" + index + + " does not support '[]' in 'path'. Use explicit indices like '[0]' or array wildcards like '[*]'." + ); + } + return template; + } + + /** + * Parse a destination path used when writing the enriched value back to the document. + * + * @param configuredPath output path declared in the enrich spec + * @param index enrich rule index used in validation messages + * @return parsed output path template + */ + static EnrichrPathTemplate parseOutput( String configuredPath, int index ) { + return parse( configuredPath, index, "outputPath" ); + } + + /** + * Parse a human-readable dot path into typed path segments. + * + * @param configuredPath path string declared in the enrich spec + * @param index enrich rule index used in validation messages + * @param fieldName spec field currently being parsed, either {@code path} or {@code outputPath} + * @return parsed path template + */ + private static EnrichrPathTemplate parse( String configuredPath, int index, String fieldName ) { + List tokens = tokenize( configuredPath ); + List segments = new ArrayList<>( tokens.size() ); + int wildcardCount = 0; + boolean hasAppendSegment = false; + + for ( String token : tokens ) { + if ( "[]".equals( token ) ) { + segments.add( Segment.append() ); + hasAppendSegment = true; + continue; + } + + if ( token.startsWith( "[" ) && token.endsWith( "]" ) ) { + String innerValue = token.substring( 1, token.length() - 1 ).trim(); + if ( innerValue.isEmpty() ) { + throw new SpecException( + "Enrichr enrichment at index:" + index + " has an invalid '" + fieldName + "' segment '" + token + "'." + ); + } + + if ( "*".equals( innerValue ) ) { + segments.add( Segment.wildcard() ); + wildcardCount++; + continue; + } + + try { + int arrayIndex = Integer.parseInt( innerValue ); + if ( arrayIndex < 0 ) { + throw new NumberFormatException( "negative" ); + } + segments.add( Segment.arrayIndex( innerValue ) ); + continue; + } + catch ( NumberFormatException e ) { + throw new SpecException( + "Enrichr enrichment at index:" + index + " has an invalid '" + fieldName + "' segment '" + token + + "'. Supported array syntax is '[0]', '[1]', or '[*]'." + ); + } + } + + segments.add( Segment.mapKey( token ) ); + } + + return new EnrichrPathTemplate( configuredPath, segments, wildcardCount, hasAppendSegment ); + } + + /** + * Split a dot path into raw tokens while preserving bracketed array segments as standalone elements. + * + * @param configuredPath path string declared in the enrich spec + * @return tokenized path segments + */ + private static List tokenize( String configuredPath ) { + String intermediatePath = configuredPath.replace( "[", ".[" ).replace( "..", "." ); + if ( intermediatePath.charAt( 0 ) == '.' ) { + intermediatePath = intermediatePath.substring( 1 ); + } + + String[] rawKeys = intermediatePath.split( "\\." ); + List keys = new ArrayList<>( rawKeys.length ); + Collections.addAll( keys, rawKeys ); + return keys; + } + + /** + * Resolve all input values that satisfy this template. + * + * @param input document to search + * @return zero or more concrete path matches + */ + List match( Object input ) { + List matches = new ArrayList<>(); + collectMatches( input, 0, new ArrayList(), new ArrayList(), matches ); + return matches; + } + + /** + * Recursively walk the input document and collect every concrete path that matches this template. + * + * @param currentValue current node being inspected + * @param segmentIndex current segment position within the template + * @param resolvedKeys concrete path keys collected so far + * @param wildcardBindings wildcard array indices collected so far + * @param matches destination list for resolved matches + */ + private void collectMatches( + Object currentValue, + int segmentIndex, + List resolvedKeys, + List wildcardBindings, + List matches + ) { + if ( segmentIndex == segments.size() ) { + matches.add( new EnrichrPathMatch( currentValue, resolvedKeys, wildcardBindings, resolvePath( wildcardBindings ) ) ); + return; + } + + if ( currentValue == null ) { + return; + } + + Segment segment = segments.get( segmentIndex ); + if ( segment.type == SegmentType.MAP_KEY ) { + if ( currentValue instanceof Map ) { + @SuppressWarnings( "unchecked" ) + Map map = (Map) currentValue; + if ( map.containsKey( segment.value ) ) { + resolvedKeys.add( segment.value ); + collectMatches( map.get( segment.value ), segmentIndex + 1, resolvedKeys, wildcardBindings, matches ); + resolvedKeys.remove( resolvedKeys.size() - 1 ); + } + } + return; + } + + if ( segment.type == SegmentType.ARRAY_INDEX ) { + if ( currentValue instanceof List ) { + List list = (List) currentValue; + int arrayIndex = Integer.parseInt( segment.value ); + if ( arrayIndex < list.size() ) { + resolvedKeys.add( segment.value ); + collectMatches( list.get( arrayIndex ), segmentIndex + 1, resolvedKeys, wildcardBindings, matches ); + resolvedKeys.remove( resolvedKeys.size() - 1 ); + } + } + return; + } + + if ( segment.type == SegmentType.ARRAY_WILDCARD ) { + if ( currentValue instanceof List ) { + List list = (List) currentValue; + for ( int index = 0; index < list.size(); index++ ) { + String resolvedIndex = String.valueOf( index ); + resolvedKeys.add( resolvedIndex ); + wildcardBindings.add( resolvedIndex ); + collectMatches( list.get( index ), segmentIndex + 1, resolvedKeys, wildcardBindings, matches ); + wildcardBindings.remove( wildcardBindings.size() - 1 ); + resolvedKeys.remove( resolvedKeys.size() - 1 ); + } + } + return; + } + + return; + } + + /** + * Return a traversr instance for writing values to this template after wildcard substitution. + * + * @return traversr configured for this path template + */ + SimpleTraversr getTraversr() { + return traversr; + } + + /** + * Return the number of {@code [*]} segments declared in this template. + * + * @return wildcard segment count + */ + int getWildcardCount() { + return wildcardCount; + } + + /** + * Indicate whether the template contains output append semantics via {@code []}. + * + * @return {@code true} when the template includes an append segment + */ + boolean hasAppendSegment() { + return hasAppendSegment; + } + + /** + * Resolve traversr keys for a concrete output path. + * + * @param wildcardBindings wildcard indices captured from the input path + * @return keys suitable for {@link SimpleTraversr#set(Object, List, Object)} + */ + List resolveKeys( List wildcardBindings ) { + validateBindings( wildcardBindings ); + + List resolvedKeys = new ArrayList<>( segments.size() ); + int wildcardIndex = 0; + for ( Segment segment : segments ) { + if ( segment.type == SegmentType.MAP_KEY || segment.type == SegmentType.ARRAY_INDEX ) { + resolvedKeys.add( segment.value ); + } + else if ( segment.type == SegmentType.ARRAY_WILDCARD ) { + resolvedKeys.add( wildcardBindings.get( wildcardIndex++ ) ); + } + else { + resolvedKeys.add( "[]" ); + } + } + return resolvedKeys; + } + + /** + * Render a concrete human-readable path by substituting wildcard bindings into this template. + * + * @param wildcardBindings wildcard indices captured from the input path + * @return resolved path string + */ + String resolvePath( List wildcardBindings ) { + validateBindings( wildcardBindings ); + + StringBuilder pathBuilder = new StringBuilder(); + int wildcardIndex = 0; + for ( int index = 0; index < segments.size(); index++ ) { + if ( index > 0 ) { + pathBuilder.append( '.' ); + } + + Segment segment = segments.get( index ); + if ( segment.type == SegmentType.MAP_KEY ) { + pathBuilder.append( segment.value ); + } + else if ( segment.type == SegmentType.ARRAY_INDEX ) { + pathBuilder.append( '[' ).append( segment.value ).append( ']' ); + } + else if ( segment.type == SegmentType.ARRAY_WILDCARD ) { + pathBuilder.append( '[' ).append( wildcardBindings.get( wildcardIndex++ ) ).append( ']' ); + } + else { + pathBuilder.append( "[]" ); + } + } + return pathBuilder.toString(); + } + + /** + * Ensure enough wildcard values were captured from the input path to resolve this template. + * + * @param wildcardBindings wildcard indices captured from the input path + */ + private void validateBindings( List wildcardBindings ) { + if ( wildcardBindings.size() < wildcardCount ) { + throw new IllegalArgumentException( + "Expected at least " + wildcardCount + " wildcard bindings for path '" + configuredPath + "', got " + wildcardBindings.size() + "." + ); + } + } + + /** + * Internal representation of one parsed path segment. + */ + private enum SegmentType { + MAP_KEY, + ARRAY_INDEX, + ARRAY_WILDCARD, + ARRAY_APPEND + } + + private static final class Segment { + private final SegmentType type; + private final String value; + + private Segment( SegmentType type, String value ) { + this.type = type; + this.value = value; + } + + private static Segment mapKey( String value ) { + return new Segment( SegmentType.MAP_KEY, value ); + } + + private static Segment arrayIndex( String value ) { + return new Segment( SegmentType.ARRAY_INDEX, value ); + } + + private static Segment wildcard() { + return new Segment( SegmentType.ARRAY_WILDCARD, null ); + } + + private static Segment append() { + return new Segment( SegmentType.ARRAY_APPEND, null ); + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPendingEnrichment.java b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPendingEnrichment.java new file mode 100644 index 00000000..245ae1f8 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/enrich/EnrichrPendingEnrichment.java @@ -0,0 +1,84 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.exception.TransformException; +import io.joltcommunity.jolt.traversr.SimpleTraversr; + +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutionException; + +/** + * Represents an enrichment invocation that has already been started but not yet written back to the + * document. + *

+ * This is used by both sync and async execution paths so invocation and document mutation remain cleanly + * separated. + */ +public class EnrichrPendingEnrichment { + + private final Object input; + private final SimpleTraversr outputTraversr; + private final List outputKeys; + private final CompletionStage enrichedValueStage; + private final String outputPath; + + /** + * Create a pending write-back operation for one resolved enrichment match. + */ + EnrichrPendingEnrichment( + Object input, + SimpleTraversr outputTraversr, + List outputKeys, + CompletionStage enrichedValueStage, + String outputPath + ) { + this.input = input; + this.outputTraversr = outputTraversr; + this.outputKeys = outputKeys; + this.enrichedValueStage = enrichedValueStage; + this.outputPath = outputPath; + } + + /** + * Wait for the enrichment result and write it to the resolved output path. + */ + public void apply() { + Object enrichedValue = resolveValue( enrichedValueStage, outputPath ); + outputTraversr.set( input, outputKeys, enrichedValue ); + } + + /** + * Resolve the asynchronous result into a concrete value while preserving interruption semantics. + */ + private static Object resolveValue( CompletionStage enrichedValueStage, String outputPath ) { + try { + return enrichedValueStage.toCompletableFuture().get(); + } + catch ( InterruptedException e ) { + Thread.currentThread().interrupt(); + throw new TransformException( "Enrichr asynchronous enrichment was interrupted for outputPath '" + outputPath + "'.", e ); + } + catch ( ExecutionException e ) { + Throwable cause = e.getCause(); + if ( cause instanceof RuntimeException ) { + throw (RuntimeException) cause; + } + throw new TransformException( "Enrichr asynchronous enrichment failed for outputPath '" + outputPath + "'.", cause ); + } + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/exception/JoltException.java b/jolt-core/src/main/java/io/joltcommunity/jolt/exception/JoltException.java similarity index 77% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/exception/JoltException.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/exception/JoltException.java index 9028ee9b..42332b98 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/exception/JoltException.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/exception/JoltException.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.exception; +package io.joltcommunity.jolt.exception; /** * Base Jolt Exception */ public class JoltException extends RuntimeException { - public JoltException( String msg ) { + public JoltException(String msg) { super(msg); } - public JoltException( String msg, Throwable t ) { + public JoltException(String msg, Throwable t) { super(msg, t); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/exception/SpecException.java b/jolt-core/src/main/java/io/joltcommunity/jolt/exception/SpecException.java similarity index 87% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/exception/SpecException.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/exception/SpecException.java index a1363203..afefdd86 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/exception/SpecException.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/exception/SpecException.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.exception; +package io.joltcommunity.jolt.exception; /** * Exception thrown by JOLT SpecTransforms during initialization. diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/exception/TransformException.java b/jolt-core/src/main/java/io/joltcommunity/jolt/exception/TransformException.java similarity index 78% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/exception/TransformException.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/exception/TransformException.java index bcc9c3f0..b7d59817 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/exception/TransformException.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/exception/TransformException.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,18 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.exception; +package io.joltcommunity.jolt.exception; /** * Exception thrown by JOLT transforms. Should only be thrown from methods processing data. */ public class TransformException extends JoltException { - public TransformException( String msg ) { + public TransformException(String msg) { super(msg); } - public TransformException( String msg, Throwable t ) { + public TransformException(String msg, Throwable t) { super(msg, t); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/DataType.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/DataType.java similarity index 67% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/DataType.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/modifier/DataType.java index 9fbc021d..ba5ac064 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/DataType.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/DataType.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +15,10 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.modifier; +package io.joltcommunity.jolt.modifier; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.tree.WalkedPath; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -26,14 +27,14 @@ /** * From the spec we need to guess the DataType of the incoming input - * + *

* This is useful for, * a) in cases where the spec suggested a list but input was map - * and vice versa, where we can just skip processing instead of - * throwing random array/map errors + * and vice versa, where we can just skip processing instead of + * throwing random array/map errors * b) in case where the input is actually null and we need to create - * appropriate data structure and then apply spec logic - * + * appropriate data structure and then apply spec logic + *

* Note: By design jolt does not stop processing on bad input data */ public abstract class DataType { @@ -41,12 +42,11 @@ public abstract class DataType { private static final RUNTIME runtimeInstance = new RUNTIME(); private static final MAP mapInstance = new MAP(); - public static DataType determineDataType( int confirmedArrayAtIndex, int confirmedMapAtIndex, int maxExplicitIndex ) { + public static DataType determineDataType(int confirmedArrayAtIndex, int confirmedMapAtIndex, int maxExplicitIndex) { // based on provided flags, set appropriate dataType - if ( confirmedArrayAtIndex > -1 ) { - return new LIST( maxExplicitIndex ); - } - else if ( confirmedMapAtIndex > -1 ) { + if (confirmedArrayAtIndex > -1) { + return new LIST(maxExplicitIndex); + } else if (confirmedMapAtIndex > -1) { return mapInstance; } // only a single "*" key was defined in spec. We need to get dataType at runtime from input @@ -55,13 +55,60 @@ else if ( confirmedMapAtIndex > -1 ) { } } + /** + * Determines if an input is compatible with current DataType + */ + public abstract boolean isCompatible(Object input); + + /** + * MAP and LIST types overrides this method to return appropriate new map or list + */ + protected abstract Object createValue(); + + /** + * LIST overrides this method to expand the source (list) such that in can support + * an index specified in spec that is outside the range input list, returns original size + * of the input + */ + public Integer expand(Object source) { + throw new RuntimeException("Expand not supported in " + this.getClass().getSimpleName() + " Type"); + } + + /** + * Creates an empty map/list, as required by spec, in the parent map/list at given key/index + * + * @param keyOrIndex of the parent object to create + * @param walkedPath containing the parent object + * @param opMode to determine if this write operation is allowed + * @return newly created object + */ + @SuppressWarnings("unchecked") + public Object create(String keyOrIndex, WalkedPath walkedPath, OpMode opMode) { + Object parent = walkedPath.lastElement().getTreeRef(); + Optional origSizeOptional = walkedPath.lastElement().getOrigSize(); + int index = -1; + try { + index = Integer.parseInt(keyOrIndex); + } catch (Exception ignored) { + } + Object value = null; + if (parent instanceof Map && opMode.isApplicable((Map) parent, keyOrIndex)) { + value = createValue(); + ((Map) parent).put(keyOrIndex, value); + } else if (parent instanceof List && opMode.isApplicable((List) parent, index, origSizeOptional.get())) { + value = createValue(); + ((List) parent).set(index, value); + } + return value; + } + /** * List type that records maxIndex from spec, and uses that to expand a source (list) properly */ public static final class LIST extends DataType { private final int maxIndexFromSpec; - private LIST( int maxIndexFromSpec ) { + private LIST(int maxIndexFromSpec) { this.maxIndexFromSpec = maxIndexFromSpec; } @@ -71,22 +118,22 @@ protected Object createValue() { } @Override - @SuppressWarnings( "unchecked" ) - public Integer expand( Object input ) { + @SuppressWarnings("unchecked") + public Integer expand(Object input) { List source = (List) input; int reqIndex = maxIndexFromSpec; int currLastIndex = source.size() - 1; int origSize = currLastIndex + 1; - if ( reqIndex >= source.size() ) { - while ( currLastIndex++ < reqIndex ) { - source.add( null ); + if (reqIndex >= source.size()) { + while (currLastIndex++ < reqIndex) { + source.add(null); } } return origSize; } @Override - public boolean isCompatible( final Object input ) { + public boolean isCompatible(final Object input) { return input == null || input instanceof List; } } @@ -101,7 +148,7 @@ protected Object createValue() { } @Override - public boolean isCompatible( final Object input ) { + public boolean isCompatible(final Object input) { return input == null || input instanceof Map; } } @@ -111,62 +158,13 @@ public boolean isCompatible( final Object input ) { */ public static final class RUNTIME extends DataType { @Override - public boolean isCompatible( final Object input ) { + public boolean isCompatible(final Object input) { return input != null; } @Override protected Object createValue() { - throw new RuntimeException( "Cannot create for RUNTIME Type" ); + throw new RuntimeException("Cannot create for RUNTIME Type"); } } - - /** - * Determines if an input is compatible with current DataType - */ - public abstract boolean isCompatible(Object input); - - /** - * MAP and LIST types overrides this method to return appropriate new map or list - */ - protected abstract Object createValue(); - - /** - * LIST overrides this method to expand the source (list) such that in can support - * an index specified in spec that is outside the range input list, returns original size - * of the input - */ - public Integer expand( Object source ) { - throw new RuntimeException( "Expand not supported in " + this.getClass().getSimpleName() + " Type" ); - } - - /** - * Creates an empty map/list, as required by spec, in the parent map/list at given key/index - * - * @param keyOrIndex of the parent object to create - * @param walkedPath containing the parent object - * @param opMode to determine if this write operation is allowed - * @return newly created object - */ - @SuppressWarnings( "unchecked" ) - public Object create( String keyOrIndex, WalkedPath walkedPath, OpMode opMode ) { - Object parent = walkedPath.lastElement().getTreeRef(); - Optional origSizeOptional = walkedPath.lastElement().getOrigSize(); - int index = -1; - try { - index = Integer.parseInt( keyOrIndex ); - } - catch ( Exception ignored ) { - } - Object value = null; - if ( parent instanceof Map && opMode.isApplicable( (Map) parent, keyOrIndex ) ) { - value = createValue(); - ( (Map) parent ).put( keyOrIndex, value ); - } - else if ( parent instanceof List && opMode.isApplicable( (List) parent, index, origSizeOptional.get() ) ) { - value = createValue(); - ( (List) parent ).set( index, value ); - } - return value; - } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/TemplatrSpecBuilder.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/ModifierSpecBuilder.java similarity index 50% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/TemplatrSpecBuilder.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/modifier/ModifierSpecBuilder.java index 42ae8a18..9784b45c 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/TemplatrSpecBuilder.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/ModifierSpecBuilder.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,17 +15,17 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.modifier; +package io.joltcommunity.jolt.modifier; -import com.bazaarvoice.jolt.common.spec.SpecBuilder; -import com.bazaarvoice.jolt.modifier.function.Function; -import com.bazaarvoice.jolt.modifier.spec.ModifierCompositeSpec; -import com.bazaarvoice.jolt.modifier.spec.ModifierLeafSpec; -import com.bazaarvoice.jolt.modifier.spec.ModifierSpec; +import io.joltcommunity.jolt.common.spec.SpecBuilder; +import io.joltcommunity.jolt.modifier.function.Function; +import io.joltcommunity.jolt.modifier.spec.ModifierCompositeSpec; +import io.joltcommunity.jolt.modifier.spec.ModifierLeafSpec; +import io.joltcommunity.jolt.modifier.spec.ModifierSpec; import java.util.Map; -public class TemplatrSpecBuilder extends SpecBuilder { +public class ModifierSpecBuilder extends SpecBuilder { public static final String CARET = "^"; public static final String AT = "@"; @@ -34,19 +35,18 @@ public class TemplatrSpecBuilder extends SpecBuilder { private final Map functionsMap; - public TemplatrSpecBuilder( OpMode opMode, Map functionsMap ) { + public ModifierSpecBuilder(OpMode opMode, Map functionsMap) { this.opMode = opMode; this.functionsMap = functionsMap; } @Override - @SuppressWarnings( "unchecked" ) - public ModifierSpec createSpec( final String lhs, final Object rhs ) { - if( rhs instanceof Map && (!( (Map) rhs ).isEmpty())) { - return new ModifierCompositeSpec(lhs, (Map)rhs, opMode, this ); - } - else { - return new ModifierLeafSpec( lhs, rhs, opMode, functionsMap ); + @SuppressWarnings("unchecked") + public ModifierSpec createSpec(final String lhs, final Object rhs) { + if (rhs instanceof Map && (!((Map) rhs).isEmpty())) { + return new ModifierCompositeSpec(lhs, (Map) rhs, opMode, this); + } else { + return new ModifierLeafSpec(lhs, rhs, opMode, functionsMap); } } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/OpMode.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/OpMode.java similarity index 59% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/OpMode.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/modifier/OpMode.java index 11bc7adf..86230619 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/OpMode.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/OpMode.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,73 +15,99 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.modifier; +package io.joltcommunity.jolt.modifier; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.SpecException; import java.util.HashMap; import java.util.List; import java.util.Map; /** - * OpMode differentiates different flavors of Templatr - * - * Templatr can fill in leaf values as required in spec from a specified context, self or a hardcoded + * OpMode differentiates different flavors of Modifier + *

+ * Modifier can fill in leaf values as required in spec from a specified context, self or a hardcoded * default value. However whether or not that 'write' operation should go through, is determined by * this enum. - * - * All of these opModes validates if the if the source (map or list) and the key/index are valid, + *

+ * All of these opModes validates if the source (map or list) and the key/index are valid, * i.e. not null or >= 0, etc. - * + *

  * OVERWRITR always writes
  * DEFAULTR only writes when the the value at the key/index is null
  * DEFINR only writes when source does not contain the key/index
- *
+ * 
*/ public enum OpMode { OVERWRITR("+") { @Override - public boolean isApplicable( final Map source, final String key ) { + public boolean isApplicable(final Map source, final String key) { return super.isApplicable(source, key); } + @Override - public boolean isApplicable( final List source, final int reqIndex , int origSize) { - return super.isApplicable(source, reqIndex , origSize); + public boolean isApplicable(final List source, final int reqIndex, int origSize) { + return super.isApplicable(source, reqIndex, origSize); } }, DEFAULTR("~") { @Override - public boolean isApplicable( final Map source, final String key ) { - return super.isApplicable( source, key ) && source.get( key ) == null; + public boolean isApplicable(final Map source, final String key) { + return super.isApplicable(source, key) && source.get(key) == null; } + @Override - public boolean isApplicable( final List source, final int reqIndex, int origSize ) { - return super.isApplicable(source, reqIndex, origSize ) && source.get( reqIndex ) == null; + public boolean isApplicable(final List source, final int reqIndex, int origSize) { + return super.isApplicable(source, reqIndex, origSize) && source.get(reqIndex) == null; } }, DEFINER("_") { @Override - public boolean isApplicable( final Map source, final String key ) { - return super.isApplicable(source, key) && !source.containsKey( key ); + public boolean isApplicable(final Map source, final String key) { + return super.isApplicable(source, key) && !source.containsKey(key); } + @Override - public boolean isApplicable( final List source, final int reqIndex, int origSize ) { - return super.isApplicable(source, reqIndex, origSize ) && + public boolean isApplicable(final List source, final int reqIndex, int origSize) { + return super.isApplicable(source, reqIndex, origSize) && // only new index contains null - reqIndex >= origSize && source.get( reqIndex ) == null; + reqIndex >= origSize && source.get(reqIndex) == null; } }; + /** + * Static validity checker and instance getter from given op String + */ + private static final Map opModeMap; + + static { + opModeMap = new HashMap<>(); + opModeMap.put(OVERWRITR.op, OVERWRITR); + opModeMap.put(DEFAULTR.op, DEFAULTR); + opModeMap.put(DEFINER.op, DEFINER); + } + /** * Identifier OP prefix that is defined in SPEC */ - private String op; + private final String op; - private OpMode( final String op ) { + private OpMode(final String op) { this.op = op; } + public static boolean isValid(String op) { + return opModeMap.containsKey(op); + } + + public static OpMode from(String op) { + if (isValid(op)) { + return opModeMap.get(op); + } + throw new SpecException("OpMode " + op + " is not valid"); + } + public String getOp() { return op; } @@ -89,9 +116,8 @@ public String toString() { return op + "modify"; } - /** - * Given a source map and a input key returns true if it is ok to go ahead with + * Given a source map and an input key returns true if it is ok to go ahead with * write operation given a specific opMode */ public boolean isApplicable(Map source, String key) { @@ -105,27 +131,4 @@ public boolean isApplicable(Map source, String key) { public boolean isApplicable(List source, int reqIndex, int origSize) { return source != null && reqIndex >= 0 && origSize >= 0; } - - /** - * Static validity checker and instance getter from given op String - */ - private static Map opModeMap; - - static { - opModeMap = new HashMap<>( ); - opModeMap.put( OVERWRITR.op, OVERWRITR ); - opModeMap.put( DEFAULTR.op, DEFAULTR ); - opModeMap.put( DEFINER.op, DEFINER ); - } - - public static boolean isValid(String op) { - return opModeMap.containsKey( op ); - } - - public static OpMode from(String op) { - if ( isValid( op ) ) { - return opModeMap.get( op ); - } - throw new SpecException( "OpMode " + op + " is not valid" ); - } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Dates.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Dates.java new file mode 100644 index 00000000..e4c9ae99 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Dates.java @@ -0,0 +1,304 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.List; + +@SuppressWarnings("deprecated") +public class Dates { + + // Default to ISO8601 format at UTC timezone + public static final String defaultDatePattern = "yyyy-MM-dd'T'HH:mm:ssX"; + + public static final Function now = args -> now(); + + public static final class now extends Function.ListFunction { + + @Override + protected Optional applyList(List input) { + if (input.size() != 2) { + return Optional.empty(); + } else { + return now(input.get(0), input.get(1)); + } + } + } + + @SuppressWarnings("unchecked") + public static final class fromEpochMilli extends Function.BaseFunction { + + @Override + protected Optional applyList(List input) { + if (input.size() != 3) { + return Optional.empty(); + } else { + return (Optional) fromEpochMilli(input.get(0), input.get(1), input.get(2)); + } + } + + @Override + protected Optional applySingle(Object arg) { + return fromEpochMilli(arg, defaultDatePattern, "UTC"); + } + } + + public static final class toEpochMilli extends Function.ListFunction { + + @Override + protected Optional applyList(List input) { + if (input.size() != 3) { + return Optional.empty(); + } else { + return toEpochMilli(input.get(0), input.get(1), input.get(2)); + } + } + } + + public static final class formatDate extends Function.ListFunction { + + @Override + protected Optional applyList(List input) { + if (input.size() == 3) { + return formatDate(input.get(0), input.get(1), input.get(2)); + } else if (input.size() == 4){ + return formatDate(input.get(0), input.get(1), input.get(2), input.get(3)); + } else if (input.size() == 5){ + return formatDate(input.get(0), input.get(1), input.get(2), input.get(3), input.get(4)); + } else { + return Optional.empty(); + } + } + } + + public static final class dateAdd extends Function.ListFunction { + + @Override + protected Optional applyList(List input) { + if (input.size() != 4) { + return Optional.empty(); + } else { + return dateAdd(input.get(0), input.get(1), input.get(2), input.get(3)); + } + } + } + + public static final class dateSubstract extends Function.ListFunction { + + @Override + protected Optional applyList(List input) { + if (input.size() != 4) { + return Optional.empty(); + } else { + return dateSubstract(input.get(0), input.get(1), input.get(2), input.get(3)); + } + } + } + + /** + * This function returns current time, in EPOCH, of the current locale/timezone. + */ + private static Optional now() { + return Optional.of(Instant.now().toEpochMilli()); + } + + /** + * Returns the current time formatted with the provided pattern. If no pattern is provided, + * it defaults to {@link Dates#defaultDatePattern} (ISO8601 format at UTC timezone) + */ + private static Optional now(Object pattern, Object zoneId) { + if (!((pattern instanceof String patternStr) + && zoneId instanceof String zoneIdStr)) + return Optional.empty(); + + try { + Instant instant = Instant.now(); + DateTimeFormatter formatter = createFormatterWithTimeZone(patternStr, zoneIdStr); + return Optional.of(formatter.format(instant)); + } catch (Exception e) { + return Optional.empty(); + } + } + + /** + * Given a {@link java.lang.Number} representing an EPOCH in milliseconds and the pattern and time-zone in which + * it has to be converted returns a String following that representation. + */ + private static Optional fromEpochMilli(Object arg, Object format, Object zoneId) { + Optional optEpoch = castToLong(arg); + if (arg == null + || !(format instanceof String sdfFormat) + || !optEpoch.isPresent() + || !(zoneId instanceof String zoneIdStr)) + return Optional.empty(); + + Long epoch = optEpoch.get(); + try { + Instant instant = Instant.ofEpochMilli(epoch); + DateTimeFormatter formatter = createFormatterWithTimeZone(sdfFormat, zoneIdStr); + return Optional.of(formatter.format(instant)); + } catch (Exception e) { + return Optional.empty(); + } + } + + /** + * Given a String representing a {@link java.util.Date} and the pattern used to represent it, + * returns the EPOCH in milliseconds of that date. The pattern uses the same pattern in + * {@link java.time.format.DateTimeFormatter}. + */ + private static Optional toEpochMilli(Object date, Object format, Object zoneId) { + if (!((date instanceof String dateStr) + && (format instanceof String formatStr) + && (zoneId instanceof String zoneIdStr))) + return Optional.empty(); + + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatStr); + TemporalAccessor temporal = formatter.parse(dateStr); + Instant instant = parseToInstant(temporal, zoneIdStr); + return Optional.of(instant.toEpochMilli()); + } catch (Exception e) { + return Optional.empty(); + } + } + + + private static Optional formatDate(Object date, Object fromPattern, Object toPattern) { + String defaultZoneId = ZoneOffset.UTC.getId(); + return formatDate(date, fromPattern, toPattern, defaultZoneId, defaultZoneId); + } + + private static Optional formatDate(Object date, Object fromPattern, Object toPattern, Object zoneId) { + return formatDate(date, fromPattern, toPattern, zoneId, zoneId); + } + + /** + * Transforms a date from one pattern to another. + */ + private static Optional formatDate(Object date, Object fromPattern, Object toPattern, Object fromZoneId, Object toZoneId) { + if (!((date instanceof String dateStr) + && (fromPattern instanceof String fromPatternStr) + && (toPattern instanceof String toPatternStr) + && (fromZoneId instanceof String fromZoneIdStr) + && (toZoneId instanceof String toZoneIdStr))) + return Optional.empty(); + + try { + DateTimeFormatter fromFormatter = DateTimeFormatter.ofPattern(fromPatternStr); + DateTimeFormatter toFormatter = DateTimeFormatter.ofPattern(toPatternStr).withZone(ZoneId.of(toZoneIdStr)); + TemporalAccessor temporal = fromFormatter.parse(dateStr); + Instant instant = parseToInstant(temporal, fromZoneIdStr); + return Optional.of(toFormatter.format(instant)); + } catch (Exception e) { + return Optional.empty(); + } + } + + private static Optional modifyDate(Object date, Object pattern, Object duration, Object zoneId, boolean add) { + if (!((date instanceof String dateStr) + && (pattern instanceof String patternStr) + && (duration instanceof String durationStr) + && (zoneId instanceof String zoneIdStr))) + return Optional.empty(); + + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(patternStr); + TemporalAccessor temporal = formatter.parse(dateStr); + PeriodDuration periodDuration = computePeriodDuration(durationStr); + Instant instant = parseToInstant(temporal, zoneIdStr); + + LocalDateTime resultDateTime = LocalDateTime.ofInstant(instant, ZoneId.of(zoneIdStr)); + resultDateTime = add ? resultDateTime.plus(periodDuration.period).plus(periodDuration.duration) : + resultDateTime.minus(periodDuration.period).minus(periodDuration.duration); + + return Optional.of(formatter.format(resultDateTime.atZone(ZoneId.of(zoneIdStr)))); + } catch (Exception e) { + return Optional.empty(); + } + } + + /** + * Adds a duration to a date, with duration expressed in ISO8601 format (e.g., P1D). + */ + private static Optional dateAdd(Object date, Object pattern, Object duration, Object zoneId) { + return modifyDate(date, pattern, duration, zoneId,true); + } + + /** + * Subtracts a duration from a date, with duration expressed in ISO8601 format (e.g., P1D). + */ + private static Optional dateSubstract(Object date, Object pattern, Object duration, Object zoneId) { + return modifyDate(date, pattern, duration, zoneId, false); + } + + private static DateTimeFormatter createFormatterWithTimeZone(String pattern, String zoneId) { + return DateTimeFormatter.ofPattern(pattern).withZone(ZoneId.of(zoneId)); + } + + private static Optional castToLong(Object obj) { + if (obj instanceof Number) { + return Optional.of(((Number) obj).longValue()); + } + return Optional.empty(); + } + + /** + * Parses a {@link TemporalAccessor} object into an {@link Instant}. + * The method checks the available fields in the provided {@code temporal} + * and determines the most appropriate way to convert it to an {@code Instant}. + */ + private static Instant parseToInstant(TemporalAccessor temporal, String zoneId) { + if (temporal.isSupported(ChronoField.INSTANT_SECONDS)) { + return Instant.from(temporal); + } else if (temporal.isSupported(ChronoField.HOUR_OF_DAY)) { + LocalDateTime localDateTime = LocalDateTime.from(temporal); + return localDateTime.atZone(ZoneId.of(zoneId)).toInstant(); + } else { + LocalDate localDate = LocalDate.from(temporal); + return localDate.atStartOfDay(ZoneId.of(zoneId)).toInstant(); + } + } + + private record PeriodDuration(Period period, Duration duration) { + } + + private static PeriodDuration computePeriodDuration(String periodDuration) { + if (periodDuration == null) { + return new PeriodDuration(Period.ZERO, Duration.ZERO); + } + + String[] splitted = periodDuration.split("T"); + if (splitted.length == 1) { + // has only date-based fields + return new PeriodDuration(Period.parse(periodDuration), Duration.ZERO); + } else { + Duration duration = Duration.parse("PT" + splitted[1]); + if ("P".equals(splitted[0])) { + // has only time-based fields + return new PeriodDuration(Period.ZERO, duration); + } else { + return new PeriodDuration(Period.parse(splitted[0]), duration); + } + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Function.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Function.java new file mode 100644 index 00000000..627262a2 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Function.java @@ -0,0 +1,400 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.annotation.Experimental; +import io.joltcommunity.jolt.common.Optional; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static java.util.UUID.*; + +/** + * Modifier supports a Function on RHS that accepts jolt path expressions as arguments and evaluates + * them at runtime before calling it. Function always returns an Optional, and the value is written + * only if the optional is not empty. + *

+ * function spec is defined by "key": "=functionName(args...)" + *

+ *

+ * input: + * { "num": -1.0 } + * spec: + * { "num": "=abs(@(1,&0))" } + * will call the stock function Math.abs() and will pass the matching value at "num" + *

+ * spec: + * { "num": "=abs" } + * an alternative shortcut will do the same thing + *

+ * output: + * { "num": 1.0 } + *

+ *

+ *

+ * input: + * { "value": -1.0 } + *

+ * spec: + * { "absValue": "=abs(@(1,value))" } + * will evaluate the jolt path expression @(1,value) and pass the output to stock function Math.abs() + *

+ * output: + * { "value": -1.0, "absValue": 1.0 } + *

+ *

+ *

+ * Currently defined stock functions are: + *

+ * toLower - returns toLower value of toString() value of first arg, rest is ignored + * toUpper - returns toUpper value of toString() value of first arg, rest is ignored + * concat - concatenate all given arguments' toString() values + *

+ * min - returns the min of all numbers provided in the arguments, non-numbers are ignored + * max - returns the max of all numbers provided in the arguments, non-numbers are ignored + * abs - returns the absolute value of first argument, rest is ignored + * toInteger - returns the intValue() value of first argument if its numeric, rest is ignored + * toDouble - returns the doubleValue() value of first argument if its numeric, rest is ignored + * toLong - returns the longValue() value of first argument if its numeric, rest is ignored + *

+ * All of these functions returns Optional.EMPTY if unsuccessful, which results in a no-op when performing + * the actual write in the json doc. + *

+ * i.e. + * input: + * { "value1": "xyz" } --- note: string, not number + * { "value1": "1.0" } --- note: string, not number + *

+ * spec: + * { "value1": "=abs" } --- fails silently + * { "value2": "=abs" } + *

+ * output: + * { "value1": "xyz", "value2": "1" } --- note: "absValue": null is not inserted + *

+ *

+ * This is work in progress, and probably will be changed in future releases. Hence it is marked for + * removal as it'll eventually be moved to a different package as the Function feature is baked into + * other transforms as well. In short this interface is not yet ready to be implemented outside jolt! + */ + +@Experimental +public interface Function { + + /** + * Does nothing + *

+ * spec - "key": "=noop" + *

+ * will cause the key to remain unchanged + */ + Function noop = args -> Optional.empty(); + + /** + * Returns the first argument, null or otherwise + *

+ * spec - "key": [ "=isPresent", "otherValue" ] + *

+ * input - "key": null + * output - "key": null + *

+ * input - "key": "value" + * output - "key": "value" + *

+ * input - key is missing + * output - "key": "otherValue" + */ + Function isPresent = args -> { + if (args.length == 0) { + return Optional.empty(); + } + return Optional.of(args[0]); + }; + + /** + * Returns the first argument if in not null + *

+ * spec - "key": ["=notNull", "otherValue" ] + *

+ * input - "key": null + * output - "key": "otherValue" + *

+ * input - "key": "value" + * output - "key": "value" + */ + Function notNull = args -> { + if (args.length == 0 || args[0] == null) { + return Optional.empty(); + } + return Optional.of(args[0]); + }; + + /** + * Returns the first argument if it is null + *

+ * spec - "key": ["=inNull", "otherValue" ] + *

+ * input - "key": null + * output - "key": null + *

+ * input - "key": "value" + * output - "key": "otherValue" + */ + Function isNull = args -> { + if (args.length == 0 || args[0] != null) { + return Optional.empty(); + } + return Optional.of(args[0]); + }; + + Function uuid = args -> Optional.of(randomUUID().toString()); + + Optional apply(Object... args); + + /** + * Abstract class that processes var-args and calls two abstract methods + *

+ * If its single list arg, or many args, calls applyList() + * else calls applySingle() + * + * @param type of return value + */ + @SuppressWarnings("unchecked") + abstract class BaseFunction implements Function { + + public final Optional apply(final Object... args) { + if (args.length == 0) { + return Optional.empty(); + } else if (args.length == 1) { + if (args[0] instanceof List) { + if (((List) args[0]).isEmpty()) { + return Optional.empty(); + } else { + return applyList((List) args[0]); + } + } else if (args[0] instanceof Object[]) { + if (((Object[]) args[0]).length == 0) { + return Optional.empty(); + } else { + return applyList(Arrays.asList(((Object[]) args[0]))); + } + } else if (args[0] == null) { + return Optional.empty(); + } else { + return (Optional) applySingle(args[0]); + } + } else { + return applyList(Arrays.asList(args)); + } + } + + protected abstract Optional applyList(final List input); + + protected abstract Optional applySingle(final Object arg); + } + + /** + * Abstract class that provides rudimentary abstraction to quickly implement + * a function that works on an single value input + *

+ * i.e. toUpperCase a string + * + * @param type of return value + */ + @SuppressWarnings("unchecked") + abstract class SingleFunction extends BaseFunction { + + protected final Optional applyList(final List input) { + List ret = new ArrayList<>(input.size()); + for (Object o : input) { + Optional optional = applySingle(o); + ret.add(optional.isPresent() ? optional.get() : o); + } + return Optional.of(ret); + } + + protected abstract Optional applySingle(final Object arg); + } + + /** + * Abstract class that provides rudimentary abstraction to quickly implement + * a function that works on an List of input + *

+ * i.e. find the max item from a list, etc. + */ + @SuppressWarnings("unchecked") + abstract class ListFunction extends BaseFunction { + + protected abstract Optional applyList(final List argList); + + protected final Optional applySingle(final Object arg) { + return Optional.empty(); + } + } + + /** + * Abstract class that provides rudimentary abstraction to quickly implement + * a function that classifies first arg as special input and rest as regular + * input. + * + * @param type of special argument + * @param type of return value + */ + @SuppressWarnings("unchecked") + abstract class ArgDrivenFunction implements Function { + + private final Class specialArgType; + + private ArgDrivenFunction() { + /** + * inspired from {@link com.google.common.reflect.TypeCapture#capture()} + * copied, coz jolt-core is designed to have no dependency + * modified, coz the instanceof check and subsequently throwing exception + * is unnecessary as we already know this class has genericSuperClass of + * Parametrized type. In worst case if an implementation does not specify + * the generics, we fall back to Object.class, and that's ok. + */ + Type superclass = getClass().getGenericSuperclass(); + if (superclass instanceof ParameterizedType) { + specialArgType = (Class) ((ParameterizedType) superclass).getActualTypeArguments()[0]; + } else { + specialArgType = (Class) Object.class; + } + } + + private Optional getSpecialArg(Object[] args) { + if ((args.length >= 2) && specialArgType.isInstance(args[0])) { + SOURCE specialArg = (SOURCE) args[0]; + return Optional.of(specialArg); + } + return Optional.empty(); + } + + @Override + public final Optional apply(Object... args) { + + if (args.length == 1 && args[0] instanceof List) { + args = ((List) args[0]).toArray(); + } + + Optional specialArgOptional = getSpecialArg(args); + if (specialArgOptional.isPresent()) { + SOURCE specialArg = specialArgOptional.get(); + if (args.length == 2) { + if (args[1] instanceof List) { + return applyList(specialArg, (List) args[1]); + } else { + return (Optional) applySingle(specialArg, args[1]); + } + } else { + List input = Arrays.asList(Arrays.copyOfRange(args, 1, args.length)); + return applyList(specialArg, input); + } + } else { + return Optional.empty(); + } + } + + protected abstract Optional applyList(SOURCE specialArg, List args); + + protected abstract Optional applySingle(SOURCE specialArg, Object arg); + } + + /** + * Extends ArgDrivenConverter to provide rudimentary abstraction to quickly + * implement a function that works on a single input + *

+ * i.e. increment(1, value) + * + * @param type of special argument + * @param type of return value + */ + @SuppressWarnings("unchecked") + abstract class ArgDrivenSingleFunction extends ArgDrivenFunction { + + protected final Optional applyList(S specialArg, List input) { + List ret = new ArrayList<>(input.size()); + for (Object o : input) { + Optional optional = applySingle(specialArg, o); + ret.add(optional.isPresent() ? optional.get() : o); + } + return (Optional) Optional.of(ret); + } + + protected abstract Optional applySingle(S specialArg, Object arg); + } + + /** + * Extends ArgDrivenConverter to provide rudimentary abstraction to quickly + * implement a function that works on an input list|array + *

+ * i.e. join('-', ...) + * + * @param type of special argument + */ + @SuppressWarnings("unchecked") + abstract class ArgDrivenListFunction extends ArgDrivenFunction { + + protected abstract Optional applyList(S specialArg, List args); + + protected final Optional applySingle(S specialArg, Object arg) { + return Optional.empty(); + } + } + + /** + * squashNull is a special kind of null processing,the input is always a list or map as a singleton + * + * @param type of return value + */ + abstract class SquashFunction implements Function { + + public final Optional apply(final Object... args) { + if (args.length == 0) { + return Optional.empty(); + } else if (args.length == 1) { + if (args[0] instanceof List) { + if (((List) args[0]).isEmpty()) { + return Optional.empty(); + } else { + return (Optional) applySingle((List) args[0]); + } + } else if (args[0] instanceof Object[]) { + if (((Object[]) args[0]).length == 0) { + return Optional.empty(); + } else { + return (Optional) applySingle(Arrays.asList(((Object[]) args[0]))); + } + } else if (args[0] == null) { + return Optional.empty(); + } else { + return (Optional) applySingle(args[0]); + } + } else { + return (Optional) applySingle(Arrays.asList(args)); + } + } + + protected abstract Optional applySingle(final Object arg); + } + +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/FunctionArg.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/FunctionArg.java new file mode 100644 index 00000000..25c65e24 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/FunctionArg.java @@ -0,0 +1,109 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.PathEvaluatingTraversal; +import io.joltcommunity.jolt.common.pathelement.PathElement; +import io.joltcommunity.jolt.common.pathelement.TransposePathElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; + +import java.util.Map; + +public abstract class FunctionArg { + + public static FunctionArg forSelf(PathEvaluatingTraversal traversal) { + return new SelfLookupArg(traversal); + } + + public static FunctionArg forContext(PathEvaluatingTraversal traversal) { + return new ContextLookupArg(traversal); + } + + public static FunctionArg forLiteral(Object obj, boolean parseArg) { + if (parseArg) { + if (obj instanceof String arg) { + if (arg.isEmpty()) { + return new LiteralArg(null); + } else if (arg.startsWith("'") && arg.endsWith("'")) { + return new LiteralArg(arg.substring(1, arg.length() - 1)); + } else if (arg.equalsIgnoreCase("true") || arg.equalsIgnoreCase("false")) { + return new LiteralArg(Boolean.parseBoolean(arg)); + } else { + Optional optional = Objects.toNumber(arg); + if (optional.isPresent()) { + return new LiteralArg(optional.get()); + } + return new LiteralArg(arg); + } + } else { + return new LiteralArg(obj); + } + } else { + return new LiteralArg(obj); + } + } + + public abstract Optional evaluateArg(WalkedPath walkedPath, Map context); + + private static final class SelfLookupArg extends FunctionArg { + private final TransposePathElement pathElement; + + private SelfLookupArg(PathEvaluatingTraversal traversal) { + PathElement pathElement = traversal.get(traversal.size() - 1); + if (pathElement instanceof TransposePathElement) { + this.pathElement = (TransposePathElement) pathElement; + } else { + throw new SpecException("Expected @ path element here"); + } + } + + @Override + public Optional evaluateArg(final WalkedPath walkedPath, final Map context) { + return pathElement.objectEvaluate(walkedPath); + } + } + + private static final class ContextLookupArg extends FunctionArg { + private final PathEvaluatingTraversal traversal; + + private ContextLookupArg(PathEvaluatingTraversal traversal) { + this.traversal = traversal; + } + + @Override + public Optional evaluateArg(final WalkedPath walkedPath, final Map context) { + return traversal.read(context, walkedPath); + } + } + + private static final class LiteralArg extends FunctionArg { + + private final Optional returnValue; + + private LiteralArg(final Object object) { + this.returnValue = Optional.of(object); + } + + @Override + public Optional evaluateArg(final WalkedPath walkedPath, final Map context) { + return returnValue; + } + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/FunctionEvaluator.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/FunctionEvaluator.java similarity index 65% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/FunctionEvaluator.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/FunctionEvaluator.java index e9ef3cbf..e7589e3b 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/FunctionEvaluator.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/FunctionEvaluator.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,35 +15,45 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.modifier.function; +package io.joltcommunity.jolt.modifier.function; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.tree.WalkedPath; import java.util.Map; -@SuppressWarnings( "deprecated" ) +@SuppressWarnings("deprecated") public class FunctionEvaluator { - public static FunctionEvaluator forFunctionEvaluation( Function function, FunctionArg... functionArgs ) { - return new FunctionEvaluator( function, functionArgs ); - } - - public static FunctionEvaluator forArgEvaluation( FunctionArg functionArgs ) { - return new FunctionEvaluator( null, functionArgs ); - } - // function that is evaluated and applied as output private final Function function; // arguments of the function, not evaluated and can be a jolt path expression that // either point to a context or self, or a value present at the matching level private final FunctionArg[] functionArgs; - private FunctionEvaluator( final Function function, final FunctionArg... functionArgs ) { + private FunctionEvaluator(final Function function, final FunctionArg... functionArgs) { this.function = function; this.functionArgs = functionArgs; } + public static FunctionEvaluator forFunctionEvaluation(Function function, FunctionArg... functionArgs) { + return new FunctionEvaluator(function, functionArgs); + } + + public static FunctionEvaluator forArgEvaluation(FunctionArg functionArgs) { + return new FunctionEvaluator(null, functionArgs); + } + + private static Object[] evaluateArgsValue(final FunctionArg[] functionArgs, final Map context, final WalkedPath walkedPath) { + + Object[] evaluatedArgs = new Object[functionArgs.length]; + for (int i = 0; i < functionArgs.length; i++) { + FunctionArg arg = functionArgs[i]; + Optional evaluatedValue = arg.evaluateArg(walkedPath, context); + evaluatedArgs[i] = evaluatedValue.get(); + } + return evaluatedArgs; + } public Optional evaluate(Optional inputOptional, WalkedPath walkedPath, Map context) { @@ -50,23 +61,23 @@ public Optional evaluate(Optional inputOptional, WalkedPath walk try { // "key": "@0", "key": literal - if(function == null) { - valueOptional = functionArgs[0].evaluateArg( walkedPath, context ); + if (function == null) { + valueOptional = functionArgs[0].evaluateArg(walkedPath, context); } // "key": "=abs(@(1,&0))" // this is most usual case, a single argument is passed and we need to evaluate and // pass the value, if present, to the spec function - else if( functionArgs.length == 1 ) { - Optional evaluatedArgValue = functionArgs[0].evaluateArg( walkedPath, context ); - valueOptional = evaluatedArgValue.isPresent() ? function.apply( evaluatedArgValue.get() ): function.apply( ); + else if (functionArgs.length == 1) { + Optional evaluatedArgValue = functionArgs[0].evaluateArg(walkedPath, context); + valueOptional = evaluatedArgValue.isPresent() ? function.apply(evaluatedArgValue.get()) : function.apply(); } // "key": "=abs(@(1,&0),-1,-3)" // this is more complicated case! if args is an array, after evaluation we cannot pass a missing value wrapped in // object[] into function. In such case null will be passed however, in json null is also a valid value, so it is // upto the implementer to interpret the value. Ideally we can almost always pass a list straight from input. - else if( functionArgs.length > 1 ) { - Object[] evaluatedArgs = evaluateArgsValue( functionArgs, context, walkedPath ); - valueOptional = function.apply( evaluatedArgs ); + else if (functionArgs.length > 1) { + Object[] evaluatedArgs = evaluateArgsValue(functionArgs, context, walkedPath); + valueOptional = function.apply(evaluatedArgs); } // // FYI this is where the "magic" happens that allows functions that take a single method @@ -76,23 +87,12 @@ else if( functionArgs.length > 1 ) { // "key": "=abs" else { // pass current value as arg if present - valueOptional = inputOptional.isPresent() ? function.apply( inputOptional.get()) : function.apply( ); + valueOptional = inputOptional.isPresent() ? function.apply(inputOptional.get()) : function.apply(); } + } catch (Exception ignored) { } - catch(Exception ignored) {} return valueOptional; } - - private static Object[] evaluateArgsValue( final FunctionArg[] functionArgs, final Map context, final WalkedPath walkedPath ) { - - Object[] evaluatedArgs = new Object[functionArgs.length]; - for(int i=0; i evaluatedValue = arg.evaluateArg( walkedPath, context ); - evaluatedArgs[i] = evaluatedValue.get(); - } - return evaluatedArgs; - } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Lists.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Lists.java similarity index 64% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Lists.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Lists.java index 86ee53c5..7a5a7b66 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/modifier/function/Lists.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Lists.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,14 +15,14 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.modifier.function; +package io.joltcommunity.jolt.modifier.function; -import com.bazaarvoice.jolt.common.Optional; +import io.joltcommunity.jolt.common.Optional; import java.util.Arrays; import java.util.List; -@SuppressWarnings( "deprecated" ) +@SuppressWarnings("deprecated") public class Lists { /** @@ -30,9 +31,9 @@ public class Lists { public static final class firstElement extends Function.ListFunction { @Override - protected Optional applyList( final List argList ) { + protected Optional applyList(final List argList) { return argList.size() > 0 ? - Optional.of( argList.get( 0 ) ) : + Optional.of(argList.get(0)) : Optional.empty(); } } @@ -43,9 +44,9 @@ protected Optional applyList( final List argList ) { public static final class lastElement extends Function.ListFunction { @Override - protected Optional applyList( final List argList ) { + protected Optional applyList(final List argList) { return argList.size() > 0 ? - Optional.of( argList.get( argList.size() - 1 ) ) : + Optional.of(argList.get(argList.size() - 1)) : Optional.empty(); } } @@ -56,9 +57,9 @@ protected Optional applyList( final List argList ) { public static final class elementAt extends Function.ArgDrivenListFunction { @Override - protected Optional applyList( final Integer specialArg, final List args ) { - if ( specialArg != null && args != null && args.size() > specialArg ) { - return Optional.of( args.get( specialArg ) ); + protected Optional applyList(final Integer specialArg, final List args) { + if (specialArg != null && args != null && args.size() > specialArg) { + return Optional.of(args.get(specialArg)); } return Optional.empty(); } @@ -69,13 +70,13 @@ protected Optional applyList( final Integer specialArg, final List { @Override - protected Optional applyList( final List input ) { - return Optional.of( input ); + protected Optional applyList(final List input) { + return Optional.of(input); } @Override - protected Optional applySingle( final Object arg ) { - return Optional.of( Arrays.asList( arg ) ); + protected Optional applySingle(final Object arg) { + return Optional.of(Arrays.asList(arg)); } } @@ -85,21 +86,21 @@ protected Optional applySingle( final Object arg ) { public static final class sort extends Function.BaseFunction { @Override - protected Optional applyList( final List argList ) { + protected Optional applyList(final List argList) { try { Object[] dest = argList.toArray(); - Arrays.sort( dest ); - return Optional.of( dest ); + Arrays.sort(dest); + return Optional.of(dest); } // if any of the elements are not Comparable it'll throw a ClassCastException - catch(Exception ignored) { + catch (Exception ignored) { return Optional.empty(); } } @Override - protected Optional applySingle( final Object arg ) { - return Optional.of( arg ); + protected Optional applySingle(final Object arg) { + return Optional.of(arg); } } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Math.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Math.java new file mode 100644 index 00000000..a5e5550d --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Math.java @@ -0,0 +1,559 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.List; + +@SuppressWarnings("deprecated") +public class Math { + + /** + * Given a list of objects, returns the max value in its appropriate type + * also, interprets String as Number and returns appropriately + *

+ * max(1,2l,3d) == Optional.of(3d) + * max(1,2l,"3.0") == Optional.of(3.0) + * max("a", "b", "c") == Optional.empty() + * max([]) == Optional.empty() + */ + public static Optional max(List args) { + if (args == null || args.size() == 0) { + return Optional.empty(); + } + + Integer maxInt = Integer.MIN_VALUE; + Double maxDouble = -(Double.MAX_VALUE); + Long maxLong = Long.MIN_VALUE; + boolean found = false; + + for (Object arg : args) { + if (arg instanceof Integer) { + maxInt = java.lang.Math.max(maxInt, (Integer) arg); + found = true; + } else if (arg instanceof Double) { + maxDouble = java.lang.Math.max(maxDouble, (Double) arg); + found = true; + } else if (arg instanceof Long) { + maxLong = java.lang.Math.max(maxLong, (Long) arg); + found = true; + } else if (arg instanceof String) { + Optional optional = Objects.toNumber(arg); + if (optional.isPresent()) { + arg = optional.get(); + if (arg instanceof Integer) { + maxInt = java.lang.Math.max(maxInt, (Integer) arg); + found = true; + } else if (arg instanceof Double) { + maxDouble = java.lang.Math.max(maxDouble, (Double) arg); + found = true; + } else if (arg instanceof Long) { + maxLong = java.lang.Math.max(maxLong, (Long) arg); + found = true; + } + } + } + } + if (!found) { + return Optional.empty(); + } + + // explicit getter method calls to avoid runtime autoboxing + // autoBoxing will cause it to return the different type + // check MathTest#testAutoBoxingIssue for example + if (maxInt.longValue() >= maxDouble.longValue() && maxInt.longValue() >= maxLong) { + return Optional.of(maxInt); + } else if (maxLong >= maxDouble.longValue()) { + return Optional.of(maxLong); + } else { + return Optional.of(maxDouble); + } + } + + /** + * Given a list of objects, returns the min value in its appropriate type + * also, interprets String as Number and returns appropriately + *

+ * min(1d,2l,3) == Optional.of(1d) + * min("1.0",2l,d) == Optional.of(1.0) + * min("a", "b", "c") == Optional.empty() + * min([]) == Optional.empty() + */ + public static Optional min(List args) { + if (args == null || args.size() == 0) { + return Optional.empty(); + } + Integer minInt = Integer.MAX_VALUE; + Double minDouble = Double.MAX_VALUE; + Long minLong = Long.MAX_VALUE; + boolean found = false; + + for (Object arg : args) { + if (arg instanceof Integer) { + minInt = java.lang.Math.min(minInt, (Integer) arg); + found = true; + } else if (arg instanceof Double) { + minDouble = java.lang.Math.min(minDouble, (Double) arg); + found = true; + } else if (arg instanceof Long) { + minLong = java.lang.Math.min(minLong, (Long) arg); + found = true; + } else if (arg instanceof String) { + Optional optional = Objects.toNumber(arg); + if (optional.isPresent()) { + arg = optional.get(); + if (arg instanceof Integer) { + minInt = java.lang.Math.min(minInt, (Integer) arg); + found = true; + } else if (arg instanceof Double) { + minDouble = java.lang.Math.min(minDouble, (Double) arg); + found = true; + } else if (arg instanceof Long) { + minLong = java.lang.Math.min(minLong, (Long) arg); + found = true; + } + } + } + } + if (!found) { + return Optional.empty(); + } + // explicit getter method calls to avoid runtime autoboxing + if (minInt.longValue() <= minDouble.longValue() && minInt.longValue() <= minLong) { + return Optional.of(minInt); + } else if (minLong <= minDouble.longValue()) { + return Optional.of(minLong); + } else { + return Optional.of(minDouble); + } + } + + /** + * Given any object, returns, if possible. its absolute value wrapped in Optional + * Interprets String as Number + *

+ * abs("-123") == Optional.of(123) + * abs("123") == Optional.of(123) + * abs("12.3") == Optional.of(12.3) + *

+ * abs("abc") == Optional.empty() + * abs(null) == Optional.empty() + */ + public static Optional abs(Object arg) { + if (arg instanceof Integer) { + return Optional.of(java.lang.Math.abs((Integer) arg)); + } else if (arg instanceof Double) { + return Optional.of(java.lang.Math.abs((Double) arg)); + } else if (arg instanceof Long) { + return Optional.of(java.lang.Math.abs((Long) arg)); + } else if (arg instanceof String) { + return abs(Objects.toNumber(arg).get()); + } + return Optional.empty(); + } + + /** + * Given a list of numbers, returns their avg as double + * any value in the list that is not a valid number is ignored + *

+ * avg(2,"2","abc") == Optional.of(2.0) + */ + public static Optional avg(List args) { + double sum = 0d; + int count = 0; + for (Object arg : args) { + Optional numberOptional = Objects.toNumber(arg); + if (numberOptional.isPresent()) { + sum = sum + numberOptional.get().doubleValue(); + count = count + 1; + } + } + return count == 0 ? Optional.empty() : Optional.of(sum / count); + } + + public static Optional intSum(List args) { + Integer sum = 0; + for (Object arg : args) { + Optional numberOptional = Objects.toInteger(arg); + if (numberOptional.isPresent()) { + sum = sum + numberOptional.get(); + } + } + return Optional.of(sum); + } + + public static Optional doubleSum(List args) { + Double sum = 0.0; + for (Object arg : args) { + Optional numberOptional = Objects.toDouble(arg); + if (numberOptional.isPresent()) { + sum = sum + numberOptional.get(); + } + } + return Optional.of(sum); + } + + public static Optional longSum(List args) { + Long sum = 0l; + for (Object arg : args) { + Optional numberOptional = Objects.toLong(arg); + if (numberOptional.isPresent()) { + sum = sum + numberOptional.get(); + } + } + return Optional.of(sum); + } + + public static Optional intSubtract(List argList) { + + if (argList == null || argList.size() != 2) { + return Optional.empty(); + } + + if (!(argList.get(0) instanceof Integer && argList.get(1) instanceof Integer)) { + return Optional.empty(); + } + + int a = (Integer) argList.get(0); + int b = (Integer) argList.get(1); + + return Optional.of(a - b); + } + + public static Optional doubleSubtract(List argList) { + + if (argList == null || argList.size() != 2) { + return Optional.empty(); + } + + if (!(argList.get(0) instanceof Double && argList.get(1) instanceof Double)) { + return Optional.empty(); + } + + double a = (Double) argList.get(0); + double b = (Double) argList.get(1); + + return Optional.of(a - b); + } + + public static Optional longSubtract(List argList) { + + if (argList == null || argList.size() != 2) { + return Optional.empty(); + } + + if (!(argList.get(0) instanceof Long && argList.get(1) instanceof Long)) { + return Optional.empty(); + } + + long a = (Long) argList.get(0); + long b = (Long) argList.get(1); + + return Optional.of(a - b); + } + + public static Optional multiply(List argList) { + if (argList.size() != 2) { + return Optional.empty(); + } + + Optional leftOpt = Objects.toNumber(argList.get(0)); + Optional rightOpt = Objects.toNumber(argList.get(1)); + + if (!leftOpt.isPresent() || !rightOpt.isPresent()) { + return Optional.empty(); + } + + Number left = leftOpt.get(); + Number right = rightOpt.get(); + + // 1. If either is BigDecimal + if (left instanceof BigDecimal || right instanceof BigDecimal) { + BigDecimal l = (left instanceof BigDecimal) ? (BigDecimal) left : new BigDecimal(left.toString()); + BigDecimal r = (right instanceof BigDecimal) ? (BigDecimal) right : new BigDecimal(right.toString()); + return Optional.of(l.multiply(r)); + } + // 2. If left is BigInteger and right is Double + if (left instanceof BigInteger && right instanceof Double) { + BigDecimal l = new BigDecimal(left.toString()); + BigDecimal r = BigDecimal.valueOf((Double) right); + return Optional.of(l.multiply(r)); + } + // 3. If left is Double and right is BigInteger + if (left instanceof Double && right instanceof BigInteger) { + BigDecimal l = BigDecimal.valueOf((Double) left); + BigDecimal r = new BigDecimal(right.toString()); + return Optional.of(l.multiply(r)); + } + // 4. If either is BigInteger + if (left instanceof BigInteger || right instanceof BigInteger) { + BigInteger l = (left instanceof BigInteger) ? (BigInteger) left : BigInteger.valueOf(left.longValue()); + BigInteger r = (right instanceof BigInteger) ? (BigInteger) right : BigInteger.valueOf(right.longValue()); + return Optional.of(l.multiply(r)); + } + // 5. If either is Double + if (left instanceof Double || right instanceof Double) { + return Optional.of(left.doubleValue() * right.doubleValue()); + } + // 6. Otherwise, use long multiplication + return Optional.of(left.longValue() * right.longValue()); + } + + public static Optional multiplyAndRound(List argList, int digitsAfterDecimalPoint, RoundingMode roundingMode) { + Optional result = multiply(argList); + if (result.isPresent()) { + Number resultValue = result.get(); + if (resultValue instanceof Double) { + BigDecimal bigDecimal = BigDecimal.valueOf((Double) resultValue).setScale(digitsAfterDecimalPoint, roundingMode); + return Optional.of(bigDecimal.doubleValue()); + } + } + return result; + } + + public static Optional divide(List argList) { + + if (argList == null || argList.size() != 2) { + return Optional.empty(); + } + + Optional numerator = Objects.toNumber(argList.get(0)); + Optional denominator = Objects.toNumber(argList.get(1)); + + if (numerator.isPresent() && denominator.isPresent()) { + + Double drDoubleValue = denominator.get().doubleValue(); + if (drDoubleValue == 0) { + return Optional.empty(); + } + + Double nrDoubleValue = numerator.get().doubleValue(); + Double result = nrDoubleValue / drDoubleValue; + return Optional.of(result); + } + + return Optional.empty(); + } + + public static Optional divideAndRound(List argList, int digitsAfterDecimalPoint, RoundingMode roundingMode) { + + Optional divideResult = divide(argList); + + if (divideResult.isPresent()) { + Double divResult = divideResult.get(); + BigDecimal bigDecimal = new BigDecimal(divResult).setScale(digitsAfterDecimalPoint, roundingMode); + return Optional.of(bigDecimal.doubleValue()); + } + + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + public static final class max extends Function.BaseFunction { + @Override + protected Optional applyList(final List argList) { + return (Optional) max(argList); + } + + @Override + protected Optional applySingle(final Object arg) { + if (arg instanceof Number) { + return Optional.of(arg); + } else { + return Optional.empty(); + } + } + } + + @SuppressWarnings("unchecked") + public static final class min extends Function.BaseFunction { + + @Override + protected Optional applyList(final List argList) { + return (Optional) min(argList); + } + + @Override + protected Optional applySingle(Object arg) { + if (arg instanceof Number) { + return Optional.of(arg); + } else { + return Optional.empty(); + } + } + } + + @SuppressWarnings("unchecked") + public static final class abs extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + return abs(arg); + } + } + + @SuppressWarnings("unchecked") + public static final class multiply extends Function.ListFunction { + + @Override + protected Optional applyList(List argList) { + return (Optional) multiply(argList); + } + + } + + @SuppressWarnings("unchecked") + public static final class multiplyAndRound extends Function.ListFunction { + @Override + protected Optional applyList(List argList) { + do { + if (argList.size() < 3) { + break; + } + Object digitsObj = argList.get(0); + + if (!(digitsObj instanceof Integer)) { + break; + } + int digitsAfterDecimalPoint = (Integer) digitsObj; + if (argList.size() == 3) { + List numbers = argList.subList(1, argList.size()); + return (Optional) multiplyAndRound(numbers, digitsAfterDecimalPoint, RoundingMode.HALF_UP); + } + Object roundingModeObj = argList.get(1); + if (!(roundingModeObj instanceof String)) { + break; + } + RoundingMode roundingMode; + try { + roundingMode = RoundingMode.valueOf(((String) roundingModeObj).toUpperCase()); + } catch (IllegalArgumentException e) { + break; + } + List numbers = argList.subList(2, argList.size()); + return (Optional) multiplyAndRound(numbers, digitsAfterDecimalPoint, roundingMode); + } while (false); + return Optional.empty(); + } + } + + @SuppressWarnings("unchecked") + public static final class divide extends Function.ListFunction { + + @Override + protected Optional applyList(List argList) { + return (Optional) divide(argList); + } + + } + + @SuppressWarnings("unchecked") + public static final class divideAndRound extends Function.ListFunction { + + @Override + protected Optional applyList(List argList) { + do { + if (argList.size() < 3) { + break; + } + Object digitsObj = argList.get(0); + + if (!(digitsObj instanceof Integer)) { + break; + } + int digitsAfterDecimalPoint = (Integer) digitsObj; + if (argList.size() == 3) { + List numbers = argList.subList(1, argList.size()); + return (Optional) divideAndRound(numbers, digitsAfterDecimalPoint, RoundingMode.HALF_UP); + } + Object roundingModeObj = argList.get(1); + if (!(roundingModeObj instanceof String)) { + break; + } + RoundingMode roundingMode; + try { + roundingMode = RoundingMode.valueOf(((String) roundingModeObj).toUpperCase()); + } catch (IllegalArgumentException e) { + break; + } + List numbers = argList.subList(2, argList.size()); + return (Optional) divideAndRound(numbers, digitsAfterDecimalPoint, roundingMode); + } while (false); + return Optional.empty(); + } + } + + @SuppressWarnings("unchecked") + public static final class avg extends Function.ListFunction { + @Override + protected Optional applyList(final List argList) { + return (Optional) avg(argList); + } + } + + @SuppressWarnings("unchecked") + public static final class intSum extends Function.ListFunction { + @Override + protected Optional applyList(final List argIntList) { + return (Optional) intSum(argIntList); + } + } + + @SuppressWarnings("unchecked") + public static final class doubleSum extends Function.ListFunction { + @Override + protected Optional applyList(final List argDoubleList) { + return (Optional) doubleSum(argDoubleList); + } + } + + @SuppressWarnings("unchecked") + public static final class longSum extends Function.ListFunction { + @Override + protected Optional applyList(final List argLongList) { + return (Optional) longSum(argLongList); + } + } + + @SuppressWarnings("unchecked") + public static final class intSubtract extends Function.ListFunction { + @Override + protected Optional applyList(final List argIntList) { + return (Optional) intSubtract(argIntList); + } + } + + @SuppressWarnings("unchecked") + public static final class doubleSubtract extends Function.ListFunction { + @Override + protected Optional applyList(final List argDoubleList) { + return (Optional) doubleSubtract(argDoubleList); + } + } + + @SuppressWarnings("unchecked") + public static final class longSubtract extends Function.ListFunction { + @Override + protected Optional applyList(final List argLongList) { + return (Optional) longSubtract(argLongList); + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Objects.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Objects.java new file mode 100644 index 00000000..c575a14a --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Objects.java @@ -0,0 +1,296 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Objects { + + /** + * Given any object, returns, if possible. its Java number equivalent wrapped in Optional + * Interprets String as Number + *

+ * toNumber("123") == Optional.of(123) + * toNumber("-123") == Optional.of(-123) + * toNumber("12.3") == Optional.of(12.3) + *

+ * toNumber("abc") == Optional.empty() + * toNumber(null) == Optional.empty() + *

+ * also, see: MathTest#testNitPicks + */ + public static Optional toNumber(Object arg) { + if (arg instanceof Number) { + return Optional.of(((Number) arg)); + } else if (arg instanceof String) { + try { + return Optional.of((Number) Integer.parseInt((String) arg)); + } catch (Exception ignored) { + } + try { + return Optional.of((Number) Long.parseLong((String) arg)); + } catch (Exception ignored) { + } + try { + return Optional.of((Number) Double.parseDouble((String) arg)); + } catch (Exception ignored) { + } + return Optional.empty(); + } else { + return Optional.empty(); + } + } + + /** + * Returns int value of argument, if possible, wrapped in Optional + * Interprets String as Number + */ + public static Optional toInteger(Object arg) { + if (arg instanceof Number) { + return Optional.of(((Number) arg).intValue()); + } else if (arg instanceof String) { + Optional optional = toNumber(arg); + if (optional.isPresent()) { + return Optional.of(optional.get().intValue()); + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } + + /** + * Returns long value of argument, if possible, wrapped in Optional + * Interprets String as Number + */ + public static Optional toLong(Object arg) { + if (arg instanceof Number) { + return Optional.of(((Number) arg).longValue()); + } else if (arg instanceof String) { + Optional optional = toNumber(arg); + if (optional.isPresent()) { + return Optional.of(optional.get().longValue()); + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } + + /** + * Returns double value of argument, if possible, wrapped in Optional + * Interprets String as Number + */ + public static Optional toDouble(Object arg) { + if (arg instanceof Number) { + return Optional.of(((Number) arg).doubleValue()); + } else if (arg instanceof String) { + Optional optional = toNumber(arg); + if (optional.isPresent()) { + return Optional.of(optional.get().doubleValue()); + } else { + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } + + /** + * Returns boolean value of argument, if possible, wrapped in Optional + * Interprets Strings "true" & "false" as boolean + */ + public static Optional toBoolean(Object arg) { + if (arg instanceof Boolean) { + return Optional.of((Boolean) arg); + } else if (arg instanceof String) { + if ("true".equalsIgnoreCase((String) arg)) { + return Optional.of(Boolean.TRUE); + } else if ("false".equalsIgnoreCase((String) arg)) { + return Optional.of(Boolean.FALSE); + } + } + return Optional.empty(); + } + + /** + * Returns String representation of argument, wrapped in Optional + *

+ * for array argument, returns Arrays.toString() + * for others, returns Objects.toString() + *

+ * Note: this method does not return Optional.empty() + */ + public static Optional toString(Object arg) { + if (arg instanceof String) { + return Optional.of((String) arg); + } else if (arg instanceof Object[]) { + return Optional.of(Arrays.toString((Object[]) arg)); + } else { + return Optional.of(java.util.Objects.toString(arg)); + } + } + + /** + * Squashes nulls in a list or map. + *

+ * Modifies the data. + */ + public static void squashNulls(Object input) { + if (input instanceof List inputList) { + inputList.removeIf(java.util.Objects::isNull); + } else if (input instanceof Map) { + Map inputMap = (Map) input; + + List keysToNuke = new ArrayList<>(); + for (Map.Entry entry : inputMap.entrySet()) { + if (entry.getValue() == null) { + keysToNuke.add(entry.getKey()); + } + } + + inputMap.keySet().removeAll(keysToNuke); + } + } + + /** + * Recursively squash nulls in maps and lists. + *

+ * Modifies the data. + */ + public static void recursivelySquashNulls(Object input) { + + // Makes two passes thru the data. + Objects.squashNulls(input); + + if (input instanceof List inputList) { + inputList.forEach(Objects::recursivelySquashNulls); + } else if (input instanceof Map) { + Map inputMap = (Map) input; + + for (Map.Entry entry : inputMap.entrySet()) { + recursivelySquashNulls(entry.getValue()); + } + } + } + + /** + * Squashes/Deletes duplicates in lists. + *

+ * Modifies the data. + */ + public static Optional squashDuplicates(Object input) { + if (input instanceof List inputList) { + return Optional.of(inputList.stream().distinct().collect(Collectors.toList())); + } + return Optional.of(input); + } + + public static final class toInteger extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + return toInteger(arg); + } + } + + public static final class toLong extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + return toLong(arg); + } + } + + public static final class toDouble extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + return toDouble(arg); + } + } + + public static final class toBoolean extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + return toBoolean(arg); + } + } + + public static final class toString extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + return Objects.toString(arg); + } + } + + public static final class squashNulls extends Function.SquashFunction { + @Override + protected Optional applySingle(final Object arg) { + Objects.squashNulls(arg); + return Optional.of(arg); + } + } + + public static final class recursivelySquashNulls extends Function.SquashFunction { + @Override + protected Optional applySingle(final Object arg) { + Objects.recursivelySquashNulls(arg); + return Optional.of(arg); + } + } + + public static final class squashDuplicates extends Function.SquashFunction { + @Override + protected Optional applySingle(final Object arg) { + return Objects.squashDuplicates(arg); + } + } + + /** + * Size is a special snowflake and needs specific care + */ + public static final class size implements Function { + + @Override + public Optional apply(Object... args) { + if (args.length == 0) { + return Optional.empty(); + } else if (args.length == 1) { + if (args[0] == null) { + return Optional.empty(); + } else if (args[0] instanceof List) { + return Optional.of(((List) args[0]).size()); + } else if (args[0] instanceof String) { + return Optional.of(((String) args[0]).length()); + } else if (args[0] instanceof Map) { + return Optional.of(((Map) args[0]).size()); + } else { + return Optional.empty(); + } + } else { + return Optional.of(args.length); + } + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Strings.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Strings.java new file mode 100644 index 00000000..714c63de --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/function/Strings.java @@ -0,0 +1,281 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.PatternSyntaxException; + +@SuppressWarnings("deprecated") +public class Strings { + + private static Optional padString(boolean leftPad, String source, List args) { + + // There is only one path that leads to success and many + // ways for this to fail. So using a do/while loop + // to make the bailing easy. + do { + + if (source == null || args == null) { + break; + } + + if (!(args.get(0) instanceof Integer width && + args.get(1) instanceof String filler)) { + break; + } + + // if the width param is stupid; bail + if (width <= 0 || width > 500) { + break; + } + + // filler can only be a single char + // otherwise the math becomes hard + if (filler.length() != 1) { + break; + } + + char fillerChar = filler.charAt(0); + + // if the desired width of the overall padding is smaller than + // the source string, then just return the source string. + if (width <= source.length()) { + return Optional.of(source); + } + + int padLength = width - source.length(); + char[] padArray = new char[padLength]; + + Arrays.fill(padArray, fillerChar); + + StringBuilder sb = new StringBuilder(); + + if (leftPad) { + sb.append(padArray).append(source); + } else { + sb.append(source).append(padArray); + } + + return Optional.of(sb.toString()); + + } while (false); + + return Optional.empty(); + } + + public static final class toLowerCase extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + + if (!(arg instanceof String argString)) { + return Optional.empty(); + } + + return Optional.of(argString.toLowerCase()); + } + } + + public static final class toUpperCase extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + + if (!(arg instanceof String argString)) { + return Optional.empty(); + } + + return Optional.of(argString.toUpperCase()); + } + } + + public static final class trim extends Function.SingleFunction { + @Override + protected Optional applySingle(final Object arg) { + + if (!(arg instanceof String argString)) { + return Optional.empty(); + } + + return Optional.of(argString.trim()); + } + } + + public static final class concat extends Function.ListFunction { + @Override + protected Optional applyList(final List argList) { + StringBuilder sb = new StringBuilder(); + for (Object arg : argList) { + if (arg != null) { + sb.append(arg.toString()); + } + } + return Optional.of(sb.toString()); + } + } + + public static final class substring extends Function.ListFunction { + + @Override + protected Optional applyList(List argList) { + + // There is only one path that leads to success and many + // ways for this to fail. So using a do/while loop + // to make the bailing easy. + do { + + // if argList is null or not the right size; bail + if (argList == null || argList.size() != 3) { + break; + } + + if (!(argList.get(0) instanceof String tuna && + argList.get(1) instanceof Integer && + argList.get(2) instanceof Integer)) { + break; + } + + // If we get here, then all these casts should work. + int start = (Integer) argList.get(1); + int end = (Integer) argList.get(2); + + // do start and end make sense? + if (start >= end || start < 0 || end < 1 || end > tuna.length()) { + break; + } + + return Optional.of(tuna.substring(start, end)); + + } while (false); + + // if we got here, then return an Optional.empty. + return Optional.empty(); + } + } + + @SuppressWarnings("unchecked") + public static final class join extends Function.ArgDrivenListFunction { + + @Override + protected Optional applyList(final String specialArg, final List args) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < args.size(); i++) { + Object arg = args.get(i); + if (arg != null) { + String argString = arg.toString(); + if (!("".equals(argString))) { + sb.append(argString); + if (i < args.size() - 1) { + sb.append(specialArg); + } + } + } + } + return Optional.of(sb.toString()); + } + } + + public static final class split extends Function.ArgDrivenSingleFunction { + @Override + protected Optional applySingle(final String separator, final Object source) { + if (source == null || separator == null) { + return Optional.empty(); + } else if (source instanceof String inputString) { + // only try to split input strings + return Optional.of(Arrays.asList(inputString.split(separator))); + } else { + return Optional.empty(); + } + } + } + + public static final class leftPad extends Function.ArgDrivenListFunction { + @Override + protected Optional applyList(String source, List args) { + + return padString(true, source, args); + } + } + + public static final class rightPad extends Function.ArgDrivenListFunction { + @Override + protected Optional applyList(String source, List args) { + + return padString(false, source, args); + } + } + + public static final class replace extends Function.ListFunction { + @Override + protected Optional applyList(List args) { + + // There is only one path that leads to success and many + // ways for this to fail. So using a do/while loop + // to make the bailing easy. + do { + + if (args.size() != 3) { + break; + } + + if (!(args.get(0) instanceof String source && + args.get(1) instanceof String target && + args.get(2) instanceof String replacement)) { + break; + } + + return Optional.of(source.replace(target, replacement)); + + } while (false); + + return Optional.empty(); + } + } + + public static final class replaceAll extends Function.ListFunction { + @Override + protected Optional applyList(List args) { + + // There is only one path that leads to success and many + // ways for this to fail. So using a do/while loop + // to make the bailing easy. + do { + + if (args.size() != 3) { + break; + } + + if (!(args.get(0) instanceof String source && + args.get(1) instanceof String regex && + args.get(2) instanceof String replacement)) { + break; + } + + try { + return Optional.of(source.replaceAll(regex, replacement)); + } catch (PatternSyntaxException e) { + // if the regex is invalid, we just return an empty Optional + break; + } + + } while (false); + + return Optional.empty(); + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierCompositeSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierCompositeSpec.java new file mode 100644 index 00000000..5264bab2 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierCompositeSpec.java @@ -0,0 +1,190 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.spec; + +import io.joltcommunity.jolt.common.ComputedKeysComparator; +import io.joltcommunity.jolt.common.ExecutionStrategy; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.pathelement.*; +import io.joltcommunity.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.spec.OrderedCompositeSpec; +import io.joltcommunity.jolt.common.tree.ArrayMatchedElement; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.modifier.DataType; +import io.joltcommunity.jolt.modifier.OpMode; +import io.joltcommunity.jolt.modifier.ModifierSpecBuilder; + +import java.util.*; + +/** + * Composite spec is non-leaf level spec that contains one or many child specs and processes + * them based on a pre-determined execution strategy + */ +public class ModifierCompositeSpec extends ModifierSpec implements OrderedCompositeSpec { + private static final HashMap orderMap; + private static final ComputedKeysComparator computedKeysComparator; + + static { + orderMap = new HashMap<>(); + orderMap.put(ArrayPathElement.class, 1); + orderMap.put(StarRegexPathElement.class, 2); + orderMap.put(StarDoublePathElement.class, 3); + orderMap.put(StarSinglePathElement.class, 4); + orderMap.put(StarAllPathElement.class, 5); + computedKeysComparator = ComputedKeysComparator.fromOrder(orderMap); + } + + private final Map literalChildren; + private final List computedChildren; + private final ExecutionStrategy executionStrategy; + private final DataType specDataType; + + public ModifierCompositeSpec(final String key, final Map spec, final OpMode opMode, ModifierSpecBuilder specBuilder) { + super(key, opMode); + + Map literals = new LinkedHashMap<>(); + ArrayList computed = new ArrayList<>(); + + List children = specBuilder.createSpec(spec); + + // remember max explicit index from spec to expand input array at runtime + // need to validate spec such that it does not specify both array and literal path element + int maxExplicitIndexFromSpec = -1, confirmedMapAtIndex = -1, confirmedArrayAtIndex = -1; + + for (int i = 0; i < children.size(); i++) { + ModifierSpec childSpec = children.get(i); + PathElement childPathElement = childSpec.pathElement; + + // for every child, + // a) mark current index as either must be map or must be array + // b) mark it as literal or computed + // c) if arrayPathElement, + // - make sure its an explicit index type + // - save the max explicit index in spec + if (childPathElement instanceof LiteralPathElement) { + confirmedMapAtIndex = i; + literals.put(childPathElement.getRawKey(), childSpec); + } else if (childPathElement instanceof ArrayPathElement childArrayPathElement) { + confirmedArrayAtIndex = i; + + if (!childArrayPathElement.isExplicitArrayIndex()) { + throw new SpecException(opMode.name() + " RHS only supports explicit Array path element"); + } + int explicitIndex = childArrayPathElement.getExplicitArrayIndex(); + // if explicit index from spec also enforces "[...]?" don't bother using that as max index + if (!childSpec.checkValue) { + maxExplicitIndexFromSpec = Math.max(maxExplicitIndexFromSpec, explicitIndex); + } + + literals.put(String.valueOf(explicitIndex), childSpec); + } else { + // StarPathElements evaluates to String keys in a Map, EXCEPT StarAllPathElement + // which can be both all keys in a map or all indexes in a list + if (!(childPathElement instanceof StarAllPathElement)) { + confirmedMapAtIndex = i; + } + computed.add(childSpec); + } + + // Bail as soon as both confirmedMapAtIndex & confirmedArrayAtIndex is set + if (confirmedMapAtIndex > -1 && confirmedArrayAtIndex > -1) { + throw new SpecException(opMode.name() + " RHS cannot mix int array index and string map key, defined spec for " + key + " contains: " + children.get(confirmedMapAtIndex).pathElement.getCanonicalForm() + " conflicting " + children.get(confirmedArrayAtIndex).pathElement.getCanonicalForm()); + } + } + + // set the dataType from calculated indexes + specDataType = DataType.determineDataType(confirmedArrayAtIndex, confirmedMapAtIndex, maxExplicitIndexFromSpec); + + // Only the computed children need to be sorted + computed.sort(computedKeysComparator); + + computed.trimToSize(); + + literalChildren = Collections.unmodifiableMap(literals); + computedChildren = Collections.unmodifiableList(computed); + + // extract generic execution strategy + executionStrategy = determineExecutionStrategy(); + + } + + @Override + @SuppressWarnings("unchecked") + public void applyElement(final String inputKey, Optional inputOptional, MatchedElement thisLevel, final WalkedPath walkedPath, final Map context) { + + Object input = inputOptional.get(); + // sanity checks, cannot work on a list spec with map input and vice versa, and runtime with null input + if (!specDataType.isCompatible(input)) { + return; + } + + // create input if it is null + if (input == null) { + input = specDataType.create(inputKey, walkedPath, opMode); + // if input has changed, wrap + if (input != null) { + inputOptional = Optional.of(input); + } + } + + // if input is List, create special ArrayMatchedElement, which tracks the original size of the input array + if (input instanceof List) { + // LIST means spec had array index explicitly specified, hence expand if needed + if (specDataType instanceof DataType.LIST) { + int origSize = specDataType.expand(input); + thisLevel = new ArrayMatchedElement(thisLevel.getRawKey(), origSize); + } else { + // specDataType is RUNTIME, so spec had no array index explicitly specified, no need to expand + thisLevel = new ArrayMatchedElement(thisLevel.getRawKey(), ((List) input).size()); + } + } + + // add self to walked path + walkedPath.add(input, thisLevel); + // Handle the rest of the children + executionStrategy.process(this, inputOptional, walkedPath, null, context); + // We are done, so remove ourselves from the walkedPath + walkedPath.removeLastElement(); + } + + @Override + public Map getLiteralChildren() { + return literalChildren; + } + + @Override + public List getComputedChildren() { + return computedChildren; + } + + @Override + public ExecutionStrategy determineExecutionStrategy() { + + if (computedChildren.isEmpty()) { + return ExecutionStrategy.ALL_LITERALS; + } else if (literalChildren.isEmpty()) { + return ExecutionStrategy.COMPUTED; + } else if (opMode.equals(OpMode.DEFINER) && specDataType instanceof DataType.LIST) { + return ExecutionStrategy.CONFLICT; + } else { + return ExecutionStrategy.ALL_LITERALS_WITH_COMPUTED; + } + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierLeafSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierLeafSpec.java new file mode 100644 index 00000000..96af92b6 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierLeafSpec.java @@ -0,0 +1,142 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.spec; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.SpecStringParser; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.modifier.OpMode; +import io.joltcommunity.jolt.modifier.ModifierSpecBuilder; +import io.joltcommunity.jolt.modifier.function.Function; +import io.joltcommunity.jolt.modifier.function.FunctionArg; +import io.joltcommunity.jolt.modifier.function.FunctionEvaluator; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("deprecated") +public class ModifierLeafSpec extends ModifierSpec { + + private final List functionEvaluatorList; + + @SuppressWarnings("unchecked") + public ModifierLeafSpec(final String rawJsonKey, Object rhsObj, final OpMode opMode, final Map functionsMap) { + super(rawJsonKey, opMode); + functionEvaluatorList = new LinkedList<>(); + + FunctionEvaluator functionEvaluator; + + // "key": "expression1" + if ((rhsObj instanceof String)) { + functionEvaluator = buildFunctionEvaluator((String) rhsObj, functionsMap); + functionEvaluatorList.add(functionEvaluator); + } + // "key": ["expression1", "expression2", "expression3"] + else if (rhsObj instanceof List rhsList && !rhsList.isEmpty()) { + for (Object rhs : rhsList) { + if (rhs instanceof String) { + functionEvaluator = buildFunctionEvaluator(rhs.toString(), functionsMap); + functionEvaluatorList.add(functionEvaluator); + } else { + functionEvaluator = FunctionEvaluator.forArgEvaluation(FunctionArg.forLiteral(rhs, false)); + functionEvaluatorList.add(functionEvaluator); + } + } + } + // "key": anyObjectOrLiteral --- just set as-is + else { + functionEvaluator = FunctionEvaluator.forArgEvaluation(FunctionArg.forLiteral(rhsObj, false)); + functionEvaluatorList.add(functionEvaluator); + } + } + + private static FunctionEvaluator buildFunctionEvaluator(final String rhs, final Map functionsMap) { + final FunctionEvaluator functionEvaluator; + // "key": "@0" --- evaluate expression then set + if (!rhs.startsWith(ModifierSpecBuilder.FUNCTION)) { + return FunctionEvaluator.forArgEvaluation(constructSingleArg(rhs, false)); + } else { + String functionName; + // "key": "=abs" --- call function with current value then set output if present + if (!rhs.contains("(") && !rhs.endsWith(")")) { + functionName = rhs.substring(ModifierSpecBuilder.FUNCTION.length()); + return FunctionEvaluator.forFunctionEvaluation(functionsMap.get(functionName)); + } + // "key": "=abs(@(1,&0))" --- evaluate expression then call function with + // expression-output, then set output if present + else { + String fnString = rhs.substring(ModifierSpecBuilder.FUNCTION.length()); + List fnArgs = SpecStringParser.parseFunctionArgs(fnString); + functionName = fnArgs.remove(0); + functionEvaluator = FunctionEvaluator.forFunctionEvaluation(functionsMap.get(functionName), constructArgs(fnArgs)); + } + } + return functionEvaluator; + } + + private static Optional getFirstAvailable(List functionEvaluatorList, Optional inputOptional, WalkedPath walkedPath, Map context) { + Optional valueOptional = Optional.empty(); + for (FunctionEvaluator functionEvaluator : functionEvaluatorList) { + try { + valueOptional = functionEvaluator.evaluate(inputOptional, walkedPath, context); + if (valueOptional.isPresent()) { + return valueOptional; + } + } catch (Exception ignored) { + } + } + return valueOptional; + } + + private static FunctionArg[] constructArgs(List argsList) { + FunctionArg[] argsArray = new FunctionArg[argsList.size()]; + for (int i = 0; i < argsList.size(); i++) { + String arg = argsList.get(i); + argsArray[i] = constructSingleArg(arg, true); + } + return argsArray; + } + + private static FunctionArg constructSingleArg(String arg, boolean forFunction) { + if (arg.startsWith(ModifierSpecBuilder.CARET)) { + return FunctionArg.forContext(TRAVERSAL_BUILDER.build(arg.substring(1))); + } else if (arg.startsWith(ModifierSpecBuilder.AT)) { + return FunctionArg.forSelf(TRAVERSAL_BUILDER.build(arg)); + } else { + return FunctionArg.forLiteral(arg, forFunction); + } + } + + @Override + public void applyElement(final String inputKey, final Optional inputOptional, final MatchedElement thisLevel, final WalkedPath walkedPath, final Map context) { + + Object parent = walkedPath.lastElement().getTreeRef(); + + walkedPath.add(inputOptional.get(), thisLevel); + + Optional valueOptional = getFirstAvailable(functionEvaluatorList, inputOptional, walkedPath, context); + + if (valueOptional.isPresent()) { + setData(parent, thisLevel, valueOptional.get(), opMode); + } + + walkedPath.removeLastElement(); + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierSpec.java new file mode 100644 index 00000000..09a3b0fe --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/modifier/spec/ModifierSpec.java @@ -0,0 +1,149 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.spec; + +import io.joltcommunity.jolt.SpecDriven; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.PathEvaluatingTraversal; +import io.joltcommunity.jolt.common.TransposeReader; +import io.joltcommunity.jolt.common.TraversalBuilder; +import io.joltcommunity.jolt.common.pathelement.ArrayPathElement; +import io.joltcommunity.jolt.common.pathelement.LiteralPathElement; +import io.joltcommunity.jolt.common.pathelement.MatchablePathElement; +import io.joltcommunity.jolt.common.pathelement.StarPathElement; +import io.joltcommunity.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.tree.ArrayMatchedElement; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.TransformException; +import io.joltcommunity.jolt.modifier.OpMode; + +import java.util.List; +import java.util.Map; + +import static io.joltcommunity.jolt.common.PathElementBuilder.buildMatchablePathElement; + + +/** + * Base Modifier spec + */ +public abstract class ModifierSpec implements BaseSpec { + + // traversal builder that uses a TransposeReader to create a PathEvaluatingTraversal + protected static final TraversalBuilder TRAVERSAL_BUILDER = new TraversalBuilder() { + @Override + @SuppressWarnings("unchecked") + public T buildFromPath(final String path) { + return (T) new TransposeReader(path); + } + }; + + protected final OpMode opMode; + protected final MatchablePathElement pathElement; + protected final boolean checkValue; + + /** + * Constructor for ModifierSpec. + * Builds the left-hand side (LHS) path element and validates it against the specification. + * + * @param rawJsonKey The raw JSON key to process. + * @param opMode The operation mode {@link OpMode} to use. + * @throws SpecException If the path element is invalid for the given operation mode. + */ + protected ModifierSpec(String rawJsonKey, OpMode opMode) { + String prefix = rawJsonKey.substring(0, 1); + String suffix = rawJsonKey.length() > 1 ? rawJsonKey.substring(rawJsonKey.length() - 1) : null; + + if (OpMode.isValid(prefix)) { + this.opMode = OpMode.from(prefix); + rawJsonKey = rawJsonKey.substring(1); + } else { + this.opMode = opMode; + } + + if (suffix != null && suffix.equals("?") && !(rawJsonKey.endsWith("\\?"))) { + checkValue = true; + rawJsonKey = rawJsonKey.substring(0, rawJsonKey.length() - 1); + } else { + checkValue = false; + } + + this.pathElement = buildMatchablePathElement(rawJsonKey); + if (!(pathElement instanceof StarPathElement) && !(pathElement instanceof LiteralPathElement) && !(pathElement instanceof ArrayPathElement)) { + throw new SpecException(opMode.name() + " cannot have " + pathElement.getClass().getSimpleName() + " RHS"); + } + } + + /** + * Static utility method for facilitating writes on the input object. + * + * @param parent The source object (either a Map or List). + * @param matchedElement The current spec (leaf) element that was matched with the input. + * @param value The value to write. + * @param opMode The operation mode to determine if the write is applicable. + * @throws RuntimeException If the parent object is neither a Map nor a List. + */ + @SuppressWarnings("unchecked") + protected static void setData(Object parent, MatchedElement matchedElement, Object value, OpMode opMode) { + if (parent instanceof Map source) { + String key = matchedElement.getRawKey(); + if (opMode.isApplicable(source, key)) { + source.put(key, value); + } + } else if (parent instanceof List source && matchedElement instanceof ArrayMatchedElement) { + int origSize = ((ArrayMatchedElement) matchedElement).getOrigSize(); + int reqIndex = ((ArrayMatchedElement) matchedElement).getRawIndex(); + if (opMode.isApplicable(source, reqIndex, origSize)) { + source.set(reqIndex, value); + } + } else { + throw new RuntimeException("Should not come here!"); + } + } + + @Override + public MatchablePathElement getPathElement() { + return pathElement; + } + + @Override + public boolean apply(final String inputKey, final Optional inputOptional, final WalkedPath walkedPath, final Map output, final Map context) { + if (output != null) { + throw new TransformException("Expected a null output"); + } + + MatchedElement thisLevel = pathElement.match(inputKey, walkedPath); + if (thisLevel == null) { + return false; + } + + if (!checkValue) { // there was no trailing "?" so no check is necessary + applyElement(inputKey, inputOptional, thisLevel, walkedPath, context); + } else if (inputOptional.isPresent()) { + applyElement(inputKey, inputOptional, thisLevel, walkedPath, context); + } + return true; + } + + /** + * Modifier specific override that is used in BaseSpec#apply(...) + * The name is changed for easy identification during debugging + */ + protected abstract void applyElement(final String key, final Optional inputOptional, final MatchedElement thisLevel, final WalkedPath walkedPath, final Map context); +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/Removr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/Removr.java similarity index 57% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/Removr.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/removr/Removr.java index 7d0e8a7e..2dc0e9c1 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/Removr.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/Removr.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt.removr; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.removr.spec.RemovrCompositeSpec; +import io.joltcommunity.jolt.SpecDriven; +import io.joltcommunity.jolt.Transform; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.removr.spec.RemovrCompositeSpec; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.HashMap; import java.util.Map; @@ -28,7 +31,7 @@ * For comparison : * Shiftr walks the input data and asks its spec "Where should this go?" * Defaultr walks the spec and asks "Does this exist in the data? If not, add it." - * + *

* While, Removr walks the spec and asks "if this exists, remove it." *

* Example : Given input JSON like @@ -68,16 +71,16 @@ * } * } * - * - * * Removr Wildcards - * + *

+ * * Removr Wildcards + *

* '*' Wildcard - * Valid only on the LHS ( input JSON keys ) side of a Removr Spec - * The '*' wildcard can be used by itself or to match part of a key. - * - * '*' wildcard by itself : - * To remove "all" keys under an input, use the * by itself on the LHS. - *

+ * Valid only on the LHS ( input JSON keys ) side of a Removr Spec
+ * The '*' wildcard can be used by itself or to match part of a key.
+ * 

+ * '*' wildcard by itself : + * To remove "all" keys under an input, use the * by itself on the LHS. + *

  *    // example input
  *    {
  *     "ratings":{
@@ -112,14 +115,14 @@
  *      },
  *    }
  *    
- * In this example, "Set1" and "Set2" under rating both have the same structure, and thus we can use the '*' - * to allow use to write more compact rules to remove "b" from all children under ratings. This is especially useful when we don't know - * how many children will be under ratings, but we would like to nuke certain part of it across. - * - * '*' wildcard as part of a key : - * This is useful for working with input JSON with keys that are "prefixed". - * Ex : if you had an input document like - *
+ * In this example, "Set1" and "Set2" under rating both have the same structure, and thus we can use the '*'
+ * to allow us to write more compact rules to remove "b" from all children under ratings. This is especially useful when we don't know
+ * how many children will be under ratings, but we would like to remove certain parts of it altogether.
+ * 

+ * '*' wildcard as part of a key : + * This is useful for working with input JSON with keys that are "prefixed". + * Ex : if you had an input document like + *

  *        {
  *         "ratings_legacy":{
  *              "Set1":{
@@ -144,11 +147,11 @@
  *          }
  *       }
  *    
- * - * A 'rating_*' would match both keys. As in Shiftr wildcard matching, * wildcard is as non greedy as possible, which enable us to give more than one * in key. - * - * For an ouput that removed Set1 from all ratings_* key, the spec would be, - *
+ * 

+ * A 'rating_*' would match both keys. As in Shiftr wildcard matching, * wildcard is as non greedy as possible, which enable us to give more than one * in key. + *

+ * For an ouput that removed Set1 from all ratings_* key, the spec would be, + *

  *        {
  *         "ratings_*":{
  *              "Set1":""
@@ -157,44 +160,43 @@
  * 

* *

- * * Arrays - * + * * Arrays + *

* Removr can also handle data in Arrays. - * - * It can walk thru all the elements of an array with the "*" wildcard. - * - * Additionally, it can remove individual array indicies. To do this the LHS key - * must be a number but in String format. - * - * Example - *

+ * 

+ * It can walk thru all the elements of an array with the "*" wildcard. + *

+ * Additionally, it can remove individual array indices. To do this the LHS key + * must be a number but in String format. + *

+ * Example + *

  *  "spec": {
  *    "array": {
  *      "0" : ""
  *    }
  *  }
  *  
- * - * In this case, Removr will remove the zero-th item from the input "array", which will cause data at - * index "1" to become the new "0". Because of this, Remover matches all the literal/explicit - * indices first, sorts them from Biggest to Smallest, then does the removing. + *

+ * In this case, Removr will remove the zero-th item from the input "array", which will cause data at + * index "1" to become the new "0". Because of this, Remover matches all the literal/explicit + * indices first, sorts them from Biggest to Smallest, then does the removing. *

*/ public class Removr implements SpecDriven, Transform { - private static final String ROOT_KEY = "root"; private final RemovrCompositeSpec rootSpec; @Inject - public Removr( Object spec ) { - if ( spec == null ){ - throw new SpecException( "Removr expected a spec of Map type, got 'null'." ); + public Removr(Object spec) { + if (spec == null) { + throw new SpecException("Removr expected a spec of Map type, got 'null'."); } - if ( ! ( spec instanceof Map ) ) { - throw new SpecException( "Removr expected a spec of Map type, got " + spec.getClass().getSimpleName() ); + if (!(spec instanceof Map)) { + throw new SpecException("Removr expected a spec of Map type, got " + spec.getClass().getSimpleName()); } - rootSpec = new RemovrCompositeSpec( ROOT_KEY, (Map) spec ); + rootSpec = new RemovrCompositeSpec(ROOT_KEY, (Map) spec); } /** @@ -203,12 +205,12 @@ public Removr( Object spec ) { * @param input the JSON object to transform in plain vanilla Jackson Map style */ @Override - public Object transform( Object input ) { + public Object transform(Object input) { // Wrap the input in a map to fool the CompositeSpec to recurse itself. - Map wrappedMap = new HashMap<>(); + Map wrappedMap = new HashMap<>(); wrappedMap.put(ROOT_KEY, input); - rootSpec.applyToMap( wrappedMap ); + rootSpec.applyToMap(wrappedMap); return input; } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrCompositeSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrCompositeSpec.java new file mode 100644 index 00000000..fc5c87a6 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrCompositeSpec.java @@ -0,0 +1,161 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.removr.spec; + +import io.joltcommunity.jolt.common.pathelement.LiteralPathElement; +import io.joltcommunity.jolt.common.pathelement.StarAllPathElement; +import io.joltcommunity.jolt.common.pathelement.StarPathElement; +import io.joltcommunity.jolt.exception.SpecException; + +import java.util.*; + +/** + * Removr Spec that has children. In a removr spec, whenever the RHS is a Map, we build a RemovrCompositeSpec + *

+ * Sample Spec:
+ *
+ *     "spec": {
+ *         "ineedtoberemoved":"" //literal leaf element
+ *         "TAG-*$*": "",       //Leaf Computed element
+ *         "TAG-*#*": "",
+ *
+ *         "*pants*" : "",
+ *
+ *          "buckets": {     //composite literal Path element
+ *             "a$*": ""    //Computed Leaf element
+ *          },
+ *          "rating*":{    //composite computed path element
+ *             "*":{       //composite computed path element
+ *                 "a":""  //literal leaf element
+ *             }
+ *         }
+ *     }
+ *  
+ */ +public class RemovrCompositeSpec extends RemovrSpec { + + private final List allChildNodes; + + public RemovrCompositeSpec(String rawKey, Map spec) { + super(rawKey); + List all = new ArrayList<>(); + + for (String rawLhsStr : spec.keySet()) { + Object rawRhs = spec.get(rawLhsStr); + String[] keyStrings = rawLhsStr.split("\\|"); + for (String keyString : keyStrings) { + RemovrSpec childSpec; + if (rawRhs instanceof Map) { + childSpec = new RemovrCompositeSpec(keyString, (Map) rawRhs); + } else if (rawRhs instanceof String && ((String) rawRhs).trim().isEmpty()) { + childSpec = new RemovrLeafSpec(keyString); + } else { + throw new SpecException("Invalid Removr spec RHS. Should be an empty string or Map"); + } + all.add(childSpec); + } + } + allChildNodes = Collections.unmodifiableList(all); + } + + @Override + public List applyToMap(Map inputMap) { + + if (pathElement instanceof LiteralPathElement) { + Object subInput = inputMap.get(pathElement.getRawKey()); + processChildren(allChildNodes, subInput); + } else if (pathElement instanceof StarPathElement star) { + + // Compare my pathElement with each key from the input. + // If it matches, recursively call process the child nodes. + for (Map.Entry entry : inputMap.entrySet()) { + + if (star.stringMatch(entry.getKey())) { + processChildren(allChildNodes, entry.getValue()); + } + } + } + + // Composite Nodes always return an empty list, as they dont actually remove anything. + return Collections.emptyList(); + } + + @Override + public List applyToList(List inputList) { + + // IF the input is a List, the only thing that will match is a Literal or a "*" + if (pathElement instanceof LiteralPathElement) { + + Integer pathElementInt = getNonNegativeIntegerFromLiteralPathElement(); + + if (pathElementInt != null && pathElementInt < inputList.size()) { + Object subObj = inputList.get(pathElementInt); + processChildren(allChildNodes, subObj); + } + } else if (pathElement instanceof StarAllPathElement) { + for (Object entry : inputList) { + processChildren(allChildNodes, entry); + } + } + + // Composite Nodes always return an empty list, as they dont actually remove anything. + return Collections.emptyList(); + } + + /** + * Call our child nodes, build up the set of keys or indices to actually remove, and then + * remove them. + */ + private void processChildren(List children, Object subInput) { + + if (subInput != null) { + + if (subInput instanceof List) { + + List subList = (List) subInput; + Set indicesToRemove = new HashSet<>(); + + // build a list of all indices to remove + for (RemovrSpec childSpec : children) { + indicesToRemove.addAll(childSpec.applyToList(subList)); + } + + List uniqueIndicesToRemove = new ArrayList<>(indicesToRemove); + // Sort the list from Biggest to Smallest, so that when we remove items from the input + // list we don't muck up the order. + // Aka removing 0 _then_ 3 would be bad, because we would have actually removed + // 0 and 4 from the "original" list. + uniqueIndicesToRemove.sort(Comparator.reverseOrder()); + + for (int index : uniqueIndicesToRemove) { + subList.remove(index); + } + } else if (subInput instanceof Map) { + + Map subInputMap = (Map) subInput; + + List keysToRemove = new LinkedList<>(); + + for (RemovrSpec childSpec : children) { + keysToRemove.addAll(childSpec.applyToMap(subInputMap)); + } + + keysToRemove.forEach(subInputMap.keySet()::remove); + } + } + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrLeafSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrLeafSpec.java similarity index 54% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrLeafSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrLeafSpec.java index 9d583274..654ccd8f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrLeafSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrLeafSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,57 +14,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.removr.spec; +package io.joltcommunity.jolt.removr.spec; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarPathElement; +import io.joltcommunity.jolt.common.pathelement.LiteralPathElement; +import io.joltcommunity.jolt.common.pathelement.StarAllPathElement; +import io.joltcommunity.jolt.common.pathelement.StarPathElement; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Spec for handling the leaf level of the Removr Transform. */ public class RemovrLeafSpec extends RemovrSpec { - public RemovrLeafSpec( String rawKey ) { - super( rawKey ); + public RemovrLeafSpec(String rawKey) { + super(rawKey); } /** * Build a list of keys to remove from the input map, using the pathElement - * from the Spec. + * from the Spec. * * @param inputMap : Input map from which the spec key needs to be removed. */ @Override - public List applyToMap( Map inputMap ) { - if ( inputMap == null ) { + public List applyToMap(Map inputMap) { + if (inputMap == null) { return null; } List keysToBeRemoved = new LinkedList<>(); - if ( pathElement instanceof LiteralPathElement ) { + if (pathElement instanceof LiteralPathElement) { // if we are a literal, check to see if we match - if ( inputMap.containsKey( pathElement.getRawKey() ) ) { - keysToBeRemoved.add( pathElement.getRawKey() ); + if (inputMap.containsKey(pathElement.getRawKey())) { + keysToBeRemoved.add(pathElement.getRawKey()); } - } - else if ( pathElement instanceof StarPathElement ) { - - StarPathElement star = (StarPathElement) pathElement; + } else if (pathElement instanceof StarPathElement star) { // if we are a wildcard, check each input key to see if it matches us - for( String key : inputMap.keySet() ) { + for (String key : inputMap.keySet()) { - if ( star.stringMatch( key ) ) { - keysToBeRemoved.add( key ); + if (star.stringMatch(key)) { + keysToBeRemoved.add(key); } } } @@ -75,27 +69,26 @@ else if ( pathElement instanceof StarPathElement ) { * @param inputList : Input List from which the spec key needs to be removed. */ @Override - public List applyToList( List inputList ) { - if ( inputList == null ) { + public List applyToList(List inputList) { + if (inputList == null) { return null; } - if ( pathElement instanceof LiteralPathElement ) { + if (pathElement instanceof LiteralPathElement) { Integer pathElementInt = getNonNegativeIntegerFromLiteralPathElement(); - if ( pathElementInt != null && pathElementInt < inputList.size() ) { - return Collections.singletonList( pathElementInt ); + if (pathElementInt != null && pathElementInt < inputList.size()) { + return Collections.singletonList(pathElementInt); } - } - else if ( pathElement instanceof StarAllPathElement ) { + } else if (pathElement instanceof StarAllPathElement) { // To be clear, this is kinda silly. // If you just wanted to remove the whole list, you could have just // directly removed it, instead of stepping into it and using the "*". - List toReturn = new ArrayList<>( inputList.size() ); - for( int index = 0; index < inputList.size(); index++ ) { - toReturn.add( index ); + List toReturn = new ArrayList<>(inputList.size()); + for (int index = 0; index < inputList.size(); index++) { + toReturn.add(index); } return toReturn; diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrSpec.java similarity index 56% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrSpec.java index 8f0d968e..8268c3a9 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/removr/spec/RemovrSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/removr/spec/RemovrSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.removr.spec; +package io.joltcommunity.jolt.removr.spec; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.MatchablePathElement; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarDoublePathElement; -import com.bazaarvoice.jolt.common.pathelement.StarRegexPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarSinglePathElement; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.utils.StringTools; +import io.joltcommunity.jolt.common.pathelement.*; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.utils.StringTools; import java.util.List; import java.util.Map; @@ -32,23 +27,17 @@ public abstract class RemovrSpec { protected final MatchablePathElement pathElement; - public RemovrSpec(String rawJsonKey) { - PathElement pathElement = parse( rawJsonKey ); - - if (!(pathElement instanceof MatchablePathElement)) { - throw new SpecException("Spec LHS key=" + rawJsonKey + " is not a valid LHS key."); - } - - this.pathElement = (MatchablePathElement) pathElement; + protected RemovrSpec(String rawJsonKey) { + this.pathElement = parse(rawJsonKey); } // Ex Keys : *, cdv-*, *-$de - public static PathElement parse(String key) { - if ( "*".equals( key ) ) { - return new StarAllPathElement( key ); + private static MatchablePathElement parse(String key) { + if ("*".equals(key)) { + return new StarAllPathElement(key); } - int numOfStars = StringTools.countMatches( key, "*" ); + int numOfStars = StringTools.countMatches(key, "*"); if (numOfStars == 1) { return new StarSinglePathElement(key); } else if (numOfStars == 2) { @@ -70,13 +59,12 @@ protected Integer getNonNegativeIntegerFromLiteralPathElement() { Integer pathElementInt = null; try { - pathElementInt = Integer.parseInt( pathElement.getRawKey() ); + pathElementInt = Integer.parseInt(pathElement.getRawKey()); - if ( pathElementInt < 0 ) { + if (pathElementInt < 0) { return null; } - } - catch( NumberFormatException nfe ) { + } catch (NumberFormatException nfe) { // If the data is an Array, but the spec keys are Non-Integer Strings, // we are annoyed, but we don't stop the whole transform. // Just this part of the Transform won't work. @@ -87,17 +75,17 @@ protected Integer getNonNegativeIntegerFromLiteralPathElement() { /** * Build a list of indices to remove from the input list, using the pathElement - * from the Spec. + * from the Spec. * * @return the indicies to remove, otherwise empty List. */ - public abstract List applyToList( List inputList ); + public abstract List applyToList(List inputList); /** * Build a list of keys to remove from the input map, using the pathElement - * from the Spec. + * from the Spec. * * @return the keys to remove, otherwise empty List. */ - public abstract List applyToMap( Map inputMap ); + public abstract List applyToMap(Map inputMap); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrSpecBuilder.java b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrSpecBuilder.java similarity index 56% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrSpecBuilder.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrSpecBuilder.java index a448936c..395152e7 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrSpecBuilder.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrSpecBuilder.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,24 +15,23 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.shiftr; +package io.joltcommunity.jolt.shiftr; -import com.bazaarvoice.jolt.common.spec.SpecBuilder; -import com.bazaarvoice.jolt.shiftr.spec.ShiftrCompositeSpec; -import com.bazaarvoice.jolt.shiftr.spec.ShiftrLeafSpec; -import com.bazaarvoice.jolt.shiftr.spec.ShiftrSpec; +import io.joltcommunity.jolt.common.spec.SpecBuilder; +import io.joltcommunity.jolt.shiftr.spec.ShiftrCompositeSpec; +import io.joltcommunity.jolt.shiftr.spec.ShiftrLeafSpec; +import io.joltcommunity.jolt.shiftr.spec.ShiftrSpec; import java.util.Map; public class ShiftrSpecBuilder extends SpecBuilder { - @SuppressWarnings( "unchecked" ) + @SuppressWarnings("unchecked") @Override - public ShiftrSpec createSpec( final String keyString, final Object rawRhs ) { - if( rawRhs instanceof Map ) { - return new ShiftrCompositeSpec(keyString, (Map) rawRhs ); - } - else { - return new ShiftrLeafSpec(keyString, rawRhs ); + public ShiftrSpec createSpec(final String keyString, final Object rawRhs) { + if (rawRhs instanceof Map) { + return new ShiftrCompositeSpec(keyString, (Map) rawRhs); + } else { + return new ShiftrLeafSpec(keyString, rawRhs); } } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrTraversr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrTraversr.java new file mode 100644 index 00000000..0d463a5a --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrTraversr.java @@ -0,0 +1,67 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.shiftr; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.SimpleTraversr; +import io.joltcommunity.jolt.traversr.traversal.TraversalStep; + +import java.util.ArrayList; +import java.util.List; + +/** + * Traverser that does not overwrite data. + */ +public class ShiftrTraversr extends SimpleTraversr { + + public ShiftrTraversr(String humanPath) { + super(humanPath); + } + + public ShiftrTraversr(List paths) { + super(paths); + } + + /** + * Do a Shift style insert : + * 1) if there is no data "there", then just set it + * 2) if there is already a list "there", just add the data to the list + * 3) if there something other than a list there, grab it and stuff it and the data into a list + * and overwrite what is there with a list. + */ + public Optional handleFinalSet(TraversalStep traversalStep, Object tree, String key, DataType data) { + + Optional optSub = traversalStep.get(tree, key); + + if (!optSub.isPresent() || optSub.get() == null) { + // nothing is here so just set the data + traversalStep.overwriteSet(tree, key, data); + } else if (optSub.get() instanceof List) { + // there is a list here, so we just add to it + ((List) optSub.get()).add(data); + } else { + // take whatever is there and make it the first element in an Array + List temp = new ArrayList<>(); + temp.add(optSub.get()); + temp.add(data); + + traversalStep.overwriteSet(tree, key, temp); + } + + return Optional.of(data); + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrWriter.java similarity index 66% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrWriter.java index ea82e0dd..08fe6e46 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/ShiftrWriter.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/ShiftrWriter.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.shiftr; +package io.joltcommunity.jolt.shiftr; -import com.bazaarvoice.jolt.common.PathEvaluatingTraversal; -import com.bazaarvoice.jolt.traversr.Traversr; +import io.joltcommunity.jolt.common.PathEvaluatingTraversal; +import io.joltcommunity.jolt.traversr.Traversr; import java.util.List; @@ -26,12 +27,12 @@ */ public class ShiftrWriter extends PathEvaluatingTraversal { - public ShiftrWriter( String dotNotation ) { - super( dotNotation ); + public ShiftrWriter(String dotNotation) { + super(dotNotation); } @Override - protected Traversr createTraversr( List paths ) { - return new ShiftrTraversr( paths ); + protected Traversr createTraversr(List paths) { + return new ShiftrTraversr(paths); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrCompositeSpec.java similarity index 54% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrCompositeSpec.java index 2068082a..5770f814 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrCompositeSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrCompositeSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,36 +14,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.shiftr.spec; - -import com.bazaarvoice.jolt.common.ComputedKeysComparator; -import com.bazaarvoice.jolt.common.ExecutionStrategy; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.pathelement.AmpPathElement; -import com.bazaarvoice.jolt.common.pathelement.AtPathElement; -import com.bazaarvoice.jolt.common.pathelement.DollarPathElement; -import com.bazaarvoice.jolt.common.pathelement.HashPathElement; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarAllPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarDoublePathElement; -import com.bazaarvoice.jolt.common.pathelement.StarPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarRegexPathElement; -import com.bazaarvoice.jolt.common.pathelement.StarSinglePathElement; -import com.bazaarvoice.jolt.common.pathelement.TransposePathElement; -import com.bazaarvoice.jolt.common.spec.BaseSpec; -import com.bazaarvoice.jolt.common.spec.OrderedCompositeSpec; -import com.bazaarvoice.jolt.common.spec.SpecBuilder; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.shiftr.ShiftrSpecBuilder; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +package io.joltcommunity.jolt.shiftr.spec; + +import io.joltcommunity.jolt.common.ComputedKeysComparator; +import io.joltcommunity.jolt.common.ExecutionStrategy; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.pathelement.*; +import io.joltcommunity.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.spec.OrderedCompositeSpec; +import io.joltcommunity.jolt.common.spec.SpecBuilder; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.shiftr.ShiftrSpecBuilder; + +import java.util.*; /** * Spec that has children, which it builds and then manages during Transforms. @@ -75,12 +61,12 @@ public class ShiftrCompositeSpec extends ShiftrSpec implements OrderedCompositeS static { orderMap = new HashMap<>(); - orderMap.put( AmpPathElement.class, 1 ); - orderMap.put( StarRegexPathElement.class, 2 ); - orderMap.put( StarDoublePathElement.class, 3 ); - orderMap.put( StarSinglePathElement.class, 4 ); - orderMap.put( StarAllPathElement.class, 5 ); - computedKeysComparator = ComputedKeysComparator.fromOrder( orderMap ); + orderMap.put(AmpPathElement.class, 1); + orderMap.put(StarRegexPathElement.class, 2); + orderMap.put(StarDoublePathElement.class, 3); + orderMap.put(StarSinglePathElement.class, 4); + orderMap.put(StarAllPathElement.class, 5); + computedKeysComparator = ComputedKeysComparator.fromOrder(orderMap); specBuilder = new ShiftrSpecBuilder(); } @@ -90,52 +76,51 @@ public class ShiftrCompositeSpec extends ShiftrSpec implements OrderedCompositeS private final List computedChildren; // children that are regex matches against the input data private final ExecutionStrategy executionStrategy; - public ShiftrCompositeSpec(String rawKey, Map spec ) { - super( rawKey ); + public ShiftrCompositeSpec(String rawKey, Map spec) { + super(rawKey); ArrayList special = new ArrayList<>(); Map literals = new LinkedHashMap<>(); ArrayList computed = new ArrayList<>(); // self check - if ( pathElement instanceof AtPathElement ) { - throw new SpecException( "@ Shiftr key, can not have children." ); + if (pathElement instanceof AtPathElement) { + throw new SpecException("@ Shiftr key, can not have children."); } - if ( pathElement instanceof DollarPathElement ) { - throw new SpecException( "$ Shiftr key, can not have children." ); + if (pathElement instanceof DollarPathElement) { + throw new SpecException("$ Shiftr key, can not have children."); } - List children = specBuilder.createSpec( spec ); + List children = specBuilder.createSpec(spec); - if ( children.isEmpty() ) { - throw new SpecException( "Shift ShiftrSpec format error : ShiftrSpec line with empty {} as value is not valid." ); + if (children.isEmpty()) { + throw new SpecException("Shift ShiftrSpec format error : ShiftrSpec line with empty {} as value is not valid."); } - for ( ShiftrSpec child : children ) { - if ( child.pathElement instanceof LiteralPathElement ) { - literals.put( child.pathElement.getRawKey(), child ); + for (ShiftrSpec child : children) { + if (child.pathElement instanceof LiteralPathElement) { + literals.put(child.pathElement.getRawKey(), child); } // special is it is "@" or "$" - else if ( child.pathElement instanceof AtPathElement || - child.pathElement instanceof HashPathElement || - child.pathElement instanceof DollarPathElement || - child.pathElement instanceof TransposePathElement ) { - special.add( child ); - } - else { // star || (& with children) - computed.add( child ); + else if (child.pathElement instanceof AtPathElement || + child.pathElement instanceof HashPathElement || + child.pathElement instanceof DollarPathElement || + child.pathElement instanceof TransposePathElement) { + special.add(child); + } else { // star || (& with children) + computed.add(child); } } // Only the computed children need to be sorted - Collections.sort( computed, computedKeysComparator ); + computed.sort(computedKeysComparator); special.trimToSize(); computed.trimToSize(); - specialChildren = Collections.unmodifiableList( special ); - literalChildren = Collections.unmodifiableMap( literals ); - computedChildren = Collections.unmodifiableList( computed ); + specialChildren = Collections.unmodifiableList(special); + literalChildren = Collections.unmodifiableMap(literals); + computedChildren = Collections.unmodifiableList(computed); executionStrategy = determineExecutionStrategy(); } @@ -153,22 +138,19 @@ public List getComputedChildren() { @Override public ExecutionStrategy determineExecutionStrategy() { - if ( computedChildren.isEmpty() ) { + if (computedChildren.isEmpty()) { return ExecutionStrategy.AVAILABLE_LITERALS; - } - else if ( literalChildren.isEmpty() ) { + } else if (literalChildren.isEmpty()) { return ExecutionStrategy.COMPUTED; } - for ( BaseSpec computed : computedChildren ) { - if ( ! ( computed.getPathElement() instanceof StarPathElement ) ) { + for (BaseSpec computed : computedChildren) { + if (!(computed.getPathElement() instanceof StarPathElement starPathElement)) { return ExecutionStrategy.CONFLICT; } - StarPathElement starPathElement = (StarPathElement) computed.getPathElement(); - - for ( String literal : literalChildren.keySet() ) { - if ( starPathElement.stringMatch( literal ) ) { + for (String literal : literalChildren.keySet()) { + if (starPathElement.stringMatch(literal)) { return ExecutionStrategy.CONFLICT; } } @@ -179,50 +161,49 @@ else if ( literalChildren.isEmpty() ) { /** * If this Spec matches the inputKey, then perform one step in the Shiftr parallel treewalk. - * + *

* Step one level down the input "tree" by carefully handling the List/Map nature the input to - * get the "one level down" data. - * + * get the "one level down" data. + *

* Step one level down the Spec tree by carefully and efficiently applying our children to the - * "one level down" data. + * "one level down" data. * * @return true if this this spec "handles" the inputKey such that no sibling specs need to see it */ @Override - public boolean apply( String inputKey, Optional inputOptional, WalkedPath walkedPath, Map output, Map context ) - { - MatchedElement thisLevel = pathElement.match( inputKey, walkedPath ); - if ( thisLevel == null ) { + public boolean apply(String inputKey, Optional inputOptional, WalkedPath walkedPath, Map output, Map context) { + MatchedElement thisLevel = pathElement.match(inputKey, walkedPath); + if (thisLevel == null) { return false; } // If we are a TransposePathElement, try to swap the "input" with what we lookup from the Transpose - if ( pathElement instanceof TransposePathElement ) { + if (pathElement instanceof TransposePathElement) { TransposePathElement tpe = (TransposePathElement) this.pathElement; // Note the data found may not be a String, thus we have to call the special objectEvaluate // Optional, because the input data could have been a valid null. - Optional optional = tpe.objectEvaluate( walkedPath ); - if ( !optional.isPresent() ) { + Optional optional = tpe.objectEvaluate(walkedPath); + if (!optional.isPresent()) { return false; } inputOptional = optional; } // add ourselves to the path, so that our children can reference us - walkedPath.add( inputOptional.get(), thisLevel ); + walkedPath.add(inputOptional.get(), thisLevel); // Handle any special / key based children first, but don't have them block anything - for( ShiftrSpec subSpec : specialChildren ) { - subSpec.apply( inputKey, inputOptional, walkedPath, output, context ); + for (ShiftrSpec subSpec : specialChildren) { + subSpec.apply(inputKey, inputOptional, walkedPath, output, context); } // Handle the rest of the children - executionStrategy.process( this, inputOptional, walkedPath, output, context ); + executionStrategy.process(this, inputOptional, walkedPath, output, context); // We are done, so remove ourselves from the walkedPath - walkedPath.removeLast(); + walkedPath.removeLastElement(); // we matched so increment the matchCount of our parent walkedPath.lastElement().getMatchedElement().incrementHashCount(); diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrLeafSpec.java similarity index 53% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrLeafSpec.java index b882bf51..63633ec8 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrLeafSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrLeafSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,73 +14,66 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.shiftr.spec; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.PathEvaluatingTraversal; -import com.bazaarvoice.jolt.common.TraversalBuilder; -import com.bazaarvoice.jolt.common.pathelement.AtPathElement; -import com.bazaarvoice.jolt.common.pathelement.DollarPathElement; -import com.bazaarvoice.jolt.common.pathelement.HashPathElement; -import com.bazaarvoice.jolt.common.pathelement.TransposePathElement; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.shiftr.ShiftrWriter; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; +package io.joltcommunity.jolt.shiftr.spec; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.PathEvaluatingTraversal; +import io.joltcommunity.jolt.common.TraversalBuilder; +import io.joltcommunity.jolt.common.pathelement.AtPathElement; +import io.joltcommunity.jolt.common.pathelement.DollarPathElement; +import io.joltcommunity.jolt.common.pathelement.HashPathElement; +import io.joltcommunity.jolt.common.pathelement.TransposePathElement; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.shiftr.ShiftrWriter; + +import java.util.*; /** * Leaf level Spec object. - * + *

* If this Spec's PathElement matches the input (successful parallel tree walk) - * this Spec has the information needed to write the given data to the output object. + * this Spec has the information needed to write the given data to the output object. */ public class ShiftrLeafSpec extends ShiftrSpec { // traversal builder that uses a ShifterWriter to create a PathEvaluatingTraversal private static final TraversalBuilder TRAVERSAL_BUILDER = new TraversalBuilder() { @Override - @SuppressWarnings( "unchecked" ) - public T buildFromPath( final String path ) { - return (T) new ShiftrWriter( path ); + @SuppressWarnings("unchecked") + public T buildFromPath(final String path) { + return (T) new ShiftrWriter(path); } }; // List of the processed version of the "write specifications" private final List shiftrWriters; - public ShiftrLeafSpec( String rawKey, Object rhs ) { - super( rawKey ); + public ShiftrLeafSpec(String rawKey, Object rhs) { + super(rawKey); List writers; - if ( rhs instanceof String ) { + if (rhs instanceof String) { // leaf level so spec is an dot notation write path - writers = Arrays.asList( TRAVERSAL_BUILDER.build( rhs ) ); - } - else if ( rhs instanceof List ) { + writers = Arrays.asList(TRAVERSAL_BUILDER.build(rhs)); + } else if (rhs instanceof List) { // leaf level list // Spec : "foo": ["a", "b"] : Shift the value of "foo" to both "a" and "b" - @SuppressWarnings( "unchecked" ) + @SuppressWarnings("unchecked") List rhsList = (List) rhs; - writers = new ArrayList<>( rhsList.size() ); - for ( Object dotNotation : rhsList ) { - writers.add( TRAVERSAL_BUILDER.build( dotNotation ) ); + writers = new ArrayList<>(rhsList.size()); + for (Object dotNotation : rhsList) { + writers.add(TRAVERSAL_BUILDER.build(dotNotation)); } - } - else if ( rhs == null ) { + } else if (rhs == null) { // this means someone wanted to match something, but not send it anywhere. Basically like a removal. writers = Collections.emptyList(); - } - else { - throw new SpecException( "Invalid Shiftr spec RHS. Should be map, string, or array of strings. Spec in question : " + rhs ); + } else { + throw new SpecException("Invalid Shiftr spec RHS. Should be map, string, or array of strings. Spec in question : " + rhs); } - shiftrWriters = Collections.unmodifiableList( writers ); + shiftrWriters = Collections.unmodifiableList(writers); } /** @@ -88,43 +82,38 @@ else if ( rhs == null ) { * @return true if this this spec "handles" the inputkey such that no sibling specs need to see it */ @Override - public boolean apply( String inputKey, Optional inputOptional, WalkedPath walkedPath, Map output, Map context){ + public boolean apply(String inputKey, Optional inputOptional, WalkedPath walkedPath, Map output, Map context) { Object input = inputOptional.get(); - MatchedElement thisLevel = pathElement.match( inputKey, walkedPath ); - if ( thisLevel == null ) { + MatchedElement thisLevel = pathElement.match(inputKey, walkedPath); + if (thisLevel == null) { return false; } Object data; boolean realChild = false; // by default don't block further Shiftr matches - if ( this.pathElement instanceof DollarPathElement || - this.pathElement instanceof HashPathElement ) { + if (this.pathElement instanceof DollarPathElement || + this.pathElement instanceof HashPathElement) { // The data is already encoded in the thisLevel object created by the pathElement.match called above data = thisLevel.getCanonicalForm(); - } - else if ( this.pathElement instanceof AtPathElement ) { + } else if (this.pathElement instanceof AtPathElement) { // The data is our parent's data data = input; - } - else if ( this.pathElement instanceof TransposePathElement ) { + } else if (this.pathElement instanceof TransposePathElement tpe) { // We try to walk down the tree to find the value / data we want - TransposePathElement tpe = (TransposePathElement) this.pathElement; // Note the data found may not be a String, thus we have to call the special objectEvaluate - Optional evaledData = tpe.objectEvaluate( walkedPath ); - if ( evaledData.isPresent() ) { + Optional evaledData = tpe.objectEvaluate(walkedPath); + if (evaledData.isPresent()) { data = evaledData.get(); - } - else { + } else { // if we could not find the value we want looking down the tree, bail return false; } - } - else { + } else { // the data is the input data = input; // tell our parent that we matched and no further processing for this inputKey should be done @@ -132,16 +121,16 @@ else if ( this.pathElement instanceof TransposePathElement ) { } // Add our the LiteralPathElement for this level, so that write path References can use it as &(0,0) - walkedPath.add( input, thisLevel ); + walkedPath.add(input, thisLevel); // Write out the data - for ( PathEvaluatingTraversal outputPath : shiftrWriters ) { - outputPath.write( data, output, walkedPath ); + for (PathEvaluatingTraversal outputPath : shiftrWriters) { + outputPath.write(data, output, walkedPath); } - walkedPath.removeLast(); + walkedPath.removeLastElement(); - if ( realChild ) { + if (realChild) { // we were a "real" child, so increment the matchCount of our parent walkedPath.lastElement().getMatchedElement().incrementHashCount(); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrSpec.java similarity index 66% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrSpec.java index c12d2779..4804c6e0 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/shiftr/spec/ShiftrSpec.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/shiftr/spec/ShiftrSpec.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,24 +14,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.shiftr.spec; +package io.joltcommunity.jolt.shiftr.spec; -import com.bazaarvoice.jolt.common.PathElementBuilder; -import com.bazaarvoice.jolt.common.pathelement.MatchablePathElement; -import com.bazaarvoice.jolt.common.spec.BaseSpec; +import io.joltcommunity.jolt.common.PathElementBuilder; +import io.joltcommunity.jolt.common.pathelement.MatchablePathElement; +import io.joltcommunity.jolt.common.spec.BaseSpec; /** * A Spec Object represents a single line from the JSON Shiftr Spec. - * + *

* At a minimum a single Spec has : - * Raw LHS spec value - * Some kind of PathElement (based off that raw LHS value) - * + * Raw LHS spec value + * Some kind of PathElement (based off that raw LHS value) + *

* Additionally there are 2 distinct subclasses of the base Spec - * LeafSpec : where the RHS is a String or Array of Strings, that specify an write path for the data from this level in the tree - * CompositeSpec : where the RHS is a map of children Specs - * + * LeafSpec : where the RHS is a String or Array of Strings, that specify an write path for the data from this level in the tree + * CompositeSpec : where the RHS is a map of children Specs + *

* Mapping of JSON Shiftr Spec to Spec objects : + *

  * {
  *   rating-*" : {      // CompositeSpec with one child and a Star PathElement
  *     "&(1)" : {       // CompositeSpec with one child and a Reference PathElement
@@ -40,20 +42,21 @@
  *     }
  *   }
  * }
- *
+ * 
+ *

* The tree structure of formed by the CompositeSpecs is what is used during Shiftr transforms - * to do the parallel tree walk with the input data tree. - * + * to do the parallel tree walk with the input data tree. + *

* During the parallel tree walk a stack of data (a WalkedPath) is maintained, and used when - * a tree walk encounters an Outputting spec to evaluate the wildcards in the write DotNotationPath. + * a tree walk encounters an Outputting spec to evaluate the wildcards in the write DotNotationPath. */ public abstract class ShiftrSpec implements BaseSpec { // The processed key from the JSON config protected final MatchablePathElement pathElement; - public ShiftrSpec(String rawJsonKey) { - this.pathElement = PathElementBuilder.buildMatchablePathElement( rawJsonKey ); + protected ShiftrSpec(String rawJsonKey) { + this.pathElement = PathElementBuilder.buildMatchablePathElement(rawJsonKey); } @Override diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversal.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/SimpleTraversal.java similarity index 59% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversal.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/traversr/SimpleTraversal.java index 6e896596..4f540555 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversal.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/SimpleTraversal.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,63 +14,63 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.traversr; +package io.joltcommunity.jolt.traversr; -import com.bazaarvoice.jolt.common.Optional; +import io.joltcommunity.jolt.common.Optional; import java.util.Arrays; import java.util.List; /** * Utility class for use in custom Transforms. - * + *

* Allows a programmer to just provide a single "human readable path" - * that they will want to be able to execute against multiple trees of data. - * + * that they will want to be able to execute against multiple trees of data. + *

* Internally, parses the "human readable path" into a Traversr and a set of keys, - * so that the user only needs to call get/set with their input tree. - * + * so that the user only needs to call get/set with their input tree. + *

* Because the path is static, it is assumed that you will always be reading and writing - * objects of the same type to the tree, therefore this class can take a generic - * parameter "K" to reduce casting. + * objects of the same type to the tree, therefore this class can take a generic + * parameter "K" to reduce casting. */ public class SimpleTraversal { private final SimpleTraversr traversr; private final List keys; - /** - * Google Maps.newHashMap() trick to fill in generic type - */ - public static SimpleTraversal newTraversal(String humanReadablePath) { - return new SimpleTraversal<>( humanReadablePath ); - } + public SimpleTraversal(String humanReadablePath) { + traversr = new SimpleTraversr(humanReadablePath); - public SimpleTraversal( String humanReadablePath ) { - traversr = new SimpleTraversr( humanReadablePath ); - - String[] keysArray = humanReadablePath.split( "\\." ); + String[] keysArray = humanReadablePath.split("\\."); // extract the 3 from "[3]", but don't mess with "[]" - for ( int index = 0; index < keysArray.length; index++) { + for (int index = 0; index < keysArray.length; index++) { - String key = keysArray[ index ]; - if ( key.charAt( 0 ) == '[' && key.charAt( key.length() -1 ) == ']' ) { - if ( key.length() > 2 ) { - keysArray[index] = key.substring( 1, key.length() - 1 ); + String key = keysArray[index]; + if (key.charAt(0) == '[' && key.charAt(key.length() - 1) == ']') { + if (key.length() > 2) { + keysArray[index] = key.substring(1, key.length() - 1); } } } - keys = Arrays.asList( keysArray ); + keys = Arrays.asList(keysArray); + } + + /** + * Google Maps.newHashMap() trick to fill in generic type + */ + public static SimpleTraversal newTraversal(String humanReadablePath) { + return new SimpleTraversal<>(humanReadablePath); } /** * @param tree tree of Map and List JSON structure to navigate * @return the object you wanted, or null if the object or any step along the path to it were not there */ - public Optional get( Object tree ) { - return (Optional) traversr.get( tree, keys ); + public Optional get(Object tree) { + return (Optional) traversr.get(tree, keys); } /** @@ -77,15 +78,15 @@ public Optional get( Object tree ) { * @param data JSON style data object you want to set * @return returns the data object if successfully set, otherwise null if there was a problem walking the path */ - public Optional set( Object tree, DataType data ) { - return (Optional) traversr.set( tree, keys, data ); + public Optional set(Object tree, DataType data) { + return (Optional) traversr.set(tree, keys, data); } /** * @param tree tree of Map and List JSON structure to navigate * @return removes and returns the data object if it was able to successfully navigate to it and remove it. */ - public Optional remove( Object tree ) { - return traversr.remove( tree, keys ); + public Optional remove(Object tree) { + return traversr.remove(tree, keys); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/SimpleTraversr.java similarity index 55% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/traversr/SimpleTraversr.java index d603da48..6cfe95cb 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/SimpleTraversr.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/SimpleTraversr.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,51 +14,52 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.traversr; +package io.joltcommunity.jolt.traversr; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.traversal.TraversalStep; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.traversal.TraversalStep; import java.util.List; /** * Simple Traversr that - * + *

  * 1 Does overwrite sets at the leaf level
  * 2 Will create intermediate container objects only on SET operations
+ * 
*/ public class SimpleTraversr extends Traversr { - public SimpleTraversr( String humanPath ) { - super( humanPath ); + public SimpleTraversr(String humanPath) { + super(humanPath); } - public SimpleTraversr( List paths ) { - super( paths ); + public SimpleTraversr(List paths) { + super(paths); } @Override - public Optional handleFinalSet( TraversalStep traversalStep, Object tree, String key, DataType data ) { - return traversalStep.overwriteSet( tree, key, data ); + public Optional handleFinalSet(TraversalStep traversalStep, Object tree, String key, DataType data) { + return traversalStep.overwriteSet(tree, key, data); } /** * Only make a new instance of a container object for SET, if there is nothing "there". */ @Override - public Optional handleIntermediateGet( TraversalStep traversalStep, Object tree, String key, TraversalStep.Operation op ) { + public Optional handleIntermediateGet(TraversalStep traversalStep, Object tree, String key, TraversalStep.Operation op) { - Optional optSub = traversalStep.get( tree, key ); + Optional optSub = traversalStep.get(tree, key); Object sub = optSub.get(); - if ( sub == null && op == TraversalStep.Operation.SET ) { + if (sub == null && op == TraversalStep.Operation.SET) { // get our child to make the container object, so it will be happy with it sub = traversalStep.getChild().newContainer(); - traversalStep.overwriteSet( tree, key, sub ); + traversalStep.overwriteSet(tree, key, sub); } - return Optional.of( (DataType) sub ); + return Optional.of((DataType) sub); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/Traversr.java similarity index 53% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/traversr/Traversr.java index aa0e089e..3a17d37f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/Traversr.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/Traversr.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,47 +14,49 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.traversr; +package io.joltcommunity.jolt.traversr; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.traversal.ArrayTraversalStep; -import com.bazaarvoice.jolt.traversr.traversal.AutoExpandArrayTraversalStep; -import com.bazaarvoice.jolt.traversr.traversal.MapTraversalStep; -import com.bazaarvoice.jolt.traversr.traversal.TraversalStep; -import com.bazaarvoice.jolt.traversr.traversal.TraversalStep.Operation; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.traversal.ArrayTraversalStep; +import io.joltcommunity.jolt.traversr.traversal.AutoExpandArrayTraversalStep; +import io.joltcommunity.jolt.traversr.traversal.MapTraversalStep; +import io.joltcommunity.jolt.traversr.traversal.TraversalStep; +import io.joltcommunity.jolt.traversr.traversal.TraversalStep.Operation; import java.util.List; /** * Traversr allows you to walk JSON tree structures of data, and to GET and SET operations. - * + *

* Corner cases that arise during tree walk, are handled by subclasses. * Ex: If no data exists mid tree walk quit or insert a new container? - * Or if there is data but it is the wrong type : overwrite or skip? - * + * Or if there is data but it is the wrong type : overwrite or skip? + *

* Traversr analyzes the path path to be traversed and creates a "linked list" of Traversal objects. - * + *

* Then that list of Traversals can be used many times to write data into different JSON tree structures - * with different key values. - * + * with different key values. + *

* For example given a Shiftr output path of : "tuna[&1].bob.&3[]" some of the keys are known, - * "tuna" and "bob", but other keys will only be known later. - * + * "tuna" and "bob", but other keys will only be known later. + *

* However, the structure of the output path will not change, which means we can do some work before - * the keys are known. - * + * the keys are known. + *

* First the output path is turned into its canonical form : "tuna.[4].[&1].bob.&3.[]". * Then, a series of Traversals is created. - * tuna -> MapTraversal - * [&1] -> ArrayTraversal - * bob -> MapTraversal - * &3 -> MapTraversal - * [] -> AutoExpandArrayTraversal - * + *

+ * tuna -> MapTraversal
+ * [&1] -> ArrayTraversal
+ * bob  -> MapTraversal
+ * &3   -> MapTraversal
+ * []   -> AutoExpandArrayTraversal
+ * 
+ *

* Later, a list of keys can then be provided, such as - * [ "tuna", "2", "bob", "smith", "[]" ], and they can be quickly used without having to build or - * parse any more objects. - * + * [ "tuna", "2", "bob", "smith", "[]" ], and they can be quickly used without having to build or + * parse any more objects. + *

* The list of keys are all Strings, which ArrayTraversals will convert to Integers as needed. */ public abstract class Traversr { @@ -61,23 +64,23 @@ public abstract class Traversr { private final TraversalStep root; private final int traversalLength; - public Traversr ( String humanPath ) { + public Traversr(String humanPath) { - String intermediatePath = humanPath.replace( "[", ".[" ); + String intermediatePath = humanPath.replace("[", ".["); // given this replace and split strategy, we can end up with double dots, "..", which will generate an empty path element. // so remove any ".." ;) - intermediatePath = intermediatePath.replace( "..", "." ); + intermediatePath = intermediatePath.replace("..", "."); - if ( intermediatePath.charAt( 0 ) == '.') { + if (intermediatePath.charAt(0) == '.') { // if the path started with an array, aka "[0].tuna", remove the leading . - intermediatePath = intermediatePath.substring( 1 ); + intermediatePath = intermediatePath.substring(1); } - String[] paths = intermediatePath.split( "\\." ); + String[] paths = intermediatePath.split("\\."); TraversalStep rooty = null; - for ( int index = paths.length -1 ; index >= 0; index--) { - rooty = makePathElement( paths[index], rooty ); + for (int index = paths.length - 1; index >= 0; index--) { + rooty = makePathElement(paths[index], rooty); } traversalLength = paths.length; root = rooty; @@ -87,10 +90,10 @@ public Traversr ( String humanPath ) { * Constructor where we provide a known good set of pathElement Strings in a list. * Aka, no need to extract it from a "Human Readable" form. */ - public Traversr( List paths ) { + public Traversr(List paths) { TraversalStep rooty = null; - for ( int index = paths.size() -1 ; index >= 0; index--) { - rooty = makePathElement( paths.get(index), rooty ); + for (int index = paths.size() - 1; index >= 0; index--) { + rooty = makePathElement(paths.get(index), rooty); } traversalLength = paths.size(); root = rooty; @@ -98,29 +101,27 @@ public Traversr( List paths ) { private TraversalStep makePathElement(String path, TraversalStep child) { - if ( "[]".equals( path ) ) { - return new AutoExpandArrayTraversalStep( this, child ); - } - else if ( path.startsWith( "[" ) && path.endsWith( "]" ) ) { - return new ArrayTraversalStep( this, child ); - } - else { - return new MapTraversalStep( this, child ); + if ("[]".equals(path)) { + return new AutoExpandArrayTraversalStep(this, child); + } else if (path.startsWith("[") && path.endsWith("]")) { + return new ArrayTraversalStep(this, child); + } else { + return new MapTraversalStep(this, child); } } /** * Note : Calling this method MAY modify the tree object by adding new Maps and Lists as needed - * for the traversal. This is determined by the behavior of the implementations of the - * abstract methods of this class. + * for the traversal. This is determined by the behavior of the implementations of the + * abstract methods of this class. */ - public Optional get( Object tree, List keys ) { + public Optional get(Object tree, List keys) { - if ( keys.size() != traversalLength ) { - throw new TraversrException( "Traversal Path and number of keys mismatch, traversalLength:" + traversalLength + " numKeys:" + keys.size() ); + if (keys.size() != traversalLength) { + throw new TraversrException("Traversal Path and number of keys mismatch, traversalLength:" + traversalLength + " numKeys:" + keys.size()); } - return root.traverse( tree, TraversalStep.Operation.GET, keys.iterator(), null ); + return root.traverse(tree, TraversalStep.Operation.GET, keys.iterator(), null); } /** @@ -128,10 +129,10 @@ public Optional get( Object tree, List keys ) { * @param data JSON style data object you want to set * @return returns the data object if successfully set, otherwise null if there was a problem walking the path */ - public Optional set( Object tree, List keys, DataType data ) { + public Optional set(Object tree, List keys, DataType data) { - if ( keys.size() != traversalLength ) { - throw new TraversrException( "Traversal Path and number of keys mismatch, traversalLength:" + traversalLength + " numKeys:" + keys.size() ); + if (keys.size() != traversalLength) { + throw new TraversrException("Traversal Path and number of keys mismatch, traversalLength:" + traversalLength + " numKeys:" + keys.size()); } /* @@ -141,52 +142,53 @@ public Optional set( Object tree, List keys, DataType data ) { The problem is that, we have no way to return our newly created top level container. All we return is a reference to the data, if we were successful in our set. */ - if ( tree == null ) { + if (tree == null) { return Optional.empty(); } - return root.traverse( tree, TraversalStep.Operation.SET, keys.iterator(), data ); + return root.traverse(tree, TraversalStep.Operation.SET, keys.iterator(), data); } /** * Note : Calling this method MAY modify the tree object by adding new Maps and Lists as needed - * for the traversal. This is determined by the behavior of the implementations of the - * abstract methods of this class. + * for the traversal. This is determined by the behavior of the implementations of the + * abstract methods of this class. */ - public Optional remove( Object tree, List keys ) { + public Optional remove(Object tree, List keys) { - if ( keys.size() != traversalLength ) { - throw new TraversrException( "Traversal Path and number of keys mismatch, traversalLength:" + traversalLength + " numKeys:" + keys.size() ); + if (keys.size() != traversalLength) { + throw new TraversrException("Traversal Path and number of keys mismatch, traversalLength:" + traversalLength + " numKeys:" + keys.size()); } - if ( tree == null ) { + if (tree == null) { return Optional.empty(); } - return root.traverse( tree, TraversalStep.Operation.REMOVE, keys.iterator(), null ); + return root.traverse(tree, TraversalStep.Operation.REMOVE, keys.iterator(), null); } // TODO extract these methods to an interface, and then sublasses of Traverser like ShiftrTraversr can do the // Swing style "I implement the interface and pass myself down" trick. // Means we can still can have a ShiftrTraversr, but less of a an explicit dependency inversion going // on between the Traversr and its Traversals. + /** * Allow subclasses to control how "sets" are done, if/once the traversal has made it to the the last element. - * + *

* Overwrite existing data? List-ize existing data with new data? * * @return the data object if the set was successful, or null if not */ - public abstract Optional handleFinalSet( TraversalStep traversalStep, Object tree, String key, DataType data ); + public abstract Optional handleFinalSet(TraversalStep traversalStep, Object tree, String key, DataType data); /** * Allow subclasses to control how gets are handled for intermediate traversals. - * + *

* Example: we are a MapTraversal and out key is "foo". - * We simply do a 'tree.get( "foo" )'. However, if we get a null back, or we get back - * a data type incompatible with our child Traversal, what do we do? - * + * We simply do a 'tree.get( "foo" )'. However, if we get a null back, or we get back + * a data type incompatible with our child Traversal, what do we do? + *

* Overwrite or just return? */ - public abstract Optional handleIntermediateGet( TraversalStep traversalStep, Object tree, String key, Operation op ); + public abstract Optional handleIntermediateGet(TraversalStep traversalStep, Object tree, String key, Operation op); } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/TraversrException.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/TraversrException.java similarity index 66% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/TraversrException.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/traversr/TraversrException.java index cfa0d2a3..9b2a83d1 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/TraversrException.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/TraversrException.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +14,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.traversr; +package io.joltcommunity.jolt.traversr; -public class TraversrException extends RuntimeException{ +public class TraversrException extends RuntimeException { - public TraversrException( String message ) { + public TraversrException(String message) { super(message); } - public TraversrException( String message, Exception e) { - super( message, e ); + public TraversrException(String message, Exception e) { + super(message, e); } } diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/ArrayTraversalStep.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/ArrayTraversalStep.java new file mode 100644 index 00000000..4f987ce4 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/ArrayTraversalStep.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.traversr.traversal; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.Traversr; + +import java.util.ArrayList; +import java.util.List; + +/** + * TraversalStep that expects to handle List objects. + */ +public class ArrayTraversalStep extends BaseTraversalStep, DataType> { + + public ArrayTraversalStep(Traversr traversr, TraversalStep child) { + super(traversr, child); + } + + private static void ensureArraySize(List list, Integer upperIndex) { + for (int sizing = list.size(); sizing <= upperIndex; sizing++) { + list.add(null); + } + } + + public Class getStepType() { + return List.class; + } + + public List newContainer() { + return new ArrayList<>(); + } + + @Override + public Optional get(List list, String key) { + + int arrayIndex = Integer.parseInt(key); + if (arrayIndex < list.size()) { + return Optional.of((DataType) list.get(arrayIndex)); + } + + return Optional.empty(); + } + + @Override + public Optional remove(List list, String key) { + + int arrayIndex = Integer.parseInt(key); + if (arrayIndex < list.size()) { + return Optional.of((DataType) list.remove(arrayIndex)); + } + + return Optional.empty(); + } + + @Override + public Optional overwriteSet(List list, String key, DataType data) { + + int arrayIndex = Integer.parseInt(key); + ensureArraySize(list, arrayIndex); // make sure it is big enough + list.set(arrayIndex, data); + return Optional.of(data); + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/AutoExpandArrayTraversalStep.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/AutoExpandArrayTraversalStep.java new file mode 100644 index 00000000..fa5f0d3f --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/AutoExpandArrayTraversalStep.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.traversr.traversal; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.Traversr; +import io.joltcommunity.jolt.traversr.TraversrException; + +import java.util.List; + +/** + * Subclass of ArrayTraversalStep that does not care about array index numbers. + * Instead it will just do an array add on any set. + *

+ * Consequently, get and remove are rather meaningless. + *

+ * This exists, because we need a way in the human readable path, so say that we + * always want a list value. + *

+ * Example : "tuna.marlin.[]" + * We want the value of marlin to always be a list, and anytime we set data + * to marlin, it should just be added to the list. + */ +public class AutoExpandArrayTraversalStep extends ArrayTraversalStep { + + public AutoExpandArrayTraversalStep(Traversr traversr, TraversalStep child) { + super(traversr, child); + } + + @Override + public Optional get(List list, String key) { + + if (!"[]".equals(key)) { + throw new TraversrException("AutoExpandArrayTraversal expects a '[]' key. Was: " + key); + } + + return Optional.empty(); + } + + @Override + public Optional remove(List list, String key) { + + if (!"[]".equals(key)) { + throw new TraversrException("AutoExpandArrayTraversal expects a '[]' key. Was: " + key); + } + + return Optional.empty(); + } + + @Override + public Optional overwriteSet(List list, String key, DataType data) { + + if (!"[]".equals(key)) { + throw new TraversrException("AutoExpandArrayTraversal expects a '[]' key. Was: " + key); + } + + list.add(data); + return Optional.of(data); + } +} diff --git a/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/BaseTraversalStep.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/BaseTraversalStep.java new file mode 100644 index 00000000..45867a79 --- /dev/null +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/BaseTraversalStep.java @@ -0,0 +1,69 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.traversr.traversal; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.Traversr; + +import java.util.Iterator; + + +public abstract class BaseTraversalStep implements TraversalStep { + + protected final TraversalStep child; + protected final Traversr traversr; + + public BaseTraversalStep(Traversr traversr, TraversalStep child) { + this.traversr = traversr; + this.child = child; + } + + public TraversalStep getChild() { + return child; + } + + public final Optional traverse(StepType tree, Operation op, Iterator keys, DataType data) { + + if (tree == null) { + return Optional.empty(); + } + + if (getStepType().isAssignableFrom(tree.getClass())) { + + String key = keys.next(); + + if (child == null) { + // End of the Traversal so do the set or get + return switch (op) { + case GET -> this.get(tree, key); + case SET -> (Optional) traversr.handleFinalSet(this, tree, key, data); + case REMOVE -> this.remove(tree, key); + }; + } else { + + // We just an intermediate step, so traverse and then hand over control to our child + Optional optSub = traversr.handleIntermediateGet(this, tree, key, op); + + if (optSub.isPresent()) { + return child.traverse(optSub.get(), op, keys, data); + } + } + } + + return Optional.empty(); + } +} diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/MapTraversalStep.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/MapTraversalStep.java similarity index 59% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/MapTraversalStep.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/MapTraversalStep.java index 1f64ce9d..44f5f06f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/MapTraversalStep.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/MapTraversalStep.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.traversr.traversal; +package io.joltcommunity.jolt.traversr.traversal; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.Traversr; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.Traversr; import java.util.LinkedHashMap; import java.util.Map; @@ -24,43 +25,43 @@ /** * TraversalStep that expects to handle Map objects. */ -public class MapTraversalStep extends BaseTraversalStep, DataType> { +public class MapTraversalStep extends BaseTraversalStep, DataType> { - public MapTraversalStep( Traversr traversr, TraversalStep child ) { - super( traversr, child ); + public MapTraversalStep(Traversr traversr, TraversalStep child) { + super(traversr, child); } public Class getStepType() { return Map.class; } - public Map newContainer() { + public Map newContainer() { return new LinkedHashMap<>(); } @Override @SuppressWarnings("unchecked") - public Optional get( Map map, String key ) { + public Optional get(Map map, String key) { // This here was the whole point of adding the Optional stuff. // Aka, I need a way to distinguish between the key not existing in the map // or the key existing but having a _valid_ null value. - if ( ! map.containsKey( key ) ) { + if (!map.containsKey(key)) { return Optional.empty(); } - return Optional.of( (DataType) map.get( key ) ); + return Optional.of((DataType) map.get(key)); } @Override @SuppressWarnings("unchecked") - public Optional remove( Map map, String key ) { - return Optional.of( (DataType) map.remove( key ) ); + public Optional remove(Map map, String key) { + return Optional.of((DataType) map.remove(key)); } @Override - public Optional overwriteSet( Map map, String key, DataType data ) { - map.put( key, data ); - return Optional.of( data ); + public Optional overwriteSet(Map map, String key, DataType data) { + map.put(key, data); + return Optional.of(data); } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/TraversalStep.java b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/TraversalStep.java similarity index 74% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/TraversalStep.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/TraversalStep.java index fce8ab05..7f109b7f 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/traversr/traversal/TraversalStep.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/traversr/traversal/TraversalStep.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.traversr.traversal; +package io.joltcommunity.jolt.traversr.traversal; -import com.bazaarvoice.jolt.common.Optional; +import io.joltcommunity.jolt.common.Optional; import java.util.Iterator; @@ -24,31 +25,26 @@ */ public interface TraversalStep { - /** - * The three things you can do with a Traversal. - */ - public enum Operation { SET, GET, REMOVE } - /** * Return the data for the key from the provided tree object. * * @return data object if available, or null. */ - public Optional get( StepType tree, String key ); + public Optional get(StepType tree, String key); /** * Remove and return the data for the key from the provided tree object. * * @return data object if available, or null. */ - public Optional remove( StepType tree, String key ); + public Optional remove(StepType tree, String key); /** * Insert the data into the tree, overwriting any data that is there. * * @return returns the data object if successful or null if it could not */ - public Optional overwriteSet( StepType tree, String key, DataType data ); + public Optional overwriteSet(StepType tree, String key, DataType data); /** * @return the child Traversal or null if this Traversal has no child @@ -64,7 +60,7 @@ public enum Operation { SET, GET, REMOVE } /** * Return the Class of the Generic T, so that it can be used in an - * "instanceof" style check. + * "instanceof" style check. * * @return Class that matches Generic parameter T */ @@ -72,15 +68,20 @@ public enum Operation { SET, GET, REMOVE } /** * The meat of the Traversal. - * + *

* Pull a key from the iterator, use it to make the traversal, and then - * call traverse on your child Traversal. + * call traverse on your child Traversal. * * @param tree tree of data to walk - * @param op the Operation to perform is this is the last node of the Traversal + * @param op the Operation to perform is this is the last node of the Traversal * @param keys keys to use * @param data the data to place if the operation is SET * @return if SET, null for fail or the "data" object for ok. if GET, PANTS */ - public Optional traverse( StepType tree, Operation op, Iterator keys, DataType data ); + public Optional traverse(StepType tree, Operation op, Iterator keys, DataType data); + + /** + * The three things you can do with a Traversal. + */ + public enum Operation {SET, GET, REMOVE} } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/utils/JoltUtils.java b/jolt-core/src/main/java/io/joltcommunity/jolt/utils/JoltUtils.java similarity index 52% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/utils/JoltUtils.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/utils/JoltUtils.java index 1eb0811b..2d0f8469 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/utils/JoltUtils.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/utils/JoltUtils.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.utils; +package io.joltcommunity.jolt.utils; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Handy utilities that do NOT depend on JsonUtil / Jackson live here @@ -35,28 +31,28 @@ public class JoltUtils { * (contents changed by this call) * @param keyToRemove the key to remove from the document */ - public static void removeRecursive( Object json, String keyToRemove ) { - if ( ( json == null ) || ( keyToRemove == null ) ) { + public static void removeRecursive(Object json, String keyToRemove) { + if ((json == null) || (keyToRemove == null)) { return; } - if ( json instanceof Map ) { + if (json instanceof Map) { Map jsonMap = cast(json); // If this level of the tree has the key we are looking for, remove it // Do the lookup instead of just the remove to avoid un-necessarily // dying on ImmutableMaps. - if ( jsonMap.containsKey( keyToRemove ) ) { - jsonMap.remove( keyToRemove ); + if (jsonMap.containsKey(keyToRemove)) { + jsonMap.remove(keyToRemove); } // regardless, recurse down the tree - for ( Object value : jsonMap.values() ) { - removeRecursive( value, keyToRemove ); + for (Object value : jsonMap.values()) { + removeRecursive(value, keyToRemove); } } - if ( json instanceof List ) { - for ( Object value : (List) json ) { - removeRecursive( value, keyToRemove ); + if (json instanceof List) { + for (Object value : (List) json) { + removeRecursive(value, keyToRemove); } } } @@ -64,61 +60,58 @@ public static void removeRecursive( Object json, String keyToRemove ) { /** * Navigate a JSON tree (made up of Maps and Lists) to "lookup" the value - * at a particular path. - * + * at a particular path. + *

* Example : given Json - * + *

* Object json = * { - * "a" : { - * "b" : [ "x", "y", "z" ] - * } + * "a" : { + * "b" : [ "x", "y", "z" ] * } - * + * } + *

* navigate( json, "a", "b", 0 ) will return "x". - * + *

* It will traverse down the nested "a" and return the zeroth item of the "b" array. - * + *

* You will either get your data, or null. - * + *

* It should never throw an Exception; even if - * - you ask to index an array with a negative number - * - you ask to index an array wiht a number bigger than the array size - * - you ask to index a map that does not exist - * - your input data has objects in it other than Map, List, String, Number. + * - you ask to index an array with a negative number + * - you ask to index an array wiht a number bigger than the array size + * - you ask to index a map that does not exist + * - your input data has objects in it other than Map, List, String, Number. * * @param source the source JSON object (Map, List, String, Number) - * @param paths varargs path you want to travel + * @param paths varargs path you want to travel * @return the object of Type at final destination */ - public static T navigate( final Object source, final Object... paths ) { + public static T navigate(final Object source, final Object... paths) { Object destination = source; - for ( Object path : paths ) { + for (Object path : paths) { - if ( path == null || destination == null ) { + if (path == null || destination == null) { return null; } - if ( destination instanceof Map ) { - destination = ((Map) destination).get( path ); - } - else if ( destination instanceof List ) { + if (destination instanceof Map) { + destination = ((Map) destination).get(path); + } else if (destination instanceof List destList) { - if ( ! (path instanceof Integer) ) { + if (!(path instanceof Integer)) { return null; } - List destList = (List) destination; int pathInt = (Integer) path; - if ( pathInt < 0 || pathInt >= destList.size() ) { + if (pathInt < 0 || pathInt >= destList.size()) { return null; } - destination = destList.get( pathInt ); - } - else { + destination = destList.get(pathInt); + } else { // the input at this level is not a Map or List // so return null return null; @@ -129,57 +122,52 @@ else if ( destination instanceof List ) { /** * Navigate a JSON tree (made up of Maps and Lists) to "lookup" the value - * at a particular path. - * + * at a particular path. + *

* You will either get your data, or an exception will be thrown. - * + *

* This method should generally only be used in situations where you "know" - * that the navigate call will "always succeed". + * that the navigate call will "always succeed". * * @param source the source JSON object (Map, List, String, Number) - * @param paths varargs path you want to travel + * @param paths varargs path you want to travel * @return the object of Type at final destination * @throws UnsupportedOperationException if there was any problem walking the JSON tree structure */ - public static T navigateStrict( final Object source, final Object... paths ) throws UnsupportedOperationException { + public static T navigateStrict(final Object source, final Object... paths) throws UnsupportedOperationException { Object destination = source; - for ( Object path : paths ) { - if ( path == null ) { + for (Object path : paths) { + if (path == null) { throw new UnsupportedOperationException("path is null"); } - if ( destination == null ) { + if (destination == null) { throw new UnsupportedOperationException("source is null"); } - if ( destination instanceof Map ) { - Map temp = (Map) destination; - if (temp.containsKey( path ) ) { + if (destination instanceof Map temp) { + if (temp.containsKey(path)) { // if we don't check for containsKey first, then the Map.get call // would return null for keys that don't actually exist. - destination = ((Map) destination).get(path); - } - else { - throw new UnsupportedOperationException("no entry for '" + path + "' found while traversing the JSON"); + destination = temp.get(path); + } else { + throw new UnsupportedOperationException("no entry for '" + path + "' found while traversing the JSON"); } - } - else if ( destination instanceof List ) { + } else if (destination instanceof List destList) { - if ( ! (path instanceof Integer) ) { - throw new UnsupportedOperationException( "path '" + path + "' is trying to be used as an array index"); + if (!(path instanceof Integer)) { + throw new UnsupportedOperationException("path '" + path + "' is trying to be used as an array index"); } - List destList = (List) destination; int pathInt = (Integer) path; - if ( pathInt < 0 || pathInt > destList.size() ) { - throw new UnsupportedOperationException( "path '" + path + "' is negative or outside the range of the list"); + if (pathInt < 0 || pathInt > destList.size()) { + throw new UnsupportedOperationException("path '" + path + "' is negative or outside the range of the list"); } - destination = destList.get( pathInt ); - } - else { + destination = destList.get(pathInt); + } else { throw new UnsupportedOperationException("Navigation supports only Map and List source types and non-null String and Integer path types"); } } @@ -188,42 +176,36 @@ else if ( destination instanceof List ) { /** * Navigate a JSON tree (made up of Maps and Lists) to "lookup" the value - * at a particular path, but will return the supplied default value if - * there are any problems. + * at a particular path, but will return the supplied default value if + * there are any problems. * * @param source the source JSON object (Map, List, String, Number) - * @param paths varargs path you want to travel + * @param paths varargs path you want to travel * @return the object of Type at final destination or defaultValue if non existent */ - public static T navigateOrDefault( final T defaultValue, final Object source, final Object... paths ) { + public static T navigateOrDefault(final T defaultValue, final Object source, final Object... paths) { Object destination = source; - for ( Object path : paths ) { - if(path == null || destination == null) { + for (Object path : paths) { + if (path == null || destination == null) { return defaultValue; } - if(destination instanceof Map) { - Map destinationMap = (Map) destination; - if(!destinationMap.containsKey(path)) { + if (destination instanceof Map destinationMap) { + if (!destinationMap.containsKey(path)) { return defaultValue; - } - else { + } else { destination = destinationMap.get(path); } - } - else if(path instanceof Integer && destination instanceof List) { + } else if (path instanceof Integer && destination instanceof List destList) { - List destList = (List) destination; int pathInt = (Integer) path; - if ( pathInt < 0 || pathInt >= destList.size() ) { + if (pathInt < 0 || pathInt >= destList.size()) { return defaultValue; + } else { + destination = destList.get(pathInt); } - else { - destination = destList.get( pathInt ); - } - } - else { + } else { return defaultValue; } } @@ -235,15 +217,14 @@ else if(path instanceof Integer && destination instanceof List) { */ @Deprecated public static T navigateSafe(final T defaultValue, final Object source, final Object... paths) { - return navigateOrDefault( defaultValue, source, paths ); + return navigateOrDefault(defaultValue, source, paths); } - /** * Vacant implies there are empty placeholders, i.e. a vacant hotel * Given a json document, checks if it has any "leaf" values, can handle deep nesting of lists and maps - * + *

* i.e. { "a": [ "x": {}, "y": [] ], "b": { "p": [], "q": {} }} ==> is empty * * @param obj source @@ -251,27 +232,27 @@ public static T navigateSafe(final T defaultValue, final Object source, fina */ public static boolean isVacantJson(final Object obj) { Collection values = null; - if(obj instanceof Collection) { - if(((Collection) obj).size() == 0) { + if (obj instanceof Collection) { + if (((Collection) obj).size() == 0) { return true; } values = (Collection) obj; } - if(obj instanceof Map) { - if(((Map) obj).size() == 0) { + if (obj instanceof Map) { + if (((Map) obj).size() == 0) { return true; } values = ((Map) obj).values(); } int processedEmpty = 0; - if(values != null) { - for (Object value: values) { - if(!isVacantJson(value)) { + if (values != null) { + for (Object value : values) { + if (!isVacantJson(value)) { return false; } processedEmpty++; } - if(processedEmpty == values.size()) { + if (processedEmpty == values.size()) { return true; } } @@ -288,11 +269,11 @@ public static boolean isBlankJson(final Object obj) { if (obj == null) { return true; } - if(obj instanceof Collection) { - return (((Collection) obj).size() == 0); + if (obj instanceof Collection) { + return (((Collection) obj).size() == 0); } - if(obj instanceof Map) { - return (((Map) obj).size() == 0); + if (obj instanceof Map) { + return (((Map) obj).size() == 0); } throw new UnsupportedOperationException("map or list is supported, got ${obj?obj.getClass():null}"); } @@ -300,9 +281,9 @@ public static boolean isBlankJson(final Object obj) { /** * Given a json document, finds out absolute path to every leaf element - * + *

* i.e. { "a": [ "x": { "y": "alpha" }], "b": { "p": [ "beta", "gamma" ], "q": {} }} will yield - * + *

* 1) "a",0,"x","y" -> to "alpha" * 2) "b","p", 0 -> to "beta" * 3) "b", "p", 1 -> to "gamma" @@ -315,19 +296,15 @@ public static List listKeyChains(final Object source) { List keyChainList = new LinkedList<>(); - if(source instanceof Map) { - Map sourceMap = (Map) source; - for (Object key: sourceMap.keySet()) { + if (source instanceof Map sourceMap) { + for (Object key : sourceMap.keySet()) { keyChainList.addAll(listKeyChains(key, sourceMap.get(key))); } - } - else if(source instanceof List) { - List sourceList = (List) source; - for(int i=0; i listKeyChains(final Object key, final Object value) { List keyChainList = new LinkedList<>(); List childKeyChainList = listKeyChains(value); - if(childKeyChainList.size() > 0) { - for(Object[] childKeyChain: childKeyChainList) { + if (childKeyChainList.size() > 0) { + for (Object[] childKeyChain : childKeyChainList) { Object[] keyChain = new Object[childKeyChain.length + 1]; keyChain[0] = key; System.arraycopy(childKeyChain, 0, keyChain, 1, childKeyChain.length); keyChainList.add(keyChain); } - } - else { - keyChainList.add(new Object[] {key}); + } else { + keyChainList.add(new Object[]{key}); } return keyChainList; } @@ -368,18 +344,16 @@ public static List listKeyChains(final Object key, final Object value) */ public static String toSimpleTraversrPath(Object[] paths) { StringBuilder pathBuilder = new StringBuilder(); - for(int i=0; i T cast(Object object) { */ @SuppressWarnings("unchecked") public static E[] cast(Object[] object) { - return (E[])(object); + return (E[]) (object); } /** * Given a 'fluffy' json document, it recursively removes all null elements * to compact the json document - * + *

* Warning: mutates the doc, destroys array order * * @param source @@ -425,25 +399,23 @@ public static Object compactJson(Object source) { for (Object item : (List) source) { if (item instanceof List) { compactJson(item); - } - else if (item instanceof Map) { + } else if (item instanceof Map) { compactJson(item); } } - ((List) source).removeAll(Collections.singleton(null)); - } - else if (source instanceof Map) { + ((List) source).removeAll(Collections.singleton(null)); + } else if (source instanceof Map) { List keysToRemove = new LinkedList(); for (Object key : ((Map) source).keySet()) { - Object value = ((Map)source).get(key); + Object value = ((Map) source).get(key); if (value instanceof List) { - if (((List) value).size() == 0) + if (((List) value).size() == 0) keysToRemove.add(key); else { compactJson(value); } } else if (value instanceof Map) { - if (((Map) value).size() == 0) { + if (((Map) value).size() == 0) { keysToRemove.add(key); } else { compactJson(value); @@ -452,12 +424,11 @@ else if (source instanceof Map) { keysToRemove.add(key); } } - for(Object key: keysToRemove) { - ((Map) source).remove(key); + for (Object key : keysToRemove) { + ((Map) source).remove(key); } - } - else { - throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" ); + } else { + throw new UnsupportedOperationException("Only Map/String and List/Integer types are supported"); } return source; @@ -467,128 +438,116 @@ else if (source instanceof Map) { * For a given non-null (json) object, save the valve in the nested path provided * * @param source the source json object - * @param value the value to store - * @param paths var args Object path to navigate down and store the object in + * @param value the value to store + * @param paths var args Object path to navigate down and store the object in * @return previously stored value if available, null otherwise */ - @SuppressWarnings( "unchecked" ) - public static T store( Object source, T value, Object... paths ) { + @SuppressWarnings("unchecked") + public static T store(Object source, T value, Object... paths) { int destKeyIndex = paths.length - 1; - if(destKeyIndex < 0) { - throw new IllegalArgumentException( "No path information provided" ); + if (destKeyIndex < 0) { + throw new IllegalArgumentException("No path information provided"); } - if(source == null) { - throw new NullPointerException( "source cannot be null" ); + if (source == null) { + throw new NullPointerException("source cannot be null"); } - for ( int i = 0; i < destKeyIndex; i++ ) { + for (int i = 0; i < destKeyIndex; i++) { Object currentPath = paths[i]; - Object nextPath = paths[i+1]; - source = getOrCreateNextObject( source, currentPath, nextPath ); + Object nextPath = paths[i + 1]; + source = getOrCreateNextObject(source, currentPath, nextPath); } Object path = paths[destKeyIndex]; - if(source instanceof Map && path instanceof String) { - return cast( ( (Map) source ).put( path, value ) ); - } - else if(source instanceof List && path instanceof Integer) { - ensureListAvailability( (List) source, (int) path ); - return cast( ( (List) source ).set( (int) path, value ) ); - } - else { - throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" ); + if (source instanceof Map && path instanceof String) { + return cast(((Map) source).put(path, value)); + } else if (source instanceof List && path instanceof Integer) { + ensureListAvailability((List) source, (int) path); + return cast(((List) source).set((int) path, value)); + } else { + throw new UnsupportedOperationException("Only Map/String and List/Integer types are supported"); } } /** * For a given non-null (json) object, removes and returns the value in the nested path provided - * + *

* Warning: changes array order, to maintain order, use store(source, null, path ...) instead * * @param source the source json object - * @param paths var args Object path to navigate down and remove + * @param paths var args Object path to navigate down and remove * @return existing value if available, null otherwise */ - @SuppressWarnings( "unchecked" ) - public static T remove( Object source, Object... paths ) { + @SuppressWarnings("unchecked") + public static T remove(Object source, Object... paths) { int destKeyIndex = paths.length - 1; - if(destKeyIndex < 0) { - throw new IllegalArgumentException( "No path information provided" ); + if (destKeyIndex < 0) { + throw new IllegalArgumentException("No path information provided"); } - if(source == null) { - throw new NullPointerException( "source cannot be null" ); + if (source == null) { + throw new NullPointerException("source cannot be null"); } - for ( int i = 0; i < destKeyIndex; i++ ) { + for (int i = 0; i < destKeyIndex; i++) { Object currentPath = paths[i]; - Object nextPath = paths[i+1]; - source = getOrCreateNextObject( source, currentPath, nextPath ); + Object nextPath = paths[i + 1]; + source = getOrCreateNextObject(source, currentPath, nextPath); } Object path = paths[destKeyIndex]; - if(source instanceof Map && path instanceof String) { - return cast( ( (Map) source ).remove( path ) ); - } - else if(source instanceof List && path instanceof Integer) { - ensureListAvailability( (List) source, (int) path ); - return cast( ( (List) source ).remove( (int) path) ); - } - else { - throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" ); + if (source instanceof Map && path instanceof String) { + return cast(((Map) source).remove(path)); + } else if (source instanceof List && path instanceof Integer) { + ensureListAvailability((List) source, (int) path); + return cast(((List) source).remove((int) path)); + } else { + throw new UnsupportedOperationException("Only Map/String and List/Integer types are supported"); } } - @SuppressWarnings( "unchecked" ) - private static void ensureListAvailability( List source, int index ) { - for ( int i = source.size(); i <= index; i++ ) { - source.add( i, null ); + @SuppressWarnings("unchecked") + private static void ensureListAvailability(List source, int index) { + for (int i = source.size(); i <= index; i++) { + source.add(i, null); } } - @SuppressWarnings( "unchecked" ) - private static Object getOrCreateNextObject( Object source, Object key, Object nextKey ) { + @SuppressWarnings("unchecked") + private static Object getOrCreateNextObject(Object source, Object key, Object nextKey) { Object value; - if ( source instanceof Map && key instanceof String ) { - if ( ( value = ( (Map) source ).get( key ) ) == null ) { + if (source instanceof Map && key instanceof String) { + if ((value = ((Map) source).get(key)) == null) { Object newValue; - if ( nextKey instanceof String ) { - newValue = new HashMap(); - } - else if ( nextKey instanceof Integer ) { - newValue = new LinkedList(); - } - else { - throw new UnsupportedOperationException( "Only String and Integer types are supported" ); + if (nextKey instanceof String) { + newValue = new HashMap<>(); + } else if (nextKey instanceof Integer) { + newValue = new LinkedList<>(); + } else { + throw new UnsupportedOperationException("Only String and Integer types are supported"); } - ( (Map) source ).put( key, newValue ); + ((Map) source).put(key, newValue); value = newValue; } - } - else if ( source instanceof List && key instanceof Integer ) { - ensureListAvailability( ( (List) source ), (int) key ); - if ( ( value = ( (List) source ).get( (int) key ) ) == null ) { + } else if (source instanceof List && key instanceof Integer) { + ensureListAvailability(((List) source), (int) key); + if ((value = ((List) source).get((int) key)) == null) { Object newValue; - if ( nextKey instanceof String ) { - newValue = new HashMap(); - } - else if ( nextKey instanceof Integer ) { - newValue = new LinkedList(); - } - else { - throw new UnsupportedOperationException( "Only String and Integer types are supported" ); + if (nextKey instanceof String) { + newValue = new HashMap<>(); + } else if (nextKey instanceof Integer) { + newValue = new LinkedList<>(); + } else { + throw new UnsupportedOperationException("Only String and Integer types are supported"); } - ( (List) source ).set( (int) key, newValue ); + ((List) source).set((int) key, newValue); value = newValue; } - } - else if(source == null || key == null) { - throw new NullPointerException( "source and/or key cannot be null" ); - } - else { - throw new UnsupportedOperationException( "Only Map and List types are supported" ); + } else if (source == null || key == null) { + throw new NullPointerException("source and/or key cannot be null"); + } else { + throw new UnsupportedOperationException("Only Map and List types are supported"); } - if ( ( nextKey instanceof String && value instanceof Map ) || ( nextKey instanceof Integer && value instanceof List ) ) { + if ((nextKey instanceof String && value instanceof Map) || (nextKey instanceof Integer && value instanceof List)) { return value; - } - else { - throw new UnsupportedOperationException( "Only Map/String and List/Integer types are supported" ); + } else { + throw new UnsupportedOperationException("Only Map/String and List/Integer types are supported"); } } } diff --git a/jolt-core/src/main/java/com/bazaarvoice/jolt/utils/StringTools.java b/jolt-core/src/main/java/io/joltcommunity/jolt/utils/StringTools.java similarity index 80% rename from jolt-core/src/main/java/com/bazaarvoice/jolt/utils/StringTools.java rename to jolt-core/src/main/java/io/joltcommunity/jolt/utils/StringTools.java index c647ef8d..967488f0 100644 --- a/jolt-core/src/main/java/com/bazaarvoice/jolt/utils/StringTools.java +++ b/jolt-core/src/main/java/io/joltcommunity/jolt/utils/StringTools.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +15,11 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.utils; +package io.joltcommunity.jolt.utils; /** - * * This class mimics the behavior of apache StringTools, except that it works on CharSequence instead of String - * + *

* Also, with this, jolt-core can finally be free of apache-common dependency */ public class StringTools { @@ -28,7 +28,7 @@ public class StringTools { * Count the num# of matches of subSequence in sourceSequence * * @param sourceSequence to find occurrence from - * @param subSequence to find occurrence of + * @param subSequence to find occurrence of * @return num of occurrences of subSequence in sourceSequence */ public static int countMatches(CharSequence sourceSequence, CharSequence subSequence) { @@ -40,18 +40,18 @@ public static int countMatches(CharSequence sourceSequence, CharSequence subSequ int sourceSequenceIndex = 0; int subSequenceIndex = 0; - while(sourceSequenceIndex < sourceSequence.length()) { - if(sourceSequence.charAt(sourceSequenceIndex) == subSequence.charAt(subSequenceIndex)) { + while (sourceSequenceIndex < sourceSequence.length()) { + if (sourceSequence.charAt(sourceSequenceIndex) == subSequence.charAt(subSequenceIndex)) { sourceSequenceIndex++; subSequenceIndex++; - while(sourceSequenceIndex < sourceSequence.length() && subSequenceIndex < subSequence.length()) { - if(sourceSequence.charAt(sourceSequenceIndex) != subSequence.charAt(subSequenceIndex)) { + while (sourceSequenceIndex < sourceSequence.length() && subSequenceIndex < subSequence.length()) { + if (sourceSequence.charAt(sourceSequenceIndex) != subSequence.charAt(subSequenceIndex)) { break; } sourceSequenceIndex++; subSequenceIndex++; } - if(subSequenceIndex == subSequence.length()) { + if (subSequenceIndex == subSequence.length()) { count++; } subSequenceIndex = 0; @@ -99,6 +99,6 @@ public static boolean isBlank(CharSequence sourceSequence) { * @return true if source is empty */ public static boolean isEmpty(CharSequence sourceSequence) { - return sourceSequence == null || sourceSequence.length() == 0; + return sourceSequence == null || sourceSequence.isEmpty(); } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java deleted file mode 100644 index 71b3d4d4..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrTest.java +++ /dev/null @@ -1,294 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.chainr.spec.ChainrEntry; -import com.bazaarvoice.jolt.chainr.transforms.ExplodingTestTransform; -import com.bazaarvoice.jolt.chainr.transforms.GoodTestTransform; -import com.bazaarvoice.jolt.chainr.transforms.TransformTestResult; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.exception.TransformException; -import com.google.common.collect.ImmutableList; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class ChainrTest { - - private List> newChainrSpec() { - return new ArrayList<>(); - } - - private Map newActivity( String opname ) { - Map activity = new HashMap<>(); - activity.put( ChainrEntry.OPERATION_KEY, opname ); - return activity; - } - - private Map newActivity( String operation, Object spec ) { - Map activity = new HashMap<>(); - activity.put( ChainrEntry.OPERATION_KEY, operation ); - if ( spec != null ) { - activity.put( ChainrEntry.SPEC_KEY, spec ); - } - return activity; - } - - private Map newCustomJavaActivity( Class cls, Object spec ) { - Map activity = new HashMap<>(); - activity.put( ChainrEntry.OPERATION_KEY, cls.getName() ); - if ( spec != null ) { - activity.put( ChainrEntry.SPEC_KEY, spec ); - } - - return activity; - } - - private List> newCustomJavaChainrSpec( Class cls, Object delegateSpec ) - { - List> retvalue = this.newChainrSpec(); - retvalue.add( newCustomJavaActivity( cls, delegateSpec ) ); - return retvalue; - } - - private List> newShiftrChainrSpec( Object shiftrSpec ) { - List> retvalue = this.newChainrSpec(); - retvalue.add( newActivity( "shift", shiftrSpec ) ); - return retvalue; - } - - private List> newShiftrDefaultrSpec( Object defaultrSpec ) { - List> retvalue = this.newChainrSpec(); - retvalue.add( newActivity( "default", defaultrSpec ) ); - return retvalue; - } - - private List> newShiftrRemovrSpec( Object removrSpec ) { - List> retvalue = this.newChainrSpec(); - retvalue.add( newActivity( "remove", removrSpec ) ); - return retvalue; - } - - private List> newShiftrSortrSpec( Object sortrSpec ) { - List> retvalue = this.newChainrSpec(); - retvalue.add( newActivity( "sort", sortrSpec ) ); - return retvalue; - } - - @Test - public void process_itCallsShiftr() throws IOException { - Map testUnit = JsonUtils.classpathToMap( "/json/shiftr/queryMappingXform.json" ); - - Object input = testUnit.get( "input" ); - Object shiftrSpec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Object chainrSpec = this.newShiftrChainrSpec( shiftrSpec ); - - Chainr unit = Chainr.fromSpec( chainrSpec ); - Object actual = unit.transform( input, null ); - - JoltTestUtil.runDiffy( "failed Shiftr call.", expected, actual ); - } - - @Test - public void process_itCallsDefaultr() throws IOException { - Map testUnit = JsonUtils.classpathToMap( "/json/defaultr/firstSample.json" ); - - Object input = testUnit.get( "input" ); - Object defaultrSpec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Object chainrSpec = this.newShiftrDefaultrSpec( defaultrSpec ); - - Chainr unit = Chainr.fromSpec( chainrSpec ); - Object actual = unit.transform( input, null ); - - JoltTestUtil.runDiffy( "failed Defaultr call.", expected, actual ); - } - - @Test - public void process_itCallsRemover() throws IOException { - Map testUnit = JsonUtils.classpathToMap( "/json/removr/firstSample.json" ); - - Object input = testUnit.get( "input" ); - Object removrSpec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Object chainrSpec = this.newShiftrRemovrSpec( removrSpec ); - - Chainr unit = Chainr.fromSpec( chainrSpec ); - Object actual = unit.transform( input, null ); - - JoltTestUtil.runDiffy( "failed Removr call.", expected, actual ); - } - - @Test - public void process_itCallsSortr() throws IOException { - Object input = JsonUtils.classpathToObject( "/json/sortr/simple/input.json" ); - Object expected = JsonUtils.classpathToObject( "/json/sortr/simple/output.json" ); - Object chainrSpec = this.newShiftrSortrSpec( null ); - - Chainr unit = Chainr.fromSpec( chainrSpec ); - Object actual = unit.transform( input, null ); - - JoltTestUtil.runDiffy( "failed Sortr call.", expected, actual ); - - String orderErrorMessage = SortrTest.verifyOrder( actual, expected ); - Assert.assertNull( orderErrorMessage, orderErrorMessage ); - } - - @Test - public void process_itCallsCustomJavaTransform() { - List> spec = this.newChainrSpec(); - Object delegateSpec = new HashMap(); - spec.add( this.newCustomJavaActivity( GoodTestTransform.class, delegateSpec ) ); - Object input = new Object(); - - Chainr unit = Chainr.fromSpec( spec ); - TransformTestResult actual = (TransformTestResult) unit.transform( input, null ); - - Assert.assertEquals( input, actual.input ); - Assert.assertEquals( delegateSpec, actual.spec ); - } - - @DataProvider - public Object[][] failureSpecCases() { - return new Object[][] { - { null }, - { "foo" }, - { this.newActivity( null ) }, - { this.newActivity( "pants" ) }, - }; - } - - @Test(dataProvider = "failureSpecCases", expectedExceptions = SpecException.class) - public void process_itBlowsUp_fromSpec(Object spec) { - Chainr.fromSpec( spec ); - Assert.fail("Should have failed during spec initialization."); - } - - @DataProvider - public Object[][] failureTransformCases() { - return new Object[][] { - { this.newCustomJavaChainrSpec( ExplodingTestTransform.class, null ) } - }; - } - - @Test(dataProvider = "failureTransformCases", expectedExceptions = TransformException.class) - public void process_itBlowsUp_fromTransform(Object spec) { - Chainr unit = Chainr.fromSpec( spec ); - unit.transform( new HashMap(), null ); - Assert.fail("Should have failed during transform."); - } - - - - - @DataProvider - public Object[][] getTestCaseNames() { - return new Object[][] { - {"andrewkcarter1", false}, - {"andrewkcarter2", false}, - {"firstSample", true}, - {"ismith", false}, - {"ritwickgupta", false}, - {"wolfermann1", false}, - {"wolfermann2", false}, - {"wolfermann2", false} - }; - } - - @Test(dataProvider = "getTestCaseNames") - public void runTestCases(String testCaseName, boolean sorted ) throws IOException { - String testPath = "/json/chainr/integration/" + testCaseName; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); - - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Chainr unit = Chainr.fromSpec( spec ); - - Assert.assertFalse( unit.hasContextualTransforms() ); - Assert.assertEquals( unit.getContextualTransforms().size(), 0 ); - - Object actual = unit.transform( input, null ); - - JoltTestUtil.runDiffy( "failed case " + testPath, expected, actual ); - - if ( sorted ) { - // Make sure the sort actually worked. - String orderErrorMessage = SortrTest.verifyOrder( actual, expected ); - Assert.assertNull( orderErrorMessage, orderErrorMessage ); - } - } - - - - - @Test - public void testReuseChainr() { - // Spec which moves "attributeMap"'s keys to a root "attributes" list. - Map specShift = JsonUtils.javason( - "{" + - "'operation':'shift'," + - "'spec' : { 'attributeMap' : { '*' : { '$' : 'attributes[#2]' } } }" + - "}" - ); - - List> chainrSpec = ImmutableList.of( specShift ); - - // Create a single Chainr from the spec - Chainr chainr = Chainr.fromSpec(chainrSpec); - - // Test input with three attributes - Map content = JsonUtils.javason( - "{ 'attributeMap' : { " + - "'attribute1' : 1, 'attribute2' : 2, 'attribute3' : 3 }" + - "}" - ); - - Object transformed = chainr.transform(content); - - // First time everything checks out - Assert.assertTrue( transformed instanceof Map ); - Map transformedMap = (Map) transformed; - Assert.assertEquals( transformedMap.get( "attributes" ), ImmutableList.of( "attribute1", "attribute2", "attribute3" ) ); - - // Create a new identical input - content = JsonUtils.javason( - "{ 'attributeMap' : { " + - "'attribute1' : 1, 'attribute2' : 2, 'attribute3' : 3 }" + - "}" - ); - - // Create a new transform from the same Chainr - transformed = chainr.transform(content); - - Assert.assertTrue( transformed instanceof Map ); - transformedMap = (Map) transformed; - // The following assert fails because attributes will have three leading null values: - // transformedMap["attributes"] == [null, null, null, "attribute1", "attribute2", "attribute3"] - Assert.assertEquals( transformedMap.get( "attributes" ), ImmutableList.of( "attribute1", "attribute2", "attribute3" ) ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/DefaultrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/DefaultrTest.java deleted file mode 100644 index 46a29265..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/DefaultrTest.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.exception.SpecException; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -public class DefaultrTest { - - @DataProvider - public Object[][] getDiffyTestCases() { - return new Object[][] { - {"arrayMismatch1"}, - {"arrayMismatch2"}, - {"defaultNulls"}, - {"expansionOnly"}, - {"firstSample"}, - {"identity"}, - {"nestedArrays1"}, - {"nestedArrays2"}, - {"orOrdering"}, - {"photosArray"}, - {"starsOfStars"}, - {"topLevelIsArray"}, - }; - } - - @Test(dataProvider = "getDiffyTestCases" ) - public void runDiffyTests( String testCaseName ) throws IOException { - - String testPath = "/json/defaultr/" + testCaseName; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); - - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Defaultr defaultr = new Defaultr(spec); - Object actual = defaultr.transform( input ); - - JoltTestUtil.runDiffy( "failed case " + testPath, expected, actual ); - } - - @Test - public void deepCopyTest() throws IOException { - Map testUnit = JsonUtils.classpathToMap( "/json/defaultr/__deepCopyTest.json" ); - - Object spec = testUnit.get( "spec" ); - - Defaultr defaultr = new Defaultr(spec); - { - Object input = testUnit.get( "input" ); - Map fiddle = (Map) defaultr.transform( input ); - - List array = (List) fiddle.get( "array" ); - array.add("a"); - - Map subMap = (Map) fiddle.get( "map" ); - subMap.put("c", "c"); - } - { - Map testUnit2 = JsonUtils.classpathToMap( "/json/defaultr/__deepCopyTest.json" ); - - Object input = testUnit2.get( "input" ); - Object expected = testUnit2.get( "expected" ); - - Object actual = defaultr.transform( input ); - JoltTestUtil.runDiffy( "Same spec deepcopy fail.", expected, actual ); - } - } - - @Test(expectedExceptions = SpecException.class) - public void throwExceptionOnBadSpec() throws IOException { - Object spec = JsonUtils.jsonToMap( "{ \"tuna*\": \"marlin\" }" ); - new Defaultr( spec ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java deleted file mode 100644 index abfdc0b4..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/JoltTestUtil.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import org.testng.Assert; - -import java.io.IOException; - -public class JoltTestUtil { - - private static final Diffy diffy = new Diffy(); - private static final Diffy arrayOrderObliviousDiffy = new ArrayOrderObliviousDiffy(); - - public static void runDiffy( String failureMessage, Object expected, Object actual ) throws IOException { - runDiffy( diffy, failureMessage, expected, actual ); - } - - public static void runDiffy( Object expected, Object actual ) throws IOException { - runDiffy( diffy, "Failed", expected, actual ); - } - - public static void runArrayOrderObliviousDiffy( String failureMessage, Object expected, Object actual ) throws IOException { - runDiffy( arrayOrderObliviousDiffy, failureMessage, expected, actual ); - } - - public static void runArrayOrderObliviousDiffy( Object expected, Object actual ) throws IOException { - runDiffy( arrayOrderObliviousDiffy, "Failed", expected, actual ); - } - - - private static void runDiffy( Diffy diffy, String failureMessage, Object expected, Object actual ) { - String actualObject = JsonUtils.toPrettyJsonString( actual ); - Diffy.Result result = diffy.diff( expected, actual ); - if (!result.isEmpty()) { - Assert.fail( "\nActual object\n" + actualObject + "\n" + failureMessage + "\nDiffy output\n" + result.toString()); - } - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/ModifierTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/ModifierTest.java deleted file mode 100644 index e07f0445..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/ModifierTest.java +++ /dev/null @@ -1,295 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.common.SpecStringParser; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.modifier.function.Function; -import com.google.common.collect.Lists; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -@SuppressWarnings( "deprecated" ) -public class ModifierTest { - - enum TemplatrTestCase { - OVERWRITR { - @Override - Modifier getTemplatr( final Object spec ) { - return new Modifier.Overwritr( spec ); - } - }, - DEFAULTR { - @Override - Modifier getTemplatr( final Object spec ) { - return new Modifier.Defaultr(spec); - } - }, - DEFINR { - @Override - Modifier getTemplatr( final Object spec ) { - return new Modifier.Definr( spec ); - } - }; - - abstract Modifier getTemplatr(Object spec); - } - - @BeforeClass - @SuppressWarnings( "unchecked" ) - public void setup() throws Exception { - // accessing built ins such that we can test a custom impl of function - // this is a special test case, and not a recommended approach of using function - Field f = Modifier.class.getDeclaredField("STOCK_FUNCTIONS"); - f.setAccessible( true ); - Map BUILT_INS = (Map) f.get( null ); - BUILT_INS.put( "minLabelComputation", new MinLabelComputation() ); - BUILT_INS.put( "maxLabelComputation", new MaxLabelComputation() ); - } - - @DataProvider - public Iterator getTestCases() { - List testCases = Lists.newLinkedList(); - - testCases.add( new Object[]{"/json/modifier/mapLiteral.json"} ); - testCases.add( new Object[]{"/json/modifier/mapLiteralWithNullInput.json"} ); - testCases.add( new Object[]{"/json/modifier/mapLiteralWithMissingInput.json"} ); - testCases.add( new Object[]{"/json/modifier/mapLiteralWithEmptyInput.json"} ); - - testCases.add( new Object[]{"/json/modifier/arrayElementAt.json"} ); - - testCases.add( new Object[]{"/json/modifier/arrayLiteral.json"} ); - testCases.add( new Object[]{"/json/modifier/arrayLiteralWithNullInput.json"} ); - testCases.add( new Object[]{"/json/modifier/arrayLiteralWithEmptyInput.json"} ); - testCases.add( new Object[]{"/json/modifier/arrayLiteralWithMissingInput.json"} ); - - testCases.add( new Object[]{"/json/modifier/simple.json"} ); - testCases.add( new Object[]{"/json/modifier/simpleArray.json"} ); - testCases.add( new Object[]{"/json/modifier/arrayObject.json"} ); - - testCases.add( new Object[]{"/json/modifier/simpleMapNullToArray.json"} ); - testCases.add( new Object[]{"/json/modifier/simpleMapRuntimeNull.json"} ); - - testCases.add( new Object[]{"/json/modifier/simpleLookup.json"} ); - testCases.add( new Object[]{"/json/modifier/complexLookup.json"} ); - - testCases.add( new Object[]{"/json/modifier/simpleArrayLookup.json"} ); - testCases.add( new Object[]{"/json/modifier/complexArrayLookup.json"} ); - - testCases.add( new Object[]{"/json/modifier/valueCheckSimpleArray.json"} ); - testCases.add( new Object[]{"/json/modifier/valueCheckSimpleArrayNullInput.json"} ); - testCases.add( new Object[]{"/json/modifier/valueCheckSimpleArrayEmptyInput.json"} ); - - testCases.add( new Object[]{"/json/modifier/valueCheckSimpleMap.json"} ); - testCases.add( new Object[]{"/json/modifier/valueCheckSimpleMapNullInput.json"} ); - testCases.add( new Object[]{"/json/modifier/valueCheckSimpleMapEmptyInput.json"} ); - - testCases.add( new Object[]{"/json/modifier/simpleMapOpOverride.json"} ); - testCases.add( new Object[]{"/json/modifier/simpleArrayOpOverride.json"} ); - - testCases.add( new Object[]{"/json/modifier/testListOfFunction.json"} ); - - return testCases.iterator(); - } - - @Test (dataProvider = "getTestCases") - public void testOverwritrTransform(String testFile) throws Exception { - doTest( testFile, TemplatrTestCase.OVERWRITR ); - } - - @Test (dataProvider = "getTestCases") - public void testDefaultrTransform(String testFile) throws Exception { - doTest( testFile, TemplatrTestCase.DEFAULTR ); - } - - @Test (dataProvider = "getTestCases") - public void testDefinrTransform(String testFile) throws Exception { - doTest( testFile, TemplatrTestCase.DEFINR ); - } - - public void doTest(String testFile, TemplatrTestCase testCase) throws Exception { - Map testUnit = JsonUtils.classpathToMap( testFile ); - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object context = testUnit.get( "context" ); - Object expected = testUnit.get( testCase.name() ); - if(expected != null) { - Modifier modifier = testCase.getTemplatr( spec ); - Object actual = modifier.transform( input, (Map) context ); - JoltTestUtil.runArrayOrderObliviousDiffy( testCase.name() + " failed case " + testFile, expected, actual ); - } - } - - @DataProvider - public Iterator getSpecValidationTestCases() { - List testCases = Lists.newLinkedList(); - List testObjects = JsonUtils.classpathToList( "/json/modifier/validation/specThatShouldFail.json" ); - - for(TemplatrTestCase testCase: TemplatrTestCase.values()) { - for(Object specObj: testObjects) { - testCases.add( new Object[] {testCase, specObj} ); - } - } - - return testCases.iterator(); - } - - @Test(expectedExceptions = SpecException.class, dataProvider = "getSpecValidationTestCases") - public void testInvalidSpecs(TemplatrTestCase testCase, Object spec) { - testCase.getTemplatr( spec ); - } - - @DataProvider - public Iterator getFunctionTests() { - List testCases = Lists.newLinkedList(); - - testCases.add( new Object[]{"/json/modifier/functions/stringsSplitTest.json", TemplatrTestCase.OVERWRITR}); - testCases.add( new Object[]{"/json/modifier/functions/padStringsTest.json", TemplatrTestCase.OVERWRITR}); - testCases.add( new Object[]{"/json/modifier/functions/stringsTests.json", TemplatrTestCase.OVERWRITR}); - testCases.add( new Object[]{"/json/modifier/functions/mathTests.json", TemplatrTestCase.OVERWRITR} ); - testCases.add( new Object[]{"/json/modifier/functions/arrayTests.json", TemplatrTestCase.OVERWRITR} ); - testCases.add( new Object[]{"/json/modifier/functions/sizeTests.json", TemplatrTestCase.OVERWRITR} ); - testCases.add( new Object[]{"/json/modifier/functions/labelsLookupTest.json", TemplatrTestCase.DEFAULTR} ); - testCases.add( new Object[]{"/json/modifier/functions/valueTests.json", TemplatrTestCase.OVERWRITR } ); - - return testCases.iterator(); - } - - - @Test (dataProvider = "getFunctionTests") - public void testFunctions(String testFile, TemplatrTestCase testCase) throws Exception { - doTest( testFile, testCase); - } - - @DataProvider - public Iterator getSquashTests() { - List testCases = Lists.newLinkedList(); - - testCases.add( new Object[]{"/json/modifier/functions/squashNullsTests.json"}); - testCases.add( new Object[]{"/json/modifier/functions/deleteDuplicatesTests.json"}); - - return testCases.iterator(); - } - - @Test (dataProvider = "getSquashTests") - public void doSquashNullsTest(String testFile) throws Exception { - TemplatrTestCase testCase = TemplatrTestCase.OVERWRITR; - Map testUnit = JsonUtils.classpathToMap( testFile ); - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object context = testUnit.get( "context" ); - Object expected = testUnit.get( testCase.name() ); - if(expected != null) { - Modifier modifier = testCase.getTemplatr( spec ); - Object actual = modifier.transform( input, (Map) context ); - JoltTestUtil.runDiffy( testCase.name() + " failed case " + testFile, expected, actual ); - } - } - - @DataProvider - public Iterator fnArgParseTestCases(){ - List testCases = Lists.newLinkedList(); - - testCases.add( new Object[] {"fn(abc,efg,pqr)", new String[] {"fn", "abc", "efg", "pqr"} } ); - testCases.add( new Object[] {"fn(abc,@(1,2),pqr)", new String[] {"fn", "abc", "@(1,2)", "pqr"} } ); - testCases.add( new Object[] {"fn(abc,efg,pqr,)", new String[] {"fn", "abc", "efg", "pqr", ""} } ); - testCases.add( new Object[] {"fn(abc,,@(1,,2),,pqr,,)", new String[] {"fn", "abc", "","@(1,,2)","", "pqr", "", ""} } ); - testCases.add( new Object[] {"fn(abc,'e,f,g',pqr)", new String[] {"fn", "abc", "'e,f,g'", "pqr"} } ); - testCases.add( new Object[] {"fn(abc,'e(,f,)g',pqr)", new String[] {"fn", "abc", "'e(,f,)g'", "pqr"} } ); - - return testCases.iterator(); - } - - @Test( dataProvider = "fnArgParseTestCases") - public void testFunctionArgParse(String argString, String[] expected) throws Exception { - List actual = SpecStringParser.parseFunctionArgs( argString ); - JoltTestUtil.runArrayOrderObliviousDiffy(" failed case " + argString, expected, actual ); - } - - @Test - public void testModifierFirstElementArray() throws IOException { - Map input = new HashMap() {{ - put("input", new Integer[]{5, 4}); - }}; - - Map spec = new HashMap() {{ - put("first", "=firstElement(@(1,input))"); - }}; - - Map expected = new HashMap() {{ - put("input", new Integer[]{5, 4}); - put("first", 5); - }}; - - Modifier modifier = new Modifier.Overwritr( spec ); - Object actual = modifier.transform( input, null ); - JoltTestUtil.runArrayOrderObliviousDiffy( "failed modifierFirstElementArray", expected, actual ); - } - - @SuppressWarnings( "unused" ) - public static final class MinLabelComputation implements Function { - @Override - @SuppressWarnings( "unchecked" ) - public Optional apply( final Object... args ) { - Map valueLabels = (Map) args[0]; - Integer min = Integer.MAX_VALUE; - Set valueLabelKeys = valueLabels.keySet(); - for (String labelKey: valueLabelKeys ) { - Integer val = null; - try { - val = Integer.parseInt( labelKey ); - } - catch(Exception ignored) {} - if(val != null) { - min = Math.min( val, min ); - } - } - return Optional.of( valueLabels.get( min.toString() ) ); - } - } - - @SuppressWarnings( "unused" ) - public static final class MaxLabelComputation implements Function { - @Override - @SuppressWarnings( "unchecked" ) - public Optional apply( final Object... args ) { - Map valueLabels = (Map) args[0]; - Integer max = Integer.MIN_VALUE; - Set valueLabelKeys = valueLabels.keySet(); - for (String labelKey: valueLabelKeys ) { - Integer val = null; - try { - val = Integer.parseInt( labelKey ); - } - catch(Exception ignored) {} - if(val != null) { - max = Math.max( val, max ); - } - } - return Optional.of( valueLabels.get( max.toString() ) ); - } - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/RemovrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/RemovrTest.java deleted file mode 100644 index a34b65f7..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/RemovrTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.exception.SpecException; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Map; - -public class RemovrTest { - - @DataProvider - public Object[][] getTestCaseNames() { - return new Object[][] { - {"firstSample"}, - {"boundaryConditions"}, - {"removrWithWildcardSupport"}, - {"multiStarSupport"}, - {"starDoublePathElementBoundaryConditions"}, - // Array tests - {"array_canPassThruNestedArrays"}, - {"array_canHandleTopLevelArray"}, - {"array_nonStarInArrayDoesNotDie"}, - {"array_removeAnArrayIndex"}, - {"array_removeJsonArrayFields"} - }; - } - - @Test(dataProvider = "getTestCaseNames") - public void runTestCases(String testCaseName) throws IOException { - - String testPath = "/json/removr/" + testCaseName; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); - - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Removr removr = new Removr( spec ); - Object actual = removr.transform( input ); - - JoltTestUtil.runDiffy( "failed case " + testPath, expected, actual ); - } - - @DataProvider - public Object[][] getNegativeTestCaseNames() { - return new Object[][] { - {"negativeTestCases"} - }; - } - - @Test(dataProvider = "getNegativeTestCaseNames", expectedExceptions = SpecException.class) - public void runNegativeTestCases(String testCaseName) throws IOException { - - String testPath = "/json/removr/" + testCaseName; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); - - Object spec = testUnit.get( "spec" ); - new Removr( spec ); - } - -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java deleted file mode 100644 index 49e6f0c7..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/ShiftrTest.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Map; - -public class ShiftrTest { - - // TODO: test arrays better (wildcards test array could be in reverse order) - @DataProvider - public Object[][] getTestCaseUnits() { - return new Object[][] { - {"arrayExample"}, - {"arrayMismatch"}, - {"bucketToPrefixSoup"}, - {"declaredOutputArray"}, - {"escapeAllTheThings"}, - {"escapeAllTheThings2"}, - {"explicitArrayKey"}, - {"filterParallelArrays"}, - {"filterParents1"}, - {"filterParents2"}, - {"filterParents3"}, - {"firstSample"}, - {"hashDefault"}, - {"identity"}, - {"inputArrayToPrefix"}, - {"invertMap"}, - {"json-ld-escaping"}, - {"keyref"}, - {"lhsAmpMatch"}, - {"listKeys"}, - {"mapToList"}, - {"mapToList2"}, - {"mergeParallelArrays1_and-transpose"}, - {"mergeParallelArrays2_and-do-not-transpose"}, - {"mergeParallelArrays3_and-filter"}, - {"multiPlacement"}, - {"objectToArray"}, - {"passNullThru"}, - {"passThru"}, - {"pollaxman_218_duplicate_speclines_bug"}, - {"prefixDataToArray"}, - {"prefixedData"}, - {"prefixSoupToBuckets"}, - {"queryMappingXform"}, - {"shiftToTrash"}, - {"simpleLHSEscape"}, - {"simpleRHSEscape"}, - {"singlePlacement"}, - {"specialKeys"}, - {"transposeArrayContents1"}, - {"transposeArrayContents2"}, - {"transposeComplex1"}, - {"transposeComplex2"}, - {"transposeComplex3_both-sides-multipart"}, - {"transposeComplex4_lhs-multipart-rhs-sugar"}, - {"transposeComplex5_at-logic-with-embedded-array-lookups"}, - {"transposeComplex6_rhs-complex-at"}, - {"transposeComplex7_coerce-int-string-conversion"}, - {"transposeComplex8_coerce-boolean-string-conversion"}, - {"transposeComplex9_lookup_an_array_index"}, - {"transposeInverseMap1"}, - {"transposeInverseMap2"}, - {"transposeLHS1"}, - {"transposeLHS2"}, - {"transposeLHS3"}, - {"transposeNestedLookup"}, - {"transposeSimple1"}, - {"transposeSimple2"}, - {"transposeSimple3"}, - {"wildcards"}, - {"wildcardSelfAndRef"}, - {"wildcardsWithOr"} - }; - } - - // TODO: test arrays better (wildcards test array could be in reverse order) - - @Test(dataProvider = "getTestCaseUnits") - public void runTestUnits(String testCaseName) throws IOException { - - String testPath = "/json/shiftr/" + testCaseName; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); - - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); - - Shiftr shiftr = new Shiftr( spec ); - Object actual = shiftr.transform( input ); - - JoltTestUtil.runDiffy( "failed case " + testPath, expected, actual ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrIncrementTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrIncrementTest.java deleted file mode 100644 index 24a88124..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrIncrementTest.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.chainr; - -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JoltTestUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.exception.TransformException; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.HashMap; - -public class ChainrIncrementTest { - - @DataProvider - public Object[][] fromToTests() { - - Object chainrSpec = JsonUtils.classpathToObject( "/json/chainr/increments/spec.json" ); - - return new Object[][] { - {chainrSpec, 0, 1}, - {chainrSpec, 0, 3}, - {chainrSpec, 1, 3}, - {chainrSpec, 1, 4} - }; - } - - @Test( dataProvider = "fromToTests") - public void testChainrIncrementsFromTo( Object chainrSpec, int start, int end ) throws IOException { - Chainr chainr = Chainr.fromSpec( chainrSpec ); - - Object expected = JsonUtils.classpathToObject( "/json/chainr/increments/" + start + "-" + end + ".json" ); - - Object actual = chainr.transform( start, end, new HashMap() ); - - JoltTestUtil.runDiffy( "failed incremental From-To Chainr", expected, actual ); - } - - - @DataProvider - public Object[][] toTests() { - - Object chainrSpec = JsonUtils.classpathToObject( "/json/chainr/increments/spec.json" ); - - return new Object[][] { - {chainrSpec, 1}, - {chainrSpec, 3} - }; - } - - @Test( dataProvider = "toTests") - public void testChainrIncrementsTo( Object chainrSpec, int end ) throws IOException { - - Chainr chainr = Chainr.fromSpec( chainrSpec ); - - Object expected = JsonUtils.classpathToObject( "/json/chainr/increments/0-" + end + ".json" ); - - Object actual = chainr.transform( end, new HashMap() ); - - JoltTestUtil.runDiffy( "failed incremental To Chainr", expected, actual ); - } - - @DataProvider - public Object[][] failTests() { - - Object chainrSpec = JsonUtils.classpathToObject( "/json/chainr/increments/spec.json" ); - - return new Object[][] { - {chainrSpec, 0, 0}, - {chainrSpec, -2, 2}, - {chainrSpec, 0, -2}, - {chainrSpec, 1, 10000} - }; - } - - @Test( dataProvider = "failTests", expectedExceptions = TransformException.class) - public void testFails( Object chainrSpec, int start, int end ) throws IOException { - Chainr chainr = Chainr.fromSpec( chainrSpec ); - chainr.transform( start, end, new HashMap()); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/DeepCopyTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/common/DeepCopyTest.java deleted file mode 100644 index 22d3e4be..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/DeepCopyTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common; - -import com.bazaarvoice.jolt.JoltTestUtil; -import com.bazaarvoice.jolt.JsonUtils; -import org.testng.annotations.Test; - -import java.util.List; -import java.util.Map; - -public class DeepCopyTest { - - @Test - public void deepCopyTest() throws Exception { - - Object input = JsonUtils.classpathToObject( "/json/deepcopy/original.json" ); - - Map fiddle = (Map) DeepCopy.simpleDeepCopy( input ); - - JoltTestUtil.runDiffy( "Verify that the DeepCopy did in fact make a copy.", input, fiddle ); - - // The test is to make a deep copy, then manipulate the copy, and verify that the original did not change ;) - // copy and fiddle - List array = (List) fiddle.get( "array" ); - array.add( "c" ); - array.set( 1, 3 ); - Map subMap = (Map) fiddle.get( "map" ); - subMap.put("c", "c"); - subMap.put("b", 3 ); - - // Verify that the input to the copy was unmodified - Object unmodified = JsonUtils.classpathToObject( "/json/deepcopy/original.json" ); - JoltTestUtil.runDiffy( "Verify that the deepcopy was actually deep / input is unmodified", unmodified, input ); - - // Verify we made the modifications we wanted to. - Object expectedModified = JsonUtils.classpathToObject( "/json/deepcopy/modifed.json" ); - JoltTestUtil.runDiffy( "Verify fiddled post deepcopy object looks correct / was modifed.", expectedModified, fiddle ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarDoublePathElementTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarDoublePathElementTest.java deleted file mode 100644 index 1ce8a6ca..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarDoublePathElementTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import org.testng.Assert; -import org.testng.annotations.Test; - -public class StarDoublePathElementTest { - - @Test - public void testStarInFirstAndMiddle() { - - StarPathElement star = new StarDoublePathElement( "*a*" ); - - Assert.assertTrue( star.stringMatch( "bbbaaccccc" ) ); - Assert.assertFalse( star.stringMatch( "abbbbbbbbcc" ) ); - Assert.assertFalse( star.stringMatch( "bbba" ) ); - - MatchedElement lpe = star.match( "bbbaccc", null ); - // * -> bbb - // a -> a - // * -> ccc - Assert.assertEquals( "bbbaccc", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "bbb", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "ccc", lpe.getSubKeyRef( 2 ) ); - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - - } - - @Test - public void testStarAtFrontAndEnd() { - - StarPathElement star = new StarDoublePathElement( "*a*c" ); - - Assert.assertTrue( star.stringMatch( "bbbbadddc" ) ); - Assert.assertTrue( star.stringMatch( "bacc" ) ); - Assert.assertFalse( star.stringMatch( "bac" ) ); - Assert.assertFalse( star.stringMatch( "baa" ) ); - - MatchedElement lpe = star.match( "abcadefc", null ); - // * -> abc - // a -> a index 4 - // * -> def - // c -> c - Assert.assertEquals( "abcadefc", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "abc", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "def", lpe.getSubKeyRef( 2 ) ); - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - - } - - @Test - public void testStarAtMiddleAndEnd() { - - StarPathElement star = new StarDoublePathElement( "a*b*" ); - - Assert.assertTrue( star.stringMatch( "adbc" ) ); - Assert.assertTrue( star.stringMatch( "abbc" ) ); - Assert.assertFalse( star.stringMatch( "adddddd" ) ); - Assert.assertFalse( star.stringMatch( "addb" ) ); - Assert.assertFalse( star.stringMatch( "abc" ) ); - - MatchedElement lpe = star.match( "abcbbac", null ); - // a -> a - // * -> bc index 1 - // b -> b index 3 - // * -> bac index 4 - // c -> c - Assert.assertEquals( "abcbbac", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "bc", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "bac", lpe.getSubKeyRef( 2 ) ); - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - - } - - - @Test - public void testStarsInMiddle() { - - StarPathElement star = new StarDoublePathElement( "a*b*c" ); - - Assert.assertTrue( star.stringMatch( "a123b456c" ) ); - Assert.assertTrue( star.stringMatch( "abccbcc" ) ); - - MatchedElement lpe = star.match( "abccbcc", null ); - // a -> a - // * -> bcc index 1 - // b -> b - // * -> c index 2 - // c -> c - Assert.assertEquals( "abccbcc", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "bcc", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "c", lpe.getSubKeyRef( 2 ) ); - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - - } - - - @Test - public void testStarsInMiddleNonGreedy() { - - StarPathElement star = new StarDoublePathElement( "a*b*c" ); - - MatchedElement lpe = star.match( "abbccbccc", null ); - // a -> a - // * -> b index 1 - // b -> b - // * -> ccbcc index 2 - // c -> c - Assert.assertEquals( "abbccbccc", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "b", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "ccbcc", lpe.getSubKeyRef( 2 ) ); - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarRegexPathElementTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarRegexPathElementTest.java deleted file mode 100644 index 1a3bec0e..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarRegexPathElementTest.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -public class StarRegexPathElementTest { - - @DataProvider - public Object[][] getStarPatternTests() { - return new Object[][] { - {"easy star test", "rating-*-*", "rating-tuna-marlin", "tuna", "marlin"}, - {"easy facet usage", "terms--config--*--*--cdv", "terms--config--Expertise--12345--cdv", "Expertise", "12345"}, - {"degenerate ProductId in facet", "terms--config--*--*--cdv", "terms--config--Expertise--12345--6789--cdv", "Expertise", "12345--6789"}, - {"multi metachar test", "rating.$.*.*", "rating.$.marlin$.test.", "marlin$", "test."}, - }; - } - - @Test( dataProvider = "getStarPatternTests") - public void starPatternTest( String testName, String spec, String dataKey, String expected1, String expected2 ) { - - StarPathElement star = new StarRegexPathElement( spec ); - - MatchedElement lpe = star.match( dataKey, null ); - - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - Assert.assertEquals( dataKey, lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( expected1, lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( expected2, lpe.getSubKeyRef( 2 ) ); - } - - @Test - public void mustMatchSomethingTest() { - - StarPathElement star = new StarRegexPathElement( "tuna-*-*"); - - Assert.assertNull( star.match( "tuna--", null ) ); - Assert.assertNull( star.match( "tuna-bob-", null ) ); - Assert.assertNull( star.match( "tuna--bob", null ) ); - - StarPathElement multiMetacharStarpathelement = new StarRegexPathElement( "rating-$-*-*"); - - Assert.assertNull( multiMetacharStarpathelement.match( "rating-capGrp1-capGrp2", null ) ); - Assert.assertNull( multiMetacharStarpathelement.match( "rating-$capGrp1-capGrp2", null ) ); - Assert.assertNotNull(multiMetacharStarpathelement.match( "rating-$-capGrp1-capGrp2",null) ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarSinglePathElementTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarSinglePathElementTest.java deleted file mode 100644 index 3822a655..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/pathelement/StarSinglePathElementTest.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.pathelement; - -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import org.testng.Assert; -import org.testng.annotations.Test; - -public class StarSinglePathElementTest { - - @Test - public void testStarAtFront() { - - StarPathElement star = new StarSinglePathElement( "*-tuna" ); - Assert.assertTrue( star.stringMatch( "tuna-tuna" ) ); - Assert.assertTrue( star.stringMatch( "bob-tuna" ) ); - Assert.assertFalse( star.stringMatch( "-tuna" ) ); // * has to catch something - Assert.assertFalse( star.stringMatch( "tuna" ) ); - Assert.assertFalse( star.stringMatch( "tuna-bob" ) ); - - MatchedElement lpe = star.match( "bob-tuna", null ); - Assert.assertEquals( "bob-tuna", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "bob", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( 2, lpe.getSubKeyCount() ); - - Assert.assertNull( star.match( "-tuna", null ) ); - } - - @Test - public void testStarAtEnd() { - - StarPathElement star = new StarSinglePathElement( "tuna-*" ); - Assert.assertTrue( star.stringMatch( "tuna-tuna" ) ); - Assert.assertTrue( star.stringMatch( "tuna-bob" ) ); - Assert.assertFalse( star.stringMatch( "tuna-" ) ); - Assert.assertFalse( star.stringMatch( "tuna" ) ); - Assert.assertFalse( star.stringMatch( "bob-tuna" ) ); - - MatchedElement lpe = star.match( "tuna-bob", null ); - Assert.assertEquals( "tuna-bob", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "bob", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( 2, lpe.getSubKeyCount() ); - - Assert.assertNull( star.match( "tuna-", null ) ); - } - - @Test - public void testStarInMiddle() { - - StarPathElement star = new StarSinglePathElement( "tuna-*-marlin" ); - Assert.assertTrue( star.stringMatch( "tuna-tuna-marlin" ) ); - Assert.assertTrue( star.stringMatch( "tuna-bob-marlin" ) ); - Assert.assertFalse( star.stringMatch( "tuna--marlin" ) ); - Assert.assertFalse( star.stringMatch( "tunamarlin" ) ); - Assert.assertFalse( star.stringMatch( "marlin-bob-tuna" ) ); - - MatchedElement lpe = star.match( "tuna-bob-marlin", null ); - Assert.assertEquals( "tuna-bob-marlin", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( "bob", lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( 2, lpe.getSubKeyCount() ); - - Assert.assertNull( star.match( "bob", null ) ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/reference/PathAndGroupReferenceTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/common/reference/PathAndGroupReferenceTest.java deleted file mode 100644 index 00efa94c..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/reference/PathAndGroupReferenceTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.reference; - -import com.bazaarvoice.jolt.exception.SpecException; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -public class PathAndGroupReferenceTest { - - @DataProvider - public Object[][] getValidReferenceTests() { - return new Object[][] { - { "", 0, 0, "(0,0)" }, - { "3", 3, 0, "(3,0)" }, - { "(3)", 3, 0, "(3,0)" }, - {"(1,2)", 1, 2, "(1,2)" } - }; - } - - @Test( dataProvider = "getValidReferenceTests" ) - public void validAmpReferencePatternTest(String key, int pathIndex, int keyGroup, String canonicalForm) { - - PathAndGroupReference amp = new AmpReference( "&" + key ); - Assert.assertEquals( pathIndex, amp.getPathIndex() ); - Assert.assertEquals( keyGroup, amp.getKeyGroup() ); - Assert.assertEquals( "&" + canonicalForm, amp.getCanonicalForm() ); - } - - @Test( dataProvider = "getValidReferenceTests" ) - public void validDollarReferencePatternTest(String key, int pathIndex, int keyGroup, String canonicalForm) { - - PathAndGroupReference amp = new DollarReference( "$" + key ); - Assert.assertEquals( pathIndex, amp.getPathIndex() ); - Assert.assertEquals( keyGroup, amp.getKeyGroup() ); - Assert.assertEquals( "$" + canonicalForm, amp.getCanonicalForm() ); - } - - - @DataProvider - public Object[][] getFailReferenceTests() { - return new Object[][] { - { "pants" }, - { "-1" }, - { "(-1,2)" }, - { "(1,-2)" }, - }; - } - - @Test( dataProvider = "getFailReferenceTests", expectedExceptions = SpecException.class ) - public void failAmpReferencePatternTest(String key ) { - new AmpReference( "&" + key ); - } - - @Test( dataProvider = "getFailReferenceTests", expectedExceptions = SpecException.class ) - public void failDollarReferencePatternTest(String key ) { - new DollarReference( "$" + key ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/reference/PathReferenceTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/common/reference/PathReferenceTest.java deleted file mode 100644 index 5d12aa40..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/common/reference/PathReferenceTest.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.common.reference; - -import com.bazaarvoice.jolt.exception.SpecException; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -public class PathReferenceTest { - - @DataProvider - public Object[][] getValidReferenceTests() { - return new Object[][] { - { "", 0, "0" }, - { "3", 3, "3" }, - { "12", 12, "12" } - }; - } - - @Test( dataProvider = "getValidReferenceTests" ) - public void validAmpReferencePatternTest(String key, int pathIndex, String canonicalForm) { - - PathReference ref = new HashReference( "#" + key ); - Assert.assertEquals( pathIndex, ref.getPathIndex() ); - Assert.assertEquals( "#" + canonicalForm, ref.getCanonicalForm() ); - } - - - @DataProvider - public Object[][] getFailReferenceTests() { - return new Object[][] { - { "pants" }, - { "-1" }, - { "(1)" } - }; - } - - @Test( dataProvider = "getFailReferenceTests", expectedExceptions = SpecException.class ) - public void failAmpReferencePatternTest(String key ) { - new HashReference( "#" + key ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/ListsTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/ListsTest.java deleted file mode 100644 index 3b87a60f..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/ListsTest.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; -import org.testng.annotations.DataProvider; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -@SuppressWarnings( "deprecated" ) -public class ListsTest extends AbstractTester { - - @DataProvider(parallel = true) - public Iterator getTestCases() { - List testCases = new LinkedList<>( ); - - Function FIRST_ELEMENT = new Lists.firstElement(); - Function LAST_ELEMENT = new Lists.lastElement(); - Function ELEMENT_AT = new Lists.elementAt(); - - Function SIZE = new Objects.size(); - - testCases.add( new Object[] {"first-empty-array", FIRST_ELEMENT, new Object[0], Optional.empty() } ); - testCases.add( new Object[] {"first-empty-list", FIRST_ELEMENT, Arrays.asList( ), Optional.empty() } ); - - testCases.add( new Object[] {"first-null", FIRST_ELEMENT, null, Optional.empty() } ); - testCases.add( new Object[] {"first-array", FIRST_ELEMENT, new Object[]{ 1, 2, 3 }, Optional.of( 1 ) } ); - testCases.add( new Object[] {"first-list", FIRST_ELEMENT, Arrays.asList( 1, 2, 3 ), Optional.of( 1 ) } ); - - - - testCases.add( new Object[] {"last-empty-array", LAST_ELEMENT, new Object[0], Optional.empty() } ); - testCases.add( new Object[] {"last-empty-list", LAST_ELEMENT, Arrays.asList( ), Optional.empty() } ); - - testCases.add( new Object[] {"last-null", LAST_ELEMENT, null, Optional.empty() } ); - testCases.add( new Object[] {"last-array", LAST_ELEMENT, new Object[]{ 1, 2, 3 }, Optional.of( 3 ) } ); - testCases.add( new Object[] {"last-list", LAST_ELEMENT, Arrays.asList( 1, 2, 3 ), Optional.of( 3 ) } ); - - - - testCases.add( new Object[] {"at-empty-array", ELEMENT_AT, new Object[] {5}, Optional.empty() } ); - testCases.add( new Object[] {"at-empty-list", ELEMENT_AT, Arrays.asList( 5 ), Optional.empty() } ); - testCases.add( new Object[] {"at-empty-null", ELEMENT_AT, new Object[] {null, 1}, Optional.empty() } ); - testCases.add( new Object[] {"at-empty-invalid", ELEMENT_AT, new Object(), Optional.empty() } ); - - testCases.add( new Object[] {"at-array", ELEMENT_AT, new Object[]{ 1, 2, 3, 1 }, Optional.of( 3 ) } ); - testCases.add( new Object[] {"at-list", ELEMENT_AT, Arrays.asList( 1, 2, 3, 1 ), Optional.of( 3 ) } ); - - testCases.add( new Object[] {"at-array-missing", ELEMENT_AT, new Object[]{ 5, 1, 2, 3 }, Optional.empty() } ); - testCases.add( new Object[] {"at-list-missing", ELEMENT_AT, Arrays.asList( 5, 1, 2, 3 ), Optional.empty() } ); - - - testCases.add( new Object[] {"size-list", SIZE, new Object[]{ 5, 1, 2, 3 }, Optional.of(4) } ); - testCases.add( new Object[] {"size-list-empty", SIZE, Arrays.asList( ), Optional.of(0) } ); - - return testCases.iterator(); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/MathTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/MathTest.java deleted file mode 100644 index 1e7c10b8..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/MathTest.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import static com.bazaarvoice.jolt.modifier.function.Math.abs; -import static com.bazaarvoice.jolt.modifier.function.Objects.toNumber; - -@SuppressWarnings( "deprecated" ) -public class MathTest extends AbstractTester { - - @DataProvider(parallel = true) - public Iterator getTestCases() { - List testCases = new LinkedList<>( ); - - Function MAX_OF = new Math.max(); - Function MIN_OF = new Math.min(); - Function ABS_OF = new Math.abs(); - Function TO_INTEGER = new Objects.toInteger(); - Function TO_DOUBLE = new Objects.toDouble(); - Function TO_LONG = new Objects.toLong(); - - Function INT_SUM_OF = new Math.intSum(); - Function DOUBLE_SUM_OF = new Math.doubleSum(); - Function LONG_SUM_OF = new Math.longSum(); - - Function INT_SUBTRACT_OF = new Math.intSubtract(); - Function DOUBLE_SUBTRACT_OF = new Math.doubleSubtract(); - Function LONG_SUBTRACT_OF = new Math.longSubtract(); - - Function DIV_OF = new Math.divide(); - Function DIV_AND_ROUND_OF = new Math.divideAndRound(); - - testCases.add( new Object[] { "max-empty-array", MAX_OF, new Object[] {}, Optional.empty() } ); - testCases.add( new Object[] { "max-empty-list", MAX_OF, new ArrayList( ), Optional.empty() } ); - testCases.add( new Object[] { "max-null", MAX_OF, null, Optional.empty() } ); - testCases.add( new Object[] { "max-object", MAX_OF, new Object(), Optional.empty() } ); - - testCases.add( new Object[] { "max-single-int-array", MAX_OF, new Object[] {1}, Optional.of( 1 ) } ); - testCases.add( new Object[] { "max-single-long-array", MAX_OF, new Object[] {1L}, Optional.of( 1L ) } ); - testCases.add( new Object[] { "max-single-double-array", MAX_OF, new Object[] {1.0}, Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "max-single-int-list", MAX_OF, Arrays.asList( 1 ), Optional.of( 1 ) } ); - testCases.add( new Object[] { "max-single-long-list", MAX_OF, Arrays.asList( 1L ), Optional.of( 1L ) } ); - testCases.add( new Object[] { "max-single-double-list", MAX_OF, Arrays.asList( 1.0 ), Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "max-single-int-array-extra-arg", MAX_OF, new Object[] {1, "a"}, Optional.of( 1 ) } ); - testCases.add( new Object[] { "max-single-long-array-extra-arg", MAX_OF, new Object[] {1L, "a"}, Optional.of( 1L ) } ); - testCases.add( new Object[] { "max-single-double-array-extra-arg", MAX_OF, new Object[] {1.0, "a"}, Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "max-single-int-list-extra-arg", MAX_OF, Arrays.asList( 1, "a" ), Optional.of( 1 ) } ); - testCases.add( new Object[] { "max-single-long-list-extra-arg", MAX_OF, Arrays.asList( 1L, "a" ), Optional.of( 1L ) } ); - testCases.add( new Object[] { "max-single-double-list-extra-arg", MAX_OF, Arrays.asList( 1.0, "a" ), Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "max-multi-int-array", MAX_OF, new Object[] {1, 3, 2, 5}, Optional.of( 5 ) } ); - testCases.add( new Object[] { "max-multi-long-array", MAX_OF, new Object[] {1L, 3L, 2L, 5L}, Optional.of( 5L ) } ); - testCases.add( new Object[] { "max-multi-double-array", MAX_OF, new Object[] {1.0, 3.0, 2.0, 5.0}, Optional.of( 5.0 ) } ); - - testCases.add( new Object[] { "max-multi-int-list", MAX_OF, Arrays.asList( 1, 3, 2, 5 ), Optional.of( 5 ) } ); - testCases.add( new Object[] { "max-multi-long-list", MAX_OF, Arrays.asList( 1L, 3L, 2L, 5L ), Optional.of( 5L ) } ); - testCases.add( new Object[] { "max-multi-double-list", MAX_OF, Arrays.asList( 1.0, 3.0, 2.0, 5.0 ), Optional.of( 5.0 ) } ); - - testCases.add( new Object[] { "max-combo-int-array", MAX_OF, new Object[] {1.0, 3L, null, 5}, Optional.of( 5 ) } ); - testCases.add( new Object[] { "max-combo-long-array", MAX_OF, new Object[] {1.0, 3L, null, 5L}, Optional.of( 5L ) } ); - testCases.add( new Object[] { "max-combo-double-array", MAX_OF, new Object[] {1.0, 3L, null, 5.0}, Optional.of( 5.0 ) } ); - - testCases.add( new Object[] { "max-combo-int-list", MAX_OF, Arrays.asList( 1.0, 3L, null, 5 ), Optional.of( 5 ) } ); - testCases.add( new Object[] { "max-combo-long-list", MAX_OF, Arrays.asList( 1.0, 3L, null, 5L ), Optional.of( 5L ) } ); - testCases.add( new Object[] { "max-combo-double-list", MAX_OF, Arrays.asList( 1.0, 3L, null, 5.0 ), Optional.of( 5.0 ) } ); - - testCases.add( new Object[] { "max-NaN", MAX_OF, Arrays.asList( 1.0, Double.NaN ), Optional.of( Double.NaN ) } ); - testCases.add( new Object[] { "max-positive-infinity", MAX_OF, Arrays.asList( 1.0, Double.POSITIVE_INFINITY ), Optional.of( Double.POSITIVE_INFINITY ) } ); - testCases.add( new Object[] { "max-NaN-positive-infinity", MAX_OF, Arrays.asList( 1.0, Double.NaN, Double.POSITIVE_INFINITY ), Optional.of( Double.NaN ) } ); - - - - testCases.add( new Object[] { "min-empty-array", MIN_OF, new Object[] {}, Optional.empty() } ); - testCases.add( new Object[] { "min-empty-list", MIN_OF, new ArrayList( ), Optional.empty() } ); - testCases.add( new Object[] { "min-null", MIN_OF, null, Optional.empty() } ); - testCases.add( new Object[] { "min-object", MIN_OF, new Object(), Optional.empty() } ); - - testCases.add( new Object[] { "min-single-int-array", MIN_OF, new Object[] {1}, Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-single-long-array", MIN_OF, new Object[] {1L}, Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-single-double-array", MIN_OF, new Object[] {1.0}, Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-single-int-list", MIN_OF, Arrays.asList( 1 ), Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-single-long-list", MIN_OF, Arrays.asList( 1L ), Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-single-double-list", MIN_OF, Arrays.asList( 1.0 ), Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-single-int-array-extra-arg", MIN_OF, new Object[] {1, "a"}, Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-single-long-array-extra-arg", MIN_OF, new Object[] {1L, "a"}, Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-single-double-array-extra-arg", MIN_OF, new Object[] {1.0, "a"}, Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-single-int-list-extra-arg", MIN_OF, Arrays.asList( 1, "a" ), Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-single-long-list-extra-arg", MIN_OF, Arrays.asList( 1L, "a" ), Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-single-double-list-extra-arg", MIN_OF, Arrays.asList( 1.0, "a" ), Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-multi-int-array", MIN_OF, new Object[] {1, 3, 2, 5}, Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-multi-long-array", MIN_OF, new Object[] {1L, 3L, 2L, 5L}, Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-multi-double-array", MIN_OF, new Object[] {1.0, 3.0, 2.0, 5.0}, Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-multi-int-list", MIN_OF, Arrays.asList( 1, 3, 2, 5 ), Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-multi-long-list", MIN_OF, Arrays.asList( 1L, 3L, 2L, 5L ), Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-multi-double-list", MIN_OF, Arrays.asList( 1.0, 3.0, 2.0, 5.0 ), Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-combo-int-array", MIN_OF, new Object[] {1, 3L, null, 5.0}, Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-combo-long-array", MIN_OF, new Object[] {1L, 3, null, 5.0}, Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-combo-double-array", MIN_OF, new Object[] {1.0, 3L, null, 5}, Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-combo-int-list", MIN_OF, Arrays.asList( 1, 3L, null, 5.0 ), Optional.of( 1 ) } ); - testCases.add( new Object[] { "min-combo-long-list", MIN_OF, Arrays.asList( 1L, 3, null, 5.0 ), Optional.of( 1L ) } ); - testCases.add( new Object[] { "min-combo-double-list", MIN_OF, Arrays.asList( 1.0, 3L, null, 5 ), Optional.of( 1.0 ) } ); - - testCases.add( new Object[] { "min-NaN", MIN_OF, Arrays.asList( -1.0, Double.NaN ), Optional.of( Double.NaN ) } ); - testCases.add( new Object[] { "min-negative-Infinity", MIN_OF, Arrays.asList( -1.0, Double.NEGATIVE_INFINITY ), Optional.of( Double.NEGATIVE_INFINITY ) } ); - testCases.add( new Object[] { "min-NaN-positive-infinity", MIN_OF, Arrays.asList( -1.0, Double.NaN, Double.NEGATIVE_INFINITY ), Optional.of( Double.NaN ) } ); - - - testCases.add( new Object[] { "abs-null", ABS_OF, null, Optional.empty() } ); - testCases.add( new Object[] { "abs-invalid", ABS_OF, new Object(), Optional.empty() } ); - testCases.add( new Object[] { "abs-empty-list", ABS_OF, new Object[] {}, Optional.empty() } ); - testCases.add( new Object[] { "abs-empty-array", ABS_OF, Arrays.asList( ), Optional.empty() } ); - - testCases.add( new Object[] { "abs-single-negative-int", ABS_OF, -1, Optional.of( 1 ) } ); - testCases.add( new Object[] { "abs-single-negative-long", ABS_OF, -1L, Optional.of(1L) } ); - testCases.add( new Object[] { "abs-single-negative-double", ABS_OF, -1.0, Optional.of(1.0) } ); - testCases.add( new Object[] { "abs-single-positive-int", ABS_OF, 1, Optional.of( 1 ) } ); - testCases.add( new Object[] { "abs-single-positive-long", ABS_OF, 1L, Optional.of(1L) } ); - testCases.add( new Object[] { "abs-single-positive-double", ABS_OF, 1.0, Optional.of(1.0) } ); - - testCases.add( new Object[] { "abs-list", ABS_OF, new Object[] { -1, -1L, -1.0 }, Optional.of( Arrays.asList( 1, 1L, 1.0 ) ) } ); - testCases.add( new Object[] { "abs-array", ABS_OF, Arrays.asList( -1, -1L, -1.0 ), Optional.of( Arrays.asList( 1, 1L, 1.0 ) ) } ); - - testCases.add( new Object[] { "abs-Nan", ABS_OF, Double.NaN, Optional.of(Double.NaN) } ); - testCases.add( new Object[] { "abs-PosInfinity", ABS_OF, Double.POSITIVE_INFINITY, Optional.of(Double.POSITIVE_INFINITY) } ); - testCases.add( new Object[] { "abs-NefInfinity", ABS_OF, Double.NEGATIVE_INFINITY, Optional.of(Double.POSITIVE_INFINITY) } ); - - - testCases.add( new Object[] { "toInt-null", TO_INTEGER, null, Optional.empty() } ); - testCases.add( new Object[] { "toInt-invalid", TO_INTEGER, new Object(), Optional.empty() } ); - testCases.add( new Object[] { "toInt-empty-array", TO_INTEGER, new Object[] {}, Optional.empty() } ); - testCases.add( new Object[] { "toInt-empty-list", TO_INTEGER, Arrays.asList( ), Optional.empty() } ); - - testCases.add( new Object[] { "toInt-single-positive-string", TO_INTEGER, "1", Optional.of( 1 ) } ); - testCases.add( new Object[] { "toInt-single-negative-string", TO_INTEGER, "-1", Optional.of( -1 ) } ); - testCases.add( new Object[] { "toInt-single-positive-int", TO_INTEGER, 1, Optional.of( 1 ) } ); - testCases.add( new Object[] { "toInt-single-negative-int", TO_INTEGER, -1, Optional.of( -1 ) } ); - testCases.add( new Object[] { "toInt-single-positive-long", TO_INTEGER, 1L, Optional.of( 1 ) } ); - testCases.add( new Object[] { "toInt-single-negative-long", TO_INTEGER, -1L, Optional.of( -1 ) } ); - testCases.add( new Object[] { "toInt-single-positive-double", TO_INTEGER, 1.0, Optional.of( 1 ) } ); - testCases.add( new Object[] { "toInt-single-negative-double", TO_INTEGER, -1.0, Optional.of( -1 ) } ); - - testCases.add( new Object[] { "toInt-single-positive-string-list", TO_INTEGER, new Object[] {"1", "2"}, Optional.of( Arrays.asList( 1, 2 ) ) } ); - testCases.add( new Object[] { "toInt-single-negative-string-array", TO_INTEGER, Arrays.asList( "-1", "-2" ), Optional.of( Arrays.asList( -1, -2 ) ) } ); - testCases.add( new Object[] { "toInt-single-positive-int-list", TO_INTEGER, new Object[] { 1, 2 }, Optional.of( Arrays.asList( 1, 2 ) ) } ); - testCases.add( new Object[] { "toInt-single-negative-int-array", TO_INTEGER, Arrays.asList( -1, -2 ), Optional.of( Arrays.asList( -1, -2 ) ) } ); - testCases.add( new Object[] { "toInt-single-positive-long-list", TO_INTEGER, new Object[] {1L, 2L}, Optional.of( Arrays.asList( 1, 2 ) ) } ); - testCases.add( new Object[] { "toInt-single-negative-long-array", TO_INTEGER, Arrays.asList( -1L, -2L ), Optional.of( Arrays.asList( -1, -2 ) ) } ); - testCases.add( new Object[] { "toInt-single-positive-double-list", TO_INTEGER, new Object[] {1.0, 2.0}, Optional.of( Arrays.asList( 1, 2 ) ) } ); - testCases.add( new Object[] { "toInt-single-negative-double-array", TO_INTEGER, Arrays.asList( -1.0, -2.0 ), Optional.of( Arrays.asList( -1, -2 ) ) } ); - - - testCases.add( new Object[] { "toDouble-null", TO_DOUBLE, null, Optional.empty() } ); - testCases.add( new Object[] { "toDouble-invalid", TO_DOUBLE, new Object(), Optional.empty() } ); - testCases.add( new Object[] { "toDouble-empty-array", TO_DOUBLE, new Object[] {}, Optional.empty() } ); - testCases.add( new Object[] { "toDouble-empty-list", TO_DOUBLE, Arrays.asList( ), Optional.empty() } ); - - testCases.add( new Object[] { "toDouble-single-positive-string", TO_DOUBLE, "1", Optional.of( 1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-negative-string", TO_DOUBLE, "-1", Optional.of( -1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-positive-int", TO_DOUBLE, 1, Optional.of( 1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-negative-int", TO_DOUBLE, -1, Optional.of( -1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-positive-long", TO_DOUBLE, 1L, Optional.of( 1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-negative-long", TO_DOUBLE, -1L, Optional.of( -1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-positive-double", TO_DOUBLE, 1.0, Optional.of( 1.0 ) } ); - testCases.add( new Object[] { "toDouble-single-negative-double", TO_DOUBLE, -1.0, Optional.of( -1.0 ) } ); - - testCases.add( new Object[] { "toDouble-single-positive-string-list", TO_DOUBLE, new Object[] {"1", "2"}, Optional.of( Arrays.asList( 1.0, 2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-negative-string-array", TO_DOUBLE, Arrays.asList( "-1", "-2" ), Optional.of( Arrays.asList( -1.0, -2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-positive-int-list", TO_DOUBLE, new Object[] { 1, 2 }, Optional.of( Arrays.asList( 1.0, 2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-negative-int-array", TO_DOUBLE, Arrays.asList( -1, -2 ), Optional.of( Arrays.asList( -1.0, -2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-positive-long-list", TO_DOUBLE, new Object[] {1L, 2L}, Optional.of( Arrays.asList( 1.0, 2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-negative-long-array", TO_DOUBLE, Arrays.asList( -1L, -2L ), Optional.of( Arrays.asList( -1.0, -2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-positive-double-list", TO_DOUBLE, new Object[] {1.0, 2.0}, Optional.of( Arrays.asList( 1.0, 2.0 ) ) } ); - testCases.add( new Object[] { "toDouble-single-negative-double-array", TO_DOUBLE, Arrays.asList( -1.0, -2.0 ), Optional.of( Arrays.asList( -1.0, -2.0 ) ) } ); - - - testCases.add( new Object[] { "toLong-null", TO_LONG, null, Optional.empty() } ); - testCases.add( new Object[] { "toLong-invalid", TO_LONG, new Object(), Optional.empty() } ); - testCases.add( new Object[] { "toLong-empty-array", TO_LONG, new Object[] {}, Optional.empty() } ); - testCases.add( new Object[] { "toLong-empty-list", TO_LONG, Arrays.asList( ), Optional.empty() } ); - - testCases.add( new Object[] { "toLong-single-positive-string", TO_LONG, "1", Optional.of( 1L ) } ); - testCases.add( new Object[] { "toLong-single-negative-string", TO_LONG, "-1", Optional.of( -1L ) } ); - testCases.add( new Object[] { "toLong-single-positive-int", TO_LONG, 1, Optional.of( 1L ) } ); - testCases.add( new Object[] { "toLong-single-negative-int", TO_LONG, -1, Optional.of( -1L ) } ); - testCases.add( new Object[] { "toLong-single-positive-long", TO_LONG, 1L, Optional.of( 1L ) } ); - testCases.add( new Object[] { "toLong-single-negative-long", TO_LONG, -1L, Optional.of( -1L ) } ); - testCases.add( new Object[] { "toLong-single-positive-double", TO_LONG, 1L, Optional.of( 1L ) } ); - testCases.add( new Object[] { "toLong-single-negative-double", TO_LONG, -1L, Optional.of( -1L ) } ); - - testCases.add( new Object[] { "toLong-single-positive-string-list", TO_LONG, new Object[] {"1", "2"}, Optional.of( Arrays.asList( 1L, 2L ) ) } ); - testCases.add( new Object[] { "toLong-single-negative-string-array", TO_LONG, Arrays.asList( "-1", "-2" ), Optional.of( Arrays.asList( -1L, -2L ) ) } ); - testCases.add( new Object[] { "toLong-single-positive-int-list", TO_LONG, new Object[] { 1, 2 }, Optional.of( Arrays.asList( 1L, 2L ) ) } ); - testCases.add( new Object[] { "toLong-single-negative-int-array", TO_LONG, Arrays.asList( -1, -2 ), Optional.of( Arrays.asList( -1L, -2L ) ) } ); - testCases.add( new Object[] { "toLong-single-positive-long-list", TO_LONG, new Object[] {1L, 2L}, Optional.of( Arrays.asList( 1L, 2L ) ) } ); - testCases.add( new Object[] { "toLong-single-negative-long-array", TO_LONG, Arrays.asList( -1L, -2L ), Optional.of( Arrays.asList( -1L, -2L ) ) } ); - testCases.add( new Object[] { "toLong-single-positive-double-list", TO_LONG, new Object[] {1L, 2L}, Optional.of( Arrays.asList( 1L, 2L ) ) } ); - testCases.add( new Object[] { "toLong-single-negative-double-array", TO_LONG, Arrays.asList( -1L, -2L ), Optional.of( Arrays.asList( -1L, -2L ) ) } ); - - testCases.add( new Object[] { "toInteger-combo-string-array", TO_INTEGER, Arrays.asList( "-1", 2, -3L, 4.0 ), Optional.of( Arrays.asList( -1, 2, -3, 4 ) ) } ); - testCases.add( new Object[] { "toLong-combo-int-array", TO_LONG, Arrays.asList( "-1", 2, -3L, 4.0 ), Optional.of( Arrays.asList( -1L, 2L, -3L, 4L ) ) } ); - testCases.add( new Object[] { "toDouble-combo-long-array", TO_DOUBLE, Arrays.asList( "-1", 2, -3L, 4.0 ), Optional.of( Arrays.asList( -1.0, 2.0, -3.0, 4.0 ) ) } ); - - testCases.add( new Object[] { "intsum-combo-string-array", INT_SUM_OF, Arrays.asList(1, 2.0, "random", 0), Optional.of(3)}); - testCases.add( new Object[] { "intsum-single-value", INT_SUM_OF, 2, Optional.empty()}); - testCases.add( new Object[] { "intsum-combo-intstring-array", INT_SUM_OF, Arrays.asList(1L, 2, "-3.0", 0), Optional.of(0)}); - - testCases.add( new Object[] { "doublesum-combo-string-array", DOUBLE_SUM_OF, Arrays.asList(1, 2.0, "random", 0), Optional.of(3.0)}); - testCases.add( new Object[] { "doublesum-single-value", DOUBLE_SUM_OF, 2, Optional.empty()}); - testCases.add( new Object[] { "doublesum-combo-intstring-array", DOUBLE_SUM_OF, Arrays.asList(1L, 2, "-3.0", 0), Optional.of(0.0)}); - - testCases.add( new Object[] { "longsum-combo-string-array", LONG_SUM_OF, Arrays.asList(1, 2.0, "random", 0), Optional.of(3L)}); - testCases.add( new Object[] { "longsum-single-value", LONG_SUM_OF, 2, Optional.empty()}); - testCases.add( new Object[] { "longsum-combo-intstring-array", LONG_SUM_OF, Arrays.asList(1L, 2, "-3.0", 0), Optional.of(0L)}); - - testCases.add( new Object[] { "intsubtract-happy-path", INT_SUBTRACT_OF, Arrays.asList(4, 1), Optional.of(3)}); - testCases.add( new Object[] { "intsubtract-single-value", INT_SUBTRACT_OF, 2, Optional.empty()}); - testCases.add( new Object[] { "intsubtract-wrong-type", INT_SUBTRACT_OF, Arrays.asList(4L, 1), Optional.empty()}); - - testCases.add( new Object[] { "doublesubtract-happy-path", DOUBLE_SUBTRACT_OF, Arrays.asList(4.0, 1.0), Optional.of(3.0)}); - testCases.add( new Object[] { "doublesubtract-single-value", DOUBLE_SUBTRACT_OF, 2.0, Optional.empty()}); - testCases.add( new Object[] { "doublesubtract-wrong-type", DOUBLE_SUBTRACT_OF, Arrays.asList(4L, 1), Optional.empty()}); - - testCases.add( new Object[] { "longsubtract-happy-path", LONG_SUBTRACT_OF, Arrays.asList(4L, 1L), Optional.of(3L)}); - testCases.add( new Object[] { "longsubtract-single-value", LONG_SUBTRACT_OF, 2L, Optional.empty()}); - testCases.add( new Object[] { "longsubtract-wrong-type", LONG_SUBTRACT_OF, Arrays.asList(4.0, 1), Optional.empty()}); - - // Test to make sure "div" only uses the first and second element in the array and ignores the rest. - testCases.add( new Object[] { "div-combo-array", DIV_OF, Arrays.asList(10L, 5.0, 2), Optional.empty()}); - testCases.add( new Object[] { "div-combo-string-array", DIV_OF, Arrays.asList(10L, "5", 2), Optional.empty()}); - testCases.add( new Object[] { "div-single-element-array", DIV_OF, Arrays.asList("5"), Optional.empty()}); - testCases.add( new Object[] { "div-single-element", DIV_OF, "10", Optional.empty()}); - - // Dividing by 0 returns an empty result. - testCases.add( new Object[] { "div-combo-invalid-array", DIV_OF, Arrays.asList(10L, 0, 2), Optional.empty()}); - - // Dividing 0 by any number returns 0.0(double) - testCases.add( new Object[] { "div-combo-valid-array", DIV_OF, Arrays.asList(0.0, 10), Optional.of(0.0)}); - - testCases.add( new Object[] { "divAndRound-single-precision-array", DIV_AND_ROUND_OF, Arrays.asList(1, 5.0, 2), Optional.of(2.5)}); - testCases.add( new Object[] { "divAndRound-double-precision-array", DIV_AND_ROUND_OF, Arrays.asList(2, 5.0, 2), Optional.of(2.50)}); - testCases.add( new Object[] { "divAndRound-trailing-precision-array", DIV_AND_ROUND_OF, Arrays.asList(3, 5.0, 2), Optional.of(2.500)}); - testCases.add( new Object[] { "divAndRound-no-precision-array", DIV_AND_ROUND_OF, Arrays.asList(0, 5.0, 2), Optional.of(3.0)}); // Round up as >= 0.5 - testCases.add( new Object[] { "divAndRound-no-precision-array", DIV_AND_ROUND_OF, Arrays.asList(0, 4.8, 2), Optional.of(2.0)}); // Round down as < 0.5 - - return testCases.iterator(); - } - - @Test - @SuppressWarnings( "all" ) - public void testNitPicks() { - // we want to be able to return the min/max element of input type, not - // autoboxed type -- wanted to return int (2), returned double (2.0) - Object c = (1.0 > 2 ? 1.0 : 2); - assert c.getClass() == Double.class && c.equals( 2.0 ); - - // toNumber parsing preference ordering (int-then-long-then-double) demo - assert toNumber("123").equals( Optional.of( 123 ) ); - assert toNumber("123123123123123123").equals( Optional.of( 123123123123123123l ) ); - assert toNumber("123123123123123123123123123123123123").equals( Optional.of( 123123123123123123123123123123123123d ) ); - - // abs returns numbers in their appropriate type, not given type (string in this case) - assert abs( "-123" ).equals( Optional.of( 123 )); - assert abs("-123123123123123123").equals( Optional.of( 123123123123123123l ) ); - assert abs("-123123123123123123123123123123123123").equals( Optional.of( 123123123123123123123123123123123123d ) ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/StringsTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/StringsTest.java deleted file mode 100644 index b6016458..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/StringsTest.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.modifier.function; - -import com.bazaarvoice.jolt.common.Optional; -import org.testng.annotations.DataProvider; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -@SuppressWarnings("deprecated") -public class StringsTest extends AbstractTester { - - @DataProvider(parallel = true) - public Iterator getTestCases() { - List testCases = new LinkedList<>( ); - - Function SPLIT = new Strings.split(); - - testCases.add( new Object[] {"split-invalid-null", SPLIT, null, Optional.empty() } ); - testCases.add( new Object[] {"split-invalid-string", SPLIT, "", Optional.empty() } ); - - testCases.add( new Object[] {"split-null-string", SPLIT, new Object[] {",", null}, Optional.empty() } ); - testCases.add( new Object[] {"split-null-separator", SPLIT, new Object[] {null, "test"}, Optional.empty() } ); - - testCases.add( new Object[] {"split-empty-string", SPLIT, new Object[] {",", ""}, Optional.of( Arrays.asList("") ) } ); - testCases.add( new Object[] {"split-single-token-string", SPLIT, new Object[] {",", "test"}, Optional.of( Arrays.asList("test") )} ); - - testCases.add( new Object[] {"split-double-token-string", SPLIT, new Object[] {",", "test,TEST"}, Optional.of( Arrays.asList("test", "TEST") )} ); - testCases.add( new Object[] {"split-multi-token-string", SPLIT, new Object[] {",", "test,TEST,Test,TeSt"}, Optional.of( Arrays.asList("test", "TEST", "Test", "TeSt") )} ); - testCases.add( new Object[] {"split-spaced-token-string", SPLIT, new Object[] {",", "test, TEST"}, Optional.of( Arrays.asList("test", " TEST") )} ); - testCases.add( new Object[] {"split-long-separator-spaced-token-string", SPLIT, new Object[] {", ", "test, TEST"}, Optional.of( Arrays.asList("test", "TEST") )} ); - - testCases.add( new Object[] {"split-regex-token-string", SPLIT, new Object[] {"[eE]", "test,TEST"}, Optional.of( Arrays.asList("t", "st,T", "ST") )} ); - testCases.add( new Object[] {"split-regex2-token-string", SPLIT, new Object[] {"\\s+", "test TEST Test TeSt"}, Optional.of( Arrays.asList("test", "TEST", "Test", "TeSt") )} ); - - return testCases.iterator(); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversrTest.java deleted file mode 100644 index 86e4737a..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrTraversrTest.java +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.shiftr; - -import com.bazaarvoice.jolt.JoltTestUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.common.Optional; -import com.bazaarvoice.jolt.traversr.Traversr; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class ShiftrTraversrTest { - - @DataProvider - public Object[][] inAndOutTestCases() throws Exception { - return new Object[][] { - { - "simple place", - Arrays.asList( "tuna" ), - "tuna", - "a.b", - Arrays.asList( "a", "b" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : \"tuna\" } }" ) - }, - { - "simple explicit array place", - Arrays.asList( "tuna" ), - null, - "a.b[]", - Arrays.asList( "a", "b", "[]" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ \"tuna\" ] } }" ) - }, - { - "simple explicit array place with sub", - Arrays.asList( "tuna" ), - null, - "a.b[].c", - Arrays.asList( "a", "b", "[]", "c" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ { \"c\" : \"tuna\" } ] } }" ) - }, - { - "simple array place", - Arrays.asList( "tuna" ), - "tuna", - "a.b.[1]", - Arrays.asList( "a", "b", "1" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ null, \"tuna\" ] } }" ) - }, - { - "nested array place", - Arrays.asList( "tuna" ), - "tuna", - "a.b[1].c", - Arrays.asList( "a", "b", "1", "c" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ null, { \"c\" : \"tuna\" } ] } }" ) - }, - { - "simple place into write array", - Arrays.asList( "tuna", "marlin" ), - Arrays.asList( "tuna", "marlin" ), - "a.b", - Arrays.asList( "a", "b" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ \"tuna\", \"marlin\" ] } }" ) - }, - { - "simple array place with nested write array", - Arrays.asList( "tuna", "marlin" ), - Arrays.asList( "tuna", "marlin" ), - "a.b.[1]", - Arrays.asList( "a", "b", "1" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ null, [ \"tuna\", \"marlin\" ] ] } }" ) - }, - { - "nested array place with nested ouptut array", - Arrays.asList( "tuna", "marlin" ), - Arrays.asList( "tuna", "marlin" ), - "a.b.[1].c", - Arrays.asList( "a", "b", "1", "c" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : [ null, { \"c\" : [ \"tuna\", \"marlin\"] } ] } }" ) - } - }; - } - - @Test(dataProvider = "inAndOutTestCases") - public void setTest(String testCaseName, List outputs, Object notUsedInThisTest, String traversrPath, List keys, Map expected) throws Exception - { - Map actual = new HashMap<>(); - - Traversr traversr = new ShiftrTraversr( traversrPath ); - for ( String output : outputs ) { - traversr.set( actual, keys, output ); - } - - JoltTestUtil.runDiffy( testCaseName, expected, actual ); - } - - @Test(dataProvider = "inAndOutTestCases") - public void getTest(String testCaseName, List notUsedInThisTest, Object expected, String traversrPath, List keys, Map tree) throws Exception - { - Traversr traversr = new ShiftrTraversr( traversrPath ); - Optional actual = traversr.get( tree, keys); - - JoltTestUtil.runDiffy( testCaseName, expected, actual.get() ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java deleted file mode 100644 index 4fd7f91f..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrUnitTest.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.shiftr; - -import com.bazaarvoice.jolt.JoltTestUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.Shiftr; -import com.bazaarvoice.jolt.common.PathElementBuilder; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.pathelement.TransposePathElement; -import com.bazaarvoice.jolt.exception.SpecException; -import com.google.common.base.Joiner; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -public class ShiftrUnitTest { - - @DataProvider - public Object[][] shiftrTestCases() throws IOException { - return new Object[][] { - { - "Simple * and Reference", - JsonUtils.jsonToMap("{ \"tuna-*-marlin-*\" : { \"rating-*\" : \"&(1,2).&.value\" } }"), - JsonUtils.jsonToMap("{ \"tuna-A-marlin-AAA\" : { \"rating-BBB\" : \"bar\" } }"), - JsonUtils.jsonToMap("{ \"AAA\" : { \"rating-BBB\" : { \"value\" : \"bar\" } } }") - }, - { - "Shift to two places", - JsonUtils.jsonToMap("{ \"tuna-*-marlin-*\" : { \"rating-*\" : [ \"&(1,2).&.value\", \"foo\"] } }"), - JsonUtils.jsonToMap("{ \"tuna-A-marlin-AAA\" : { \"rating-BBB\" : \"bar\" } }"), - JsonUtils.jsonToMap("{ \"foo\" : \"bar\", \"AAA\" : { \"rating-BBB\" : { \"value\" : \"bar\" } } }") - }, - { - "Or", - JsonUtils.jsonToMap("{ \"tuna|marlin\" : \"&-write\" }"), - JsonUtils.jsonToMap("{ \"tuna\" : \"snapper\" }"), - JsonUtils.jsonToMap("{ \"tuna-write\" : \"snapper\" }") - }, - { - "KeyRef", - JsonUtils.jsonToMap("{ \"rating-*\" : { \"&(0,1)\" : { \"match\" : \"&\" } } }"), - JsonUtils.jsonToMap("{ \"rating-a\" : { \"a\" : { \"match\": \"a-match\" }, \"random\" : { \"match\" : \"noise\" } }," + - " \"rating-c\" : { \"c\" : { \"match\": \"c-match\" }, \"random\" : { \"match\" : \"noise\" } } }"), - JsonUtils.jsonToMap("{ \"match\" : [ \"a-match\", \"c-match\" ] }") - }, - { - "Complex array write", - JsonUtils.jsonToMap("{ \"tuna-*-marlin-*\" : { \"rating-*\" : \"tuna[&(1,1)].marlin[&(1,2)].&(0,1)\" } }"), - JsonUtils.jsonToMap("{ \"tuna-2-marlin-3\" : { \"rating-BBB\" : \"bar\" }," + - "\"tuna-1-marlin-0\" : { \"rating-AAA\" : \"mahi\" } }"), - JsonUtils.jsonToMap("{ \"tuna\" : [ null, " + - " { \"marlin\" : [ { \"AAA\" : \"mahi\" } ] }, " + - " { \"marlin\" : [ null, null, null, { \"BBB\" : \"bar\" } ] } " + - " ] " + - " }") - } - }; - } - - @Test(dataProvider = "shiftrTestCases") - public void shiftrUnitTest(String testName, Map spec, Map data, Map expected) throws Exception { - - Shiftr shiftr = new Shiftr( spec ); - Object actual = shiftr.transform( data ); - - JoltTestUtil.runDiffy( testName, expected, actual ); - } - - - @DataProvider - public Object[][] badSpecs() throws IOException { - return new Object[][] { - { - "Null Spec", - null, - }, - { - "List Spec", - new ArrayList<>(), - }, - { - "Empty spec", - JsonUtils.jsonToMap( "{ }" ), - }, - { - "Empty sub-spec", - JsonUtils.javason( "{ 'tuna' : {} }" ), - }, - { - "Bad @", - JsonUtils.javason( "{ 'tuna-*-marlin-*' : { 'rating-@' : '&(1,2).&.value' } }" ), - }, - { - "RHS @ by itself", - JsonUtils.javason( "{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).@.value' } }" ), - }, - { - "RHS @ with bad Parens", - JsonUtils.javason( "{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).@(data.&(1,1).value' } }" ), - }, - { - "RHS *", - JsonUtils.javason( "{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).*.value' } }" ), - }, - { - "RHS $", - JsonUtils.javason( "{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).$.value' } }" ), - }, - { - "Two Arrays", - JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-*' : [ '&(1,2).photos[&(0,1)]-subArray[&(1,2)].value', 'foo'] } }"), - }, - { - "Can't mix * and & in the same key", - JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-&(1,2)-*' : [ '&(1,2).value', 'foo'] } }"), - }, - { - "Don't put negative numbers in array references", - JsonUtils.javason("{ 'tuna' : 'marlin[-1]' }"), - } - }; - } - - @Test(dataProvider = "badSpecs", expectedExceptions = SpecException.class) - public void failureUnitTest(String testName, Object spec) { - new Shiftr( spec ); - } - - /** - * @return canonical dotNotation String built from the given paths - */ - public String buildCanonicalString( List paths ) { - - List pathStrs = new ArrayList<>( paths.size() ); - for( PathElement pe : paths ) { - pathStrs.add( pe.getCanonicalForm() ); - } - - return Joiner.on(".").join( pathStrs ); - } - - - @DataProvider - public Object[][] validRHS() throws IOException { - return new Object[][]{ - { "@a", "@(0,a)" }, - { "@abc", "@(0,abc)" }, - { "@a.b.c", "@(0,a).b.c" }, - { "@(a.b\\.c)", "@(0,a.b\\.c)" }, - { "@a.b.c", "@(0,a).b.c" }, - { "@a.b.@c", "@(0,a).b.@(0,c)" }, - { "@(a[2].&).b.@c", "@(0,a.[2].&(0,0)).b.@(0,c)" }, - { "a[&2].@b[1].c", "a.[&(2,0)].@(0,b).[1].c" } - }; - } - - @Test(dataProvider = "validRHS" ) - public void validRHSTests( String dotNotation, String expected ) { - List paths = PathElementBuilder.parseDotNotationRHS( dotNotation ); - String actualCanonicalForm = buildCanonicalString( paths ); - - Assert.assertEquals( actualCanonicalForm, expected, "TestCase: " + dotNotation ); - } - - @Test - public void testTransposePathParsing() { - - List paths = PathElementBuilder.parseDotNotationRHS( "test.@(2,foo\\.bar)" ); - - Assert.assertEquals( paths.size(), 2 ); - TransposePathElement actualApe = (TransposePathElement) paths.get( 1 ); - - Assert.assertEquals( actualApe.getCanonicalForm(), "@(2,foo\\.bar)" ); - } - - @DataProvider - public Object[][] badRHS() throws IOException { - return new Object[][]{ - { "@" }, - { "a@" }, - { "@a@b" }, - { "@(a.b.&(2,2)" }, // missing trailing ) - { "@(a.b.&(2,2).d" }, // missing trailing ) - { "@(a.b.@c).d" }, - { "@(a.*.c)" }, // @ can not contain a * - { "@(a.$2.c)" }, // @ can not contain a $ - }; - } - - @Test(dataProvider = "badRHS", expectedExceptions = SpecException.class) - public void failureRHSTests( String dotNotation ) { - PathElementBuilder.parseDotNotationRHS( dotNotation ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrWritrTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrWritrTest.java deleted file mode 100644 index acb2878e..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/ShiftrWritrTest.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.shiftr; - -import com.bazaarvoice.jolt.common.PathElementBuilder; -import com.bazaarvoice.jolt.common.pathelement.AmpPathElement; -import com.bazaarvoice.jolt.common.pathelement.ArrayPathElement; -import com.bazaarvoice.jolt.common.pathelement.EvaluatablePathElement; -import com.bazaarvoice.jolt.common.pathelement.LiteralPathElement; -import com.bazaarvoice.jolt.common.pathelement.MatchablePathElement; -import com.bazaarvoice.jolt.common.pathelement.PathElement; -import com.bazaarvoice.jolt.common.reference.AmpReference; -import com.bazaarvoice.jolt.common.tree.MatchedElement; -import com.bazaarvoice.jolt.common.tree.WalkedPath; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.util.List; - -// Todo Now that the PathElement classes have been split out (no longer inner classes) -// each class should get a test -public class ShiftrWritrTest { - - @Test - public void referenceTest() { - - ShiftrWriter path = new ShiftrWriter( "SecondaryRatings.tuna-&(0,1)-marlin.Value" ); - - Assert.assertEquals( "SecondaryRatings", path.get( 0 ).getRawKey() ); - Assert.assertEquals( "SecondaryRatings", path.get( 0 ).toString() ); - Assert.assertEquals( "Value", path.get( 2 ).getRawKey() ); - Assert.assertEquals( "Value", path.get( 2 ).toString() ); - Assert.assertEquals( "Value", path.get( 2 ).toString() ); - - AmpPathElement refElement = (AmpPathElement) path.get( 1 ); - - Assert.assertEquals( 3, refElement.getTokens().size() ); - Assert.assertEquals( "tuna-", (String) refElement.getTokens().get(0) ); - Assert.assertEquals( "-marlin", (String) refElement.getTokens().get(2) ); - - Assert.assertTrue( refElement.getTokens().get(1) instanceof AmpReference ); - AmpReference ref = (AmpReference) refElement.getTokens().get(1); - Assert.assertEquals( 0, ref.getPathIndex() ); - Assert.assertEquals( 1, ref.getKeyGroup() ); - } - - @Test - public void arrayRefTest() { - - ShiftrWriter path = new ShiftrWriter( "ugc.photos-&1-bob[&2]" ); - - Assert.assertEquals( 3, path.size() ); - { // 0 - PathElement pe = path.get( 0 ); - Assert.assertTrue( pe instanceof LiteralPathElement, "First pathElement should be a literal one." ); - } - - { // 1 - PathElement pe = path.get( 1 ); - Assert.assertTrue( pe instanceof AmpPathElement, "Second pathElement should be a AmpPathElement." ); - - AmpPathElement refElement = (AmpPathElement) pe; - - Assert.assertEquals( 3, refElement.getTokens().size() ); - - { - Assert.assertTrue( refElement.getTokens().get(0) instanceof String ); - Assert.assertEquals( "photos-", (String) refElement.getTokens().get(0) ); - } - { - Assert.assertTrue( refElement.getTokens().get(1) instanceof AmpReference ); - AmpReference ref = (AmpReference) refElement.getTokens().get(1); - Assert.assertEquals( "&(1,0)", ref.getCanonicalForm() ); - Assert.assertEquals( 1, ref.getPathIndex() ); - Assert.assertEquals( 0, ref.getKeyGroup() ); - } - { - Assert.assertTrue( refElement.getTokens().get(2) instanceof String ); - Assert.assertEquals( "-bob", (String) refElement.getTokens().get(2) ); - } - } - - { // 2 - PathElement pe = path.get( 2 ); - Assert.assertTrue( pe instanceof ArrayPathElement, "Third pathElement should be a literal one." ); - - ArrayPathElement arrayElement = (ArrayPathElement) pe; - Assert.assertEquals( "[&(2,0)]", arrayElement.getCanonicalForm() ); - } - } - - @Test - public void calculateOutputTest_refsOnly() { - - MatchablePathElement pe1 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS( "tuna-*-marlin-*" ); - MatchablePathElement pe2 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS( "rating-*" ); - - MatchedElement lpe = pe1.match( "tuna-marlin", new WalkedPath() ); - Assert.assertNull( lpe ); - - lpe = pe1.match( "tuna-A-marlin-AAA", new WalkedPath() ); - Assert.assertEquals( "tuna-A-marlin-AAA", lpe.getRawKey() ); - Assert.assertEquals( "tuna-A-marlin-AAA", lpe.getSubKeyRef( 0 ) ); - Assert.assertEquals( 3, lpe.getSubKeyCount() ); - Assert.assertEquals( "A" , lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "AAA" , lpe.getSubKeyRef( 2 ) ); - - MatchedElement lpe2 = pe2.match( "rating-BBB", new WalkedPath( null, lpe ) ); - Assert.assertEquals( "rating-BBB", lpe2.getRawKey() ); - Assert.assertEquals( "rating-BBB", lpe2.getSubKeyRef( 0 ) ); - Assert.assertEquals( 2, lpe2.getSubKeyCount() ); - Assert.assertEquals( "BBB" , lpe2.getSubKeyRef( 1 ) ); - - ShiftrWriter outputPath = new ShiftrWriter( "&(1,2).&.value" ); - WalkedPath twoSteps = new WalkedPath( null, lpe ); - twoSteps.add( null, lpe2 ); - { - EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get( 0 ); - String evaledLeafOutput = outputElement.evaluate( twoSteps ); - Assert.assertEquals( "AAA", evaledLeafOutput ); - } - { - EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get( 1 ); - String evaledLeafOutput = outputElement.evaluate( twoSteps ); - Assert.assertEquals( "rating-BBB", evaledLeafOutput ); - } - { - EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get( 2 ); - String evaledLeafOutput = outputElement.evaluate( twoSteps ); - Assert.assertEquals( "value", evaledLeafOutput ); - } - } - - @Test - public void calculateOutputTest_arrayIndexes() { - - // simulate Shiftr LHS specs - MatchablePathElement pe1 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS( "tuna-*-marlin-*" ); - MatchablePathElement pe2 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS( "rating-*" ); - - // match them against some data to get LiteralPathElements with captured values - MatchedElement lpe = pe1.match( "tuna-2-marlin-3", new WalkedPath() ); - Assert.assertEquals( "2" , lpe.getSubKeyRef( 1 ) ); - Assert.assertEquals( "3" , lpe.getSubKeyRef( 2 ) ); - - MatchedElement lpe2 = pe2.match( "rating-BBB", new WalkedPath( null, lpe ) ); - Assert.assertEquals( 2, lpe2.getSubKeyCount() ); - Assert.assertEquals( "BBB" , lpe2.getSubKeyRef( 1 ) ); - - // Build an write path path - ShiftrWriter shiftrWriter = new ShiftrWriter( "tuna[&(1,1)].marlin[&(1,2)].&(0,1)" ); - - Assert.assertEquals( 5, shiftrWriter.size() ); - Assert.assertEquals( "tuna.[&(1,1)].marlin.[&(1,2)].&(0,1)", shiftrWriter.getCanonicalForm() ); - - // Evaluate the write path against the LiteralPath elements we build above ( like Shiftr does ) - WalkedPath twoSteps = new WalkedPath( null, lpe ); - twoSteps.add( null, lpe2 ); - List stringPath = shiftrWriter.evaluate( twoSteps ); - - Assert.assertEquals( "tuna", stringPath.get( 0 ) ); - Assert.assertEquals( "2", stringPath.get( 1 ) ); - Assert.assertEquals( "marlin", stringPath.get( 2 ) ); - Assert.assertEquals( "3", stringPath.get( 3 ) ); - Assert.assertEquals( "BBB", stringPath.get( 4 ) ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/spec/KeyOrderingTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/spec/KeyOrderingTest.java deleted file mode 100644 index efb18831..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/spec/KeyOrderingTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.shiftr.spec; - -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.SpecDriven; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; -import java.util.Map; - -public class KeyOrderingTest { - - @DataProvider - public Object[][] shiftrKeyOrderingTestCases() throws IOException { - return new Object[][] { - { - "Simple * and &", - JsonUtils.jsonToMap( "{ \"*\" : { \"a\" : \"b\" }, \"&\" : { \"a\" : \"b\" } }" ), - Arrays.asList( "&(0,0)", "*" ) - }, - { - "2* and 2&", - JsonUtils.jsonToMap( "{ \"rating-*\" : { \"a\" : \"b\" }, \"rating-range-*\" : { \"a\" : \"b\" }, \"&\" : { \"a\" : \"b\" }, \"tuna-&(0)\" : { \"a\" : \"b\" } }" ), - Arrays.asList( "tuna-&(0,0)", "&(0,0)", "rating-range-*", "rating-*" ) - }, - { - "2& alpha-number based fallback", - JsonUtils.jsonToMap( "{ \"&\" : { \"a\" : \"b\" }, \"&(0,1)\" : { \"a\" : \"b\" } }" ), - Arrays.asList( "&(0,0)", "&(0,1)" ) - }, - { - "2* and 2& alpha fallback", - JsonUtils.jsonToMap( "{ \"aaaa-*\" : { \"a\" : \"b\" }, \"bbbb-*\" : { \"a\" : \"b\" }, \"aaaa-&\" : { \"a\" : \"b\" }, \"bbbb-&(0)\" : { \"a\" : \"b\" } }" ), - Arrays.asList( "aaaa-&(0,0)", "bbbb-&(0,0)", "aaaa-*", "bbbb-*" ) - } - }; - } - - @Test(dataProvider = "shiftrKeyOrderingTestCases" ) - public void testKeyOrdering( String testName, Map spec, List expectedOrder ) { - - ShiftrCompositeSpec root = new ShiftrCompositeSpec( SpecDriven.ROOT_KEY, spec ); - - for ( int index = 0; index < expectedOrder.size(); index++) { - String expected = expectedOrder.get( index ); - Assert.assertEquals( expected, root.getComputedChildren().get( index ).pathElement.getCanonicalForm(), testName ); - } - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/spec/SpecParsingTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/spec/SpecParsingTest.java deleted file mode 100644 index 86ef99d7..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/shiftr/spec/SpecParsingTest.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2015 Bazaarvoice, 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.bazaarvoice.jolt.shiftr.spec; - -import com.bazaarvoice.jolt.common.SpecStringParser; -import com.google.common.collect.Lists; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Arrays; -import java.util.List; - -public class SpecParsingTest { - - @DataProvider - public Object[][] RHSParsingTestsRemoveEscapes() throws IOException { - return new Object[][] { - { - "simple, no escape", - "a.b.c", - Arrays.asList( "a", "b", "c" ), - }, - { - "ref and array, no escape", - "a.&(1,2).[]", - Arrays.asList( "a", "&(1,2)", "[]" ) - }, - { - "single transpose, no escape", - "a.@(l.m.n).c", - Arrays.asList( "a", "@(l.m.n)", "c" ) - }, - { - "non-special char escape passes thru", - "a\\\\bc.def", - Arrays.asList( "a\\bc", "def" ) - }, - { - "single escape", - "a\\.b.c", - Arrays.asList( "a.b", "c" ) - }, - { - "escaping rhs", - "data.\\\\$rating-&1", - Arrays.asList( "data", "\\$rating-&1" ) - }, - { - "@Class example", - "a.@Class.c", - Arrays.asList( "a", "@(Class)", "c" ) - } - }; - } - - @Test(dataProvider = "RHSParsingTestsRemoveEscapes") - public void testRHSParsingRemoveEscapes( String testName, String unSweetendDotNotation, List expected ) { - - List actual = SpecStringParser.parseDotNotation( Lists.newArrayList(), SpecStringParser.stringIterator( unSweetendDotNotation ), unSweetendDotNotation ); - - Assert.assertEquals( actual, expected, "Failed test name " + testName ); - } - - @DataProvider - public Object[][] removeEscapeCharsTests() throws IOException { - - return new Object[][] { - { "starts with escape", "\\@pants", "@pants" }, - { "escape in the middle", "rating-\\&pants", "rating-&pants" }, - { "escape the escape char", "rating\\\\pants", "rating\\pants" }, - }; - } - - @Test(dataProvider = "removeEscapeCharsTests" ) - public void testRemoveEscapeChars( String testName, String input, String expected ) { - - String actual = SpecStringParser.removeEscapeChars( input ); - Assert.assertEquals( actual, expected, "Failed test name " + testName ); - } - - - @DataProvider - public Object[][] removeEscapedValuesTest() throws IOException { - - return new Object[][] { - { "starts with escape", "\\@pants", "pants" }, - { "escape in the middle", "rating-\\&pants", "rating-pants" }, - { "escape the escape char", "rating\\\\pants", "ratingpants" }, - { "escape the array", "\\[\\]pants", "pants" }, - }; - } - - @Test(dataProvider = "removeEscapedValuesTest" ) - public void testEscapeParsing( String testName, String input, String expected ) { - - String actual = SpecStringParser.removeEscapedValues( input ); - Assert.assertEquals( actual, expected, "Failed test name " + testName ); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/traversr/SimpleTraversalTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/traversr/SimpleTraversalTest.java deleted file mode 100644 index 1dae4d5b..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/traversr/SimpleTraversalTest.java +++ /dev/null @@ -1,224 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.traversr; - -import com.bazaarvoice.jolt.JoltTestUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.common.Optional; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class SimpleTraversalTest { - - @DataProvider - public Object[][] inAndOutTestCases() throws Exception { - return new Object[][] { - { - "Simple Map Test", - SimpleTraversal.newTraversal( "a.b" ), - JsonUtils.jsonToMap( "{ \"a\" : null }" ), - JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : \"tuna\" } }" ), - "tuna" - }, - { - "Simple explicit array test", - SimpleTraversal.newTraversal( "a.[1].b" ), - JsonUtils.jsonToMap( "{ \"a\" : null }" ), - JsonUtils.jsonToMap( "{ \"a\" : [ null, { \"b\" : \"tuna\" } ] }" ), - "tuna" - }, - { - "Leading Array test", - SimpleTraversal.newTraversal( "[0].a" ), - JsonUtils.jsonToObject( "[ ]" ), - JsonUtils.jsonToObject( "[ { \"a\" : \"b\" } ]" ), - "b" - }, - { - "Auto expand array test", - SimpleTraversal.newTraversal( "a.[].b" ), - JsonUtils.jsonToMap( "{ \"a\" : null }" ), - JsonUtils.jsonToMap( "{ \"a\" : [ { \"b\" : null } ] }" ), - null - } - }; - } - - @Test( dataProvider = "inAndOutTestCases") - public void getTests( String testDescription, SimpleTraversal simpleTraversal, Object ignoredForTest, Object input, String expected ) throws IOException { - - Object original = JsonUtils.cloneJson( input ); - Object tree = JsonUtils.cloneJson( input ); - - Optional actual = simpleTraversal.get( tree ); - - Assert.assertEquals( expected, actual.get() ); - JoltTestUtil.runDiffy( "Get should not have modified the input", original, tree ); - } - - @Test( dataProvider = "inAndOutTestCases") - public void setTests( String testDescription, SimpleTraversal simpleTraversal, Object start, Object expected, String toSet ) { - - Object actual = JsonUtils.cloneJson( start ); - - Assert.assertEquals( toSet, simpleTraversal.set( actual, toSet ).get() ); // set should be successful - - Assert.assertEquals( expected, actual ); - } - - @Test - public void testAutoArray() throws IOException - { - SimpleTraversal traversal = SimpleTraversal.newTraversal( "a.[].b" ); - - Object expected = JsonUtils.jsonToMap( "{ \"a\" : [ { \"b\" : \"one\" }, { \"b\" : \"two\" } ] }" ); - - Object actual = new HashMap(); - - Assert.assertFalse( traversal.get( actual ).isPresent() ); - Assert.assertEquals( 0, ((HashMap) actual).size() ); // get didn't add anything - - // Add two things and validate the Auto Expand array - Assert.assertEquals( "one", traversal.set( actual, "one" ).get() ); - Assert.assertEquals( "two", traversal.set( actual, "two" ).get() ); - - JoltTestUtil.runDiffy( expected, actual ); - } - - @Test - public void testOverwrite() throws IOException - { - SimpleTraversal traversal = SimpleTraversal.newTraversal( "a.b" ); - - Object actual = JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : \"tuna\" } }" ); - Object expectedOne = JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : \"one\" } }" ); - Object expectedTwo = JsonUtils.jsonToMap( "{ \"a\" : { \"b\" : \"two\" } }" ); - - Assert.assertEquals( "tuna", traversal.get( actual ).get() ); - - // Set twice and verify that the sets did in fact overwrite - Assert.assertEquals( "one", traversal.set( actual, "one" ).get() ); - JoltTestUtil.runDiffy( expectedOne, actual ); - - Assert.assertEquals( "two", traversal.set( actual, "two" ).get() ); - JoltTestUtil.runDiffy( expectedTwo, actual ); - } - - @DataProvider - public Object[][] removeTestCases() throws Exception { - return new Object[][] { - { - "Inception Map Test", - SimpleTraversal.newTraversal( "__queryContext" ), - JsonUtils.javason( "{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }" ), - JsonUtils.javason( "{ 'Id' : '1234' }" ), - JsonUtils.javason( "{ 'catalogLin' : [ 'a', 'b' ] }" ) - }, - { - "List Test", - SimpleTraversal.newTraversal( "a.list.[1]" ), - JsonUtils.javason( "{ 'a' : { 'list' : [ 'a', 'b', 'c' ] } }" ), - JsonUtils.javason( "{ 'a' : { 'list' : [ 'a', 'c' ] } }" ), - "b" - }, - { - "Map leave empty Map", - SimpleTraversal.newTraversal( "a.list" ), - JsonUtils.javason( "{ 'a' : { 'list' : [ 'a', 'b', 'c' ] } }" ), - JsonUtils.javason( "{ 'a' : { } }" ), - Arrays.asList( "a","b","c" ) - }, - { - "Map leave empty List", - SimpleTraversal.newTraversal( "a.list.[0]" ), - JsonUtils.javason( "{ 'a' : { 'list' : [ 'a' ] } }" ), - JsonUtils.javason( "{ 'a' : { 'list' : [ ] } }" ), - "a" - } - }; - } - - @Test( dataProvider = "removeTestCases") - public void removeTests( String testDescription, SimpleTraversal simpleTraversal, - Object start, Object expectedLeft, Object expectedReturn ) - throws Exception - { - - Optional actualRemoveOpt = simpleTraversal.remove( start ); - JoltTestUtil.runDiffy( testDescription, expectedReturn, actualRemoveOpt.get() ); - - JoltTestUtil.runDiffy( testDescription, expectedLeft, start ); - } - - @Test(expectedExceptions = ClassCastException.class) - public void exceptionTestListIsMap() throws Exception - { - Object tree = JsonUtils.javason( "{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }" ); - - SimpleTraversal trav = SimpleTraversal.newTraversal( "__queryContext" ); - // barfs here, needs the 'List list =' part to trigger it - @SuppressWarnings( "unused" ) - List list = trav.get( tree ).get(); - } - - @Test(expectedExceptions = ClassCastException.class) - public void exceptionTestMapIsList() throws Exception - { - Object tree = JsonUtils.javason( "{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }" ); - - SimpleTraversal trav = SimpleTraversal.newTraversal( "__queryContext.catalogLin" ); - // barfs here, needs the 'Map map =' part to trigger it - @SuppressWarnings( "unused" ) - Map map = trav.get( tree ).get(); - } - - @Test(expectedExceptions = ClassCastException.class) - public void exceptionTestListIsMapErasure() throws Exception - { - Object tree = JsonUtils.javason( "{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }" ); - - SimpleTraversal> trav = SimpleTraversal.newTraversal( "__queryContext" ); - // this works - Map queryContext = trav.get( tree ).get(); - - // this does not - @SuppressWarnings( "unused" ) - Map catalogLin = queryContext.get( "catalogLin" ); - Assert.fail( "Expected ClassCast Exception"); - } - - @Test(expectedExceptions = ClassCastException.class) - public void exceptionTestLMapIsListErasure() throws Exception - { - Object tree = JsonUtils.javason( "{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : { 'a' : 'b' } } }" ); - - SimpleTraversal> trav = SimpleTraversal.newTraversal( "__queryContext" ); - // this works - Map queryContext = trav.get( tree ).get(); - - // this does not - @SuppressWarnings( "unused" ) - List catalogLin = queryContext.get( "catalogLin" ); - Assert.fail( "Expected ClassCast Exception"); - } -} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsNavigateTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsNavigateTest.java deleted file mode 100644 index 513878cc..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsNavigateTest.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.utils; - -import com.bazaarvoice.jolt.JsonUtils; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import java.util.List; -import java.util.Map; - -import static com.bazaarvoice.jolt.utils.JoltUtils.navigate; -import static com.bazaarvoice.jolt.utils.JoltUtils.navigateOrDefault; -import static com.bazaarvoice.jolt.utils.JoltUtils.navigateStrict; - -public class JoltUtilsNavigateTest { - - private Object jsonSource; - private Object jsonSource_empty; - - @BeforeClass - public void setup() { - - String jsonSourceString = "{ " + - " 'a': { " + - " 'b': [ 0, 1, 2, 1.618 ] " + - " }, " + - " 'p': [ 'm', 'n', " + - " { " + - " '1': 1, " + - " '2': 2, " + - " 'pi': 3.14159 " + - " } " + - " ], " + - " 'x': 'y' " + - "}\n"; - - jsonSource = JsonUtils.javason(jsonSourceString); - - String jsonSourceString_empty = - "{" + - "'e': { 'f': {}, 'g': [] }," + - "'h': [ {}, [] ]" + - "}"; - - jsonSource_empty = JsonUtils.javason(jsonSourceString_empty); - } - - - @DataProvider (parallel = true) - public Object[][] validNavigateTests() { - - return new Object[][] { - - { 0, new Object[] {"a", "b", 0}}, - { 1, new Object[] {"a", "b", 1}}, - { 2, new Object[] {"a", "b", 2}}, - { 1.618, new Object[] {"a", "b", 3}}, - { "m", new Object[] {"p", 0} }, - { "n", new Object[] {"p", 1}}, - { 1, new Object[] {"p", 2, "1"}}, - { 2, new Object[] {"p", 2, "2"}}, - { 3.14159, new Object[] {"p", 2, "pi"}}, - { "y", new Object[] {"x"}}, - - { ((Map) jsonSource).get("a"), new Object[] {"a"}}, - { ((Map)(((Map) jsonSource).get("a"))).get("b"), new Object[] {"a", "b"}}, - { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(0), new Object[] {"a", "b", 0}}, - { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(1), new Object[] {"a", "b", 1}}, - { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(2), new Object[] {"a", "b", 2}}, - { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(3), new Object[] {"a", "b", 3}}, - { ((Map) jsonSource).get("p"), new Object[] {"p"}}, - { ((List)(((Map) jsonSource).get("p"))).get(0), new Object[] {"p", 0}}, - { ((List)(((Map) jsonSource).get("p"))).get(1), new Object[] {"p", 1}}, - { ((List)(((Map) jsonSource).get("p"))).get(2), new Object[] {"p", 2}}, - { ((Map)((List)(((Map) jsonSource).get("p"))).get(2)).get("1"), new Object[] {"p", 2, "1"}}, - { ((Map)((List)(((Map) jsonSource).get("p"))).get(2)).get("2"), new Object[] {"p", 2, "2"}}, - { ((Map)((List)(((Map) jsonSource).get("p"))).get(2)).get("pi"), new Object[] {"p", 2, "pi"}}, - - { ((Map) jsonSource).get("x"), new Object[] {"x"} }, - }; - } - - @Test (dataProvider = "validNavigateTests" ) - public void navigate_happy_tests(Object expected, Object[] path) { - Object actual = navigate(jsonSource, path); - Assert.assertEquals(actual, expected); - } - - @Test (dataProvider = "validNavigateTests" ) - public void navigateStrict_happy_tests(Object expected, Object[] path) { - Object actual = navigateStrict(jsonSource, path); - Assert.assertEquals(actual, expected); - } - - @Test (dataProvider = "validNavigateTests" ) - public void navigateOrDefault_happy_tests(Object expected, Object[] path) { - Object actual = navigateOrDefault(null, jsonSource, path); - Assert.assertEquals(actual, expected); - } - - - - @Test( expectedExceptions = UnsupportedOperationException.class ) - public void navigateStrictThrowsException() { - Object actual = navigateStrict(jsonSource, "pants", "shoes"); - Assert.fail( "Should have thrown an Exception" ); - } - - - @DataProvider (parallel = true) - public Object[][] navigateOrDefault_testCases() { - - return new Object[][] { - - { new Object[] {"a", "b" }}, // verify that trying to read from two nested that don't exist works - { new Object[] {"h", -3 }}, // verify that trying to read from an existing list with a negative index does not blow up - { new Object[] {"h", 4 }}, // verify that trying to read from an existing list with a index bigger that the list does not blow up - }; - } - - @Test (dataProvider = "navigateOrDefault_testCases" ) - public void navigatorSafe(Object[] path) { - - Object actual = navigateOrDefault( "pants", jsonSource_empty, path); - Assert.assertEquals(actual, "pants"); - } -} \ No newline at end of file diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsRemoveTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsRemoveTest.java deleted file mode 100644 index 60037862..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsRemoveTest.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.utils; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtils; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import org.testng.Assert; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import org.testng.collections.Lists; - -import java.util.List; -import java.util.Map; - -public class JoltUtilsRemoveTest { - - private Diffy diffy = new Diffy(); - - private Map ab = ImmutableMap.builder().put( "a", "b" ).build(); - private Map cd = ImmutableMap.builder().put( "c", "d" ).build(); - private Map top = ImmutableMap.builder().put( "A", ab ).put( "B", cd ).build(); - - @DataProvider - public Object[][] removeRecursiveCases() { - - Map empty = ImmutableMap.builder().build(); - Map barToFoo = ImmutableMap.builder().put( "bar", "foo" ).build(); - Map fooToBar = ImmutableMap.builder().put( "foo", "bar" ).build(); - return new Object[][] { - { null, null, null }, - { null, "foo", null }, - { "foo", null, "foo" }, - { "foo", "foo", "foo" }, - { Maps.newHashMap(), "foo", empty }, - { Maps.newHashMap( barToFoo ), "foo", barToFoo }, - { Maps.newHashMap( fooToBar ), "foo", empty }, - { Lists.newArrayList(), "foo", ImmutableList.builder().build() }, - { - Lists.newArrayList( ImmutableList.builder() - .add( Maps.newHashMap( barToFoo ) ) - .build() ), - "foo", - ImmutableList.builder() - .add( barToFoo ) - .build() - }, - { - Lists.newArrayList( ImmutableList.builder() - .add( Maps.newHashMap( fooToBar ) ) - .build() ), - "foo", - ImmutableList.builder() - .add( empty ) - .build() - } - }; - } - - @Test(dataProvider = "removeRecursiveCases") - public void testRemoveRecursive(Object json, String key, Object expected) { - - JoltUtils.removeRecursive( json, key ); - - Diffy.Result result = diffy.diff( expected, json ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } - - @Test - public void runFixtureTests() { - - String testFixture = "/json/utils/joltUtils-removeRecursive.json"; - @SuppressWarnings("unchecked") - List> tests = (List>) JsonUtils.classpathToObject( testFixture ); - - for ( Map testUnit : tests ) { - - Object data = testUnit.get( "input" ); - String toRemove = (String) testUnit.get( "remove" ); - Object expected = testUnit.get( "expected" ); - - JoltUtils.removeRecursive( data, toRemove ); - - Diffy.Result result = diffy.diff( expected, data ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); - } - } - } - - @Test( description = "No exception if we don't try to remove from an ImmutableMap.") - public void doNotUnnecessarilyDieOnImmutableMaps() - { - Map expected = JsonUtils.jsonToMap( JsonUtils.toJsonString( top ) ); - - JoltUtils.removeRecursive( top, "tuna" ); - - Diffy.Result result = diffy.diff( expected, top ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); - } - } - - @Test( expectedExceptions = UnsupportedOperationException.class, description = "Exception if try to remove from an Immutable map.") - public void correctExceptionWithImmutableMap() - { - JoltUtils.removeRecursive( top, "c" ); - } -} \ No newline at end of file diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsSquashTest.java b/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsSquashTest.java deleted file mode 100644 index 3fd4ef98..00000000 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsSquashTest.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt.utils; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.modifier.function.Objects; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class JoltUtilsSquashTest { - - private Diffy diffy = new Diffy(); - - @Test - public void squashNullsInAListTest() { - List actual = new ArrayList(); - actual.addAll( Arrays.asList( "a", null, 1, null, "b", 2) ); - - List expectedList = Arrays.asList( "a", 1, "b", 2); - - Objects.squashNulls( actual ); - - Diffy.Result result = diffy.diff( expectedList, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } - - @Test - public void squashNullsInAMapTest() { - Map actual = new HashMap<>(); - actual.put( "a", 1 ); - actual.put( "b", null ); - actual.put( "c", "C" ); - - Map expectedMap = new HashMap<>(); - expectedMap.put( "a", 1 ); - expectedMap.put( "c", "C" ); - - Objects.squashNulls( actual ); - - Diffy.Result result = diffy.diff( expectedMap, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } - - - @Test - public void recursivelySquashNullsTest() - { - Map actual = JsonUtils.javason( "{ 'a' : 1, 'b' : null, 'c' : [ null, 4, null, 5, { 'x' : 'X', 'y' : null } ] }" ); - Map expected = JsonUtils.javason( "{ 'a' : 1, 'c' : [ 4, 5, { 'x' : 'X' } ] }" ); - - Objects.recursivelySquashNulls( actual ); - - Diffy.Result result = diffy.diff( expected, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } -} \ No newline at end of file diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/CardinalityTransformTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/CardinalityTransformTest.java similarity index 57% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/CardinalityTransformTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/CardinalityTransformTest.java index 778587bc..e7f2e5ea 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/CardinalityTransformTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/CardinalityTransformTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -24,56 +26,83 @@ import java.util.Map; public class CardinalityTransformTest { + @DataProvider + public Object[][] invalidSpecs() { + return new Object[][]{ + {null}, + {"InvalidSpecType"}, + {new HashMap<>()} + }; + } + + @Test(dataProvider = "invalidSpecs", expectedExceptions = SpecException.class) + public void throwsSpecExceptionForInvalidSpecs(Object invalidSpec) { + new CardinalityTransform(invalidSpec); + } + + @Test + public void initializesSuccessfullyWithValidSpec() { + Map validSpec = new HashMap<>() {{ + put("key", "ONE"); + }}; + CardinalityTransform transform = new CardinalityTransform(validSpec); + Assert.assertNotNull(transform); + } @DataProvider public Object[][] getTestCaseUnits() { - return new Object[][] { + return new Object[][]{ {"oneLiteralTestData"}, {"manyLiteralTestData"}, {"starTestData"}, + {"starRegexTestData"}, + {"thisLevelIsNull"}, + {"exceptionScalarInput"}, + {"scalarInputData"}, + {"nullScalarInputData"}, {"atTestData"} }; } - @Test (dataProvider = "getTestCaseUnits") + @Test(dataProvider = "getTestCaseUnits") public void runTestUnits(String testCaseName) throws IOException { String testPath = "/json/cardinality/" + testCaseName; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); - Object input = testUnit.get( "input" ); - Object spec = testUnit.get( "spec" ); - Object expected = testUnit.get( "expected" ); + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); - CardinalityTransform cardinalityTransform = new CardinalityTransform( spec ); - Object actual = cardinalityTransform.transform( input ); + CardinalityTransform cardinalityTransform = new CardinalityTransform(spec); + Object actual = cardinalityTransform.transform(input); - JoltTestUtil.runDiffy( "failed case " + testPath, expected, actual ); + JoltTestUtil.runDiffy("failed case " + testPath, expected, actual); } - @Test(expectedExceptions=SpecException.class) + @Test(expectedExceptions = SpecException.class) public void testSpecExceptions() throws IOException { String testPath = "/json/cardinality/failCardinalityType"; - Map testUnit = JsonUtils.classpathToMap( testPath + ".json" ); + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); - Object spec = testUnit.get( "spec" ); + Object spec = testUnit.get("spec"); // Should throw exception - new CardinalityTransform( spec ); + new CardinalityTransform(spec); } @Test public void testArrayCardinalityOne() throws IOException { // The above tests cover cardinality on elements that are Lists, this test covers elements that are arrays - Map input = new HashMap() {{ + Map input = new HashMap<>() {{ put("input", new Integer[]{5, 4}); }}; - Map spec = new HashMap() {{ + Map spec = new HashMap<>() {{ put("input", "ONE"); }}; - Map expected = new HashMap() {{ + Map expected = new HashMap<>() {{ put("input", 5); }}; @@ -85,15 +114,15 @@ public void testArrayCardinalityOne() throws IOException { @Test public void testArrayCardinalityMany() throws IOException { // The above tests cover cardinality on elements that are Lists, this test covers elements that are arrays - Map input = new HashMap() {{ + Map input = new HashMap<>() {{ put("input", new Integer[]{5, 4}); }}; - Map spec = new HashMap() {{ + Map spec = new HashMap<>() {{ put("input", "MANY"); }}; - Map expected = new HashMap() {{ + Map expected = new HashMap<>() {{ put("input", new Integer[]{5, 4}); }}; diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrContextTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/ChainrContextTest.java similarity index 51% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrContextTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/ChainrContextTest.java index 3ec74a42..29cd2e54 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/ChainrContextTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/ChainrContextTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import com.google.common.collect.Lists; import org.testng.Assert; @@ -31,36 +32,36 @@ public class ChainrContextTest { public Iterator getTests() { String testPath = "/json/chainr/context/spec_with_context.json"; - Map testSuite = JsonUtils.classpathToMap( testPath ); + Map testSuite = JsonUtils.classpathToMap(testPath); - Object spec = testSuite.get( "spec" ); - List tests = (List) testSuite.get( "tests" ); + Object spec = testSuite.get("spec"); + List tests = (List) testSuite.get("tests"); List accum = Lists.newLinkedList(); - for ( Map testCase : tests ) { + for (Map testCase : tests) { - String testCaseName = (String) testCase.get( "testCaseName" ); - Object input = testCase.get( "input" ); - Map context = (Map) testCase.get( "context" ); - Object expected = testCase.get( "expected" ); + String testCaseName = (String) testCase.get("testCaseName"); + Object input = testCase.get("input"); + Map context = (Map) testCase.get("context"); + Object expected = testCase.get("expected"); - accum.add( new Object[] { testCaseName, spec, input, context, expected } ); + accum.add(new Object[]{testCaseName, spec, input, context, expected}); } return accum.iterator(); } - @Test( dataProvider = "getTests" ) - public void successCase( String testCaseName, Object spec, Object input, Map context, Object expected ) throws IOException { + @Test(dataProvider = "getTests") + public void successCase(String testCaseName, Object spec, Object input, Map context, Object expected) throws IOException { - Chainr unit = Chainr.fromSpec( spec ); + Chainr unit = Chainr.fromSpec(spec); - Assert.assertTrue( unit.hasContextualTransforms() ); - Assert.assertEquals( unit.getContextualTransforms().size(), 2 ); + Assert.assertTrue(unit.hasContextualTransforms()); + Assert.assertEquals(unit.getContextualTransforms().size(), 2); - Object actual = unit.transform( input, context ); + Object actual = unit.transform(input, context); - JoltTestUtil.runDiffy( "failed case " + testCaseName, expected, actual ); + JoltTestUtil.runDiffy("failed case " + testCaseName, expected, actual); } } diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/ChainrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/ChainrTest.java new file mode 100644 index 00000000..580b6f90 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/ChainrTest.java @@ -0,0 +1,290 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.chainr.transforms.ExplodingTestTransform; +import io.joltcommunity.jolt.chainr.transforms.GoodTestTransform; +import io.joltcommunity.jolt.chainr.transforms.TransformTestResult; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.TransformException; +import com.google.common.collect.ImmutableList; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ChainrTest { + + private List> newChainrSpec() { + return new ArrayList<>(); + } + + private Map newActivity(String opname) { + Map activity = new HashMap<>(); + activity.put(ChainrEntry.OPERATION_KEY, opname); + return activity; + } + + private Map newActivity(String operation, Object spec) { + Map activity = new HashMap<>(); + activity.put(ChainrEntry.OPERATION_KEY, operation); + if (spec != null) { + activity.put(ChainrEntry.SPEC_KEY, spec); + } + return activity; + } + + private Map newCustomJavaActivity(Class cls, Object spec) { + Map activity = new HashMap<>(); + activity.put(ChainrEntry.OPERATION_KEY, cls.getName()); + if (spec != null) { + activity.put(ChainrEntry.SPEC_KEY, spec); + } + + return activity; + } + + private List> newCustomJavaChainrSpec(Class cls, Object delegateSpec) { + List> retvalue = this.newChainrSpec(); + retvalue.add(newCustomJavaActivity(cls, delegateSpec)); + return retvalue; + } + + private List> newShiftrChainrSpec(Object shiftrSpec) { + List> retvalue = this.newChainrSpec(); + retvalue.add(newActivity("shift", shiftrSpec)); + return retvalue; + } + + private List> newShiftrDefaultrSpec(Object defaultrSpec) { + List> retvalue = this.newChainrSpec(); + retvalue.add(newActivity("default", defaultrSpec)); + return retvalue; + } + + private List> newShiftrRemovrSpec(Object removrSpec) { + List> retvalue = this.newChainrSpec(); + retvalue.add(newActivity("remove", removrSpec)); + return retvalue; + } + + private List> newShiftrSortrSpec(Object sortrSpec) { + List> retvalue = this.newChainrSpec(); + retvalue.add(newActivity("sort", sortrSpec)); + return retvalue; + } + + @Test + public void process_itCallsShiftr() throws IOException { + Map testUnit = JsonUtils.classpathToMap("/json/shiftr/queryMappingXform.json"); + + Object input = testUnit.get("input"); + Object shiftrSpec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Object chainrSpec = this.newShiftrChainrSpec(shiftrSpec); + + Chainr unit = Chainr.fromSpec(chainrSpec); + Object actual = unit.transform(input, null); + + JoltTestUtil.runDiffy("failed Shiftr call.", expected, actual); + } + + @Test + public void process_itCallsDefaultr() throws IOException { + Map testUnit = JsonUtils.classpathToMap("/json/defaultr/firstSample.json"); + + Object input = testUnit.get("input"); + Object defaultrSpec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Object chainrSpec = this.newShiftrDefaultrSpec(defaultrSpec); + + Chainr unit = Chainr.fromSpec(chainrSpec); + Object actual = unit.transform(input, null); + + JoltTestUtil.runDiffy("failed Defaultr call.", expected, actual); + } + + @Test + public void process_itCallsRemover() throws IOException { + Map testUnit = JsonUtils.classpathToMap("/json/removr/firstSample.json"); + + Object input = testUnit.get("input"); + Object removrSpec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Object chainrSpec = this.newShiftrRemovrSpec(removrSpec); + + Chainr unit = Chainr.fromSpec(chainrSpec); + Object actual = unit.transform(input, null); + + JoltTestUtil.runDiffy("failed Removr call.", expected, actual); + } + + @Test + public void process_itCallsSortr() throws IOException { + Object input = JsonUtils.classpathToObject("/json/sortr/simple/input.json"); + Object expected = JsonUtils.classpathToObject("/json/sortr/simple/output.json"); + Object chainrSpec = this.newShiftrSortrSpec(null); + + Chainr unit = Chainr.fromSpec(chainrSpec); + Object actual = unit.transform(input, null); + + JoltTestUtil.runDiffy("failed Sortr call.", expected, actual); + + String orderErrorMessage = SortrTest.verifyOrder(actual, expected); + Assert.assertNull(orderErrorMessage, orderErrorMessage); + } + + @Test + public void process_itCallsCustomJavaTransform() { + List> spec = this.newChainrSpec(); + Object delegateSpec = new HashMap<>(); + spec.add(this.newCustomJavaActivity(GoodTestTransform.class, delegateSpec)); + Object input = new Object(); + + Chainr unit = Chainr.fromSpec(spec); + TransformTestResult actual = (TransformTestResult) unit.transform(input, null); + + Assert.assertEquals(input, actual.input); + Assert.assertEquals(delegateSpec, actual.spec); + } + + @DataProvider + public Object[][] failureSpecCases() { + return new Object[][]{ + {null}, + {"foo"}, + {this.newActivity(null)}, + {this.newActivity("pants")}, + }; + } + + @Test(dataProvider = "failureSpecCases", expectedExceptions = SpecException.class) + public void process_itBlowsUp_fromSpec(Object spec) { + Chainr.fromSpec(spec); + Assert.fail("Should have failed during spec initialization."); + } + + @DataProvider + public Object[][] failureTransformCases() { + return new Object[][]{ + {this.newCustomJavaChainrSpec(ExplodingTestTransform.class, null)} + }; + } + + @Test(dataProvider = "failureTransformCases", expectedExceptions = TransformException.class) + public void process_itBlowsUp_fromTransform(Object spec) { + Chainr unit = Chainr.fromSpec(spec); + unit.transform(new HashMap<>(), null); + Assert.fail("Should have failed during transform."); + } + + + @DataProvider + public Object[][] getTestCaseNames() { + return new Object[][]{ + {"andrewkcarter1", false}, + {"andrewkcarter2", false}, + {"firstSample", true}, + {"ismith", false}, + {"ritwickgupta", false}, + {"wolfermann1", false}, + {"wolfermann2", false}, + {"wolfermann2", false} + }; + } + + @Test(dataProvider = "getTestCaseNames") + public void runTestCases(String testCaseName, boolean sorted) throws IOException { + String testPath = "/json/chainr/integration/" + testCaseName; + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); + + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Chainr unit = Chainr.fromSpec(spec); + + Assert.assertFalse(unit.hasContextualTransforms()); + Assert.assertEquals(unit.getContextualTransforms().size(), 0); + + Object actual = unit.transform(input, null); + + JoltTestUtil.runDiffy("failed case " + testPath, expected, actual); + + if (sorted) { + // Make sure the sort actually worked. + String orderErrorMessage = SortrTest.verifyOrder(actual, expected); + Assert.assertNull(orderErrorMessage, orderErrorMessage); + } + } + + + @Test + public void testReuseChainr() { + // Spec which moves "attributeMap"'s keys to a root "attributes" list. + Map specShift = JsonUtils.javason( + "{" + + "'operation':'shift'," + + "'spec' : { 'attributeMap' : { '*' : { '$' : 'attributes[#2]' } } }" + + "}" + ); + + List> chainrSpec = ImmutableList.of(specShift); + + // Create a single Chainr from the spec + Chainr chainr = Chainr.fromSpec(chainrSpec); + + // Test input with three attributes + Map content = JsonUtils.javason( + "{ 'attributeMap' : { " + + "'attribute1' : 1, 'attribute2' : 2, 'attribute3' : 3 }" + + "}" + ); + + Object transformed = chainr.transform(content); + + // First time everything checks out + Assert.assertTrue(transformed instanceof Map); + Map transformedMap = (Map) transformed; + Assert.assertEquals(transformedMap.get("attributes"), ImmutableList.of("attribute1", "attribute2", "attribute3")); + + // Create a new identical input + content = JsonUtils.javason( + "{ 'attributeMap' : { " + + "'attribute1' : 1, 'attribute2' : 2, 'attribute3' : 3 }" + + "}" + ); + + // Create a new transform from the same Chainr + transformed = chainr.transform(content); + + Assert.assertTrue(transformed instanceof Map); + transformedMap = (Map) transformed; + // The following assert fails because attributes will have three leading null values: + // transformedMap["attributes"] == [null, null, null, "attribute1", "attribute2", "attribute3"] + Assert.assertEquals(transformedMap.get("attributes"), ImmutableList.of("attribute1", "attribute2", "attribute3")); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/DefaultrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/DefaultrTest.java new file mode 100644 index 00000000..554a1973 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/DefaultrTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public class DefaultrTest { + + @DataProvider + public Object[][] getDiffyTestCases() { + return new Object[][]{ + {"arrayMismatch1"}, + {"arrayMismatch2"}, + {"defaultNulls"}, + {"expansionOnly"}, + {"firstSample"}, + {"identity"}, + {"nestedArrays1"}, + {"nestedArrays2"}, + {"orOrdering"}, + {"photosArray"}, + {"starsOfStars"}, + {"topLevelIsArray"}, + }; + } + + @Test(dataProvider = "getDiffyTestCases") + public void runDiffyTests(String testCaseName) throws IOException { + + String testPath = "/json/defaultr/" + testCaseName; + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); + + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Defaultr defaultr = new Defaultr(spec); + Object actual = defaultr.transform(input); + + JoltTestUtil.runDiffy("failed case " + testPath, expected, actual); + } + + @Test + public void deepCopyTest() throws IOException { + Map testUnit = JsonUtils.classpathToMap("/json/defaultr/__deepCopyTest.json"); + + Object spec = testUnit.get("spec"); + + Defaultr defaultr = new Defaultr(spec); + { + Object input = testUnit.get("input"); + Map fiddle = (Map) defaultr.transform(input); + + List array = (List) fiddle.get("array"); + array.add("a"); + + Map subMap = (Map) fiddle.get("map"); + subMap.put("c", "c"); + } + { + Map testUnit2 = JsonUtils.classpathToMap("/json/defaultr/__deepCopyTest.json"); + + Object input = testUnit2.get("input"); + Object expected = testUnit2.get("expected"); + + Object actual = defaultr.transform(input); + JoltTestUtil.runDiffy("Same spec deepcopy fail.", expected, actual); + } + } + + @Test(expectedExceptions = SpecException.class) + public void throwExceptionOnBadSpec() throws IOException { + Object spec = JsonUtils.jsonToMap("{ \"tuna*\": \"marlin\" }"); + new Defaultr(spec); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/EnrichrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/EnrichrTest.java new file mode 100644 index 00000000..19d58562 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/EnrichrTest.java @@ -0,0 +1,390 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.enrich.EnrichrExternalApiTestHelper; +import io.joltcommunity.jolt.enrich.EnrichrTestHelper; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class EnrichrTest { + + @DataProvider + public Object[][] getFixtureTestCases() { + return new Object[][]{ + {"/json/enrich/classNameSync.json"}, + {"/json/enrich/asyncPublisher.json"}, + {"/json/enrich/contextKeySync.json"}, + {"/json/enrich/arrayIndexSync.json"}, + {"/json/enrich/arrayWildcardSync.json"}, + {"/json/enrich/arrayWildcardAppendSync.json"}, + {"/json/enrich/nestedArrayWildcardAsync.json"} + }; + } + + @DataProvider + public Object[][] getExternalApiFixtureTestCases() { + return new Object[][]{ + {"/json/enrich/externalApiAsync.json"}, + {"/json/enrich/externalApiArrayAsync.json"} + }; + } + + @Test(dataProvider = "getFixtureTestCases") + @SuppressWarnings( "unchecked" ) + public void enrich_fixtureTests( String testFile ) throws IOException { + Map testUnit = JsonUtils.classpathToMap( testFile ); + Object spec = testUnit.get( "spec" ); + Object input = testUnit.get( "input" ); + Map context = fixtureContext( + (Map) testUnit.get( "context" ), + (List) testUnit.get( "helperContextKeys" ) + ); + Object expected = testUnit.get( "expected" ); + + Chainr chainr = Chainr.fromSpec( spec ); + + Assert.assertTrue( chainr.hasContextualTransforms() ); + Assert.assertEquals( chainr.getContextualTransforms().size(), 1 ); + + Object actual = chainr.transform( input, context ); + + JoltTestUtil.runDiffy( "failed case " + testFile, expected, actual ); + } + + @Test(dataProvider = "getExternalApiFixtureTestCases") + @SuppressWarnings( "unchecked" ) + public void enrich_fixtureExternalApiTest( String testFile ) throws Exception { + Map testUnit = JsonUtils.classpathToMap( testFile ); + + HttpServer server = HttpServer.create( new InetSocketAddress( "127.0.0.1", 0 ), 0 ); + server.createContext( "/profiles", this::handleProfileLookup ); + server.start(); + + try { + Map context = new LinkedHashMap<>( (Map) testUnit.get( "context" ) ); + context.put( "customerLookupClient", new EnrichrExternalApiTestHelper( "http://127.0.0.1:" + server.getAddress().getPort() ) ); + + Chainr chainr = Chainr.fromSpec( testUnit.get( "spec" ) ); + + Assert.assertTrue( chainr.hasContextualTransforms() ); + Assert.assertEquals( chainr.getContextualTransforms().size(), 1 ); + + Object actual = chainr.transform( testUnit.get( "input" ), context ); + + JoltTestUtil.runDiffy( "failed case " + testFile, testUnit.get( "expected" ), actual ); + } + finally { + server.stop( 0 ); + } + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsNullSpec() { + new Enrichr( null ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsNonMapSpec() { + new Enrichr( "not-a-map" ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsNonListEnrichments() { + Map spec = new LinkedHashMap<>(); + spec.put( "enrichments", "not-a-list" ); + new Enrichr( spec ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsBlankExecutionMode() { + new Enrichr( newEnrichSpec( " ", enrichmentRule( "name", null, "uppercase" ) ) ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsNonStringExecutionMode() { + Map spec = newEnrichSpec( null, enrichmentRule( "name", null, "uppercase" ) ); + spec.put( "executionMode", Boolean.TRUE ); + new Enrichr( spec ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsAppendSyntaxInInputPath() { + new Enrichr( newEnrichSpec( null, enrichmentRule( "customers.[].id", null, "uppercase" ) ) ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsWildcardPathWithFixedOutputPath() { + new Enrichr( newEnrichSpec( null, enrichmentRule( "customers.[*].id", "profiles.lookup", "uppercase" ) ) ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsWildcardBindingCountMismatch() { + new Enrichr( newEnrichSpec( null, enrichmentRule( "orders.[*].items.[*].sku", "orders.[*].inventory", "uppercase" ) ) ); + } + + @Test + public void enrich_itOverwritesTheSourceField() { + Map input = new LinkedHashMap<>(); + input.put( "name", "alice" ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( null, enrichmentRule( "name", null, "uppercase" ) ) ); + + Object output = chainr.transform( input, null ); + + Assert.assertSame( output, input ); + Assert.assertEquals( input.get( "name" ), "ALICE" ); + } + + @Test + public void enrich_itCanWriteToAnotherFieldAndUseContext() { + Map customer = new LinkedHashMap<>(); + customer.put( "id", "cust-123" ); + + Map input = new LinkedHashMap<>(); + input.put( "customer", customer ); + + Map context = new LinkedHashMap<>(); + context.put( "tenant", "acme" ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( null, enrichmentRule( "customer.id", "customer.profile", "describe" ) ) ); + + Object output = chainr.transform( input, context ); + + Assert.assertSame( output, input ); + + @SuppressWarnings( "unchecked" ) + Map profile = (Map) customer.get( "profile" ); + Assert.assertEquals( profile.get( "original" ), "cust-123" ); + Assert.assertEquals( profile.get( "inputType" ), "LinkedHashMap" ); + Assert.assertEquals( profile.get( "tenant" ), "acme" ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsMissingEnrichments() { + new Enrichr( newEnrichSpec( null ) ); + } + + @Test + public void enrich_itCanUseATargetResolvedFromContext() { + Map customer = new LinkedHashMap<>(); + customer.put( "id", "cust-123" ); + + Map input = new LinkedHashMap<>(); + input.put( "customer", customer ); + + Map context = new LinkedHashMap<>(); + context.put( "tenant", "acme" ); + context.put( "lookupBean", new EnrichrTestHelper() ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( null, contextEnrichmentRule( "customer.id", "customer.profile", "lookupBean", "describeViaBean" ) ) ); + + Object output = chainr.transform( input, context ); + + Assert.assertSame( output, input ); + + @SuppressWarnings( "unchecked" ) + Map profile = (Map) customer.get( "profile" ); + Assert.assertEquals( profile.get( "original" ), "cust-123" ); + Assert.assertEquals( profile.get( "tenant" ), "acme" ); + } + + @Test + public void enrich_itResolvesCompletionStageResults() { + Map input = new LinkedHashMap<>(); + input.put( "name", "alice" ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( "sync", enrichmentRule( "name", null, "asyncUppercase" ) ) ); + + Object output = chainr.transform( input, null ); + + Assert.assertSame( output, input ); + Assert.assertEquals( input.get( "name" ), "ALICE" ); + } + + @Test + public void enrich_itResolvesPublisherResultsInAsyncMode() { + Map customer = new LinkedHashMap<>(); + customer.put( "id", "cust-123" ); + + Map input = new LinkedHashMap<>(); + input.put( "customer", customer ); + + Map context = new LinkedHashMap<>(); + context.put( "tenant", "acme" ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( "async", enrichmentRule( "customer.id", "customer.profile", "publisherDescribe" ) ) ); + + Object output = chainr.transform( input, context ); + + Assert.assertSame( output, input ); + + @SuppressWarnings( "unchecked" ) + Map profile = (Map) customer.get( "profile" ); + Assert.assertEquals( profile.get( "original" ), "cust-123" ); + Assert.assertEquals( profile.get( "tenant" ), "acme" ); + } + + @Test + public void enrich_itIgnoresMissingPathsInSyncMode() { + Map input = new LinkedHashMap<>(); + input.put( "name", "alice" ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( "sync", enrichmentRule( "customer.id", "customer.profile", "describe" ) ) ); + + Object output = chainr.transform( input, null ); + + Assert.assertSame( output, input ); + Assert.assertEquals( input.size(), 1 ); + Assert.assertFalse( input.containsKey( "customer" ) ); + } + + @Test + public void enrich_itIgnoresMissingPathsInAsyncMode() { + Map input = new LinkedHashMap<>(); + input.put( "name", "alice" ); + + Chainr chainr = Chainr.fromSpec( newChainrSpec( "async", enrichmentRule( "customer.id", "customer.profile", "publisherDescribe" ) ) ); + + Object output = chainr.transform( input, null ); + + Assert.assertSame( output, input ); + Assert.assertEquals( input.size(), 1 ); + Assert.assertFalse( input.containsKey( "customer" ) ); + } + + @Test( expectedExceptions = SpecException.class ) + public void enrich_itRejectsUnsupportedExecutionMode() { + Chainr.fromSpec( newChainrSpec( "parallel", enrichmentRule( "name", null, "uppercase" ) ) ); + } + + @SafeVarargs + private final Map newEnrichSpec( String executionMode, Map... rules ) { + Map enrichSpec = new LinkedHashMap<>(); + if ( executionMode != null ) { + enrichSpec.put( "executionMode", executionMode ); + } + + List> enrichments = new ArrayList<>(); + for ( Map rule : rules ) { + enrichments.add( rule ); + } + enrichSpec.put( "enrichments", enrichments ); + return enrichSpec; + } + + private List> newChainrSpec( String executionMode, Map rule ) { + List> spec = new ArrayList<>(); + Map enrichOperation = new LinkedHashMap<>(); + enrichOperation.put( ChainrEntry.OPERATION_KEY, "enrich" ); + + enrichOperation.put( ChainrEntry.SPEC_KEY, newEnrichSpec( executionMode, rule ) ); + spec.add( enrichOperation ); + return spec; + } + + private Map enrichmentRule( String path, String outputPath, String methodName ) { + Map rule = new LinkedHashMap<>(); + rule.put( "path", path ); + if ( outputPath != null ) { + rule.put( "outputPath", outputPath ); + } + rule.put( "className", EnrichrTestHelper.class.getName() ); + rule.put( "method", methodName ); + return rule; + } + + private Map contextEnrichmentRule( String path, String outputPath, String contextKey, String methodName ) { + Map rule = new LinkedHashMap<>(); + rule.put( "path", path ); + if ( outputPath != null ) { + rule.put( "outputPath", outputPath ); + } + rule.put( "contextKey", contextKey ); + rule.put( "method", methodName ); + return rule; + } + + private Map fixtureContext( Map rawContext, List helperContextKeys ) { + Map context = rawContext == null ? new LinkedHashMap() : new LinkedHashMap<>( rawContext ); + + if ( helperContextKeys != null ) { + for ( String helperContextKey : helperContextKeys ) { + context.put( helperContextKey, new EnrichrTestHelper() ); + } + } + + return context.isEmpty() ? null : context; + } + + private void handleProfileLookup( HttpExchange exchange ) throws IOException { + String requestPath = exchange.getRequestURI().getPath(); + String prefix = "/profiles/"; + + if ( ! "GET".equals( exchange.getRequestMethod() ) || ! requestPath.startsWith( prefix ) ) { + exchange.sendResponseHeaders( 404, -1 ); + exchange.close(); + return; + } + + String customerId = requestPath.substring( prefix.length() ); + String tenant = extractQueryParam( exchange.getRequestURI().getRawQuery(), "tenant" ); + + Map responseBody = new LinkedHashMap<>(); + responseBody.put( "customerId", customerId ); + responseBody.put( "tenant", tenant ); + responseBody.put( "segment", "gold" ); + responseBody.put( "source", "external-api" ); + + byte[] responseBytes = JsonUtils.toJsonString( responseBody ).getBytes( StandardCharsets.UTF_8 ); + exchange.getResponseHeaders().add( "Content-Type", "application/json" ); + exchange.sendResponseHeaders( 200, responseBytes.length ); + + try ( OutputStream outputStream = exchange.getResponseBody() ) { + outputStream.write( responseBytes ); + } + } + + private String extractQueryParam( String rawQuery, String key ) { + if ( rawQuery == null || rawQuery.isEmpty() ) { + return null; + } + + for ( String entry : rawQuery.split( "&" ) ) { + String[] keyValue = entry.split( "=", 2 ); + if ( key.equals( keyValue[0] ) ) { + return keyValue.length == 2 ? URLDecoder.decode( keyValue[1], StandardCharsets.UTF_8 ) : ""; + } + } + + return null; + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/InaccessibleEnrichrMethodHelper.java b/jolt-core/src/test/java/io/joltcommunity/jolt/InaccessibleEnrichrMethodHelper.java new file mode 100644 index 00000000..d5698940 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/InaccessibleEnrichrMethodHelper.java @@ -0,0 +1,23 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt; + +class InaccessibleEnrichrMethodHelper { + + public static Object inaccessibleStatic( Object value ) { + return value; + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/JoltTestUtil.java b/jolt-core/src/test/java/io/joltcommunity/jolt/JoltTestUtil.java new file mode 100644 index 00000000..4fff99fa --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/JoltTestUtil.java @@ -0,0 +1,52 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import org.testng.Assert; + +import java.io.IOException; + +public class JoltTestUtil { + + private static final Diffy diffy = new Diffy(); + private static final Diffy arrayOrderObliviousDiffy = new ArrayOrderObliviousDiffy(); + + public static void runDiffy(String failureMessage, Object expected, Object actual) throws IOException { + runDiffy(diffy, failureMessage, expected, actual); + } + + public static void runDiffy(Object expected, Object actual) throws IOException { + runDiffy(diffy, "Failed", expected, actual); + } + + public static void runArrayOrderObliviousDiffy(String failureMessage, Object expected, Object actual) throws IOException { + runDiffy(arrayOrderObliviousDiffy, failureMessage, expected, actual); + } + + public static void runArrayOrderObliviousDiffy(Object expected, Object actual) throws IOException { + runDiffy(arrayOrderObliviousDiffy, "Failed", expected, actual); + } + + + private static void runDiffy(Diffy diffy, String failureMessage, Object expected, Object actual) { + String actualObject = JsonUtils.toPrettyJsonString(actual); + Diffy.Result result = diffy.diff(expected, actual); + if (!result.isEmpty()) { + Assert.fail("\nActual object\n" + actualObject + "\n" + failureMessage + "\nDiffy output\n" + result.toString()); + } + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/ModifierTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/ModifierTest.java new file mode 100644 index 00000000..6383ab49 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/ModifierTest.java @@ -0,0 +1,317 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.common.SpecStringParser; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.modifier.function.Function; +import com.google.common.collect.Lists; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.util.*; + +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +@SuppressWarnings("deprecated") +public class ModifierTest { + + @BeforeClass + @SuppressWarnings("unchecked") + public void setup() throws Exception { + // accessing built ins such that we can test a custom impl of function + // this is a special test case, and not a recommended approach of using function + Field f = Modifier.class.getDeclaredField("STOCK_FUNCTIONS"); + f.setAccessible(true); + Map BUILT_INS = (Map) f.get(null); + BUILT_INS.put("minLabelComputation", new MinLabelComputation()); + BUILT_INS.put("maxLabelComputation", new MaxLabelComputation()); + } + + @DataProvider + public Iterator getTestCases() { + List testCases = Lists.newLinkedList(); + + testCases.add(new Object[]{"/json/modifier/mapLiteral.json"}); + testCases.add(new Object[]{"/json/modifier/mapLiteralWithNullInput.json"}); + testCases.add(new Object[]{"/json/modifier/mapLiteralWithMissingInput.json"}); + testCases.add(new Object[]{"/json/modifier/mapLiteralWithEmptyInput.json"}); + + testCases.add(new Object[]{"/json/modifier/arrayElementAt.json"}); + + testCases.add(new Object[]{"/json/modifier/arrayLiteral.json"}); + testCases.add(new Object[]{"/json/modifier/arrayLiteralWithNullInput.json"}); + testCases.add(new Object[]{"/json/modifier/arrayLiteralWithEmptyInput.json"}); + testCases.add(new Object[]{"/json/modifier/arrayLiteralWithMissingInput.json"}); + + testCases.add(new Object[]{"/json/modifier/simple.json"}); + testCases.add(new Object[]{"/json/modifier/simpleArray.json"}); + testCases.add(new Object[]{"/json/modifier/arrayObject.json"}); + + testCases.add(new Object[]{"/json/modifier/simpleMapNullToArray.json"}); + testCases.add(new Object[]{"/json/modifier/simpleMapRuntimeNull.json"}); + + testCases.add(new Object[]{"/json/modifier/simpleLookup.json"}); + testCases.add(new Object[]{"/json/modifier/complexLookup.json"}); + + testCases.add(new Object[]{"/json/modifier/simpleArrayLookup.json"}); + testCases.add(new Object[]{"/json/modifier/complexArrayLookup.json"}); + + testCases.add(new Object[]{"/json/modifier/valueCheckSimpleArray.json"}); + testCases.add(new Object[]{"/json/modifier/valueCheckSimpleArrayNullInput.json"}); + testCases.add(new Object[]{"/json/modifier/valueCheckSimpleArrayEmptyInput.json"}); + + testCases.add(new Object[]{"/json/modifier/valueCheckSimpleMap.json"}); + testCases.add(new Object[]{"/json/modifier/valueCheckSimpleMapNullInput.json"}); + testCases.add(new Object[]{"/json/modifier/valueCheckSimpleMapEmptyInput.json"}); + + testCases.add(new Object[]{"/json/modifier/simpleMapOpOverride.json"}); + testCases.add(new Object[]{"/json/modifier/simpleArrayOpOverride.json"}); + + testCases.add(new Object[]{"/json/modifier/testListOfFunction.json"}); + + return testCases.iterator(); + } + + @Test(dataProvider = "getTestCases") + public void testOverwritrTransform(String testFile) throws Exception { + doTest(testFile, ModifierTestCase.OVERWRITR); + } + + @Test(dataProvider = "getTestCases") + public void testDefaultrTransform(String testFile) throws Exception { + doTest(testFile, ModifierTestCase.DEFAULTR); + } + + @Test(dataProvider = "getTestCases") + public void testDefinrTransform(String testFile) throws Exception { + doTest(testFile, ModifierTestCase.DEFINR); + } + + public void doTest(String testFile, ModifierTestCase testCase) throws Exception { + Map testUnit = JsonUtils.classpathToMap(testFile); + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object context = testUnit.get("context"); + Object expected = testUnit.get(testCase.name()); + if (expected != null) { + Modifier modifier = testCase.getModifier(spec); + Object actual = modifier.transform(input, (Map) context); + JoltTestUtil.runArrayOrderObliviousDiffy(testCase.name() + " failed case " + testFile, expected, actual); + } + } + + @DataProvider + public Iterator getSpecValidationTestCases() { + List testCases = Lists.newLinkedList(); + List testObjects = JsonUtils.classpathToList("/json/modifier/validation/specThatShouldFail.json"); + + for (ModifierTestCase testCase : ModifierTestCase.values()) { + for (Object specObj : testObjects) { + testCases.add(new Object[]{testCase, specObj}); + } + } + + return testCases.iterator(); + } + + @Test(expectedExceptions = SpecException.class, dataProvider = "getSpecValidationTestCases") + public void testInvalidSpecs(ModifierTestCase testCase, Object spec) { + testCase.getModifier(spec); + } + + @DataProvider + public Iterator getFunctionTests() { + List testCases = Lists.newLinkedList(); + + testCases.add(new Object[]{"/json/modifier/functions/stringsSplitTest.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/padStringsTest.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/stringsTests.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/mathTests.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/arrayTests.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/sizeTests.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/labelsLookupTest.json", ModifierTestCase.DEFAULTR}); + testCases.add(new Object[]{"/json/modifier/functions/valueTests.json", ModifierTestCase.OVERWRITR}); + testCases.add(new Object[]{"/json/modifier/functions/dateTests.json", ModifierTestCase.OVERWRITR}); + + return testCases.iterator(); + } + + @Test(dataProvider = "getFunctionTests") + public void testFunctions(String testFile, ModifierTestCase testCase) throws Exception { + doTest(testFile, testCase); + } + + @DataProvider + public Iterator getSquashTests() { + List testCases = Lists.newLinkedList(); + + testCases.add(new Object[]{"/json/modifier/functions/squashNullsTests.json"}); + testCases.add(new Object[]{"/json/modifier/functions/deleteDuplicatesTests.json"}); + + return testCases.iterator(); + } + + @Test(dataProvider = "getSquashTests") + public void doSquashNullsTest(String testFile) throws Exception { + ModifierTestCase testCase = ModifierTestCase.OVERWRITR; + Map testUnit = JsonUtils.classpathToMap(testFile); + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object context = testUnit.get("context"); + Object expected = testUnit.get(testCase.name()); + if (expected != null) { + Modifier modifier = testCase.getModifier(spec); + Object actual = modifier.transform(input, (Map) context); + JoltTestUtil.runDiffy(testCase.name() + " failed case " + testFile, expected, actual); + } + } + + @DataProvider + public Iterator fnArgParseTestCases() { + List testCases = Lists.newLinkedList(); + + testCases.add(new Object[]{"fn(abc,efg,pqr)", new String[]{"fn", "abc", "efg", "pqr"}}); + testCases.add(new Object[]{"fn(abc,@(1,2),pqr)", new String[]{"fn", "abc", "@(1,2)", "pqr"}}); + testCases.add(new Object[]{"fn(abc,efg,pqr,)", new String[]{"fn", "abc", "efg", "pqr", ""}}); + testCases.add(new Object[]{"fn(abc,,@(1,,2),,pqr,,)", new String[]{"fn", "abc", "", "@(1,,2)", "", "pqr", "", ""}}); + testCases.add(new Object[]{"fn(abc,'e,f,g',pqr)", new String[]{"fn", "abc", "'e,f,g'", "pqr"}}); + testCases.add(new Object[]{"fn(abc,'e(,f,)g',pqr)", new String[]{"fn", "abc", "'e(,f,)g'", "pqr"}}); + + return testCases.iterator(); + } + + @Test(dataProvider = "fnArgParseTestCases") + public void testFunctionArgParse(String argString, String[] expected) throws Exception { + List actual = SpecStringParser.parseFunctionArgs(argString); + JoltTestUtil.runArrayOrderObliviousDiffy(" failed case " + argString, expected, actual); + } + + @Test + public void testModifierFirstElementArray() throws IOException { + Map input = new HashMap<>() {{ + put("input", new Integer[]{5, 4}); + }}; + + Map spec = new HashMap<>() {{ + put("first", "=firstElement(@(1,input))"); + }}; + + Map expected = new HashMap<>() {{ + put("input", new Integer[]{5, 4}); + put("first", 5); + }}; + + Modifier modifier = new Modifier.Overwritr(spec); + Object actual = modifier.transform(input, null); + JoltTestUtil.runArrayOrderObliviousDiffy("failed modifierFirstElementArray", expected, actual); + } + + @Test + public void testUuidFunction() { + Map input = new HashMap<>() {{ + put("id", null); + }}; + + Map spec = new HashMap<>() {{ + put("id", "=uuid"); + }}; + + Modifier modifier = new Modifier.Overwritr(spec); + Object actual = modifier.transform(input, null); + + Object generatedUuid = ((Map) actual).get("id"); + assertNotNull(generatedUuid, "UUID should not be null"); + assertTrue(generatedUuid instanceof String, "UUID should be a String"); + // Validate UUID format (8-4-4-4-12 hex digits) + String uuidPattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"; + assertTrue(((String) generatedUuid).matches(uuidPattern), + "Generated value should match UUID format: " + generatedUuid); + } + + enum ModifierTestCase { + OVERWRITR { + @Override + Modifier getModifier(final Object spec) { + return new Modifier.Overwritr(spec); + } + }, + DEFAULTR { + @Override + Modifier getModifier(final Object spec) { + return new Modifier.Defaultr(spec); + } + }, + DEFINR { + @Override + Modifier getModifier(final Object spec) { + return new Modifier.Definr(spec); + } + }; + + abstract Modifier getModifier(Object spec); + } + + @SuppressWarnings("unused") + public static final class MinLabelComputation implements Function { + @Override + @SuppressWarnings("unchecked") + public Optional apply(final Object... args) { + Map valueLabels = (Map) args[0]; + Integer min = Integer.MAX_VALUE; + Set valueLabelKeys = valueLabels.keySet(); + for (String labelKey : valueLabelKeys) { + Integer val = null; + try { + val = Integer.parseInt(labelKey); + } catch (Exception ignored) { + } + if (val != null) { + min = Math.min(val, min); + } + } + return Optional.of(valueLabels.get(min.toString())); + } + } + + @SuppressWarnings("unused") + public static final class MaxLabelComputation implements Function { + @Override + @SuppressWarnings("unchecked") + public Optional apply(final Object... args) { + Map valueLabels = (Map) args[0]; + Integer max = Integer.MIN_VALUE; + Set valueLabelKeys = valueLabels.keySet(); + for (String labelKey : valueLabelKeys) { + Integer val = null; + try { + val = Integer.parseInt(labelKey); + } catch (Exception ignored) { + } + if (val != null) { + max = Math.max(val, max); + } + } + return Optional.of(valueLabels.get(max.toString())); + } + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/RemovrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/RemovrTest.java new file mode 100644 index 00000000..b33a8d3e --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/RemovrTest.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.removr.Removr; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; + +public class RemovrTest { + + @DataProvider + public Object[][] getTestCaseNames() { + return new Object[][]{ + {"firstSample"}, + {"boundaryConditions"}, + {"removrWithWildcardSupport"}, + {"multiStarSupport"}, + {"starDoublePathElementBoundaryConditions"}, + // Array tests + {"array_canPassThruNestedArrays"}, + {"array_canHandleTopLevelArray"}, + {"array_nonStarInArrayDoesNotDie"}, + {"array_removeAnArrayIndex"}, + {"array_removeJsonArrayFields"} + }; + } + + @Test(dataProvider = "getTestCaseNames") + public void runTestCases(String testCaseName) throws IOException { + + String testPath = "/json/removr/" + testCaseName; + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); + + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Removr removr = new Removr(spec); + Object actual = removr.transform(input); + + JoltTestUtil.runDiffy("failed case " + testPath, expected, actual); + } + + @DataProvider + public Object[][] getNegativeTestCaseNames() { + return new Object[][]{ + {"negativeTestCases"} + }; + } + + @Test(dataProvider = "getNegativeTestCaseNames", expectedExceptions = SpecException.class) + public void runNegativeTestCases(String testCaseName) throws IOException { + + String testPath = "/json/removr/" + testCaseName; + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); + + Object spec = testUnit.get("spec"); + new Removr(spec); + } + + @DataProvider + public Object[][] badSpecs() throws IOException { + return new Object[][]{ + { + "Null Spec", + null, + }, + { + "List Spec", + new ArrayList<>(), + }, + { + "Invalid rhs string", + JsonUtils.javason("{ 'tuna' : 'marlin[-1]' }"), + }, + { + "Invalid rhs type - not a Map (number)", + JsonUtils.javason("{ 'tuna' : 123 }"), + }, + { + "Invalid rhs type - not a Map (array)", + JsonUtils.javason("{ 'tuna' : [] }"), + } + }; + } + + @Test(dataProvider = "badSpecs", expectedExceptions = SpecException.class) + public void failureUnitTest(String testName, Object spec) { + new Removr(spec); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/ShiftrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/ShiftrTest.java new file mode 100644 index 00000000..f26c75a3 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/ShiftrTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Map; + +public class ShiftrTest { + + // TODO: test arrays better (wildcards test array could be in reverse order) + @DataProvider + public Object[][] getTestCaseUnits() { + return new Object[][]{ + {"arrayExample"}, + {"arrayMismatch"}, + {"bucketToPrefixSoup"}, + {"declaredOutputArray"}, + {"escapeAllTheThings"}, + {"escapeAllTheThings2"}, + {"explicitArrayKey"}, + {"filterParallelArrays"}, + {"filterParents1"}, + {"filterParents2"}, + {"filterParents3"}, + {"firstSample"}, + {"hashDefault"}, + {"identity"}, + {"inputArrayToPrefix"}, + {"invertMap"}, + {"json-ld-escaping"}, + {"keyref"}, + {"lhsAmpMatch"}, + {"listKeys"}, + {"mapToList"}, + {"mapToList2"}, + {"mergeParallelArrays1_and-transpose"}, + {"mergeParallelArrays2_and-do-not-transpose"}, + {"mergeParallelArrays3_and-filter"}, + {"multiPlacement"}, + {"objectToArray"}, + {"passNullThru"}, + {"passThru"}, + {"pollaxman_218_duplicate_speclines_bug"}, + {"prefixDataToArray"}, + {"prefixedData"}, + {"prefixSoupToBuckets"}, + {"queryMappingXform"}, + {"shiftToTrash"}, + {"simpleLHSEscape"}, + {"simpleRHSEscape"}, + {"singlePlacement"}, + {"specialKeys"}, + {"transposeArrayContents1"}, + {"transposeArrayContents2"}, + {"transposeComplex1"}, + {"transposeComplex2"}, + {"transposeComplex3_both-sides-multipart"}, + {"transposeComplex4_lhs-multipart-rhs-sugar"}, + {"transposeComplex5_at-logic-with-embedded-array-lookups"}, + {"transposeComplex6_rhs-complex-at"}, + {"transposeComplex7_coerce-int-string-conversion"}, + {"transposeComplex8_coerce-boolean-string-conversion"}, + {"transposeComplex9_lookup_an_array_index"}, + {"transposeInverseMap1"}, + {"transposeInverseMap2"}, + {"transposeLHS1"}, + {"transposeLHS2"}, + {"transposeLHS3"}, + {"transposeNestedLookup"}, + {"transposeSimple1"}, + {"transposeSimple2"}, + {"transposeSimple3"}, + {"transposeLargeNumber"}, + {"wildcards"}, + {"wildcardSelfAndRef"}, + {"wildcardsWithOr"} + }; + } + + // TODO: test arrays better (wildcards test array could be in reverse order) + + @Test(dataProvider = "getTestCaseUnits") + public void runTestUnits(String testCaseName) throws IOException { + + String testPath = "/json/shiftr/" + testCaseName; + Map testUnit = JsonUtils.classpathToMap(testPath + ".json"); + + Object input = testUnit.get("input"); + Object spec = testUnit.get("spec"); + Object expected = testUnit.get("expected"); + + Shiftr shiftr = new Shiftr(spec); + Object actual = shiftr.transform(input); + + JoltTestUtil.runDiffy("failed case " + testPath, expected, actual); + } +} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/SortrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/SortrTest.java similarity index 50% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/SortrTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/SortrTest.java index d666820d..dc7e3b45 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/SortrTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/SortrTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import org.apache.commons.lang3.StringUtils; import org.testng.Assert; @@ -21,68 +22,35 @@ import org.testng.annotations.Test; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; public class SortrTest { - @DataProvider - public Object[][] getTestCaseNames() { - return new Object[][] { - { "simple" } - }; - } - - @Test(dataProvider = "getTestCaseNames") - public void runTestCases(String testCaseName) throws IOException { - - if ("".equals( testCaseName )) { - return; - } - - String testPath = "/json/sortr/"+testCaseName; - Map input = JsonUtils.classpathToMap(testPath + "/input.json"); - Map expected = JsonUtils.classpathToMap( testPath + "/output.json" ); - - Sortr sortr = new Sortr(); - Map actual = (Map) sortr.transform( input ); - - JoltTestUtil.runDiffy( "Make sure it is still the same object : " + testPath, expected, actual ); - - // Make sure the sort actually worked. - String orderErrorMessage = verifyOrder( actual, expected ); - Assert.assertNull( orderErrorMessage, orderErrorMessage ); - } - - public static String verifyOrder( Object actual, Object expected ) { - if ( actual instanceof Map && expected instanceof Map ) { - return verifyMapOrder( (Map) actual, (Map) expected ); - } else if ( actual instanceof List && expected instanceof List ) { - return verifyListOrder( (List) actual, (List) expected ) ; + public static String verifyOrder(Object actual, Object expected) { + if (actual instanceof Map && expected instanceof Map) { + return verifyMapOrder((Map) actual, (Map) expected); + } else if (actual instanceof List && expected instanceof List) { + return verifyListOrder((List) actual, (List) expected); } else { return null; } } - private static String verifyMapOrder( Map actual, Map expected ) { + private static String verifyMapOrder(Map actual, Map expected) { Iterator actualIter = actual.keySet().iterator(); Iterator expectedIter = expected.keySet().iterator(); - for( int index = 0; index < actual.size(); index++ ) { + for (int index = 0; index < actual.size(); index++) { String actualKey = actualIter.next(); String expectedKey = expectedIter.next(); - if ( ! StringUtils.equals( actualKey, expectedKey ) ) { + if (!StringUtils.equals(actualKey, expectedKey)) { return "Found out of order keys '" + actualKey + "' and '" + expectedKey + "'"; } - String result = verifyOrder( actual.get( actualKey), expected.get(expectedKey) ); - if ( result != null ) { + String result = verifyOrder(actual.get(actualKey), expected.get(expectedKey)); + if (result != null) { return result; } } @@ -90,11 +58,11 @@ private static String verifyMapOrder( Map actual, Map actual, List expected ) { + private static String verifyListOrder(List actual, List expected) { - for( int index = 0; index < actual.size(); index++ ) { - String result = verifyOrder( actual.get( index ), expected.get(index) ); - if ( result != null ) { + for (int index = 0; index < actual.size(); index++) { + String result = verifyOrder(actual.get(index), expected.get(index)); + if (result != null) { return result; } } @@ -102,22 +70,49 @@ private static String verifyListOrder( List actual, List expecte return null; // success } + @DataProvider + public Object[][] getTestCaseNames() { + return new Object[][]{ + {"simple"} + }; + } + + @Test(dataProvider = "getTestCaseNames") + public void runTestCases(String testCaseName) throws IOException { + + if ("".equals(testCaseName)) { + return; + } + + String testPath = "/json/sortr/" + testCaseName; + Map input = JsonUtils.classpathToMap(testPath + "/input.json"); + Map expected = JsonUtils.classpathToMap(testPath + "/output.json"); + + Sortr sortr = new Sortr(); + Map actual = (Map) sortr.transform(input); + + JoltTestUtil.runDiffy("Make sure it is still the same object : " + testPath, expected, actual); + + // Make sure the sort actually worked. + String orderErrorMessage = verifyOrder(actual, expected); + Assert.assertNull(orderErrorMessage, orderErrorMessage); + } + @Test public void testDoesNotBlowUpOnUnmodifiableArray() { List hasNan = new ArrayList<>(); - hasNan.add( 1 ); - hasNan.add( Double.NaN ); - hasNan.add( 2 ); + hasNan.add(1); + hasNan.add(Double.NaN); + hasNan.add(2); - Map map = new HashMap<>(); + Map map = new HashMap<>(); map.put("a", "shouldBeFirst"); - map.put("hasNan", Collections.unmodifiableList( hasNan ) ); + map.put("hasNan", Collections.unmodifiableList(hasNan)); try { - Sortr.sortJson( map ); - } - catch( UnsupportedOperationException uoe ) { - Assert.fail( "Sort threw a UnsupportedOperationException" ); + Sortr.sortJson(map); + } catch (UnsupportedOperationException uoe) { + Assert.fail("Sort threw a UnsupportedOperationException"); } } } diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrIncrementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrIncrementTest.java new file mode 100644 index 00000000..feea08b1 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrIncrementTest.java @@ -0,0 +1,97 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.chainr; + +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JoltTestUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.exception.TransformException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.HashMap; + +public class ChainrIncrementTest { + + @DataProvider + public Object[][] fromToTests() { + + Object chainrSpec = JsonUtils.classpathToObject("/json/chainr/increments/spec.json"); + + return new Object[][]{ + {chainrSpec, 0, 1}, + {chainrSpec, 0, 3}, + {chainrSpec, 1, 3}, + {chainrSpec, 1, 4} + }; + } + + @Test(dataProvider = "fromToTests") + public void testChainrIncrementsFromTo(Object chainrSpec, int start, int end) throws IOException { + Chainr chainr = Chainr.fromSpec(chainrSpec); + + Object expected = JsonUtils.classpathToObject("/json/chainr/increments/" + start + "-" + end + ".json"); + + Object actual = chainr.transform(start, end, new HashMap<>()); + + JoltTestUtil.runDiffy("failed incremental From-To Chainr", expected, actual); + } + + + @DataProvider + public Object[][] toTests() { + + Object chainrSpec = JsonUtils.classpathToObject("/json/chainr/increments/spec.json"); + + return new Object[][]{ + {chainrSpec, 1}, + {chainrSpec, 3} + }; + } + + @Test(dataProvider = "toTests") + public void testChainrIncrementsTo(Object chainrSpec, int end) throws IOException { + + Chainr chainr = Chainr.fromSpec(chainrSpec); + + Object expected = JsonUtils.classpathToObject("/json/chainr/increments/0-" + end + ".json"); + + Object actual = chainr.transform(end, new HashMap<>()); + + JoltTestUtil.runDiffy("failed incremental To Chainr", expected, actual); + } + + @DataProvider + public Object[][] failTests() { + + Object chainrSpec = JsonUtils.classpathToObject("/json/chainr/increments/spec.json"); + + return new Object[][]{ + {chainrSpec, 0, 0}, + {chainrSpec, -2, 2}, + {chainrSpec, 0, -2}, + {chainrSpec, 1, 10000} + }; + } + + @Test(dataProvider = "failTests", expectedExceptions = TransformException.class) + public void testFails(Object chainrSpec, int start, int end) throws IOException { + Chainr chainr = Chainr.fromSpec(chainrSpec); + chainr.transform(start, end, new HashMap<>()); + } +} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrInitializationTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrInitializationTest.java similarity index 51% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrInitializationTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrInitializationTest.java index fa1a99c8..82fd5fae 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrInitializationTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrInitializationTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +14,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; - -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.ContextualTransform; -import com.bazaarvoice.jolt.JoltTransform; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.Transform; -import com.bazaarvoice.jolt.chainr.transforms.TransformTestResult; -import com.bazaarvoice.jolt.exception.SpecException; -import com.bazaarvoice.jolt.exception.TransformException; +package io.joltcommunity.jolt.chainr; + +import io.joltcommunity.jolt.chainr.transforms.TransformTestResult; +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.TransformException; import com.beust.jcommander.internal.Lists; +import io.joltcommunity.jolt.*; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -36,74 +33,75 @@ public class ChainrInitializationTest { @DataProvider public Object[][] badTransforms() { - return new Object[][] { - {JsonUtils.classpathToObject( "/json/chainr/transforms/bad_transform_loadsExplodingTransform.json" )} + return new Object[][]{ + {JsonUtils.classpathToObject("/json/chainr/transforms/bad_transform_loadsExplodingTransform.json")} }; } - @Test(dataProvider = "badTransforms", expectedExceptions = TransformException.class ) + @Test(dataProvider = "badTransforms", expectedExceptions = TransformException.class) public void testBadTransforms(Object chainrSpec) { - Chainr unit = Chainr.fromSpec( chainrSpec ); - unit.transform( new HashMap(), null );// should fail here - Assert.fail( "Should not have gotten here" ); + Chainr unit = Chainr.fromSpec(chainrSpec); + unit.transform(new HashMap<>(), null);// should fail here + Assert.fail("Should not have gotten here"); } @DataProvider public Object[][] passingTestCases() { - return new Object[][] { - {new Object(), JsonUtils.classpathToObject( "/json/chainr/transforms/loadsGoodTransform.json" )} + return new Object[][]{ + {new Object(), JsonUtils.classpathToObject("/json/chainr/transforms/loadsGoodTransform.json")} }; } - @Test(dataProvider = "passingTestCases" ) + @Test(dataProvider = "passingTestCases") public void testPassing(Object input, Object spec) { - Chainr unit = Chainr.fromSpec( spec ); - TransformTestResult actual = (TransformTestResult) unit.transform( input, null ); + Chainr unit = Chainr.fromSpec(spec); + TransformTestResult actual = (TransformTestResult) unit.transform(input, null); - Assert.assertEquals( input, actual.input ); - Assert.assertNotNull( actual.spec ); + Assert.assertEquals(input, actual.input); + Assert.assertNotNull(actual.spec); } - @Test( expectedExceptions = IllegalArgumentException.class ) + @Test(expectedExceptions = IllegalArgumentException.class) public void chainrBuilderFailsOnNullLoader() { - Object validSpec = JsonUtils.classpathToObject( "/json/chainr/transforms/loadsGoodTransform.json" ); - new ChainrBuilder( validSpec ).loader( null ); + Object validSpec = JsonUtils.classpathToObject("/json/chainr/transforms/loadsGoodTransform.json"); + new ChainrBuilder(validSpec).loader(null); } - @Test( expectedExceptions = IllegalArgumentException.class ) + @Test(expectedExceptions = IllegalArgumentException.class) public void failsOnNullListOfJoltTransforms() { - new Chainr( null ); + new Chainr(null); } - @Test( expectedExceptions = SpecException.class ) + @Test(expectedExceptions = SpecException.class) public void failsOnStupidTransform() { List badSpec = Lists.newArrayList(); // Stupid JoltTransform that implements the base interface, and not one of the useful ones - badSpec.add( new JoltTransform() {} ); + badSpec.add(new JoltTransform() { + }); - new Chainr( badSpec ); + new Chainr(badSpec); } - @Test( expectedExceptions = SpecException.class ) + @Test(expectedExceptions = SpecException.class) public void failsOnOverEagerTransform() { List badSpec = Lists.newArrayList(); // Stupid JoltTransform that implements both "real" interfaces - badSpec.add( new OverEagerTransform() ); + badSpec.add(new OverEagerTransform()); - new Chainr( badSpec ); + new Chainr(badSpec); } private static class OverEagerTransform implements Transform, ContextualTransform { @Override - public Object transform( Object input, Map context ) { + public Object transform(Object input, Map context) { return null; } @Override - public Object transform( Object input ) { + public Object transform(Object input) { return null; } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrSpecFormatTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrSpecFormatTest.java similarity index 54% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrSpecFormatTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrSpecFormatTest.java index 12375b7a..ee834c42 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrSpecFormatTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrSpecFormatTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; +package io.joltcommunity.jolt.chainr; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.chainr.spec.ChainrSpec; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.chainr.spec.ChainrSpec; +import io.joltcommunity.jolt.exception.SpecException; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -28,21 +29,21 @@ public class ChainrSpecFormatTest { @DataProvider public Object[][] badFormatSpecs() throws IOException { - return new Object[][] { - {JsonUtils.classpathToObject( "/json/chainr/specformat/bad_spec_arrayClassName.json" )}, - {JsonUtils.classpathToObject( "/json/chainr/specformat/bad_spec_ClassName.json" )}, - {JsonUtils.classpathToObject( "/json/chainr/specformat/bad_spec_NonTransformClass.json" )}, - {JsonUtils.classpathToObject( "/json/chainr/specformat/bad_spec_empty.json" )} + return new Object[][]{ + {JsonUtils.classpathToObject("/json/chainr/specformat/bad_spec_arrayClassName.json")}, + {JsonUtils.classpathToObject("/json/chainr/specformat/bad_spec_ClassName.json")}, + {JsonUtils.classpathToObject("/json/chainr/specformat/bad_spec_NonTransformClass.json")}, + {JsonUtils.classpathToObject("/json/chainr/specformat/bad_spec_empty.json")} }; } - @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class ) + @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class) public void testBadSpecs(Object chainrSpec) { - new ChainrSpec( chainrSpec ); + new ChainrSpec(chainrSpec); } - @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class ) + @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class) public void staticChainrMethod(Object chainrSpec) { - Chainr.fromSpec( chainrSpec ); // should fail when parsing spec + Chainr.fromSpec(chainrSpec); // should fail when parsing spec } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrSpecLoadingTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrSpecLoadingTest.java similarity index 55% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrSpecLoadingTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrSpecLoadingTest.java index 0b5d967a..726331f6 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/ChainrSpecLoadingTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/ChainrSpecLoadingTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr; +package io.joltcommunity.jolt.chainr; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.chainr.instantiator.DefaultChainrInstantiator; -import com.bazaarvoice.jolt.chainr.spec.ChainrEntry; -import com.bazaarvoice.jolt.chainr.spec.ChainrSpec; -import com.bazaarvoice.jolt.exception.SpecException; +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.chainr.instantiator.DefaultChainrInstantiator; +import io.joltcommunity.jolt.chainr.spec.ChainrEntry; +import io.joltcommunity.jolt.chainr.spec.ChainrSpec; +import io.joltcommunity.jolt.exception.SpecException; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -29,23 +30,23 @@ public class ChainrSpecLoadingTest { @DataProvider public Object[][] badFormatSpecs() throws IOException { - return new Object[][] { - {JsonUtils.classpathToObject( "/json/chainr/specloading/bad_spec_SpecTransform.json" )} + return new Object[][]{ + {JsonUtils.classpathToObject("/json/chainr/specloading/bad_spec_SpecTransform.json")} }; } - @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class ) - public void testBadSpecs( Object chainrSpecObj ) { - ChainrSpec chainrSpec = new ChainrSpec( chainrSpecObj ); - ChainrEntry chainrEntry = chainrSpec.getChainrEntries().get( 0 ); + @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class) + public void testBadSpecs(Object chainrSpecObj) { + ChainrSpec chainrSpec = new ChainrSpec(chainrSpecObj); + ChainrEntry chainrEntry = chainrSpec.getChainrEntries().get(0); DefaultChainrInstantiator instantiator = new DefaultChainrInstantiator(); // This should fail - instantiator.hydrateTransform( chainrEntry ); + instantiator.hydrateTransform(chainrEntry); } - @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class ) - public void staticChainrMethod( Object chainrSpec ) { - Chainr.fromSpec( chainrSpec ); // should fail when parsing spec + @Test(dataProvider = "badFormatSpecs", expectedExceptions = SpecException.class) + public void staticChainrMethod(Object chainrSpec) { + Chainr.fromSpec(chainrSpec); // should fail when parsing spec } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/BadSpecTransform.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/BadSpecTransform.java similarity index 77% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/BadSpecTransform.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/BadSpecTransform.java index dc04e25a..ae1b3e87 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/BadSpecTransform.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/BadSpecTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.SpecDriven; -import com.bazaarvoice.jolt.Transform; +import io.joltcommunity.jolt.SpecDriven; +import io.joltcommunity.jolt.Transform; /** * Chainr should barf on this class, as it is a SpecTransform without a single arg constructor. @@ -25,7 +26,7 @@ public class BadSpecTransform implements SpecDriven, Transform { @Override - public Object transform( Object input ) { + public Object transform(Object input) { return input; } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/ExplodingTestTransform.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/ExplodingTestTransform.java similarity index 65% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/ExplodingTestTransform.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/ExplodingTestTransform.java index b674cadf..1e6fe4e6 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/ExplodingTestTransform.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/ExplodingTestTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.ContextualTransform; -import com.bazaarvoice.jolt.exception.TransformException; +import io.joltcommunity.jolt.ContextualTransform; +import io.joltcommunity.jolt.exception.TransformException; import java.util.Map; public class ExplodingTestTransform implements ContextualTransform { @Override - public Object transform( Object input, Map context ) { - throw new TransformException( "kaboom" ); + public Object transform(Object input, Map context) { + throw new TransformException("kaboom"); } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodContextDrivenTransform.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodContextDrivenTransform.java similarity index 74% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodContextDrivenTransform.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodContextDrivenTransform.java index 2cf29af1..0fceaff0 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodContextDrivenTransform.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodContextDrivenTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.ContextualTransform; +import io.joltcommunity.jolt.ContextualTransform; import java.util.Map; @@ -26,11 +27,11 @@ public class GoodContextDrivenTransform implements ContextualTransform { private static final String STATIC_KEY = "c"; @Override - public Object transform( Object input, Map context ) { + public Object transform(Object input, Map context) { String contextValue = (String) context.get(CONTEXT_KEY); - ((Map) input).put( STATIC_KEY, contextValue ); + ((Map) input).put(STATIC_KEY, contextValue); return input; } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodSpecAndContextDrivenTransform.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodSpecAndContextDrivenTransform.java similarity index 61% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodSpecAndContextDrivenTransform.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodSpecAndContextDrivenTransform.java index 9228dc4f..6dcdcbeb 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodSpecAndContextDrivenTransform.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodSpecAndContextDrivenTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.ContextualTransform; -import com.bazaarvoice.jolt.SpecDriven; +import io.joltcommunity.jolt.ContextualTransform; +import io.joltcommunity.jolt.SpecDriven; +import jakarta.inject.Inject; -import javax.inject.Inject; import java.util.Map; public class GoodSpecAndContextDrivenTransform implements SpecDriven, ContextualTransform { @@ -30,16 +31,16 @@ public class GoodSpecAndContextDrivenTransform implements SpecDriven, Contextual private final String specKeyValue; @Inject - public GoodSpecAndContextDrivenTransform( Object spec ) { - specKeyValue = (String) ((Map) spec).get( SPEC_DRIVEN_KEY ); + public GoodSpecAndContextDrivenTransform(Object spec) { + specKeyValue = (String) ((Map) spec).get(SPEC_DRIVEN_KEY); } @Override - public Object transform( Object input, Map context ) { + public Object transform(Object input, Map context) { - String contextValue = (String) context.get( CONTEXT_KEY ); + String contextValue = (String) context.get(CONTEXT_KEY); - ((Map) input).put( specKeyValue, contextValue ); + ((Map) input).put(specKeyValue, contextValue); return input; } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodTestTransform.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodTestTransform.java similarity index 66% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodTestTransform.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodTestTransform.java index d6ec9dfb..f64f8531 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/GoodTestTransform.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/GoodTestTransform.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,24 +14,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; -import com.bazaarvoice.jolt.SpecDriven; -import com.bazaarvoice.jolt.Transform; - -import javax.inject.Inject; +import io.joltcommunity.jolt.SpecDriven; +import io.joltcommunity.jolt.Transform; +import jakarta.inject.Inject; public class GoodTestTransform implements SpecDriven, Transform { private final Object spec; @Inject - public GoodTestTransform( Object spec ) { + public GoodTestTransform(Object spec) { this.spec = spec; } @Override - public Object transform( Object input ) { - return new TransformTestResult( input, spec ); + public Object transform(Object input) { + return new TransformTestResult(input, spec); } } diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/TransformTestResult.java b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/TransformTestResult.java similarity index 80% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/TransformTestResult.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/TransformTestResult.java index 2a09e83b..573de8d0 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/chainr/transforms/TransformTestResult.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/chainr/transforms/TransformTestResult.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,14 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.chainr.transforms; +package io.joltcommunity.jolt.chainr.transforms; public class TransformTestResult { public final Object input; public final Object spec; - TransformTestResult( Object input, Object spec ) { + TransformTestResult(Object input, Object spec) { this.input = input; this.spec = spec; } diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/DeepCopyTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/DeepCopyTest.java new file mode 100644 index 00000000..568bc4fb --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/DeepCopyTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import io.joltcommunity.jolt.JoltTestUtil; +import io.joltcommunity.jolt.JsonUtils; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Map; + +public class DeepCopyTest { + + @Test + public void deepCopyTest() throws Exception { + + Object input = JsonUtils.classpathToObject("/json/deepcopy/original.json"); + + Map fiddle = (Map) DeepCopy.simpleDeepCopy(input); + + JoltTestUtil.runDiffy("Verify that the DeepCopy did in fact make a copy.", input, fiddle); + + // The test is to make a deep copy, then manipulate the copy, and verify that the original did not change ;) + // copy and fiddle + List array = (List) fiddle.get("array"); + array.add("c"); + array.set(1, 3); + Map subMap = (Map) fiddle.get("map"); + subMap.put("c", "c"); + subMap.put("b", 3); + + // Verify that the input to the copy was unmodified + Object unmodified = JsonUtils.classpathToObject("/json/deepcopy/original.json"); + JoltTestUtil.runDiffy("Verify that the deepcopy was actually deep / input is unmodified", unmodified, input); + + // Verify we made the modifications we wanted to. + Object expectedModified = JsonUtils.classpathToObject("/json/deepcopy/modifed.json"); + JoltTestUtil.runDiffy("Verify fiddled post deepcopy object looks correct / was modifed.", expectedModified, fiddle); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testSimpleDeepCopy_NonSerializable() { + Object nonSerializable = new Object(); // Not Serializable + DeepCopy.simpleDeepCopy(nonSerializable); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/PathElementBuilderTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/PathElementBuilderTest.java new file mode 100644 index 00000000..1d36dbb2 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/PathElementBuilderTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import io.joltcommunity.jolt.common.pathelement.LiteralPathElement; +import io.joltcommunity.jolt.common.pathelement.PathElement; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class PathElementBuilderTest { + @Test(expectedExceptions = SpecException.class) + public void testTooManyArrayBrackets() { + PathElementBuilder.parseSingleKeyLHS("[foo][bar]"); + } + + @Test(expectedExceptions = SpecException.class) + public void testTooManyArrayBracketsWithEscapes() { + PathElementBuilder.parseSingleKeyLHS("[foo\\][bar]"); + } + + @Test(expectedExceptions = SpecException.class) + public void testInvalidAtInMiddle() { + PathElementBuilder.parseSingleKeyLHS("foo@bar"); + } + + public void testInvalidAtInMiddleWithEscapes() { + PathElement pe = PathElementBuilder.parseSingleKeyLHS("foo\\@bar"); + Assert.assertTrue(pe instanceof LiteralPathElement); + } + + @Test(expectedExceptions = SpecException.class) + public void testMixStarAndAmp() { + PathElementBuilder.parseSingleKeyLHS("foo*&bar"); + } + + @Test(expectedExceptions = SpecException.class) + public void testInvalidTransposeKey() { + // Assuming TransposePathElement.parse throws SpecException for invalid keys + PathElementBuilder.parseSingleKeyLHS("@("); + } + + @Test(expectedExceptions = SpecException.class) + public void testInvalidTransposeKeyWithPrefix() { + PathElementBuilder.parseSingleKeyLHS("foo@bar"); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testNullInput() { + PathElementBuilder.parseSingleKeyLHS(null); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/SpecStringParserTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/SpecStringParserTest.java new file mode 100644 index 00000000..b4ea719a --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/SpecStringParserTest.java @@ -0,0 +1,155 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common; + +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.List; + +public class SpecStringParserTest { + @Test + public void testSimpleDotNotation() { + String input = "foo.bar.baz"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo", "bar", "baz")); + } + + @Test + public void testEscapedDot() { + String input = "foo\\.bar.baz"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo.bar", "baz")); + } + + @Test + public void testEscapedEscape() { + String input = "foo\\\\bar.baz"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo\\bar", "baz")); + } + + @Test + public void testAtPathElement() { + String input = "foo.@(bar.baz).qux"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo", "@(bar.baz)", "qux")); + } + + @Test + public void testEscapedAt() { + String input = "foo.\\@bar.baz"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo", "\\@bar", "baz")); + } + + @Test + public void testEmptyInput() { + String input = ""; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testTrailingDot() { + String input = "foo.bar."; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo", "bar")); + } + + @Test + public void testLeadingDot() { + String input = ".foo.bar"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo", "bar")); + } + + @Test + public void testBracketedAt() { + String input = "foo.@(bar.[baz]).qux"; + List result = SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + Assert.assertEquals(result, List.of("foo", "@(bar.[baz])", "qux")); + } + + @Test(expectedExceptions = SpecException.class) + public void testInvalidAtPathElementThrowsException() { + // Invalid: "@." is not a valid AtPathElement + String input = "foo.@.bar"; + SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + } + + @Test(expectedExceptions = SpecException.class) + public void testUnmatchedParenthesisThrowsException() { + // Invalid: "@(bar.baz" has unmatched parenthesis + String input = "foo.@(bar.baz.qux"; + SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(input), + input + ); + } + + @Test(expectedExceptions = NullPointerException.class) + public void testNullInputThrowsException() { + SpecStringParser.parseDotNotation( + new ArrayList<>(), + SpecStringParser.stringIterator(null), + null + ); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/AmpPathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/AmpPathElementTest.java new file mode 100644 index 00000000..b1ef5c9a --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/AmpPathElementTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.reference.AmpReference; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +public class AmpPathElementTest { + + @Test + public void testConstructorWithLiteralOnly() { + AmpPathElement element = new AmpPathElement("photos"); + List tokens = element.getTokens(); + Assert.assertEquals(tokens.size(), 1); + Assert.assertEquals(tokens.get(0), "photos"); + Assert.assertEquals(element.getCanonicalForm(), "photos"); + } + + @Test + public void testConstructorWithReferenceOnly() { + AmpPathElement element = new AmpPathElement("&(1,1)"); + List tokens = element.getTokens(); + Assert.assertEquals(tokens.size(), 1); + Assert.assertTrue(tokens.get(0) instanceof AmpReference); + Assert.assertEquals(element.getCanonicalForm(), "&(1,1)"); + } + + @Test + public void testConstructorWithLiteralAndReference() { + AmpPathElement element = new AmpPathElement("photos-&(1,1)"); + List tokens = element.getTokens(); + Assert.assertEquals(tokens.size(), 2); + Assert.assertEquals(tokens.get(0), "photos-"); + Assert.assertTrue(tokens.get(1) instanceof AmpReference); + Assert.assertEquals(element.getCanonicalForm(), "photos-&(1,1)"); + } + + @Test + public void testConstructorWithMultipleReferences() { + AmpPathElement element = new AmpPathElement("a&b&(2,3)c"); + List tokens = element.getTokens(); + Assert.assertEquals(tokens.size(), 5); + Assert.assertEquals(tokens.get(0), "a"); + Assert.assertTrue(tokens.get(1) instanceof AmpReference); + Assert.assertEquals(tokens.get(2), "b"); + Assert.assertTrue(tokens.get(3) instanceof AmpReference); + Assert.assertEquals(tokens.get(4), "c"); + Assert.assertEquals(element.getCanonicalForm(), "a&(0,0)b&(2,3)c"); + } + + @Test + public void testConstructorWithEmptyString() { + AmpPathElement element = new AmpPathElement(""); + List tokens = element.getTokens(); + Assert.assertTrue(tokens.isEmpty()); + Assert.assertEquals(element.getCanonicalForm(), ""); + } + + @Test + public void testEvaluateWithSingleReference() { + AmpPathElement element = new AmpPathElement("&(1,2)"); + WalkedPath walkedPath = new WalkedPath(); + walkedPath.add(null, new MatchedElement("root")); + walkedPath.add(null, new MatchedElement("photos-foo-bar", List.of("foo", "bar"))); + walkedPath.add(null, new MatchedElement("current")); + Assert.assertEquals(element.evaluate(walkedPath), "bar"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/ArrayPathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/ArrayPathElementTest.java new file mode 100644 index 00000000..4832ad13 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/ArrayPathElementTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.reference.AmpReference; +import io.joltcommunity.jolt.common.reference.HashReference; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class ArrayPathElementTest { + @Test + public void testConstructorAutoExpand() { + ArrayPathElement ape = new ArrayPathElement("[]"); + Assert.assertEquals(ape.getCanonicalForm(), "[]"); + Assert.assertFalse(ape.isExplicitArrayIndex()); + } + + @Test + public void testConstructorExplicitIndex() { + ArrayPathElement ape = new ArrayPathElement("[3]"); + Assert.assertEquals(ape.getCanonicalForm(), "[3]"); + Assert.assertTrue(ape.isExplicitArrayIndex()); + Assert.assertEquals(ape.getExplicitArrayIndex(), Integer.valueOf(3)); + } + + @Test + public void testConstructorReference() { + ArrayPathElement ape = new ArrayPathElement("[&0]"); + Assert.assertEquals(ape.getCanonicalForm(), "[&(0,0)]"); + } + + @Test + public void testConstructorHash() { + ArrayPathElement ape = new ArrayPathElement("[#0]"); + Assert.assertEquals(ape.getCanonicalForm(), "[#0]"); + } + + @Test + public void testConstructorTranspose() { + ArrayPathElement ape = new ArrayPathElement("[@(1,foo)]"); + Assert.assertEquals(ape.getCanonicalForm(), "[@(1,foo)]"); + } + + @Test(expectedExceptions = SpecException.class) + public void testConstructorInvalidKeyNoBrackets() { + new ArrayPathElement("foo"); + } + + @Test(expectedExceptions = SpecException.class) + public void testConstructorInvalidExplicitIndex() { + new ArrayPathElement("[abc]"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/HashPathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/HashPathElementTest.java new file mode 100644 index 00000000..b6b80539 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/HashPathElementTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.annotations.Test; + +public class HashPathElementTest { + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "HashPathElement cannot have empty String as input\\.") + public void testBlankKeyThrowsException() { + new HashPathElement(""); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "LHS # should start with a # : foo") + public void testKeyWithoutHashThrowsException() { + new HashPathElement("foo"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "HashPathElement input is too short : #") + public void testKeyTooShortThrowsException() { + new HashPathElement("#"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "HashPathElement, mismatched parens : #\\(foo") + public void testMismatchedParensThrowsException() { + new HashPathElement("#(foo"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarAllPathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarAllPathElementTest.java new file mode 100644 index 00000000..5a618673 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarAllPathElementTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.annotations.Test; + +public class StarAllPathElementTest { + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarAllPathElement key should just be a single '\\*'\\. Was: foo") + public void testNonStarKeyThrowsException() { + new StarAllPathElement("foo"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarAllPathElement key should just be a single '\\*'\\. Was: \\*\\*") + public void testMultipleStarsThrowsException() { + new StarAllPathElement("**"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarAllPathElement key should just be a single '\\*'\\. Was: ") + public void testEmptyKeyThrowsException() { + new StarAllPathElement(""); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarDoublePathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarDoublePathElementTest.java new file mode 100644 index 00000000..31d88e9f --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarDoublePathElementTest.java @@ -0,0 +1,150 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class StarDoublePathElementTest { + + @Test + public void testStarInFirstAndMiddle() { + + StarPathElement star = new StarDoublePathElement("*a*"); + + Assert.assertTrue(star.stringMatch("bbbaaccccc")); + Assert.assertFalse(star.stringMatch("abbbbbbbbcc")); + Assert.assertFalse(star.stringMatch("bbba")); + + MatchedElement lpe = star.match("bbbaccc", null); + // * -> bbb + // a -> a + // * -> ccc + Assert.assertEquals(lpe.getSubKeyRef(0), "bbbaccc"); + Assert.assertEquals(lpe.getSubKeyRef(1), "bbb"); + Assert.assertEquals(lpe.getSubKeyRef(2), "ccc"); + Assert.assertEquals(lpe.getSubKeyCount(), 3); + + } + + @Test + public void testStarAtFrontAndEnd() { + + StarPathElement star = new StarDoublePathElement("*a*c"); + + Assert.assertTrue(star.stringMatch("bbbbadddc")); + Assert.assertTrue(star.stringMatch("bacc")); + Assert.assertFalse(star.stringMatch("bac")); + Assert.assertFalse(star.stringMatch("baa")); + + MatchedElement lpe = star.match("abcadefc", null); + // * -> abc + // a -> a index 4 + // * -> def + // c -> c + Assert.assertEquals(lpe.getSubKeyRef(0), "abcadefc"); + Assert.assertEquals(lpe.getSubKeyRef(1), "abc"); + Assert.assertEquals(lpe.getSubKeyRef(2), "def"); + Assert.assertEquals(lpe.getSubKeyCount(), 3); + + } + + @Test + public void testStarAtMiddleAndEnd() { + + StarPathElement star = new StarDoublePathElement("a*b*"); + + Assert.assertTrue(star.stringMatch("adbc")); + Assert.assertTrue(star.stringMatch("abbc")); + Assert.assertFalse(star.stringMatch("adddddd")); + Assert.assertFalse(star.stringMatch("addb")); + Assert.assertFalse(star.stringMatch("abc")); + + MatchedElement lpe = star.match("abcbbac", null); + // a -> a + // * -> bc index 1 + // b -> b index 3 + // * -> bac index 4 + // c -> c + Assert.assertEquals(lpe.getSubKeyRef(0), "abcbbac"); + Assert.assertEquals(lpe.getSubKeyRef(1), "bc"); + Assert.assertEquals(lpe.getSubKeyRef(2), "bac"); + Assert.assertEquals(lpe.getSubKeyCount(), 3); + + } + + + @Test + public void testStarsInMiddle() { + + StarPathElement star = new StarDoublePathElement("a*b*c"); + + Assert.assertTrue(star.stringMatch("a123b456c")); + Assert.assertTrue(star.stringMatch("abccbcc")); + + MatchedElement lpe = star.match("abccbcc", null); + // a -> a + // * -> bcc index 1 + // b -> b + // * -> c index 2 + // c -> c + Assert.assertEquals(lpe.getSubKeyRef(0), "abccbcc"); + Assert.assertEquals(lpe.getSubKeyRef(1), "bcc"); + Assert.assertEquals(lpe.getSubKeyRef(2), "c"); + Assert.assertEquals(lpe.getSubKeyCount(), 3); + + } + + + @Test + public void testStarsInMiddleNonGreedy() { + + StarPathElement star = new StarDoublePathElement("a*b*c"); + + MatchedElement lpe = star.match("abbccbccc", null); + // a -> a + // * -> b index 1 + // b -> b + // * -> ccbcc index 2 + // c -> c + Assert.assertEquals(lpe.getSubKeyRef(0), "abbccbccc"); + Assert.assertEquals(lpe.getSubKeyRef(1), "b"); + Assert.assertEquals(lpe.getSubKeyRef(2), "ccbcc"); + Assert.assertEquals(lpe.getSubKeyCount(), 3); + + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarDoublePathElement should have two '\\*' in its key\\. Was: abc") + public void testNoStarThrowsException() { + new StarDoublePathElement("abc"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarDoublePathElement should have two '\\*' in its key\\. Was: a\\*b") + public void testOneStarThrowsException() { + new StarDoublePathElement("a*b"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarDoublePathElement should have two '\\*' in its key\\. Was: a\\*b\\*c\\*d") + public void testMoreThanTwoStarsThrowsException() { + new StarDoublePathElement("a*b*c*d"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarRegexPathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarRegexPathElementTest.java new file mode 100644 index 00000000..a49a5cbc --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarRegexPathElementTest.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.tree.MatchedElement; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class StarRegexPathElementTest { + + @DataProvider + public Object[][] getStarPatternTests() { + return new Object[][]{ + {"easy star test", "rating-*-*", "rating-tuna-marlin", "tuna", "marlin"}, + {"easy facet usage", "terms--config--*--*--cdv", "terms--config--Expertise--12345--cdv", "Expertise", "12345"}, + {"degenerate ProductId in facet", "terms--config--*--*--cdv", "terms--config--Expertise--12345--6789--cdv", "Expertise", "12345--6789"}, + {"multi metachar test", "rating.$.*.*", "rating.$.marlin$.test.", "marlin$", "test."}, + }; + } + + @Test(dataProvider = "getStarPatternTests") + public void starPatternTest(String testName, String spec, String dataKey, String expected1, String expected2) { + + StarPathElement star = new StarRegexPathElement(spec); + + MatchedElement lpe = star.match(dataKey, null); + + Assert.assertEquals(3, lpe.getSubKeyCount()); + Assert.assertEquals(dataKey, lpe.getSubKeyRef(0)); + Assert.assertEquals(expected1, lpe.getSubKeyRef(1)); + Assert.assertEquals(expected2, lpe.getSubKeyRef(2)); + } + + @Test + public void mustMatchSomethingTest() { + + StarPathElement star = new StarRegexPathElement("tuna-*-*"); + + Assert.assertNull(star.match("tuna--", null)); + Assert.assertNull(star.match("tuna-bob-", null)); + Assert.assertNull(star.match("tuna--bob", null)); + + StarPathElement multiMetacharStarpathelement = new StarRegexPathElement("rating-$-*-*"); + + Assert.assertNull(multiMetacharStarpathelement.match("rating-capGrp1-capGrp2", null)); + Assert.assertNull(multiMetacharStarpathelement.match("rating-$capGrp1-capGrp2", null)); + Assert.assertNotNull(multiMetacharStarpathelement.match("rating-$-capGrp1-capGrp2", null)); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarSinglePathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarSinglePathElementTest.java new file mode 100644 index 00000000..3e2d202f --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/StarSinglePathElementTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class StarSinglePathElementTest { + + @Test + public void testStarAtFront() { + + StarPathElement star = new StarSinglePathElement("*-tuna"); + Assert.assertTrue(star.stringMatch("tuna-tuna")); + Assert.assertTrue(star.stringMatch("bob-tuna")); + Assert.assertFalse(star.stringMatch("-tuna")); // * has to catch something + Assert.assertFalse(star.stringMatch("tuna")); + Assert.assertFalse(star.stringMatch("tuna-bob")); + + MatchedElement lpe = star.match("bob-tuna", null); + Assert.assertEquals(lpe.getSubKeyRef(0), "bob-tuna"); + Assert.assertEquals(lpe.getSubKeyRef(1), "bob"); + Assert.assertEquals(lpe.getSubKeyCount(), 2); + + Assert.assertNull(star.match("-tuna", null)); + } + + @Test + public void testStarAtEnd() { + + StarPathElement star = new StarSinglePathElement("tuna-*"); + Assert.assertTrue(star.stringMatch("tuna-tuna")); + Assert.assertTrue(star.stringMatch("tuna-bob")); + Assert.assertFalse(star.stringMatch("tuna-")); + Assert.assertFalse(star.stringMatch("tuna")); + Assert.assertFalse(star.stringMatch("bob-tuna")); + + MatchedElement lpe = star.match("tuna-bob", null); + Assert.assertEquals(lpe.getSubKeyRef(0), "tuna-bob"); + Assert.assertEquals(lpe.getSubKeyRef(1), "bob"); + Assert.assertEquals(lpe.getSubKeyCount(), 2); + + Assert.assertNull(star.match("tuna-", null)); + } + + @Test + public void testStarInMiddle() { + + StarPathElement star = new StarSinglePathElement("tuna-*-marlin"); + Assert.assertTrue(star.stringMatch("tuna-tuna-marlin")); + Assert.assertTrue(star.stringMatch("tuna-bob-marlin")); + Assert.assertFalse(star.stringMatch("tuna--marlin")); + Assert.assertFalse(star.stringMatch("tunamarlin")); + Assert.assertFalse(star.stringMatch("marlin-bob-tuna")); + + MatchedElement lpe = star.match("tuna-bob-marlin", null); + Assert.assertEquals(lpe.getSubKeyRef(0), "tuna-bob-marlin"); + Assert.assertEquals(lpe.getSubKeyRef(1), "bob"); + Assert.assertEquals(lpe.getSubKeyCount(), 2); + + Assert.assertNull(star.match("bob", null)); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarSinglePathElement should only have one '\\*' in its key\\. Was: .*") + public void testMultipleStarsThrowsException() { + new StarSinglePathElement("foo**bar"); + } + + @Test(expectedExceptions = SpecException.class, + expectedExceptionsMessageRegExp = "StarSinglePathElement should have a key that is just '\\*'\\. Was: \\*") + public void testSingleStarThrowsException() { + new StarSinglePathElement("*"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/TransposePathElementTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/TransposePathElementTest.java new file mode 100644 index 00000000..47d4f7f0 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/pathelement/TransposePathElementTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.pathelement; + +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.PathStep; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.annotations.Test; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; + +public class TransposePathElementTest { + + @Test + public void testParseSimpleKey() { + TransposePathElement element = TransposePathElement.parse("@author"); + assertEquals(element.getCanonicalForm(), "@(0,author)"); + } + + @Test + public void testParseNumericKey() { + TransposePathElement element = TransposePathElement.parse("@2"); + assertEquals(element.getCanonicalForm(), "@(2,)"); + } + + @Test + public void testParseKeyWithSubPath() { + TransposePathElement element = TransposePathElement.parse("@2,book"); + assertEquals(element.getCanonicalForm(), "@(2,book)"); + } + + @Test + public void testParseKeyWithParens() { + TransposePathElement element = TransposePathElement.parse("@(author)"); + assertEquals(element.getCanonicalForm(), "@(0,author)"); + } + + @Test + public void testParseKeyWithEscape() { + TransposePathElement element = TransposePathElement.parse("@(a.b\\.c)"); + assertEquals(element.getCanonicalForm(), "@(0,a.b\\.c)"); + } + + @Test + public void testParseKeyWithAmpPathReference() { + TransposePathElement element = TransposePathElement.parse("@(a.&2.c)"); + assertEquals(element.getCanonicalForm(), "@(0,a.&(2,0).c)"); + } + + @Test + public void testParseKeyWithAmpPathReferenceDirectly() { + TransposePathElement element = TransposePathElement.parse("@&1"); + assertEquals(element.getCanonicalForm(), "@(0,&(1,0))"); + } + + @Test(expectedExceptions = SpecException.class) + public void testParseInvalidKeyThrowsException() { + TransposePathElement.parse("@"); + } + + @Test(expectedExceptions = SpecException.class) + public void testParseKeyWithNestedAtThrowsException() { + TransposePathElement.parse("@author@book"); + } + + @Test(expectedExceptions = SpecException.class) + public void testParseKeyWithWildcardThrowsException() { + TransposePathElement.parse("@author*"); + } + + @Test + public void testEvaluateReturnsNullForNonString() { + Date date = new Date(); + Map map = new HashMap<>(); + map.put("date", date); + WalkedPath walkedPath = new WalkedPath(); + PathStep pathStep = new PathStep(map, new MatchedElement("date")); + walkedPath.add(pathStep); + TransposePathElement pe = TransposePathElement.parse("@date"); + String result = pe.evaluate(walkedPath); + assertNull(result, "Expected null for non-string data type"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/reference/PathAndGroupReferenceTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/reference/PathAndGroupReferenceTest.java new file mode 100644 index 00000000..e0076b99 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/reference/PathAndGroupReferenceTest.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.reference; + +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class PathAndGroupReferenceTest { + + @DataProvider + public Object[][] getValidReferenceTests() { + return new Object[][]{ + {"", 0, 0, "(0,0)"}, + {"3", 3, 0, "(3,0)"}, + {"(3)", 3, 0, "(3,0)"}, + {"(1,2)", 1, 2, "(1,2)"} + }; + } + + @Test(dataProvider = "getValidReferenceTests") + public void validAmpReferencePatternTest(String key, int pathIndex, int keyGroup, String canonicalForm) { + + PathAndGroupReference amp = new AmpReference("&" + key); + Assert.assertEquals(pathIndex, amp.getPathIndex()); + Assert.assertEquals(keyGroup, amp.getKeyGroup()); + Assert.assertEquals("&" + canonicalForm, amp.getCanonicalForm()); + } + + @Test(dataProvider = "getValidReferenceTests") + public void validDollarReferencePatternTest(String key, int pathIndex, int keyGroup, String canonicalForm) { + + PathAndGroupReference amp = new DollarReference("$" + key); + Assert.assertEquals(pathIndex, amp.getPathIndex()); + Assert.assertEquals(keyGroup, amp.getKeyGroup()); + Assert.assertEquals("$" + canonicalForm, amp.getCanonicalForm()); + } + + + @DataProvider + public Object[][] getFailReferenceTests() { + return new Object[][]{ + {"pants"}, + {"-1"}, + {"(-1,2)"}, + {"(1,-2)"}, + }; + } + + @Test(dataProvider = "getFailReferenceTests", expectedExceptions = SpecException.class) + public void failAmpReferencePatternTest(String key) { + new AmpReference("&" + key); + } + + @Test(dataProvider = "getFailReferenceTests", expectedExceptions = SpecException.class) + public void failDollarReferencePatternTest(String key) { + new DollarReference("$" + key); + } + + @DataProvider + public Object[][] invalidReferenceTests() { + return new Object[][]{ + {null}, + {""}, + {"pants"}, + }; + } + + @Test(dataProvider = "invalidReferenceTests", expectedExceptions = SpecException.class) + public void invalidAmpReferencePatternTest(String key) { + new AmpReference(key); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/common/reference/PathReferenceTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/common/reference/PathReferenceTest.java new file mode 100644 index 00000000..7e0fab54 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/common/reference/PathReferenceTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.common.reference; + +import io.joltcommunity.jolt.exception.SpecException; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class PathReferenceTest { + + @DataProvider + public Object[][] getValidReferenceTests() { + return new Object[][]{ + {"", 0, "0"}, + {"3", 3, "3"}, + {"12", 12, "12"} + }; + } + + @Test(dataProvider = "getValidReferenceTests") + public void validAmpReferencePatternTest(String key, int pathIndex, String canonicalForm) { + + PathReference ref = new HashReference("#" + key); + Assert.assertEquals(pathIndex, ref.getPathIndex()); + Assert.assertEquals("#" + canonicalForm, ref.getCanonicalForm()); + } + + + @DataProvider + public Object[][] getFailReferenceTests() { + return new Object[][]{ + {"pants"}, + {"-1"}, + {"(1)"} + }; + } + + @Test(dataProvider = "getFailReferenceTests", expectedExceptions = SpecException.class) + public void failHashReferencePatternTest(String key) { + new HashReference("#" + key); + } + + @DataProvider + public Object[][] invalidReferenceTests() { + return new Object[][]{ + {null}, + {""}, + {"pants"}, + }; + } + + @Test(dataProvider = "invalidReferenceTests", expectedExceptions = SpecException.class) + public void invalidHashReferencePatternTest(String key) { + new HashReference(key); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrExternalApiTestHelper.java b/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrExternalApiTestHelper.java new file mode 100644 index 00000000..f9697a60 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrExternalApiTestHelper.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.JsonUtils; + +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.CompletionStage; + +public class EnrichrExternalApiTestHelper { + + private final String baseUrl; + private final HttpClient httpClient; + + public EnrichrExternalApiTestHelper( String baseUrl ) { + this.baseUrl = baseUrl; + this.httpClient = HttpClient.newHttpClient(); + } + + public CompletionStage lookupProfile( Object value, Object input, Map context ) { + String tenant = context == null || context.get( "tenant" ) == null ? "" : String.valueOf( context.get( "tenant" ) ); + String uri = baseUrl + "/profiles/" + value + "?tenant=" + URLEncoder.encode( tenant, StandardCharsets.UTF_8 ); + + HttpRequest request = HttpRequest.newBuilder( URI.create( uri ) ).GET().build(); + + return httpClient.sendAsync( request, HttpResponse.BodyHandlers.ofString() ) + .thenApply( response -> { + if ( response.statusCode() >= 400 ) { + throw new IllegalStateException( "Unexpected HTTP status: " + response.statusCode() ); + } + return JsonUtils.jsonToObject( response.body() ); + } ); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrInternalsTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrInternalsTest.java new file mode 100644 index 00000000..4c75dc56 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrInternalsTest.java @@ -0,0 +1,522 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import io.joltcommunity.jolt.exception.SpecException; +import io.joltcommunity.jolt.exception.TransformException; +import io.joltcommunity.jolt.traversr.SimpleTraversr; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class EnrichrInternalsTest { + + @Test( expectedExceptions = SpecException.class ) + public void executionMode_rejectsNonStringValues() { + EnrichrExecutionMode.fromSpec( 1 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void executionMode_rejectsBlankValues() { + EnrichrExecutionMode.fromSpec( " " ); + } + + @Test( expectedExceptions = SpecException.class ) + public void manager_rejectsNonMapSpecs() { + new EnrichrManager( "not-a-map", 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void manager_rejectsBlankRequiredStrings() { + new EnrichrManager( rule( " ", null, EnrichrTestHelper.class.getName(), null, "uppercase" ), 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void manager_rejectsNonStringRequiredStrings() { + Map rule = rule( "name", null, EnrichrTestHelper.class.getName(), null, "uppercase" ); + rule.put( "path", 1 ); + + new EnrichrManager( rule, 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void manager_rejectsBlankOptionalStrings() { + new EnrichrManager( rule( "name", " ", EnrichrTestHelper.class.getName(), null, "uppercase" ), 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void manager_rejectsNonStringOptionalStrings() { + Map rule = rule( "name", null, EnrichrTestHelper.class.getName(), null, "uppercase" ); + rule.put( "outputPath", 1 ); + + new EnrichrManager( rule, 0 ); + } + + @Test + public void manager_returnsNullWhenTheSourcePathIsMissing() { + EnrichrManager manager = new EnrichrManager( rule( "customer.id", null, EnrichrTestHelper.class.getName(), null, "uppercase" ), 0 ); + + Assert.assertTrue( manager.match( new LinkedHashMap<>() ).isEmpty() ); + } + + @Test + public void manager_supportsLeadingDotPaths() { + EnrichrManager manager = new EnrichrManager( rule( ".name", null, EnrichrTestHelper.class.getName(), null, "uppercase" ), 0 ); + Map input = new LinkedHashMap<>(); + input.put( "name", "alice" ); + + List matches = manager.match( input ); + + Assert.assertEquals( matches.size(), 1 ); + Assert.assertNotNull( manager.prepare( matches.get( 0 ), input, null ) ); + } + + @Test + public void pathMatch_exposesResolvedMetadataAndDefensivelyCopiesLists() { + List resolvedKeys = new ArrayList<>( Arrays.asList( "customers", "0", "id" ) ); + List wildcardBindings = new ArrayList<>( Collections.singletonList( "0" ) ); + + EnrichrPathMatch match = new EnrichrPathMatch( "alice", resolvedKeys, wildcardBindings, "customers.[0].id" ); + + resolvedKeys.add( "mutated" ); + wildcardBindings.add( "mutated" ); + + Assert.assertEquals( match.getValue(), "alice" ); + Assert.assertEquals( match.getResolvedInputKeys(), Arrays.asList( "customers", "0", "id" ) ); + Assert.assertEquals( match.getWildcardBindings(), Collections.singletonList( "0" ) ); + Assert.assertEquals( match.getResolvedInputPath(), "customers.[0].id" ); + } + + @Test( expectedExceptions = UnsupportedOperationException.class ) + public void pathMatch_returnsImmutableResolvedKeys() { + EnrichrPathMatch match = new EnrichrPathMatch( + "alice", + new ArrayList<>( Collections.singletonList( "name" ) ), + new ArrayList(), + "name" + ); + + match.getResolvedInputKeys().add( "mutated" ); + } + + @Test( expectedExceptions = SpecException.class ) + public void pathTemplate_rejectsWhitespaceOnlyArraySegments() { + EnrichrPathTemplate.parseOutput( "customers.[ ].id", 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void pathTemplate_rejectsNegativeArrayIndices() { + EnrichrPathTemplate.parseOutput( "customers.[-1].id", 0 ); + } + + @Test + public void pathTemplate_treatsPartialBracketTokensAsMapKeys() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseInput( "[0", 0 ); + Map input = new LinkedHashMap<>(); + input.put( "[0", "alice" ); + + List matches = template.match( input ); + + Assert.assertEquals( matches.size(), 1 ); + Assert.assertEquals( matches.get( 0 ).getValue(), "alice" ); + } + + @Test + public void pathTemplate_match_returnsEmptyWhenIntermediateValueIsNull() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseInput( "customer.id", 0 ); + Map input = new LinkedHashMap<>(); + input.put( "customer", null ); + + Assert.assertTrue( template.match( input ).isEmpty() ); + } + + @Test + public void pathTemplate_match_ignoresMapSegmentsOnNonMaps() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseInput( "customer.id", 0 ); + + Assert.assertTrue( template.match( Collections.singletonList( "not-a-map" ) ).isEmpty() ); + } + + @Test + public void pathTemplate_match_ignoresArrayIndexSegmentsOnNonLists() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseInput( "customer.[0]", 0 ); + Map input = new LinkedHashMap<>(); + input.put( "customer", "not-a-list" ); + + Assert.assertTrue( template.match( input ).isEmpty() ); + } + + @Test + public void pathTemplate_match_ignoresOutOfBoundsArrayIndices() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseInput( "customer.[1]", 0 ); + Map input = new LinkedHashMap<>(); + input.put( "customer", Collections.singletonList( "only-one" ) ); + + Assert.assertTrue( template.match( input ).isEmpty() ); + } + + @Test + public void pathTemplate_match_ignoresWildcardSegmentsOnNonLists() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseInput( "customer.[*]", 0 ); + Map input = new LinkedHashMap<>(); + input.put( "customer", "not-a-list" ); + + Assert.assertTrue( template.match( input ).isEmpty() ); + } + + @Test + public void pathTemplate_match_treatsAppendSegmentsAsOutputOnly() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseOutput( "profiles.[]", 0 ); + Map input = new LinkedHashMap<>(); + input.put( "profiles", new ArrayList() ); + + Assert.assertTrue( template.match( input ).isEmpty() ); + Assert.assertTrue( template.hasAppendSegment() ); + } + + @Test + public void pathTemplate_resolvesExplicitArrayIndexOutputPaths() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseOutput( "customers.[0].profile", 0 ); + + Assert.assertEquals( template.resolveKeys( Collections.emptyList() ), Arrays.asList( "customers", "0", "profile" ) ); + Assert.assertEquals( template.resolvePath( Collections.emptyList() ), "customers.[0].profile" ); + Assert.assertNotNull( template.getTraversr() ); + Assert.assertEquals( template.getWildcardCount(), 0 ); + } + + @Test + public void pathTemplate_rejectsMissingWildcardBindings() { + EnrichrPathTemplate template = EnrichrPathTemplate.parseOutput( "customers.[*].profile", 0 ); + + try { + template.resolveKeys( Collections.emptyList() ); + Assert.fail( "Expected an IllegalArgumentException" ); + } + catch ( IllegalArgumentException e ) { + Assert.assertTrue( e.getMessage().contains( "Expected at least 1 wildcard bindings" ) ); + Assert.assertTrue( e.getMessage().contains( "customers.[*].profile" ) ); + } + } + + @Test( expectedExceptions = SpecException.class ) + public void methodInvoker_requiresEitherAClassNameOrContextKey() { + new EnrichrMethodInvoker( "uppercase", null, null, 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void methodInvoker_rejectsClassNameAndContextKeyTogether() { + new EnrichrMethodInvoker( "uppercase", "bean", EnrichrTestHelper.class.getName(), 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void methodInvoker_wrapsClassLoadingFailures() { + new EnrichrMethodInvoker( "uppercase", null, "does.not.Exist", 0 ); + } + + @Test( expectedExceptions = SpecException.class ) + public void methodInvoker_rejectsUnsupportedMethodSignatures() { + new EnrichrMethodInvoker( "invalid", null, InvalidSignatureHelper.class.getName(), 0 ); + } + + @Test + public void methodInvoker_supportsInstanceMethodsLoadedByClassName() throws Exception { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "instanceUppercase", null, InstanceHelper.class.getName(), 0 ); + + Assert.assertEquals( invoker.invokeAsync( "alice", null, null ).toCompletableFuture().get(), "ALICE" ); + } + + @Test + public void methodInvoker_supportsTwoArgumentMethods() throws Exception { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "appendInputSuffix", null, StaticHelper.class.getName(), 0 ); + Map input = new LinkedHashMap<>(); + input.put( "suffix", "tail" ); + + Assert.assertEquals( invoker.invokeAsync( "alice", input, null ).toCompletableFuture().get(), "alice-tail" ); + } + + @Test + public void methodInvoker_convertsNullResultsToCompletedStages() throws Exception { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "returnNull", null, StaticHelper.class.getName(), 0 ); + + Assert.assertNull( invoker.invokeAsync( "alice", null, null ).toCompletableFuture().get() ); + } + + @Test + public void methodInvoker_rejectsNullContextForContextBeans() { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "instanceUppercase", "bean", null, 0 ); + + try { + invoker.invokeAsync( "alice", null, null ); + Assert.fail( "Expected a TransformException" ); + } + catch ( TransformException e ) { + Assert.assertTrue( e.getMessage().contains( "transform context is null" ) ); + } + } + + @Test + public void methodInvoker_rejectsMissingContextTargets() { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "instanceUppercase", "bean", null, 0 ); + + try { + invoker.invokeAsync( "alice", null, new LinkedHashMap<>() ); + Assert.fail( "Expected a TransformException" ); + } + catch ( TransformException e ) { + Assert.assertTrue( e.getMessage().contains( "contextKey 'bean'" ) ); + } + } + + @Test + public void methodInvoker_reusesCachedContextMethods() throws Exception { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "instanceUppercase", "bean", null, 0 ); + Map context = new LinkedHashMap<>(); + context.put( "bean", new InstanceHelper() ); + + Assert.assertEquals( invoker.invokeAsync( "alice", null, context ).toCompletableFuture().get(), "ALICE" ); + Assert.assertEquals( invoker.invokeAsync( "bob", null, context ).toCompletableFuture().get(), "BOB" ); + } + + @Test + public void methodInvoker_wrapsInvocationFailures() { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "explode", null, StaticHelper.class.getName(), 0 ); + + try { + invoker.invokeAsync( "alice", null, null ); + Assert.fail( "Expected a TransformException" ); + } + catch ( TransformException e ) { + Assert.assertTrue( e.getCause() instanceof IllegalStateException ); + } + } + + @Test + public void methodInvoker_wrapsIllegalAccessFailures() { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( + "inaccessibleStatic", + null, + "io.joltcommunity.jolt.InaccessibleEnrichrMethodHelper", + 0 + ); + + try { + invoker.invokeAsync( "alice", null, null ); + Assert.fail( "Expected a TransformException" ); + } + catch ( TransformException e ) { + Assert.assertTrue( e.getCause() instanceof IllegalAccessException ); + } + } + + @Test + public void methodInvoker_rejectsPublishersThatEmitMoreThanOneValue() throws Exception { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "multiValuePublisher", null, StaticHelper.class.getName(), 0 ); + + try { + invoker.invokeAsync( "alice", null, null ).toCompletableFuture().get(); + Assert.fail( "Expected a failure from the multi-value publisher" ); + } + catch ( ExecutionException e ) { + Assert.assertTrue( e.getCause() instanceof TransformException ); + Assert.assertTrue( e.getCause().getMessage().contains( "at most one value" ) ); + } + } + + @Test + public void methodInvoker_surfacesPublisherErrors() throws Exception { + EnrichrMethodInvoker invoker = new EnrichrMethodInvoker( "errorPublisher", null, StaticHelper.class.getName(), 0 ); + + try { + invoker.invokeAsync( "alice", null, null ).toCompletableFuture().get(); + Assert.fail( "Expected a failure from the error publisher" ); + } + catch ( ExecutionException e ) { + Assert.assertTrue( e.getCause() instanceof IllegalArgumentException ); + Assert.assertEquals( e.getCause().getMessage(), "publisher failure" ); + } + } + + @Test + public void pendingEnrichment_rethrowsRuntimeFailures() { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally( new IllegalStateException( "boom" ) ); + + try { + newPendingEnrichment( future, "value" ).apply(); + Assert.fail( "Expected the runtime exception to be rethrown" ); + } + catch ( IllegalStateException e ) { + Assert.assertEquals( e.getMessage(), "boom" ); + } + } + + @Test + public void pendingEnrichment_wrapsCheckedFailures() { + CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally( new IOException( "checked" ) ); + + try { + newPendingEnrichment( future, "value" ).apply(); + Assert.fail( "Expected a TransformException" ); + } + catch ( TransformException e ) { + Assert.assertTrue( e.getCause() instanceof IOException ); + Assert.assertTrue( e.getMessage().contains( "outputPath 'value'" ) ); + } + } + + @Test + public void pendingEnrichment_restoresInterruptStatusWhenInterrupted() { + CompletableFuture future = new CompletableFuture<>(); + + try { + Thread.currentThread().interrupt(); + newPendingEnrichment( future, "value" ).apply(); + Assert.fail( "Expected a TransformException" ); + } + catch ( TransformException e ) { + Assert.assertTrue( Thread.currentThread().isInterrupted() ); + Assert.assertTrue( e.getMessage().contains( "interrupted" ) ); + } + finally { + Thread.interrupted(); + } + } + + private Map rule( String path, String outputPath, String className, String contextKey, String method ) { + Map rule = new LinkedHashMap<>(); + rule.put( "path", path ); + if ( outputPath != null ) { + rule.put( "outputPath", outputPath ); + } + if ( className != null ) { + rule.put( "className", className ); + } + if ( contextKey != null ) { + rule.put( "contextKey", contextKey ); + } + rule.put( "method", method ); + return rule; + } + + private EnrichrPendingEnrichment newPendingEnrichment( CompletableFuture future, String outputPath ) { + return new EnrichrPendingEnrichment( + new LinkedHashMap<>(), + new SimpleTraversr<>( outputPath ), + Collections.singletonList( outputPath ), + future, + outputPath + ); + } + + public static final class InstanceHelper { + public Object instanceUppercase( Object value ) { + return String.valueOf( value ).toUpperCase(); + } + } + + public static final class StaticHelper { + + public static Object appendInputSuffix( Object value, Object input ) { + @SuppressWarnings( "unchecked" ) + Map inputMap = (Map) input; + return value + "-" + inputMap.get( "suffix" ); + } + + public static Object returnNull( Object value ) { + return null; + } + + public static Object explode( Object value ) { + throw new IllegalStateException( "boom" ); + } + + public static Publisher multiValuePublisher( Object value, Object input, Map context ) { + return subscriber -> subscriber.onSubscribe( new Subscription() { + private boolean completed; + + @Override + public void request( long n ) { + if ( completed ) { + return; + } + + completed = true; + subscriber.onNext( value ); + subscriber.onNext( String.valueOf( value ).toUpperCase() ); + subscriber.onComplete(); + } + + @Override + public void cancel() { + completed = true; + } + } ); + } + + public static Publisher errorPublisher( Object value, Object input, Map context ) { + return subscriber -> subscriber.onSubscribe( new Subscription() { + private boolean completed; + + @Override + public void request( long n ) { + if ( completed ) { + return; + } + + completed = true; + subscriber.onError( new IllegalArgumentException( "publisher failure" ) ); + } + + @Override + public void cancel() { + completed = true; + } + } ); + } + } + + public static final class InvalidSignatureHelper { + + public static Object invalid() { + return "invalid"; + } + + public static Object invalid( Object value, int primitive ) { + return "invalid"; + } + + public static Object invalid( Object value, Object input, List context ) { + return "invalid"; + } + + public static Object invalid( Object one, Object two, Object three, Object four ) { + return "invalid"; + } + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrTestHelper.java b/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrTestHelper.java new file mode 100644 index 00000000..96f3d6a4 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/enrich/EnrichrTestHelper.java @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Jolt Community + * + * 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 io.joltcommunity.jolt.enrich; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; + +public class EnrichrTestHelper { + + public static Object uppercase( Object value ) { + return String.valueOf( value ).toUpperCase(); + } + + public static CompletionStage asyncUppercase( Object value ) { + return CompletableFuture.completedFuture( uppercase( value ) ); + } + + public static Object describe( Object value, Object input, Map context ) { + Map enriched = new LinkedHashMap<>(); + enriched.put( "original", value ); + enriched.put( "inputType", input == null ? null : input.getClass().getSimpleName() ); + enriched.put( "tenant", context == null ? null : context.get( "tenant" ) ); + return enriched; + } + + public static Publisher publisherDescribe( Object value, Object input, Map context ) { + return new SingleValuePublisher( describe( value, input, context ) ); + } + + public Object describeViaBean( Object value, Object input, Map context ) { + return describe( value, input, context ); + } + + private static final class SingleValuePublisher implements Publisher { + + private final Object value; + + private SingleValuePublisher( Object value ) { + this.value = value; + } + + @Override + public void subscribe( final Subscriber subscriber ) { + subscriber.onSubscribe( new Subscription() { + private boolean completed; + + @Override + public void request( long n ) { + if ( completed ) { + return; + } + if ( n <= 0 ) { + completed = true; + subscriber.onError( new IllegalArgumentException( "Subscription request must be positive." ) ); + return; + } + + completed = true; + if ( value != null ) { + subscriber.onNext( value ); + } + subscriber.onComplete(); + } + + @Override + public void cancel() { + completed = true; + } + } ); + } + } +} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/AbstractTester.java b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/AbstractTester.java similarity index 57% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/AbstractTester.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/AbstractTester.java index 02680885..82174ee4 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/modifier/function/AbstractTester.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/AbstractTester.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +15,10 @@ * limitations under the License. */ -package com.bazaarvoice.jolt.modifier.function; +package io.joltcommunity.jolt.modifier.function; -import com.bazaarvoice.jolt.common.Optional; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.modifier.function.Function; import org.testng.annotations.Test; import java.util.Iterator; @@ -24,27 +26,25 @@ import static org.testng.Assert.assertEquals; -@SuppressWarnings( "deprecated" ) +@SuppressWarnings("deprecated") public abstract class AbstractTester { - @SuppressWarnings( "unused" ) + @SuppressWarnings("unused") public abstract Iterator getTestCases(); @Test(dataProvider = "getTestCases") public void testFunctions(String name, Function function, Object args, Optional expected) { Optional actual; - if(args instanceof List) { - actual = function.apply( (List) args ); + if (args instanceof List) { + actual = function.apply(args); + } else if (args instanceof Object[]) { + actual = function.apply((Object[]) args); + } else { + actual = function.apply(args); } - else if (args instanceof Object[]){ - actual = function.apply( (Object[]) args ); - } - else { - actual = function.apply( args ); - } - assertEquals( actual.isPresent(), expected.isPresent(), "actual and expected should both be present or not" ); - if ( actual.isPresent() ) { - assertEquals( actual.get(), expected.get(), name + " failed"); + assertEquals(actual.isPresent(), expected.isPresent(), "actual and expected should both be present or not"); + if (actual.isPresent()) { + assertEquals(actual.get(), expected.get(), name + " failed"); } } } diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/DatesTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/DatesTest.java new file mode 100644 index 00000000..9de08e03 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/DatesTest.java @@ -0,0 +1,154 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +@SuppressWarnings("deprecated") +public class DatesTest extends AbstractTester { + + @Override + @DataProvider(parallel = true) + public Iterator getTestCases() { + List testCases = new LinkedList<>(); + Function NOW = new Dates.now(); + Function TO_EPOCH = new Dates.toEpochMilli(); + Function FROM_EPOCH = new Dates.fromEpochMilli(); + Function FORMAT_DATE = new Dates.formatDate(); + Function DATE_ADD = new Dates.dateAdd(); + Function DATE_SUBSTRACT = new Dates.dateSubstract(); + + testCases.add(new Object[]{"now-pattern-invalid", NOW, new Object[]{"ABCD", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"now-pattern-int", NOW, new Object[]{1, "UTC"}, Optional.empty()}); + + testCases.add(new Object[]{"fromEpoch-default-long", FROM_EPOCH, new Object[]{1L}, Optional.of("1970-01-01T00:00:00Z")}); + testCases.add(new Object[]{"fromEpoch-pattern-long", FROM_EPOCH, new Object[]{1L, "yyyy", "UTC"}, Optional.of("1970")}); + testCases.add(new Object[]{"fromEpoch-default-int", FROM_EPOCH, new Object[]{1}, Optional.of("1970-01-01T00:00:00Z")}); + testCases.add(new Object[]{"fromEpoch-pattern-int", FROM_EPOCH, new Object[]{1, "yyyy", "UTC"}, Optional.of("1970")}); + testCases.add(new Object[]{"fromEpoch-pattern-iso8601", FROM_EPOCH, new Object[]{1771176362001L, "yyyy-MM-dd'T'HH:mm:ssX", "UTC"}, Optional.of("2026-02-15T17:26:02Z")}); + testCases.add(new Object[]{"fromEpoch-pattern-iso8601", FROM_EPOCH, new Object[]{1771176362001L, "yyyy-MM-dd'T'HH:mm:ssX", "Europe/Paris"}, Optional.of("2026-02-15T18:26:02+01")}); + + testCases.add(new Object[]{"fromEpoch-pattern-null", FROM_EPOCH, null, Optional.empty()}); + testCases.add(new Object[]{"fromEpoch-pattern-numeric", FROM_EPOCH, new Object[]{1, 1, "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"fromEpoch-pattern-invalid", FROM_EPOCH, new Object[]{1, "ABCD", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"fromEpoch-epoch-string", FROM_EPOCH, new Object[]{"1", "yyyy", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"fromEpoch-null-args", FROM_EPOCH, new Object[]{null}, Optional.empty()}); + + testCases.add(new Object[]{"toEpoch-pattern-day", TO_EPOCH, new Object[]{"2000-01-01", "yyyy-MM-dd", "UTC"}, Optional.of(946684800000L)}); + testCases.add(new Object[]{"toEpoch-pattern-seconds", TO_EPOCH, new Object[]{"2000-01-01T00:00:00Z", "yyyy-MM-dd'T'HH:mm:ss'Z'", "UTC"}, Optional.of(946684800000L)}); + testCases.add(new Object[]{"toEpoch-pattern-seconds", TO_EPOCH, new Object[]{"2000-01-01T00:00:00Z", "yyyy-MM-dd'T'HH:mm:ss'Z'", "Europe/Paris"}, Optional.of(946681200000L)}); + testCases.add(new Object[]{"toEpoch-pattern-milliseconds", TO_EPOCH, new Object[]{"2000-01-01T00:00:00.000Z", "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "UTC"}, Optional.of(946684800000L)}); + + testCases.add(new Object[]{"toEpoch-pattern-missing", TO_EPOCH, new Object[]{"1970-01-01"}, Optional.empty()}); + testCases.add(new Object[]{"toEpoch-timezone-missing", TO_EPOCH, new Object[]{"1970-01-01", "yyyy-MM-dd"}, Optional.empty()}); + testCases.add(new Object[]{"toEpoch-pattern-invalid", TO_EPOCH, new Object[]{"1970-01-01", "ABCD", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"toEpoch-pattern-numeric", TO_EPOCH, new Object[]{"1970-01-01", 1, "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"toEpoch-date-invalid", TO_EPOCH, new Object[]{1}, Optional.empty()}); + testCases.add(new Object[]{"toEpoch-null-args", TO_EPOCH, null, Optional.empty()}); + + testCases.add(new Object[]{"format-date-only", FORMAT_DATE, new Object[]{"2000-01-01", "yyyy-MM-dd", "yyyyMMdd"}, Optional.of("20000101")}); + testCases.add(new Object[]{"format-with-time", FORMAT_DATE, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm:ss"}, Optional.of("2000-01-01 12:30:45")}); + testCases.add(new Object[]{"format-with-time", FORMAT_DATE, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm:ss", "Europe/Paris"}, Optional.of("2000-01-01 12:30:45")}); + testCases.add(new Object[]{"format-to-iso8601", FORMAT_DATE, new Object[]{"20000101", "yyyyMMdd", "yyyy-MM-dd'T'HH:mm:ss'Z'"}, Optional.of("2000-01-01T00:00:00Z")}); + testCases.add(new Object[]{"format-with-default-timezone", FORMAT_DATE, new Object[]{"20000101", "yyyyMMdd", "yyyy-MM-dd'T'HH:mm:ssXXX"}, Optional.of("2000-01-01T00:00:00Z")}); + testCases.add(new Object[]{"format-with-paris-timezone", FORMAT_DATE, new Object[]{"20000101", "yyyyMMdd", "yyyy-MM-dd'T'HH:mm:ssXXX","Europe/Paris"}, Optional.of("2000-01-01T00:00:00+01:00")}); + testCases.add(new Object[]{"format-to-a-different-timezone", FORMAT_DATE, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd HH:mm:ss", "Europe/Paris", "UTC" }, Optional.of("2000-01-01 11:30:45")}); + + testCases.add(new Object[]{"format-missing-parameter", FORMAT_DATE, new Object[]{"2000-01-01", "yyyy-MM-dd"}, Optional.empty()}); + testCases.add(new Object[]{"format-from-pattern-invalid", FORMAT_DATE, new Object[]{"2000-01-01", "ABCD", "yyyy-MM-dd"}, Optional.empty()}); + testCases.add(new Object[]{"format-to-pattern-invalid", FORMAT_DATE, new Object[]{"2000-01-01", "yyyy-MM-dd", "ABCD"}, Optional.empty()}); + testCases.add(new Object[]{"format-pattern-numeric", FORMAT_DATE, new Object[]{"2000-01-01", "yyyy-MM-dd", 1}, Optional.empty()}); + testCases.add(new Object[]{"format-too-many-timezones", FORMAT_DATE, new Object[]{"2000-01-01", "yyyy-MM-dd", "yyyy-MM-dd", "UTC", "UTC", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"format-null-args", FORMAT_DATE, null, Optional.empty()}); + + testCases.add(new Object[]{"dateAdd-one-day", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", "P1D", "UTC"}, Optional.of("2000-01-02")}); + testCases.add(new Object[]{"dateAdd-one-month", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", "P1M", "UTC"}, Optional.of("2000-02-01")}); + testCases.add(new Object[]{"dateAdd-one-year", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", "P1Y", "UTC"}, Optional.of("2001-01-01")}); + testCases.add(new Object[]{"dateAdd-complex", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", "P1Y2M3D", "UTC"}, Optional.of("2001-03-04")}); + testCases.add(new Object[]{"dateAdd-with-time", DATE_ADD, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "P1D", "UTC"}, Optional.of("2000-01-02T12:30:45")}); + testCases.add(new Object[]{"dateAdd-with-time", DATE_ADD, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "P1D", "Europe/Paris"}, Optional.of("2000-01-02T12:30:45")}); + testCases.add(new Object[]{"dateAdd-with-time-one-hour", DATE_ADD, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "PT1H", "UTC"}, Optional.of("2000-01-01T13:30:45")}); + testCases.add(new Object[]{"dateAdd-with-time-day-and-hour", DATE_ADD, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "P1MT1H", "UTC"}, Optional.of("2000-02-01T13:30:45")}); + + testCases.add(new Object[]{"dateAdd-timezone-missing", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", "P1D"}, Optional.empty()}); + testCases.add(new Object[]{"dateAdd-duration-invalid", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", "INVALID", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"dateAdd-duration-numeric", DATE_ADD, new Object[]{"2000-01-01", "yyyy-MM-dd", 1, "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"dateAdd-null-args", DATE_ADD, null, Optional.empty()}); + + testCases.add(new Object[]{"dateSubstract-one-day", DATE_SUBSTRACT, new Object[]{"2000-01-02", "yyyy-MM-dd", "P1D", "UTC"}, Optional.of("2000-01-01")}); + testCases.add(new Object[]{"dateSubstract-one-month", DATE_SUBSTRACT, new Object[]{"2000-02-01", "yyyy-MM-dd", "P1M", "UTC"}, Optional.of("2000-01-01")}); + testCases.add(new Object[]{"dateSubstract-one-year", DATE_SUBSTRACT, new Object[]{"2001-01-01", "yyyy-MM-dd", "P1Y", "UTC"}, Optional.of("2000-01-01")}); + testCases.add(new Object[]{"dateSubstract-complex", DATE_SUBSTRACT, new Object[]{"2001-03-04", "yyyy-MM-dd", "P1Y2M3D", "UTC"}, Optional.of("2000-01-01")}); + testCases.add(new Object[]{"dateSubstract-with-time", DATE_SUBSTRACT, new Object[]{"2000-01-02T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "P1D", "UTC"}, Optional.of("2000-01-01T12:30:45")}); + testCases.add(new Object[]{"dateSubstract-with-time-one-hour", DATE_SUBSTRACT, new Object[]{"2000-01-01T12:30:45", "yyyy-MM-dd'T'HH:mm:ss", "PT1H", "UTC"}, Optional.of("2000-01-01T11:30:45")}); + + testCases.add(new Object[]{"dateSubstract-timezone-missing", DATE_SUBSTRACT, new Object[]{"2000-01-01", "yyyy-MM-dd", "P1D"}, Optional.empty()}); + testCases.add(new Object[]{"dateAdd-duration-invalid", DATE_SUBSTRACT, new Object[]{"2000-01-01", "yyyy-MM-dd", "INVALID", "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"dateAdd-duration-numeric", DATE_SUBSTRACT, new Object[]{"2000-01-01", "yyyy-MM-dd", 1, "UTC"}, Optional.empty()}); + testCases.add(new Object[]{"dateSubstract-null-args", DATE_SUBSTRACT, null, Optional.empty()}); + + return testCases.iterator(); + } + + @Test + public void nowEpochMillis() { + Optional opt = Dates.now.apply(); + assert (opt.isPresent() && (opt.get() instanceof Long)); + } + + @Test + public void nowReturnsEmptyIfNoPatternAndTimeZoneIsProvided() { + Optional opt = (new Dates.now()).apply(); + assert !opt.isPresent(); + } + + @Test + public void nowReturnsEmptyIfNoTimeZoneIsProvided() { + Optional opt = (new Dates.now()).apply(List.of("yyyy-MM-dd")); + assert !opt.isPresent(); + } + + @Test + public void nowReturnsFormattedDate() { + Optional opt = (new Dates.now()).apply("yyyy-MM-dd", "UTC"); + assert (opt.isPresent() && (opt.get() instanceof String)); + String date = (String) opt.get(); + assert (date.matches("\\d{4}-\\d{2}-\\d{2}")); + } + + @Test + public void nowReturnsFormattedDateWithUtcTimeZone() { + Optional opt = (new Dates.now()).apply("yyyy-MM-dd'T'HH:mm:ssX", "UTC"); + assert (opt.isPresent() && (opt.get() instanceof String)); + String date = (String) opt.get(); + assert (date.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")); + } + + @Test + public void nowReturnsFormattedDateWithParisTimeZone() { + Optional opt = (new Dates.now()).apply("yyyy-MM-dd'T'HH:mm:ssZ", "Europe/Paris"); + assert (opt.isPresent() && (opt.get() instanceof String)); + String date = (String) opt.get(); + assert (date.matches("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+0[12]00")); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/ListsTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/ListsTest.java new file mode 100644 index 00000000..f41a76e7 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/ListsTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; +import org.testng.annotations.DataProvider; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +@SuppressWarnings("deprecated") +public class ListsTest extends AbstractTester { + + @DataProvider(parallel = true) + public Iterator getTestCases() { + List testCases = new LinkedList<>(); + + Function FIRST_ELEMENT = new Lists.firstElement(); + Function LAST_ELEMENT = new Lists.lastElement(); + Function ELEMENT_AT = new Lists.elementAt(); + + Function SIZE = new Objects.size(); + + testCases.add(new Object[]{"first-empty-array", FIRST_ELEMENT, new Object[0], Optional.empty()}); + testCases.add(new Object[]{"first-empty-list", FIRST_ELEMENT, Arrays.asList(), Optional.empty()}); + + testCases.add(new Object[]{"first-null", FIRST_ELEMENT, null, Optional.empty()}); + testCases.add(new Object[]{"first-array", FIRST_ELEMENT, new Object[]{1, 2, 3}, Optional.of(1)}); + testCases.add(new Object[]{"first-list", FIRST_ELEMENT, Arrays.asList(1, 2, 3), Optional.of(1)}); + + + testCases.add(new Object[]{"last-empty-array", LAST_ELEMENT, new Object[0], Optional.empty()}); + testCases.add(new Object[]{"last-empty-list", LAST_ELEMENT, Arrays.asList(), Optional.empty()}); + + testCases.add(new Object[]{"last-null", LAST_ELEMENT, null, Optional.empty()}); + testCases.add(new Object[]{"last-array", LAST_ELEMENT, new Object[]{1, 2, 3}, Optional.of(3)}); + testCases.add(new Object[]{"last-list", LAST_ELEMENT, Arrays.asList(1, 2, 3), Optional.of(3)}); + + + testCases.add(new Object[]{"at-empty-array", ELEMENT_AT, new Object[]{5}, Optional.empty()}); + testCases.add(new Object[]{"at-empty-list", ELEMENT_AT, Arrays.asList(5), Optional.empty()}); + testCases.add(new Object[]{"at-empty-null", ELEMENT_AT, new Object[]{null, 1}, Optional.empty()}); + testCases.add(new Object[]{"at-empty-invalid", ELEMENT_AT, new Object(), Optional.empty()}); + + testCases.add(new Object[]{"at-array", ELEMENT_AT, new Object[]{1, 2, 3, 1}, Optional.of(3)}); + testCases.add(new Object[]{"at-list", ELEMENT_AT, Arrays.asList(1, 2, 3, 1), Optional.of(3)}); + + testCases.add(new Object[]{"at-array-missing", ELEMENT_AT, new Object[]{5, 1, 2, 3}, Optional.empty()}); + testCases.add(new Object[]{"at-list-missing", ELEMENT_AT, Arrays.asList(5, 1, 2, 3), Optional.empty()}); + + + testCases.add(new Object[]{"size-list", SIZE, new Object[]{5, 1, 2, 3}, Optional.of(4)}); + testCases.add(new Object[]{"size-list-empty", SIZE, Arrays.asList(), Optional.of(0)}); + + return testCases.iterator(); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/MathTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/MathTest.java new file mode 100644 index 00000000..a2d942c6 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/MathTest.java @@ -0,0 +1,337 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.*; + +import static io.joltcommunity.jolt.modifier.function.Math.abs; +import static io.joltcommunity.jolt.modifier.function.Objects.toNumber; + +@SuppressWarnings("deprecated") +public class MathTest extends AbstractTester { + + @DataProvider(parallel = true) + public Iterator getTestCases() { + List testCases = new LinkedList<>(); + + Function MAX_OF = new Math.max(); + Function MIN_OF = new Math.min(); + Function ABS_OF = new Math.abs(); + Function TO_INTEGER = new Objects.toInteger(); + Function TO_DOUBLE = new Objects.toDouble(); + Function TO_LONG = new Objects.toLong(); + + Function INT_SUM_OF = new Math.intSum(); + Function DOUBLE_SUM_OF = new Math.doubleSum(); + Function LONG_SUM_OF = new Math.longSum(); + + Function INT_SUBTRACT_OF = new Math.intSubtract(); + Function DOUBLE_SUBTRACT_OF = new Math.doubleSubtract(); + Function LONG_SUBTRACT_OF = new Math.longSubtract(); + + Function DIV_OF = new Math.divide(); + Function DIV_AND_ROUND_OF = new Math.divideAndRound(); + + Function MUL_OF = new Math.multiply(); + Function MUL_AND_ROUND_OF = new Math.multiplyAndRound(); + + testCases.add(new Object[]{"max-empty-array", MAX_OF, new Object[]{}, Optional.empty()}); + testCases.add(new Object[]{"max-empty-list", MAX_OF, new ArrayList<>(), Optional.empty()}); + testCases.add(new Object[]{"max-null", MAX_OF, null, Optional.empty()}); + testCases.add(new Object[]{"max-object", MAX_OF, new Object(), Optional.empty()}); + + testCases.add(new Object[]{"max-single-int-array", MAX_OF, new Object[]{1}, Optional.of(1)}); + testCases.add(new Object[]{"max-single-long-array", MAX_OF, new Object[]{1L}, Optional.of(1L)}); + testCases.add(new Object[]{"max-single-double-array", MAX_OF, new Object[]{1.0}, Optional.of(1.0)}); + + testCases.add(new Object[]{"max-single-int-list", MAX_OF, Arrays.asList(1), Optional.of(1)}); + testCases.add(new Object[]{"max-single-long-list", MAX_OF, Arrays.asList(1L), Optional.of(1L)}); + testCases.add(new Object[]{"max-single-double-list", MAX_OF, Arrays.asList(1.0), Optional.of(1.0)}); + + testCases.add(new Object[]{"max-single-int-array-extra-arg", MAX_OF, new Object[]{1, "a"}, Optional.of(1)}); + testCases.add(new Object[]{"max-single-long-array-extra-arg", MAX_OF, new Object[]{1L, "a"}, Optional.of(1L)}); + testCases.add(new Object[]{"max-single-double-array-extra-arg", MAX_OF, new Object[]{1.0, "a"}, Optional.of(1.0)}); + + testCases.add(new Object[]{"max-single-int-list-extra-arg", MAX_OF, Arrays.asList(1, "a"), Optional.of(1)}); + testCases.add(new Object[]{"max-single-long-list-extra-arg", MAX_OF, Arrays.asList(1L, "a"), Optional.of(1L)}); + testCases.add(new Object[]{"max-single-double-list-extra-arg", MAX_OF, Arrays.asList(1.0, "a"), Optional.of(1.0)}); + + testCases.add(new Object[]{"max-multi-int-array", MAX_OF, new Object[]{1, 3, 2, 5}, Optional.of(5)}); + testCases.add(new Object[]{"max-multi-long-array", MAX_OF, new Object[]{1L, 3L, 2L, 5L}, Optional.of(5L)}); + testCases.add(new Object[]{"max-multi-double-array", MAX_OF, new Object[]{1.0, 3.0, 2.0, 5.0}, Optional.of(5.0)}); + + testCases.add(new Object[]{"max-multi-int-list", MAX_OF, Arrays.asList(1, 3, 2, 5), Optional.of(5)}); + testCases.add(new Object[]{"max-multi-long-list", MAX_OF, Arrays.asList(1L, 3L, 2L, 5L), Optional.of(5L)}); + testCases.add(new Object[]{"max-multi-double-list", MAX_OF, Arrays.asList(1.0, 3.0, 2.0, 5.0), Optional.of(5.0)}); + + testCases.add(new Object[]{"max-combo-int-array", MAX_OF, new Object[]{1.0, 3L, null, 5}, Optional.of(5)}); + testCases.add(new Object[]{"max-combo-long-array", MAX_OF, new Object[]{1.0, 3L, null, 5L}, Optional.of(5L)}); + testCases.add(new Object[]{"max-combo-double-array", MAX_OF, new Object[]{1.0, 3L, null, 5.0}, Optional.of(5.0)}); + + testCases.add(new Object[]{"max-combo-int-list", MAX_OF, Arrays.asList(1.0, 3L, null, 5), Optional.of(5)}); + testCases.add(new Object[]{"max-combo-long-list", MAX_OF, Arrays.asList(1.0, 3L, null, 5L), Optional.of(5L)}); + testCases.add(new Object[]{"max-combo-double-list", MAX_OF, Arrays.asList(1.0, 3L, null, 5.0), Optional.of(5.0)}); + + testCases.add(new Object[]{"max-NaN", MAX_OF, Arrays.asList(1.0, Double.NaN), Optional.of(Double.NaN)}); + testCases.add(new Object[]{"max-positive-infinity", MAX_OF, Arrays.asList(1.0, Double.POSITIVE_INFINITY), Optional.of(Double.POSITIVE_INFINITY)}); + testCases.add(new Object[]{"max-NaN-positive-infinity", MAX_OF, Arrays.asList(1.0, Double.NaN, Double.POSITIVE_INFINITY), Optional.of(Double.NaN)}); + + + testCases.add(new Object[]{"min-empty-array", MIN_OF, new Object[]{}, Optional.empty()}); + testCases.add(new Object[]{"min-empty-list", MIN_OF, new ArrayList<>(), Optional.empty()}); + testCases.add(new Object[]{"min-null", MIN_OF, null, Optional.empty()}); + testCases.add(new Object[]{"min-object", MIN_OF, new Object(), Optional.empty()}); + + testCases.add(new Object[]{"min-single-int-array", MIN_OF, new Object[]{1}, Optional.of(1)}); + testCases.add(new Object[]{"min-single-long-array", MIN_OF, new Object[]{1L}, Optional.of(1L)}); + testCases.add(new Object[]{"min-single-double-array", MIN_OF, new Object[]{1.0}, Optional.of(1.0)}); + + testCases.add(new Object[]{"min-single-int-list", MIN_OF, Arrays.asList(1), Optional.of(1)}); + testCases.add(new Object[]{"min-single-long-list", MIN_OF, Arrays.asList(1L), Optional.of(1L)}); + testCases.add(new Object[]{"min-single-double-list", MIN_OF, Arrays.asList(1.0), Optional.of(1.0)}); + + testCases.add(new Object[]{"min-single-int-array-extra-arg", MIN_OF, new Object[]{1, "a"}, Optional.of(1)}); + testCases.add(new Object[]{"min-single-long-array-extra-arg", MIN_OF, new Object[]{1L, "a"}, Optional.of(1L)}); + testCases.add(new Object[]{"min-single-double-array-extra-arg", MIN_OF, new Object[]{1.0, "a"}, Optional.of(1.0)}); + + testCases.add(new Object[]{"min-single-int-list-extra-arg", MIN_OF, Arrays.asList(1, "a"), Optional.of(1)}); + testCases.add(new Object[]{"min-single-long-list-extra-arg", MIN_OF, Arrays.asList(1L, "a"), Optional.of(1L)}); + testCases.add(new Object[]{"min-single-double-list-extra-arg", MIN_OF, Arrays.asList(1.0, "a"), Optional.of(1.0)}); + + testCases.add(new Object[]{"min-multi-int-array", MIN_OF, new Object[]{1, 3, 2, 5}, Optional.of(1)}); + testCases.add(new Object[]{"min-multi-long-array", MIN_OF, new Object[]{1L, 3L, 2L, 5L}, Optional.of(1L)}); + testCases.add(new Object[]{"min-multi-double-array", MIN_OF, new Object[]{1.0, 3.0, 2.0, 5.0}, Optional.of(1.0)}); + + testCases.add(new Object[]{"min-multi-int-list", MIN_OF, Arrays.asList(1, 3, 2, 5), Optional.of(1)}); + testCases.add(new Object[]{"min-multi-long-list", MIN_OF, Arrays.asList(1L, 3L, 2L, 5L), Optional.of(1L)}); + testCases.add(new Object[]{"min-multi-double-list", MIN_OF, Arrays.asList(1.0, 3.0, 2.0, 5.0), Optional.of(1.0)}); + + testCases.add(new Object[]{"min-combo-int-array", MIN_OF, new Object[]{1, 3L, null, 5.0}, Optional.of(1)}); + testCases.add(new Object[]{"min-combo-long-array", MIN_OF, new Object[]{1L, 3, null, 5.0}, Optional.of(1L)}); + testCases.add(new Object[]{"min-combo-double-array", MIN_OF, new Object[]{1.0, 3L, null, 5}, Optional.of(1.0)}); + + testCases.add(new Object[]{"min-combo-int-list", MIN_OF, Arrays.asList(1, 3L, null, 5.0), Optional.of(1)}); + testCases.add(new Object[]{"min-combo-long-list", MIN_OF, Arrays.asList(1L, 3, null, 5.0), Optional.of(1L)}); + testCases.add(new Object[]{"min-combo-double-list", MIN_OF, Arrays.asList(1.0, 3L, null, 5), Optional.of(1.0)}); + + testCases.add(new Object[]{"min-NaN", MIN_OF, Arrays.asList(-1.0, Double.NaN), Optional.of(Double.NaN)}); + testCases.add(new Object[]{"min-negative-Infinity", MIN_OF, Arrays.asList(-1.0, Double.NEGATIVE_INFINITY), Optional.of(Double.NEGATIVE_INFINITY)}); + testCases.add(new Object[]{"min-NaN-positive-infinity", MIN_OF, Arrays.asList(-1.0, Double.NaN, Double.NEGATIVE_INFINITY), Optional.of(Double.NaN)}); + + + testCases.add(new Object[]{"abs-null", ABS_OF, null, Optional.empty()}); + testCases.add(new Object[]{"abs-invalid", ABS_OF, new Object(), Optional.empty()}); + testCases.add(new Object[]{"abs-empty-list", ABS_OF, new Object[]{}, Optional.empty()}); + testCases.add(new Object[]{"abs-empty-array", ABS_OF, Arrays.asList(), Optional.empty()}); + + testCases.add(new Object[]{"abs-single-negative-int", ABS_OF, -1, Optional.of(1)}); + testCases.add(new Object[]{"abs-single-negative-long", ABS_OF, -1L, Optional.of(1L)}); + testCases.add(new Object[]{"abs-single-negative-double", ABS_OF, -1.0, Optional.of(1.0)}); + testCases.add(new Object[]{"abs-single-positive-int", ABS_OF, 1, Optional.of(1)}); + testCases.add(new Object[]{"abs-single-positive-long", ABS_OF, 1L, Optional.of(1L)}); + testCases.add(new Object[]{"abs-single-positive-double", ABS_OF, 1.0, Optional.of(1.0)}); + + testCases.add(new Object[]{"abs-list", ABS_OF, new Object[]{-1, -1L, -1.0}, Optional.of(Arrays.asList(1, 1L, 1.0))}); + testCases.add(new Object[]{"abs-array", ABS_OF, Arrays.asList(-1, -1L, -1.0), Optional.of(Arrays.asList(1, 1L, 1.0))}); + + testCases.add(new Object[]{"abs-Nan", ABS_OF, Double.NaN, Optional.of(Double.NaN)}); + testCases.add(new Object[]{"abs-PosInfinity", ABS_OF, Double.POSITIVE_INFINITY, Optional.of(Double.POSITIVE_INFINITY)}); + testCases.add(new Object[]{"abs-NefInfinity", ABS_OF, Double.NEGATIVE_INFINITY, Optional.of(Double.POSITIVE_INFINITY)}); + + + testCases.add(new Object[]{"toInt-null", TO_INTEGER, null, Optional.empty()}); + testCases.add(new Object[]{"toInt-invalid", TO_INTEGER, new Object(), Optional.empty()}); + testCases.add(new Object[]{"toInt-empty-array", TO_INTEGER, new Object[]{}, Optional.empty()}); + testCases.add(new Object[]{"toInt-empty-list", TO_INTEGER, Arrays.asList(), Optional.empty()}); + + testCases.add(new Object[]{"toInt-single-positive-string", TO_INTEGER, "1", Optional.of(1)}); + testCases.add(new Object[]{"toInt-single-negative-string", TO_INTEGER, "-1", Optional.of(-1)}); + testCases.add(new Object[]{"toInt-single-positive-int", TO_INTEGER, 1, Optional.of(1)}); + testCases.add(new Object[]{"toInt-single-negative-int", TO_INTEGER, -1, Optional.of(-1)}); + testCases.add(new Object[]{"toInt-single-positive-long", TO_INTEGER, 1L, Optional.of(1)}); + testCases.add(new Object[]{"toInt-single-negative-long", TO_INTEGER, -1L, Optional.of(-1)}); + testCases.add(new Object[]{"toInt-single-positive-double", TO_INTEGER, 1.0, Optional.of(1)}); + testCases.add(new Object[]{"toInt-single-negative-double", TO_INTEGER, -1.0, Optional.of(-1)}); + + testCases.add(new Object[]{"toInt-single-positive-string-list", TO_INTEGER, new Object[]{"1", "2"}, Optional.of(Arrays.asList(1, 2))}); + testCases.add(new Object[]{"toInt-single-negative-string-array", TO_INTEGER, Arrays.asList("-1", "-2"), Optional.of(Arrays.asList(-1, -2))}); + testCases.add(new Object[]{"toInt-single-positive-int-list", TO_INTEGER, new Object[]{1, 2}, Optional.of(Arrays.asList(1, 2))}); + testCases.add(new Object[]{"toInt-single-negative-int-array", TO_INTEGER, Arrays.asList(-1, -2), Optional.of(Arrays.asList(-1, -2))}); + testCases.add(new Object[]{"toInt-single-positive-long-list", TO_INTEGER, new Object[]{1L, 2L}, Optional.of(Arrays.asList(1, 2))}); + testCases.add(new Object[]{"toInt-single-negative-long-array", TO_INTEGER, Arrays.asList(-1L, -2L), Optional.of(Arrays.asList(-1, -2))}); + testCases.add(new Object[]{"toInt-single-positive-double-list", TO_INTEGER, new Object[]{1.0, 2.0}, Optional.of(Arrays.asList(1, 2))}); + testCases.add(new Object[]{"toInt-single-negative-double-array", TO_INTEGER, Arrays.asList(-1.0, -2.0), Optional.of(Arrays.asList(-1, -2))}); + + + testCases.add(new Object[]{"toDouble-null", TO_DOUBLE, null, Optional.empty()}); + testCases.add(new Object[]{"toDouble-invalid", TO_DOUBLE, new Object(), Optional.empty()}); + testCases.add(new Object[]{"toDouble-empty-array", TO_DOUBLE, new Object[]{}, Optional.empty()}); + testCases.add(new Object[]{"toDouble-empty-list", TO_DOUBLE, Arrays.asList(), Optional.empty()}); + + testCases.add(new Object[]{"toDouble-single-positive-string", TO_DOUBLE, "1", Optional.of(1.0)}); + testCases.add(new Object[]{"toDouble-single-negative-string", TO_DOUBLE, "-1", Optional.of(-1.0)}); + testCases.add(new Object[]{"toDouble-single-positive-int", TO_DOUBLE, 1, Optional.of(1.0)}); + testCases.add(new Object[]{"toDouble-single-negative-int", TO_DOUBLE, -1, Optional.of(-1.0)}); + testCases.add(new Object[]{"toDouble-single-positive-long", TO_DOUBLE, 1L, Optional.of(1.0)}); + testCases.add(new Object[]{"toDouble-single-negative-long", TO_DOUBLE, -1L, Optional.of(-1.0)}); + testCases.add(new Object[]{"toDouble-single-positive-double", TO_DOUBLE, 1.0, Optional.of(1.0)}); + testCases.add(new Object[]{"toDouble-single-negative-double", TO_DOUBLE, -1.0, Optional.of(-1.0)}); + + testCases.add(new Object[]{"toDouble-single-positive-string-list", TO_DOUBLE, new Object[]{"1", "2"}, Optional.of(Arrays.asList(1.0, 2.0))}); + testCases.add(new Object[]{"toDouble-single-negative-string-array", TO_DOUBLE, Arrays.asList("-1", "-2"), Optional.of(Arrays.asList(-1.0, -2.0))}); + testCases.add(new Object[]{"toDouble-single-positive-int-list", TO_DOUBLE, new Object[]{1, 2}, Optional.of(Arrays.asList(1.0, 2.0))}); + testCases.add(new Object[]{"toDouble-single-negative-int-array", TO_DOUBLE, Arrays.asList(-1, -2), Optional.of(Arrays.asList(-1.0, -2.0))}); + testCases.add(new Object[]{"toDouble-single-positive-long-list", TO_DOUBLE, new Object[]{1L, 2L}, Optional.of(Arrays.asList(1.0, 2.0))}); + testCases.add(new Object[]{"toDouble-single-negative-long-array", TO_DOUBLE, Arrays.asList(-1L, -2L), Optional.of(Arrays.asList(-1.0, -2.0))}); + testCases.add(new Object[]{"toDouble-single-positive-double-list", TO_DOUBLE, new Object[]{1.0, 2.0}, Optional.of(Arrays.asList(1.0, 2.0))}); + testCases.add(new Object[]{"toDouble-single-negative-double-array", TO_DOUBLE, Arrays.asList(-1.0, -2.0), Optional.of(Arrays.asList(-1.0, -2.0))}); + + + testCases.add(new Object[]{"toLong-null", TO_LONG, null, Optional.empty()}); + testCases.add(new Object[]{"toLong-invalid", TO_LONG, new Object(), Optional.empty()}); + testCases.add(new Object[]{"toLong-empty-array", TO_LONG, new Object[]{}, Optional.empty()}); + testCases.add(new Object[]{"toLong-empty-list", TO_LONG, Arrays.asList(), Optional.empty()}); + + testCases.add(new Object[]{"toLong-single-positive-string", TO_LONG, "1", Optional.of(1L)}); + testCases.add(new Object[]{"toLong-single-negative-string", TO_LONG, "-1", Optional.of(-1L)}); + testCases.add(new Object[]{"toLong-single-positive-int", TO_LONG, 1, Optional.of(1L)}); + testCases.add(new Object[]{"toLong-single-negative-int", TO_LONG, -1, Optional.of(-1L)}); + testCases.add(new Object[]{"toLong-single-positive-long", TO_LONG, 1L, Optional.of(1L)}); + testCases.add(new Object[]{"toLong-single-negative-long", TO_LONG, -1L, Optional.of(-1L)}); + testCases.add(new Object[]{"toLong-single-positive-double", TO_LONG, 1L, Optional.of(1L)}); + testCases.add(new Object[]{"toLong-single-negative-double", TO_LONG, -1L, Optional.of(-1L)}); + + testCases.add(new Object[]{"toLong-single-positive-string-list", TO_LONG, new Object[]{"1", "2"}, Optional.of(Arrays.asList(1L, 2L))}); + testCases.add(new Object[]{"toLong-single-negative-string-array", TO_LONG, Arrays.asList("-1", "-2"), Optional.of(Arrays.asList(-1L, -2L))}); + testCases.add(new Object[]{"toLong-single-positive-int-list", TO_LONG, new Object[]{1, 2}, Optional.of(Arrays.asList(1L, 2L))}); + testCases.add(new Object[]{"toLong-single-negative-int-array", TO_LONG, Arrays.asList(-1, -2), Optional.of(Arrays.asList(-1L, -2L))}); + testCases.add(new Object[]{"toLong-single-positive-long-list", TO_LONG, new Object[]{1L, 2L}, Optional.of(Arrays.asList(1L, 2L))}); + testCases.add(new Object[]{"toLong-single-negative-long-array", TO_LONG, Arrays.asList(-1L, -2L), Optional.of(Arrays.asList(-1L, -2L))}); + testCases.add(new Object[]{"toLong-single-positive-double-list", TO_LONG, new Object[]{1L, 2L}, Optional.of(Arrays.asList(1L, 2L))}); + testCases.add(new Object[]{"toLong-single-negative-double-array", TO_LONG, Arrays.asList(-1L, -2L), Optional.of(Arrays.asList(-1L, -2L))}); + + testCases.add(new Object[]{"toInteger-combo-string-array", TO_INTEGER, Arrays.asList("-1", 2, -3L, 4.0), Optional.of(Arrays.asList(-1, 2, -3, 4))}); + testCases.add(new Object[]{"toLong-combo-int-array", TO_LONG, Arrays.asList("-1", 2, -3L, 4.0), Optional.of(Arrays.asList(-1L, 2L, -3L, 4L))}); + testCases.add(new Object[]{"toDouble-combo-long-array", TO_DOUBLE, Arrays.asList("-1", 2, -3L, 4.0), Optional.of(Arrays.asList(-1.0, 2.0, -3.0, 4.0))}); + + testCases.add(new Object[]{"intsum-combo-string-array", INT_SUM_OF, Arrays.asList(1, 2.0, "random", 0), Optional.of(3)}); + testCases.add(new Object[]{"intsum-single-value", INT_SUM_OF, 2, Optional.empty()}); + testCases.add(new Object[]{"intsum-combo-intstring-array", INT_SUM_OF, Arrays.asList(1L, 2, "-3.0", 0), Optional.of(0)}); + + testCases.add(new Object[]{"doublesum-combo-string-array", DOUBLE_SUM_OF, Arrays.asList(1, 2.0, "random", 0), Optional.of(3.0)}); + testCases.add(new Object[]{"doublesum-single-value", DOUBLE_SUM_OF, 2, Optional.empty()}); + testCases.add(new Object[]{"doublesum-combo-intstring-array", DOUBLE_SUM_OF, Arrays.asList(1L, 2, "-3.0", 0), Optional.of(0.0)}); + + testCases.add(new Object[]{"longsum-combo-string-array", LONG_SUM_OF, Arrays.asList(1, 2.0, "random", 0), Optional.of(3L)}); + testCases.add(new Object[]{"longsum-single-value", LONG_SUM_OF, 2, Optional.empty()}); + testCases.add(new Object[]{"longsum-combo-intstring-array", LONG_SUM_OF, Arrays.asList(1L, 2, "-3.0", 0), Optional.of(0L)}); + + testCases.add(new Object[]{"intsubtract-happy-path", INT_SUBTRACT_OF, Arrays.asList(4, 1), Optional.of(3)}); + testCases.add(new Object[]{"intsubtract-single-value", INT_SUBTRACT_OF, 2, Optional.empty()}); + testCases.add(new Object[]{"intsubtract-wrong-type", INT_SUBTRACT_OF, Arrays.asList(4L, 1), Optional.empty()}); + + testCases.add(new Object[]{"doublesubtract-happy-path", DOUBLE_SUBTRACT_OF, Arrays.asList(4.0, 1.0), Optional.of(3.0)}); + testCases.add(new Object[]{"doublesubtract-single-value", DOUBLE_SUBTRACT_OF, 2.0, Optional.empty()}); + testCases.add(new Object[]{"doublesubtract-wrong-type", DOUBLE_SUBTRACT_OF, Arrays.asList(4L, 1), Optional.empty()}); + + testCases.add(new Object[]{"longsubtract-happy-path", LONG_SUBTRACT_OF, Arrays.asList(4L, 1L), Optional.of(3L)}); + testCases.add(new Object[]{"longsubtract-single-value", LONG_SUBTRACT_OF, 2L, Optional.empty()}); + testCases.add(new Object[]{"longsubtract-wrong-type", LONG_SUBTRACT_OF, Arrays.asList(4.0, 1), Optional.empty()}); + + // Test to make sure "mul" only uses the first and second element in the array and ignores the rest. + testCases.add(new Object[]{"mul-combo-array", MUL_OF, Arrays.asList(10L, 5.0, 2), Optional.empty()}); + testCases.add(new Object[]{"mul-combo-string-array", MUL_OF, Arrays.asList(10L, "5", 2), Optional.empty()}); + testCases.add(new Object[]{"mul-single-element-array", MUL_OF, Arrays.asList("5"), Optional.empty()}); + testCases.add(new Object[]{"mul-single-element", MUL_OF, "10", Optional.empty()}); + testCases.add(new Object[]{"mul-emptyleft-element", MUL_OF, Arrays.asList(10L, null), Optional.empty()}); + testCases.add(new Object[]{"mul-emptyright-element", MUL_OF, Arrays.asList(null, 10L), Optional.empty()}); + testCases.add(new Object[]{"mulAndRound-empty-element", MUL_AND_ROUND_OF, Arrays.asList(1, null, null), Optional.empty()}); + + // Mul 0 by any number returns 0.0(double) + testCases.add(new Object[]{"mul-combo-valid-array", MUL_OF, Arrays.asList(0.0, 10), Optional.of(0.0)}); + testCases.add(new Object[]{"mul-combo-valid-array-bigdec-int", MUL_OF, Arrays.asList(BigDecimal.valueOf(10.1), 10), Optional.of(BigDecimal.valueOf(101.0))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigdec-bigint", MUL_OF, Arrays.asList(BigDecimal.valueOf(10.1), BigInteger.valueOf(10L)), Optional.of(BigDecimal.valueOf(101.0))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigdec-double", MUL_OF, Arrays.asList(BigDecimal.valueOf(10.1), 10.1), Optional.of(BigDecimal.valueOf(102.01))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigdec-bigdec", MUL_OF, Arrays.asList(BigDecimal.valueOf(10.1), BigDecimal.valueOf(10.1)), Optional.of(BigDecimal.valueOf(102.01))}); + testCases.add(new Object[]{"mul-combo-valid-array-int-bigdec", MUL_OF, Arrays.asList(10, BigDecimal.valueOf(10.1)), Optional.of(BigDecimal.valueOf(101.0))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigint-bigdec", MUL_OF, Arrays.asList(BigInteger.valueOf(10L), BigDecimal.valueOf(10.1)), Optional.of(BigDecimal.valueOf(101.0))}); + testCases.add(new Object[]{"mul-combo-valid-array-double-bigdec", MUL_OF, Arrays.asList(10.1, BigDecimal.valueOf(10.1)), Optional.of(BigDecimal.valueOf(102.01))}); + testCases.add(new Object[]{"mul-combo-valid-array-double-bigint", MUL_OF, Arrays.asList(10.1, BigInteger.valueOf(10L)), Optional.of(BigDecimal.valueOf(101.0))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigint-double", MUL_OF, Arrays.asList(BigInteger.valueOf(10L), 10.1), Optional.of(BigDecimal.valueOf(101.0))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigint-int", MUL_OF, Arrays.asList(BigInteger.valueOf(10L), 10L), Optional.of(BigInteger.valueOf(100))}); + testCases.add(new Object[]{"mul-combo-valid-array-bigint-bigint", MUL_OF, Arrays.asList(BigInteger.valueOf(10L), BigInteger.valueOf(10L)), Optional.of(BigInteger.valueOf(100))}); + testCases.add(new Object[]{"mul-combo-valid-array-int-bigint", MUL_OF, Arrays.asList(10L, BigInteger.valueOf(10L)), Optional.of(BigInteger.valueOf(100))}); + testCases.add(new Object[]{"mul-combo-valid-array-int-double", MUL_OF, Arrays.asList(10, 10.1), Optional.of(101.0)}); + testCases.add(new Object[]{"mul-combo-valid-array-int-int", MUL_OF, Arrays.asList(10, 10), Optional.of(100L)}); + + testCases.add(new Object[]{"mulAndRound-single-precision-array", MUL_AND_ROUND_OF, Arrays.asList(1, 5.2, 2), Optional.of(10.4)}); + testCases.add(new Object[]{"mulAndRound-double-precision-array", MUL_AND_ROUND_OF, Arrays.asList(2, 5.2, 2), Optional.of(10.40)}); + testCases.add(new Object[]{"mulAndRound-trailing-precision-array", MUL_AND_ROUND_OF, Arrays.asList(3, 5.2, 2), Optional.of(10.400)}); + testCases.add(new Object[]{"mulAndRound-bigdec-single-precision-array", MUL_AND_ROUND_OF, Arrays.asList(1, BigDecimal.valueOf(5.2), 2), Optional.of(BigDecimal.valueOf(10.4))}); + testCases.add(new Object[]{"mulAndRound-bigdec-double-precision-array", MUL_AND_ROUND_OF, Arrays.asList(2, BigDecimal.valueOf(5.2), 2), Optional.of(BigDecimal.valueOf(10.4))}); + testCases.add(new Object[]{"mulAndRound-bigdec-trailing-precision-array", MUL_AND_ROUND_OF, Arrays.asList(3, BigDecimal.valueOf(5.2), 2), Optional.of(BigDecimal.valueOf(10.4))}); + testCases.add(new Object[]{"mulAndRound-no-precision-array", MUL_AND_ROUND_OF, Arrays.asList(0, 5.25, 2), Optional.of(11.0)}); // Round up as >= 0.5 + testCases.add(new Object[]{"mulAndRound-no-precision-array", MUL_AND_ROUND_OF, Arrays.asList(0, 5.15, 2), Optional.of(10.0)}); // Round down as < 0.5 + + + // Test to make sure "div" only uses the first and second element in the array and ignores the rest. + testCases.add(new Object[]{"div-combo-array", DIV_OF, Arrays.asList(10L, 5.0, 2), Optional.empty()}); + testCases.add(new Object[]{"div-combo-string-array", DIV_OF, Arrays.asList(10L, "5", 2), Optional.empty()}); + testCases.add(new Object[]{"div-single-element-array", DIV_OF, Arrays.asList("5"), Optional.empty()}); + testCases.add(new Object[]{"div-single-element", DIV_OF, "10", Optional.empty()}); + + // Dividing by 0 returns an empty result. + testCases.add(new Object[]{"div-combo-invalid-array", DIV_OF, Arrays.asList(10L, 0, 2), Optional.empty()}); + + // Dividing 0 by any number returns 0.0(double) + testCases.add(new Object[]{"div-combo-valid-array", DIV_OF, Arrays.asList(0.0, 10), Optional.of(0.0)}); + + testCases.add(new Object[]{"divAndRound-single-precision-array", DIV_AND_ROUND_OF, Arrays.asList(1, 5.0, 2), Optional.of(2.5)}); + testCases.add(new Object[]{"divAndRound-double-precision-array", DIV_AND_ROUND_OF, Arrays.asList(2, 5.0, 2), Optional.of(2.50)}); + testCases.add(new Object[]{"divAndRound-trailing-precision-array", DIV_AND_ROUND_OF, Arrays.asList(3, 5.0, 2), Optional.of(2.500)}); + testCases.add(new Object[]{"divAndRound-no-precision-array", DIV_AND_ROUND_OF, Arrays.asList(0, 5.0, 2), Optional.of(3.0)}); // Round up as >= 0.5 + testCases.add(new Object[]{"divAndRound-no-precision-array", DIV_AND_ROUND_OF, Arrays.asList(0, 4.8, 2), Optional.of(2.0)}); // Round down as < 0.5 + + return testCases.iterator(); + } + + @Test + @SuppressWarnings("all") + public void testNitPicks() { + // we want to be able to return the min/max element of input type, not + // autoboxed type -- wanted to return int (2), returned double (2.0) + Object c = (1.0 > 2 ? 1.0 : 2); + assert c.getClass() == Double.class && c.equals(2.0); + + // toNumber parsing preference ordering (int-then-long-then-double) demo + assert toNumber("123").equals(Optional.of(123)); + assert toNumber("123123123123123123").equals(Optional.of(123123123123123123l)); + assert toNumber("123123123123123123123123123123123123").equals(Optional.of(123123123123123123123123123123123123d)); + + // abs returns numbers in their appropriate type, not given type (string in this case) + assert abs("-123").equals(Optional.of(123)); + assert abs("-123123123123123123").equals(Optional.of(123123123123123123l)); + assert abs("-123123123123123123123123123123123123").equals(Optional.of(123123123123123123123123123123123123d)); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/StringsTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/StringsTest.java new file mode 100644 index 00000000..12c1c17f --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/modifier/function/StringsTest.java @@ -0,0 +1,56 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.modifier.function; + +import io.joltcommunity.jolt.common.Optional; +import org.testng.annotations.DataProvider; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +@SuppressWarnings("deprecated") +public class StringsTest extends AbstractTester { + + @DataProvider(parallel = true) + public Iterator getTestCases() { + List testCases = new LinkedList<>(); + + Function SPLIT = new Strings.split(); + + testCases.add(new Object[]{"split-invalid-null", SPLIT, null, Optional.empty()}); + testCases.add(new Object[]{"split-invalid-string", SPLIT, "", Optional.empty()}); + + testCases.add(new Object[]{"split-null-string", SPLIT, new Object[]{",", null}, Optional.empty()}); + testCases.add(new Object[]{"split-null-separator", SPLIT, new Object[]{null, "test"}, Optional.empty()}); + + testCases.add(new Object[]{"split-empty-string", SPLIT, new Object[]{",", ""}, Optional.of(Arrays.asList(""))}); + testCases.add(new Object[]{"split-single-token-string", SPLIT, new Object[]{",", "test"}, Optional.of(Arrays.asList("test"))}); + + testCases.add(new Object[]{"split-double-token-string", SPLIT, new Object[]{",", "test,TEST"}, Optional.of(Arrays.asList("test", "TEST"))}); + testCases.add(new Object[]{"split-multi-token-string", SPLIT, new Object[]{",", "test,TEST,Test,TeSt"}, Optional.of(Arrays.asList("test", "TEST", "Test", "TeSt"))}); + testCases.add(new Object[]{"split-spaced-token-string", SPLIT, new Object[]{",", "test, TEST"}, Optional.of(Arrays.asList("test", " TEST"))}); + testCases.add(new Object[]{"split-long-separator-spaced-token-string", SPLIT, new Object[]{", ", "test, TEST"}, Optional.of(Arrays.asList("test", "TEST"))}); + + testCases.add(new Object[]{"split-regex-token-string", SPLIT, new Object[]{"[eE]", "test,TEST"}, Optional.of(Arrays.asList("t", "st,T", "ST"))}); + testCases.add(new Object[]{"split-regex2-token-string", SPLIT, new Object[]{"\\s+", "test TEST Test TeSt"}, Optional.of(Arrays.asList("test", "TEST", "Test", "TeSt"))}); + + return testCases.iterator(); + } +} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java b/jolt-core/src/test/java/io/joltcommunity/jolt/sample/JoltSample.java similarity index 54% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/sample/JoltSample.java index a266afed..5cf0025f 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/sample/JoltSample.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/sample/JoltSample.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.sample; +package io.joltcommunity.jolt.sample; -import com.bazaarvoice.jolt.Chainr; -import com.bazaarvoice.jolt.JsonUtils; +import io.joltcommunity.jolt.Chainr; +import io.joltcommunity.jolt.JsonUtils; import java.util.List; @@ -24,12 +25,12 @@ public class JoltSample { public static void main(String[] args) { - List chainrSpecJSON = JsonUtils.classpathToList( "/json/sample/spec.json" ); - Chainr chainr = Chainr.fromSpec( chainrSpecJSON ); + List chainrSpecJSON = JsonUtils.classpathToList("/json/sample/spec.json"); + Chainr chainr = Chainr.fromSpec(chainrSpecJSON); - Object inputJSON = JsonUtils.classpathToObject( "/json/sample/input.json" ); + Object inputJSON = JsonUtils.classpathToObject("/json/sample/input.json"); - Object transformedOutput = chainr.transform( inputJSON ); - System.out.println( JsonUtils.toJsonString( transformedOutput ) ); + Object transformedOutput = chainr.transform(inputJSON); + System.out.println(JsonUtils.toJsonString(transformedOutput)); } } diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrTraversrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrTraversrTest.java new file mode 100644 index 00000000..cbd84568 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrTraversrTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.shiftr; + +import io.joltcommunity.jolt.JoltTestUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.common.Optional; +import io.joltcommunity.jolt.traversr.Traversr; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ShiftrTraversrTest { + + @DataProvider + public Object[][] inAndOutTestCases() throws Exception { + return new Object[][]{ + { + "simple place", + List.of("tuna"), + "tuna", + "a.b", + Arrays.asList("a", "b"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : \"tuna\" } }") + }, + { + "simple explicit array place", + List.of("tuna"), + null, + "a.b[]", + Arrays.asList("a", "b", "[]"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ \"tuna\" ] } }") + }, + { + "simple explicit array place with sub", + List.of("tuna"), + null, + "a.b[].c", + Arrays.asList("a", "b", "[]", "c"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ { \"c\" : \"tuna\" } ] } }") + }, + { + "simple array place", + List.of("tuna"), + "tuna", + "a.b.[1]", + Arrays.asList("a", "b", "1"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ null, \"tuna\" ] } }") + }, + { + "nested array place", + List.of("tuna"), + "tuna", + "a.b[1].c", + Arrays.asList("a", "b", "1", "c"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ null, { \"c\" : \"tuna\" } ] } }") + }, + { + "simple place into write array", + Arrays.asList("tuna", "marlin"), + Arrays.asList("tuna", "marlin"), + "a.b", + Arrays.asList("a", "b"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ \"tuna\", \"marlin\" ] } }") + }, + { + "simple array place with nested write array", + Arrays.asList("tuna", "marlin"), + Arrays.asList("tuna", "marlin"), + "a.b.[1]", + Arrays.asList("a", "b", "1"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ null, [ \"tuna\", \"marlin\" ] ] } }") + }, + { + "nested array place with nested ouptut array", + Arrays.asList("tuna", "marlin"), + Arrays.asList("tuna", "marlin"), + "a.b.[1].c", + Arrays.asList("a", "b", "1", "c"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : [ null, { \"c\" : [ \"tuna\", \"marlin\"] } ] } }") + } + }; + } + + @Test(dataProvider = "inAndOutTestCases") + public void setTest(String testCaseName, List outputs, Object notUsedInThisTest, String traversrPath, List keys, Map expected) throws Exception { + Map actual = new HashMap<>(); + + Traversr traversr = new ShiftrTraversr(traversrPath); + for (String output : outputs) { + traversr.set(actual, keys, output); + } + + JoltTestUtil.runDiffy(testCaseName, expected, actual); + } + + @Test(dataProvider = "inAndOutTestCases") + public void getTest(String testCaseName, List notUsedInThisTest, Object expected, String traversrPath, List keys, Map tree) throws Exception { + Traversr traversr = new ShiftrTraversr(traversrPath); + Optional actual = traversr.get(tree, keys); + + JoltTestUtil.runDiffy(testCaseName, expected, actual.get()); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrUnitTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrUnitTest.java new file mode 100644 index 00000000..e11adef9 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrUnitTest.java @@ -0,0 +1,289 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.shiftr; + +import io.joltcommunity.jolt.JoltTestUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.Shiftr; +import io.joltcommunity.jolt.common.PathElementBuilder; +import io.joltcommunity.jolt.common.pathelement.PathElement; +import io.joltcommunity.jolt.common.pathelement.TransposePathElement; +import io.joltcommunity.jolt.exception.SpecException; +import com.google.common.base.Joiner; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ShiftrUnitTest { + + @DataProvider + public Object[][] shiftrTestCases() throws IOException { + return new Object[][]{ + { + "Simple * and Reference", + JsonUtils.jsonToMap(""" + { + "tuna-*-marlin-*" : { + "rating-*" : "&(1,2).&.value" + } + }"""), + JsonUtils.jsonToMap(""" + { + "tuna-A-marlin-AAA" : { + "rating-BBB" : "bar" + } + }"""), + JsonUtils.jsonToMap(""" + { + "AAA" : { + "rating-BBB" : { + "value" : "bar" + } + } + }""") + }, + { + "Shift to two places", + JsonUtils.jsonToMap(""" + { + "tuna-*-marlin-*" : { + "rating-*" : [ "&(1,2).&.value", "foo"] + } + }"""), + JsonUtils.jsonToMap(""" + { + "tuna-A-marlin-AAA" : { + "rating-BBB" : "bar" + } + }"""), + JsonUtils.jsonToMap(""" + { + "foo" : "bar", "AAA" : { + "rating-BBB" : { + "value" : "bar" + } + } + }""") + }, + { + "Or", + JsonUtils.jsonToMap("{ \"tuna|marlin\" : \"&-write\" }"), + JsonUtils.jsonToMap("{ \"tuna\" : \"snapper\" }"), + JsonUtils.jsonToMap("{ \"tuna-write\" : \"snapper\" }") + }, + { + "KeyRef", + JsonUtils.jsonToMap(""" + { + "rating-*" : { + "&(0,1)" : { + "match" : "&" + } + } + }"""), + JsonUtils.jsonToMap(""" + { + "rating-a" : { + "a" : { + "match": "a-match" + }, + "random" : { + "match" : "noise" + } + }, + "rating-c" : { + "c" : { + "match": "c-match" + }, + "random" : { + "match" : "noise" + } + } + }"""), + JsonUtils.jsonToMap(""" + { + "match" : [ + "a-match", + "c-match" + ] + }""") + }, + { + "Complex array write", + JsonUtils.jsonToMap(""" + { + "tuna-*-marlin-*" : { + "rating-*" : "tuna[&(1,1)].marlin[&(1,2)].&(0,1)" + } + }"""), + JsonUtils.jsonToMap(""" + { + "tuna-2-marlin-3" : { "rating-BBB" : "bar" }, + "tuna-1-marlin-0" : { "rating-AAA" : "mahi" } + }"""), + JsonUtils.jsonToMap(""" + { + "tuna": [ + null, + { "marlin" : [ { "AAA" : "mahi" } ] }, + { "marlin" : [ null, null, null, { "BBB" : "bar" } ] } + ] + }""") + } + }; + } + + @Test(dataProvider = "shiftrTestCases") + public void shiftrUnitTest(String testName, Map spec, Map data, Map expected) throws Exception { + + Shiftr shiftr = new Shiftr(spec); + Object actual = shiftr.transform(data); + + JoltTestUtil.runDiffy(testName, expected, actual); + } + + + @DataProvider + public Object[][] badSpecs() throws IOException { + return new Object[][]{ + { + "Null Spec", + null, + }, + { + "List Spec", + new ArrayList<>(), + }, + { + "Empty spec", + JsonUtils.jsonToMap("{ }"), + }, + { + "Empty sub-spec", + JsonUtils.javason("{ 'tuna' : {} }"), + }, + { + "Bad @", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-@' : '&(1,2).&.value' } }"), + }, + { + "RHS @ by itself", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).@.value' } }"), + }, + { + "RHS @ with bad Parens", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).@(data.&(1,1).value' } }"), + }, + { + "RHS *", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).*.value' } }"), + }, + { + "RHS $", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-*' : '&(1,2).$.value' } }"), + }, + { + "Two Arrays", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-*' : [ '&(1,2).photos[&(0,1)]-subArray[&(1,2)].value', 'foo'] } }"), + }, + { + "Can't mix * and & in the same key", + JsonUtils.javason("{ 'tuna-*-marlin-*' : { 'rating-&(1,2)-*' : [ '&(1,2).value', 'foo'] } }"), + }, + { + "Don't put negative numbers in array references", + JsonUtils.javason("{ 'tuna' : 'marlin[-1]' }"), + } + }; + } + + @Test(dataProvider = "badSpecs", expectedExceptions = SpecException.class) + public void failureUnitTest(String testName, Object spec) { + new Shiftr(spec); + } + + /** + * @return canonical dotNotation String built from the given paths + */ + public String buildCanonicalString(List paths) { + + List pathStrs = new ArrayList<>(paths.size()); + for (PathElement pe : paths) { + pathStrs.add(pe.getCanonicalForm()); + } + + return Joiner.on(".").join(pathStrs); + } + + + @DataProvider + public Object[][] validRHS() throws IOException { + return new Object[][]{ + {"@a", "@(0,a)"}, + {"@abc", "@(0,abc)"}, + {"@a.b.c", "@(0,a).b.c"}, + {"@(a.b\\.c)", "@(0,a.b\\.c)"}, + {"@a.b.c", "@(0,a).b.c"}, + {"@a.b.@c", "@(0,a).b.@(0,c)"}, + {"@(a[2].&).b.@c", "@(0,a.[2].&(0,0)).b.@(0,c)"}, + {"a[&2].@b[1].c", "a.[&(2,0)].@(0,b).[1].c"} + }; + } + + @Test(dataProvider = "validRHS") + public void validRHSTests(String dotNotation, String expected) { + List paths = PathElementBuilder.parseDotNotationRHS(dotNotation); + String actualCanonicalForm = buildCanonicalString(paths); + + Assert.assertEquals(actualCanonicalForm, expected, "TestCase: " + dotNotation); + } + + @Test + public void testTransposePathParsing() { + + List paths = PathElementBuilder.parseDotNotationRHS("test.@(2,foo\\.bar)"); + + Assert.assertEquals(paths.size(), 2); + TransposePathElement actualApe = (TransposePathElement) paths.get(1); + + Assert.assertEquals(actualApe.getCanonicalForm(), "@(2,foo\\.bar)"); + } + + @DataProvider + public Object[][] badRHS() throws IOException { + return new Object[][]{ + {"@"}, + {"a@"}, + {"@a@b"}, + {"@(a.b.&(2,2)"}, // missing trailing ) + {"@(a.b.&(2,2).d"}, // missing trailing ) + {"@(a.b.@c).d"}, + {"@(a.*.c)"}, // @ can not contain a * + {"@(a.$2.c)"}, // @ can not contain a $ + }; + } + + @Test(dataProvider = "badRHS", expectedExceptions = SpecException.class) + public void failureRHSTests(String dotNotation) { + PathElementBuilder.parseDotNotationRHS(dotNotation); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrWritrTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrWritrTest.java new file mode 100644 index 00000000..2fced4ef --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/ShiftrWritrTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.shiftr; + +import io.joltcommunity.jolt.common.PathElementBuilder; +import io.joltcommunity.jolt.common.pathelement.*; +import io.joltcommunity.jolt.common.reference.AmpReference; +import io.joltcommunity.jolt.common.tree.MatchedElement; +import io.joltcommunity.jolt.common.tree.WalkedPath; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.List; + +// Todo Now that the PathElement classes have been split out (no longer inner classes) +// each class should get a test +public class ShiftrWritrTest { + + @Test + public void referenceTest() { + + ShiftrWriter path = new ShiftrWriter("SecondaryRatings.tuna-&(0,1)-marlin.Value"); + + Assert.assertEquals(path.get(0).getRawKey(), "SecondaryRatings"); + Assert.assertEquals(path.get(0).toString(), "SecondaryRatings"); + Assert.assertEquals(path.get(2).getRawKey(), "Value"); + Assert.assertEquals(path.get(2).toString(), "Value"); + Assert.assertEquals(path.get(2).toString(), "Value"); + + AmpPathElement refElement = (AmpPathElement) path.get(1); + + Assert.assertEquals(refElement.getTokens().size(), 3); + Assert.assertEquals((String) refElement.getTokens().get(0), "tuna-"); + Assert.assertEquals((String) refElement.getTokens().get(2), "-marlin"); + + Assert.assertTrue(refElement.getTokens().get(1) instanceof AmpReference); + AmpReference ref = (AmpReference) refElement.getTokens().get(1); + Assert.assertEquals(ref.getPathIndex(), 0); + Assert.assertEquals(ref.getKeyGroup(), 1); + } + + @Test + public void arrayRefTest() { + + ShiftrWriter path = new ShiftrWriter("ugc.photos-&1-bob[&2]"); + + Assert.assertEquals(path.size(), 3); + { // 0 + PathElement pe = path.get(0); + Assert.assertTrue(pe instanceof LiteralPathElement, "First pathElement should be a literal one."); + } + + { // 1 + PathElement pe = path.get(1); + Assert.assertTrue(pe instanceof AmpPathElement, "Second pathElement should be a AmpPathElement."); + + AmpPathElement refElement = (AmpPathElement) pe; + + Assert.assertEquals(refElement.getTokens().size(), 3); + + { + Assert.assertTrue(refElement.getTokens().get(0) instanceof String); + Assert.assertEquals((String) refElement.getTokens().get(0), "photos-"); + } + { + Assert.assertTrue(refElement.getTokens().get(1) instanceof AmpReference); + AmpReference ref = (AmpReference) refElement.getTokens().get(1); + Assert.assertEquals(ref.getCanonicalForm(), "&(1,0)"); + Assert.assertEquals(ref.getPathIndex(), 1); + Assert.assertEquals(ref.getKeyGroup(), 0); + } + { + Assert.assertTrue(refElement.getTokens().get(2) instanceof String); + Assert.assertEquals((String) refElement.getTokens().get(2), "-bob"); + } + } + + { // 2 + PathElement pe = path.get(2); + Assert.assertTrue(pe instanceof ArrayPathElement, "Third pathElement should be a literal one."); + + ArrayPathElement arrayElement = (ArrayPathElement) pe; + Assert.assertEquals(arrayElement.getCanonicalForm(), "[&(2,0)]"); + } + } + + @Test + public void calculateOutputTest_refsOnly() { + + MatchablePathElement pe1 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS("tuna-*-marlin-*"); + MatchablePathElement pe2 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS("rating-*"); + + MatchedElement lpe = pe1.match("tuna-marlin", new WalkedPath()); + Assert.assertNull(lpe); + + lpe = pe1.match("tuna-A-marlin-AAA", new WalkedPath()); + Assert.assertEquals(lpe.getRawKey(), "tuna-A-marlin-AAA"); + Assert.assertEquals(lpe.getSubKeyRef(0), "tuna-A-marlin-AAA"); + Assert.assertEquals(lpe.getSubKeyCount(), 3); + Assert.assertEquals(lpe.getSubKeyRef(1), "A"); + Assert.assertEquals(lpe.getSubKeyRef(2), "AAA"); + + MatchedElement lpe2 = pe2.match("rating-BBB", new WalkedPath(null, lpe)); + Assert.assertEquals(lpe2.getRawKey(), "rating-BBB"); + Assert.assertEquals(lpe2.getSubKeyRef(0), "rating-BBB"); + Assert.assertEquals(lpe2.getSubKeyCount(), 2); + Assert.assertEquals(lpe2.getSubKeyRef(1), "BBB"); + + ShiftrWriter outputPath = new ShiftrWriter("&(1,2).&.value"); + WalkedPath twoSteps = new WalkedPath(null, lpe); + twoSteps.add(null, lpe2); + { + EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get(0); + String evaledLeafOutput = outputElement.evaluate(twoSteps); + Assert.assertEquals(evaledLeafOutput, "AAA"); + } + { + EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get(1); + String evaledLeafOutput = outputElement.evaluate(twoSteps); + Assert.assertEquals(evaledLeafOutput, "rating-BBB"); + } + { + EvaluatablePathElement outputElement = (EvaluatablePathElement) outputPath.get(2); + String evaledLeafOutput = outputElement.evaluate(twoSteps); + Assert.assertEquals(evaledLeafOutput, "value"); + } + } + + @Test + public void calculateOutputTest_arrayIndexes() { + + // simulate Shiftr LHS specs + MatchablePathElement pe1 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS("tuna-*-marlin-*"); + MatchablePathElement pe2 = (MatchablePathElement) PathElementBuilder.parseSingleKeyLHS("rating-*"); + + // match them against some data to get LiteralPathElements with captured values + MatchedElement lpe = pe1.match("tuna-2-marlin-3", new WalkedPath()); + Assert.assertEquals(lpe.getSubKeyRef(1), "2"); + Assert.assertEquals(lpe.getSubKeyRef(2), "3"); + + MatchedElement lpe2 = pe2.match("rating-BBB", new WalkedPath(null, lpe)); + Assert.assertEquals(lpe2.getSubKeyCount(), 2); + Assert.assertEquals(lpe2.getSubKeyRef(1), "BBB"); + + // Build an write path path + ShiftrWriter shiftrWriter = new ShiftrWriter("tuna[&(1,1)].marlin[&(1,2)].&(0,1)"); + + Assert.assertEquals(shiftrWriter.size(), 5); + Assert.assertEquals(shiftrWriter.getCanonicalForm(), "tuna.[&(1,1)].marlin.[&(1,2)].&(0,1)"); + + // Evaluate the write path against the LiteralPath elements we build above ( like Shiftr does ) + WalkedPath twoSteps = new WalkedPath(null, lpe); + twoSteps.add(null, lpe2); + List stringPath = shiftrWriter.evaluate(twoSteps); + + Assert.assertEquals(stringPath.get(0), "tuna"); + Assert.assertEquals(stringPath.get(1), "2"); + Assert.assertEquals(stringPath.get(2), "marlin"); + Assert.assertEquals(stringPath.get(3), "3"); + Assert.assertEquals(stringPath.get(4), "BBB"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/spec/KeyOrderingTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/spec/KeyOrderingTest.java new file mode 100644 index 00000000..11a2de29 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/spec/KeyOrderingTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.shiftr.spec; + +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.SpecDriven; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class KeyOrderingTest { + + @DataProvider + public Object[][] shiftrKeyOrderingTestCases() throws IOException { + return new Object[][]{ + { + "Simple * and &", + JsonUtils.jsonToMap("{ \"*\" : { \"a\" : \"b\" }, \"&\" : { \"a\" : \"b\" } }"), + Arrays.asList("&(0,0)", "*") + }, + { + "2* and 2&", + JsonUtils.jsonToMap("{ \"rating-*\" : { \"a\" : \"b\" }, \"rating-range-*\" : { \"a\" : \"b\" }, \"&\" : { \"a\" : \"b\" }, \"tuna-&(0)\" : { \"a\" : \"b\" } }"), + Arrays.asList("tuna-&(0,0)", "&(0,0)", "rating-range-*", "rating-*") + }, + { + "2& alpha-number based fallback", + JsonUtils.jsonToMap("{ \"&\" : { \"a\" : \"b\" }, \"&(0,1)\" : { \"a\" : \"b\" } }"), + Arrays.asList("&(0,0)", "&(0,1)") + }, + { + "2* and 2& alpha fallback", + JsonUtils.jsonToMap("{ \"aaaa-*\" : { \"a\" : \"b\" }, \"bbbb-*\" : { \"a\" : \"b\" }, \"aaaa-&\" : { \"a\" : \"b\" }, \"bbbb-&(0)\" : { \"a\" : \"b\" } }"), + Arrays.asList("aaaa-&(0,0)", "bbbb-&(0,0)", "aaaa-*", "bbbb-*") + } + }; + } + + @Test(dataProvider = "shiftrKeyOrderingTestCases") + public void testKeyOrdering(String testName, Map spec, List expectedOrder) { + + ShiftrCompositeSpec root = new ShiftrCompositeSpec(SpecDriven.ROOT_KEY, spec); + + for (int index = 0; index < expectedOrder.size(); index++) { + String expected = expectedOrder.get(index); + Assert.assertEquals(expected, root.getComputedChildren().get(index).pathElement.getCanonicalForm(), testName); + } + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/spec/SpecParsingTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/spec/SpecParsingTest.java new file mode 100644 index 00000000..65715b45 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/shiftr/spec/SpecParsingTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.shiftr.spec; + +import io.joltcommunity.jolt.common.SpecStringParser; +import com.google.common.collect.Lists; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + +public class SpecParsingTest { + + @DataProvider + public Object[][] RHSParsingTestsRemoveEscapes() throws IOException { + return new Object[][]{ + { + "simple, no escape", + "a.b.c", + Arrays.asList("a", "b", "c"), + }, + { + "ref and array, no escape", + "a.&(1,2).[]", + Arrays.asList("a", "&(1,2)", "[]") + }, + { + "single transpose, no escape", + "a.@(l.m.n).c", + Arrays.asList("a", "@(l.m.n)", "c") + }, + { + "non-special char escape passes thru", + "a\\\\bc.def", + Arrays.asList("a\\bc", "def") + }, + { + "single escape", + "a\\.b.c", + Arrays.asList("a.b", "c") + }, + { + "escaping rhs", + "data.\\\\$rating-&1", + Arrays.asList("data", "\\$rating-&1") + }, + { + "@Class example", + "a.@Class.c", + Arrays.asList("a", "@(Class)", "c") + } + }; + } + + @Test(dataProvider = "RHSParsingTestsRemoveEscapes") + public void testRHSParsingRemoveEscapes(String testName, String unSweetendDotNotation, List expected) { + + List actual = SpecStringParser.parseDotNotation(Lists.newArrayList(), SpecStringParser.stringIterator(unSweetendDotNotation), unSweetendDotNotation); + + Assert.assertEquals(actual, expected, "Failed test name " + testName); + } + + @DataProvider + public Object[][] removeEscapeCharsTests() throws IOException { + + return new Object[][]{ + {"starts with escape", "\\@pants", "@pants"}, + {"escape in the middle", "rating-\\&pants", "rating-&pants"}, + {"escape the escape char", "rating\\\\pants", "rating\\pants"}, + }; + } + + @Test(dataProvider = "removeEscapeCharsTests") + public void testRemoveEscapeChars(String testName, String input, String expected) { + + String actual = SpecStringParser.removeEscapeChars(input); + Assert.assertEquals(actual, expected, "Failed test name " + testName); + } + + + @DataProvider + public Object[][] removeEscapedValuesTest() throws IOException { + + return new Object[][]{ + {"starts with escape", "\\@pants", "pants"}, + {"escape in the middle", "rating-\\&pants", "rating-pants"}, + {"escape the escape char", "rating\\\\pants", "ratingpants"}, + {"escape the array", "\\[\\]pants", "pants"}, + }; + } + + @Test(dataProvider = "removeEscapedValuesTest") + public void testEscapeParsing(String testName, String input, String expected) { + + String actual = SpecStringParser.removeEscapedValues(input); + Assert.assertEquals(actual, expected, "Failed test name " + testName); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/traversr/SimpleTraversalTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/traversr/SimpleTraversalTest.java new file mode 100644 index 00000000..5666e131 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/traversr/SimpleTraversalTest.java @@ -0,0 +1,218 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.traversr; + +import io.joltcommunity.jolt.JoltTestUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.common.Optional; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SimpleTraversalTest { + + @DataProvider + public Object[][] inAndOutTestCases() throws Exception { + return new Object[][]{ + { + "Simple Map Test", + SimpleTraversal.newTraversal("a.b"), + JsonUtils.jsonToMap("{ \"a\" : null }"), + JsonUtils.jsonToMap("{ \"a\" : { \"b\" : \"tuna\" } }"), + "tuna" + }, + { + "Simple explicit array test", + SimpleTraversal.newTraversal("a.[1].b"), + JsonUtils.jsonToMap("{ \"a\" : null }"), + JsonUtils.jsonToMap("{ \"a\" : [ null, { \"b\" : \"tuna\" } ] }"), + "tuna" + }, + { + "Leading Array test", + SimpleTraversal.newTraversal("[0].a"), + JsonUtils.jsonToObject("[ ]"), + JsonUtils.jsonToObject("[ { \"a\" : \"b\" } ]"), + "b" + }, + { + "Auto expand array test", + SimpleTraversal.newTraversal("a.[].b"), + JsonUtils.jsonToMap("{ \"a\" : null }"), + JsonUtils.jsonToMap("{ \"a\" : [ { \"b\" : null } ] }"), + null + } + }; + } + + @Test(dataProvider = "inAndOutTestCases") + public void getTests(String testDescription, SimpleTraversal simpleTraversal, Object ignoredForTest, Object input, String expected) throws IOException { + + Object original = JsonUtils.cloneJson(input); + Object tree = JsonUtils.cloneJson(input); + + Optional actual = simpleTraversal.get(tree); + + Assert.assertEquals(actual.get(), expected); + JoltTestUtil.runDiffy("Get should not have modified the input", original, tree); + } + + @Test(dataProvider = "inAndOutTestCases") + public void setTests(String testDescription, SimpleTraversal simpleTraversal, Object start, Object expected, String toSet) { + + Object actual = JsonUtils.cloneJson(start); + + Assert.assertEquals(toSet, simpleTraversal.set(actual, toSet).get()); // set should be successful + + Assert.assertEquals(actual, expected); + } + + @Test + public void testAutoArray() throws IOException { + SimpleTraversal traversal = SimpleTraversal.newTraversal("a.[].b"); + + Object expected = JsonUtils.jsonToMap("{ \"a\" : [ { \"b\" : \"one\" }, { \"b\" : \"two\" } ] }"); + + Object actual = new HashMap<>(); + + Assert.assertFalse(traversal.get(actual).isPresent()); + Assert.assertEquals(((HashMap) actual).size(), 0); // get didn't add anything + + // Add two things and validate the Auto Expand array + Assert.assertEquals(traversal.set(actual, "one").get(), "one"); + Assert.assertEquals(traversal.set(actual, "two").get(), "two"); + + JoltTestUtil.runDiffy(expected, actual); + } + + @Test + public void testOverwrite() throws IOException { + SimpleTraversal traversal = SimpleTraversal.newTraversal("a.b"); + + Object actual = JsonUtils.jsonToMap("{ \"a\" : { \"b\" : \"tuna\" } }"); + Object expectedOne = JsonUtils.jsonToMap("{ \"a\" : { \"b\" : \"one\" } }"); + Object expectedTwo = JsonUtils.jsonToMap("{ \"a\" : { \"b\" : \"two\" } }"); + + Assert.assertEquals(traversal.get(actual).get(), "tuna"); + + // Set twice and verify that the sets did in fact overwrite + Assert.assertEquals(traversal.set(actual, "one").get(), "one"); + JoltTestUtil.runDiffy(expectedOne, actual); + + Assert.assertEquals(traversal.set(actual, "two").get(), "two"); + JoltTestUtil.runDiffy(expectedTwo, actual); + } + + @DataProvider + public Object[][] removeTestCases() throws Exception { + return new Object[][]{ + { + "Inception Map Test", + SimpleTraversal.newTraversal("__queryContext"), + JsonUtils.javason("{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }"), + JsonUtils.javason("{ 'Id' : '1234' }"), + JsonUtils.javason("{ 'catalogLin' : [ 'a', 'b' ] }") + }, + { + "List Test", + SimpleTraversal.newTraversal("a.list.[1]"), + JsonUtils.javason("{ 'a' : { 'list' : [ 'a', 'b', 'c' ] } }"), + JsonUtils.javason("{ 'a' : { 'list' : [ 'a', 'c' ] } }"), + "b" + }, + { + "Map leave empty Map", + SimpleTraversal.newTraversal("a.list"), + JsonUtils.javason("{ 'a' : { 'list' : [ 'a', 'b', 'c' ] } }"), + JsonUtils.javason("{ 'a' : { } }"), + Arrays.asList("a", "b", "c") + }, + { + "Map leave empty List", + SimpleTraversal.newTraversal("a.list.[0]"), + JsonUtils.javason("{ 'a' : { 'list' : [ 'a' ] } }"), + JsonUtils.javason("{ 'a' : { 'list' : [ ] } }"), + "a" + } + }; + } + + @Test(dataProvider = "removeTestCases") + public void removeTests(String testDescription, SimpleTraversal simpleTraversal, + Object start, Object expectedLeft, Object expectedReturn) + throws Exception { + + Optional actualRemoveOpt = simpleTraversal.remove(start); + JoltTestUtil.runDiffy(testDescription, expectedReturn, actualRemoveOpt.get()); + + JoltTestUtil.runDiffy(testDescription, expectedLeft, start); + } + + @Test(expectedExceptions = ClassCastException.class) + public void exceptionTestListIsMap() throws Exception { + Object tree = JsonUtils.javason("{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }"); + + SimpleTraversal trav = SimpleTraversal.newTraversal("__queryContext"); + // barfs here, needs the 'List list =' part to trigger it + @SuppressWarnings("unused") + List list = trav.get(tree).get(); + } + + @Test(expectedExceptions = ClassCastException.class) + public void exceptionTestMapIsList() throws Exception { + Object tree = JsonUtils.javason("{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }"); + + SimpleTraversal trav = SimpleTraversal.newTraversal("__queryContext.catalogLin"); + // barfs here, needs the 'Map map =' part to trigger it + @SuppressWarnings("unused") + Map map = trav.get(tree).get(); + } + + @Test(expectedExceptions = ClassCastException.class) + public void exceptionTestListIsMapErasure() throws Exception { + Object tree = JsonUtils.javason("{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : [ 'a', 'b' ] } }"); + + SimpleTraversal> trav = SimpleTraversal.newTraversal("__queryContext"); + // this works + Map queryContext = trav.get(tree).get(); + + // this does not + @SuppressWarnings("unused") + Map catalogLin = queryContext.get("catalogLin"); + Assert.fail("Expected ClassCast Exception"); + } + + @Test(expectedExceptions = ClassCastException.class) + public void exceptionTestLMapIsListErasure() throws Exception { + Object tree = JsonUtils.javason("{ 'Id' : '1234', '__queryContext' : { 'catalogLin' : { 'a' : 'b' } } }"); + + SimpleTraversal> trav = SimpleTraversal.newTraversal("__queryContext"); + // this works + Map queryContext = trav.get(tree).get(); + + // this does not + @SuppressWarnings("unused") + List catalogLin = queryContext.get("catalogLin"); + Assert.fail("Expected ClassCast Exception"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsNavigateTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsNavigateTest.java new file mode 100644 index 00000000..19221ee7 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsNavigateTest.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.utils; + +import io.joltcommunity.jolt.JsonUtils; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.util.List; +import java.util.Map; + +import static io.joltcommunity.jolt.utils.JoltUtils.*; + +public class JoltUtilsNavigateTest { + + private Object jsonSource; + private Object jsonSource_empty; + + @BeforeClass + public void setup() { + + String jsonSourceString = "{ " + + " 'a': { " + + " 'b': [ 0, 1, 2, 1.618 ] " + + " }, " + + " 'p': [ 'm', 'n', " + + " { " + + " '1': 1, " + + " '2': 2, " + + " 'pi': 3.14159 " + + " } " + + " ], " + + " 'x': 'y' " + + "}\n"; + + jsonSource = JsonUtils.javason(jsonSourceString); + + String jsonSourceString_empty = + "{" + + "'e': { 'f': {}, 'g': [] }," + + "'h': [ {}, [] ]" + + "}"; + + jsonSource_empty = JsonUtils.javason(jsonSourceString_empty); + } + + + @DataProvider(parallel = true) + public Object[][] validNavigateTests() { + + return new Object[][]{ + + {0, new Object[]{"a", "b", 0}}, + {1, new Object[]{"a", "b", 1}}, + {2, new Object[]{"a", "b", 2}}, + {1.618, new Object[]{"a", "b", 3}}, + {"m", new Object[]{"p", 0}}, + {"n", new Object[]{"p", 1}}, + {1, new Object[]{"p", 2, "1"}}, + {2, new Object[]{"p", 2, "2"}}, + {3.14159, new Object[]{"p", 2, "pi"}}, + {"y", new Object[]{"x"}}, + + {((Map) jsonSource).get("a"), new Object[]{"a"}}, + {((Map) (((Map) jsonSource).get("a"))).get("b"), new Object[]{"a", "b"}}, + {((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(0), new Object[]{"a", "b", 0}}, + {((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(1), new Object[]{"a", "b", 1}}, + {((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(2), new Object[]{"a", "b", 2}}, + {((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(3), new Object[]{"a", "b", 3}}, + {((Map) jsonSource).get("p"), new Object[]{"p"}}, + {((List) (((Map) jsonSource).get("p"))).get(0), new Object[]{"p", 0}}, + {((List) (((Map) jsonSource).get("p"))).get(1), new Object[]{"p", 1}}, + {((List) (((Map) jsonSource).get("p"))).get(2), new Object[]{"p", 2}}, + {((Map) ((List) (((Map) jsonSource).get("p"))).get(2)).get("1"), new Object[]{"p", 2, "1"}}, + {((Map) ((List) (((Map) jsonSource).get("p"))).get(2)).get("2"), new Object[]{"p", 2, "2"}}, + {((Map) ((List) (((Map) jsonSource).get("p"))).get(2)).get("pi"), new Object[]{"p", 2, "pi"}}, + + {((Map) jsonSource).get("x"), new Object[]{"x"}}, + }; + } + + @Test(dataProvider = "validNavigateTests") + public void navigate_happy_tests(Object expected, Object[] path) { + Object actual = navigate(jsonSource, path); + Assert.assertEquals(actual, expected); + } + + @Test(dataProvider = "validNavigateTests") + public void navigateStrict_happy_tests(Object expected, Object[] path) { + Object actual = navigateStrict(jsonSource, path); + Assert.assertEquals(actual, expected); + } + + @Test(dataProvider = "validNavigateTests") + public void navigateOrDefault_happy_tests(Object expected, Object[] path) { + Object actual = navigateOrDefault(null, jsonSource, path); + Assert.assertEquals(actual, expected); + } + + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void navigateStrictThrowsException() { + Object actual = navigateStrict(jsonSource, "pants", "shoes"); + Assert.fail("Should have thrown an Exception"); + } + + + @DataProvider(parallel = true) + public Object[][] navigateOrDefault_testCases() { + + return new Object[][]{ + + {new Object[]{"a", "b"}}, // verify that trying to read from two nested that don't exist works + {new Object[]{"h", -3}}, // verify that trying to read from an existing list with a negative index does not blow up + {new Object[]{"h", 4}}, // verify that trying to read from an existing list with a index bigger that the list does not blow up + }; + } + + @Test(dataProvider = "navigateOrDefault_testCases") + public void navigatorSafe(Object[] path) { + + Object actual = navigateOrDefault("pants", jsonSource_empty, path); + Assert.assertEquals(actual, "pants"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsRemoveTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsRemoveTest.java new file mode 100644 index 00000000..b727f78a --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsRemoveTest.java @@ -0,0 +1,125 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.utils; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtils; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.testng.collections.Lists; + +import java.util.List; +import java.util.Map; + +public class JoltUtilsRemoveTest { + + private Diffy diffy = new Diffy(); + + private Map ab = ImmutableMap.builder().put("a", "b").build(); + private Map cd = ImmutableMap.builder().put("c", "d").build(); + private Map top = ImmutableMap.builder().put("A", ab).put("B", cd).build(); + + @DataProvider + public Object[][] removeRecursiveCases() { + + Map empty = ImmutableMap.builder().build(); + Map barToFoo = ImmutableMap.builder().put("bar", "foo").build(); + Map fooToBar = ImmutableMap.builder().put("foo", "bar").build(); + return new Object[][]{ + {null, null, null}, + {null, "foo", null}, + {"foo", null, "foo"}, + {"foo", "foo", "foo"}, + {Maps.newHashMap(), "foo", empty}, + {Maps.newHashMap(barToFoo), "foo", barToFoo}, + {Maps.newHashMap(fooToBar), "foo", empty}, + {Lists.newArrayList(), "foo", ImmutableList.builder().build()}, + { + Lists.newArrayList(ImmutableList.builder() + .add(Maps.newHashMap(barToFoo)) + .build()), + "foo", + ImmutableList.builder() + .add(barToFoo) + .build() + }, + { + Lists.newArrayList(ImmutableList.builder() + .add(Maps.newHashMap(fooToBar)) + .build()), + "foo", + ImmutableList.builder() + .add(empty) + .build() + } + }; + } + + @Test(dataProvider = "removeRecursiveCases") + public void testRemoveRecursive(Object json, String key, Object expected) { + + JoltUtils.removeRecursive(json, key); + + Diffy.Result result = diffy.diff(expected, json); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + + @Test + public void runFixtureTests() { + + String testFixture = "/json/utils/joltUtils-removeRecursive.json"; + @SuppressWarnings("unchecked") + List> tests = (List>) JsonUtils.classpathToObject(testFixture); + + for (Map testUnit : tests) { + + Object data = testUnit.get("input"); + String toRemove = (String) testUnit.get("remove"); + Object expected = testUnit.get("expected"); + + JoltUtils.removeRecursive(data, toRemove); + + Diffy.Result result = diffy.diff(expected, data); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + } + + @Test(description = "No exception if we don't try to remove from an ImmutableMap.") + public void doNotUnnecessarilyDieOnImmutableMaps() { + Map expected = JsonUtils.jsonToMap(JsonUtils.toJsonString(top)); + + JoltUtils.removeRecursive(top, "tuna"); + + Diffy.Result result = diffy.diff(expected, top); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + + @Test(expectedExceptions = UnsupportedOperationException.class, description = "Exception if try to remove from an Immutable map.") + public void correctExceptionWithImmutableMap() { + JoltUtils.removeRecursive(top, "c"); + } +} diff --git a/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsSquashTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsSquashTest.java new file mode 100644 index 00000000..8319cc56 --- /dev/null +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsSquashTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.utils; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.modifier.function.Objects; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.Serializable; +import java.util.*; + +public class JoltUtilsSquashTest { + + private final Diffy diffy = new Diffy(); + + @Test + public void squashNullsInAListTest() { + List actual = new ArrayList(Arrays.asList("a", null, 1, null, "b", 2)); + + List expectedList = Arrays.asList("a", 1, "b", 2); + + Objects.squashNulls(actual); + + Diffy.Result result = diffy.diff(expectedList, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + + @Test + public void squashNullsInAMapTest() { + Map actual = new HashMap<>(); + actual.put("a", 1); + actual.put("b", null); + actual.put("c", "C"); + + Map expectedMap = new HashMap<>(); + expectedMap.put("a", 1); + expectedMap.put("c", "C"); + + Objects.squashNulls(actual); + + Diffy.Result result = diffy.diff(expectedMap, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + + + @Test + public void recursivelySquashNullsTest() { + Map actual = JsonUtils.javason("{ 'a' : 1, 'b' : null, 'c' : [ null, 4, null, 5, { 'x' : 'X', 'y' : null } ] }"); + Map expected = JsonUtils.javason("{ 'a' : 1, 'c' : [ 4, 5, { 'x' : 'X' } ] }"); + + Objects.recursivelySquashNulls(actual); + + Diffy.Result result = diffy.diff(expected, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } +} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsTest.java similarity index 75% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsTest.java index 6ae9db89..4ff34b35 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/JoltUtilsTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/JoltUtilsTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +14,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.utils; +package io.joltcommunity.jolt.utils; -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.traversr.SimpleTraversal; +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.traversr.SimpleTraversal; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; @@ -31,15 +32,13 @@ import java.util.List; import java.util.Map; -import static com.bazaarvoice.jolt.utils.JoltUtils.isBlankJson; -import static com.bazaarvoice.jolt.utils.JoltUtils.isVacantJson; -import static com.bazaarvoice.jolt.utils.JoltUtils.navigate; +import static io.joltcommunity.jolt.utils.JoltUtils.*; public class JoltUtilsTest { private Diffy diffy = new Diffy(); - private Object[] flattenedValues = {0, 1, 2, 1.618, "m", "n", 1, 2, 3.14159, "y" }; + private Object[] flattenedValues = {0, 1, 2, 1.618, "m", "n", 1, 2, 3.14159, "y"}; private Object jsonSource; private Object jsonSource_empty; @@ -48,26 +47,26 @@ public class JoltUtilsTest { @SuppressWarnings("unchecked") public void setup() { String jsonSourceString = "{ " + - " 'a': { " + - " 'b': [ 0, 1, 2, 1.618 ] " + - " }, " + - " 'p': [ 'm', 'n', " + - " { " + - " '1': 1, " + - " '2': 2, " + - " 'pi': 3.14159 " + - " } " + - " ], " + - " 'x': 'y' " + - "}\n"; + " 'a': { " + + " 'b': [ 0, 1, 2, 1.618 ] " + + " }, " + + " 'p': [ 'm', 'n', " + + " { " + + " '1': 1, " + + " '2': 2, " + + " 'pi': 3.14159 " + + " } " + + " ], " + + " 'x': 'y' " + + "}\n"; jsonSource = JsonUtils.javason(jsonSourceString); String jsonSourceString_empty = - "{" + - "'e': { 'f': {}, 'g': [] }," + - "'h': [ {}, [] ]" + - "}"; + "{" + + "'e': { 'f': {}, 'g': [] }," + + "'h': [ {}, [] ]" + + "}"; jsonSource_empty = JsonUtils.javason(jsonSourceString_empty); } @@ -89,7 +88,7 @@ public void testIsEmptyJson() { @Test public void testListKeyChains() { List keyChains = JoltUtils.listKeyChains(jsonSource); - for(int i=0; i(humanReadablePath).set(duplicate, navigate(jsonSource, paths)); } @@ -128,15 +127,15 @@ public Iterator storeTestCases() { String testFixture = "/json/utils/joltUtils-store-remove-compact.json"; @SuppressWarnings("unchecked") - List> tests = (List>) JsonUtils.classpathToObject( testFixture ); + List> tests = (List>) JsonUtils.classpathToObject(testFixture); List testCases = new LinkedList<>(); - for(Map testCase: tests) { - testCases.add(new Object[] { + for (Map testCase : tests) { + testCases.add(new Object[]{ testCase.get("description"), testCase.get("source"), - ((List)testCase.get("path")).toArray(), + ((List) testCase.get("path")).toArray(), testCase.get("value"), testCase.get("output") }); @@ -149,12 +148,12 @@ public Iterator storeTestCases() { * Given a source, an output, and a pair of path-to-values, stores-then-validates-then-removes those * resulting in a mutated source, which is finally compacted and matched with given output */ - @Test (dataProvider = "storeTestCases") + @Test(dataProvider = "storeTestCases") public void testStoreRemoveCompact(String description, Object source, Object[] path, Object value, Object output) { Object existingValue = JoltUtils.navigateOrDefault(null, source, path); - Assert.assertEquals( JoltUtils.store(source, value, path), existingValue); - Assert.assertEquals( JoltUtils.remove( source, path ), value ); + Assert.assertEquals(JoltUtils.store(source, value, path), existingValue); + Assert.assertEquals(JoltUtils.remove(source, path), value); // check the json object int noCompactionSize = JoltUtils.listKeyChains(source).size(); @@ -164,4 +163,4 @@ public void testStoreRemoveCompact(String description, Object source, Object[] p Assert.assertTrue(noCompactionSize >= compactedSize); Assert.assertTrue(diffy.diff(output, source).isEmpty()); } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/StringToolsTest.java b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/StringToolsTest.java similarity index 62% rename from jolt-core/src/test/java/com/bazaarvoice/jolt/utils/StringToolsTest.java rename to jolt-core/src/test/java/io/joltcommunity/jolt/utils/StringToolsTest.java index fce18a1e..0f98d250 100644 --- a/jolt-core/src/test/java/com/bazaarvoice/jolt/utils/StringToolsTest.java +++ b/jolt-core/src/test/java/io/joltcommunity/jolt/utils/StringToolsTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.utils; +package io.joltcommunity.jolt.utils; import com.beust.jcommander.internal.Lists; import org.apache.commons.lang3.RandomStringUtils; @@ -25,37 +26,37 @@ import java.util.List; /** -* StringTools Tester. -*/ + * StringTools Tester. + */ public class StringToolsTest { - @DataProvider (parallel = true) + @DataProvider(parallel = true) public Iterator testCaseGenerator() { List testCases = Lists.newArrayList(); - testCases.add(new String[] {null, null}); - testCases.add(new String[] {"", ""}); + testCases.add(new String[]{null, null}); + testCases.add(new String[]{"", ""}); - testCases.add(new String[] {null, ""}); - testCases.add(new String[] {"", null}); + testCases.add(new String[]{null, ""}); + testCases.add(new String[]{"", null}); - testCases.add(new String[] {RandomStringUtils.randomAscii(1<<2), null}); - testCases.add(new String[] {RandomStringUtils.randomAscii(1<<2), ""}); + testCases.add(new String[]{RandomStringUtils.secure().nextAscii(1 << 2), null}); + testCases.add(new String[]{RandomStringUtils.secure().nextAscii(1 << 2), ""}); - testCases.add(new String[] {null, RandomStringUtils.randomAscii(1<<2)}); - testCases.add(new String[] {"", RandomStringUtils.randomAscii(1<<2)}); + testCases.add(new String[]{null, RandomStringUtils.secure().nextAscii(1 << 2)}); + testCases.add(new String[]{"", RandomStringUtils.secure().nextAscii(1 << 2)}); - int i=1<<6; - while(i-- > 0) { - testCases.add(new String[] { RandomStringUtils.randomAscii(1<<10), RandomStringUtils.randomAscii(1<<2) }); - testCases.add(new String[] { RandomStringUtils.randomAscii(1<<2), RandomStringUtils.randomAscii(1<<10) }); + int i = 1 << 6; + while (i-- > 0) { + testCases.add(new String[]{RandomStringUtils.secure().nextAscii(1 << 10), RandomStringUtils.secure().nextAscii(1 << 2)}); + testCases.add(new String[]{RandomStringUtils.secure().nextAscii(1 << 2), RandomStringUtils.secure().nextAscii(1 << 10)}); - testCases.add(new String[] { RandomStringUtils.randomAlphabetic(1<<10), RandomStringUtils.randomAlphabetic(1<<2) }); - testCases.add(new String[] { RandomStringUtils.randomAlphabetic(1<<2), RandomStringUtils.randomAlphabetic(1<<10) }); + testCases.add(new String[]{RandomStringUtils.secure().nextAlphabetic(1 << 10), RandomStringUtils.secure().nextAlphabetic(1 << 2)}); + testCases.add(new String[]{RandomStringUtils.secure().nextAlphabetic(1 << 2), RandomStringUtils.secure().nextAlphabetic(1 << 10)}); - testCases.add(new String[] { RandomStringUtils.randomAlphanumeric(1<<10), RandomStringUtils.randomAlphanumeric(1<<2) }); - testCases.add(new String[] { RandomStringUtils.randomAlphanumeric(1<<2), RandomStringUtils.randomAlphanumeric(1<<10) }); + testCases.add(new String[]{RandomStringUtils.secure().nextAlphanumeric(1 << 10), RandomStringUtils.secure().nextAlphanumeric(1 << 2)}); + testCases.add(new String[]{RandomStringUtils.secure().nextAlphanumeric(1 << 2), RandomStringUtils.secure().nextAlphanumeric(1 << 10)}); } return testCases.iterator(); diff --git a/jolt-core/src/test/resources/json/cardinality/atTestData.json b/jolt-core/src/test/resources/json/cardinality/atTestData.json index cd4a7a19..f65e54ad 100644 --- a/jolt-core/src/test/resources/json/cardinality/atTestData.json +++ b/jolt-core/src/test/resources/json/cardinality/atTestData.json @@ -1,105 +1,108 @@ { - "input" : { - - "photos" : { - "url" : [ "http://pants.com/123-normal.jpg", "http://pants.com/123-thumbnail.jpg" ], - "caption" : "Nice pants" + "input": { + "photos": { + "url": [ + "http://pants.com/123-normal.jpg", + "http://pants.com/123-thumbnail.jpg" + ], + "caption": "Nice pants" }, - - "photosArray" : [ + "photosArray": [ { - "url" : [ "http://pants.com/123-normal.jpg", "http://pants.com/123-thumbnail.jpg" ], - "caption" : "Nice pants" + "url": [ + "http://pants.com/123-normal.jpg", + "http://pants.com/123-thumbnail.jpg" + ], + "caption": "Nice pants" }, { - "url" : [ "http://pants.com/123-thumbnail.jpg", "http://pants.com/123-normal.jpg" ], - "caption" : "Nice pants" + "url": [ + "http://pants.com/123-thumbnail.jpg", + "http://pants.com/123-normal.jpg" + ], + "caption": "Nice pants" } ], - - "views" : [ - { "count" : 1024 }, - { "count" : 2048 } + "views": [ + { + "count": 1024 + }, + { + "count": 2048 + } ], - - "nullParent" : null, - - "emptyArray" : [], - - "zion" : { - "neo" : "anderson", - "matrix" : "sweet" + "nullParent": null, + "emptyArray": [], + "zion": { + "neo": "anderson", + "matrix": "sweet" } }, - - "spec" : { - + "spec": { // This is the more common use case - "photos" : { - "@" : "MANY", // make photos an array - "*" : { // for each item in the array - "url" : "ONE" // url should be singular + "photos": { + "@": "MANY", + // make photos an array + "*": { + // for each item in the array + "url": "ONE" + // url should be singular } }, - - "photosArray" : { - "*" : { // for each item in the array - "url" : "ONE" // url should be singular + "photosArray": { + "*": { + // for each item in the array + "url": "ONE" + // url should be singular } }, - // This is a corner case - "views" : { - "@" : "ONE", // make the views be singular - "count" : "MANY" // make the count be many + "views": { + "@": "ONE", + // make the views be singular + "count": "MANY" + // make the count be many }, - - "nullParent" : { - "@" : "MANY" + "nullParent": { + "@": "MANY" }, - - "emptyArray" : { - "@" : "MANY" + "emptyArray": { + "@": "MANY" }, - - "zion" : { - "neo" : "ONE", - "*" : "MANY" + "zion": { + "neo": "ONE", + "*": "MANY" } }, - - "expected" : { - "photos" : [ + "expected": { + "photos": [ { - "url" : "http://pants.com/123-normal.jpg", - "caption" : "Nice pants" + "url": "http://pants.com/123-normal.jpg", + "caption": "Nice pants" } ], - - "photosArray" : [ + "photosArray": [ { - "url" : "http://pants.com/123-normal.jpg", - "caption" : "Nice pants" + "url": "http://pants.com/123-normal.jpg", + "caption": "Nice pants" }, { - "url" : "http://pants.com/123-thumbnail.jpg", - "caption" : "Nice pants" + "url": "http://pants.com/123-thumbnail.jpg", + "caption": "Nice pants" } ], - - "views" : { - "count" : [ 1024 ] + "views": { + "count": [ + 1024 + ] }, - - "nullParent" : [], - - "emptyArray" : [], - - "zion" : { - "neo" : "anderson", - "matrix" : [ + "nullParent": [], + "emptyArray": [], + "zion": { + "neo": "anderson", + "matrix": [ "sweet" ] } } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/cardinality/exceptionScalarInput.json b/jolt-core/src/test/resources/json/cardinality/exceptionScalarInput.json new file mode 100644 index 00000000..cc8e9383 --- /dev/null +++ b/jolt-core/src/test/resources/json/cardinality/exceptionScalarInput.json @@ -0,0 +1,13 @@ +{ + "input": { + "photos": "scalar" + }, + "spec": { + "photos": { + "scalar": "MANY" + } + }, + "expected": { + "photos": "scalar" + } +} diff --git a/jolt-core/src/test/resources/json/cardinality/failCardinalityType.json b/jolt-core/src/test/resources/json/cardinality/failCardinalityType.json index 76b7126e..85c243ed 100644 --- a/jolt-core/src/test/resources/json/cardinality/failCardinalityType.json +++ b/jolt-core/src/test/resources/json/cardinality/failCardinalityType.json @@ -1,13 +1,14 @@ { - "input" : { - - "photos" : { - "url" : [ "http://pants.com/123-normal.jpg", "http://pants.com/123-thumbnail.jpg" ], - "caption" : "Nice pants" + "input": { + "photos": { + "url": [ + "http://pants.com/123-normal.jpg", + "http://pants.com/123-thumbnail.jpg" + ], + "caption": "Nice pants" } }, - - "spec" : { - "photos" : "pants" + "spec": { + "photos": "pants" } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/cardinality/manyLiteralTestData.json b/jolt-core/src/test/resources/json/cardinality/manyLiteralTestData.json index 6624f32f..dff155ff 100644 --- a/jolt-core/src/test/resources/json/cardinality/manyLiteralTestData.json +++ b/jolt-core/src/test/resources/json/cardinality/manyLiteralTestData.json @@ -1,24 +1,19 @@ { - "input" : { - "categories" : { - "brand" : "Apple", - "category" : "iPod" + "input": { + "categories": { + "brand": "Apple", + "category": "iPod" }, - - "stuff" : { - "nestedStuff" : [ + "stuff": { + "nestedStuff": [ "item", "thingy" ] }, - - "isSingleton" : "value", - - "nullData" : null, - - "emptyArray" : [] + "isSingleton": "value", + "nullData": null, + "emptyArray": [] }, - // Rules // MANY // if array, then done @@ -29,45 +24,33 @@ // if array, grab [0] if it exists // - "spec" : { - + "spec": { // In the input above Category is a Map, we want it to be an Array // Take the input map, and set it as index 0 of an array - "categories" : "MANY", - - "stuff" : { - "nestedStuff" : "MANY" + "categories": "MANY", + "stuff": { + "nestedStuff": "MANY" }, - - "data" : "MANY", - - "nullData" : "MANY", - - "emptyArray" : "MANY", - - "doesNotHitAnything" : "MANY" - + "data": "MANY", + "nullData": "MANY", + "emptyArray": "MANY", + "doesNotHitAnything": "MANY" }, - - "expected" : { - "categories" : [ + "expected": { + "categories": [ { - "brand" : "Apple", - "category" : "iPod" + "brand": "Apple", + "category": "iPod" } ], - - "stuff" : { - "nestedStuff" : [ + "stuff": { + "nestedStuff": [ "item", "thingy" ] }, - - "isSingleton" : "value", - - "nullData" : [], - - "emptyArray" : [] + "isSingleton": "value", + "nullData": [], + "emptyArray": [] } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/cardinality/nullScalarInputData.json b/jolt-core/src/test/resources/json/cardinality/nullScalarInputData.json new file mode 100644 index 00000000..762c8434 --- /dev/null +++ b/jolt-core/src/test/resources/json/cardinality/nullScalarInputData.json @@ -0,0 +1,14 @@ +{ + "input": { + "photos": null + }, + "spec": { + "photos": { + "url": "ONE", + "caption": "MANY" + } + }, + "expected": { + "photos": null + } +} diff --git a/jolt-core/src/test/resources/json/cardinality/oneLiteralTestData.json b/jolt-core/src/test/resources/json/cardinality/oneLiteralTestData.json index 794a1b06..0b77ab25 100644 --- a/jolt-core/src/test/resources/json/cardinality/oneLiteralTestData.json +++ b/jolt-core/src/test/resources/json/cardinality/oneLiteralTestData.json @@ -1,34 +1,32 @@ { - "input" : { - - "review" : { - "rating" : [ 5, 4 ] + "input": { + "review": { + "rating": [ + 5, + 4 + ] }, - - "data" : { - "stats" : [ { - "viewed" : 10, - "clicks" : 20 + "data": { + "stats": [ + { + "viewed": 10, + "clicks": 20 }, { - "viewed" : 3, - "clicks" : 2 - } ] + "viewed": 3, + "clicks": 2 + } + ] }, - - "isMap" : { - "data" : "stuff" + "isMap": { + "data": "stuff" }, - - "isSingleton" : "whatever", - - "emptyListContainer" : { - "emptyList" : [] + "isSingleton": "whatever", + "emptyListContainer": { + "emptyList": [] }, - - "nullData" : null + "nullData": null }, - // Rules // MANY // if array, then done @@ -39,53 +37,39 @@ // if array, grab [0] if it exists // - "spec" : { - - "review" : { + "spec": { + "review": { // We want review.rating to be a single value, but in the input it is an Array // So pick the first item from the Array, and use that - "rating" : "ONE" + "rating": "ONE" }, - - "data" : { - "stats" : "ONE" + "data": { + "stats": "ONE" }, - - "isMap" : "ONE", - - "isSingleton" : "ONE", - - "emptyListContainer" : { - "emptyList" : "ONE" + "isMap": "ONE", + "isSingleton": "ONE", + "emptyListContainer": { + "emptyList": "ONE" }, - - "nullData" : "ONE" - + "nullData": "ONE" }, - - "expected" : { - - "review" : { - "rating" : 5 + "expected": { + "review": { + "rating": 5 }, - - "data" : { - "stats" : { - "viewed" : 10, - "clicks" : 20 + "data": { + "stats": { + "viewed": 10, + "clicks": 20 } }, - - "isMap" : { - "data" : "stuff" + "isMap": { + "data": "stuff" }, - - "isSingleton" : "whatever", - - "emptyListContainer" : { - "emptyList" : null + "isSingleton": "whatever", + "emptyListContainer": { + "emptyList": null }, - - "nullData" : null + "nullData": null } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/cardinality/scalarInputData.json b/jolt-core/src/test/resources/json/cardinality/scalarInputData.json new file mode 100644 index 00000000..0ff48d2e --- /dev/null +++ b/jolt-core/src/test/resources/json/cardinality/scalarInputData.json @@ -0,0 +1,14 @@ +{ + "input": { + "photos": "scalar" + }, + "spec": { + "photos": { + "url": "ONE", + "caption": "MANY" + } + }, + "expected": { + "photos": "scalar" + } +} \ No newline at end of file diff --git a/jolt-core/src/test/resources/json/cardinality/starRegexTestData.json b/jolt-core/src/test/resources/json/cardinality/starRegexTestData.json new file mode 100644 index 00000000..9daf2942 --- /dev/null +++ b/jolt-core/src/test/resources/json/cardinality/starRegexTestData.json @@ -0,0 +1,32 @@ +{ + "input": { + "rating-primary": [ + 5, + 4 + ], + "rating-quality-1": [ + 4, + 5 + ], + "rating-quality-2": [ + 5, + 4 + ], + "rating-multi": 3 + }, + "spec": { + "rating-*-*": "ONE", + "rating-multi": "MANY" + }, + "expected": { + "rating-primary": [ + 5, + 4 + ], + "rating-quality-1": 4, + "rating-quality-2": 5, + "rating-multi": [ + 3 + ] + } +} diff --git a/jolt-core/src/test/resources/json/cardinality/starTestData.json b/jolt-core/src/test/resources/json/cardinality/starTestData.json index ee759941..a7c74f18 100644 --- a/jolt-core/src/test/resources/json/cardinality/starTestData.json +++ b/jolt-core/src/test/resources/json/cardinality/starTestData.json @@ -1,21 +1,25 @@ { - "input" : { - "rating-primary" : [ 5, 4 ], - "rating-quality" : [ 4, 5 ], - - "rating-multi" : 3 + "input": { + "rating-primary": [ + 5, + 4 + ], + "rating-quality": [ + 4, + 5 + ], + "rating-multi": 3 }, - - "spec" : { - "rating-*" : "ONE", - - "rating-multi" : "MANY" // this is more specific than the "rating-*" so it should "win" + "spec": { + "rating-*": "ONE", + "rating-multi": "MANY" + // this is more specific than the "rating-*" so it should "win" }, - - "expected" : { - "rating-primary" : 5, - "rating-quality" : 4, - - "rating-multi" : [ 3 ] + "expected": { + "rating-primary": 5, + "rating-quality": 4, + "rating-multi": [ + 3 + ] } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/cardinality/thisLevelIsNull.json b/jolt-core/src/test/resources/json/cardinality/thisLevelIsNull.json new file mode 100644 index 00000000..9bafa55d --- /dev/null +++ b/jolt-core/src/test/resources/json/cardinality/thisLevelIsNull.json @@ -0,0 +1,26 @@ +{ + "input": { + "photos": { + "url": [ + "http://pants.com/123-normal.jpg", + "http://pants.com/123-thumbnail.jpg" + ], + "caption": "Nice pants" + } + }, + "spec": { + "photos-*": { + "url": "ONE", + "caption": "MANY" + } + }, + "expected": { + "photos": { + "url": [ + "http://pants.com/123-normal.jpg", + "http://pants.com/123-thumbnail.jpg" + ], + "caption": "Nice pants" + } + } +} diff --git a/jolt-core/src/test/resources/json/chainr/context/spec_with_context.json b/jolt-core/src/test/resources/json/chainr/context/spec_with_context.json index d542a160..7fbb7baa 100644 --- a/jolt-core/src/test/resources/json/chainr/context/spec_with_context.json +++ b/jolt-core/src/test/resources/json/chainr/context/spec_with_context.json @@ -4,41 +4,40 @@ { "operation": "default", "spec": { - "B": "bb" // default in a silly "B" value + "B": "bb" + // default in a silly "B" value } }, { "operation": "shift", "spec": { - "a": "a", // pass the input "a" value thru - "B": "b" // "adjust" the silly capital B value + "a": "a", + // pass the input "a" value thru + "B": "b" + // "adjust" the silly capital B value } }, { - "operation": "com.bazaarvoice.jolt.chainr.transforms.GoodContextDrivenTransform" + "operation": "io.joltcommunity.jolt.chainr.transforms.GoodContextDrivenTransform" }, { - "operation": "com.bazaarvoice.jolt.chainr.transforms.GoodSpecAndContextDrivenTransform", - "spec" : { - "KEY_TO_ADD" : "d" + "operation": "io.joltcommunity.jolt.chainr.transforms.GoodSpecAndContextDrivenTransform", + "spec": { + "KEY_TO_ADD": "d" } } ], - - "tests" : [ + "tests": [ { - "testCaseName" : "cc & dd", - + "testCaseName": "cc & dd", // Input data for the unit test data. Will verify that this data passes all the way thru. "input": { - "a" : "aa" + "a": "aa" }, - - "context" : { - "test_context_key_1" : "cc", - "test_context_key_2" : "dd" + "context": { + "test_context_key_1": "cc", + "test_context_key_2": "dd" }, - "expected": { "a": "aa", "b": "bb", @@ -47,17 +46,15 @@ } }, { - "testCaseName" : "xx & yy", - + "testCaseName": "xx & yy", "input": { - "a" : "aa" + "a": "aa" }, - - "context" : { - "test_context_key_1" : "xx", // verify the context is being used - "test_context_key_2" : "yy" + "context": { + "test_context_key_1": "xx", + // verify the context is being used + "test_context_key_2": "yy" }, - "expected": { "a": "aa", "b": "bb", diff --git a/jolt-core/src/test/resources/json/chainr/increments/0-1.json b/jolt-core/src/test/resources/json/chainr/increments/0-1.json index 0de9bf0a..b1987777 100644 --- a/jolt-core/src/test/resources/json/chainr/increments/0-1.json +++ b/jolt-core/src/test/resources/json/chainr/increments/0-1.json @@ -1,3 +1,3 @@ { "0": "0" -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/chainr/increments/0-3.json b/jolt-core/src/test/resources/json/chainr/increments/0-3.json index 4ce99007..b9a52e3d 100644 --- a/jolt-core/src/test/resources/json/chainr/increments/0-3.json +++ b/jolt-core/src/test/resources/json/chainr/increments/0-3.json @@ -2,4 +2,4 @@ "0": "0", "1": "1", "2": "2" -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/chainr/increments/1-3.json b/jolt-core/src/test/resources/json/chainr/increments/1-3.json index 6c0c3a6f..1cbca498 100644 --- a/jolt-core/src/test/resources/json/chainr/increments/1-3.json +++ b/jolt-core/src/test/resources/json/chainr/increments/1-3.json @@ -1,4 +1,4 @@ { "1": "1", "2": "2" -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/chainr/increments/1-4.json b/jolt-core/src/test/resources/json/chainr/increments/1-4.json index b29b32c6..40b74a32 100644 --- a/jolt-core/src/test/resources/json/chainr/increments/1-4.json +++ b/jolt-core/src/test/resources/json/chainr/increments/1-4.json @@ -2,4 +2,4 @@ "1": "1", "2": "2", "3": "3" -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/chainr/increments/spec.json b/jolt-core/src/test/resources/json/chainr/increments/spec.json index 9c242e04..043683e8 100644 --- a/jolt-core/src/test/resources/json/chainr/increments/spec.json +++ b/jolt-core/src/test/resources/json/chainr/increments/spec.json @@ -23,4 +23,4 @@ "3": "3" } } -] \ No newline at end of file +] diff --git a/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json index f31d3d35..b7412ecf 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json +++ b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter1.json @@ -18,7 +18,6 @@ } ] }, - "spec": [ { "operation": "shift", @@ -26,27 +25,26 @@ "entities": { // The "*" matches each Map pair of "type" and "data". // We then write the Map pair, to the value of the "type" key, in a forced array - "*" : "@type[]" + "*": "@type[]" } } } ], - "expected": { - "alpha":[ + "alpha": [ { - "type":"alpha", - "data":"foo" + "type": "alpha", + "data": "foo" }, { - "type":"alpha", - "data":"baz" + "type": "alpha", + "data": "baz" } ], - "beta":[ + "beta": [ { - "type":"beta", - "data":"bar" + "type": "beta", + "data": "bar" } ] } diff --git a/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json index 9a93425b..9ae1458b 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json +++ b/jolt-core/src/test/resources/json/chainr/integration/andrewkcarter2.json @@ -25,12 +25,10 @@ } ] }, - "spec": [ { "operation": "shift", "spec": { - "books": { "*": { "availability": { @@ -45,19 +43,18 @@ } } ], - "expected": { - "PaperBooks":[ + "PaperBooks": [ { - "title":"bar", - "availability":[ + "title": "bar", + "availability": [ "online", "paperback" ] }, { - "title":"baz", - "availability":[ + "title": "baz", + "availability": [ "paperback" ] } diff --git a/jolt-core/src/test/resources/json/chainr/integration/firstSample.json b/jolt-core/src/test/resources/json/chainr/integration/firstSample.json index 4bfc5f6c..c3a9cec9 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/firstSample.json +++ b/jolt-core/src/test/resources/json/chainr/integration/firstSample.json @@ -16,7 +16,6 @@ } } }, - // The test uses this Chainr spec, on the input data to produce the "expected" JSON below "spec": [ { @@ -59,7 +58,6 @@ "operation": "sort" } ], - // The expected result "expected": { "~a": "aa", diff --git a/jolt-core/src/test/resources/json/chainr/integration/ismith.json b/jolt-core/src/test/resources/json/chainr/integration/ismith.json index dca352b3..e88bc496 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/ismith.json +++ b/jolt-core/src/test/resources/json/chainr/integration/ismith.json @@ -13,34 +13,31 @@ "bar": "N" } }, - "spec": [ { // Add a boolean true and false to the document at a known location - "operation" : "default", - "spec" : - { - "ref" : { - "True" : true, - "False" : false + "operation": "default", + "spec": { + "ref": { + "True": true, + "False": false } } }, { - "operation" : "shift", - "spec" : - { - "map" : { - "foo" : { - "N" : { + "operation": "shift", + "spec": { + "map": { + "foo": { + "N": { // If the value of Foo was "N" then // 1) Look up the tree 4 levels, then // 2) Walk back down the tree to "ref.False" // 3) Grab the value at "ref.False" and put it in the output with key "output-key" - "@(4,ref.False)" : "output-key" + "@(4,ref.False)": "output-key" }, - "Y" : { - "@(4,ref.True)" : "output-key" + "Y": { + "@(4,ref.True)": "output-key" } } } @@ -48,16 +45,14 @@ }, { // Cleanup the silly boolean references added in the first step - "operation" : "remove", - "spec" : - { - "ref" : "" + "operation": "remove", + "spec": { + "ref": "" } } ], - // The expected result "expected": { - "output-key" : true + "output-key": true } } diff --git a/jolt-core/src/test/resources/json/chainr/integration/ritwickgupta.json b/jolt-core/src/test/resources/json/chainr/integration/ritwickgupta.json index eca5c7bf..1582241d 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/ritwickgupta.json +++ b/jolt-core/src/test/resources/json/chainr/integration/ritwickgupta.json @@ -58,7 +58,6 @@ } ] }, - // The test uses this Chainr spec, on the input data to produce the "expected" JSON below "spec": [ { @@ -75,7 +74,6 @@ } } ], - // The expected result "expected": { "phone_num": "6503301096", diff --git a/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json b/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json index 8921ec9d..a11c5071 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json +++ b/jolt-core/src/test/resources/json/chainr/integration/wolfermann1.json @@ -16,7 +16,6 @@ } } }, - "spec": [ { "operation": "shift", @@ -36,7 +35,6 @@ } } ], - "expected": { "level1": { "L1Attribute": "6643287c-4800-49dd-b5cb-e0cf3ea637a9", diff --git a/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json b/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json index 687367e0..c90ff5b5 100644 --- a/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json +++ b/jolt-core/src/test/resources/json/chainr/integration/wolfermann2.json @@ -31,7 +31,6 @@ ] } }, - "spec": [ { "operation": "shift", @@ -62,7 +61,6 @@ } } ], - "expected": { "Data2": { "Data": [ diff --git a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_ClassName.json b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_ClassName.json index b0985f35..a22a6c67 100644 --- a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_ClassName.json +++ b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_ClassName.json @@ -1,5 +1,5 @@ [ { - "operation" : "com.bazaarvoice.jolt.ThisShouldNeverResolveToAClass" + "operation": "io.joltcommunity.jolt.ThisShouldNeverResolveToAClass" } ] diff --git a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_MissingSpec.json b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_MissingSpec.json index aab69f49..f181af67 100644 --- a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_MissingSpec.json +++ b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_MissingSpec.json @@ -1,5 +1,5 @@ [ { - "operation" : "com.bazaarvoice.jolt.chainr.transforms.GoodTestTransform" + "operation": "io.joltcommunity.jolt.chainr.transforms.GoodTestTransform" } ] diff --git a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_NonTransformClass.json b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_NonTransformClass.json index ea3c89c8..715f382b 100644 --- a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_NonTransformClass.json +++ b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_NonTransformClass.json @@ -1,5 +1,5 @@ [ { - "operation" : "java.lang.String" + "operation": "java.lang.String" } ] diff --git a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_arrayClassName.json b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_arrayClassName.json index 7a70759f..4b1152c6 100644 --- a/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_arrayClassName.json +++ b/jolt-core/src/test/resources/json/chainr/specformat/bad_spec_arrayClassName.json @@ -1,5 +1,5 @@ [ { - "operation" : [] + "operation": [] } ] diff --git a/jolt-core/src/test/resources/json/chainr/specloading/bad_spec_SpecTransform.json b/jolt-core/src/test/resources/json/chainr/specloading/bad_spec_SpecTransform.json index 0a16de09..dff54125 100644 --- a/jolt-core/src/test/resources/json/chainr/specloading/bad_spec_SpecTransform.json +++ b/jolt-core/src/test/resources/json/chainr/specloading/bad_spec_SpecTransform.json @@ -1,8 +1,8 @@ [ { - "operation" : "com.bazaarvoice.jolt.chainr.transforms.BadSpecTransform", - "spec" : { - "valid_spec" : "but transform is bad, as it does not have a single arg constructor" + "operation": "io.joltcommunity.jolt.chainr.transforms.BadSpecTransform", + "spec": { + "valid_spec": "but transform is bad, as it does not have a single arg constructor" } } -] \ No newline at end of file +] diff --git a/jolt-core/src/test/resources/json/chainr/transforms/bad_transform_loadsExplodingTransform.json b/jolt-core/src/test/resources/json/chainr/transforms/bad_transform_loadsExplodingTransform.json index 6db5a0da..89dbf66e 100644 --- a/jolt-core/src/test/resources/json/chainr/transforms/bad_transform_loadsExplodingTransform.json +++ b/jolt-core/src/test/resources/json/chainr/transforms/bad_transform_loadsExplodingTransform.json @@ -1,5 +1,5 @@ [ { - "operation" : "com.bazaarvoice.jolt.chainr.transforms.ExplodingTestTransform" + "operation": "io.joltcommunity.jolt.chainr.transforms.ExplodingTestTransform" } ] diff --git a/jolt-core/src/test/resources/json/chainr/transforms/loadsGoodTransform.json b/jolt-core/src/test/resources/json/chainr/transforms/loadsGoodTransform.json index 66d679f0..cb28d95a 100644 --- a/jolt-core/src/test/resources/json/chainr/transforms/loadsGoodTransform.json +++ b/jolt-core/src/test/resources/json/chainr/transforms/loadsGoodTransform.json @@ -1,8 +1,8 @@ [ { - "operation" : "com.bazaarvoice.jolt.chainr.transforms.GoodTestTransform", - "spec" : { - "a" : "b" + "operation": "io.joltcommunity.jolt.chainr.transforms.GoodTestTransform", + "spec": { + "a": "b" } } ] diff --git a/jolt-core/src/test/resources/json/deepcopy/modifed.json b/jolt-core/src/test/resources/json/deepcopy/modifed.json index f914fe96..0662a3f4 100644 --- a/jolt-core/src/test/resources/json/deepcopy/modifed.json +++ b/jolt-core/src/test/resources/json/deepcopy/modifed.json @@ -1,8 +1,12 @@ { - "array" : [ "a", 3, "c" ], - "map" : { - "a" : "a", - "b" : 3, - "c" : "c" + "array": [ + "a", + 3, + "c" + ], + "map": { + "a": "a", + "b": 3, + "c": "c" } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/deepcopy/original.json b/jolt-core/src/test/resources/json/deepcopy/original.json index b99e2121..2777106f 100644 --- a/jolt-core/src/test/resources/json/deepcopy/original.json +++ b/jolt-core/src/test/resources/json/deepcopy/original.json @@ -1,7 +1,10 @@ { - "array" : [ "a", 2 ], - "map" : { - "a" : "a", - "b" : 2 + "array": [ + "a", + 2 + ], + "map": { + "a": "a", + "b": 2 } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/defaultr/__deepCopyTest.json b/jolt-core/src/test/resources/json/defaultr/__deepCopyTest.json index 83896028..d5dafc3d 100644 --- a/jolt-core/src/test/resources/json/defaultr/__deepCopyTest.json +++ b/jolt-core/src/test/resources/json/defaultr/__deepCopyTest.json @@ -1,12 +1,10 @@ { "input": { }, - "spec": { "array": [], "map": {} }, - "expected": { "array": [], "map": {} diff --git a/jolt-core/src/test/resources/json/defaultr/arrayMismatch1.json b/jolt-core/src/test/resources/json/defaultr/arrayMismatch1.json index 37de7050..0551596b 100644 --- a/jolt-core/src/test/resources/json/defaultr/arrayMismatch1.json +++ b/jolt-core/src/test/resources/json/defaultr/arrayMismatch1.json @@ -9,7 +9,6 @@ } ] }, - "spec": { "photos": { "*": { @@ -22,7 +21,6 @@ } } }, - "expected": { "photos": [ { diff --git a/jolt-core/src/test/resources/json/defaultr/arrayMismatch2.json b/jolt-core/src/test/resources/json/defaultr/arrayMismatch2.json index 7bbfb0aa..c697a24b 100644 --- a/jolt-core/src/test/resources/json/defaultr/arrayMismatch2.json +++ b/jolt-core/src/test/resources/json/defaultr/arrayMismatch2.json @@ -2,7 +2,6 @@ "input": { "photos": {} }, - "spec": { "photos[]": { "*": { @@ -15,7 +14,6 @@ } } }, - "expected": { "photos": {} } diff --git a/jolt-core/src/test/resources/json/defaultr/defaultNulls.json b/jolt-core/src/test/resources/json/defaultr/defaultNulls.json index a293c128..66cf743f 100644 --- a/jolt-core/src/test/resources/json/defaultr/defaultNulls.json +++ b/jolt-core/src/test/resources/json/defaultr/defaultNulls.json @@ -1,11 +1,9 @@ { "input": { }, - "spec": { "foo": null }, - "expected": { "foo": null } diff --git a/jolt-core/src/test/resources/json/defaultr/expansionOnly.json b/jolt-core/src/test/resources/json/defaultr/expansionOnly.json index 6b1da666..8646f628 100644 --- a/jolt-core/src/test/resources/json/defaultr/expansionOnly.json +++ b/jolt-core/src/test/resources/json/defaultr/expansionOnly.json @@ -1,7 +1,6 @@ { "input": { }, - "spec": { "Rating": 3, "RatingRange": 5, @@ -26,7 +25,6 @@ "Range": 3, "Value": 2 }, - "quality|value": { "Range": 7, "MaxLabel": "Great", @@ -43,7 +41,6 @@ } } }, - "expected": { "Rating": 3, "RatingRange": 5, diff --git a/jolt-core/src/test/resources/json/defaultr/firstSample.json b/jolt-core/src/test/resources/json/defaultr/firstSample.json index 876753e6..59758deb 100644 --- a/jolt-core/src/test/resources/json/defaultr/firstSample.json +++ b/jolt-core/src/test/resources/json/defaultr/firstSample.json @@ -13,7 +13,6 @@ } } }, - "spec": { "RatingRange": 5, "SecondaryRatings": { @@ -34,7 +33,6 @@ } } }, - "expected": { "Rating": 3, "RatingRange": 5, diff --git a/jolt-core/src/test/resources/json/defaultr/identity.json b/jolt-core/src/test/resources/json/defaultr/identity.json index fd39d457..9eb015ec 100644 --- a/jolt-core/src/test/resources/json/defaultr/identity.json +++ b/jolt-core/src/test/resources/json/defaultr/identity.json @@ -11,10 +11,8 @@ } } }, - "spec": { }, - "expected": { "rating": { "primary": { diff --git a/jolt-core/src/test/resources/json/defaultr/nestedArrays1.json b/jolt-core/src/test/resources/json/defaultr/nestedArrays1.json index addaae83..9e7c9cf0 100644 --- a/jolt-core/src/test/resources/json/defaultr/nestedArrays1.json +++ b/jolt-core/src/test/resources/json/defaultr/nestedArrays1.json @@ -1,43 +1,41 @@ { - "input" : + "input": [ [ - [ - { - "order" : { - "id" : "OD1" - } - }, - { - "order" : { - "id" : "OD2" - } + { + "order": { + "id": "OD1" } - ] - ], - "spec" : { - "*[]" : { - "*" : { - "order" : { - "url" : "http://default.com" + }, + { + "order": { + "id": "OD2" + } + } + ] + ], + "spec": { + "*[]": { + "*": { + "order": { + "url": "http://default.com" } } } }, - "expected" : + "expected": [ [ - [ - { - "order" : { - "id" : "OD1", - "url" : "http://default.com" - } - }, - { - "order" : { - "id" : "OD2", - "url" : "http://default.com" - } + { + "order": { + "id": "OD1", + "url": "http://default.com" } - ] + }, + { + "order": { + "id": "OD2", + "url": "http://default.com" + } + } ] + ] } diff --git a/jolt-core/src/test/resources/json/defaultr/nestedArrays2.json b/jolt-core/src/test/resources/json/defaultr/nestedArrays2.json index 4e2b9c03..e8e9f039 100644 --- a/jolt-core/src/test/resources/json/defaultr/nestedArrays2.json +++ b/jolt-core/src/test/resources/json/defaultr/nestedArrays2.json @@ -1,45 +1,44 @@ { - "input" : { - "pants" : + "input": { + "pants": [ [ - [ - { - "order" : { - "id" : "OD1" - } - }, - { - "order" : { - "id" : "OD2" - } + { + "order": { + "id": "OD1" + } + }, + { + "order": { + "id": "OD2" } - ] + } ] + ] }, - "spec" : { - "pants[]" : { - "*[]" : { - "*" : { - "order" : { - "url" : "http://default.com" + "spec": { + "pants[]": { + "*[]": { + "*": { + "order": { + "url": "http://default.com" } } } } }, - "expected" : { - "pants" : [ + "expected": { + "pants": [ [ { - "order" : { - "id" : "OD1", - "url" : "http://default.com" + "order": { + "id": "OD1", + "url": "http://default.com" } }, { - "order" : { - "id" : "OD2", - "url" : "http://default.com" + "order": { + "id": "OD2", + "url": "http://default.com" } } ] diff --git a/jolt-core/src/test/resources/json/defaultr/orOrdering.json b/jolt-core/src/test/resources/json/defaultr/orOrdering.json index 760a817e..4ce5cb77 100644 --- a/jolt-core/src/test/resources/json/defaultr/orOrdering.json +++ b/jolt-core/src/test/resources/json/defaultr/orOrdering.json @@ -3,15 +3,12 @@ "foo": null, "bar": null }, - "spec": { "foo|z": "loses due to z being after x alphabetically", "foo|x": "foo|x wins", - "x|bar|y": "loses", "z|bar": "z|bar wins cause it is more specific" }, - "expected": { "foo": "foo|x wins", "bar": "z|bar wins cause it is more specific" diff --git a/jolt-core/src/test/resources/json/defaultr/photosArray.json b/jolt-core/src/test/resources/json/defaultr/photosArray.json index 42d8a5da..19d0f3f2 100644 --- a/jolt-core/src/test/resources/json/defaultr/photosArray.json +++ b/jolt-core/src/test/resources/json/defaultr/photosArray.json @@ -10,7 +10,6 @@ } ] }, - "spec": { "photos[]": { "*": { @@ -26,7 +25,6 @@ } } }, - "expected": { "photos": [ { @@ -47,5 +45,4 @@ } ] } - } diff --git a/jolt-core/src/test/resources/json/defaultr/starsOfStars.json b/jolt-core/src/test/resources/json/defaultr/starsOfStars.json index 2d3f0959..4d22278a 100644 --- a/jolt-core/src/test/resources/json/defaultr/starsOfStars.json +++ b/jolt-core/src/test/resources/json/defaultr/starsOfStars.json @@ -11,29 +11,50 @@ } } }, - "spec": { "*": { "*": { "label[]": { "1": "defaultLabel", - "2": [ 1, "tuna", 3, "marlin" ] + "2": [ + 1, + "tuna", + 3, + "marlin" + ] } } } }, - "expected": { "rating": { "primary": { "value": 3, "max": 5, - "label": [ null, "defaultLabel", [ 1, "tuna", 3, "marlin" ] ] + "label": [ + null, + "defaultLabel", + [ + 1, + "tuna", + 3, + "marlin" + ] + ] }, "quality": { "value": 3, "max": 7, - "label": [ null, "defaultLabel", [ 1, "tuna", 3, "marlin" ] ] + "label": [ + null, + "defaultLabel", + [ + 1, + "tuna", + 3, + "marlin" + ] + ] } } } diff --git a/jolt-core/src/test/resources/json/defaultr/topLevelIsArray.json b/jolt-core/src/test/resources/json/defaultr/topLevelIsArray.json index 49436f07..54af2e03 100644 --- a/jolt-core/src/test/resources/json/defaultr/topLevelIsArray.json +++ b/jolt-core/src/test/resources/json/defaultr/topLevelIsArray.json @@ -8,7 +8,6 @@ "caption": "Review all the things." } ], - "spec": { "*": { "url": "http://default.com", @@ -19,7 +18,6 @@ "caption": "The Best" } }, - "expected": [ { "url": "http://slashdot.org", diff --git a/jolt-core/src/test/resources/json/enrich/arrayIndexSync.json b/jolt-core/src/test/resources/json/enrich/arrayIndexSync.json new file mode 100644 index 00000000..061681d7 --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/arrayIndexSync.json @@ -0,0 +1,39 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "sync", + "enrichments": [ + { + "path": "customers.[1].id", + "outputPath": "customers.[1].profile", + "className": "io.joltcommunity.jolt.enrich.EnrichrTestHelper", + "method": "uppercase" + } + ] + } + } + ], + "input": { + "customers": [ + { + "id": "cust-1" + }, + { + "id": "cust-2" + } + ] + }, + "expected": { + "customers": [ + { + "id": "cust-1" + }, + { + "id": "cust-2", + "profile": "CUST-2" + } + ] + } +} diff --git a/jolt-core/src/test/resources/json/enrich/arrayWildcardAppendSync.json b/jolt-core/src/test/resources/json/enrich/arrayWildcardAppendSync.json new file mode 100644 index 00000000..d92ca081 --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/arrayWildcardAppendSync.json @@ -0,0 +1,42 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "sync", + "enrichments": [ + { + "path": "customers.[*].id", + "outputPath": "profiles.[]", + "className": "io.joltcommunity.jolt.enrich.EnrichrTestHelper", + "method": "uppercase" + } + ] + } + } + ], + "input": { + "customers": [ + { + "id": "cust-1" + }, + { + "id": "cust-2" + } + ] + }, + "expected": { + "customers": [ + { + "id": "cust-1" + }, + { + "id": "cust-2" + } + ], + "profiles": [ + "CUST-1", + "CUST-2" + ] + } +} diff --git a/jolt-core/src/test/resources/json/enrich/arrayWildcardSync.json b/jolt-core/src/test/resources/json/enrich/arrayWildcardSync.json new file mode 100644 index 00000000..8b8e0dda --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/arrayWildcardSync.json @@ -0,0 +1,40 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "sync", + "enrichments": [ + { + "path": "customers.[*].id", + "outputPath": "customers.[*].profile", + "className": "io.joltcommunity.jolt.enrich.EnrichrTestHelper", + "method": "uppercase" + } + ] + } + } + ], + "input": { + "customers": [ + { + "id": "cust-1" + }, + { + "id": "cust-2" + } + ] + }, + "expected": { + "customers": [ + { + "id": "cust-1", + "profile": "CUST-1" + }, + { + "id": "cust-2", + "profile": "CUST-2" + } + ] + } +} diff --git a/jolt-core/src/test/resources/json/enrich/asyncPublisher.json b/jolt-core/src/test/resources/json/enrich/asyncPublisher.json new file mode 100644 index 00000000..ca1d9a96 --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/asyncPublisher.json @@ -0,0 +1,36 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "async", + "enrichments": [ + { + "path": "customer.id", + "outputPath": "customer.profile", + "className": "io.joltcommunity.jolt.enrich.EnrichrTestHelper", + "method": "publisherDescribe" + } + ] + } + } + ], + "input": { + "customer": { + "id": "cust-123" + } + }, + "context": { + "tenant": "acme" + }, + "expected": { + "customer": { + "id": "cust-123", + "profile": { + "original": "cust-123", + "inputType": "LinkedHashMap", + "tenant": "acme" + } + } + } +} diff --git a/jolt-core/src/test/resources/json/enrich/classNameSync.json b/jolt-core/src/test/resources/json/enrich/classNameSync.json new file mode 100644 index 00000000..6bb290c4 --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/classNameSync.json @@ -0,0 +1,23 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "sync", + "enrichments": [ + { + "path": "name", + "className": "io.joltcommunity.jolt.enrich.EnrichrTestHelper", + "method": "uppercase" + } + ] + } + } + ], + "input": { + "name": "alice" + }, + "expected": { + "name": "ALICE" + } +} diff --git a/jolt-core/src/test/resources/json/enrich/contextKeySync.json b/jolt-core/src/test/resources/json/enrich/contextKeySync.json new file mode 100644 index 00000000..90867d9d --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/contextKeySync.json @@ -0,0 +1,39 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "sync", + "enrichments": [ + { + "path": "customer.id", + "outputPath": "customer.profile", + "contextKey": "lookupBean", + "method": "describeViaBean" + } + ] + } + } + ], + "input": { + "customer": { + "id": "cust-123" + } + }, + "context": { + "tenant": "acme" + }, + "helperContextKeys": [ + "lookupBean" + ], + "expected": { + "customer": { + "id": "cust-123", + "profile": { + "original": "cust-123", + "inputType": "LinkedHashMap", + "tenant": "acme" + } + } + } +} diff --git a/jolt-core/src/test/resources/json/enrich/externalApiArrayAsync.json b/jolt-core/src/test/resources/json/enrich/externalApiArrayAsync.json new file mode 100644 index 00000000..f7cf5741 --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/externalApiArrayAsync.json @@ -0,0 +1,53 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "async", + "enrichments": [ + { + "path": "customers.[*].id", + "outputPath": "customers.[*].profile", + "contextKey": "customerLookupClient", + "method": "lookupProfile" + } + ] + } + } + ], + "input": { + "customers": [ + { + "id": "cust-101" + }, + { + "id": "cust-202" + } + ] + }, + "context": { + "tenant": "acme" + }, + "expected": { + "customers": [ + { + "id": "cust-101", + "profile": { + "customerId": "cust-101", + "tenant": "acme", + "segment": "gold", + "source": "external-api" + } + }, + { + "id": "cust-202", + "profile": { + "customerId": "cust-202", + "tenant": "acme", + "segment": "gold", + "source": "external-api" + } + } + ] + } +} diff --git a/jolt-core/src/test/resources/json/enrich/externalApiAsync.json b/jolt-core/src/test/resources/json/enrich/externalApiAsync.json new file mode 100644 index 00000000..84115f2b --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/externalApiAsync.json @@ -0,0 +1,37 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "async", + "enrichments": [ + { + "path": "customer.id", + "outputPath": "customer.profile", + "contextKey": "customerLookupClient", + "method": "lookupProfile" + } + ] + } + } + ], + "input": { + "customer": { + "id": "cust-123" + } + }, + "context": { + "tenant": "acme" + }, + "expected": { + "customer": { + "id": "cust-123", + "profile": { + "customerId": "cust-123", + "tenant": "acme", + "segment": "gold", + "source": "external-api" + } + } + } +} diff --git a/jolt-core/src/test/resources/json/enrich/nestedArrayWildcardAsync.json b/jolt-core/src/test/resources/json/enrich/nestedArrayWildcardAsync.json new file mode 100644 index 00000000..ae371529 --- /dev/null +++ b/jolt-core/src/test/resources/json/enrich/nestedArrayWildcardAsync.json @@ -0,0 +1,63 @@ +{ + "spec": [ + { + "operation": "enrich", + "spec": { + "executionMode": "async", + "enrichments": [ + { + "path": "orders.[*].items.[*].sku", + "outputPath": "orders.[*].items.[*].inventoryCode", + "className": "io.joltcommunity.jolt.enrich.EnrichrTestHelper", + "method": "asyncUppercase" + } + ] + } + } + ], + "input": { + "orders": [ + { + "items": [ + { + "sku": "sku-1" + }, + { + "sku": "sku-2" + } + ] + }, + { + "items": [ + { + "sku": "sku-3" + } + ] + } + ] + }, + "expected": { + "orders": [ + { + "items": [ + { + "sku": "sku-1", + "inventoryCode": "SKU-1" + }, + { + "sku": "sku-2", + "inventoryCode": "SKU-2" + } + ] + }, + { + "items": [ + { + "sku": "sku-3", + "inventoryCode": "SKU-3" + } + ] + } + ] + } +} diff --git a/jolt-core/src/test/resources/json/modifier/arrayElementAt.json b/jolt-core/src/test/resources/json/modifier/arrayElementAt.json index f4b79ed2..429112fe 100644 --- a/jolt-core/src/test/resources/json/modifier/arrayElementAt.json +++ b/jolt-core/src/test/resources/json/modifier/arrayElementAt.json @@ -1,38 +1,54 @@ { "input": { "simpleArray": [ - 0, // 0 - null, // 1 - "pants", // 2 - 3 // 3 + 0, + // 0 + null, + // 1 + "pants", + // 2 + 3 + // 3 ] }, - "spec": { - "element0" : "=elementAt(0,@(1,simpleArray))", - "element1" : "=elementAt(1,@(1,simpleArray))", - "element2" : "=elementAt(2,@(1,simpleArray))" + "element0": "=elementAt(0,@(1,simpleArray))", + "element1": "=elementAt(1,@(1,simpleArray))", + "element2": "=elementAt(2,@(1,simpleArray))" }, - "context": { }, - "OVERWRITR": { - "simpleArray": [0, null, "pants", 3], - "element0" : 0, - "element1" : null, - "element2" : "pants" + "simpleArray": [ + 0, + null, + "pants", + 3 + ], + "element0": 0, + "element1": null, + "element2": "pants" }, "DEFAULTR": { - "simpleArray": [0, null, "pants", 3], - "element0" : 0, - "element1" : null, - "element2" : "pants" + "simpleArray": [ + 0, + null, + "pants", + 3 + ], + "element0": 0, + "element1": null, + "element2": "pants" }, "DEFINR": { - "simpleArray": [0, null, "pants", 3], - "element0" : 0, - "element1" : null, - "element2" : "pants" + "simpleArray": [ + 0, + null, + "pants", + 3 + ], + "element0": 0, + "element1": null, + "element2": "pants" } } diff --git a/jolt-core/src/test/resources/json/modifier/arrayLiteral.json b/jolt-core/src/test/resources/json/modifier/arrayLiteral.json index 87c8d713..5d2625d8 100644 --- a/jolt-core/src/test/resources/json/modifier/arrayLiteral.json +++ b/jolt-core/src/test/resources/json/modifier/arrayLiteral.json @@ -1,10 +1,14 @@ { "input": { "simpleArray": [ - 0,null,2,3,4,null + 0, + null, + 2, + 3, + 4, + null ] }, - "spec": { "simpleArray": { "*": 0, @@ -14,25 +18,61 @@ "[13]": null } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleArray": [ - 0,0,0,0,0,0,0,0,0,0,0,0,0,0 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ] }, - "DEFAULTR": { "simpleArray": [ - 0,0,2,3,4,0,0,1,0,5,0,5,0,0 + 0, + 0, + 2, + 3, + 4, + 0, + 0, + 1, + 0, + 5, + 0, + 5, + 0, + 0 ] }, "DEFINR": { "simpleArray": [ - 0,null,2,3,4,null,0,1,0,5,0,5,0,null + 0, + null, + 2, + 3, + 4, + null, + 0, + 1, + 0, + 5, + 0, + 5, + 0, + null ] } } diff --git a/jolt-core/src/test/resources/json/modifier/arrayLiteralWithEmptyInput.json b/jolt-core/src/test/resources/json/modifier/arrayLiteralWithEmptyInput.json index dfa37133..04c92feb 100644 --- a/jolt-core/src/test/resources/json/modifier/arrayLiteralWithEmptyInput.json +++ b/jolt-core/src/test/resources/json/modifier/arrayLiteralWithEmptyInput.json @@ -2,7 +2,6 @@ "input": { "simpleArray": [] }, - "spec": { "simpleArray": { "*": 0, @@ -12,25 +11,61 @@ "[13]": null } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleArray": [ - 0,0,0,0,0,0,0,0,0,0,0,0,0,0 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 ] }, - "DEFAULTR": { "simpleArray": [ - 0,0,0,0,0,0,0,1,0,5,0,5,0,0 + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 5, + 0, + 0 ] }, "DEFINR": { "simpleArray": [ - 0,0,0,0,0,0,0,1,0,5,0,5,0,null + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 5, + 0, + 5, + 0, + null ] } } diff --git a/jolt-core/src/test/resources/json/modifier/arrayLiteralWithMissingInput.json b/jolt-core/src/test/resources/json/modifier/arrayLiteralWithMissingInput.json index 421cacb8..83c1a25a 100644 --- a/jolt-core/src/test/resources/json/modifier/arrayLiteralWithMissingInput.json +++ b/jolt-core/src/test/resources/json/modifier/arrayLiteralWithMissingInput.json @@ -2,33 +2,40 @@ "input": { // missing simpleArray }, - "spec": { "simpleArray": { "*": 0, "[4]": 1 } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleArray": [ - 0,0,0,0,0 + 0, + 0, + 0, + 0, + 0 ] }, - "DEFAULTR": { "simpleArray": [ - 0,0,0,0,1 + 0, + 0, + 0, + 0, + 1 ] }, - "DEFINR": { "simpleArray": [ - 0,0,0,0,1 + 0, + 0, + 0, + 0, + 1 ] } } diff --git a/jolt-core/src/test/resources/json/modifier/arrayLiteralWithNullInput.json b/jolt-core/src/test/resources/json/modifier/arrayLiteralWithNullInput.json index 157d9805..6d748b31 100644 --- a/jolt-core/src/test/resources/json/modifier/arrayLiteralWithNullInput.json +++ b/jolt-core/src/test/resources/json/modifier/arrayLiteralWithNullInput.json @@ -2,28 +2,31 @@ "input": { "simpleArray": null }, - "spec": { "simpleArray": { "*": 0, "[4]": 1 } }, - "context": {}, - "OVERWRITR": { "simpleArray": [ - 0,0,0,0,0 + 0, + 0, + 0, + 0, + 0 ] }, - "DEFAULTR": { "simpleArray": [ - 0,0,0,0,1 + 0, + 0, + 0, + 0, + 1 ] }, - "DEFINR": { "simpleArray": null } diff --git a/jolt-core/src/test/resources/json/modifier/arrayObject.json b/jolt-core/src/test/resources/json/modifier/arrayObject.json index 5c03ae09..68fb6b92 100644 --- a/jolt-core/src/test/resources/json/modifier/arrayObject.json +++ b/jolt-core/src/test/resources/json/modifier/arrayObject.json @@ -1,18 +1,24 @@ { "input": { "objectMapFill": [ - {},{},{} + {}, + {}, + {} ], "objectArrayFill": [ - [],[],[] + [], + [], + [] ] }, - "spec": { "objectMapFill": { - "*": {"a": "b"}, - "[3]": {"x": "y"} - + "*": { + "a": "b" + }, + "[3]": { + "x": "y" + } }, "objectArrayFill": { "*": "=toList(0)", @@ -21,39 +27,100 @@ "emptyMap": {}, "emptyList": [] }, - "context": {}, - "OVERWRITR": { "objectMapFill": [ - {"a": "b"},{"a": "b"},{"a": "b"},{"a": "b", "x": "y"} + { + "a": "b" + }, + { + "a": "b" + }, + { + "a": "b" + }, + { + "a": "b", + "x": "y" + } ], "objectArrayFill": [ - [0],[0],[0],[0],[0] + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ], + [ + 0 + ] ], "emptyMap": {}, "emptyList": [] }, - "DEFAULTR": { - "objectMapFill" : [ - {"a": "b"},{"a": "b"},{"a": "b"},{"a": "b", "x": "y"} + "objectMapFill": [ + { + "a": "b" + }, + { + "a": "b" + }, + { + "a": "b" + }, + { + "a": "b", + "x": "y" + } ], - "objectArrayFill" : [ - [],[],[],[0],[4] + "objectArrayFill": [ + [], + [], + [], + [ + 0 + ], + [ + 4 + ] ], - "emptyMap" : { }, - "emptyList" : [] + "emptyMap": {}, + "emptyList": [] }, - "DEFINR": { - "objectMapFill" : [ - {"a": "b"},{"a": "b"},{"a": "b"},{"x": "y"} + "objectMapFill": [ + { + "a": "b" + }, + { + "a": "b" + }, + { + "a": "b" + }, + { + "x": "y" + } ], - "objectArrayFill" : [ - [],[],[],[0],[4] + "objectArrayFill": [ + [], + [], + [], + [ + 0 + ], + [ + 4 + ] ], - "emptyMap" : {}, - "emptyList" : [] + "emptyMap": {}, + "emptyList": [] } } diff --git a/jolt-core/src/test/resources/json/modifier/complexArrayLookup.json b/jolt-core/src/test/resources/json/modifier/complexArrayLookup.json index 71dc4147..ae3d5888 100644 --- a/jolt-core/src/test/resources/json/modifier/complexArrayLookup.json +++ b/jolt-core/src/test/resources/json/modifier/complexArrayLookup.json @@ -103,7 +103,8 @@ "DimensionLabel": "Gender", "Id": "Gender" } - }] + } + ] }, "context": { "cdvs": { diff --git a/jolt-core/src/test/resources/json/modifier/functions/arrayTests.json b/jolt-core/src/test/resources/json/modifier/functions/arrayTests.json index aa0a26a6..df34bf33 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/arrayTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/arrayTests.json @@ -7,7 +7,6 @@ "value": 2.0 } ], - "spec": { "*": { "max": "=max(@(2,[&1].value) , ^value , 0)", @@ -18,11 +17,9 @@ "long": "=toLong(@(2,[&1].value))" } }, - "context": { - "value" : -1.0 + "value": -1.0 }, - "OVERWRITR": [ { "value": 2, diff --git a/jolt-core/src/test/resources/json/modifier/functions/dateTests.json b/jolt-core/src/test/resources/json/modifier/functions/dateTests.json new file mode 100644 index 00000000..8c4ef37f --- /dev/null +++ b/jolt-core/src/test/resources/json/modifier/functions/dateTests.json @@ -0,0 +1,126 @@ +{ + "input": { + "epochMilli": 1609459200000, + "dateString1": "2021-01-01", + "dateString2": "2021-01-01 12:00:00", + "dateString3": "2000-01-01T12:30:45", + "dateForOps": "2000-01-01", + "dateTimeForOps": "2000-01-01 12:00:00", + "invalidDate": "not-a-date", + "invalidDuration": "invalid-duration", + "numberValue": 12345 + }, + "spec": { + "fromEpochMilli": { + "basic": "=fromEpochMilli(@(2,epochMilli))", + "withPattern": "=fromEpochMilli(@(2,epochMilli), 'yyyy-MM-dd', 'UTC')", + "withTimezone": "=fromEpochMilli(@(2,epochMilli), 'yyyy-MM-dd HH:mm:ss', 'America/New_York')", + "badArgs1": "=fromEpochMilli(@(2,invalidDate), 'yyyy-MM-dd', 'UTC')", + "badArgs2": "=fromEpochMilli(@(2,epochMilli), 123, 'UTC')", + "badArgs3": "=fromEpochMilli(@(2,epochMilli), 'yyyy-MM-dd', 456)", + "badArgs4": "=fromEpochMilli(@(2,epochMilli), 'INVALID-PATTERN', 'UTC')", + "badArgs5": "=fromEpochMilli(@(2,epochMilli), 'yyyy-MM-dd', 'Invalid/Timezone')" + }, + + "toEpochMilli": { + "basic": "=toEpochMilli(@(2,dateString1), 'yyyy-MM-dd', 'UTC')", + "withTime": "=toEpochMilli(@(2,dateString2), 'yyyy-MM-dd HH:mm:ss', 'UTC')", + "withTimeZone": "=toEpochMilli(@(2,dateString2), 'yyyy-MM-dd HH:mm:ss', 'Europe/Paris')", + "badArgs1": "=toEpochMilli(@(2,numberValue), 'yyyy-MM-dd', 'UTC')", + "badArgs2": "=toEpochMilli(@(2,invalidDate), 'yyyy-MM-dd', 'UTC')", + "badArgs3": "=toEpochMilli(@(2,dateString1), 123, 'UTC')", + "badArgs4": "=toEpochMilli(@(2,dateString1), 'yyyy-MM-dd', 456)", + "badArgs5": "=toEpochMilli(@(2,dateString1), 'INVALID-PATTERN', 'UTC')", + "badArgs6": "=toEpochMilli(@(2,dateString1), 'yyyy-MM-dd', 'Invalid/Timezone')" + }, + + "dateAdd": { + "addDays": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 'P1D', 'UTC')", + "addMonths": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 'P2M', 'UTC')", + "addYears": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 'P1Y', 'UTC')", + "addCombined": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 'P1Y2M3D', 'UTC')", + "addHours": "=dateAdd(@(2,dateTimeForOps), 'yyyy-MM-dd HH:mm:ss', 'PT2H', 'UTC')", + "badArgs1": "=dateAdd(@(2,numberValue), 'yyyy-MM-dd', 'P1D', 'UTC')", + "badArgs2": "=dateAdd(@(2,invalidDate), 'yyyy-MM-dd', 'P1D', 'UTC')", + "badArgs3": "=dateAdd(@(2,dateForOps), 123, 'P1D', 'UTC')", + "badArgs4": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 456, 'UTC')", + "badArgs5": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 'P1D', 789)", + "badArgs6": "=dateAdd(@(2,dateForOps), 'INVALID-PATTERN', 'P1D', 'UTC')", + "badArgs7": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', @(2,invalidDuration), 'UTC')", + "badArgs8": "=dateAdd(@(2,dateForOps), 'yyyy-MM-dd', 'P1D', 'Invalid/Timezone')" + }, + + "dateSubstract": { + "subtractDays": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', 'P1D', 'UTC')", + "subtractMonths": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', 'P2M', 'UTC')", + "subtractYears": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', 'P1Y', 'UTC')", + "subtractHours": "=dateSubstract(@(2,dateTimeForOps), 'yyyy-MM-dd HH:mm:ss', 'PT2H', 'UTC')", + "badArgs1": "=dateSubstract(@(2,numberValue), 'yyyy-MM-dd', 'P1D', 'UTC')", + "badArgs2": "=dateSubstract(@(2,invalidDate), 'yyyy-MM-dd', 'P1D', 'UTC')", + "badArgs3": "=dateSubstract(@(2,dateForOps), 123, 'P1D', 'UTC')", + "badArgs4": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', 456, 'UTC')", + "badArgs5": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', 'P1D', 789)", + "badArgs6": "=dateSubstract(@(2,dateForOps), 'INVALID-PATTERN', 'P1D', 'UTC')", + "badArgs7": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', @(2,invalidDuration), 'UTC')", + "badArgs8": "=dateSubstract(@(2,dateForOps), 'yyyy-MM-dd', 'P1D', 'Invalid/Timezone')" + }, + "formatDate": { + "formatDate": "=formatDate(@(2,dateForOps), 'yyyy-MM-dd', 'yyyy MM dd')", + "formatDateTime": "=formatDate(@(2,dateTimeForOps), yyyy-MM-dd HH:mm:ss, yyyy-MM-dd'T'HH:mm:ss'Z')", + "formatWithTimeZone": "=formatDate(@(2,dateTimeForOps), yyyy-MM-dd HH:mm:ss, yyyy-MM-dd'T'HH:mm:ssXXX, Europe/Paris)", + "formatFromTimeZoneToTimeZone": "=formatDate(@(2,dateTimeForOps), yyyy-MM-dd HH:mm:ss, yyyy-MM-dd'T'HH:mm:ss'Z',Europe/Paris, UTC)", + "badArgs1": "=formatDate(@(2,numberValue), 'yyyy-MM-dd', 'yyyy MM dd')", + "badArgs2": "=formatDate(@(2,invalidDate), 'yyyy-MM-dd', 'yyyy MM dd')", + "badArgs3": "=formatDate(@(2,dateForOps), 'yyyy-MM-dd', 456)", + "badArgs4": "=formatDate(@(2,dateForOps), 'yyyy-MM-dd', 'yyyy MM dd', 789)", + "badArgs5": "=formatDate(@(2,dateForOps), 'INVALID-PATTERN', 'yyyy MM dd')", + "badArgs6": "=formatDate(@(2,dateForOps), 'yyyy-MM-dd', 'INVALID-PATTERN')", + "badArgs7": "=formatDate(@(2,dateForOps), 'yyyy-MM-dd', 'yyyy MM dd', 'Invalid/Timezone')", + "badArgs8": "=formatDate(@(2,dateForOps), 'yyyy-MM-dd', 'yyyy MM dd', 'UTC', 'Invalid/Timezone')" + } + }, + "OVERWRITR": { + "epochMilli": 1609459200000, + "dateString1": "2021-01-01", + "dateString2": "2021-01-01 12:00:00", + "dateString3": "2000-01-01T12:30:45", + "dateForOps": "2000-01-01", + "dateTimeForOps": "2000-01-01 12:00:00", + "invalidDate": "not-a-date", + "invalidDuration": "invalid-duration", + "numberValue": 12345, + + "fromEpochMilli": { + "basic": "2021-01-01T00:00:00Z", + "withPattern": "2021-01-01", + "withTimezone": "2020-12-31 19:00:00" + }, + + "toEpochMilli": { + "basic": 1609459200000, + "withTime": 1609502400000, + "withTimeZone": 1609498800000 + }, + + "dateAdd": { + "addDays": "2000-01-02", + "addMonths": "2000-03-01", + "addYears": "2001-01-01", + "addCombined": "2001-03-04", + "addHours": "2000-01-01 14:00:00" + }, + + "dateSubstract": { + "subtractDays": "1999-12-31", + "subtractMonths": "1999-11-01", + "subtractYears": "1999-01-01", + "subtractHours": "2000-01-01 10:00:00" + }, + "formatDate": { + "formatDate": "2000 01 01", + "formatDateTime": "2000-01-01T12:00:00Z", + "formatWithTimeZone": "2000-01-01T12:00:00+01:00", + "formatFromTimeZoneToTimeZone": "2000-01-01T11:00:00Z" + } + } +} diff --git a/jolt-core/src/test/resources/json/modifier/functions/deleteDuplicatesTests.json b/jolt-core/src/test/resources/json/modifier/functions/deleteDuplicatesTests.json index e7c8d87f..4597a70d 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/deleteDuplicatesTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/deleteDuplicatesTests.json @@ -1,21 +1,51 @@ { - "input": { - "squashDuplicates1" : [ "abc", "abc", "xyz", "cde", "bcd" ], - "squashDuplicates2" : [ 1, 1, 2, 2, 3, 4 ], - "squashNoDuplicates" : [ "abc", "xyz", "cde", "bcd" ] - }, - - "spec": { - "squashDuplicates1" : "=squashDuplicates", - "squashDuplicates2" : "=squashDuplicates", - "squashNoDuplicates" : "=squashDuplicates" - }, - - "context": {}, - - "OVERWRITR": { - "squashDuplicates1" : [ "abc", "xyz", "cde", "bcd" ], - "squashDuplicates2" : [ 1, 2, 3, 4 ], - "squashNoDuplicates" : [ "abc", "xyz", "cde", "bcd" ] - } + "input": { + "squashDuplicates1": [ + "abc", + "abc", + "xyz", + "cde", + "bcd" + ], + "squashDuplicates2": [ + 1, + 1, + 2, + 2, + 3, + 4 + ], + "squashNoDuplicates": [ + "abc", + "xyz", + "cde", + "bcd" + ] + }, + "spec": { + "squashDuplicates1": "=squashDuplicates", + "squashDuplicates2": "=squashDuplicates", + "squashNoDuplicates": "=squashDuplicates" + }, + "context": {}, + "OVERWRITR": { + "squashDuplicates1": [ + "abc", + "xyz", + "cde", + "bcd" + ], + "squashDuplicates2": [ + 1, + 2, + 3, + 4 + ], + "squashNoDuplicates": [ + "abc", + "xyz", + "cde", + "bcd" + ] + } } diff --git a/jolt-core/src/test/resources/json/modifier/functions/labelsLookupTest.json b/jolt-core/src/test/resources/json/modifier/functions/labelsLookupTest.json index ef1485f9..07bce2c9 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/labelsLookupTest.json +++ b/jolt-core/src/test/resources/json/modifier/functions/labelsLookupTest.json @@ -4,7 +4,6 @@ "Leg4857": { "minLabel": null, "maxLabel": null - }, "EaseOfAssembly": {}, "Quality": {}, @@ -18,8 +17,7 @@ }, "Cleanliness": {} } - } - , + }, "spec": { "ratings": { "*": { @@ -28,11 +26,10 @@ } } }, - "context": { - "cdvs": { }, - "tags": { }, - "additionalFields": { }, + "cdvs": {}, + "tags": {}, + "additionalFields": {}, "ratings": { "Leg4857": { "dimensionLabel": "Leg", @@ -71,24 +68,23 @@ } } }, - "DEFAULTR": { - "ratings" : { - "Leg4857" : { - "minLabel" : "Too loose", - "maxLabel" : "Too tight" + "ratings": { + "Leg4857": { + "minLabel": "Too loose", + "maxLabel": "Too tight" }, - "EaseOfAssembly" : { }, - "Quality" : { }, - "Shrinkage69" : { - "minLabel" : "More shrinkage", - "maxLabel" : "Less shrinkage" + "EaseOfAssembly": {}, + "Quality": {}, + "Shrinkage69": { + "minLabel": "More shrinkage", + "maxLabel": "Less shrinkage" }, - "Panels2564" : { - "minLabel" : "Highest", - "maxLabel" : "Lowest" + "Panels2564": { + "minLabel": "Highest", + "maxLabel": "Lowest" }, - "Cleanliness" : { } + "Cleanliness": {} } } } diff --git a/jolt-core/src/test/resources/json/modifier/functions/mathTests.json b/jolt-core/src/test/resources/json/modifier/functions/mathTests.json index 33b988f7..aa5bfb60 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/mathTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/mathTests.json @@ -7,31 +7,61 @@ "value": -2.0 }, "data3": { - "value": [-1,2,3.0] + "value": [ + -1, + 2, + 3.0 + ] }, "data4": { - "value": [5, 2], // Divide 2 integers returning double - "test1": [5, 2.0], // Divide integer and double returns double - "test2": [5, 0], // Divide by 0 returns empty hence noop - "test3": [0, 5], // Divide 0 by any number returns 0 + "value": [ + 5, + 2 + ], + // Divide 2 integers returning double + "test1": [ + 5, + 2.0 + ], + // Divide integer and double returns double + "test2": [ + 5, + 0 + ], + // Divide by 0 returns empty hence noop + "test3": [ + 0, + 5 + ], + // Divide 0 by any number returns 0 "nr": 51, "dr": 13 }, "data5": { - "aInt" : 10, - "bInt" : 3, - "aDouble" : 10.0, - "bDouble" : 3.0 + "nr": 51, + "dr": 13 + }, + "data6": { + "aInt": 10, + "bInt": 3, + "aDouble": 10.0, + "bDouble": 3.0 + }, + "data7": { + "bigInt": 123456789012345678901234567890, + "bigDec": 123456789012345678901234567890.6, + "int": 10, + "double": 3.0 } }, - "spec": { "data1": { "max": "=max(@(1,value) , ^value,0)", "min": "=min(@(1,value) , ^value,0.0)", "double": "=toDouble(@(1,value))", "value": "=abs", - "intSum": "=intSum", // Noop. Sum is a list function. Single arg is being passed here. + "intSum": "=intSum", + // Noop. Sum is a list function. Single arg is being passed here. "doubleSum": "=doubleSum" }, "data2": { @@ -57,75 +87,133 @@ "test3": "=divide", "explicit1": "=divide(@(1,value))", "explicit2": "=divide(12,3)", - "div": "=divide(@(1,nr),@(1,dr))" , // Look up the numerator and denominator from the input - "roundedDiv": "=divideAndRound(4, @(1,nr),@(1,dr))", // Round the result to the 4 decimal points - // - "badArgs1" : "=divide(1,2,3)", // too many params - "badArgs2" : "=divide(1)" // not enough params + "div": "=divide(@(1,nr),@(1,dr))", + "badArgs1": "=divide(1,2,3)", + // too many params + "badArgs2": "=divide(1)" + // not enough params }, "data5": { - "happyInt": "=intSubtract(@(1,aInt),@(1,bInt))", + "roundedDiv": "=divideAndRound(4, @(1,nr),@(1,dr))", + "roundedDivDown": "=divideAndRound(4, 'DOWN', @(1,nr),@(1,dr))", + "badArgs1": "=divideAndRound(@(1,nr),@(1,dr))", + "badArgs2": "=divideAndRound('DOWN', @(1,nr),@(1,dr))", + "badArgs3": "=divideAndRound(4, 5, @(1,nr),@(1,dr))", + "badArgs4": "=divideAndRound(4, 'WE', @(1,nr),@(1,dr))" + }, + "data6": { + "happyInt": "=intSubtract(@(1,aInt),@(1,bInt))", "happyDouble": "=doubleSubtract(@(1,aDouble),@(1,bDouble))", // // Bad Args - "badArgsInt1": "=intSubtract(1)", // not enough args - "badArgsInt2": "=intSubtract(1,2,3)", // too many args - "badArgsDouble1": "=doubleSubtract(1)", // not enough args - "badArgsDouble2": "=doubleSubtract(1,2,3)" // too many args + "badArgsInt1": "=intSubtract(1)", + // not enough args + "badArgsInt2": "=intSubtract(1,2,3)", + // too many args + "badArgsDouble1": "=doubleSubtract(1)", + // not enough args + "badArgsDouble2": "=doubleSubtract(1,2,3)" + // too many args + }, + "data7": { + "bigIntMulBigInt": "=multiply(@(1,bigInt),@(1,bigInt))", + "bigIntMulBigDec": "=multiply(@(1,bigInt),@(1,bigDec))", + "bigIntMulInt": "=multiply(@(1,bigInt),@(1,int))", + "bigIntMulDouble": "=multiply(@(1,bigInt),@(1,double))", + "rounded": "=multiplyAndRound(4, 3.453, 3.33)", + "roundedDown": "=multiplyAndRound(4, 'DOWN', 3.453, 3.33)", + "badArgs1": "=multiplyAndRound(@(1,int),@(1,double))", + "badArgs2": "=multiplyAndRound('DOWN', @(1,int),@(1,double))", + "badArgs3": "=multiplyAndRound(4, 5, @(1,int),@(1,double))", + "badArgs4": "=multiplyAndRound(4, 'WE', @(1,int),@(1,double))" } }, - "context": { - "value" : 1.0 + "value": 1.0 }, - "OVERWRITR": { - - "data1" : { - "max" : 1.0, - "min" : -2, - "double" : -2.0, - "value" : 2 - }, - "data2" : { - "max" : 1.0, - "min" : -2.0, - "integer" : -2, - "value" : 2.0 - }, - "data3" : { - "min" : -1, - "max" : 3.0, - "int" : [ -1, 2, 3 ], - "double" : [ -1.0, 2.0, 3.0 ], - "abs" : [ 1, 2, 3.0 ], - "intSum" : 4, - "doubleSum" : 4.0, - "longSum" : 4, + "data1": { + "max": 1.0, + "min": -2, + "double": -2.0, + "value": 2 + }, + "data2": { + "max": 1.0, + "min": -2.0, + "integer": -2, + "value": 2.0 + }, + "data3": { + "min": -1, + "max": 3.0, + "int": [ + -1, + 2, + 3 + ], + "double": [ + -1.0, + 2.0, + 3.0 + ], + "abs": [ + 1, + 2, + 3.0 + ], + "intSum": 4, + "doubleSum": 4.0, + "longSum": 4, "value": 4 }, "data4": { "test1": 2.5, - "test2": [5, 0], + "test2": [ + 5, + 0 + ], "test3": 0.0, "div": 3.923076923076923, - "roundedDiv":3.9231, "explicit1": 2.5, "explicit2": 4.0, - "value": [5, 2], + "value": [ + 5, + 2 + ], "nr": 51, "dr": 13 }, "data5": { + "roundedDiv": 3.9231, + "roundedDivDown": 3.9230, + "nr": 51, + "dr": 13 + }, + "data6": { // og data - "aInt" : 10, - "bInt" : 3, - "aDouble" : 10.0, - "bDouble" : 3.0, + "aInt": 10, + "bInt": 3, + "aDouble": 10.0, + "bDouble": 3.0, // // computed "happyInt": 7, "happyDouble": 7.0 + }, + "data7": { + "bigInt": 123456789012345678901234567890, + "bigDec": 123456789012345678901234567890.6, + "int": 10, + "double": 3.0, + // + // computed + "rounded" : 11.4985, + "roundedDown" : 11.4984, + "bigIntMulBigInt": 15241578753238836750495351562536198787501905199875019052100, + "bigIntMulBigDec": 1.5241578753238838E58, + "bigIntMulInt": 1234567890123456789012345678900, + "bigIntMulDouble": 3.703703670370371E29 } } } diff --git a/jolt-core/src/test/resources/json/modifier/functions/padStringsTest.json b/jolt-core/src/test/resources/json/modifier/functions/padStringsTest.json index 114e53d9..5d9c762c 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/padStringsTest.json +++ b/jolt-core/src/test/resources/json/modifier/functions/padStringsTest.json @@ -1,58 +1,73 @@ { "input": { "string": "the QuIcK brOwn fox", - "padChar" : "Y", - "padAmount" : 10 + "padChar": "Y", + "padAmount": 10 }, - "spec": { "leftPad": { - "basic1": "=leftPad('fox', 10, 'X')", // happy path : actually do some padding - "basic2": "=leftPad('brown', 5, 'X')", // ok path : desired pad width is the same as the input - "basic3": "=leftPad('brown', 2, 'X')", // ok path : desired pad width less than the size of the input - "basic4": "=leftPad(@(2,string), 22, 'X')", // happy path : actually pad a looked up string from the data - "complex1" : "=leftPad('quick', @(2,padAmount), @(2,padChar))", // happy path : lookup pad amount and pad char - "badArgs1" : "=leftPad('quick', -1, 'X')", // negative pad amount - "badArgs2" : "=leftPad('quick', 0, 'X')", // zero pad amount - "badArgs3" : "=leftPad('quick', 8, 'AB')", // pad char is not a single character - "badArgs4" : "=leftPad('quick', 1000, 'F')" // stupidly big pad width should fail + "basic1": "=leftPad('fox', 10, 'X')", + // happy path : actually do some padding + "basic2": "=leftPad('brown', 5, 'X')", + // ok path : desired pad width is the same as the input + "basic3": "=leftPad('brown', 2, 'X')", + // ok path : desired pad width less than the size of the input + "basic4": "=leftPad(@(2,string), 22, 'X')", + // happy path : actually pad a looked up string from the data + "complex1": "=leftPad('quick', @(2,padAmount), @(2,padChar))", + // happy path : lookup pad amount and pad char + "badArgs1": "=leftPad('quick', -1, 'X')", + // negative pad amount + "badArgs2": "=leftPad('quick', 0, 'X')", + // zero pad amount + "badArgs3": "=leftPad('quick', 8, 'AB')", + // pad char is not a single character + "badArgs4": "=leftPad('quick', 1000, 'F')" + // stupidly big pad width should fail }, "rightPad": { - "basic1": "=rightPad('fox', 10, 'X')", // happy path : actually do some padding - "basic2": "=rightPad('brown', 5, 'X')", // ok path : desired pad width is the same as the input - "basic3": "=rightPad('brown', 2, 'X')", // ok path : desired pad width less than the size of the input - "basic4": "=rightPad(@(2,string), 22, 'X')", // happy path : actually pad a looked up string from the data - "complex1" : "=rightPad('quick', @(2,padAmount), @(2,padChar))", // happy path : lookup pad amount and pad char - "badArgs1" : "=rightPad('quick', -1, 'X')", // negative pad amount - "badArgs2" : "=rightPad('quick', 0, 'X')", // zero pad amount - "badArgs3" : "=rightPad('quick', 8, 'AB')", // pad char is not a single character - "badArgs4" : "=leftPad('quick', 1000, 'F')" // stupidly big pad width should fail + "basic1": "=rightPad('fox', 10, 'X')", + // happy path : actually do some padding + "basic2": "=rightPad('brown', 5, 'X')", + // ok path : desired pad width is the same as the input + "basic3": "=rightPad('brown', 2, 'X')", + // ok path : desired pad width less than the size of the input + "basic4": "=rightPad(@(2,string), 22, 'X')", + // happy path : actually pad a looked up string from the data + "complex1": "=rightPad('quick', @(2,padAmount), @(2,padChar))", + // happy path : lookup pad amount and pad char + "badArgs1": "=rightPad('quick', -1, 'X')", + // negative pad amount + "badArgs2": "=rightPad('quick', 0, 'X')", + // zero pad amount + "badArgs3": "=rightPad('quick', 8, 'AB')", + // pad char is not a single character + "badArgs4": "=leftPad('quick', 1000, 'F')" + // stupidly big pad width should fail } }, - "context": {}, - "OVERWRITR": { // // the input "string": "the QuIcK brOwn fox", - "padChar" : "Y", - "padAmount" : 10, + "padChar": "Y", + "padAmount": 10, // // the things modify added "leftPad": { "basic1": "XXXXXXXfox", "basic2": "brown", - "basic3" : "brown", + "basic3": "brown", "basic4": "XXXthe QuIcK brOwn fox", - "complex1" : "YYYYYquick" + "complex1": "YYYYYquick" }, "rightPad": { "basic1": "foxXXXXXXX", "basic2": "brown", - "basic3" : "brown", + "basic3": "brown", "basic4": "the QuIcK brOwn foxXXX", - "complex1" : "quickYYYYY" + "complex1": "quickYYYYY" } } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/modifier/functions/sizeTests.json b/jolt-core/src/test/resources/json/modifier/functions/sizeTests.json index 07cdf5e1..0cfa788b 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/sizeTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/sizeTests.json @@ -1,46 +1,52 @@ { "input": { - "emptyList" : [], - "emptyMap" : {}, - "emptyString" : "", - "emptyNull" : null, - - "legitList" : [ 1, "foo" ], - "legitMap" : { "a":"b", "d":"e"}, - "legitString" : "foo", - "legitNumber" : 3.1415 + "emptyList": [], + "emptyMap": {}, + "emptyString": "", + "emptyNull": null, + "legitList": [ + 1, + "foo" + ], + "legitMap": { + "a": "b", + "d": "e" + }, + "legitString": "foo", + "legitNumber": 3.1415 }, - "spec": { "emptyListSize": "=size(@(1,emptyList))", "emptyMapSize": "=size(@(1,emptyMap))", "emptyStringSize": "=size(@(1,emptyString))", "emptyNullSize": "=size(@(1,emptyNull))", - "legitListSize": "=size(@(1,legitList))", "legitMapSize": "=size(@(1,legitMap))", "legitStringSize": "=size(@(1,legitString))", "legitNumberSize": "=size(@(1,legitNumber))" }, "context": {}, - "OVERWRITR": { // original input that passes thru - "emptyList" : [], - "emptyMap" : {}, - "emptyString" : "", - "emptyNull" : null, - + "emptyList": [], + "emptyMap": {}, + "emptyString": "", + "emptyNull": null, "emptyListSize": 0, "emptyMapSize": 0, "emptyStringSize": 0, // note emptyNullSize does not get created because there can be no value for it - "legitList" : [ 1, "foo" ], - "legitMap" : { "a":"b", "d":"e"}, - "legitString" : "foo", - "legitNumber" : 3.1415, - + "legitList": [ + 1, + "foo" + ], + "legitMap": { + "a": "b", + "d": "e" + }, + "legitString": "foo", + "legitNumber": 3.1415, "legitListSize": 2, "legitMapSize": 2, "legitStringSize": 3 diff --git a/jolt-core/src/test/resources/json/modifier/functions/squashNullsTests.json b/jolt-core/src/test/resources/json/modifier/functions/squashNullsTests.json index de588833..1746d96a 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/squashNullsTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/squashNullsTests.json @@ -1,28 +1,69 @@ { "input": { - "squashThisList" : [ "a", null, 1, null, "b" ], - "squashThisMap" : { "a": "A", "b": null, "c" : "C" }, - - "superSquashThis" : [ "a", null, { "x": "X", "y": null, "zList" : [ "z1", null, "z3" ] }, null, "b" ], - - "doNotSquash" : { "a": "A", "b": null, "c" : "C" } + "squashThisList": [ + "a", + null, + 1, + null, + "b" + ], + "squashThisMap": { + "a": "A", + "b": null, + "c": "C" + }, + "superSquashThis": [ + "a", + null, + { + "x": "X", + "y": null, + "zList": [ + "z1", + null, + "z3" + ] + }, + null, + "b" + ], + "doNotSquash": { + "a": "A", + "b": null, + "c": "C" + } }, - "spec": { - "squashThisList" : "=squashNulls", - "squashThisMap" : "=squashNulls(@(1,squashThisMap))", - - "superSquashThis" : "=recursivelySquashNulls" + "squashThisList": "=squashNulls", + "squashThisMap": "=squashNulls(@(1,squashThisMap))", + "superSquashThis": "=recursivelySquashNulls" }, - "context": {}, - "OVERWRITR": { - "squashThisList" : [ "a", 1, "b" ], - "squashThisMap" : { "a": "A", "c" : "C"}, - - "superSquashThis" : [ "a", { "x": "X", "zList" : [ "z1", "z3" ] }, "b" ], - - "doNotSquash" : { "a": "A", "b": null, "c" : "C" } + "squashThisList": [ + "a", + 1, + "b" + ], + "squashThisMap": { + "a": "A", + "c": "C" + }, + "superSquashThis": [ + "a", + { + "x": "X", + "zList": [ + "z1", + "z3" + ] + }, + "b" + ], + "doNotSquash": { + "a": "A", + "b": null, + "c": "C" + } } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/modifier/functions/stringsSplitTest.json b/jolt-core/src/test/resources/json/modifier/functions/stringsSplitTest.json index 26d2660e..9c6f17b8 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/stringsSplitTest.json +++ b/jolt-core/src/test/resources/json/modifier/functions/stringsSplitTest.json @@ -1,31 +1,29 @@ { "input": { - "splitMe" : "split me", - + "splitMe": "split me", "string": "the QuIcK brOwn fox", - - "intputMapShouldNotSplit" : { "a" : "b" } + "intputMapShouldNotSplit": { + "a": "b" + } }, - "spec": { - "splitMe" : "=split(' ',@(1,splitMe))", + "splitMe": "=split(' ',@(1,splitMe))", "split": { "single": "=split(',', @(2,string))", "multiple": "=split(' ', @(2,string))", "regex": "=split('[Oo]', @(2,string))", "regex2": "=split('\\s+', @(2,string))" }, - "intputMapShouldNotSplit" : "=split('=', @(1,intputMapShouldNotSplit))" + "intputMapShouldNotSplit": "=split('=', @(1,intputMapShouldNotSplit))" }, - "context": { }, - "OVERWRITR": { "string": "the QuIcK brOwn fox", - - "splitMe" : [ "split", "me" ], - + "splitMe": [ + "split", + "me" + ], "split": { "single": [ "the QuIcK brOwn fox" @@ -48,7 +46,8 @@ "fox" ] }, - - "intputMapShouldNotSplit" : { "a" : "b" } + "intputMapShouldNotSplit": { + "a": "b" + } } } diff --git a/jolt-core/src/test/resources/json/modifier/functions/stringsTests.json b/jolt-core/src/test/resources/json/modifier/functions/stringsTests.json index 47aa33e5..9d3b1e80 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/stringsTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/stringsTests.json @@ -1,25 +1,25 @@ { "input": { "string": "the QuIcK brOwn fox", - "zeroIndex" : 0, - "threeIndex" : 3, - "trimMe" : " tuna " + "zeroIndex": 0, + "threeIndex": 3, + "trimMe": " tuna ", + "toBeReplacedAll": "Java123is456fun" }, - "spec": { "lower": { - "leading": "=toLower(@(2,string))", + "leading": "=toLower(@(2,string))", "trailing": "=toLower(^value)", - "custom1": "=toLower(bazinga)", - "custom2": "=toLower('yabadabadoo')", - "badArgs1" : "=toLower(@2)" + "custom1": "=toLower(bazinga)", + "custom2": "=toLower('yabadabadoo')", + "badArgs1": "=toLower(@2)" }, "upper": { "leading": "=toUpper(@(2,string))", "trailing": "=toUpper(^value)", "custom1": "=toUpper(bazinga)", "custom2": "=toUpper('yabadabadoo')", - "badArgs1" : "=toLower(@2)" + "badArgs1": "=toLower(@2)" }, "join": "=join('_' , @(1,lower.leading) , , @(1,lower.trailing))", "concat": { @@ -29,41 +29,69 @@ "substring": { "basic1": "=substring(@(2,string), 0, 9)", "basic2": "=substring(@(2,string), 4, 9)", - "outOfBounds1": "=substring(@(2,string), -4, 9)", // start is negative - "outOfBounds2": "=substring(@(2,string), 0, 200)", // end it way past the size of the string - "outOfBounds3": "=substring(@(2,string), 0, 20)", // verify that asking for one char after the end fails - "badArgs1": "=substring(0, 9, @(2, substring))", // input is not a string - "badArgs2": "=substring(0, 4, 9)", // ranges are ok, but input string is not a string + "outOfBounds1": "=substring(@(2,string), -4, 9)", + // start is negative + "outOfBounds2": "=substring(@(2,string), 0, 200)", + // end it way past the size of the string + "outOfBounds3": "=substring(@(2,string), 0, 20)", + // verify that asking for one char after the end fails + "badArgs1": "=substring(0, 9, @(2, substring))", + // input is not a string + "badArgs2": "=substring(0, 4, 9)", + // ranges are ok, but input string is not a string "badArgs3": "=substring('', 0, 0)", - "badArgs4": "=substring('abc', 0, 0)", // start and end are the same, both are zero - "badArgs5": "=substring('abc', 1, 1)", // start and end are the same, and non-zero - "badArgs6": "=substring('abc', 1, 0)", // start before end - "badArgs7": "=substring('abc', 0, 1, 2)", // too many args - "badArgs8": "=substring('abc', 0)", // not enough args - "custom1": "=substring('the quick brown fox', 0, 15)", - "custom2": "=substring('the quick brown fox', 16, 19)", + "badArgs4": "=substring('abc', 0, 0)", + // start and end are the same, both are zero + "badArgs5": "=substring('abc', 1, 1)", + // start and end are the same, and non-zero + "badArgs6": "=substring('abc', 1, 0)", + // start before end + "badArgs7": "=substring('abc', 0, 1, 2)", + // too many args + "badArgs8": "=substring('abc', 0)", + // not enough args + "custom1": "=substring('the quick brown fox', 0, 15)", + "custom2": "=substring('the quick brown fox', 16, 19)", // // verify that we can actually lookup start and end indices - "advancedLookupRanges" : "=substring(@(2,string), @(2,zeroIndex), @(2,threeIndex))" + "advancedLookupRanges": "=substring(@(2,string), @(2,zeroIndex), @(2,threeIndex))" }, - "trim" :{ - "trimed" : "=trim(@(2,trimMe))" + "trim": { + "trimed": "=trim(@(2,trimMe))" }, - "trimMe" : "=trim" + "trimMe": "=trim", + "replace": { + "replaced": "=replace(@(2,string), 'fox', 'dog')", + "badArgs0": "=relace", + "badArgs1": "=replace(@(2,string), 'fox')", + "badArgs2": "=replace(@(2,string), 'fox', 2)", + "badArgs3": "=replace(@(2,string), 2, 'dog')", + "badArgs4": "=replace(2, 'fox', 'dog')" + }, + "replaceAll": { + "replaced": "=replaceAll(@(2,toBeReplacedAll), '\\d+', ' ')", + "badArgs0": "=replaceAll", + "badArgs1": "=replaceAll(@(2,toBeReplacedAll), '\\d+')", + "badArgs2": "=replaceAll(@(2,toBeReplacedAll), '\\d+', 2)", + "badArgs3": "=replaceAll(@(2,toBeReplacedAll), '[abc', ' ')", + "badArgs4": "=replaceAll(@(2,toBeReplacedAll), 2, ' ')", + "badArgs5": "=replaceAll(2, '\\d+', ' ')" + }, + "toBeReplacedAll": "=replaceAll(@(2,toBeReplacedAll), '\\d+', ' ')" }, "context": { - "value" : "JumpeD OVeR THE laZy dog" + "value": "JumpeD OVeR THE laZy dog" }, - "OVERWRITR": { // // the input - "string" : "the QuIcK brOwn fox", - "zeroIndex" : 0, - "threeIndex" : 3, + "string": "the QuIcK brOwn fox", + "zeroIndex": 0, + "threeIndex": 3, // // from the input, but overwritten by modify - "trimMe" : "tuna", + "trimMe": "tuna", + "toBeReplacedAll": "Java is fun", // // the things modify added "lower": { @@ -90,8 +118,14 @@ "custom2": "fox", "advancedLookupRanges": "the" }, - "trim" :{ - "trimed" : "tuna" + "trim": { + "trimed": "tuna" + }, + "replace": { + "replaced": "the QuIcK brOwn dog" + }, + "replaceAll": { + "replaced": "Java is fun" } } } diff --git a/jolt-core/src/test/resources/json/modifier/functions/valueTests.json b/jolt-core/src/test/resources/json/modifier/functions/valueTests.json index 87023c95..b476d60f 100644 --- a/jolt-core/src/test/resources/json/modifier/functions/valueTests.json +++ b/jolt-core/src/test/resources/json/modifier/functions/valueTests.json @@ -1,37 +1,33 @@ { "input": { - "p": "", "q": "", "r": "", - "x": null, "y": null, "z": null }, - "spec": { "a": "=isPresent", "b": "=notNull", "c": "=isNull", - "p": "=isPresent", "q": "=notNull", "r": "=isNull", - "x": "=isPresent", - "y": [ "=notNull", 3 ], + "y": [ + "=notNull", + 3 + ], "z": "=isNull" }, - "context": {}, - "OVERWRITR": { - "p" : "", - "q" : "", - "r" : "", - "x" : null, - "y" : 3, - "z" : null + "p": "", + "q": "", + "r": "", + "x": null, + "y": 3, + "z": null } } diff --git a/jolt-core/src/test/resources/json/modifier/mapLiteral.json b/jolt-core/src/test/resources/json/modifier/mapLiteral.json index 2399c13e..fc3dce5f 100644 --- a/jolt-core/src/test/resources/json/modifier/mapLiteral.json +++ b/jolt-core/src/test/resources/json/modifier/mapLiteral.json @@ -9,7 +9,6 @@ "5": null } }, - "spec": { "simpleMap": { "*": 0, @@ -19,11 +18,9 @@ "9": null } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleMap": { "0": 0, @@ -38,7 +35,6 @@ "9": 0 } }, - "DEFAULTR": { "simpleMap": { "0": 0, diff --git a/jolt-core/src/test/resources/json/modifier/mapLiteralWithEmptyInput.json b/jolt-core/src/test/resources/json/modifier/mapLiteralWithEmptyInput.json index 93a12f15..c29a5b2c 100644 --- a/jolt-core/src/test/resources/json/modifier/mapLiteralWithEmptyInput.json +++ b/jolt-core/src/test/resources/json/modifier/mapLiteralWithEmptyInput.json @@ -2,7 +2,6 @@ "input": { "simpleMap": {} }, - "spec": { "simpleMap": { "*": 0, @@ -12,11 +11,9 @@ "9": null } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleMap": { "6": 0, @@ -25,7 +22,6 @@ "9": 0 } }, - "DEFAULTR": { "simpleMap": { "6": 1, @@ -35,11 +31,11 @@ } }, "DEFINR": { - "simpleMap" : { - "6" : 1, - "7" : 5, - "8" : 5, - "9" : null + "simpleMap": { + "6": 1, + "7": 5, + "8": 5, + "9": null } } } diff --git a/jolt-core/src/test/resources/json/modifier/mapLiteralWithMissingInput.json b/jolt-core/src/test/resources/json/modifier/mapLiteralWithMissingInput.json index dce07c0b..deb81d91 100644 --- a/jolt-core/src/test/resources/json/modifier/mapLiteralWithMissingInput.json +++ b/jolt-core/src/test/resources/json/modifier/mapLiteralWithMissingInput.json @@ -1,6 +1,5 @@ { - "input": { }, - + "input": {}, "spec": { "simpleMap": { "*": 0, @@ -10,11 +9,9 @@ "9": null } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleMap": { "6": 0, @@ -23,7 +20,6 @@ "9": 0 } }, - "DEFAULTR": { "simpleMap": { "6": 1, @@ -32,13 +28,12 @@ "9": 0 } }, - "DEFINR": { - "simpleMap" : { - "6" : 1, - "7" : 5, - "8" : 5, - "9" : null + "simpleMap": { + "6": 1, + "7": 5, + "8": 5, + "9": null } } } diff --git a/jolt-core/src/test/resources/json/modifier/mapLiteralWithNullInput.json b/jolt-core/src/test/resources/json/modifier/mapLiteralWithNullInput.json index d17940d5..9d02f08b 100644 --- a/jolt-core/src/test/resources/json/modifier/mapLiteralWithNullInput.json +++ b/jolt-core/src/test/resources/json/modifier/mapLiteralWithNullInput.json @@ -2,7 +2,6 @@ "input": { "simpleMap": null }, - "spec": { "simpleMap": { "*": 0, @@ -12,11 +11,9 @@ "9": null } }, - "context": { "value": 5 }, - "OVERWRITR": { "simpleMap": { "6": 0, @@ -25,7 +22,6 @@ "9": 0 } }, - "DEFAULTR": { "simpleMap": { "6": 1, diff --git a/jolt-core/src/test/resources/json/modifier/simple.json b/jolt-core/src/test/resources/json/modifier/simple.json index c98650c0..0fb68f86 100644 --- a/jolt-core/src/test/resources/json/modifier/simple.json +++ b/jolt-core/src/test/resources/json/modifier/simple.json @@ -11,7 +11,6 @@ "comment": "anything not in spec is a pass-through" } }, - "spec": { "thumbnail": { "label": "^photo.label.thumbnail", @@ -19,9 +18,9 @@ "special": "\\^escaped value" }, "context": "^", - "*WithMatch": "@(1,&0)" // this is an explicit pass through + "*WithMatch": "@(1,&0)" + // this is an explicit pass through }, - "context": { "photo": { "label": { @@ -29,7 +28,6 @@ } } }, - "OVERWRITR": { "thumbnail": { "Url": "http://test.com/0001/1234/photoThumb.jpg", @@ -52,50 +50,48 @@ } } }, - "DEFAULTR": { - "thumbnail" : { - "Url" : "http://test.com/0001/1234/photoThumb.jpg", - "Id" : "thumbnail", - "label" : "thumbnail photo", - "default" : "defaultValue", - "special" : "\\^escaped value" + "thumbnail": { + "Url": "http://test.com/0001/1234/photoThumb.jpg", + "Id": "thumbnail", + "label": "thumbnail photo", + "default": "defaultValue", + "special": "\\^escaped value" }, - "passThroughWithMatch" : { - "comment" : "matched in spec but marked to be passThrough with @" + "passThroughWithMatch": { + "comment": "matched in spec but marked to be passThrough with @" }, - "passThroughWithoutMatch" : { - "comment" : "anything not in spec is a pass-through" + "passThroughWithoutMatch": { + "comment": "anything not in spec is a pass-through" }, - "context" : { - "photo" : { - "label" : { - "thumbnail" : "thumbnail photo" + "context": { + "photo": { + "label": { + "thumbnail": "thumbnail photo" } } } }, "DEFINR": { - "thumbnail" : { - "Url" : "http://test.com/0001/1234/photoThumb.jpg", - "Id" : "thumbnail", - "label" : "thumbnail photo", - "default" : "defaultValue", - "special" : "\\^escaped value" + "thumbnail": { + "Url": "http://test.com/0001/1234/photoThumb.jpg", + "Id": "thumbnail", + "label": "thumbnail photo", + "default": "defaultValue", + "special": "\\^escaped value" }, - "passThroughWithMatch" : { - "comment" : "matched in spec but marked to be passThrough with @" + "passThroughWithMatch": { + "comment": "matched in spec but marked to be passThrough with @" }, - "passThroughWithoutMatch" : { - "comment" : "anything not in spec is a pass-through" + "passThroughWithoutMatch": { + "comment": "anything not in spec is a pass-through" }, - "context" : { - "photo" : { - "label" : { - "thumbnail" : "thumbnail photo" + "context": { + "photo": { + "label": { + "thumbnail": "thumbnail photo" } } } } - } diff --git a/jolt-core/src/test/resources/json/modifier/simpleArray.json b/jolt-core/src/test/resources/json/modifier/simpleArray.json index a51fff3f..3ae83fa4 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleArray.json +++ b/jolt-core/src/test/resources/json/modifier/simpleArray.json @@ -12,7 +12,6 @@ ], "default": "defaultValue" }, - "spec": { "thumbnail": { "*": { @@ -21,7 +20,6 @@ } } }, - "context": { "photo": { "label": { @@ -29,7 +27,6 @@ } } }, - "OVERWRITR": { "thumbnail": [ { diff --git a/jolt-core/src/test/resources/json/modifier/simpleArrayLookup.json b/jolt-core/src/test/resources/json/modifier/simpleArrayLookup.json index 394edee5..14c5ef8d 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleArrayLookup.json +++ b/jolt-core/src/test/resources/json/modifier/simpleArrayLookup.json @@ -25,7 +25,6 @@ } } }, - "spec": { "photo": { "Sizes": { @@ -44,7 +43,6 @@ "default": "some default value" } }, - "context": { "textValues": { "photo": [ @@ -62,7 +60,6 @@ } } }, - "OVERWRITR": { "photo": { "Sizes": { diff --git a/jolt-core/src/test/resources/json/modifier/simpleArrayOpOverride.json b/jolt-core/src/test/resources/json/modifier/simpleArrayOpOverride.json index 1be966db..0d6e918f 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleArrayOpOverride.json +++ b/jolt-core/src/test/resources/json/modifier/simpleArrayOpOverride.json @@ -4,7 +4,6 @@ "a", "b", "c", - null, null, null @@ -14,17 +13,14 @@ null ] }, - "spec": { "data": { "+[0]": "aa", "~[1]": "bb", "_[2]": "cc", - "+[3]": "pp", "~[4]": "qq", "_[5]": "rr", - "+[6]": "xx", "~[7]": "yy", "_[8]": "zz" @@ -35,9 +31,7 @@ "[2]": "cc" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { "data": [ "aa", @@ -50,13 +44,12 @@ "yy", "zz" ], - "data2" : [ + "data2": [ "aa", "bb", "cc" ] }, - "DEFAULTR": { "data": [ "aa", @@ -69,13 +62,12 @@ "yy", "zz" ], - "data2" : [ + "data2": [ "a", "bb", "cc" ] }, - "DEFINR": { "data": [ "aa", @@ -88,7 +80,7 @@ "yy", "zz" ], - "data2" : [ + "data2": [ "a", null, "cc" diff --git a/jolt-core/src/test/resources/json/modifier/simpleLookup.json b/jolt-core/src/test/resources/json/modifier/simpleLookup.json index 6ec26200..a7ba5e99 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleLookup.json +++ b/jolt-core/src/test/resources/json/modifier/simpleLookup.json @@ -13,7 +13,6 @@ } } }, - "spec": { "photo": { "Sizes": { @@ -28,7 +27,6 @@ "default": "some default value" } }, - "context": { "textValues": { "photo": { @@ -40,7 +38,6 @@ } } }, - "OVERWRITR": { "photo": { "Sizes": { diff --git a/jolt-core/src/test/resources/json/modifier/simpleMapNullToArray.json b/jolt-core/src/test/resources/json/modifier/simpleMapNullToArray.json index 6805c50e..4be4dc89 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleMapNullToArray.json +++ b/jolt-core/src/test/resources/json/modifier/simpleMapNullToArray.json @@ -2,25 +2,23 @@ "input": { "simpleMap": null }, - "spec": { "simpleMap": { "*": 0, "[1]": 1 } }, - "context": {}, - "OVERWRITR": { "simpleMap": [ - 0,0 + 0, + 0 ] }, - "DEFAULTR": { "simpleMap": [ - 0,1 + 0, + 1 ] }, "DEFINR": { diff --git a/jolt-core/src/test/resources/json/modifier/simpleMapOpOverride.json b/jolt-core/src/test/resources/json/modifier/simpleMapOpOverride.json index f7ac25f9..86d8c414 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleMapOpOverride.json +++ b/jolt-core/src/test/resources/json/modifier/simpleMapOpOverride.json @@ -4,7 +4,6 @@ "a": "a", "b": "b", "c": "c", - "p": null, "q": null, "r": null @@ -17,17 +16,14 @@ // c missing } }, - "spec": { "data": { "+a": "aa", "~b": "bb", "_c": "cc", - "+p": "pp", "~q": "qq", "_r": "rr", - "+x": "xx", "~y": "yy", "_z": "zz" @@ -38,63 +34,59 @@ "c": "cc" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { "data": { - "a" : "aa", - "b" : "b", - "c" : "c", - "p" : "pp", - "q" : "qq", - "r" : null, - "x" : "xx", - "y" : "yy", - "z" : "zz" + "a": "aa", + "b": "b", + "c": "c", + "p": "pp", + "q": "qq", + "r": null, + "x": "xx", + "y": "yy", + "z": "zz" }, - "data2" : { - "a" : "aa", - "b" : "bb", - "c" : "cc" + "data2": { + "a": "aa", + "b": "bb", + "c": "cc" } }, - "DEFAULTR": { "data": { - "a" : "aa", - "b" : "b", - "c" : "c", - "p" : "pp", - "q" : "qq", - "r" : null, - "x" : "xx", - "y" : "yy", - "z" : "zz" + "a": "aa", + "b": "b", + "c": "c", + "p": "pp", + "q": "qq", + "r": null, + "x": "xx", + "y": "yy", + "z": "zz" }, - "data2" : { - "a" : "a", - "b" : "bb", - "c" : "cc" + "data2": { + "a": "a", + "b": "bb", + "c": "cc" } }, - "DEFINR": { "data": { - "a" : "aa", - "b" : "b", - "c" : "c", - "p" : "pp", - "q" : "qq", - "r" : null, - "x" : "xx", - "y" : "yy", - "z" : "zz" + "a": "aa", + "b": "b", + "c": "c", + "p": "pp", + "q": "qq", + "r": null, + "x": "xx", + "y": "yy", + "z": "zz" }, - "data2" : { - "a" : "a", - "b" : null, - "c" : "cc" + "data2": { + "a": "a", + "b": null, + "c": "cc" } } } diff --git a/jolt-core/src/test/resources/json/modifier/simpleMapRuntimeNull.json b/jolt-core/src/test/resources/json/modifier/simpleMapRuntimeNull.json index dd35c03d..4bcb8c0a 100644 --- a/jolt-core/src/test/resources/json/modifier/simpleMapRuntimeNull.json +++ b/jolt-core/src/test/resources/json/modifier/simpleMapRuntimeNull.json @@ -2,19 +2,15 @@ "input": { "simpleMap": null }, - "spec": { "simpleMap": { "*": 0 } }, - "context": {}, - "OVERWRITR": { "simpleMap": null }, - "DEFAULTR": { "simpleMap": null }, diff --git a/jolt-core/src/test/resources/json/modifier/testListOfFunction.json b/jolt-core/src/test/resources/json/modifier/testListOfFunction.json index 355078b8..26f4a0b5 100644 --- a/jolt-core/src/test/resources/json/modifier/testListOfFunction.json +++ b/jolt-core/src/test/resources/json/modifier/testListOfFunction.json @@ -4,42 +4,62 @@ "a": null, "b": null, "c": null, - - "p" : 0, - "q" : 0, - "r" : 0 + "p": 0, + "q": 0, + "r": 0 // xyz missing } }, - "spec": { "data": { - "+a": [ "=noop", "=min(1 , 2 , 3)" ], - "~b": [ "=notNull", "1" ], - "_c": [ "=min(1 , 2 , 3)" ], - - "_p": [ "=isNull", "=min(1,2,3,@(1,&0))" ], - "+q": [ "=max('a','b','c')", "@(1,&0)" ], - "~r": [ "=toDouble" ], - - "~x": [ "=notNull", "=max(1,2,3)" ], - "_y": [ "=notNull", "=toDouble" ], - "+z": [ "=nonnull", "=min('a','b','c')", "=toList" ] + "+a": [ + "=noop", + "=min(1 , 2 , 3)" + ], + "~b": [ + "=notNull", + "1" + ], + "_c": [ + "=min(1 , 2 , 3)" + ], + "_p": [ + "=isNull", + "=min(1,2,3,@(1,&0))" + ], + "+q": [ + "=max('a','b','c')", + "@(1,&0)" + ], + "~r": [ + "=toDouble" + ], + "~x": [ + "=notNull", + "=max(1,2,3)" + ], + "_y": [ + "=notNull", + "=toDouble" + ], + "+z": [ + "=nonnull", + "=min('a','b','c')", + "=toList" + ] } }, - - "context": { }, - + "context": {}, "OVERWRITR": { "data": { - "a" : 1, - "b" : "1", - "c" : null, - "p" : 0, - "q" : 0, - "r" : 0, - "x" : 3 + "a": 1, + "b": "1", + "c": null, + "p": 0, + "q": 0, + "r": 0, + "x": 3 } } } diff --git a/jolt-core/src/test/resources/json/modifier/validation/specThatShouldFail.json b/jolt-core/src/test/resources/json/modifier/validation/specThatShouldFail.json index 4121db76..cf00d5bf 100644 --- a/jolt-core/src/test/resources/json/modifier/validation/specThatShouldFail.json +++ b/jolt-core/src/test/resources/json/modifier/validation/specThatShouldFail.json @@ -95,7 +95,7 @@ } } }, - { + { "thumbnail": { "[1]": { "label": "^photo.label.main" @@ -105,7 +105,8 @@ }, "[2]": { "label": "aux photo" - } } + } + } }, { "thumbnail": { diff --git a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArray.json b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArray.json index b467f6c8..85b05ffe 100644 --- a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArray.json +++ b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArray.json @@ -1,8 +1,11 @@ { "input": { - "data": [ null,"b","c" ] + "data": [ + null, + "b", + "c" + ] }, - "spec": { "data": { "[0]": "x", @@ -11,21 +14,29 @@ "[4]?": "e" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { - "data": - [ "x", "y", "c", "d" ] + "data": [ + "x", + "y", + "c", + "d" + ] }, - "DEFAULTR": { - "data": - [ "x", "b", "c", "d" ] + "data": [ + "x", + "b", + "c", + "d" + ] }, - "DEFINR": { - "data": - [ null, "b", "c", "d" ] + "data": [ + null, + "b", + "c", + "d" + ] } } diff --git a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayEmptyInput.json b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayEmptyInput.json index 918d01a9..a8d4d11b 100644 --- a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayEmptyInput.json +++ b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayEmptyInput.json @@ -2,7 +2,6 @@ "input": { "data": [] }, - "spec": { "data": { "[0]": "x", @@ -11,20 +10,29 @@ "[4]?": "e" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { - "data": - [ "x", null, null, "d" ] + "data": [ + "x", + null, + null, + "d" + ] }, - - "DEFAULTR":{ - "data": - [ "x", null, null, "d" ] + "DEFAULTR": { + "data": [ + "x", + null, + null, + "d" + ] }, - "DEFINR":{ - "data": - [ "x", null, null, "d" ] + "DEFINR": { + "data": [ + "x", + null, + null, + "d" + ] } } diff --git a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayNullInput.json b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayNullInput.json index 9f094b50..95e332ee 100644 --- a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayNullInput.json +++ b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleArrayNullInput.json @@ -2,7 +2,6 @@ "input": { "data": null }, - "spec": { "data": { "[0]": "x", @@ -11,19 +10,24 @@ "[4]?": "e" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { - "data": - [ "x", null, null, "d" ] + "data": [ + "x", + null, + null, + "d" + ] }, - - "DEFAULTR":{ - "data": - [ "x", null, null, "d" ] + "DEFAULTR": { + "data": [ + "x", + null, + null, + "d" + ] }, - "DEFINR":{ + "DEFINR": { "data": null } } diff --git a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMap.json b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMap.json index 6edf4a89..a99595f3 100644 --- a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMap.json +++ b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMap.json @@ -5,7 +5,6 @@ "c": null } }, - "spec": { "data": { "a?": "x", @@ -13,24 +12,19 @@ "c": "z" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { "data": { "a": "x", "c": "z" } - } - , - + }, "DEFAULTR": { "data": { "a": "a", "c": "z" } }, - "DEFINR": { "data": { "a": "a", diff --git a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapEmptyInput.json b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapEmptyInput.json index d56e1f23..44fdcdbf 100644 --- a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapEmptyInput.json +++ b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapEmptyInput.json @@ -2,7 +2,6 @@ "input": { "data": {} }, - "spec": { "data": { "a?": "x", @@ -10,25 +9,20 @@ "c": "z" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { "data": { - "c" : "z" + "c": "z" } - } - , - + }, "DEFAULTR": { "data": { - "c" : "z" + "c": "z" } }, - "DEFINR": { "data": { - "c" : "z" + "c": "z" } } } diff --git a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapNullInput.json b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapNullInput.json index 8f5df416..4a096f28 100644 --- a/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapNullInput.json +++ b/jolt-core/src/test/resources/json/modifier/valueCheckSimpleMapNullInput.json @@ -2,7 +2,6 @@ "input": { "data": null }, - "spec": { "data": { "a?": "x", @@ -10,22 +9,17 @@ "c": "z" } }, - - "context": { }, - + "context": {}, "OVERWRITR": { "data": { - "c" : "z" + "c": "z" } - } - , - + }, "DEFAULTR": { "data": { - "c" : "z" + "c": "z" } }, - "DEFINR": { "data": null } diff --git a/jolt-core/src/test/resources/json/removr/array_canHandleTopLevelArray.json b/jolt-core/src/test/resources/json/removr/array_canHandleTopLevelArray.json index 0b1f449c..877d6ca1 100644 --- a/jolt-core/src/test/resources/json/removr/array_canHandleTopLevelArray.json +++ b/jolt-core/src/test/resources/json/removr/array_canHandleTopLevelArray.json @@ -1,51 +1,49 @@ { - "input": [ - [ - { - "removeThis": "gone", - "testStay": "firstArray" - }, - { - "removeThis": "gone", - "testStay": "still here firstArray" - } + "input": [ + [ + { + "removeThis": "gone", + "testStay": "firstArray" + }, + { + "removeThis": "gone", + "testStay": "still here firstArray" + } + ], + [ + { + "removeThis": "gone", + "testStay": "secondaryArray" + }, + { + "removeThis": "gone", + "testStay": "still here secondArray" + } + ] ], - [ - { - "removeThis": "gone", - "testStay": "secondaryArray" - }, - { - "removeThis": "gone", - "testStay": "still here secondArray" - } + "spec": { + "*": { + "*": { + "removeThis": "" + } + } + }, + "expected": [ + [ + { + "testStay": "firstArray" + }, + { + "testStay": "still here firstArray" + } + ], + [ + { + "testStay": "secondaryArray" + }, + { + "testStay": "still here secondArray" + } + ] ] - ], - - "spec": { - "*": { - "*" : { - "removeThis": "" - } - } - }, - - "expected": [ - [ - { - "testStay": "firstArray" - }, - { - "testStay": "still here firstArray" - } - ], - [ - { - "testStay": "secondaryArray" - }, - { - "testStay": "still here secondArray" - } - ] - ] } diff --git a/jolt-core/src/test/resources/json/removr/array_canPassThruNestedArrays.json b/jolt-core/src/test/resources/json/removr/array_canPassThruNestedArrays.json index 36a0ad39..f4f8eeb6 100644 --- a/jolt-core/src/test/resources/json/removr/array_canPassThruNestedArrays.json +++ b/jolt-core/src/test/resources/json/removr/array_canPassThruNestedArrays.json @@ -1,57 +1,55 @@ { - "input": { - "arrayObjects": [ - [ - { - "removeThis": "gone", - "testStay": "firstArray" - }, - { - "removeThis": "gone", - "testStay": "still here firstArray" + "input": { + "arrayObjects": [ + [ + { + "removeThis": "gone", + "testStay": "firstArray" + }, + { + "removeThis": "gone", + "testStay": "still here firstArray" + } + ], + [ + { + "removeThis": "gone", + "testStay": "secondaryArray" + }, + { + "removeThis": "gone", + "testStay": "still here secondArray" + } + ] + ] + }, + "spec": { + "arrayObjects": { + "*": { + "*": { + "removeThis": "" + } + } } - ], - [ - { - "removeThis": "gone", - "testStay": "secondaryArray" - }, - { - "removeThis": "gone", - "testStay": "still here secondArray" - } - ] - ] - }, - - "spec": { - "arrayObjects": { - "*": { - "*" : { - "removeThis": "" - } - } + }, + "expected": { + "arrayObjects": [ + [ + { + "testStay": "firstArray" + }, + { + "testStay": "still here firstArray" + } + ], + [ + { + "testStay": "secondaryArray" + }, + { + "testStay": "still here secondArray" + } + ] + ] } - }, - - "expected": { - "arrayObjects": [ - [ - { - "testStay": "firstArray" - }, - { - "testStay": "still here firstArray" - } - ], - [ - { - "testStay": "secondaryArray" - }, - { - "testStay": "still here secondArray" - } - ] - ] - } } diff --git a/jolt-core/src/test/resources/json/removr/array_nonStarInArrayDoesNotDie.json b/jolt-core/src/test/resources/json/removr/array_nonStarInArrayDoesNotDie.json index 69bd9b24..ab9ac834 100644 --- a/jolt-core/src/test/resources/json/removr/array_nonStarInArrayDoesNotDie.json +++ b/jolt-core/src/test/resources/json/removr/array_nonStarInArrayDoesNotDie.json @@ -1,38 +1,36 @@ { - "input": { - "arrayObjects": [ - { - "a": "b", - "testStay": "here" - }, - { - "a": "b", - "testStay": "still here" - } - ] - }, - - "spec": { - "arrayObjects": { - "pants": { - "a": "" - }, - "abc*" : { - "a": "" - } + "input": { + "arrayObjects": [ + { + "a": "b", + "testStay": "here" + }, + { + "a": "b", + "testStay": "still here" + } + ] + }, + "spec": { + "arrayObjects": { + "pants": { + "a": "" + }, + "abc*": { + "a": "" + } + } + }, + "expected": { + "arrayObjects": [ + { + "a": "b", + "testStay": "here" + }, + { + "a": "b", + "testStay": "still here" + } + ] } - }, - - "expected": { - "arrayObjects": [ - { - "a": "b", - "testStay": "here" - }, - { - "a": "b", - "testStay": "still here" - } - ] - } } diff --git a/jolt-core/src/test/resources/json/removr/array_removeAnArrayIndex.json b/jolt-core/src/test/resources/json/removr/array_removeAnArrayIndex.json index 33298631..a6be8e34 100644 --- a/jolt-core/src/test/resources/json/removr/array_removeAnArrayIndex.json +++ b/jolt-core/src/test/resources/json/removr/array_removeAnArrayIndex.json @@ -1,31 +1,48 @@ { - "input": { - "arrayObjects": [ - { "zero": "ZERO" }, - { "one": "ONE" }, - { "two": 2 }, - { "three": 3 }, - { "four": "FOUR" } - ] - }, + "input": { + "arrayObjects": [ + { + "zero": "ZERO" + }, + { + "one": "ONE" + }, + { + "two": 2 + }, + { + "three": 3 + }, + { + "four": "FOUR" + } + ] + }, + "spec": { + "arrayObjects": { + "0": "", + // test lower bound + "2": "", + "4": "", + // test upper bound of list - "spec": { - "arrayObjects": { - "0" : "", // test lower bound - "2" : "", - "4" : "", // test upper bound of list - - // Things that should not cause errors / problems - "10": "", // Index larger than input array - "-8": "", // Index value is negative - "a" : "" // Index value is not a number - } - }, - - "expected": { - "arrayObjects": [ - { "one": "ONE" }, - { "three": 3 } - ] - } + // Things that should not cause errors / problems + "10": "", + // Index larger than input array + "-8": "", + // Index value is negative + "a": "" + // Index value is not a number + } + }, + "expected": { + "arrayObjects": [ + { + "one": "ONE" + }, + { + "three": 3 + } + ] + } } diff --git a/jolt-core/src/test/resources/json/removr/array_removeJsonArrayFields.json b/jolt-core/src/test/resources/json/removr/array_removeJsonArrayFields.json index 9c4cdade..0164b64f 100644 --- a/jolt-core/src/test/resources/json/removr/array_removeJsonArrayFields.json +++ b/jolt-core/src/test/resources/json/removr/array_removeJsonArrayFields.json @@ -1,35 +1,37 @@ { - "input": { - "arrayObjects": [ - { - "removeThis": "gone", - "testStay": "here" - }, - { - "removeThis": "remove", - "testStay": "still here" - } - ], - "arrayString" : ["entire", "field", "gone"] - }, - - "spec": { - "arrayObjects": { - "*": { - "removeThis": "" - } + "input": { + "arrayObjects": [ + { + "removeThis": "gone", + "testStay": "here" + }, + { + "removeThis": "remove", + "testStay": "still here" + } + ], + "arrayString": [ + "entire", + "field", + "gone" + ] }, - "arrayString": "" - }, - - "expected": { - "arrayObjects": [ - { - "testStay": "here" - }, - { - "testStay": "still here" - } - ] - } + "spec": { + "arrayObjects": { + "*": { + "removeThis": "" + } + }, + "arrayString": "" + }, + "expected": { + "arrayObjects": [ + { + "testStay": "here" + }, + { + "testStay": "still here" + } + ] + } } diff --git a/jolt-core/src/test/resources/json/removr/boundaryConditions.json b/jolt-core/src/test/resources/json/removr/boundaryConditions.json index 18bcc71a..f50be579 100644 --- a/jolt-core/src/test/resources/json/removr/boundaryConditions.json +++ b/jolt-core/src/test/resources/json/removr/boundaryConditions.json @@ -1,33 +1,29 @@ { "input": { - "iamnotinspec":"eh?", - "nested":{ - "level1":"", - "level2":"1" + "iamnotinspec": "eh?", + "nested": { + "level1": "", + "level2": "1" }, - "wildcard":"" + "wildcard": "" }, - - "spec": - { - "iamnotininput":"", - "nested": - { - "a":{ - "b":"" + "spec": { + "iamnotininput": "", + "nested": { + "a": { + "b": "" } }, - "wildcard":{ - "*":"" + "wildcard": { + "*": "" } }, - "expected": { - "iamnotinspec":"eh?", - "nested":{ - "level1":"", - "level2":"1" + "iamnotinspec": "eh?", + "nested": { + "level1": "", + "level2": "1" }, - "wildcard":"" + "wildcard": "" } } diff --git a/jolt-core/src/test/resources/json/removr/firstSample.json b/jolt-core/src/test/resources/json/removr/firstSample.json index b2f70fb7..cceb30e3 100644 --- a/jolt-core/src/test/resources/json/removr/firstSample.json +++ b/jolt-core/src/test/resources/json/removr/firstSample.json @@ -6,25 +6,21 @@ "contentLocale": "en_US", "id": "123124", "productId": "31231231", - "ipAddress": "192.168.1.100", "ipAdd": "192.168.1.100", "lastModificationTime": "yesterday", "submissionId": "34343", - "this": "stays", - "configured": { "a": "b", "c": { - "d":"e", - "f":"g" + "d": "e", + "f": "g" } }, - "non-nested-input":{}, - "iamnotinspec":"eh?" + "non-nested-input": {}, + "iamnotinspec": "eh?" }, - "spec": { "~emVersion": "", "authorId": "", @@ -32,33 +28,28 @@ "contentLocale": "", "id": "", "productId": "", - "ipAddress|ipAdd": "", "lastModificationTime": "", "submissionId": "", - - "non-nested-input":{ - "a":"" + "non-nested-input": { + "a": "" }, "configured": { "c": { - "d":"" + "d": "" } }, - "iamnotininput":"" - + "iamnotininput": "" }, - "expected": { "this": "stays", - "configured": { "a": "b", "c": { - "f":"g" + "f": "g" } }, - "non-nested-input":{}, - "iamnotinspec":"eh?" + "non-nested-input": {}, + "iamnotinspec": "eh?" } } diff --git a/jolt-core/src/test/resources/json/removr/multiStarSupport.json b/jolt-core/src/test/resources/json/removr/multiStarSupport.json index 7d70c73e..4be75283 100644 --- a/jolt-core/src/test/resources/json/removr/multiStarSupport.json +++ b/jolt-core/src/test/resources/json/removr/multiStarSupport.json @@ -1,23 +1,19 @@ - { "input": { - "TAG.Bob$ge":"", - "TAG-Bob-grp1-grp2$ge":"", - "TAG-Bob-grp1-grp2ge":"", - "TAG-Bob-grp1-ge":"", - "TAG-Bob-grp1-$":"", - "TAG-Bob-grp1-$1":"" + "TAG.Bob$ge": "", + "TAG-Bob-grp1-grp2$ge": "", + "TAG-Bob-grp1-grp2ge": "", + "TAG-Bob-grp1-ge": "", + "TAG-Bob-grp1-$": "", + "TAG-Bob-grp1-$1": "" }, - - "spec": { "TAG.*$*": "", - "TAG-*-*$*":"" + "TAG-*-*$*": "" }, - "expected": { - "TAG-Bob-grp1-grp2ge":"", - "TAG-Bob-grp1-ge":"", - "TAG-Bob-grp1-$":"" + "TAG-Bob-grp1-grp2ge": "", + "TAG-Bob-grp1-ge": "", + "TAG-Bob-grp1-$": "" } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/removr/negativeTestCases.json b/jolt-core/src/test/resources/json/removr/negativeTestCases.json index 9d519e1e..90cb1a50 100644 --- a/jolt-core/src/test/resources/json/removr/negativeTestCases.json +++ b/jolt-core/src/test/resources/json/removr/negativeTestCases.json @@ -1,13 +1,10 @@ { "input": { - "ineedtoberemoved":"eh?" + "ineedtoberemoved": "eh?" }, - - "spec": - { - "ineedtoberemoved":"eh?" + "spec": { + "ineedtoberemoved": "eh?" }, - "expected": { } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/removr/removrWithWildcardSupport.json b/jolt-core/src/test/resources/json/removr/removrWithWildcardSupport.json index 28256dd1..c37a5f32 100644 --- a/jolt-core/src/test/resources/json/removr/removrWithWildcardSupport.json +++ b/jolt-core/src/test/resources/json/removr/removrWithWildcardSupport.json @@ -1,95 +1,83 @@ - { "input": { "TAG-Sharpness$fr": "nettete", "TAG-Sharpness#fr_fr": "nettete", "TAG-Sharpness": "Sharpness", - "TAG-Bob": "smith", "TAG-Bob$ge": "", - "TAG.Bob$ge":"", - "TAG-Bob-grp1-grp2$ge":"", - "TAG-Bob-grp1-grp2ge":"", - "TAG-Bob-grp1-ge":"", - "TAG-Bob-grp1-$":"", - "TAG-Bob-grp1-$1":"", - "ThisIsSillypantsValue" : "should be delted", - + "TAG.Bob$ge": "", + "TAG-Bob-grp1-grp2$ge": "", + "TAG-Bob-grp1-grp2ge": "", + "TAG-Bob-grp1-ge": "", + "TAG-Bob-grp1-$": "", + "TAG-Bob-grp1-$1": "", + "ThisIsSillypantsValue": "should be delted", "buckets": { "a$b": "AB", "c$d": "cd", "bucket-a$b": "ab" }, - - "ratings":{ - "Set1":{ - "a":"a", - "b":"b" + "ratings": { + "Set1": { + "a": "a", + "b": "b" }, - "Set2":{ - "a":"a", - "b":"b" + "Set2": { + "a": "a", + "b": "b" } }, - - "ratings_duplicate":{ - "Set1":{ - "a":"a", - "b":"b" + "ratings_duplicate": { + "Set1": { + "a": "a", + "b": "b" }, - "Set2":{ - "a":"a", - "b":"b" + "Set2": { + "a": "a", + "b": "b" } } }, - - "spec": { "TAG-*$*": "", "TAG.*$*": "", "TAG-*#*": "", - "TAG-*-*$*":"", - "*pants*" : "", - + "TAG-*-*$*": "", + "*pants*": "", "buckets": { "a$*": "" }, - "rating*":{ - "*":{ - "a":"" + "rating*": { + "*": { + "a": "" } } }, - "expected": { "TAG-Sharpness": "Sharpness", "TAG-Bob": "smith", - "TAG-Bob-grp1-grp2ge":"", - "TAG-Bob-grp1-ge":"", - "TAG-Bob-grp1-$":"", - + "TAG-Bob-grp1-grp2ge": "", + "TAG-Bob-grp1-ge": "", + "TAG-Bob-grp1-$": "", "buckets": { "c$d": "cd", "bucket-a$b": "ab" }, - - "ratings":{ - "Set1":{ - "b":"b" + "ratings": { + "Set1": { + "b": "b" }, - "Set2":{ - "b":"b" + "Set2": { + "b": "b" } }, - "ratings_duplicate":{ - "Set1":{ - "b":"b" + "ratings_duplicate": { + "Set1": { + "b": "b" }, - "Set2":{ - "b":"b" + "Set2": { + "b": "b" } } - } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/removr/starDoublePathElementBoundaryConditions.json b/jolt-core/src/test/resources/json/removr/starDoublePathElementBoundaryConditions.json index 41409f74..258468b3 100644 --- a/jolt-core/src/test/resources/json/removr/starDoublePathElementBoundaryConditions.json +++ b/jolt-core/src/test/resources/json/removr/starDoublePathElementBoundaryConditions.json @@ -1,38 +1,35 @@ { "input": { - "cdv-2_2$en":"q3", - "cdv-BecomeMember$en":"", - "cdv-4$en":"", - "cdv-4":"", - "cdv-":"", - "cdv-$":"", - "cdv-1$":"", - "cdv-$1":"", - "":"", - "cdv-3_1":"", - "tag-DoShoutProductsFreeYouFro$en":"", - "tag-DoShoutProductsFreeYouFro":"", + "cdv-2_2$en": "q3", + "cdv-BecomeMember$en": "", + "cdv-4$en": "", + "cdv-4": "", + "cdv-": "", + "cdv-$": "", + "cdv-1$": "", + "cdv-$1": "", + "": "", + "cdv-3_1": "", + "tag-DoShoutProductsFreeYouFro$en": "", + "tag-DoShoutProductsFreeYouFro": "", "tag-3_2$en": [ "Cotton", "Other", "Denim" ] }, - "spec": { - "cdv-*$*":"", - "tag-*$*":"" + "cdv-*$*": "", + "tag-*$*": "" }, - "expected": { - "cdv-$":"", - "cdv-1$":"", - "cdv-$1":"", - "cdv-":"", - "":"", - "cdv-4":"", - "cdv-3_1":"", - "tag-DoShoutProductsFreeYouFro":"" - + "cdv-$": "", + "cdv-1$": "", + "cdv-$1": "", + "cdv-": "", + "": "", + "cdv-4": "", + "cdv-3_1": "", + "tag-DoShoutProductsFreeYouFro": "" } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/sample/spec.json b/jolt-core/src/test/resources/json/sample/spec.json index 5f19cf4c..f70a04c3 100644 --- a/jolt-core/src/test/resources/json/sample/spec.json +++ b/jolt-core/src/test/resources/json/sample/spec.json @@ -16,10 +16,10 @@ { "operation": "default", "spec": { - "Range" : 5, - "SecondaryRatings" : { - "*" : { - "Range" : 5 + "Range": 5, + "SecondaryRatings": { + "*": { + "Range": 5 } } } diff --git a/jolt-core/src/test/resources/json/shiftr/arrayExample.json b/jolt-core/src/test/resources/json/shiftr/arrayExample.json index 482d1e16..fd30a152 100644 --- a/jolt-core/src/test/resources/json/shiftr/arrayExample.json +++ b/jolt-core/src/test/resources/json/shiftr/arrayExample.json @@ -39,7 +39,6 @@ } ] }, - // We aren't radically transforming the data here, just changing the case of the keys. // This illustrates the usage of the '[]' and '&' operators, to put all the content in the right place, aka maintain order "spec": { @@ -56,7 +55,6 @@ } } }, - "expected": { "photos": [ { @@ -85,5 +83,4 @@ } ] } - } diff --git a/jolt-core/src/test/resources/json/shiftr/arrayMismatch.json b/jolt-core/src/test/resources/json/shiftr/arrayMismatch.json index b0dbb3db..b9e0fd65 100644 --- a/jolt-core/src/test/resources/json/shiftr/arrayMismatch.json +++ b/jolt-core/src/test/resources/json/shiftr/arrayMismatch.json @@ -4,11 +4,9 @@ // array mismatches "input": { - "id": "reviewId", "text": "This is a review.", - "rating" : 5, - + "rating": 5, "statistics": { "id": "statsId", "feedbackCount": "10" @@ -20,28 +18,23 @@ } ] }, - "spec": { - - "id" : "Id", - "text" : "ReviewText", - "rating" : "Rating", - - "statistics" : { - "0" : { - "feedbackCount" : "FeedbackCount" + "id": "Id", + "text": "ReviewText", + "rating": "Rating", + "statistics": { + "0": { + "feedbackCount": "FeedbackCount" } }, - "author" : { - "id" : "AuthorId", - "name" : "AuthorName" + "author": { + "id": "AuthorId", + "name": "AuthorName" } }, - "expected": { - - "Id" : "reviewId", - "ReviewText" : "This is a review.", - "Rating" : 5 + "Id": "reviewId", + "ReviewText": "This is a review.", + "Rating": 5 } } diff --git a/jolt-core/src/test/resources/json/shiftr/bucketToPrefixSoup.json b/jolt-core/src/test/resources/json/shiftr/bucketToPrefixSoup.json index bc1f21a1..5d809087 100644 --- a/jolt-core/src/test/resources/json/shiftr/bucketToPrefixSoup.json +++ b/jolt-core/src/test/resources/json/shiftr/bucketToPrefixSoup.json @@ -1,27 +1,24 @@ { - "input" : { - "Rating":1, + "input": { + "Rating": 1, // Given input all the entries below a field are the same type // We want to turn that into prefixed data - "SecondaryRatings":{ - "Design":4, - "Price":2, - "RatingDimension3":1 + "SecondaryRatings": { + "Design": 4, + "Price": 2, + "RatingDimension3": 1 } }, - - "spec" : { - "Rating" : "rating-primary", - - "SecondaryRatings" : { - "*" : "rating-&" + "spec": { + "Rating": "rating-primary", + "SecondaryRatings": { + "*": "rating-&" } }, - - "expected" : { - "rating-primary":1, + "expected": { + "rating-primary": 1, "rating-Design": 4, "rating-Price": 2, "rating-RatingDimension3": 1 } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/shiftr/declaredOutputArray.json b/jolt-core/src/test/resources/json/shiftr/declaredOutputArray.json index a199f004..851f8488 100644 --- a/jolt-core/src/test/resources/json/shiftr/declaredOutputArray.json +++ b/jolt-core/src/test/resources/json/shiftr/declaredOutputArray.json @@ -1,36 +1,49 @@ { - "input" : { - "tuna-a" : "A", - "tuna-b" : "B", - + "input": { + "tuna-a": "A", + "tuna-b": "B", "foo-a": "bar", - - "axis-x" : "horizontal" + "axis-x": "horizontal" }, - - "spec" : { + "spec": { "foo-*": "listOfFooValues[]", - - "tuna-*" : { - "@" : "listOfTunaValues[]", - "$(0,1)" : [ "listOfTunaIds[]", "sillyListOfTunaIds[].id" ] + "tuna-*": { + "@": "listOfTunaValues[]", + "$(0,1)": [ + "listOfTunaIds[]", + "sillyListOfTunaIds[].id" + ] }, - - "axis-*" : { - "@" : "listOfAxisValues[]", - "$(0,1)" : "listOfAxisIds[]" + "axis-*": { + "@": "listOfAxisValues[]", + "$(0,1)": "listOfAxisIds[]" } }, - - "expected" : { - "listOfFooValues" : [ "bar" ], - - "listOfTunaValues" : [ "A", "B" ], - "listOfTunaIds" : [ "a", "b" ], - - "sillyListOfTunaIds" : [ { "id" : "a" }, { "id" : "b" } ], - - "listOfAxisValues" : [ "horizontal" ], - "listOfAxisIds" : [ "x" ] + "expected": { + "listOfFooValues": [ + "bar" + ], + "listOfTunaValues": [ + "A", + "B" + ], + "listOfTunaIds": [ + "a", + "b" + ], + "sillyListOfTunaIds": [ + { + "id": "a" + }, + { + "id": "b" + } + ], + "listOfAxisValues": [ + "horizontal" + ], + "listOfAxisIds": [ + "x" + ] } } diff --git a/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings.json b/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings.json index 7c734499..542b0285 100644 --- a/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings.json +++ b/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings.json @@ -6,11 +6,10 @@ "[yield": "open array", "[]yield": "full array", "]yield": "back array", - "*" : "star", - "#" : "hash", - "(" : "left paren" + "*": "star", + "#": "hash", + "(": "left paren" }, - "spec": { // TEST escaping all the things when they are the first char(s) of the spec LHS and RHS "\\@context": "\\@A", @@ -19,18 +18,17 @@ "\\[yield": "\\[D", "\\[\\]yield": "\\[\\]E", "\\]yield": "\\]F", - "\\*" : "\\*G", - "\\#" : "\\#H" + "\\*": "\\*G", + "\\#": "\\#H" }, - "expected": { - "@A" : "atSymbol", - "$B" : "Mojito", - "&C" : "mint", + "@A": "atSymbol", + "$B": "Mojito", + "&C": "mint", "[D": "open array", - "[]E" : "full array", - "]F" : "back array", - "*G" : "star", - "#H" : "hash" + "[]E": "full array", + "]F": "back array", + "*G": "star", + "#H": "hash" } } diff --git a/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings2.json b/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings2.json index 29e88475..73c47e13 100644 --- a/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings2.json +++ b/jolt-core/src/test/resources/json/shiftr/escapeAllTheThings2.json @@ -6,11 +6,10 @@ "ddd[yield": "open array", "eee[]yield": "full array", "fff]yield": "back array", - "ggg*" : "star", - "hhh#" : "hash", - "yyy(" : "left paren" + "ggg*": "star", + "hhh#": "hash", + "yyy(": "left paren" }, - "spec": { // TEST escaping all the things when they are in the middle of the LHS and RHS keys "aaa\\@context": "aaa\\@A", @@ -19,18 +18,17 @@ "ddd\\[yield": "ddd\\[D", "eee\\[\\]yield": "eee\\[\\]E", "fff\\]yield": "fff\\]F", - "ggg\\*" : "ggg\\*G", - "hhh\\#" : "hhh\\#H" + "ggg\\*": "ggg\\*G", + "hhh\\#": "hhh\\#H" }, - "expected": { - "aaa@A" : "atSymbol", - "bbb$B" : "Mojito", - "ccc&C" : "mint", + "aaa@A": "atSymbol", + "bbb$B": "Mojito", + "ccc&C": "mint", "ddd[D": "open array", - "eee[]E" : "full array", - "fff]F" : "back array", - "ggg*G" : "star", - "hhh#H" : "hash" + "eee[]E": "full array", + "fff]F": "back array", + "ggg*G": "star", + "hhh#H": "hash" } } diff --git a/jolt-core/src/test/resources/json/shiftr/explicitArrayKey.json b/jolt-core/src/test/resources/json/shiftr/explicitArrayKey.json index 09a5ef91..a8759771 100644 --- a/jolt-core/src/test/resources/json/shiftr/explicitArrayKey.json +++ b/jolt-core/src/test/resources/json/shiftr/explicitArrayKey.json @@ -1,14 +1,15 @@ { "input": { - "Photos": [ "AAA.jpg", "BBB.jpg" ] + "Photos": [ + "AAA.jpg", + "BBB.jpg" + ] }, - "spec": { "Photos": { "1": "photo-&-url" } }, - "expected": { "photo-1-url": "BBB.jpg" } diff --git a/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json b/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json index 763f7821..436254ff 100644 --- a/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json +++ b/jolt-core/src/test/resources/json/shiftr/filterParallelArrays.json @@ -1,24 +1,38 @@ { "input": { - "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ], - "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ] + "states": [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas" + ], + "capitals": [ + "Montgomery", + "Juneau", + "Phoenix", + "Little Rock" + ] }, - "spec": { "states": { "*": { - "Ar*": { // only match states that start with "Ar" - // for those states, grab the captital, and use that as the output value + "Ar*": { + // only match states that start with "Ar" + // for those states, grab the captital, and use that as the output value "@(3,capitals[&1])": "capitals[]", - "$" : "states[]" + "$": "states[]" } } } }, - - "expected": { - "states" : [ "Arizona", "Arkansas" ], - "capitals" : [ "Phoenix" , "Little Rock" ] + "states": [ + "Arizona", + "Arkansas" + ], + "capitals": [ + "Phoenix", + "Little Rock" + ] } } diff --git a/jolt-core/src/test/resources/json/shiftr/filterParents1.json b/jolt-core/src/test/resources/json/shiftr/filterParents1.json index de53d32b..b7ba0e40 100644 --- a/jolt-core/src/test/resources/json/shiftr/filterParents1.json +++ b/jolt-core/src/test/resources/json/shiftr/filterParents1.json @@ -23,17 +23,19 @@ } ] }, - "spec": { "books": { "*": { "availability": { - "*": { // match all elements of the availability array - "paperback": { // if the word paperback exists match it - "@(3,title)": "PaperBacks" // Look up the tree 3 levels, then back down and grab the value for the "title" - // and write it to PaperBacks in the output + "*": { + // match all elements of the availability array + "paperback": { + // if the word paperback exists match it + "@(3,title)": "PaperBacks" + // Look up the tree 3 levels, then back down and grab the value for the "title" + // and write it to PaperBacks in the output }, - "online" : { + "online": { "@(3,title)": "Online" } } @@ -41,9 +43,14 @@ } } }, - "expected": { - "PaperBacks": [ "Scala", "Java" ], - "Online" : [ "JavaScript", "Scala" ] + "PaperBacks": [ + "Scala", + "Java" + ], + "Online": [ + "JavaScript", + "Scala" + ] } } diff --git a/jolt-core/src/test/resources/json/shiftr/filterParents2.json b/jolt-core/src/test/resources/json/shiftr/filterParents2.json index bbfeb241..4688d984 100644 --- a/jolt-core/src/test/resources/json/shiftr/filterParents2.json +++ b/jolt-core/src/test/resources/json/shiftr/filterParents2.json @@ -32,14 +32,16 @@ } ] }, - "spec": { "books": { "*": { "availability": { - "*": { // match all elements of the availability array - "*": { // match any availability type - "@(3,book.title)": "&[]" // Look up the tree 3 levels, then back down and grab the value for the "title" + "*": { + // match all elements of the availability array + "*": { + // match any availability type + "@(3,book.title)": "&[]" + // Look up the tree 3 levels, then back down and grab the value for the "title" // and write out to the top level } } @@ -47,9 +49,14 @@ } } }, - "expected": { - "paperback": [ "Scala", "Java" ], - "online": [ "JavaScript", "Scala" ] + "paperback": [ + "Scala", + "Java" + ], + "online": [ + "JavaScript", + "Scala" + ] } } diff --git a/jolt-core/src/test/resources/json/shiftr/filterParents3.json b/jolt-core/src/test/resources/json/shiftr/filterParents3.json index e9cabd06..8d00e645 100644 --- a/jolt-core/src/test/resources/json/shiftr/filterParents3.json +++ b/jolt-core/src/test/resources/json/shiftr/filterParents3.json @@ -28,7 +28,6 @@ } ] }, - "spec": { "Planet": { "*": { @@ -42,21 +41,20 @@ } } }, - "expected": { - "PlanetsWithMoons" : [ + "PlanetsWithMoons": [ { - "Satellite" : { - "PhysicalData" : { - "pd1" : "pdv1", - "pd2" : "pdv2" + "Satellite": { + "PhysicalData": { + "pd1": "pdv1", + "pd2": "pdv2" }, - "ChemicalData" : { - "cd1" : "cdv1", - "cd2" : "cdv2" + "ChemicalData": { + "cd1": "cdv1", + "cd2": "cdv2" } }, - "Name" : "Earth" + "Name": "Earth" } ] } diff --git a/jolt-core/src/test/resources/json/shiftr/firstSample.json b/jolt-core/src/test/resources/json/shiftr/firstSample.json index bcc9e40e..9b9052ba 100644 --- a/jolt-core/src/test/resources/json/shiftr/firstSample.json +++ b/jolt-core/src/test/resources/json/shiftr/firstSample.json @@ -11,19 +11,20 @@ } } }, - "spec": { "rating": { "primary": { - "value": "Rating", // simple match. Put the value '4' in the output under the "Rating" field + "value": "Rating", + // simple match. Put the value '4' in the output under the "Rating" field "max": "RatingRange" }, - // match any children of "rating". // Shiftr has a precendence order when matching, so the "*" will match anything that isn't "primary" "*": { - "value": "SecondaryRatings.&1.Value", // &1 means, go up one level and grab that value and substitute it in - "max": "SecondaryRatings.&1.Range", // in this example &1 = "quality" + "value": "SecondaryRatings.&1.Value", + // &1 means, go up one level and grab that value and substitute it in + "max": "SecondaryRatings.&1.Range", + // in this example &1 = "quality" "$": "SecondaryRatings.&1.Id" // we want "quality" to be a value field in the output under @@ -32,13 +33,14 @@ } } }, - "expected": { "Rating": 4, "RatingRange": 5, "SecondaryRatings": { - "quality": { // created by the '"value": "SecondaryRatings.&1.Value"' or "max": "SecondaryRatings.&1.Range" lines - "Id": "quality", // created by the '"$": "SecondaryRatings.&1.Id"' from the spec + "quality": { + // created by the '"value": "SecondaryRatings.&1.Value"' or "max": "SecondaryRatings.&1.Range" lines + "Id": "quality", + // created by the '"$": "SecondaryRatings.&1.Id"' from the spec "Value": 3, "Range": 7 } diff --git a/jolt-core/src/test/resources/json/shiftr/hashDefault.json b/jolt-core/src/test/resources/json/shiftr/hashDefault.json index 3341457c..07361994 100644 --- a/jolt-core/src/test/resources/json/shiftr/hashDefault.json +++ b/jolt-core/src/test/resources/json/shiftr/hashDefault.json @@ -1,37 +1,36 @@ { "input": { - "data" : { - "1234" : { + "data": { + "1234": { "clientId": "12", - "hidden" : true + "hidden": true }, - "1235" : { + "1235": { "clientId": "35", - "hidden" : false + "hidden": false } } }, - "spec": { - "data" : { - "*" : { - "hidden" : { - "true" : { // if hidden is true, then write the value disabled to the RHS output path - // Also @(3,clientId) means go up 3 levels, to the "1234" or "1235" level, then lookup / down the tree for the value of "clientId" - "#disabled" : "clients.@(3,clientId)" + "data": { + "*": { + "hidden": { + "true": { + // if hidden is true, then write the value disabled to the RHS output path + // Also @(3,clientId) means go up 3 levels, to the "1234" or "1235" level, then lookup / down the tree for the value of "clientId" + "#disabled": "clients.@(3,clientId)" }, - "false" : { - "#enabled" : "clients.@(3,clientId)" + "false": { + "#enabled": "clients.@(3,clientId)" } } } } }, - "expected": { - "clients" : { - "12" : "disabled", - "35" : "enabled" + "clients": { + "12": "disabled", + "35": "enabled" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/identity.json b/jolt-core/src/test/resources/json/shiftr/identity.json index 7f1637a6..4c6fe21e 100644 --- a/jolt-core/src/test/resources/json/shiftr/identity.json +++ b/jolt-core/src/test/resources/json/shiftr/identity.json @@ -11,11 +11,9 @@ } } }, - "spec": { "@": "" }, - "expected": { "rating": { "primary": { diff --git a/jolt-core/src/test/resources/json/shiftr/inputArrayToPrefix.json b/jolt-core/src/test/resources/json/shiftr/inputArrayToPrefix.json index 06b6768b..c64ffb19 100644 --- a/jolt-core/src/test/resources/json/shiftr/inputArrayToPrefix.json +++ b/jolt-core/src/test/resources/json/shiftr/inputArrayToPrefix.json @@ -13,7 +13,6 @@ } ] }, - "spec": { "Photos": { "*": { @@ -23,12 +22,10 @@ } } }, - "expected": { "photo-0-id": "327703", "photo-0-caption": "TEST>> photo 1", "photo-0-url": "http://bob.com/0001/327703/photo.jpg", - "photo-1-id": "327704", "photo-1-caption": "TEST>> photo 2", "photo-1-url": "http://bob.com/0001/327704/photo.jpg" diff --git a/jolt-core/src/test/resources/json/shiftr/invertMap.json b/jolt-core/src/test/resources/json/shiftr/invertMap.json index f3bece03..76a8afd8 100644 --- a/jolt-core/src/test/resources/json/shiftr/invertMap.json +++ b/jolt-core/src/test/resources/json/shiftr/invertMap.json @@ -1,7 +1,6 @@ { "comment0": "Given a catalogConfig that tells the fields configured on a category.", "comment1": "I want to invert it and figure out, for each field which categories use it.", - "input": { "catalogConfig": { "ROOT": { @@ -11,7 +10,7 @@ "Gender", "Quality" ], - "question" : [ + "question": [ "Gender" ] } @@ -29,7 +28,7 @@ "review": [ "Quality" ], - "question" : [ + "question": [ "Age" ] } @@ -41,10 +40,11 @@ "*": { "fields": { "*": { - "*" : { + "*": { // We need to match the leaf levels of the inputs, and then use the $ wildcard, to pull the key of our parent - "*" : { - "$4" : "&1.&3.[]" // trailing [] tells Shiftr to make an array, even if there is only one output + "*": { + "$4": "&1.&3.[]" + // trailing [] tells Shiftr to make an array, even if there is only one output } } } @@ -52,17 +52,31 @@ } } }, - "expected" : { + "expected": { "Age": { - "review" : [ "ROOT", "category7" ], - "question" : [ "category8" ] + "review": [ + "ROOT", + "category7" + ], + "question": [ + "category8" + ] }, "Gender": { - "review" : [ "ROOT" ], // the trailing [] in the spec, is why "ROOT" is in an array by itself - "question" : [ "ROOT" ] + "review": [ + "ROOT" + ], + // the trailing [] in the spec, is why "ROOT" is in an array by itself + "question": [ + "ROOT" + ] }, "Quality": { - "review" : [ "ROOT", "category7", "category8" ] + "review": [ + "ROOT", + "category7", + "category8" + ] } } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/shiftr/json-ld-escaping.json b/jolt-core/src/test/resources/json/shiftr/json-ld-escaping.json index 7524b8f6..65dce457 100644 --- a/jolt-core/src/test/resources/json/shiftr/json-ld-escaping.json +++ b/jolt-core/src/test/resources/json/shiftr/json-ld-escaping.json @@ -46,7 +46,6 @@ } ] }, - "spec": { // In this test / example, we want to change some of the LHS keys in this Json document. // To accomplish this we escape the leading '@' chars both as spec matches (left hand side) @@ -56,14 +55,13 @@ "ingredient": "&1.Inputs", "yield": "\\@context.Makes", // pass the rest thru - "*" : "&1.&" + "*": "&1.&" }, "name": "Name", "ingredient": "Inputs", "yield": "Makes", - "*" : "&" + "*": "&" }, - "expected": { "@context": { "Name": "http://rdf.data-vocabulary.org/#name", diff --git a/jolt-core/src/test/resources/json/shiftr/keyref.json b/jolt-core/src/test/resources/json/shiftr/keyref.json index b8cf2de3..509cc5d5 100644 --- a/jolt-core/src/test/resources/json/shiftr/keyref.json +++ b/jolt-core/src/test/resources/json/shiftr/keyref.json @@ -11,7 +11,6 @@ } } }, - "spec": { "rating": { "primary": { @@ -19,7 +18,6 @@ } } }, - "expected": { "value": 3, "max": 5 diff --git a/jolt-core/src/test/resources/json/shiftr/lhsAmpMatch.json b/jolt-core/src/test/resources/json/shiftr/lhsAmpMatch.json index d740a1cb..6928cc70 100644 --- a/jolt-core/src/test/resources/json/shiftr/lhsAmpMatch.json +++ b/jolt-core/src/test/resources/json/shiftr/lhsAmpMatch.json @@ -13,7 +13,6 @@ } } }, - "spec": { "rating": { "*": { @@ -22,7 +21,6 @@ } } }, - "expected": { "ratings": { "primary": { diff --git a/jolt-core/src/test/resources/json/shiftr/listKeys.json b/jolt-core/src/test/resources/json/shiftr/listKeys.json index cc1962e9..848c5d29 100644 --- a/jolt-core/src/test/resources/json/shiftr/listKeys.json +++ b/jolt-core/src/test/resources/json/shiftr/listKeys.json @@ -11,7 +11,6 @@ } } }, - "spec": { "rating": { "*": { @@ -19,8 +18,10 @@ } } }, - "expected": { - "ratings": [ "primary", "quality" ] + "ratings": [ + "primary", + "quality" + ] } } diff --git a/jolt-core/src/test/resources/json/shiftr/mapToList.json b/jolt-core/src/test/resources/json/shiftr/mapToList.json index e8530b51..2f7a7c3e 100644 --- a/jolt-core/src/test/resources/json/shiftr/mapToList.json +++ b/jolt-core/src/test/resources/json/shiftr/mapToList.json @@ -5,7 +5,6 @@ "quality": 4 } }, - "spec": { "ratings": { "*": { @@ -16,7 +15,6 @@ } } }, - "expected": { "Ratings": [ { diff --git a/jolt-core/src/test/resources/json/shiftr/mapToList2.json b/jolt-core/src/test/resources/json/shiftr/mapToList2.json index ce30a777..7ebf6f29 100644 --- a/jolt-core/src/test/resources/json/shiftr/mapToList2.json +++ b/jolt-core/src/test/resources/json/shiftr/mapToList2.json @@ -11,15 +11,16 @@ } } }, - "comment0": "Shiftr was weak on transforming Maps to lists.", "comment1": "We introduced the # to the RHS so we can do it.", "comment2": "It means each node above creates an integer index.", - "spec": { "cdvHisto": { "*": { - "$": [ "ContextDataDistribution.&1.Id", "ContextDataDistribution.&1.Label" ], + "$": [ + "ContextDataDistribution.&1.Id", + "ContextDataDistribution.&1.Label" + ], "*": { "@": "ContextDataDistribution.&2.Values.[#2].Count", "$": "ContextDataDistribution.&2.Values.[#2].Value" @@ -27,7 +28,6 @@ } } }, - "expected": { "ContextDataDistribution": { "Expertise": { diff --git a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json index 05bf9d1d..c402e54b 100644 --- a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json +++ b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays1_and-transpose.json @@ -1,11 +1,22 @@ { "input": { - "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ], - "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ] + "states": [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas" + ], + "capitals": [ + "Montgomery", + "Juneau", + "Phoenix", + "Little Rock" + ] }, - - "spec": { // Level 2 : the root - "capitals": { // Level 1 : capitals + "spec": { + // Level 2 : the root + "capitals": { + // Level 1 : capitals // Write out the "value" of each array index, aka 0->Montgomery, so the "Montgomery" part // to "states.Alabama" where "Alabama" is 2 levels up the tree, @@ -13,13 +24,12 @@ "*": "states.@(2,states[&])" } }, - "expected": { - "states" : { - "Alabama" : "Montgomery", - "Alaska" : "Juneau", - "Arizona" : "Phoenix", - "Arkansas" : "Little Rock" + "states": { + "Alabama": "Montgomery", + "Alaska": "Juneau", + "Arizona": "Phoenix", + "Arkansas": "Little Rock" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json index a3b27c28..8d7920f6 100644 --- a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json +++ b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays2_and-do-not-transpose.json @@ -1,9 +1,18 @@ { "input": { - "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ], - "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ] + "states": [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas" + ], + "capitals": [ + "Montgomery", + "Juneau", + "Phoenix", + "Little Rock" + ] }, - "spec": { "states": { "*": { @@ -20,25 +29,23 @@ } } }, - - "expected": { - "states" : [ + "states": [ { - "state" : "Alabama", - "capital" : "Montgomery" + "state": "Alabama", + "capital": "Montgomery" }, { - "state" : "Alaska", - "capital" : "Juneau" + "state": "Alaska", + "capital": "Juneau" }, { - "state" : "Arizona", - "capital" : "Phoenix" + "state": "Arizona", + "capital": "Phoenix" }, { - "state" : "Arkansas", - "capital" : "Little Rock" + "state": "Arkansas", + "capital": "Little Rock" } ] } diff --git a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json index f6ca9386..49555851 100644 --- a/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json +++ b/jolt-core/src/test/resources/json/shiftr/mergeParallelArrays3_and-filter.json @@ -1,25 +1,33 @@ { "input": { - "states" : [ "Alabama", "Alaska", "Arizona", "Arkansas" ], - "capitals" : [ "Montgomery", "Juneau", "Phoenix" , "Little Rock" ] + "states": [ + "Alabama", + "Alaska", + "Arizona", + "Arkansas" + ], + "capitals": [ + "Montgomery", + "Juneau", + "Phoenix", + "Little Rock" + ] }, - "spec": { "states": { "*": { - "Ar*": { // only match states that start with "Ar" - // for those states, grab the captital, and use that as the output value + "Ar*": { + // only match states that start with "Ar" + // for those states, grab the captital, and use that as the output value "@(3,capitals[&1])": "states.&1" } } } }, - - "expected": { - "states" : { - "Arizona" : "Phoenix", - "Arkansas" : "Little Rock" + "states": { + "Arizona": "Phoenix", + "Arkansas": "Little Rock" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/multiPlacement.json b/jolt-core/src/test/resources/json/shiftr/multiPlacement.json index 431cb454..e428b397 100644 --- a/jolt-core/src/test/resources/json/shiftr/multiPlacement.json +++ b/jolt-core/src/test/resources/json/shiftr/multiPlacement.json @@ -2,11 +2,12 @@ "input": { "foo": "bar" }, - "spec": { - "foo": ["a", "b"] + "foo": [ + "a", + "b" + ] }, - "expected": { "a": "bar", "b": "bar" diff --git a/jolt-core/src/test/resources/json/shiftr/objectToArray.json b/jolt-core/src/test/resources/json/shiftr/objectToArray.json index 877bfbee..89624489 100644 --- a/jolt-core/src/test/resources/json/shiftr/objectToArray.json +++ b/jolt-core/src/test/resources/json/shiftr/objectToArray.json @@ -11,13 +11,11 @@ } } }, - "spec": { "rating": { "*": "" } }, - "expected": [ { "value": 3, diff --git a/jolt-core/src/test/resources/json/shiftr/passNullThru.json b/jolt-core/src/test/resources/json/shiftr/passNullThru.json index 82ee550f..c69eb66f 100644 --- a/jolt-core/src/test/resources/json/shiftr/passNullThru.json +++ b/jolt-core/src/test/resources/json/shiftr/passNullThru.json @@ -1,27 +1,26 @@ { "input": { "key": null, - "bunch-O-keys" : { - "a" : null, - "b" : null, - "c" : null + "bunch-O-keys": { + "a": null, + "b": null, + "c": null } }, - "spec": { "key": "value", - "bunch-O-keys" : { - "*" : "values.&", - "@(1,notLegit)" : "notLegit" // verify that if we lookup something that does not exist we do not write out a null + "bunch-O-keys": { + "*": "values.&", + "@(1,notLegit)": "notLegit" + // verify that if we lookup something that does not exist we do not write out a null } }, - "expected": { - "value" : null, - "values" : { - "a" : null, - "b" : null, - "c" : null + "value": null, + "values": { + "a": null, + "b": null, + "c": null } } } diff --git a/jolt-core/src/test/resources/json/shiftr/passThru.json b/jolt-core/src/test/resources/json/shiftr/passThru.json index 88fba56b..824aa5f4 100644 --- a/jolt-core/src/test/resources/json/shiftr/passThru.json +++ b/jolt-core/src/test/resources/json/shiftr/passThru.json @@ -1,10 +1,8 @@ { "input": { - "id": "reviewId", "text": "This is a review.", - "rating" : 5, - + "rating": 5, "statistics": { "id": "statsId", "feedbackCount": "10" @@ -13,42 +11,39 @@ { "id": "authorId", "name": "Author Name", - "other" : "this should not pass thru" + "other": "this should not pass thru" } ] }, - // I want to // 1) "rename" statistics // 2) pass thru author id // 3) pass thru everything else "spec": { + "statistics": "stats", + // "rename" statistics -> stats - "statistics" : "stats", // "rename" statistics -> stats - - "id|text|rating" : "&", // This is nice and compact. Good use of the "|" LHS operator. + "id|text|rating": "&", + // This is nice and compact. Good use of the "|" LHS operator. - "author" : { - "*" :{ - "id|name" : "&2.[&1].&" // This is a bit hokey, in that we need to enumerate the entire tree that we came down. - // Note this could have been "author[&1].&" + "author": { + "*": { + "id|name": "&2.[&1].&" + // This is a bit hokey, in that we need to enumerate the entire tree that we came down. + // Note this could have been "author[&1].&" } } }, - "expected": { - "id": "reviewId", "text": "This is a review.", - "rating" : 5, - + "rating": 5, "author": [ { "id": "authorId", "name": "Author Name" } ], - "stats": { "id": "statsId", "feedbackCount": "10" diff --git a/jolt-core/src/test/resources/json/shiftr/pollaxman_218_duplicate_speclines_bug.json b/jolt-core/src/test/resources/json/shiftr/pollaxman_218_duplicate_speclines_bug.json index 3bca9739..a5b39bba 100644 --- a/jolt-core/src/test/resources/json/shiftr/pollaxman_218_duplicate_speclines_bug.json +++ b/jolt-core/src/test/resources/json/shiftr/pollaxman_218_duplicate_speclines_bug.json @@ -1,34 +1,34 @@ { - "input" : { - "clone1_Physician" : "Physician_10000", - "clone2_Physician" : "Physician_10000", - "clone1_GCPerProIdenInfoPhyInfoStreet" : "Street", - "clone1_GCPerProIdenInfoPhyInfoAddLin2" : "Address1", - "clone1_GCPerProIdenInfoPhyInfoAddLin3" : "Address3", - "clone2_GCPerProIdenInfoPhyInfoStreet" : "Address1", - "clone2_GCPerProIdenInfoPhyInfoAddLin2" : "addresdd" + "input": { + "clone1_Physician": "Physician_10000", + "clone2_Physician": "Physician_10000", + "clone1_GCPerProIdenInfoPhyInfoStreet": "Street", + "clone1_GCPerProIdenInfoPhyInfoAddLin2": "Address1", + "clone1_GCPerProIdenInfoPhyInfoAddLin3": "Address3", + "clone2_GCPerProIdenInfoPhyInfoStreet": "Address1", + "clone2_GCPerProIdenInfoPhyInfoAddLin2": "addresdd" }, - "spec" : { + "spec": { // match clone1_Physician and clone2_Physician - "clone*_Physician" : { + "clone*_Physician": { // match the RHS of Physician_10000 - "Physician_1000*" : { + "Physician_1000*": { // if we matched then grab the "number" of the physician and use that to process the address - "@(2,clone&(1,1)_GCPerProIdenInfoPhyInfoStreet)" : "Physician&(2,1).street", - "@(2,clone&(1,1)_GCPerProIdenInfoPhyInfoAddLin2)" : "Physician&(2,1).adderLin2", - "@(2,clone&(1,1)_GCPerProIdenInfoPhyInfoAddLin3)" : "Physician&(2,1).adderLin3" + "@(2,clone&(1,1)_GCPerProIdenInfoPhyInfoStreet)": "Physician&(2,1).street", + "@(2,clone&(1,1)_GCPerProIdenInfoPhyInfoAddLin2)": "Physician&(2,1).adderLin2", + "@(2,clone&(1,1)_GCPerProIdenInfoPhyInfoAddLin3)": "Physician&(2,1).adderLin3" } } }, - "expected" : { - "Physician1" : { - "street" : "Street", - "adderLin2" : "Address1", - "adderLin3" : "Address3" + "expected": { + "Physician1": { + "street": "Street", + "adderLin2": "Address1", + "adderLin3": "Address3" }, - "Physician2" : { - "street" : "Address1", - "adderLin2" : "addresdd" + "Physician2": { + "street": "Address1", + "adderLin2": "addresdd" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/prefixDataToArray.json b/jolt-core/src/test/resources/json/shiftr/prefixDataToArray.json index 8ce9a3d9..d4ea651d 100644 --- a/jolt-core/src/test/resources/json/shiftr/prefixDataToArray.json +++ b/jolt-core/src/test/resources/json/shiftr/prefixDataToArray.json @@ -3,20 +3,17 @@ "photo-1-id": "327704", "photo-1-url": "http://bob.com/0001/327704/photo.jpg" }, - "spec": { "photo-1-id": "Photos[1].Id", "photo-1-url": "Photos[1].Url" }, - "expected": { "Photos": [ - null , + null, { "Id": "327704", "Url": "http://bob.com/0001/327704/photo.jpg" } ] } - } diff --git a/jolt-core/src/test/resources/json/shiftr/prefixSoupToBuckets.json b/jolt-core/src/test/resources/json/shiftr/prefixSoupToBuckets.json index dc8acb0f..5537073d 100644 --- a/jolt-core/src/test/resources/json/shiftr/prefixSoupToBuckets.json +++ b/jolt-core/src/test/resources/json/shiftr/prefixSoupToBuckets.json @@ -6,11 +6,9 @@ "rating-Design": 4, "rating-RatingDimension3": 1 }, - "spec": { // match one explicitly "rating-primary": "Rating", - // match the rest using the * wildcard. "rating-*": "SecondaryRatings.&(0,1)" // Assuming "rating-Price" : @@ -18,7 +16,6 @@ // "SecondaryRatings.&0" = "rating-Price" // the whole key, explict about how far up the tree to look, aka 0 // "SecondaryRatings.&(0,1)" = "Price" // the first * in the key &0 levels up the intput tree }, - "expected": { "Rating": 1, "SecondaryRatings": { diff --git a/jolt-core/src/test/resources/json/shiftr/prefixedData.json b/jolt-core/src/test/resources/json/shiftr/prefixedData.json index 60f2a62a..2d4c6c3d 100644 --- a/jolt-core/src/test/resources/json/shiftr/prefixedData.json +++ b/jolt-core/src/test/resources/json/shiftr/prefixedData.json @@ -1,32 +1,42 @@ { - "input" : { - "data" : { - "tunaOrder":["tuna-Age", "tuna-Tone", "tuna-Color"], - "marlinOrder" : [ "marlin-Spear" ] + "input": { + "data": { + "tunaOrder": [ + "tuna-Age", + "tuna-Tone", + "tuna-Color" + ], + "marlinOrder": [ + "marlin-Spear" + ] } }, - - "spec" : { - "data" : { - "tunaOrder" : { - "*" : { - "tuna-*" : { - "$(0,1)" : "TunaFishOrder[]" + "spec": { + "data": { + "tunaOrder": { + "*": { + "tuna-*": { + "$(0,1)": "TunaFishOrder[]" } } }, - "marlinOrder" : { - "*" : { - "marlin-*" : { - "$(0,1)" : "MarlinFishOrder[]" + "marlinOrder": { + "*": { + "marlin-*": { + "$(0,1)": "MarlinFishOrder[]" } } } } }, - - "expected" : { - "TunaFishOrder":["Age", "Tone", "Color"], - "MarlinFishOrder" : [ "Spear" ] + "expected": { + "TunaFishOrder": [ + "Age", + "Tone", + "Color" + ], + "MarlinFishOrder": [ + "Spear" + ] } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/shiftr/queryMappingXform.json b/jolt-core/src/test/resources/json/shiftr/queryMappingXform.json index c4b3212c..76c47935 100644 --- a/jolt-core/src/test/resources/json/shiftr/queryMappingXform.json +++ b/jolt-core/src/test/resources/json/shiftr/queryMappingXform.json @@ -1,65 +1,198 @@ { "input": { "review": { - "id": [ "ref.self.id", "text" ], - "authorid": [ "ref.author.id", "text" ], - "campaignid": [ "wholeText.campaignId", "text" ], - - "categoryancestorid": [ "wholeText.BANANA", "text" ], - - "contentlocale": [ "wholeText.contentLocale", "text" ], - "hascomments": [ "boolean.hasComments", "boolean" ], - "hasphotos": [ "boolean.hasPhotos", "boolean" ], - "hastags": [ "boolean.hasTags", "boolean" ], - "hasvideos": [ "boolean.hasVideos", "boolean" ], - "helpfulness": [ "double.helpfulness", "double" ], - "isfeatured": [ "boolean.featured", "boolean" ], - "isratingsonly": [ "boolean.ratingsOnly", "boolean" ], - "isrecommended": [ "boolean.recommended", "boolean" ], - "issubjectactive": [ "boolean.subjectActive", "boolean" ], - "issyndicated": [ "boolean.syndicated", "boolean" ], - "lastmoderatedtime": [ "datetime.lastmoderatedtime", "datetime" ], - "lastmodificationtime": [ "datetime.lastModificationTime", "datetime" ], - - "moderatorcode": [ "BANANA.BANANA", "text" ], - - "productid": [ "wholeText.subjectExternalId", "text" ], - "rating": [ "rating.primary", "integer" ], - "submissionid": [ "wholeText.submissionId", "text" ], - "submissiontime": [ "datetime.submissionTime", "datetime" ], - "totalcommentcount": [ "integer.totalCommentCount", "integer" ], - "totalfeedbackcount": [ "integer.totalFeedbackCount", "integer" ], - "totalnegativefeedbackcount": [ "integer.totalNegativeFeedbackCount", "integer" ], - "totalpositivefeedbackcount": [ "integer.totalPositiveFeedbackCount", "integer" ], - - "userlocation": [ "wholeText.geographicOrigin", "text" ], - - "additionalfield": [ "field.&", "text" ], - "contextdatavalue": [ "cdv.&", "text" ], - "tag": [ "tag.&", "text" ], - "secondaryrating": [ "rating.&", "integer" ] + "id": [ + "ref.self.id", + "text" + ], + "authorid": [ + "ref.author.id", + "text" + ], + "campaignid": [ + "wholeText.campaignId", + "text" + ], + "categoryancestorid": [ + "wholeText.BANANA", + "text" + ], + "contentlocale": [ + "wholeText.contentLocale", + "text" + ], + "hascomments": [ + "boolean.hasComments", + "boolean" + ], + "hasphotos": [ + "boolean.hasPhotos", + "boolean" + ], + "hastags": [ + "boolean.hasTags", + "boolean" + ], + "hasvideos": [ + "boolean.hasVideos", + "boolean" + ], + "helpfulness": [ + "double.helpfulness", + "double" + ], + "isfeatured": [ + "boolean.featured", + "boolean" + ], + "isratingsonly": [ + "boolean.ratingsOnly", + "boolean" + ], + "isrecommended": [ + "boolean.recommended", + "boolean" + ], + "issubjectactive": [ + "boolean.subjectActive", + "boolean" + ], + "issyndicated": [ + "boolean.syndicated", + "boolean" + ], + "lastmoderatedtime": [ + "datetime.lastmoderatedtime", + "datetime" + ], + "lastmodificationtime": [ + "datetime.lastModificationTime", + "datetime" + ], + "moderatorcode": [ + "BANANA.BANANA", + "text" + ], + "productid": [ + "wholeText.subjectExternalId", + "text" + ], + "rating": [ + "rating.primary", + "integer" + ], + "submissionid": [ + "wholeText.submissionId", + "text" + ], + "submissiontime": [ + "datetime.submissionTime", + "datetime" + ], + "totalcommentcount": [ + "integer.totalCommentCount", + "integer" + ], + "totalfeedbackcount": [ + "integer.totalFeedbackCount", + "integer" + ], + "totalnegativefeedbackcount": [ + "integer.totalNegativeFeedbackCount", + "integer" + ], + "totalpositivefeedbackcount": [ + "integer.totalPositiveFeedbackCount", + "integer" + ], + "userlocation": [ + "wholeText.geographicOrigin", + "text" + ], + "additionalfield": [ + "field.&", + "text" + ], + "contextdatavalue": [ + "cdv.&", + "text" + ], + "tag": [ + "tag.&", + "text" + ], + "secondaryrating": [ + "rating.&", + "integer" + ] }, "product": { - "id": [ "wholeText.externalId", "text" ], - "averageoverallrating": [ "double.averagePrimaryRating", "double" ], - "categoryancestorid": [ "BANANA", "BANANA" ], - "categoryid": [ "wholeText.categoryExternalId", "text" ], - "isactive": [ "boolean.active", "boolean" ], - "isdisabled": [ "boolean.disabled", "boolean" ], - "lastanswertime": [ "datetime.lastAnswerTime", "datetime" ], - "lastquestiontime": [ "datetime.lastQuestionTime", "datetime" ], - "lastreviewtime": [ "datetime.lastReviewTime", "datetime" ], - "laststorytime": [ "datetime.lastReviewTime", "datetime" ], - "name": [ "wholeText.name", "text" ], - "ratingsonlyreviewcount": [ "integer.ratingsOnlyReviewCount", "integer" ], - "totalanswercount": [ "integer.totalAnswerCount", "integer" ], - "totalquestioncount": [ "integer.totalQuestionCount", "integer" ], - "totalreviewcount": [ "integer.totalReviewCount", "integer" ], - "totalstorycount": [ "integer.totalStoryCount", "integer" ] + "id": [ + "wholeText.externalId", + "text" + ], + "averageoverallrating": [ + "double.averagePrimaryRating", + "double" + ], + "categoryancestorid": [ + "BANANA", + "BANANA" + ], + "categoryid": [ + "wholeText.categoryExternalId", + "text" + ], + "isactive": [ + "boolean.active", + "boolean" + ], + "isdisabled": [ + "boolean.disabled", + "boolean" + ], + "lastanswertime": [ + "datetime.lastAnswerTime", + "datetime" + ], + "lastquestiontime": [ + "datetime.lastQuestionTime", + "datetime" + ], + "lastreviewtime": [ + "datetime.lastReviewTime", + "datetime" + ], + "laststorytime": [ + "datetime.lastReviewTime", + "datetime" + ], + "name": [ + "wholeText.name", + "text" + ], + "ratingsonlyreviewcount": [ + "integer.ratingsOnlyReviewCount", + "integer" + ], + "totalanswercount": [ + "integer.totalAnswerCount", + "integer" + ], + "totalquestioncount": [ + "integer.totalQuestionCount", + "integer" + ], + "totalreviewcount": [ + "integer.totalReviewCount", + "integer" + ], + "totalstorycount": [ + "integer.totalStoryCount", + "integer" + ] } }, - - "spec": { "*": { "*": { @@ -68,8 +201,6 @@ } } }, - - "expected": { "product": { "totalquestioncount": { diff --git a/jolt-core/src/test/resources/json/shiftr/shiftToTrash.json b/jolt-core/src/test/resources/json/shiftr/shiftToTrash.json index 96e134fd..9860aa32 100644 --- a/jolt-core/src/test/resources/json/shiftr/shiftToTrash.json +++ b/jolt-core/src/test/resources/json/shiftr/shiftToTrash.json @@ -1,28 +1,25 @@ { "input": { - "a" : "a", - "b" : "b", - "c" : "c", - "d" : "d" + "a": "a", + "b": "b", + "c": "c", + "d": "d" }, - "spec": { - // The idea here is that I want to send everything BUT the "c" input to "foo.&". // // The way to do that is to specify the "c" key, but have it's RHS output path be null. // Shiftr will then match the literal "c" (and not do anything with it's input) before // it matches the rest of the input keys with the "*", thus accomplishing the goal. // - "*" : "foo.&", - "c" : null + "*": "foo.&", + "c": null }, - "expected": { - "foo" : { - "a" : "a", - "b" : "b", - "d" : "d" + "foo": { + "a": "a", + "b": "b", + "d": "d" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/simpleLHSEscape.json b/jolt-core/src/test/resources/json/shiftr/simpleLHSEscape.json index 24698919..4460bb25 100644 --- a/jolt-core/src/test/resources/json/shiftr/simpleLHSEscape.json +++ b/jolt-core/src/test/resources/json/shiftr/simpleLHSEscape.json @@ -15,7 +15,6 @@ } } }, - "spec": { "\\@rating": { "\\$primary": { @@ -27,12 +26,11 @@ } } }, - "expected": { - "data" : { - "$rating" : 4, - "$rating-&quality" : 3, - "$rating-#sharpness" : 5 + "data": { + "$rating": 4, + "$rating-&quality": 3, + "$rating-#sharpness": 5 } } } diff --git a/jolt-core/src/test/resources/json/shiftr/simpleRHSEscape.json b/jolt-core/src/test/resources/json/shiftr/simpleRHSEscape.json index f86badb9..0ebc1962 100644 --- a/jolt-core/src/test/resources/json/shiftr/simpleRHSEscape.json +++ b/jolt-core/src/test/resources/json/shiftr/simpleRHSEscape.json @@ -10,17 +10,14 @@ "max": 7 } }, - // Test if we process input data with a "." in it - "foo.bar" : "baz", - + "foo.bar": "baz", // Test if we can use the @ transpose operator to get to the "baz" value above - "test" : { - "use_as_data" : "data", - "use_as_path" : "path" + "test": { + "use_as_data": "data", + "use_as_path": "path" } }, - "spec": { "rating": { "primary": { @@ -31,26 +28,24 @@ "value": "data.rating\\.&1" } }, - - "foo.bar" : "foobar", - "test" : { - "use_as_data" : { - "data" : { - "@(3,foo\\.bar)" : "test.data" + "foo.bar": "foobar", + "test": { + "use_as_data": { + "data": { + "@(3,foo\\.bar)": "test.data" } }, - "use_as_path" : "test.@(2,foo\\.bar)" + "use_as_path": "test.@(2,foo\\.bar)" } }, - "expected": { - "data" : { + "data": { "rating.primary": 4, "rating.quality": 3 }, - "foobar" : "baz", + "foobar": "baz", "test": { - "data" : "baz", + "data": "baz", "baz": "path" } } diff --git a/jolt-core/src/test/resources/json/shiftr/singlePlacement.json b/jolt-core/src/test/resources/json/shiftr/singlePlacement.json index b3abd408..76b99322 100644 --- a/jolt-core/src/test/resources/json/shiftr/singlePlacement.json +++ b/jolt-core/src/test/resources/json/shiftr/singlePlacement.json @@ -2,11 +2,9 @@ "input": { "foo": "bar" }, - "spec": { "foo": "a.b.c" }, - "expected": { "a": { "b": { diff --git a/jolt-core/src/test/resources/json/shiftr/specialKeys.json b/jolt-core/src/test/resources/json/shiftr/specialKeys.json index c1994fc1..138dfd19 100644 --- a/jolt-core/src/test/resources/json/shiftr/specialKeys.json +++ b/jolt-core/src/test/resources/json/shiftr/specialKeys.json @@ -5,19 +5,27 @@ "baz": 15, "tuna": 9 }, - "spec": { "*": { "@": "a.b.originalValues", "$": "a.b.originalKeys" } }, - "expected": { "a": { "b": { - "originalKeys": ["foo", "bar", "baz", "tuna"], - "originalValues": [3, 14, 15, 9] + "originalKeys": [ + "foo", + "bar", + "baz", + "tuna" + ], + "originalValues": [ + 3, + 14, + 15, + 9 + ] } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json index 0c74b3d8..f9d3c2a7 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents1.json @@ -1,6 +1,6 @@ { "input": { - "photos" : [ + "photos": [ { "id": "1234", "fileName": "Acme.jpg" @@ -11,22 +11,20 @@ } ] }, - "spec": { - "photos" : { - "*" : { + "photos": { + "*": { "@fileName": "photos[&1].@id" } } }, - "expected": { - "photos" : [ + "photos": [ { - "1234" : "Acme.jpg" + "1234": "Acme.jpg" }, { - "9876" : "Boxy.jpg" + "9876": "Boxy.jpg" } ] } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json index 58a771bf..edb71504 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeArrayContents2.json @@ -1,6 +1,6 @@ { "input": { - "photos" : [ + "photos": [ { "id": "1234", "fileName": "Acme.jpg" @@ -11,19 +11,17 @@ } ] }, - "spec": { - "photos" : { - "*" : { + "photos": { + "*": { "@fileName": "photos.@id" } } }, - "expected": { - "photos" : { - "1234" : "Acme.jpg", - "9876" : "Boxy.jpg" + "photos": { + "1234": "Acme.jpg", + "9876": "Boxy.jpg" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json index 2e812b80..050bca16 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex1.json @@ -1,23 +1,21 @@ { "input": { - "data" : { + "data": { "clientId": "1234", - "clientNameStuff" : { + "clientNameStuff": { "clientName": "Acme", - "otherClientStuff" : "pants" + "otherClientStuff": "pants" } } }, - "spec": { - "data" : { - "@clientNameStuff.clientName" : "bookMap.@clientId" + "data": { + "@clientNameStuff.clientName": "bookMap.@clientId" } }, - "expected": { - "bookMap" : { - "1234" : "Acme" + "bookMap": { + "1234": "Acme" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json index 564126c7..c620b8b3 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex2.json @@ -1,24 +1,22 @@ { "input": { - "data" : { + "data": { "clientId": "1234", - "clientNameStuff" : { + "clientNameStuff": { "clientName": "Acme", - "otherClientStuff" : "pants" + "otherClientStuff": "pants" } } }, - "spec": { - "data" : { + "data": { // Verify LHS canonical form - "@(clientNameStuff.clientName)" : "bookMap.@clientId" + "@(clientNameStuff.clientName)": "bookMap.@clientId" } }, - "expected": { - "bookMap" : { - "1234" : "Acme" + "bookMap": { + "1234": "Acme" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json index ccffc815..8c09aec3 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex3_both-sides-multipart.json @@ -1,26 +1,24 @@ { "input": { - "data" : { - "clientIdStuff" : { + "data": { + "clientIdStuff": { "clientId": "1234", - "orderIdStuff" : "shoes" + "orderIdStuff": "shoes" }, - "clientNameStuff" : { + "clientNameStuff": { "clientName": "Acme", - "otherClientStuff" : "pants" + "otherClientStuff": "pants" } } }, - "spec": { - "data" : { + "data": { // Verify we can write data out to a "literal.@().literal" location - "@(clientNameStuff.clientName)" : "data.@(clientIdStuff.clientId).clientName" + "@(clientNameStuff.clientName)": "data.@(clientIdStuff.clientId).clientName" } }, - "expected": { - "data" : { + "data": { "1234": { "clientName": "Acme" } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json index 3f53db3e..3e6a070c 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex4_lhs-multipart-rhs-sugar.json @@ -1,23 +1,21 @@ { "input": { - "data" : { + "data": { "clientId": "1234", - "clientNameStuff" : { + "clientNameStuff": { "clientName": "Acme", - "otherClientStuff" : "pants" + "otherClientStuff": "pants" } } }, - "spec": { - "data" : { + "data": { // Verify we can write data out to a RHS with syntactic sugar @ in the middle of two literals: "literal.@SUGAR.literal" - "@(clientNameStuff.clientName)" : "data.@clientId.clientName" + "@(clientNameStuff.clientName)": "data.@clientId.clientName" } }, - "expected": { - "data" : { + "data": { "1234": { "clientName": "Acme" } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json index 50e2980b..08f4d495 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex5_at-logic-with-embedded-array-lookups.json @@ -1,23 +1,27 @@ { "input": { - "sillyPhotoData" : { - "captions" : [ "Hi!", "Sunny" ], - "fileNames" : [ "hola.jpg", "sunny.jpg"] + "sillyPhotoData": { + "captions": [ + "Hi!", + "Sunny" + ], + "fileNames": [ + "hola.jpg", + "sunny.jpg" + ] } }, - "spec": { - "sillyPhotoData" : { + "sillyPhotoData": { // This is a rather silly test, but it does prove you can have [0] inside an @() - "@(fileNames[0])" : "sillyPhotoData.@(captions[0])", - "@(fileNames[1])" : "sillyPhotoData.@(captions[1])" + "@(fileNames[0])": "sillyPhotoData.@(captions[0])", + "@(fileNames[1])": "sillyPhotoData.@(captions[1])" } }, - "expected": { - "sillyPhotoData" : { - "Hi!" : "hola.jpg", - "Sunny" : "sunny.jpg" + "sillyPhotoData": { + "Hi!": "hola.jpg", + "Sunny": "sunny.jpg" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json index 8f1a9b8c..15d9ed09 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex6_rhs-complex-at.json @@ -1,32 +1,31 @@ { "input": { - "data" : { - "clientIdStuff" : { + "data": { + "clientIdStuff": { "clientId": "1234" }, - "clientNameStuff" : { + "clientNameStuff": { "clientName": "Acme", - "otherClientStuff" : "pants" + "otherClientStuff": "pants" } } }, - "spec": { - "data" : { - "clientNameStuff" : { + "data": { + "clientNameStuff": { // walk down the tree till we find a clientName // 1) go two levels up the tree the "2" from "@(2,...)" // 2) then walk down to find a clientId : "1234" // 3) use the "1234" value as part of the output path - "clientName" : "data.@(2,clientIdStuff.clientId).clientName" + "clientName": "data.@(2,clientIdStuff.clientId).clientName" } } }, - "expected": { - "data" : { - "1234": { // in this case we want the clientId (1234) to be a level in the output tree - // rather than just a key/value pair. + "data": { + "1234": { + // in this case we want the clientId (1234) to be a level in the output tree + // rather than just a key/value pair. "clientName": "Acme" } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json index 1293dd6a..35538abd 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex7_coerce-int-string-conversion.json @@ -1,32 +1,31 @@ { "input": { - "data" : { - "clientIdStuff" : { + "data": { + "clientIdStuff": { "clientId": 1234 }, - "clientNameStuff" : { + "clientNameStuff": { "clientName": "Acme", - "otherClientStuff" : "pants" + "otherClientStuff": "pants" } } }, - "spec": { - "data" : { - "clientNameStuff" : { + "data": { + "clientNameStuff": { // walk down the tree till we find a clientName // 1) go two levels up the tree the "2" from "@(2,...)" // 2) then walk down to find a clientId : 1234 // 3) coerce the number 1234, into a String "1234", and use that as part of the output path - "clientName" : "data.@(2,clientIdStuff.clientId).clientName" + "clientName": "data.@(2,clientIdStuff.clientId).clientName" } } }, - "expected": { - "data" : { - "1234": { // in this case we want the clientId (1234) to be a level in the output tree - // rather than just a key/value pair. + "data": { + "1234": { + // in this case we want the clientId (1234) to be a level in the output tree + // rather than just a key/value pair. "clientName": "Acme" } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json index 2a58dfef..8ea18922 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex8_coerce-boolean-string-conversion.json @@ -1,33 +1,39 @@ { "input": { - "clients" : { - "Acme" : { + "clients": { + "Acme": { "clientId": "guid", - "enabled" : true + "enabled": true }, - "Axe" : { + "Axe": { "clientId": 2, - "enabled" : false + "enabled": false }, - "Bob's Burgers" : { + "Bob's Burgers": { "clientId": 3, - "enabled" : true + "enabled": true } } }, - "spec": { - "clients" : { - "*": { // clientName - "clientId": "clientsById.@(1,enabled)[]" // coerce the boolean into the string "true" or "false" + "clients": { + "*": { + // clientName + "clientId": "clientsById.@(1,enabled)[]" + // coerce the boolean into the string "true" or "false" } } }, - "expected": { - "clientsById" : { - "true" : [ "guid", 3 ], // Note the Ids, whatever they are get passed along, because they are data - "false" : [ 2 ] + "clientsById": { + "true": [ + "guid", + 3 + ], + // Note the Ids, whatever they are get passed along, because they are data + "false": [ + 2 + ] } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeComplex9_lookup_an_array_index.json b/jolt-core/src/test/resources/json/shiftr/transposeComplex9_lookup_an_array_index.json index 3693e0a4..97f918c7 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeComplex9_lookup_an_array_index.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeComplex9_lookup_an_array_index.json @@ -1,52 +1,51 @@ { "input": { - "clients" : { - "Acme" : { + "clients": { + "Acme": { "clientId": "Acme", - "index" : 1 + "index": 1 }, - "Axe" : { + "Axe": { "clientId": "AXE", - "index" : 0 + "index": 0 }, - "Bob's Burgers" : { + "Bob's Burgers": { "clientId": "BBurgers", - // the idea here is that index is non-numeric and non-coercible to numeric // in this case Shiftr will just ignore the output, thus preventing "BBurgers" from getting to the output - "index" : "abc" + "index": "abc" }, - "PhoVan" : { + "PhoVan": { "clientId": "pho", - // the idea here is that the index is a String, but is coercible to numeric - "index" : "3" + "index": "3" }, - "Weyland-Yutani" : { + "Weyland-Yutani": { "clientId": "Walmart", - // the idea here is that negative index values get ignored - "index" : -1 + "index": -1 }, - "UmbrellaCorporation" : { + "UmbrellaCorporation": { "clientId": "Monsanto", - // the idea here is that negative index values get ignored - "index" : "-2" + "index": "-2" } } }, - "spec": { - "clients" : { + "clients": { "*": { // test the abilyt to lookup the numeric index using a @ / Transpose operator, aka [@(1,index)] "clientId": "clientIdArray[@(1,index)]" } } }, - "expected": { - "clientIdArray" : [ "AXE", "Acme", null, "pho" ] + "clientIdArray": [ + "AXE", + "Acme", + null, + "pho" + ] } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json index 1c55d438..326c2a45 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap1.json @@ -1,37 +1,35 @@ { "input": { - "data" : [ + "data": [ { "clientId": "1", "clientName": "Acme", - "otherStuff" : "Boom" + "otherStuff": "Boom" }, { "clientId": "2", "clientName": "Bob's", - "otherStuff" : "Burgers" + "otherStuff": "Burgers" } ] }, - "spec": { - "data" : { + "data": { // We can use the RHS @ sign to look down the tree to find a key for the output - "*" : "data.@clientId" + "*": "data.@clientId" } }, - "expected": { - "data" : { - "1" : { + "data": { + "1": { "clientId": "1", "clientName": "Acme", - "otherStuff" : "Boom" + "otherStuff": "Boom" }, - "2" : { + "2": { "clientId": "2", "clientName": "Bob's", - "otherStuff" : "Burgers" + "otherStuff": "Burgers" } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json index 15cc196a..7d92b9b6 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeInverseMap2.json @@ -1,25 +1,23 @@ { "input": { - "data" : [ + "data": [ { "clientId": "1", "clientName": "Acme", - "otherStuff" : "Boom" + "otherStuff": "Boom" }, { "clientId": "2", "clientName": "Bob's", - "otherStuff" : "Burgers" + "otherStuff": "Burgers" } ] }, - "spec": { - "data" : { + "data": { // Verify that if the RHS @ does not find anything, nothing get written - "*" : "data.@pants" + "*": "data.@pants" } }, - "expected": null } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json b/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json index d7c15b84..300a8449 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeLHS1.json @@ -1,18 +1,17 @@ { "input": { - "data" : { + "data": { "clientId": "1234", "clientName": "Acme" } }, - "spec": { - "data" : { - "@clientName" : "CLIENTNAME" // verify that we can look down the tree + "data": { + "@clientName": "CLIENTNAME" + // verify that we can look down the tree } }, - "expected": { - "CLIENTNAME" : "Acme" + "CLIENTNAME": "Acme" } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json b/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json index e24b470c..cee8aa69 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeLHS2.json @@ -1,18 +1,17 @@ { "input": { - "data" : { + "data": { "clientId": "1234", "data": "Acme" } }, - "spec": { - "data" : { - "@&" : "CLIENTNAME" // verify that we can look down the tree, using refs + "data": { + "@&": "CLIENTNAME" + // verify that we can look down the tree, using refs } }, - "expected": { - "CLIENTNAME" : "Acme" + "CLIENTNAME": "Acme" } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json b/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json index 867bb238..05454411 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeLHS3.json @@ -1,22 +1,23 @@ { "input": { - "data" : [ + "data": [ { "clientId": "1234", "data": "Acme" } ] }, - "spec": { - "*" : { // catch the data - "*": { // for all array elements - "@&1": "CLIENTNAME" // match the key "data" based on what the key was two levels up + "*": { + // catch the data + "*": { + // for all array elements + "@&1": "CLIENTNAME" + // match the key "data" based on what the key was two levels up } } }, - "expected": { - "CLIENTNAME" : "Acme" + "CLIENTNAME": "Acme" } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeLargeNumber.json b/jolt-core/src/test/resources/json/shiftr/transposeLargeNumber.json new file mode 100644 index 00000000..bb47e65f --- /dev/null +++ b/jolt-core/src/test/resources/json/shiftr/transposeLargeNumber.json @@ -0,0 +1,23 @@ +{ + "input": { + "Student": { + "Number": 2147483648, + "KEY": "label", + "VALUE": "test" + } + }, + "spec": { + "Student": { + "VALUE": "Student.data.@(1,Number).@(1,KEY)" + } + }, + "expected": { + "Student" : { + "data" : { + "2147483648" : { + "label" : "test" + } + } + } + } +} diff --git a/jolt-core/src/test/resources/json/shiftr/transposeNestedLookup.json b/jolt-core/src/test/resources/json/shiftr/transposeNestedLookup.json index 106ed8ca..703c1b77 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeNestedLookup.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeNestedLookup.json @@ -1,47 +1,46 @@ { "input": { - - "clientsActive" : true, - "clients" : { - "Acme" : { + "clientsActive": true, + "clients": { + "Acme": { "clientId": "Acme", - "index" : 1 + "index": 1 }, - "Axe" : { + "Axe": { "clientId": "AXE", - "index" : 0 + "index": 0 } }, - "data": { "bookId": null, "bookName": "Enchiridion" } }, - "spec": { - "clientsActive" : { - "true" : { - "@(2,clients)" : { + "clientsActive": { + "true": { + "@(2,clients)": { // Test the ability to continue to match after doing a Transpose - "*" : { - "clientId" : "clientIds[@(1,index)]" + "*": { + "clientId": "clientIds[@(1,index)]" } }, // Verify that it something does not exist, it does not output a null - "@(2,pants)" : "pants" + "@(2,pants)": "pants" } }, - "data" : { + "data": { // Verify the ability for Transpose to lookup and use a "valid" null, aka one that was in the input data "@bookId": "books.@bookName" } }, - "expected": { - "clientIds" : [ "AXE", "Acme" ], - "books" : { - "Enchiridion" : null + "clientIds": [ + "AXE", + "Acme" + ], + "books": { + "Enchiridion": null } } } diff --git a/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json b/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json index e48120d4..48bb6fe0 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeSimple1.json @@ -1,18 +1,16 @@ { "input": { - "data" : { + "data": { "clientId": "1234", "clientName": "Acme" } }, - "spec": { - "data" : { - "@clientName" : "@clientId" + "data": { + "@clientName": "@clientId" } }, - "expected": { - "1234" : "Acme" + "1234": "Acme" } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json b/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json index dc0d6988..df883bca 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeSimple2.json @@ -5,13 +5,11 @@ "clientName": "Acme" } }, - "spec": { "data": { "@clientName": "bookMap.@clientId" } }, - "expected": { "bookMap": { "1234": "Acme" diff --git a/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json b/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json index 5f1153d9..4a8ae441 100644 --- a/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json +++ b/jolt-core/src/test/resources/json/shiftr/transposeSimple3.json @@ -3,12 +3,11 @@ "clientId": "1234", "clientName": "Acme" }, - "spec": { - "@clientName" : "@clientId" // verify that data can be transposed at the root level + "@clientName": "@clientId" + // verify that data can be transposed at the root level }, - "expected": { - "1234" : "Acme" + "1234": "Acme" } -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/shiftr/wildcardSelfAndRef.json b/jolt-core/src/test/resources/json/shiftr/wildcardSelfAndRef.json index 26ca3a3f..02b916e6 100644 --- a/jolt-core/src/test/resources/json/shiftr/wildcardSelfAndRef.json +++ b/jolt-core/src/test/resources/json/shiftr/wildcardSelfAndRef.json @@ -1,36 +1,56 @@ { "input": { - "tag-Pro": [ "Beautiful", "easy to apply" ], - "tag-Con": [ "none" ], - + "tag-Pro": [ + "Beautiful", + "easy to apply" + ], + "tag-Con": [ + "none" + ], "cdv-Fafa": "Groundhog", "cdv-Mario": "Red American" }, - "spec": { "tag-*": { - "@": [ "TagDimensions.&(0,1).Values", "TagDimensions.&(1,1).Values-canonical1" ], - "$(0,1)": [ "TagDimensions.&.Id", "TagDimensions.&(0,0).Id-canonical0", "TagDimensions.&0.Id-sugar0", "TagDimensions.&(1,1).Id-upref" ] + "@": [ + "TagDimensions.&(0,1).Values", + "TagDimensions.&(1,1).Values-canonical1" + ], + "$(0,1)": [ + "TagDimensions.&.Id", + "TagDimensions.&(0,0).Id-canonical0", + "TagDimensions.&0.Id-sugar0", + "TagDimensions.&(1,1).Id-upref" + ] }, - - "cdv-*": [ "ContextDataValues.&(0,1).Value", "ContextDataValues.&(0,1).Value-sugar" ] + "cdv-*": [ + "ContextDataValues.&(0,1).Value", + "ContextDataValues.&(0,1).Value-sugar" + ] }, - "expected": { "TagDimensions": { "Pro": { - "Values": [ "Beautiful", "easy to apply" ], - "Values-canonical1": [ "Beautiful", "easy to apply" ], - + "Values": [ + "Beautiful", + "easy to apply" + ], + "Values-canonical1": [ + "Beautiful", + "easy to apply" + ], "Id": "Pro", "Id-canonical0": "Pro", "Id-sugar0": "Pro", "Id-upref": "Pro" }, "Con": { - "Values": [ "none" ], - "Values-canonical1": [ "none" ], - + "Values": [ + "none" + ], + "Values-canonical1": [ + "none" + ], "Id": "Con", "Id-canonical0": "Con", "Id-sugar0": "Con", diff --git a/jolt-core/src/test/resources/json/shiftr/wildcards.json b/jolt-core/src/test/resources/json/shiftr/wildcards.json index 01c489c0..c4f0b946 100644 --- a/jolt-core/src/test/resources/json/shiftr/wildcards.json +++ b/jolt-core/src/test/resources/json/shiftr/wildcards.json @@ -12,29 +12,36 @@ "tuna.3-1-1": 311, "tuna.3-1-2": 312 }, - - "spec": { "foo": "a.b.c", "bar|baz": "a.b.d", "tuna": "a.b.e", - "tuna.*-*-*": "a.b.3Star", "tuna_*-*": "a.b.2Star", "tuna-*": "a.b.1Star" }, - - "expected": { "a": { "b": { - "c": 1, - "d": [2,3], - "e": 4, - "3Star": [311,312], - "2Star": [21,22], - "1Star": [11,12,13] - + "c": 1, + "d": [ + 2, + 3 + ], + "e": 4, + "3Star": [ + 311, + 312 + ], + "2Star": [ + 21, + 22 + ], + "1Star": [ + 11, + 12, + 13 + ] } } } diff --git a/jolt-core/src/test/resources/json/shiftr/wildcardsWithOr.json b/jolt-core/src/test/resources/json/shiftr/wildcardsWithOr.json index fd257424..a8f0e570 100644 --- a/jolt-core/src/test/resources/json/shiftr/wildcardsWithOr.json +++ b/jolt-core/src/test/resources/json/shiftr/wildcardsWithOr.json @@ -1,51 +1,67 @@ { - "input": { - "foo-a-v": 1, - "bar-a-v": 2, - "foo-b-v": 3, - "bar-b-v": 4, - "foo-c-v": 5, - "bar-c-v": 6, - "foo-d-v": 7, - "bar-d-v": 8, - - "foo-e-v": 9, - "bar-e-v": 10, - "foo-f-v": 11, - "bar-f-v": 12, - "foo-g-v": 13, - "bar-g-v": 14, - "foo-h-v": 15, - "bar-h-v": 16, - - "flat": 17 - }, - - - "spec": { - "bar-a-v|foo-a-v": "i", - "bar-b-v|foo-b-*": "h", - "bar-c-*|foo-c-v": "g", - "bar-d-*|foo-d-*": "f", - - "foo-e-v|bar-e-v": "e", - "foo-f-v|bar-f-*": "d", - "foo-g-*|bar-g-v": "c", - "foo-h-*|bar-h-*": "b", - - "flat":"a" - }, - - - "expected": { - "a" : 17, - "b" : [ 15, 16 ], - "c" : [ 14, 13 ], - "d" : [ 11, 12 ], - "e" : [ 9, 10 ], - "f" : [ 7, 8 ], - "g" : [ 5, 6 ], - "h" : [ 4, 3 ], - "i" : [ 2, 1 ] - } + "input": { + "foo-a-v": 1, + "bar-a-v": 2, + "foo-b-v": 3, + "bar-b-v": 4, + "foo-c-v": 5, + "bar-c-v": 6, + "foo-d-v": 7, + "bar-d-v": 8, + "foo-e-v": 9, + "bar-e-v": 10, + "foo-f-v": 11, + "bar-f-v": 12, + "foo-g-v": 13, + "bar-g-v": 14, + "foo-h-v": 15, + "bar-h-v": 16, + "flat": 17 + }, + "spec": { + "bar-a-v|foo-a-v": "i", + "bar-b-v|foo-b-*": "h", + "bar-c-*|foo-c-v": "g", + "bar-d-*|foo-d-*": "f", + "foo-e-v|bar-e-v": "e", + "foo-f-v|bar-f-*": "d", + "foo-g-*|bar-g-v": "c", + "foo-h-*|bar-h-*": "b", + "flat": "a" + }, + "expected": { + "a": 17, + "b": [ + 15, + 16 + ], + "c": [ + 14, + 13 + ], + "d": [ + 11, + 12 + ], + "e": [ + 9, + 10 + ], + "f": [ + 7, + 8 + ], + "g": [ + 5, + 6 + ], + "h": [ + 4, + 3 + ], + "i": [ + 2, + 1 + ] + } } diff --git a/jolt-core/src/test/resources/json/sortr/simple/input.json b/jolt-core/src/test/resources/json/sortr/simple/input.json index 5d9ddc4e..50b82bf1 100644 --- a/jolt-core/src/test/resources/json/sortr/simple/input.json +++ b/jolt-core/src/test/resources/json/sortr/simple/input.json @@ -3,12 +3,16 @@ "foo": 3, "bar": 14, "baz": 15, - "~id": 1234, - - "" : "damn empty string", - - "list" : [ - "zz", "aa", { "z": "z", "~zed" : "zed", "~id" : "ID-123", "b" : "b" } + "": "damn empty string", + "list": [ + "zz", + "aa", + { + "z": "z", + "~zed": "zed", + "~id": "ID-123", + "b": "b" + } ] -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/sortr/simple/output.json b/jolt-core/src/test/resources/json/sortr/simple/output.json index aeae9a56..0ad950ab 100644 --- a/jolt-core/src/test/resources/json/sortr/simple/output.json +++ b/jolt-core/src/test/resources/json/sortr/simple/output.json @@ -1,17 +1,18 @@ { "~id": 1234, - "" : "damn empty string", + "": "damn empty string", "bar": 14, "baz": 15, "foo": 3, - "list" : [ - "zz", "aa", + "list": [ + "zz", + "aa", { - "~id" : "ID-123", - "~zed" : "zed", - "b" : "b", + "~id": "ID-123", + "~zed": "zed", + "b": "b", "z": "z" } ], "tuna": 9 -} \ No newline at end of file +} diff --git a/jolt-core/src/test/resources/json/utils/joltUtils-removeRecursive.json b/jolt-core/src/test/resources/json/utils/joltUtils-removeRecursive.json index c3a483a9..8aa26a65 100644 --- a/jolt-core/src/test/resources/json/utils/joltUtils-removeRecursive.json +++ b/jolt-core/src/test/resources/json/utils/joltUtils-removeRecursive.json @@ -1,28 +1,25 @@ [ { - "input" : { - "L1_A" : { - "L2_A" : { - "L3_A" : "Good", - "L3_B" : "RemoveThis" + "input": { + "L1_A": { + "L2_A": { + "L3_A": "Good", + "L3_B": "RemoveThis" }, - "L2_B" : "l2_b" + "L2_B": "l2_b" }, - "L1_B" : "l1_b", - - "L3_B" : "RemoveThis" + "L1_B": "l1_b", + "L3_B": "RemoveThis" }, - - "remove" : "L3_B", - - "expected" : { - "L1_A" : { - "L2_A" : { - "L3_A" : "Good" + "remove": "L3_B", + "expected": { + "L1_A": { + "L2_A": { + "L3_A": "Good" }, - "L2_B" : "l2_b" + "L2_B": "l2_b" }, - "L1_B" : "l1_b" + "L1_B": "l1_b" } } -] \ No newline at end of file +] diff --git a/jolt-core/src/test/resources/json/utils/joltUtils-store-remove-compact.json b/jolt-core/src/test/resources/json/utils/joltUtils-store-remove-compact.json index 35cca8c6..b436bfe0 100644 --- a/jolt-core/src/test/resources/json/utils/joltUtils-store-remove-compact.json +++ b/jolt-core/src/test/resources/json/utils/joltUtils-store-remove-compact.json @@ -7,7 +7,9 @@ "a": "A", "x": "X" }, - "path": ["a"], + "path": [ + "a" + ], "value": "ABC", "output": { "1": 1, @@ -23,7 +25,9 @@ "a": "A", "x": "X" }, - "path": ["a"], + "path": [ + "a" + ], "value": 24, "output": { "1": 1, @@ -39,8 +43,13 @@ "a": "A", "x": "X" }, - "path": ["a"], - "value": { "ABC": "abc", "42": 42 }, + "path": [ + "a" + ], + "value": { + "ABC": "abc", + "42": 42 + }, "output": { "1": 1, "2": 2, @@ -55,44 +64,121 @@ "a": "A", "x": "X" }, - "path": ["a"], - "value": [24, "abc"], + "path": [ + "a" + ], + "value": [ + 24, + "abc" + ], "output": { "1": 1, "2": 2, "x": "X" } }, - { "description": "src: simple list, path: simple, value: string", - "source": [ "zero", 1, "two", 3, "four", 5, "six"], - "path": [5], + "source": [ + "zero", + 1, + "two", + 3, + "four", + 5, + "six" + ], + "path": [ + 5 + ], "value": "ABC", - "output": [ "zero", 1, "two", 3, "four", "six" ] + "output": [ + "zero", + 1, + "two", + 3, + "four", + "six" + ] }, { "description": "src: simple list, path: simple, value: int", - "source": [ "zero", 1, "two", 3, "four", 5, "six"], - "path": [5], + "source": [ + "zero", + 1, + "two", + 3, + "four", + 5, + "six" + ], + "path": [ + 5 + ], "value": 42, - "output": [ "zero", 1, "two", 3, "four", "six" ] + "output": [ + "zero", + 1, + "two", + 3, + "four", + "six" + ] }, { "description": "src: simple list, path: simple, value: map", - "source": [ "zero", 1, "two", 3, "four", 5, "six"], - "path": [5], - "value": {"ABC": "abc", "42": 42}, - "output": [ "zero", 1, "two", 3, "four", "six" ] + "source": [ + "zero", + 1, + "two", + 3, + "four", + 5, + "six" + ], + "path": [ + 5 + ], + "value": { + "ABC": "abc", + "42": 42 + }, + "output": [ + "zero", + 1, + "two", + 3, + "four", + "six" + ] }, { "description": "src: simple list, path: simple, value: list", - "source": [ "zero", 1, "two", 3, "four", 5, "six"], - "path": [5], - "value": ["ABC", 42], - "output": [ "zero", 1, "two", 3, "four", "six"] + "source": [ + "zero", + 1, + "two", + 3, + "four", + 5, + "six" + ], + "path": [ + 5 + ], + "value": [ + "ABC", + 42 + ], + "output": [ + "zero", + 1, + "two", + 3, + "four", + "six" + ] }, - { "description": "src: complex map, path: simple, value: string", "source": { @@ -100,11 +186,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -120,7 +204,9 @@ } ] }, - "path": ["a"], + "path": [ + "a" + ], "value": "ABC", "output": { "must": [ @@ -153,11 +239,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -173,7 +257,9 @@ } ] }, - "path": ["a"], + "path": [ + "a" + ], "value": 42, "output": { "must": [ @@ -206,11 +292,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -226,8 +310,13 @@ } ] }, - "path": ["a"], - "value": {"ABC": "abc", "42": 24}, + "path": [ + "a" + ], + "value": { + "ABC": "abc", + "42": 24 + }, "output": { "must": [ { @@ -259,11 +348,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -279,8 +366,13 @@ } ] }, - "path": ["a"], - "value": ["ABC", 42], + "path": [ + "a" + ], + "value": [ + "ABC", + 42 + ], "output": { "must": [ { @@ -305,7 +397,6 @@ ] } }, - { "description": "src: complex map, path: complex, value: string", "source": { @@ -313,11 +404,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -333,7 +422,14 @@ } ] }, - "path": ["a", 3, "b", 4, "c", 5], + "path": [ + "a", + 3, + "b", + 4, + "c", + 5 + ], "value": "ABC", "output": { "must": [ @@ -361,7 +457,7 @@ { "b": [ { - "c": [ ] + "c": [] } ] } @@ -375,11 +471,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -395,7 +489,14 @@ } ] }, - "path": ["a", 3, "b", 4, "c", 5], + "path": [ + "a", + 3, + "b", + 4, + "c", + 5 + ], "value": 42, "output": { "must": [ @@ -423,7 +524,7 @@ { "b": [ { - "c": [ ] + "c": [] } ] } @@ -437,11 +538,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -457,8 +556,18 @@ } ] }, - "path": ["a", 3, "b", 4, "c", 5], - "value": {"ABC": "abc", "42": 42}, + "path": [ + "a", + 3, + "b", + 4, + "c", + 5 + ], + "value": { + "ABC": "abc", + "42": 42 + }, "output": { "must": [ { @@ -485,7 +594,7 @@ { "b": [ { - "c": [ ] + "c": [] } ] } @@ -499,11 +608,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -519,8 +626,18 @@ } ] }, - "path": ["a", 3, "b", 4, "c", 5], - "value": ["ABC", 42], + "path": [ + "a", + 3, + "b", + 4, + "c", + 5 + ], + "value": [ + "ABC", + 42 + ], "output": { "must": [ { @@ -547,25 +664,22 @@ { "b": [ { - "c": [ ] + "c": [] } ] } ] } }, - { "description": "src: complex list, path: simple, value: string", "source": [ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -580,7 +694,9 @@ } } ], - "path": [5], + "path": [ + 5 + ], "value": "ABC", "output": [ { @@ -610,11 +726,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -629,7 +743,9 @@ } } ], - "path": [5], + "path": [ + 5 + ], "value": 42, "output": [ { @@ -659,11 +775,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -678,8 +792,13 @@ } } ], - "path": [5], - "value": {"ABC": "abc", "42": 42}, + "path": [ + 5 + ], + "value": { + "ABC": "abc", + "42": 42 + }, "output": [ { "bool": { @@ -708,11 +827,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -727,8 +844,13 @@ } } ], - "path": [5], - "value": ["ABC", 42], + "path": [ + 5 + ], + "value": [ + "ABC", + 42 + ], "output": [ { "bool": { @@ -751,18 +873,15 @@ } ] }, - { "description": "src: complex list, path: complex, value: string", "source": [ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -776,9 +895,15 @@ ] } } - ], - "path": [2, "a", 3, "b", 4, "c"], + "path": [ + 2, + "a", + 3, + "b", + 4, + "c" + ], "value": "ABC", "output": [ { @@ -804,7 +929,8 @@ "a": [ { "b": [ - { } // "c": null is nuked + {} + // "c": null is nuked ] } ] @@ -817,11 +943,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -835,9 +959,15 @@ ] } } - ], - "path": [2, "a", 3, "b", 4, "c"], + "path": [ + 2, + "a", + 3, + "b", + 4, + "c" + ], "value": 42, "output": [ { @@ -863,7 +993,8 @@ "a": [ { "b": [ - { } // "c": null is nuked + {} + // "c": null is nuked ] } ] @@ -876,11 +1007,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -894,10 +1023,19 @@ ] } } - ], - "path": [2, "a", 3, "b", 4, "c"], - "value": {"ABC": "abc", "42": 42}, + "path": [ + 2, + "a", + 3, + "b", + 4, + "c" + ], + "value": { + "ABC": "abc", + "42": 42 + }, "output": [ { "bool": { @@ -922,7 +1060,8 @@ "a": [ { "b": [ - { } // "c": null is nuked + {} + // "c": null is nuked ] } ] @@ -935,11 +1074,9 @@ { "bool": { "should": [ - { "bool": { "must": [ - { "range": { "firstPublishTime": { @@ -953,10 +1090,19 @@ ] } } - ], - "path": [2, "a", 3, "b", 4, "c"], - "value": ["ABC", 42], + "path": [ + 2, + "a", + 3, + "b", + 4, + "c" + ], + "value": [ + "ABC", + 42 + ], "output": [ { "bool": { @@ -981,7 +1127,8 @@ "a": [ { "b": [ - { } // "c": null is nuked + {} + // "c": null is nuked ] } ] diff --git a/json-utils/pom.xml b/json-utils/pom.xml index 68d9e325..6cc852a3 100644 --- a/json-utils/pom.xml +++ b/json-utils/pom.xml @@ -1,39 +1,84 @@ - + - 4.0.0 + 4.0.0 - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 ../parent/pom.xml - json-utils - Jolt Json Utils - jar + json-community-utils + Jolt Json Utils + jar - - - com.fasterxml.jackson.core - jackson-databind - - - com.fasterxml.jackson.core - jackson-core - + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + - - com.google.guava - guava - test - - - org.testng - testng - test - - + + + tools.jackson.core + jackson-databind + ${jackson.version} + + + tools.jackson.core + jackson-core + ${jackson.version} + + + com.google.guava + guava + ${guava.version} + test + + + org.testng + testng + ${testng.version} + test + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + @{argLine} -Dfile.encoding=UTF-8 + -Djava.awt.headless=true + + test/integration/** + + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + report + test + + report + + + + + + diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java deleted file mode 100644 index fb9504dc..00000000 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtil.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.fasterxml.jackson.core.type.TypeReference; - -import java.io.InputStream; -import java.util.List; -import java.util.Map; - -/** - * Utility methods for getting JSON content loaded from - * the filesystem, the classpath, or in memory Strings. - * - * Also has methods to serialize Java object to JSON strings. - * - * Implementations of this interface can specify their own - * Jackson ObjectMapper so that Domain specific Java Objects - * can successfully be serialized and de-serialized. - */ -public interface JsonUtil { - - // DE-SERIALIZATION - Object jsonToObject( String json ); - Object jsonToObject( String json , String charset ); - Object jsonToObject( InputStream in ); - - Map jsonToMap( String json ); - Map jsonToMap( String json, String charset ); - Map jsonToMap( InputStream in ); - - List jsonToList( String json); - List jsonToList( String json , String charset ); - List jsonToList( InputStream in ); - - Object filepathToObject( String filePath ); - Map filepathToMap( String filePath ); - List filepathToList( String filePath ); - - Object classpathToObject( String classPath ); - Map classpathToMap( String classPath ); - List classpathToList( String classPath ); - - /** - * Use the stringToType method instead. - */ - @Deprecated - T jsonTo( String json, TypeReference typeRef ); - - /** - * Use the streamToType method instead. - */ - @Deprecated - T jsonTo( InputStream in, TypeReference typeRef ); - - T stringToType (String json, TypeReference typeRef ); - T stringToType (String json, Class aClass ); - - T classpathToType(String classPath, TypeReference typeRef ); - T classpathToType(String classPath, Class aClass ); - - T fileToType (String filePath, TypeReference typeRef ); - T fileToType (String filePath, Class aClass ); - - T streamToType ( InputStream in, TypeReference typeRef ); - T streamToType ( InputStream in, Class aClass ); - - String toJsonString( Object obj ); - String toPrettyJsonString( Object obj ); - - /** - * Makes a deep copy of a Map object by converting it to a String and then - * back onto stock JSON objects. - * - * Leverages Serialization - * - * @param obj object tree to copy - * @return deep copy of the incoming obj - */ - Object cloneJson( Object obj ); -} diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java deleted file mode 100644 index bd24cf85..00000000 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtilImpl.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.bazaarvoice.jolt.exception.JsonMarshalException; -import com.bazaarvoice.jolt.exception.JsonUnmarshalException; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.module.SimpleModule; - -import java.io.ByteArrayInputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.UnsupportedEncodingException; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * Implementation of JsonUtil that allows the user to provide a configured - * Jackson ObjectMapper. - * - * All IOExceptions are caught, wrapped with context, and rethrown as RuntimeExceptions. - */ -public class JsonUtilImpl implements JsonUtil { - // thread safe: http://wiki.fasterxml.com/JacksonFAQThreadSafety - private final ObjectMapper objectMapper; - private final ObjectWriter prettyPrintWriter; - - // Default Encoding for String to JSON operations - public static final String DEFAULT_ENCODING_UTF_8 = "utf-8"; - - private static final TypeReference> mapTypeReference = - new TypeReference>() {}; - private static final TypeReference> listTypeReference = - new TypeReference>() {}; - - public static void configureStockJoltObjectMapper( ObjectMapper objectMapper ) { - - // All Json maps should be deserialized into LinkedHashMaps. - SimpleModule stockModule = new SimpleModule("stockJoltMapping", new Version(1, 0, 0, null, null, null)) - .addAbstractTypeMapping( Map.class, LinkedHashMap.class ); - - objectMapper.registerModule(stockModule); - - // allow the mapper to parse JSON with comments in it - objectMapper.configure( JsonParser.Feature.ALLOW_COMMENTS, true); - } - - /** - * By allowing the user to provide an ObjectMapper, it can be configured with - * knowledge of how to marshall and un-marshall your domain objects. - * - * @param objectMapper a configured Jackson ObjectMapper - */ - public JsonUtilImpl( ObjectMapper objectMapper ) { - - this.objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper; - - configureStockJoltObjectMapper( this.objectMapper ); - prettyPrintWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); - } - - public JsonUtilImpl() { - this( new ObjectMapper() ); - } - - // DE-SERIALIZATION - @Override - public Object jsonToObject( String json ) { - return jsonToObject( json, DEFAULT_ENCODING_UTF_8 ); - } - - @Override - public Object jsonToObject( String json, String charset ) { - try { - return jsonToObject( new ByteArrayInputStream( json.getBytes(charset) ) ); - } - catch ( UnsupportedEncodingException e ) { - throw new RuntimeException( e ); - } - } - - @Override - public Object jsonToObject( InputStream in ) { - try { - return objectMapper.readValue( in, Object.class ); - } - catch ( IOException e ) { - throw new JsonUnmarshalException("Unable to unmarshal JSON to an Object.", e ); - } - } - - @Override - public Map jsonToMap( String json) { - return jsonToMap( json, DEFAULT_ENCODING_UTF_8 ); - } - - @Override - public Map jsonToMap( String json, String charset ) { - try { - return jsonToMap( new ByteArrayInputStream( json.getBytes(charset) ) ); - } - catch ( UnsupportedEncodingException e ) { - throw new RuntimeException( e ); - } - } - - @Override - public Map jsonToMap( InputStream in ) { - try { - return objectMapper.readValue( in, mapTypeReference ); - } - catch ( IOException e ) { - throw new JsonUnmarshalException( "Unable to unmarshal JSON to a Map.", e ); - } - } - - @Override - public List jsonToList( String json) { - return jsonToList( json, DEFAULT_ENCODING_UTF_8 ); - } - - @Override - public List jsonToList( String json, String charset ) { - try { - return jsonToList( new ByteArrayInputStream( json.getBytes(charset) ) ); - } - catch ( UnsupportedEncodingException e ) { - throw new RuntimeException( e ); - } - } - - @Override - public List jsonToList( InputStream in ) { - try { - return objectMapper.readValue( in, listTypeReference ); - } - catch ( IOException e ) { - throw new JsonUnmarshalException( "Unable to unmarshal JSON to a List.", e ); - } - } - - - @Override - public Object filepathToObject( String filePath ) { - try { - FileInputStream fileInputStream = new FileInputStream( filePath ); - return jsonToObject( fileInputStream ); - } - catch ( IOException e ) { - throw new RuntimeException( "Unable to load JSON file from: " + filePath ); - } - } - - @Override - public Map filepathToMap( String filePath ) { - try { - FileInputStream fileInputStream = new FileInputStream( filePath ); - return jsonToMap( fileInputStream ); - } - catch ( IOException e ) { - throw new RuntimeException( "Unable to load JSON file from: " + filePath ); - } - } - - @Override - public List filepathToList( String filePath ) { - try { - FileInputStream fileInputStream = new FileInputStream( filePath ); - return jsonToList( fileInputStream ); - } - catch ( IOException e ) { - throw new RuntimeException( "Unable to load JSON file from: " + filePath ); - } - } - - @Override - public Object classpathToObject( String classPath ) { - try { - InputStream inputStream = this.getClass().getResourceAsStream( classPath ); - - return jsonToObject( inputStream ); - } - catch ( Exception e ) { - throw new RuntimeException( "Unable to load JSON object from classPath : " + classPath, e ); - } - } - - @Override - public Map classpathToMap( String classPath ) { - try { - InputStream inputStream = this.getClass().getResourceAsStream( classPath ); - return jsonToMap( inputStream ); - } - catch ( Exception e ) { - throw new RuntimeException( "Unable to load JSON map from classPath : " + classPath, e ); - } - } - - @Override - public List classpathToList( String classPath ) { - try { - InputStream inputStream = this.getClass().getResourceAsStream( classPath ); - return jsonToList( inputStream ); - } - catch ( Exception e ) { - throw new RuntimeException( "Unable to load JSON map from classPath : " + classPath, e ); - } - } - - @Deprecated - @Override - public T jsonTo( InputStream in, TypeReference typeRef ) { - return streamToType(in, typeRef); - } - - @Deprecated - @Override - public T jsonTo( String json, TypeReference typeRef ) { - return streamToType( new ByteArrayInputStream( json.getBytes() ), typeRef ); - } - - @Override - public T stringToType( String json, TypeReference typeRef ) { - return streamToType( new ByteArrayInputStream( json.getBytes() ), typeRef ); - } - - @Override - public T stringToType( String json, Class aClass ) { - return streamToType( new ByteArrayInputStream( json.getBytes() ), aClass ); - } - - @Override - public T classpathToType( String classPath, TypeReference typeRef ) { - return streamToType( this.getClass().getResourceAsStream( classPath ), typeRef ); - } - @Override - public T classpathToType( String classPath, Class aClass ) { - return streamToType( this.getClass().getResourceAsStream( classPath ), aClass ); - } - - @Override - public T fileToType( String filePath, TypeReference typeRef ) { - try { - FileInputStream fileInputStream = new FileInputStream( filePath ); - return streamToType( fileInputStream, typeRef ); - } - catch ( IOException e ) { - throw new RuntimeException( "Unable to load JSON file from: " + filePath ); - } - } - - @Override - public T fileToType( String filePath, Class aClass ) { - try { - FileInputStream fileInputStream = new FileInputStream( filePath ); - return streamToType( fileInputStream, aClass ); - } - catch ( IOException e ) { - throw new RuntimeException( "Unable to load JSON file from: " + filePath ); - } - } - - @Override - public T streamToType( InputStream in, TypeReference typeRef ) { - try { - return objectMapper.readValue( in, typeRef ); - } - catch ( IOException e ) { - throw new JsonUnmarshalException( "Unable to unmarshal JSON to type: " + typeRef, e ); - } - } - - @Override - public T streamToType( InputStream in, Class aClass ) { - try { - return objectMapper.readValue( in, aClass ); - } - catch ( IOException e ) { - throw new JsonUnmarshalException( "Unable to unmarshal JSON to class: " + aClass, e ); - } - } - - - // SERIALIZATION - @Override - public String toJsonString( Object obj ) { - try { - return objectMapper.writeValueAsString( obj ); - } - catch ( IOException e ) { - throw new JsonMarshalException("Unable to serialize object : " + obj, e ); - } - } - - @Override - public String toPrettyJsonString( Object obj ) { - try { - return prettyPrintWriter.writeValueAsString( obj ); - } - catch ( IOException e ) { - throw new JsonMarshalException( "Unable to serialize object : " + obj, e ); - } - } - - @Override - public Object cloneJson( Object obj ) { - String string = this.toJsonString( obj ); - return this.jsonToObject( string ); - } -} diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java b/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java deleted file mode 100644 index 1d021446..00000000 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/JsonUtils.java +++ /dev/null @@ -1,258 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.util.List; -import java.util.Map; - -/** - * Static method convenience wrappers for a JsonUtil configured with a minimal ObjectMapper. - * - * The ObjectMapper use is configured to : - * Allow comments in the JSON strings, - * Hydrates all JSON Maps into LinkedHashMaps. - */ -public class JsonUtils { - - private static final JsonUtil util = new JsonUtilImpl(); - - /** - * Construct a JsonUtil with a Jackson ObjectMapper that has been preconfigured with custom - * Modules or Mixins. - */ - public static JsonUtil customJsonUtil( ObjectMapper mapper ) { - return new JsonUtilImpl( mapper ); - } - - /** - * Removes a key recursively from anywhere in a JSON document. - * NOTE: mutates its input. - * - * Deprecated: use JoltUtils instead - * - * @param json the Jackson Object version of the JSON document - * (contents changed by this call) - * @param keyToRemove the key to remove from the document - */ - @Deprecated - public static void removeRecursive( Object json, String keyToRemove ) { - if ( ( json == null ) || ( keyToRemove == null ) ) { - return; - } - if ( json instanceof Map ) { - @SuppressWarnings("unchecked") - Map jsonMap = (Map) json; - - // If this level of the tree has the key we are looking for, remove it - // Do the lookup instead of just the remove to avoid un-necessarily - // dying on ImmutableMaps. - if ( jsonMap.containsKey( keyToRemove ) ) { - jsonMap.remove( keyToRemove ); - } - - // regardless, recurse down the tree - for ( Object value : jsonMap.values() ) { - removeRecursive( value, keyToRemove ); - } - } - if ( json instanceof List ) { - for ( Object value : (List) json ) { - removeRecursive( value, keyToRemove ); - } - } - } - - /** - * Utility for test classes, so that they can inline json in a test class. - * Does a character level replacement of apostrophe (') with double quote ("). - * - * This means you can express a snippit of JSON without having to forward - * slash escape everything. - * - * This is character based, so don't have any apostrophes (') in your test - * data. - * - * @param javason JSON-ish string you want to turn into Maps-of-Maps - * @return Maps-of-Maps - */ - public static Map javason( String javason ) { - - String json = javason.replace( '\'', '"' ); - - return jsonToMap( new ByteArrayInputStream( json.getBytes() ) ); - } - - public static JsonUtil getDefaultJsonUtil() { - return util; - } - - //// All the methods listed below are static passthrus to the JsonUtil interface - public static Object jsonToObject( String json ) { - return util.jsonToObject( json ); - } - - public static Object jsonToObject( String json, String charset ) { - return util.jsonToObject( json, charset ); - } - - public static Object jsonToObject( InputStream in ) { - return util.jsonToObject( in ); - } - - public static Map jsonToMap( String json ) { - return util.jsonToMap( json ); - } - - public static Map jsonToMap( String json, String charset ) { - return util.jsonToMap( json, charset ); - } - - public static Map jsonToMap( InputStream in ) { - return util.jsonToMap( in ); - } - - public static List jsonToList( String json ) { - return util.jsonToList( json ); - } - - public static List jsonToList( String json, String charset ) { - return util.jsonToList( json, charset ); - } - - public static List jsonToList( InputStream in ) { - return util.jsonToList( in ); - } - - public static Object filepathToObject( String filePath ) { - return util.filepathToObject( filePath ); - } - - public static Map filepathToMap( String filePath ) { - return util.filepathToMap( filePath ); - } - - public static List filepathToList( String filePath ) { - return util.filepathToList( filePath ); - } - - public static Object classpathToObject( String classPath ) { - return util.classpathToObject( classPath ); - } - - public static Map classpathToMap( String classPath ) { - return util.classpathToMap( classPath ); - } - - public static List classpathToList( String classPath ) { - return util.classpathToList( classPath ); - } - - public static T classpathToType( String classPath, TypeReference typeRef ) { - return util.classpathToType( classPath, typeRef ); - } - - public static T classpathToType( String classPath, Class aClass ) { - return util.classpathToType( classPath, aClass ); - } - - public static T stringToType ( String json, TypeReference typeRef ) { - return util.stringToType( json, typeRef ); - } - - public static T stringToType( String json, Class aClass ) { - return util.stringToType( json, aClass ); - } - - public static T fileToType ( String filePath, TypeReference typeRef ) { - return util.fileToType( filePath, typeRef ); - } - public static T fileToType ( String filePath, Class aClass ) { - return util.fileToType( filePath, aClass ); - } - - public static T streamToType( InputStream in, TypeReference typeRef ) { - return util.streamToType( in, typeRef ); - } - public static T streamToType( InputStream in, Class aClass ) { - return util.streamToType( in, aClass ); - } - - /** - * Use the stringToType method instead. - */ - @Deprecated - public static T jsonTo( String json, TypeReference typeRef ) { - return util.stringToType( json, typeRef ); - } - - /** - * Use the streamToType method instead. - */ - @Deprecated - public static T jsonTo( InputStream in, TypeReference typeRef ) { - return util.streamToType( in, typeRef ); - } - - public static String toJsonString( Object obj ) { - return util.toJsonString( obj ); - } - - public static String toPrettyJsonString( Object obj ) { - return util.toPrettyJsonString( obj ); - } - - - /** - * Makes a deep copy of a Map object by converting it to a String and then - * back onto stock JSON objects. - * - * @param obj object tree to copy - * @return deep copy of the incoming obj - */ - public static Object cloneJson( Object obj ) { - // use the "configured" util for the serialize to String part - return util.cloneJson( obj ); - } - - /** - * Navigate inside a json object in quick and dirty way. - * - * Deprecated: use JoltUtils instead - * - * @param source the source json object - * @param paths the paths array to travel - * @return the object of Type at final destination - * @throws NullPointerException if the source is null - * @throws UnsupportedOperationException if the source is not Map or List - */ - @SuppressWarnings("unchecked") - @Deprecated - public static T navigate(Object source, Object... paths) throws NullPointerException, UnsupportedOperationException { - Object destination = source; - for (Object path : paths) { - if(destination == null) throw new NullPointerException("Navigation not possible on null object"); - if(destination instanceof Map) destination = ((Map) destination).get(path); - else if(path instanceof Integer && destination instanceof List) destination = ((List) destination).get((Integer)path); - else throw new UnsupportedOperationException("Navigation supports only Map and List source types and non-null String and Integer path types"); - } - return (T) destination; - } -} diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/ArrayOrderObliviousDiffy.java b/json-utils/src/main/java/io/joltcommunity/jolt/ArrayOrderObliviousDiffy.java similarity index 60% rename from json-utils/src/main/java/com/bazaarvoice/jolt/ArrayOrderObliviousDiffy.java rename to json-utils/src/main/java/io/joltcommunity/jolt/ArrayOrderObliviousDiffy.java index 42787f27..83f5d5b1 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/ArrayOrderObliviousDiffy.java +++ b/json-utils/src/main/java/io/joltcommunity/jolt/ArrayOrderObliviousDiffy.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import java.util.List; import java.util.Map; /** * Subclass of Diffy that does not care about JSON Array order. - * + *

* Useful for diffing JSON created from Java Tools that do not - * care about preserving JSON array order from call to call. - * *cough* DevAPI *cough* + * care about preserving JSON array order from call to call. + * *cough* DevAPI *cough* */ public class ArrayOrderObliviousDiffy extends Diffy { @@ -31,40 +32,54 @@ public ArrayOrderObliviousDiffy(JsonUtil jsonUtil) { super(jsonUtil); } - public ArrayOrderObliviousDiffy() {super();} + public ArrayOrderObliviousDiffy() { + super(); + } + + private static int findNextNonNullIndex(List list, int index) { + + while (index < list.size()) { + + if (list.get(index) != null) { + break; + } + + index++; + } + return index; + } @Override protected Result diffList(List expected, List actual) { // First we got thru an n^2 operation to compare the two lists - for (int expectedIndex=0; expectedIndex < expected.size(); expectedIndex++) { + for (int expectedIndex = 0; expectedIndex < expected.size(); expectedIndex++) { Object exp = expected.get(expectedIndex); - for(int actualIndex=0; actualIndex < actual.size(); actualIndex++) { + for (int actualIndex = 0; actualIndex < actual.size(); actualIndex++) { Object act = actual.get(actualIndex); - if ( exp == null && act == null ) { + if (exp == null && act == null) { // great, we "found a match" break; } - if( act != null && exp != null ) { + if (act != null && exp != null) { // Ideally the equals method finds a match, works for identical maps and simple Strings and numbers // Also try the sub-classable diffScalar if the normal ".equals" does not work - if ( act.equals(exp) || diffScalar( exp, act ).isEmpty() ) { + if (act.equals(exp) || diffScalar(exp, act).isEmpty()) { // if the indicies match nuke them expected.set(expectedIndex, null); actual.set(actualIndex, null); break; - } - else if ( (exp instanceof List && act instanceof List) || - (exp instanceof Map && act instanceof Map) ) { + } else if ((exp instanceof List && act instanceof List) || + (exp instanceof Map && act instanceof Map)) { // ugh, n^2 again, but enter from the top so a copy is made - Diffy.Result result = diff( exp, act ); - if ( result.isEmpty() ) { + Diffy.Result result = diff(exp, act); + if (result.isEmpty()) { // score! expected.set(expectedIndex, null); actual.set(actualIndex, null); @@ -76,22 +91,22 @@ else if ( (exp instanceof List && act instanceof List) || } // See if all the indicies in the arrays were nulled out. - if ( isAllNulls( expected ) && isAllNulls( actual ) ) { + if (isAllNulls(expected) && isAllNulls(actual)) { return new Result(); } // Now we make a second pass, "lining up" and subtractively Diffy-ing non-null elements. int actualIndex = 0; - for (int expectedIndex=0; expectedIndex < expected.size() && actualIndex < actual.size(); expectedIndex++) { + for (int expectedIndex = 0; expectedIndex < expected.size() && actualIndex < actual.size(); expectedIndex++) { - expectedIndex = findNextNonNullIndex( expected, expectedIndex ); - if ( expectedIndex >= expected.size() ) { + expectedIndex = findNextNonNullIndex(expected, expectedIndex); + if (expectedIndex >= expected.size()) { break; } - actualIndex = findNextNonNullIndex( actual, actualIndex ); - if ( actualIndex >= actual.size() ) { + actualIndex = findNextNonNullIndex(actual, actualIndex); + if (actualIndex >= actual.size()) { break; } @@ -99,37 +114,24 @@ else if ( (exp instanceof List && act instanceof List) || Object act = actual.get(actualIndex); // Do an actual "subtractive" diff, with "lined up" non-null items - Result subResult = diffHelper( exp, act ); - expected.set( expectedIndex, subResult.expected ); - actual.set( actualIndex, subResult.actual ); + Result subResult = diffHelper(exp, act); + expected.set(expectedIndex, subResult.expected); + actual.set(actualIndex, subResult.actual); actualIndex++; } - return new Result( expected, actual ); + return new Result(expected, actual); } - private boolean isAllNulls( List list ) { + private boolean isAllNulls(List list) { boolean isAllNulls = true; - for( int index=0; isAllNulls && index < list.size(); index++) { - if ( list.get(index) != null ) { + for (int index = 0; isAllNulls && index < list.size(); index++) { + if (list.get(index) != null) { isAllNulls = false; } } return isAllNulls; } - - private static int findNextNonNullIndex( List list, int index ) { - - while ( index < list.size() ) { - - if (list.get(index) != null ) { - break; - } - - index++; - } - return index; - } } diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java b/json-utils/src/main/java/io/joltcommunity/jolt/Diffy.java similarity index 60% rename from json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java rename to json-utils/src/main/java/io/joltcommunity/jolt/Diffy.java index 0bed2243..bd659577 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/Diffy.java +++ b/json-utils/src/main/java/io/joltcommunity/jolt/Diffy.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,19 +14,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import java.util.List; import java.util.Map; /** * JSON Diff tool that will walk two "JSON" objects simultaneously and identify mismatches. - * + *

* Algorithm : - * 1) make a copy of both input objects - * 2) walk both objects and _remove_ items that match - * 3) return what is left of the two objects in the Result - * + * 1) make a copy of both input objects + * 2) walk both objects and _remove_ items that match + * 3) return what is left of the two objects in the Result + *

* In the case a full / "sucessful" match, Diffy returns a Result object with isEmpty() == true. */ public class Diffy { @@ -39,63 +40,62 @@ public Diffy() { /** * Pass in a custom jsonUtil to use for the cloneJson method. */ - public Diffy( JsonUtil jsonUtil ) { + public Diffy(JsonUtil jsonUtil) { this.jsonUtil = jsonUtil; } public Result diff(Object expected, Object actual) { - Object expectedCopy = jsonUtil.cloneJson( expected ); - Object actualCopy = jsonUtil.cloneJson( actual ); - return diffHelper( expectedCopy, actualCopy ); + Object expectedCopy = jsonUtil.cloneJson(expected); + Object actualCopy = jsonUtil.cloneJson(actual); + return diffHelper(expectedCopy, actualCopy); } - @SuppressWarnings( "unchecked" ) + @SuppressWarnings("unchecked") protected Result diffHelper(Object expected, Object actual) { if (expected instanceof Map) { if (!(actual instanceof Map)) { - return new Result( expected, actual ); + return new Result(expected, actual); } - return diffMap( (Map) expected, (Map) actual ); - } - else if (expected instanceof List) { + return diffMap((Map) expected, (Map) actual); + } else if (expected instanceof List) { if (!(actual instanceof List)) { - return new Result( expected, actual ); + return new Result(expected, actual); } - return diffList( (List) expected, (List) actual ); + return diffList((List) expected, (List) actual); } - return this.diffScalar( expected, actual ); + return this.diffScalar(expected, actual); } protected Result diffMap(Map expected, Map actual) { // Make a copy of the expected keySet so that we can remove things w/out concurrent mod exceptions - String[] expectedKeys = expected.keySet().toArray( new String[ expected.keySet().size() ] ); - for (String key : expectedKeys ) { - Result subResult = diffHelper( expected.get( key ), actual.get( key ) ); + String[] expectedKeys = expected.keySet().toArray(new String[expected.keySet().size()]); + for (String key : expectedKeys) { + Result subResult = diffHelper(expected.get(key), actual.get(key)); if (subResult.isEmpty()) { - expected.remove( key ); - actual.remove( key ); + expected.remove(key); + actual.remove(key); } } if (expected.isEmpty() && actual.isEmpty()) { return new Result(); } - return new Result( expected, actual ); + return new Result(expected, actual); } protected Result diffList(List expected, List actual) { - int shortlen = Math.min( expected.size(), actual.size() ); + int shortlen = Math.min(expected.size(), actual.size()); boolean emptyDiff = true; - for (int i=0; i * A sucessful/identical match returns isEmpty() == true. */ public static class Result { public Object expected; public Object actual; - public Result() {} + + public Result() { + } + public Result(Object expected, Object actual) { this.expected = expected; this.actual = actual; } + public boolean isEmpty() { return (expected == null) && (actual == null); } @Override public String toString() { - if(isEmpty()) { + if (isEmpty()) { return "There is no difference!"; - } - else { + } else { return "\nExpected:\n" + JsonUtils.toPrettyJsonString(expected) + "\n" + - "\nActual\n" + JsonUtils.toPrettyJsonString(actual); + "\nActual\n" + JsonUtils.toPrettyJsonString(actual); } } } diff --git a/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtil.java b/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtil.java new file mode 100644 index 00000000..cf3768a8 --- /dev/null +++ b/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtil.java @@ -0,0 +1,110 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import tools.jackson.core.type.TypeReference; + +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * Utility methods for getting JSON content loaded from + * the filesystem, the classpath, or in memory Strings. + *

+ * Also has methods to serialize Java object to JSON strings. + *

+ * Implementations of this interface can specify their own + * Jackson ObjectMapper so that Domain specific Java Objects + * can successfully be serialized and de-serialized. + */ +public interface JsonUtil { + + // DE-SERIALIZATION + Object jsonToObject(String json); + + Object jsonToObject(String json, String charset); + + Object jsonToObject(InputStream in); + + Map jsonToMap(String json); + + Map jsonToMap(String json, String charset); + + Map jsonToMap(InputStream in); + + List jsonToList(String json); + + List jsonToList(String json, String charset); + + List jsonToList(InputStream in); + + Object filepathToObject(String filePath); + + Map filepathToMap(String filePath); + + List filepathToList(String filePath); + + Object classpathToObject(String classPath); + + Map classpathToMap(String classPath); + + List classpathToList(String classPath); + + /** + * Use the stringToType method instead. + */ + @Deprecated + T jsonTo(String json, TypeReference typeRef); + + /** + * Use the streamToType method instead. + */ + @Deprecated + T jsonTo(InputStream in, TypeReference typeRef); + + T stringToType(String json, TypeReference typeRef); + + T stringToType(String json, Class aClass); + + T classpathToType(String classPath, TypeReference typeRef); + + T classpathToType(String classPath, Class aClass); + + T fileToType(String filePath, TypeReference typeRef); + + T fileToType(String filePath, Class aClass); + + T streamToType(InputStream in, TypeReference typeRef); + + T streamToType(InputStream in, Class aClass); + + String toJsonString(Object obj); + + String toPrettyJsonString(Object obj); + + /** + * Makes a deep copy of a Object object by converting it to a String and then + * back onto stock JSON objects. + *

+ * Leverages Serialization + * + * @param obj object tree to copy + * @return deep copy of the incoming obj + */ + Object cloneJson(Object obj); +} diff --git a/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtilImpl.java b/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtilImpl.java new file mode 100644 index 00000000..7028fb79 --- /dev/null +++ b/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtilImpl.java @@ -0,0 +1,311 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import io.joltcommunity.jolt.exception.JsonMarshalException; +import io.joltcommunity.jolt.exception.JsonUnmarshalException; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.Version; +import tools.jackson.core.json.JsonFactory; +import tools.jackson.core.json.JsonReadFeature; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ObjectWriter; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + + +import java.io.*; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Implementation of JsonUtil that allows the user to provide a configured + * Jackson ObjectMapper. + *

+ * All IOExceptions are caught, wrapped with context, and rethrown as RuntimeExceptions. + */ +public class JsonUtilImpl implements JsonUtil { + // Default Encoding for String to JSON operations + public static final String DEFAULT_ENCODING_UTF_8 = "utf-8"; + private static final TypeReference> mapTypeReference = + new TypeReference<>() { + }; + private static final TypeReference> listTypeReference = + new TypeReference<>() { + }; + // thread safe: http://wiki.fasterxml.com/JacksonFAQThreadSafety + private final ObjectMapper objectMapper; + private final ObjectWriter prettyPrintWriter; + + /** + * By allowing the user to provide an ObjectMapper, it can be configured with + * knowledge of how to marshall and un-marshall your domain objects. + * + * @param objectMapper a configured Jackson ObjectMapper + */ + public JsonUtilImpl(ObjectMapper objectMapper) { + + this.objectMapper = objectMapper == null ? buildStockJoltObjectMapper() : objectMapper; + prettyPrintWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); + } + + public JsonUtilImpl() { + this.objectMapper = buildStockJoltObjectMapper(); + prettyPrintWriter = this.objectMapper.writerWithDefaultPrettyPrinter(); + } + + + public static ObjectMapper buildStockJoltObjectMapper() { + + // All Json maps should be deserialized into LinkedHashMaps. + SimpleModule stockModule = new SimpleModule("stockJoltMapping", new Version(1, 0, 0, null, null, null)) + .addAbstractTypeMapping(Map.class, LinkedHashMap.class); + + JsonFactory jsonFactory = JsonFactory.builder() + .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS) + .build(); + + return JsonMapper.builder(jsonFactory) + .addModule(stockModule) + .configure(JsonReadFeature.ALLOW_JAVA_COMMENTS, true) + .build(); + + } + + + // DE-SERIALIZATION + @Override + public Object jsonToObject(String json) { + return jsonToObject(json, DEFAULT_ENCODING_UTF_8); + } + + @Override + public Object jsonToObject(String json, String charset) { + try (InputStream in = new ByteArrayInputStream(json.getBytes(charset))) { + return jsonToObject(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Object jsonToObject(InputStream in) { + try { + return objectMapper.readValue(in, Object.class); + } catch (JacksonException e) { + throw new JsonUnmarshalException("Unable to unmarshal JSON to an Object.", e); + } + } + + @Override + public Map jsonToMap(String json) { + return jsonToMap(json, DEFAULT_ENCODING_UTF_8); + } + + @Override + public Map jsonToMap(String json, String charset) { + try (InputStream in = new ByteArrayInputStream(json.getBytes(charset))) { + return jsonToMap(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public Map jsonToMap(InputStream in) { + try { + return objectMapper.readValue(in, mapTypeReference); + } catch (JacksonException e) { + throw new JsonUnmarshalException("Unable to unmarshal JSON to a Map.", e); + } + } + + @Override + public List jsonToList(String json) { + return jsonToList(json, DEFAULT_ENCODING_UTF_8); + } + + @Override + public List jsonToList(String json, String charset) { + try (InputStream in = new ByteArrayInputStream(json.getBytes(charset))) { + return jsonToList(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public List jsonToList(InputStream in) { + try { + return objectMapper.readValue(in, listTypeReference); + } catch (JacksonException e) { + throw new JsonUnmarshalException("Unable to unmarshal JSON to a List.", e); + } + } + + + @Override + public Object filepathToObject(String filePath) { + try (FileInputStream fileInputStream = new FileInputStream(filePath)) { + return jsonToObject(fileInputStream); + } catch (IOException e) { + throw new RuntimeException("Unable to load JSON file from: " + filePath); + } + } + + @Override + public Map filepathToMap(String filePath) { + try (FileInputStream fileInputStream = new FileInputStream(filePath)) { + return jsonToMap(fileInputStream); + } catch (IOException e) { + throw new RuntimeException("Unable to load JSON file from: " + filePath); + } + } + + @Override + public List filepathToList(String filePath) { + try (FileInputStream fileInputStream = new FileInputStream(filePath)) { + return jsonToList(fileInputStream); + } catch (IOException e) { + throw new RuntimeException("Unable to load JSON file from: " + filePath); + } + } + + @Override + public Object classpathToObject(String classPath) { + try (InputStream inputStream = this.getClass().getResourceAsStream(classPath)) { + return jsonToObject(inputStream); + } catch (Exception e) { + throw new RuntimeException("Unable to load JSON object from classPath : " + classPath, e); + } + } + + @Override + public Map classpathToMap(String classPath) { + try (InputStream inputStream = this.getClass().getResourceAsStream(classPath)) { + return jsonToMap(inputStream); + } catch (Exception e) { + throw new RuntimeException("Unable to load JSON map from classPath : " + classPath, e); + } + } + + @Override + public List classpathToList(String classPath) { + try (InputStream inputStream = this.getClass().getResourceAsStream(classPath)) { + return jsonToList(inputStream); + } catch (Exception e) { + throw new RuntimeException("Unable to load JSON map from classPath : " + classPath, e); + } + } + + @Deprecated + @Override + public T jsonTo(InputStream in, TypeReference typeRef) { + return streamToType(in, typeRef); + } + + @Deprecated + @Override + public T jsonTo(String json, TypeReference typeRef) { + return streamToType(new ByteArrayInputStream(json.getBytes()), typeRef); + } + + @Override + public T stringToType(String json, TypeReference typeRef) { + return streamToType(new ByteArrayInputStream(json.getBytes()), typeRef); + } + + @Override + public T stringToType(String json, Class aClass) { + return streamToType(new ByteArrayInputStream(json.getBytes()), aClass); + } + + @Override + public T classpathToType(String classPath, TypeReference typeRef) { + return streamToType(this.getClass().getResourceAsStream(classPath), typeRef); + } + + @Override + public T classpathToType(String classPath, Class aClass) { + return streamToType(this.getClass().getResourceAsStream(classPath), aClass); + } + + @Override + public T fileToType(String filePath, TypeReference typeRef) { + try (FileInputStream fileInputStream = new FileInputStream(filePath)) { + return streamToType(fileInputStream, typeRef); + } catch (IOException e) { + throw new RuntimeException("Unable to load JSON file from: " + filePath); + } + } + + @Override + public T fileToType(String filePath, Class aClass) { + try (FileInputStream fileInputStream = new FileInputStream(filePath)) { + return streamToType(fileInputStream, aClass); + } catch (IOException e) { + throw new RuntimeException("Unable to load JSON file from: " + filePath); + } + } + + @Override + public T streamToType(InputStream in, TypeReference typeRef) { + try { + return objectMapper.readValue(in, typeRef); + } catch (JacksonException e) { + throw new JsonUnmarshalException("Unable to unmarshal JSON to type: " + typeRef, e); + } + } + + @Override + public T streamToType(InputStream in, Class aClass) { + try { + return objectMapper.readValue(in, aClass); + } catch (JacksonException e) { + throw new JsonUnmarshalException("Unable to unmarshal JSON to class: " + aClass, e); + } + } + + + // SERIALIZATION + @Override + public String toJsonString(Object obj) { + try { + return objectMapper.writeValueAsString(obj); + } catch (JacksonException e) { + throw new JsonMarshalException("Unable to serialize object : " + obj, e); + } + } + + @Override + public String toPrettyJsonString(Object obj) { + try { + return prettyPrintWriter.writeValueAsString(obj); + } catch (JacksonException e) { + throw new JsonMarshalException("Unable to serialize object : " + obj, e); + } + } + + @Override + public Object cloneJson(Object obj) { + String string = this.toJsonString(obj); + return this.jsonToObject(string); + } +} diff --git a/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtils.java b/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtils.java new file mode 100644 index 00000000..0ce9f575 --- /dev/null +++ b/json-utils/src/main/java/io/joltcommunity/jolt/JsonUtils.java @@ -0,0 +1,263 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.ObjectMapper; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; +import java.util.Map; + +/** + * Static method convenience wrappers for a JsonUtil configured with a minimal ObjectMapper. + *

+ * The ObjectMapper use is configured to : + * Allow comments in the JSON strings, + * Hydrates all JSON Maps into LinkedHashMaps. + */ +public class JsonUtils { + + private static final JsonUtil util = new JsonUtilImpl(); + + /** + * Construct a JsonUtil with a Jackson ObjectMapper that has been preconfigured with custom + * Modules or Mixins. + */ + public static JsonUtil customJsonUtil(ObjectMapper mapper) { + return new JsonUtilImpl(mapper); + } + + /** + * Removes a key recursively from anywhere in a JSON document. + * NOTE: mutates its input. + *

+ * Deprecated: use JoltUtils instead + * + * @param json the Jackson Object version of the JSON document + * (contents changed by this call) + * @param keyToRemove the key to remove from the document + */ + @Deprecated + public static void removeRecursive(Object json, String keyToRemove) { + if ((json == null) || (keyToRemove == null)) { + return; + } + if (json instanceof Map) { + @SuppressWarnings("unchecked") + Map jsonMap = (Map) json; + + // If this level of the tree has the key we are looking for, remove it + // Do the lookup instead of just the remove to avoid un-necessarily + // dying on ImmutableMaps. + if (jsonMap.containsKey(keyToRemove)) { + jsonMap.remove(keyToRemove); + } + + // regardless, recurse down the tree + for (Object value : jsonMap.values()) { + removeRecursive(value, keyToRemove); + } + } + if (json instanceof List) { + for (Object value : (List) json) { + removeRecursive(value, keyToRemove); + } + } + } + + /** + * Utility for test classes, so that they can inline json in a test class. + * Does a character level replacement of apostrophe (') with double quote ("). + *

+ * This means you can express a snippit of JSON without having to forward + * slash escape everything. + *

+ * This is character based, so don't have any apostrophes (') in your test + * data. + * + * @param javason JSON-ish string you want to turn into Maps-of-Maps + * @return Maps-of-Maps + */ + public static Map javason(String javason) { + + String json = javason.replace('\'', '"'); + + return jsonToMap(new ByteArrayInputStream(json.getBytes())); + } + + public static JsonUtil getDefaultJsonUtil() { + return util; + } + + /// / All the methods listed below are static passthrus to the JsonUtil interface + public static Object jsonToObject(String json) { + return util.jsonToObject(json); + } + + public static Object jsonToObject(String json, String charset) { + return util.jsonToObject(json, charset); + } + + public static Object jsonToObject(InputStream in) { + return util.jsonToObject(in); + } + + public static Map jsonToMap(String json) { + return util.jsonToMap(json); + } + + public static Map jsonToMap(String json, String charset) { + return util.jsonToMap(json, charset); + } + + public static Map jsonToMap(InputStream in) { + return util.jsonToMap(in); + } + + public static List jsonToList(String json) { + return util.jsonToList(json); + } + + public static List jsonToList(String json, String charset) { + return util.jsonToList(json, charset); + } + + public static List jsonToList(InputStream in) { + return util.jsonToList(in); + } + + public static Object filepathToObject(String filePath) { + return util.filepathToObject(filePath); + } + + public static Map filepathToMap(String filePath) { + return util.filepathToMap(filePath); + } + + public static List filepathToList(String filePath) { + return util.filepathToList(filePath); + } + + public static Object classpathToObject(String classPath) { + return util.classpathToObject(classPath); + } + + public static Map classpathToMap(String classPath) { + return util.classpathToMap(classPath); + } + + public static List classpathToList(String classPath) { + return util.classpathToList(classPath); + } + + public static T classpathToType(String classPath, TypeReference typeRef) { + return util.classpathToType(classPath, typeRef); + } + + public static T classpathToType(String classPath, Class aClass) { + return util.classpathToType(classPath, aClass); + } + + public static T stringToType(String json, TypeReference typeRef) { + return util.stringToType(json, typeRef); + } + + public static T stringToType(String json, Class aClass) { + return util.stringToType(json, aClass); + } + + public static T fileToType(String filePath, TypeReference typeRef) { + return util.fileToType(filePath, typeRef); + } + + public static T fileToType(String filePath, Class aClass) { + return util.fileToType(filePath, aClass); + } + + public static T streamToType(InputStream in, TypeReference typeRef) { + return util.streamToType(in, typeRef); + } + + public static T streamToType(InputStream in, Class aClass) { + return util.streamToType(in, aClass); + } + + /** + * Use the stringToType method instead. + */ + @Deprecated + public static T jsonTo(String json, TypeReference typeRef) { + return util.stringToType(json, typeRef); + } + + /** + * Use the streamToType method instead. + */ + @Deprecated + public static T jsonTo(InputStream in, TypeReference typeRef) { + return util.streamToType(in, typeRef); + } + + public static String toJsonString(Object obj) { + return util.toJsonString(obj); + } + + public static String toPrettyJsonString(Object obj) { + return util.toPrettyJsonString(obj); + } + + + /** + * Makes a deep copy of a Object object by converting it to a String and then + * back onto stock JSON objects. + * + * @param obj object tree to copy + * @return deep copy of the incoming obj + */ + public static Object cloneJson(Object obj) { + // use the "configured" util for the serialize to String part + return util.cloneJson(obj); + } + + /** + * Navigate inside a json object in quick and dirty way. + *

+ * Deprecated: use JoltUtils instead + * + * @param source the source json object + * @param paths the paths array to travel + * @return the object of Type at final destination + * @throws NullPointerException if the source is null + * @throws UnsupportedOperationException if the source is not Map or List + */ + @SuppressWarnings("unchecked") + @Deprecated + public static T navigate(Object source, Object... paths) throws NullPointerException, UnsupportedOperationException { + Object destination = source; + for (Object path : paths) { + if (destination == null) throw new NullPointerException("Navigation not possible on null object"); + if (destination instanceof Map) destination = ((Map) destination).get(path); + else if (path instanceof Integer && destination instanceof List) + destination = ((List) destination).get((Integer) path); + else + throw new UnsupportedOperationException("Navigation supports only Map and List source types and non-null String and Integer path types"); + } + return (T) destination; + } +} diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/exception/JsonMarshalException.java b/json-utils/src/main/java/io/joltcommunity/jolt/exception/JsonMarshalException.java similarity index 70% rename from json-utils/src/main/java/com/bazaarvoice/jolt/exception/JsonMarshalException.java rename to json-utils/src/main/java/io/joltcommunity/jolt/exception/JsonMarshalException.java index f7d5866d..1b2a349f 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/exception/JsonMarshalException.java +++ b/json-utils/src/main/java/io/joltcommunity/jolt/exception/JsonMarshalException.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.exception; +package io.joltcommunity.jolt.exception; public class JsonMarshalException extends RuntimeException { - public JsonMarshalException( String msg ) { + private static final long serialVersionUID = 2234726763666700472L; + + public JsonMarshalException(String msg) { super(msg); } - public JsonMarshalException( String msg, Throwable t ) { + public JsonMarshalException(String msg, Throwable t) { super(msg, t); } diff --git a/json-utils/src/main/java/com/bazaarvoice/jolt/exception/JsonUnmarshalException.java b/json-utils/src/main/java/io/joltcommunity/jolt/exception/JsonUnmarshalException.java similarity index 70% rename from json-utils/src/main/java/com/bazaarvoice/jolt/exception/JsonUnmarshalException.java rename to json-utils/src/main/java/io/joltcommunity/jolt/exception/JsonUnmarshalException.java index 76c5327c..f274c986 100644 --- a/json-utils/src/main/java/com/bazaarvoice/jolt/exception/JsonUnmarshalException.java +++ b/json-utils/src/main/java/io/joltcommunity/jolt/exception/JsonUnmarshalException.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.exception; +package io.joltcommunity.jolt.exception; public class JsonUnmarshalException extends RuntimeException { - public JsonUnmarshalException( String msg ) { + private static final long serialVersionUID = 6668003027260407157L; + + public JsonUnmarshalException(String msg) { super(msg); } - public JsonUnmarshalException( String msg, Throwable t ) { + public JsonUnmarshalException(String msg, Throwable t) { super(msg, t); } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/DiffyUnitTest.java b/json-utils/src/test/java/com/bazaarvoice/jolt/DiffyUnitTest.java deleted file mode 100644 index 919aed82..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/DiffyUnitTest.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import org.testng.Assert; -import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -public class DiffyUnitTest { - - private Diffy unit; - - @BeforeMethod - public void setup() { - this.unit = new Diffy(); - } - - @AfterMethod - public void teardown() { - this.unit = null; - } - - private void testScalars(Object expected, Object actual, boolean expectDiff) { - Diffy.Result result = this.unit.diff( expected, actual ); - if (expectDiff) { - Assert.assertEquals( expected, result.expected ); - Assert.assertEquals( actual, result.actual ); - } - } - - private static final Object[] SCALARS = new Object[] { - null, 1, 2, true, false, 3.14, 2.71, "foo", "bar", new ArrayList(), new HashMap() - }; - - @Test - public void testAllTheScalars() { - for (int i=0; i h1 = new HashMap<>(); - { - h1.put( "a", "a" ); - Map bMap = new HashMap<>(); - bMap.put( "c", "c" ); - bMap.put( "d", "d" ); - h1.put( "b", bMap ); - } - - Map h2 = new HashMap<>(); - { - Map bMap = new HashMap<>(); - bMap.put( "c", "c" ); - bMap.put( "d", "d" ); - h2.put( "b", bMap ); - h2.put( "a", "a" ); - } - - Assert.assertTrue( h1.equals( h2 ), "1->2 Two HashMaps with the same things should be equal." ); - Assert.assertEquals( h1.hashCode(), h2.hashCode(), "1->2 Two HashMaps with the same things should have the same hashCode." ); - - Assert.assertTrue( h2.equals( h1 ), "2->1 Two HashMaps with the same things should be equal." ); - Assert.assertEquals( h2.hashCode(), h1.hashCode(), "2->1 Two HashMaps with the same things should have the same hashCode." ); - - Map lh1 = new LinkedHashMap<>(); - { - lh1.put( "a", "a" ); - Map bMap = new HashMap<>(); - bMap.put( "c", "c" ); - bMap.put( "d", "d" ); - lh1.put( "b", bMap ); - } - - Assert.assertTrue( lh1.equals( h2 ), "lh1->2 Two HashMaps with the same things should be equal." ); - Assert.assertEquals( lh1.hashCode(), h2.hashCode(), "lh1->2 Two HashMaps with the same things should have the same hashCode." ); - - Assert.assertTrue( lh1.equals( h1 ), "lh1->1 Two HashMaps with the same things should be equal." ); - Assert.assertEquals( lh1.hashCode(), h1.hashCode(), "lh1->1 Two HashMaps with the same things should have the same hashCode." ); - - Map d1 = new HashMap<>(); - { - d1.put( "a", "a" ); - Map bMap = new HashMap<>(); - bMap.put( "c", "c" ); - bMap.put( "d", "E" ); - d1.put( "b", bMap ); - } - - Assert.assertFalse( d1.equals( h2 ), "Two HashMaps that should not be equal." ); - Assert.assertNotEquals( d1.hashCode(), h2.hashCode(), "lh1->2 Two HashMaps should not have the same hashCode ." ); - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java b/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java deleted file mode 100644 index 2d365e3a..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/JsonUtilsTest.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import com.beust.jcommander.internal.Sets; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Maps; -import org.testng.Assert; -import org.testng.annotations.BeforeClass; -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; -import org.testng.collections.Lists; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class JsonUtilsTest { - - private Diffy diffy = new Diffy(); - - private Map ab = ImmutableMap.builder().put( "a", "b" ).build(); - private Map cd = ImmutableMap.builder().put( "c", "d" ).build(); - private Map top = ImmutableMap.builder().put( "A", ab ).put( "B", cd ).build(); - - private String jsonSourceString = "{ " + - " \"a\": { " + - " \"b\": [ " + - " 0, " + - " 1, " + - " 2, " + - " 1.618 " + - " ] " + - " }, " + - " \"p\": [ " + - " \"m\", " + - " \"n\", " + - " { " + - " \"1\": 1, " + - " \"2\": 2, " + - " \"pi\": 3.14159 " + - " } " + - " ], " + - " \"x\": \"y\" " + - "}\n"; - - private Object jsonSource; - - @BeforeClass - @SuppressWarnings("unchecked") - public void setup() throws IOException { - jsonSource = JsonUtils.jsonToObject(jsonSourceString); - // added for type cast checking - Set aSet = Sets.newHashSet(); - aSet.add("i"); - aSet.add("j"); - ((Map) jsonSource).put("s", aSet); - } - - @DataProvider - public Object[][] removeRecursiveCases() { - - Map empty = ImmutableMap.builder().build(); - Map barToFoo = ImmutableMap.builder().put( "bar", "foo" ).build(); - Map fooToBar = ImmutableMap.builder().put( "foo", "bar" ).build(); - return new Object[][] { - { null, null, null }, - { null, "foo", null }, - { "foo", null, "foo" }, - { "foo", "foo", "foo" }, - { Maps.newHashMap(), "foo", empty }, - { Maps.newHashMap( barToFoo ), "foo", barToFoo }, - { Maps.newHashMap( fooToBar ), "foo", empty }, - { Lists.newArrayList(), "foo", ImmutableList.builder().build() }, - { - Lists.newArrayList( ImmutableList.builder() - .add( Maps.newHashMap( barToFoo ) ) - .build() ), - "foo", - ImmutableList.builder() - .add( barToFoo ) - .build() - }, - { - Lists.newArrayList( ImmutableList.builder() - .add( Maps.newHashMap( fooToBar ) ) - .build() ), - "foo", - ImmutableList.builder() - .add( empty ) - .build() - } - }; - } - - - @Test(dataProvider = "removeRecursiveCases") - @SuppressWarnings("deprecation") - public void testRemoveRecursive(Object json, String key, Object expected) throws IOException { - - JsonUtils.removeRecursive( json, key ); - - Diffy.Result result = diffy.diff( expected, json ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } - - @Test - @SuppressWarnings("deprecation") - public void runFixtureTests() throws IOException { - - String testFixture = "/jsonUtils/jsonUtils-removeRecursive.json"; - @SuppressWarnings("unchecked") - List> tests = (List>) JsonUtils.classpathToObject( testFixture ); - - for ( Map testUnit : tests ) { - - Object data = testUnit.get( "input" ); - String toRemove = (String) testUnit.get( "remove" ); - Object expected = testUnit.get( "expected" ); - - JsonUtils.removeRecursive( data, toRemove ); - - Diffy.Result result = diffy.diff( expected, data ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); - } - } - } - - @Test - public void validateJacksonClosesInputStreams() { - - final Set closedSet = new HashSet<>(); - ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream( "{ \"a\" : \"b\" }".getBytes() ) { - @Override - public void close() throws IOException { - closedSet.add("closed"); - super.close(); - } - }; - - // Pass our wrapped InputStream to Jackson via JsonUtils. - Map map = JsonUtils.jsonToMap( byteArrayInputStream ); - - // Verify that we in fact loaded some data - Assert.assertNotNull( map ); - Assert.assertEquals( 1, map.size() ); - - // Verify that the close method was in fact called on the InputStream - Assert.assertEquals( 1, closedSet.size() ); - } - - @DataProvider (parallel = true) - public Iterator coordinates() throws IOException { - List testCases = com.beust.jcommander.internal.Lists.newArrayList(); - - testCases.add(new Object[] { 0, new Object[] {"a", "b", 0}} ); - testCases.add(new Object[] { 1, new Object[] {"a", "b", 1}} ); - testCases.add(new Object[] { 2, new Object[] {"a", "b", 2}} ); - testCases.add(new Object[] { 1.618, new Object[] {"a", "b", 3}} ); - testCases.add(new Object[] { "m", new Object[] {"p", 0}} ); - testCases.add(new Object[] { "n", new Object[] {"p", 1}} ); - testCases.add(new Object[] { 1, new Object[] {"p", 2, "1"}} ); - testCases.add(new Object[] { 2, new Object[] {"p", 2, "2"}} ); - testCases.add(new Object[] { 3.14159, new Object[] {"p", 2, "pi"}} ); - testCases.add(new Object[] { "y", new Object[] {"x"}} ); - - testCases.add(new Object[] { ((Map) jsonSource).get("a"), new Object[] {"a"}} ); - testCases.add(new Object[] { ((Map)(((Map) jsonSource).get("a"))).get("b"), new Object[] {"a", "b"}} ); - testCases.add(new Object[] { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(0), new Object[] {"a", "b", 0}} ); - testCases.add(new Object[] { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(1), new Object[] {"a", "b", 1}} ); - testCases.add(new Object[] { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(2), new Object[] {"a", "b", 2}} ); - testCases.add(new Object[] { ((List)((Map)(((Map) jsonSource).get("a"))).get("b")).get(3), new Object[] {"a", "b", 3}} ); - testCases.add(new Object[] { ((Map) jsonSource).get("p"), new Object[] {"p"}} ); - testCases.add(new Object[] { ((List)(((Map) jsonSource).get("p"))).get(0), new Object[] {"p", 0}} ); - testCases.add(new Object[] { ((List)(((Map) jsonSource).get("p"))).get(1), new Object[] {"p", 1}} ); - testCases.add(new Object[] { ((List)(((Map) jsonSource).get("p"))).get(2), new Object[] {"p", 2}} ); - testCases.add(new Object[] { ((Map)((List)(((Map) jsonSource).get("p"))).get(2)).get("1"), new Object[] {"p", 2, "1"}} ); - testCases.add(new Object[] { ((Map)((List)(((Map) jsonSource).get("p"))).get(2)).get("2"), new Object[] {"p", 2, "2"}} ); - testCases.add(new Object[] { ((Map)((List)(((Map) jsonSource).get("p"))).get(2)).get("pi"), new Object[] {"p", 2, "pi"}} ); - testCases.add(new Object[] { ((Map) jsonSource).get("x"), new Object[] {"x"}} ); - - return testCases.iterator(); - } - - /** - * Method: navigate(Object source, Object... paths) - */ - @Test (dataProvider = "coordinates") - @SuppressWarnings("deprecation") - public void navigator(Object expected, Object[] path) throws Exception { - Object actual = JsonUtils.navigate(jsonSource, path); - Assert.assertEquals(actual, expected); - } - -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java b/json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java deleted file mode 100644 index 4e469986..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/TestInstanceOfVSEnumSwitch.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2013 Bazaarvoice, 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.bazaarvoice.jolt; - -import org.testng.Assert; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Small test to see if it is more efficient to do instanceof checks - * or to have Concrete subclasses have an Enum type. - * - * Answer : - * InstanceOf Test looping 50000000 - * Took : 24156 - * Typed Test looping 50000000 - * Took : 23571 - * - * It doesn't really matter ;) - */ -public class TestInstanceOfVSEnumSwitch { - - enum Type { - STRING, - INTEGER, - BOOLEAN, - DATE, - LOGICAL - } - - interface EnumBaseInterface { - Type getType(); - List getValues(); - } - - class StringEnum implements EnumBaseInterface { - public Type getType() { return Type.STRING; } - public List getValues() { return Arrays.asList("A", "B"); } - } - class IntegerEnum implements EnumBaseInterface { - public Type getType() { return Type.INTEGER; } - public List getValues() { return Arrays.asList(1, 2); } - } - class BooleanEnum implements EnumBaseInterface { - public Type getType() { return Type.BOOLEAN; } - public List getValues() { return Arrays.asList( true, false); } - } - class DateEnum implements EnumBaseInterface { - public Type getType() { return Type.DATE; } - public List getValues() { return Arrays.asList("10", "11"); } - } - class LogicalEnum implements EnumBaseInterface { - public Type getType() { return Type.LOGICAL; } - public List getValues() { return new ArrayList<>(); } - } - - private static final int LOOP_COUNT = 1000 * 1000 * 50; - - //@Test - public void testTyped () { - - System.out.println( "Typed Test looping " + LOOP_COUNT ); - long begin = System.currentTimeMillis(); - for ( int index = 0; index < LOOP_COUNT; index++) { - int typeToMake = index % 5; - - EnumBaseInterface t; - switch( typeToMake ) { - case 0 : - t = new StringEnum(); break; - case 1 : - t = new IntegerEnum(); break; - case 2 : - t = new BooleanEnum(); break; - case 3 : - t = new DateEnum(); break; - case 4 : - t = new LogicalEnum(); break; - default : - throw new RuntimeException("pants"); - } - - switch( t.getType() ) { - case STRING: - StringEnum s = (StringEnum) t; - List sValues = s.getValues(); - Assert.assertEquals( Arrays.asList("A", "B"), sValues ); - break; - case INTEGER: - IntegerEnum i = (IntegerEnum) t; - List iValues = i.getValues(); - Assert.assertEquals( Arrays.asList( 1, 2), iValues ); - break; - case BOOLEAN: - BooleanEnum b = (BooleanEnum) t; - List bValues = b.getValues(); - Assert.assertEquals( Arrays.asList(true, false), bValues ); - break; - case DATE: - DateEnum d = (DateEnum) t; - List dValues = d.getValues(); - Assert.assertEquals( Arrays.asList("10", "11"), dValues ); - break; - case LOGICAL: - LogicalEnum l = (LogicalEnum) t; - List lValues = l.getValues(); - Assert.assertEquals( 0, lValues.size() ); - break; - } - } - - long end = System.currentTimeMillis(); - - System.out.println( "Took : " + ( end - begin ) ); - } - - - interface InstanceOfInterface { - List getValues(); - } - - class StringInstanceOf implements InstanceOfInterface { - public List getValues() { return Arrays.asList("A", "B"); } - } - class IntegerInstanceOf implements InstanceOfInterface { - public List getValues() { return Arrays.asList(1, 2); } - } - class BooleanInstanceOf implements InstanceOfInterface { - public List getValues() { return Arrays.asList( true, false); } - } - class DateInstanceOf implements InstanceOfInterface { - public List getValues() { return Arrays.asList("10", "11"); } - } - class LogicalInstanceOf implements InstanceOfInterface { - public List getValues() { return new ArrayList<>(); } - } - - - //@Test - public void testInstanceOf () { - - System.out.println( "InstanceOf Test looping " + LOOP_COUNT ); - long begin = System.currentTimeMillis(); - for ( int index = 0; index < LOOP_COUNT; index++) { - int typeToMake = index % 5; - - InstanceOfInterface t; - switch( typeToMake ) { - case 0 : - t = new StringInstanceOf(); break; - case 1 : - t = new IntegerInstanceOf(); break; - case 2 : - t = new BooleanInstanceOf(); break; - case 3 : - t = new DateInstanceOf(); break; - case 4 : - t = new LogicalInstanceOf(); break; - default : - throw new RuntimeException("pants"); - } - - if ( t instanceof StringInstanceOf ) { - StringInstanceOf s = (StringInstanceOf) t; - List sValues = s.getValues(); - Assert.assertEquals( Arrays.asList("A", "B"), sValues ); - } - else if (t instanceof IntegerInstanceOf) { - IntegerInstanceOf i = (IntegerInstanceOf) t; - List iValues = i.getValues(); - Assert.assertEquals(Arrays.asList(1, 2), iValues); - } - else if (t instanceof BooleanInstanceOf) { - BooleanInstanceOf b = (BooleanInstanceOf) t; - List bValues = b.getValues(); - Assert.assertEquals(Arrays.asList(true, false), bValues); - } - else if (t instanceof DateInstanceOf) { - DateInstanceOf d = (DateInstanceOf) t; - List dValues = d.getValues(); - Assert.assertEquals(Arrays.asList("10", "11"), dValues); - } - else if (t instanceof LogicalInstanceOf) { - LogicalInstanceOf l = (LogicalInstanceOf) t; - List lValues = l.getValues(); - Assert.assertEquals(0, lValues.size()); - } - } - - long end = System.currentTimeMillis(); - - System.out.println( "Took : " + ( end - begin ) ); - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java deleted file mode 100644 index 2b06136e..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/LogicalFilter4.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.jsonUtil.testdomain.four; - -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.io.IOException; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@JsonSerialize(using = LogicalFilter4.LogicalFilter4Serializer.class) -@JsonDeserialize(using = LogicalFilter4.LogicalFilter4Deserializer.class) -public class LogicalFilter4 implements QueryFilter4 { - - public static class LogicalFilter4Serializer extends JsonSerializer { - - @Override - public void serialize(LogicalFilter4 filter, JsonGenerator jgen, SerializerProvider provider) throws IOException { - jgen.writeStartObject(); - jgen.writeObjectField( filter.getQueryParam().toString(), filter.getFilters().values() ); - jgen.writeEndObject(); - } - } - - public static class LogicalFilter4Deserializer extends JsonDeserializer { - - @Override - public LogicalFilter4 deserialize( JsonParser jp, DeserializationContext ctxt ) throws IOException { - - ObjectCodec objectCodec = jp.getCodec(); - ObjectNode root = jp.readValueAsTree(); - - // We assume it is a LogicalFilter - Iterator iter = root.fieldNames(); - String key = iter.next(); - - JsonNode arrayNode = root.iterator().next(); - if ( arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray() ) { - throw new RuntimeException( "Invalid format of LogicalFilter encountered." ); - } - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = arrayNode.traverse( objectCodec ); - List childrenQueryFilters = subJsonParser.readValueAs( new TypeReference>() {} ); - - return new LogicalFilter4( QueryParam.valueOf( key ), childrenQueryFilters ); - } - } - - private final QueryParam queryParam; - private final Map filters; - - public LogicalFilter4(QueryParam queryParam, List filters) { - this.queryParam = queryParam; - - this.filters = new LinkedHashMap<>(); - for ( QueryFilter4 queryFilter : filters ) { - this.filters.put( queryFilter.getQueryParam(), queryFilter ); - } - } - - @Override - public Map getFilters() { - return filters; - } - - @Override - public QueryParam getQueryParam() { - return queryParam; - } - - @Override - public boolean isLogical() { - return true; - } - - @Override - public boolean isReal() { - return false; - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java deleted file mode 100644 index f5251b71..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/MappingTest4.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.jsonUtil.testdomain.four; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Map; - -public class MappingTest4 { - - private Diffy diffy = new Diffy(); - - public static class QueryFilter4Deserializer extends JsonDeserializer { - - /** - * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. - * - * Aka specify a Deserializer and "catch" some input, determine what type of Class it - * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. - */ - @Override - public QueryFilter4 deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException { - - ObjectNode root = jp.readValueAsTree(); - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = root.traverse( jp.getCodec() ); - - // Check if it is a "RealFilter" - JsonNode valueParam = root.get("value"); - - if ( valueParam == null ) { - return subJsonParser.readValueAs( LogicalFilter4.class ); - } - if ( valueParam.isBoolean() ) { - return subJsonParser.readValueAs( BooleanRealFilter4.class ); - } - else if ( valueParam.isTextual() ) { - return subJsonParser.readValueAs( StringRealFilter4.class ); - } - else if ( valueParam.isIntegralNumber() ) { - return subJsonParser.readValueAs( IntegerRealFilter4.class ); - } - else { - throw new RuntimeException("Unknown type"); - } - } - } - - @Test - public void testPolymorphicJacksonSerializationAndDeserialization() - { - ObjectMapper mapper = new ObjectMapper(); - - SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) - .addDeserializer( QueryFilter4.class, new QueryFilter4Deserializer() ); - - mapper.registerModule(testModule); - - // Verifying that we can pass in a custom Mapper and create a new JsonUtil - JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); - - String testFixture = "/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json"; - - // TEST JsonUtil and our deserialization logic - QueryFilter4 queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); - - // Make sure the hydrated QFilter looks right - Assert.assertTrue( queryFilter instanceof LogicalFilter4); - Assert.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); - Assert.assertTrue( queryFilter.isLogical() ); - Assert.assertEquals( 3, queryFilter.getFilters().size() ); - Assert.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); - - // Make sure one of the top level RealFilters looks right - QueryFilter4 productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); - Assert.assertTrue( productIdFilter.isReal() ); - Assert.assertTrue( productIdFilter instanceof StringRealFilter4); - StringRealFilter4 stringRealProductIdFilter = (StringRealFilter4) productIdFilter; - Assert.assertEquals( QueryParam.PRODUCTID, stringRealProductIdFilter.getQueryParam() ); - Assert.assertEquals( "Acme-1234", stringRealProductIdFilter.getValue() ); - - // Make sure the nested OR looks right - QueryFilter4 orFilter = queryFilter.getFilters().get( QueryParam.OR ); - Assert.assertTrue( orFilter.isLogical() ); - Assert.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); - Assert.assertEquals( 2, orFilter.getFilters().size() ); - - // Make sure nested AND looks right - QueryFilter4 nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); - Assert.assertTrue( nestedAndFilter.isLogical() ); - Assert.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); - Assert.assertEquals( 2, nestedAndFilter.getFilters().size() ); - - - // SERIALIZE TO STRING to test serialization logic - String unitTestString = jsonUtil.toJsonString( queryFilter ); - - // LOAD and Diffy the plain vanilla JSON versions of the documents - Map actual = JsonUtils.jsonToMap( unitTestString ); - Map expected = JsonUtils.classpathToMap( testFixture ); - - // Diffy the vanilla versions - Diffy.Result result = diffy.diff( expected, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java deleted file mode 100644 index c1e803d8..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/MappingTest1.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.jsonUtil.testdomain.one; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; -import com.bazaarvoice.jolt.jsonUtil.testdomain.RealFilter; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Map; - -public class MappingTest1 { - - private Diffy diffy = new Diffy(); - - public static class QueryFilter1Deserializer extends JsonDeserializer { - - /** - * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. - * - * Aka specify a Deserializer and "catch" some input, determine what type of Class it - * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. - */ - @Override - public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException { - - ObjectNode root = jp.readValueAsTree(); - - JsonNode queryParam = root.get("queryParam"); - String value = queryParam.asText(); - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = root.traverse( jp.getCodec() ); - - // Determine the "type" of filter we are dealing with Real or Logical and specify type - if ( "OR".equals( value ) || "AND".equals( value ) ) { - return subJsonParser.readValueAs( LogicalFilter1.class ); - } - else { - return subJsonParser.readValueAs( RealFilter.class ); - } - } - } - - @Test - public void testPolymorphicJacksonSerializationAndDeserialization() - { - ObjectMapper mapper = new ObjectMapper(); - - SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) - .addDeserializer( QueryFilter.class, new QueryFilter1Deserializer() ); - - mapper.registerModule(testModule); - - // Verifying that we can pass in a custom Mapper and create a new JsonUtil - JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); - - String testFixture = "/jsonUtils/testdomain/one/queryFilter-realAndLogical.json"; - - // TEST JsonUtil and our deserialization logic - QueryFilter queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); - - // Make sure the hydrated queryFilter looks right - Assert.assertTrue( queryFilter instanceof LogicalFilter1 ); - Assert.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); - Assert.assertTrue( queryFilter.isLogical() ); - Assert.assertEquals( 3, queryFilter.getFilters().size() ); - Assert.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); - - // Make sure one of the top level RealFilters looks right - QueryFilter productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); - Assert.assertTrue( productIdFilter.isReal() ); - Assert.assertEquals( QueryParam.PRODUCTID, productIdFilter.getQueryParam() ); - Assert.assertEquals( "Acme-1234", productIdFilter.getValue() ); - - // Make sure the nested OR looks right - QueryFilter orFilter = queryFilter.getFilters().get( QueryParam.OR ); - Assert.assertTrue( orFilter.isLogical() ); - Assert.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); - Assert.assertEquals( 2, orFilter.getFilters().size() ); - - // Make sure nested AND looks right - QueryFilter nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); - Assert.assertTrue( nestedAndFilter.isLogical() ); - Assert.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); - Assert.assertEquals( 2, nestedAndFilter.getFilters().size() ); - - - // SERIALIZE TO STRING to test serialization logic - String unitTestString = jsonUtil.toJsonString( queryFilter ); - - // LOAD and Diffy the plain vanilla JSON versions of the documents - Map actual = JsonUtils.jsonToMap( unitTestString ); - Map expected = JsonUtils.classpathToMap( testFixture ); - - // Diffy the vanilla versions - Diffy.Result result = diffy.diff( expected, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java deleted file mode 100644 index 3dfefa2b..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/LogicalFilter3.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.jsonUtil.testdomain.three; - -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.io.IOException; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -@JsonSerialize(using = LogicalFilter3.LogicalFilter3Serializer.class) -@JsonDeserialize(using = LogicalFilter3.LogicalFilter4Deserializer.class) -public class LogicalFilter3 implements QueryFilter { - - public static class LogicalFilter3Serializer extends JsonSerializer { - - @Override - public void serialize(LogicalFilter3 filter, JsonGenerator jgen, SerializerProvider provider) throws IOException { - jgen.writeStartObject(); - jgen.writeObjectField( filter.getQueryParam().toString(), filter.getFilters().values() ); - jgen.writeEndObject(); - } - } - - public static class LogicalFilter4Deserializer extends JsonDeserializer { - - @Override - public LogicalFilter3 deserialize( JsonParser jp, DeserializationContext ctxt ) throws IOException { - - ObjectCodec objectCodec = jp.getCodec(); - ObjectNode root = jp.readValueAsTree(); - - // We assume it is a LogicalFilter - Iterator iter = root.fieldNames(); - String key = iter.next(); - - JsonNode arrayNode = root.iterator().next(); - if ( arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray() ) { - throw new RuntimeException( "Invalid format of LogicalFilter encountered." ); - } - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = arrayNode.traverse( objectCodec ); - List childrenQueryFilters = subJsonParser.readValueAs( new TypeReference>() {} ); - - return new LogicalFilter3( QueryParam.valueOf( key ), childrenQueryFilters ); - } - } - - private final QueryParam queryParam; - private final Map filters; - - public LogicalFilter3( QueryParam queryParam, List filters ) { - this.queryParam = queryParam; - - this.filters = new LinkedHashMap<>(); - for ( QueryFilter queryFilter : filters ) { - this.filters.put( queryFilter.getQueryParam(), queryFilter ); - } - } - - @Override - public Map getFilters() { - return filters; - } - - @Override - public QueryParam getQueryParam() { - return queryParam; - } - - @Override - public String getValue() { - return null; - } - - @Override - public boolean isLogical() { - return true; - } - - @Override - public boolean isReal() { - return false; - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java deleted file mode 100644 index 30a4232f..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/three/MappingTest3.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.jsonUtil.testdomain.three; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; -import com.bazaarvoice.jolt.jsonUtil.testdomain.RealFilter; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Map; - -public class MappingTest3 { - - private Diffy diffy = new Diffy(); - - public static class QueryFilterDeserializer extends JsonDeserializer { - - /** - * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. - * - * Aka specify a Deserializer and "catch" some input, determine what type of Class it - * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. - */ - @Override - public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException { - - ObjectNode root = jp.readValueAsTree(); - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = root.traverse( jp.getCodec() ); - - // Check if it is a "RealFilter" - JsonNode queryParam = root.get("queryParam"); - if ( queryParam != null && queryParam.isValueNode() ) { - return subJsonParser.readValueAs( RealFilter.class ); - } - else { - return subJsonParser.readValueAs( LogicalFilter3.class ); - } - } - } - - @Test - public void testPolymorphicJacksonSerializationAndDeserialization() - { - ObjectMapper mapper = new ObjectMapper(); - - SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) - .addDeserializer( QueryFilter.class, new QueryFilterDeserializer() ); - - mapper.registerModule(testModule); - - // Verifying that we can pass in a custom Mapper and create a new JsonUtil - JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); - - String testFixture = "/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json"; - - // TEST JsonUtil and our deserialization logic - QueryFilter queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); - - // Make sure the hydrated QFilter looks right - Assert.assertTrue( queryFilter instanceof LogicalFilter3 ); - Assert.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); - Assert.assertTrue( queryFilter.isLogical() ); - Assert.assertEquals( 3, queryFilter.getFilters().size() ); - Assert.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); - - // Make sure one of the top level RealFilters looks right - QueryFilter productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); - Assert.assertTrue( productIdFilter.isReal() ); - Assert.assertEquals( QueryParam.PRODUCTID, productIdFilter.getQueryParam() ); - Assert.assertEquals( "Acme-1234", productIdFilter.getValue() ); - - // Make sure the nested OR looks right - QueryFilter orFilter = queryFilter.getFilters().get( QueryParam.OR ); - Assert.assertTrue( orFilter.isLogical() ); - Assert.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); - Assert.assertEquals( 2, orFilter.getFilters().size() ); - - // Make sure nested AND looks right - QueryFilter nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); - Assert.assertTrue( nestedAndFilter.isLogical() ); - Assert.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); - Assert.assertEquals( 2, nestedAndFilter.getFilters().size() ); - - - // SERIALIZE TO STRING to test serialization logic - String unitTestString = jsonUtil.toJsonString( queryFilter ); - - // LOAD and Diffy the plain vanilla JSON versions of the documents - Map actual = JsonUtils.jsonToMap( unitTestString ); - Map expected = JsonUtils.classpathToMap( testFixture ); - - // Diffy the vanilla versions - Diffy.Result result = diffy.diff( expected, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java b/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java deleted file mode 100644 index 1f20827a..00000000 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/MappingTest2.java +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright 2014 Bazaarvoice, 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.bazaarvoice.jolt.jsonUtil.testdomain.two; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; -import com.bazaarvoice.jolt.jsonUtil.testdomain.RealFilter; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.ObjectCodec; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.JsonSerializer; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.testng.Assert; -import org.testng.annotations.Test; - -import java.io.IOException; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class MappingTest2 { - - private Diffy diffy = new Diffy(); - - public static class QueryFilterDeserializer extends JsonDeserializer { - - /** - * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. - * - * Aka specify a Deserializer and "catch" some input, determine what type of Class it - * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. - */ - @Override - public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException { - - ObjectCodec objectCodec = jp.getCodec(); - ObjectNode root = jp.readValueAsTree(); - - // Check if it is a "RealFilter" - JsonNode queryParam = root.get("queryParam"); - if ( queryParam != null && queryParam.isValueNode() ) { - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = root.traverse( objectCodec ); - - return subJsonParser.readValueAs( RealFilter.class ); - } - - // We assume it is a LogicalFilter - Iterator iter = root.fieldNames(); - String key = iter.next(); - - JsonNode arrayNode = root.iterator().next(); - if ( arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray() ) { - throw new RuntimeException( "Invalid format of LogicalFilter encountered." ); - } - - // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations - JsonParser subJsonParser = arrayNode.traverse( objectCodec ); - List childrenQueryFilters = subJsonParser.readValueAs( new TypeReference>() {} ); - - return new LogicalFilter2( QueryParam.valueOf( key ), childrenQueryFilters ); - } - } - - public static class LogicalFilter2Serializer extends JsonSerializer { - - @Override - public void serialize(LogicalFilter2 filter, JsonGenerator jgen, SerializerProvider provider) throws IOException { - jgen.writeStartObject(); - jgen.writeObjectField( filter.getQueryParam().toString(), filter.getFilters().values() ); - jgen.writeEndObject(); - } - } - - - @Test - public void testPolymorphicJacksonSerializationAndDeserialization() - { - ObjectMapper mapper = new ObjectMapper(); - - SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) - .addDeserializer( QueryFilter.class, new QueryFilterDeserializer() ) - .addSerializer( LogicalFilter2.class, new LogicalFilter2Serializer() ); - - mapper.registerModule(testModule); - - // Verifying that we can pass in a custom Mapper and create a new JsonUtil - JsonUtil jsonUtil = JsonUtils.customJsonUtil( mapper ); - - String testFixture = "/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json"; - - // TEST JsonUtil and our deserialization logic - QueryFilter queryFilter = jsonUtil.classpathToType( testFixture, new TypeReference() {} ); - - // Make sure the hydrated QFilter looks right - Assert.assertTrue( queryFilter instanceof LogicalFilter2 ); - Assert.assertEquals( QueryParam.AND, queryFilter.getQueryParam() ); - Assert.assertTrue( queryFilter.isLogical() ); - Assert.assertEquals( 3, queryFilter.getFilters().size() ); - Assert.assertNotNull( queryFilter.getFilters().get( QueryParam.OR ) ); - - // Make sure one of the top level RealFilters looks right - QueryFilter productIdFilter = queryFilter.getFilters().get( QueryParam.PRODUCTID ); - Assert.assertTrue( productIdFilter.isReal() ); - Assert.assertEquals( QueryParam.PRODUCTID, productIdFilter.getQueryParam() ); - Assert.assertEquals( "Acme-1234", productIdFilter.getValue() ); - - // Make sure the nested OR looks right - QueryFilter orFilter = queryFilter.getFilters().get( QueryParam.OR ); - Assert.assertTrue( orFilter.isLogical() ); - Assert.assertEquals( QueryParam.OR, orFilter.getQueryParam() ); - Assert.assertEquals( 2, orFilter.getFilters().size() ); - - // Make sure nested AND looks right - QueryFilter nestedAndFilter = orFilter.getFilters().get( QueryParam.AND ); - Assert.assertTrue( nestedAndFilter.isLogical() ); - Assert.assertEquals( QueryParam.AND, nestedAndFilter.getQueryParam() ); - Assert.assertEquals( 2, nestedAndFilter.getFilters().size() ); - - - // SERIALIZE TO STRING to test serialization logic - String unitTestString = jsonUtil.toJsonString( queryFilter ); - - // LOAD and Diffy the plain vanilla JSON versions of the documents - Map actual = JsonUtils.jsonToMap( unitTestString ); - Map expected = JsonUtils.classpathToMap( testFixture ); - - // Diffy the vanilla versions - Diffy.Result result = diffy.diff( expected, actual ); - if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); - } - } -} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/ArrayOrderObliviousDiffyTest.java b/json-utils/src/test/java/io/joltcommunity/jolt/ArrayOrderObliviousDiffyTest.java similarity index 82% rename from json-utils/src/test/java/com/bazaarvoice/jolt/ArrayOrderObliviousDiffyTest.java rename to json-utils/src/test/java/io/joltcommunity/jolt/ArrayOrderObliviousDiffyTest.java index 038268e3..724e2296 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/ArrayOrderObliviousDiffyTest.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/ArrayOrderObliviousDiffyTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import com.beust.jcommander.internal.Lists; import org.testng.Assert; @@ -42,9 +43,9 @@ public void after() throws Exception { @DataProvider(parallel = true) public Iterator testCases() { List testCases = Lists.newArrayList(); - testCases.add(new Object[] {"arrayOrderObliviousDiffy/bugFix95"}); // see https://github.com/bazaarvoice/jolt/issues/95 - testCases.add(new Object[] {"arrayOrderObliviousDiffy/simpleCase"}); - testCases.add(new Object[] {"arrayOrderObliviousDiffy/complexCase"}); + testCases.add(new Object[]{"arrayOrderObliviousDiffy/bugFix95"}); // see https://github.com/bazaarvoice/jolt/issues/95 + testCases.add(new Object[]{"arrayOrderObliviousDiffy/simpleCase"}); + testCases.add(new Object[]{"arrayOrderObliviousDiffy/complexCase"}); return testCases.iterator(); } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/DiffyFixtureTest.java b/json-utils/src/test/java/io/joltcommunity/jolt/DiffyFixtureTest.java similarity index 71% rename from json-utils/src/test/java/com/bazaarvoice/jolt/DiffyFixtureTest.java rename to json-utils/src/test/java/io/joltcommunity/jolt/DiffyFixtureTest.java index b7effec8..ab3121a3 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/DiffyFixtureTest.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/DiffyFixtureTest.java @@ -1,5 +1,6 @@ /* - * Copyright 2013 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt; +package io.joltcommunity.jolt; import org.testng.Assert; import org.testng.annotations.DataProvider; @@ -28,42 +29,42 @@ public class DiffyFixtureTest { @DataProvider(parallel = true) public Object[][] diffyWhenThingsDontMatchTestCases() { - return new Object[][] { + return new Object[][]{ // for this test, there is only one thing different, and both Diffies return the same diff - {diffy, "esQuery1", "expectedDiff"}, + {diffy, "esQuery1", "expectedDiff"}, {aooDiffy, "esQuery1", "expectedDiff"}, // in this test, the same thing from above is different, but the order of things has been scrabled. // thus the aooDiff is way smaller than the base Diffy - {diffy, "esQuery2", "expectedDiff"}, + {diffy, "esQuery2", "expectedDiff"}, {aooDiffy, "esQuery2", "expectedAOODiff"}, - {diffy, "differentSizedLists", "expectedDiff"}, + {diffy, "differentSizedLists", "expectedDiff"}, {aooDiffy, "differentSizedLists", "expectedAOODiff"}, }; } /** * So this test a little bit Meta. - * + *

* The idea is, we want to test what Diffy returns when the inputs to Diffy do not match. - * + *

* So, run Diffy with inputs "A" and "B", and then compare the result against an expected Diff "C". - * + *

* However Diffy "ignores" nulls in the inputs and expected data, thus we do a base level Map.equals(). */ @Test(dataProvider = "diffyWhenThingsDontMatchTestCases") public void testDiffyWhenThingsDontMatch(Diffy diffy, String testCase, String expectedFile) throws Exception { - Object testActual = JsonUtils.classpathToObject("/jsonUtils/diffyWhenDifferent/" + testCase + "/testActual.json"); + Object testActual = JsonUtils.classpathToObject("/jsonUtils/diffyWhenDifferent/" + testCase + "/testActual.json"); Object testExpected = JsonUtils.classpathToObject("/jsonUtils/diffyWhenDifferent/" + testCase + "/testExpected.json"); - Map expectedDiff = JsonUtils.classpathToMap("/jsonUtils/diffyWhenDifferent/" + testCase + "/" + expectedFile + ".json"); + Map expectedDiff = JsonUtils.classpathToMap("/jsonUtils/diffyWhenDifferent/" + testCase + "/" + expectedFile + ".json"); - Diffy.Result testResult = diffy.diff( testExpected, testActual ); - Assert.assertFalse( testResult.isEmpty(), "Test diffs match when the shouldn't."); + Diffy.Result testResult = diffy.diff(testExpected, testActual); + Assert.assertFalse(testResult.isEmpty(), "Test diffs match when the shouldn't."); // expectedDiff.equals cause the Map.equals is deep and everything must be in order. - Assert.assertTrue( expectedDiff.equals( testResult.expected ), "The meta diff was not empty, when it should have."); + Assert.assertTrue(expectedDiff.equals(testResult.expected), "The meta diff was not empty, when it should have."); } } diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/DiffyUnitTest.java b/json-utils/src/test/java/io/joltcommunity/jolt/DiffyUnitTest.java new file mode 100644 index 00000000..6d355fc7 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/DiffyUnitTest.java @@ -0,0 +1,192 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.*; + +public class DiffyUnitTest { + + private static final Object[] SCALARS = new Object[]{ + null, 1, 2, true, false, 3.14, 2.71, "foo", "bar", new ArrayList<>(), new HashMap<>() + }; + private Diffy unit; + + @BeforeMethod + public void setup() { + this.unit = new Diffy(); + } + + @AfterMethod + public void teardown() { + this.unit = null; + } + + private void testScalars(Object expected, Object actual, boolean expectDiff) { + Diffy.Result result = this.unit.diff(expected, actual); + if (expectDiff) { + Assert.assertEquals(expected, result.expected); + Assert.assertEquals(actual, result.actual); + } + } + + @Test + public void testAllTheScalars() { + for (int i = 0; i < SCALARS.length; i++) { + for (int j = i; j < SCALARS.length; j++) { + this.testScalars(SCALARS[i], SCALARS[j], i != j); + } + } + } + + @Test + public void diff_itSaysListsWithSameElementsAreSame() { + Object[] stuff = new Object[]{"foo", 3, null}; + List list1 = Arrays.asList(stuff); + List list2 = Arrays.asList(stuff); + Diffy.Result result = this.unit.diff(list1, list2); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void diff_itRecognizesDifferingElementsInArrays() { + Diffy.Result result = this.unit.diff( + Arrays.asList("foo", 3, null), + Arrays.asList("foo", 3, "apple")); + Assert.assertEquals(Arrays.asList(new Object[]{null, null, null}), result.expected); + Assert.assertEquals(Arrays.asList(new Object[]{null, null, "apple"}), result.actual); + } + + @Test + public void diff_itHandlesLongerExpectedArray() { + Diffy.Result result = this.unit.diff( + Arrays.asList("foo", 3, true), + Arrays.asList("foo", 3)); + Assert.assertEquals(Arrays.asList(new Object[]{null, null, true}), result.expected); + Assert.assertEquals(Arrays.asList(new Object[]{null, null}), result.actual); + } + + @Test + public void diff_itHandlesLongerActualArray() { + Diffy.Result result = this.unit.diff( + Arrays.asList("foo", 3), + Arrays.asList("foo", 3, false)); + Assert.assertEquals(Arrays.asList(new Object[]{null, null}), result.expected); + Assert.assertEquals(Arrays.asList(new Object[]{null, null, false}), result.actual); + } + + @Test + public void diff_itSaysMapsWithSameContentsAreSame() + throws IOException { + Diffy.Result result = this.unit.diff( + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\"}"), + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\"}")); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void diff_itSaysDifferentWhenActualHasExtra() + throws IOException { + Diffy.Result result = this.unit.diff( + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\"}"), + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\", \"extra\":null}")); + Assert.assertEquals(new HashMap<>(), result.expected); + Assert.assertEquals(JsonUtils.jsonToMap("{\"extra\":null}"), result.actual); + } + + @Test + public void diff_itSaysDifferentWhenExpectedHasExtra() + throws IOException { + Diffy.Result result = this.unit.diff( + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\", \"extra\":42}"), + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\"}")); + Assert.assertEquals(JsonUtils.jsonToMap("{\"extra\":42}"), result.expected); + Assert.assertEquals(new HashMap<>(), result.actual); + } + + @Test + public void diff_itSaysDifferentWhenAnAttributeDiffers() + throws IOException { + Diffy.Result result = this.unit.diff( + JsonUtils.jsonToMap("{\"foo\":1, \"bar\":\"baz\"}"), + JsonUtils.jsonToMap("{\"foo\":\"apple\", \"bar\":\"baz\"}")); + Assert.assertEquals(JsonUtils.jsonToMap("{\"foo\":1}"), result.expected); + Assert.assertEquals(JsonUtils.jsonToMap("{\"foo\":\"apple\"}"), result.actual); + } + + /** + * Testing / exploring basic Map.equals behavior. + */ + @Test + public void verify_NestedMapEquals() { + Map h1 = new HashMap<>(); + { + h1.put("a", "a"); + Map bMap = new HashMap<>(); + bMap.put("c", "c"); + bMap.put("d", "d"); + h1.put("b", bMap); + } + + Map h2 = new HashMap<>(); + { + Map bMap = new HashMap<>(); + bMap.put("c", "c"); + bMap.put("d", "d"); + h2.put("b", bMap); + h2.put("a", "a"); + } + + Assert.assertTrue(h1.equals(h2), "1->2 Two HashMaps with the same things should be equal."); + Assert.assertEquals(h1.hashCode(), h2.hashCode(), "1->2 Two HashMaps with the same things should have the same hashCode."); + + Assert.assertTrue(h2.equals(h1), "2->1 Two HashMaps with the same things should be equal."); + Assert.assertEquals(h2.hashCode(), h1.hashCode(), "2->1 Two HashMaps with the same things should have the same hashCode."); + + Map lh1 = new LinkedHashMap<>(); + { + lh1.put("a", "a"); + Map bMap = new HashMap<>(); + bMap.put("c", "c"); + bMap.put("d", "d"); + lh1.put("b", bMap); + } + + Assert.assertTrue(lh1.equals(h2), "lh1->2 Two HashMaps with the same things should be equal."); + Assert.assertEquals(lh1.hashCode(), h2.hashCode(), "lh1->2 Two HashMaps with the same things should have the same hashCode."); + + Assert.assertTrue(lh1.equals(h1), "lh1->1 Two HashMaps with the same things should be equal."); + Assert.assertEquals(lh1.hashCode(), h1.hashCode(), "lh1->1 Two HashMaps with the same things should have the same hashCode."); + + Map d1 = new HashMap<>(); + { + d1.put("a", "a"); + Map bMap = new HashMap<>(); + bMap.put("c", "c"); + bMap.put("d", "E"); + d1.put("b", bMap); + } + + Assert.assertFalse(d1.equals(h2), "Two HashMaps that should not be equal."); + Assert.assertNotEquals(d1.hashCode(), h2.hashCode(), "lh1->2 Two HashMaps should not have the same hashCode ."); + } +} diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/JsonUtilsTest.java b/json-utils/src/test/java/io/joltcommunity/jolt/JsonUtilsTest.java new file mode 100644 index 00000000..8a8c72e3 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/JsonUtilsTest.java @@ -0,0 +1,356 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import com.beust.jcommander.internal.Sets; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import tools.jackson.core.type.TypeReference; +import org.testng.Assert; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.testng.collections.Lists; + +import java.io.*; +import java.util.*; + +public class JsonUtilsTest { + + private Diffy diffy = new Diffy(); + + private Map ab = ImmutableMap.builder().put("a", "b").build(); + private Map cd = ImmutableMap.builder().put("c", "d").build(); + private Map top = ImmutableMap.builder().put("A", ab).put("B", cd).build(); + + private String jsonSourceString = "{ " + + " \"a\": { " + + " \"b\": [ " + + " 0, " + + " 1, " + + " 2, " + + " 1.618 " + + " ] " + + " }, " + + " \"p\": [ " + + " \"m\", " + + " \"n\", " + + " { " + + " \"1\": 1, " + + " \"2\": 2, " + + " \"pi\": 3.14159 " + + " } " + + " ], " + + " \"x\": \"y\" " + + "}\n"; + + private Object jsonSource; + + @BeforeClass + @SuppressWarnings("unchecked") + public void setup() throws IOException { + jsonSource = JsonUtils.jsonToObject(jsonSourceString); + // added for type cast checking + Set aSet = Sets.newHashSet(); + aSet.add("i"); + aSet.add("j"); + ((Map) jsonSource).put("s", aSet); + } + + @DataProvider + public Object[][] removeRecursiveCases() { + + Map empty = ImmutableMap.builder().build(); + Map barToFoo = ImmutableMap.builder().put("bar", "foo").build(); + Map fooToBar = ImmutableMap.builder().put("foo", "bar").build(); + return new Object[][]{ + {null, null, null}, + {null, "foo", null}, + {"foo", null, "foo"}, + {"foo", "foo", "foo"}, + {Maps.newHashMap(), "foo", empty}, + {Maps.newHashMap(barToFoo), "foo", barToFoo}, + {Maps.newHashMap(fooToBar), "foo", empty}, + {Lists.newArrayList(), "foo", ImmutableList.builder().build()}, + { + Lists.newArrayList(ImmutableList.builder() + .add(Maps.newHashMap(barToFoo)) + .build()), + "foo", + ImmutableList.builder() + .add(barToFoo) + .build() + }, + { + Lists.newArrayList(ImmutableList.builder() + .add(Maps.newHashMap(fooToBar)) + .build()), + "foo", + ImmutableList.builder() + .add(empty) + .build() + } + }; + } + + + @Test(dataProvider = "removeRecursiveCases") + @SuppressWarnings("deprecation") + public void testRemoveRecursive(Object json, String key, Object expected) throws IOException { + + JsonUtils.removeRecursive(json, key); + + Diffy.Result result = diffy.diff(expected, json); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + + @Test + @SuppressWarnings("deprecation") + public void runFixtureTests() throws IOException { + + String testFixture = "/jsonUtils/jsonUtils-removeRecursive.json"; + @SuppressWarnings("unchecked") + List> tests = (List>) JsonUtils.classpathToObject(testFixture); + + for (Map testUnit : tests) { + + Object data = testUnit.get("input"); + String toRemove = (String) testUnit.get("remove"); + Object expected = testUnit.get("expected"); + + JsonUtils.removeRecursive(data, toRemove); + + Diffy.Result result = diffy.diff(expected, data); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } + } + + @Test + public void validateJacksonClosesInputStreams() { + + final Set closedSet = new HashSet<>(); + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream("{ \"a\" : \"b\" }".getBytes()) { + @Override + public void close() throws IOException { + closedSet.add("closed"); + super.close(); + } + }; + + // Pass our wrapped InputStream to Jackson via JsonUtils. + Map map = JsonUtils.jsonToMap(byteArrayInputStream); + + // Verify that we in fact loaded some data + Assert.assertNotNull(map); + Assert.assertEquals(1, map.size()); + + // Verify that the close method was in fact called on the InputStream + Assert.assertEquals(1, closedSet.size()); + } + + @DataProvider(parallel = true) + public Iterator coordinates() throws IOException { + List testCases = com.beust.jcommander.internal.Lists.newArrayList(); + + testCases.add(new Object[]{0, new Object[]{"a", "b", 0}}); + testCases.add(new Object[]{1, new Object[]{"a", "b", 1}}); + testCases.add(new Object[]{2, new Object[]{"a", "b", 2}}); + testCases.add(new Object[]{1.618, new Object[]{"a", "b", 3}}); + testCases.add(new Object[]{"m", new Object[]{"p", 0}}); + testCases.add(new Object[]{"n", new Object[]{"p", 1}}); + testCases.add(new Object[]{1, new Object[]{"p", 2, "1"}}); + testCases.add(new Object[]{2, new Object[]{"p", 2, "2"}}); + testCases.add(new Object[]{3.14159, new Object[]{"p", 2, "pi"}}); + testCases.add(new Object[]{"y", new Object[]{"x"}}); + + testCases.add(new Object[]{((Map) jsonSource).get("a"), new Object[]{"a"}}); + testCases.add(new Object[]{((Map) (((Map) jsonSource).get("a"))).get("b"), new Object[]{"a", "b"}}); + testCases.add(new Object[]{((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(0), new Object[]{"a", "b", 0}}); + testCases.add(new Object[]{((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(1), new Object[]{"a", "b", 1}}); + testCases.add(new Object[]{((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(2), new Object[]{"a", "b", 2}}); + testCases.add(new Object[]{((List) ((Map) (((Map) jsonSource).get("a"))).get("b")).get(3), new Object[]{"a", "b", 3}}); + testCases.add(new Object[]{((Map) jsonSource).get("p"), new Object[]{"p"}}); + testCases.add(new Object[]{((List) (((Map) jsonSource).get("p"))).get(0), new Object[]{"p", 0}}); + testCases.add(new Object[]{((List) (((Map) jsonSource).get("p"))).get(1), new Object[]{"p", 1}}); + testCases.add(new Object[]{((List) (((Map) jsonSource).get("p"))).get(2), new Object[]{"p", 2}}); + testCases.add(new Object[]{((Map) ((List) (((Map) jsonSource).get("p"))).get(2)).get("1"), new Object[]{"p", 2, "1"}}); + testCases.add(new Object[]{((Map) ((List) (((Map) jsonSource).get("p"))).get(2)).get("2"), new Object[]{"p", 2, "2"}}); + testCases.add(new Object[]{((Map) ((List) (((Map) jsonSource).get("p"))).get(2)).get("pi"), new Object[]{"p", 2, "pi"}}); + testCases.add(new Object[]{((Map) jsonSource).get("x"), new Object[]{"x"}}); + + return testCases.iterator(); + } + + /** + * Method: navigate(Object source, Object... paths) + */ + @Test(dataProvider = "coordinates") + @SuppressWarnings("deprecation") + public void navigator(Object expected, Object[] path) throws Exception { + Object actual = JsonUtils.navigate(jsonSource, path); + Assert.assertEquals(actual, expected); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testJsonToObjectWithInvalidCharsetThrowsException() { + JsonUtilImpl util = new JsonUtilImpl(); + util.jsonToObject("{\"a\":1}", "invalid-charset"); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testJsonToMapWithInvalidCharsetThrowsException() { + JsonUtilImpl util = new JsonUtilImpl(); + util.jsonToMap("{\"a\":1}", "invalid-charset"); + } + + @Test + public void testJsonToListWithValidJson() { + JsonUtilImpl util = new JsonUtilImpl(); + List result = util.jsonToList("[1, 2, 3]"); + Assert.assertEquals(result.size(), 3); + Assert.assertEquals(result.get(0), 1); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testJsonToListWithInvalidCharsetThrowsException() { + JsonUtilImpl util = new JsonUtilImpl(); + util.jsonToList("{\"a\":1}", "invalid-charset"); + } + + @Test + public void testFilepathToObject_ValidFile() throws IOException { + String path = Objects.requireNonNull(getClass().getResource("/jsonUtils/valid.json")).getFile(); + + JsonUtilImpl util = new JsonUtilImpl(); + Object result = util.filepathToObject(path); + Assert.assertTrue(result instanceof java.util.Map); + Assert.assertEquals(((java.util.Map) result).get("foo"), 123); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testFilepathToObject_FileNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.filepathToObject("non_existent_file.json"); + } + + @Test + public void testFilepathToMap_ValidFile() throws IOException { + String path = Objects.requireNonNull(getClass().getResource("/jsonUtils/valid.json")).getFile(); + JsonUtilImpl util = new JsonUtilImpl(); + Map result = util.filepathToMap(path); + Assert.assertEquals(result.get("foo"), 123); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testFilepathToMap_FileNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.filepathToMap("non_existent_map.json"); + } + + @Test + public void testFilepathToList_ValidFile() throws IOException { + String path = Objects.requireNonNull(getClass().getResource("/jsonUtils/valid_list.json")).getFile(); + JsonUtilImpl util = new JsonUtilImpl(); + List result = util.filepathToList(path); + Assert.assertEquals(result.size(), 3); + Assert.assertEquals(result.get(0), 1); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testFilepathToList_FileNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.filepathToList("non_existent_list.json"); + } + + @Test + public void testClasspathToObject_ValidResource() { + JsonUtilImpl util = new JsonUtilImpl(); + Object result = util.classpathToObject("/jsonUtils/valid.json"); + Assert.assertTrue(result instanceof Map); + Assert.assertEquals(((Map) result).get("foo"), 123); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testClasspathToObject_ResourceNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.classpathToObject("/jsonUtils/non_existent.json"); + } + + @Test + public void testClasspathToMap_ValidResource() { + JsonUtilImpl util = new JsonUtilImpl(); + Map result = util.classpathToMap("/jsonUtils/valid.json"); + Assert.assertEquals(result.get("foo"), 123); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testClasspathToMap_ResourceNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.classpathToMap("/jsonUtils/non_existent.json"); + } + + @Test + public void testClasspathToList_ValidResource() { + JsonUtilImpl util = new JsonUtilImpl(); + List result = util.classpathToList("/jsonUtils/valid_list.json"); + Assert.assertEquals(result.size(), 3); + Assert.assertEquals(result.get(0), 1); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testClasspathToList_ResourceNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.classpathToList("/jsonUtils/non_existent_list.json"); + } + + @Test + public void testFileToType_WithTypeReference_ValidFile() throws IOException { + String path = Objects.requireNonNull(getClass().getResource("/jsonUtils/valid.json")).getFile(); + + JsonUtilImpl util = new JsonUtilImpl(); + Map result = util.fileToType( + path, + new TypeReference>() {} + ); + Assert.assertEquals(result.get("foo"), 123); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testFileToType_WithTypeReference_FileNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.fileToType("non_existent_type_ref.json", new TypeReference>() {}); + } + + @Test + public void testFileToType_WithClass_ValidFile() throws IOException { + String path = Objects.requireNonNull(getClass().getResource("/jsonUtils/valid.json")).getFile(); + JsonUtilImpl util = new JsonUtilImpl(); + Map result = util.fileToType(path, Map.class); + Assert.assertEquals(result.get("foo"), 123); + } + + @Test(expectedExceptions = RuntimeException.class) + public void testFileToType_WithClass_FileNotFound() { + JsonUtilImpl util = new JsonUtilImpl(); + util.fileToType("non_existent_class.json", Map.class); + } +} diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/TestInstanceOfVSEnumSwitch.java b/json-utils/src/test/java/io/joltcommunity/jolt/TestInstanceOfVSEnumSwitch.java new file mode 100644 index 00000000..d082cde5 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/TestInstanceOfVSEnumSwitch.java @@ -0,0 +1,229 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt; + +import org.testng.Assert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Small test to see if it is more efficient to do instanceof checks + * or to have Concrete subclasses have an Enum type. + *

+ * Answer : + * InstanceOf Test looping 50000000 + * Took : 24156 + * Typed Test looping 50000000 + * Took : 23571 + *

+ * It doesn't really matter ;) + */ +public class TestInstanceOfVSEnumSwitch { + + private static final int LOOP_COUNT = 1000 * 1000 * 50; + + //@Test + public void testTyped() { + + System.out.println("Typed Test looping " + LOOP_COUNT); + long begin = System.currentTimeMillis(); + for (int index = 0; index < LOOP_COUNT; index++) { + int typeToMake = index % 5; + + EnumBaseInterface t = switch (typeToMake) { + case 0 -> new StringEnum(); + case 1 -> new IntegerEnum(); + case 2 -> new BooleanEnum(); + case 3 -> new DateEnum(); + case 4 -> new LogicalEnum(); + default -> throw new RuntimeException("pants"); + }; + + switch (t.getType()) { + case STRING: + StringEnum s = (StringEnum) t; + List sValues = s.getValues(); + Assert.assertEquals(Arrays.asList("A", "B"), sValues); + break; + case INTEGER: + IntegerEnum i = (IntegerEnum) t; + List iValues = i.getValues(); + Assert.assertEquals(Arrays.asList(1, 2), iValues); + break; + case BOOLEAN: + BooleanEnum b = (BooleanEnum) t; + List bValues = b.getValues(); + Assert.assertEquals(Arrays.asList(true, false), bValues); + break; + case DATE: + DateEnum d = (DateEnum) t; + List dValues = d.getValues(); + Assert.assertEquals(Arrays.asList("10", "11"), dValues); + break; + case LOGICAL: + LogicalEnum l = (LogicalEnum) t; + List lValues = l.getValues(); + Assert.assertEquals(0, lValues.size()); + break; + } + } + + long end = System.currentTimeMillis(); + + System.out.println("Took : " + (end - begin)); + } + + //@Test + public void testInstanceOf() { + + System.out.println("InstanceOf Test looping " + LOOP_COUNT); + long begin = System.currentTimeMillis(); + for (int index = 0; index < LOOP_COUNT; index++) { + int typeToMake = index % 5; + + InstanceOfInterface t = switch (typeToMake) { + case 0 -> new StringInstanceOf(); + case 1 -> new IntegerInstanceOf(); + case 2 -> new BooleanInstanceOf(); + case 3 -> new DateInstanceOf(); + case 4 -> new LogicalInstanceOf(); + default -> throw new RuntimeException("pants"); + }; + + if (t instanceof StringInstanceOf s) { + List sValues = s.getValues(); + Assert.assertEquals(Arrays.asList("A", "B"), sValues); + } else if (t instanceof IntegerInstanceOf i) { + List iValues = i.getValues(); + Assert.assertEquals(Arrays.asList(1, 2), iValues); + } else if (t instanceof BooleanInstanceOf b) { + List bValues = b.getValues(); + Assert.assertEquals(Arrays.asList(true, false), bValues); + } else if (t instanceof DateInstanceOf d) { + List dValues = d.getValues(); + Assert.assertEquals(Arrays.asList("10", "11"), dValues); + } else if (t instanceof LogicalInstanceOf l) { + List lValues = l.getValues(); + Assert.assertEquals(0, lValues.size()); + } + } + + long end = System.currentTimeMillis(); + + System.out.println("Took : " + (end - begin)); + } + + enum Type { + STRING, + INTEGER, + BOOLEAN, + DATE, + LOGICAL + } + + interface EnumBaseInterface { + Type getType(); + + List getValues(); + } + + interface InstanceOfInterface { + List getValues(); + } + + class StringEnum implements EnumBaseInterface { + public Type getType() { + return Type.STRING; + } + + public List getValues() { + return Arrays.asList("A", "B"); + } + } + + class IntegerEnum implements EnumBaseInterface { + public Type getType() { + return Type.INTEGER; + } + + public List getValues() { + return Arrays.asList(1, 2); + } + } + + class BooleanEnum implements EnumBaseInterface { + public Type getType() { + return Type.BOOLEAN; + } + + public List getValues() { + return Arrays.asList(true, false); + } + } + + class DateEnum implements EnumBaseInterface { + public Type getType() { + return Type.DATE; + } + + public List getValues() { + return Arrays.asList("10", "11"); + } + } + + class LogicalEnum implements EnumBaseInterface { + public Type getType() { + return Type.LOGICAL; + } + + public List getValues() { + return new ArrayList<>(); + } + } + + class StringInstanceOf implements InstanceOfInterface { + public List getValues() { + return Arrays.asList("A", "B"); + } + } + + class IntegerInstanceOf implements InstanceOfInterface { + public List getValues() { + return Arrays.asList(1, 2); + } + } + + class BooleanInstanceOf implements InstanceOfInterface { + public List getValues() { + return Arrays.asList(true, false); + } + } + + class DateInstanceOf implements InstanceOfInterface { + public List getValues() { + return Arrays.asList("10", "11"); + } + } + + class LogicalInstanceOf implements InstanceOfInterface { + public List getValues() { + return new ArrayList<>(); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/QueryFilter.java similarity index 75% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/QueryFilter.java index b02052ff..e79eb3b8 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryFilter.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/QueryFilter.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,17 +14,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain; +package io.joltcommunity.jolt.jsonUtil.testdomain; import java.util.Map; public interface QueryFilter { - Map getFilters(); + Map filters(); - QueryParam getQueryParam(); + QueryParam queryParam(); - String getValue(); + String value(); boolean isLogical(); diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/QueryParam.java similarity index 85% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/QueryParam.java index b6a09c6a..5c568d71 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/QueryParam.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/QueryParam.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain; +package io.joltcommunity.jolt.jsonUtil.testdomain; public enum QueryParam { diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/Readme.md similarity index 83% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/Readme.md index ae6b9204..0c3f7b7b 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/Readme.md +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/Readme.md @@ -6,7 +6,7 @@ morphed into fancy Jackson Serialization and Deserialization experimentation. # Fancy Jackson The end goal was, I wanted to have Json that looks sort of like ElasticSearch's, - but that could serialize and deserialize into "nice" concrete Java Objects. +but that could serialize and deserialize into "nice" concrete Java Objects. Start by looking at the Json on /resources/jsonUtils/testdomain/*. @@ -16,7 +16,7 @@ The Filter classes all have simple constructors and just use basic Jackson featu As such, the JSON representation is overly wordy. In the MappingTest class a JsonDeserializer is defined and registerd as a Module to the Jackson -ObjectMapper. It is the thing that differentiates Real and Logical filters. +ObjectMapper. It is the thing that differentiates Real and Logical filters. The "secret sause" was to create a "sub" JsonParser with the "codec" of the parent. @@ -44,15 +44,15 @@ in MappingTest2 to the LogicalFilter3 class, via class level @JsonSerialize and ## TestDomain Four -In the previous formulations, the "value" of a RealFilter was always a String. Here I make it typed (String, Integer, +In the previous formulations, the "value" of a RealFilter was always a String. Here I make it typed (String, Integer, Boolean). ## TestDomain Five -Make the "value" of a RealFilter be a typed List of values. This allowed for more overlap between the Real and +Make the "value" of a RealFilter be a typed List of values. This allowed for more overlap between the Real and Logical QueryFilters as they can share a getValues( List list ) interface, which is kinda nice. Tried again to avoid needed a custom JacksonModule for the ObjectMapper, but still could not get it to work. Was able to remove the custom @JsonSerialize and @JsonDeserialize inner classes from LogicalFilter5 by making -it "extend Map". Works but is kunky and I would not use it in practice. +it "extend Map". Works but is kunky and I would not use it in practice. diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/RealFilter.java similarity index 66% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/RealFilter.java index d4c3c145..b50f11ca 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/RealFilter.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/RealFilter.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain; +package io.joltcommunity.jolt.jsonUtil.testdomain; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -24,31 +25,18 @@ /** * Simple / Standard Pojo Jackson Annotations */ -public class RealFilter implements QueryFilter { - - private final QueryParam queryParam; - private final String value; +public record RealFilter(QueryParam queryParam, String value) implements QueryFilter { @JsonCreator - public RealFilter( @JsonProperty( "queryParam" ) QueryParam queryParam, - @JsonProperty( "value" ) String value ) { + public RealFilter(@JsonProperty("queryParam") QueryParam queryParam, + @JsonProperty("value") String value) { this.queryParam = queryParam; this.value = value; } - @Override - public QueryParam getQueryParam() { - return queryParam; - } - - @Override - public String getValue() { - return value; - } - @Override @JsonIgnore - public Map getFilters() { + public Map filters() { return null; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java similarity index 82% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java index 97869d6b..eea221ac 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/BooleanRealFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -26,8 +27,8 @@ public class BooleanRealFilter5 extends RealFilter5 { @JsonCreator public BooleanRealFilter5(@JsonProperty("field") Field field, - @JsonProperty("operator") Operator op, - @JsonProperty("values") List values) { + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { super(field, op); this.values = values; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/DateRealFilter5.java similarity index 82% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/DateRealFilter5.java index 0a4b59cc..609b3cb2 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/DateRealFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/DateRealFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -26,8 +27,8 @@ public class DateRealFilter5 extends RealFilter5 { @JsonCreator public DateRealFilter5(@JsonProperty("field") Field field, - @JsonProperty("operator") Operator op, - @JsonProperty("values") List values) { + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { super(field, op); this.values = values; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/Field.java similarity index 84% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/Field.java index b747d16b..564e9368 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Field.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/Field.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; public enum Field { diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java similarity index 90% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java index 72438a89..b40bb460 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/IntegerRealFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/LogicalFilter5.java similarity index 75% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/LogicalFilter5.java index f5f9beaa..ae68d38d 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/LogicalFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/LogicalFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; @@ -24,15 +25,16 @@ /** * In comparison to the other implementations of LogicalFilter, this one is "simpler" from a Jackson perspective, aka - * no custom Serializer and Deserializer. - * + * no custom Serializer and Deserializer. + *

* However that simplicity comes at the "complexity" of being a Map ( extends LinkedHashMap ). * A) it feels odd, and * B) it is less memory efficient in the main line case. Aka, we don't always convert to and from JSON, but now - * we are paying the computing cost of that all the time. - * + * we are paying the computing cost of that all the time. + *

* In the end, I think this is an interesting take on the LogicalFilter, but I would use the - * @JsonSerialize and @JsonDeserialize approach in practice. + * + * @JsonSerialize and @JsonDeserialize approach in practice. */ public class LogicalFilter5 extends LinkedHashMap> implements QueryFilter5 { @@ -43,24 +45,24 @@ public class LogicalFilter5 extends LinkedHashMap> * Jackson side constructor. */ @JsonCreator - public LogicalFilter5(Map> map ) { + public LogicalFilter5(Map> map) { super(2); - if ( map.size() != 1 ) { - throw new IllegalArgumentException( "Map to build a LogicalFilter5 should be size 1. Was " + map.size() ); + if (map.size() != 1) { + throw new IllegalArgumentException("Map to build a LogicalFilter5 should be size 1. Was " + map.size()); } Operator op = map.keySet().iterator().next(); List filters = map.values().iterator().next(); - if ( filters == null ) { - throw new IllegalArgumentException( "LogicalFilter5 List> was null." ); + if (filters == null) { + throw new IllegalArgumentException("LogicalFilter5 List> was null."); } this.operator = op; this.filters = filters; // populate the map that we are for Serialization - super.put( operator, filters ); + super.put(operator, filters); } /** @@ -72,7 +74,7 @@ public LogicalFilter5(Operator operator, List filters) { this.filters = filters; // populate the map that we are for Serialization - super.put( operator, filters ); + super.put(operator, filters); } @JsonIgnore diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/MappingTest5.java similarity index 53% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/MappingTest5.java index 25362435..a9185cbf 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/MappingTest5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/MappingTest5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,31 +14,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; - -import com.bazaarvoice.jolt.Diffy; -import com.bazaarvoice.jolt.JsonUtil; -import com.bazaarvoice.jolt.JsonUtils; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.Version; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.node.ObjectNode; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtil; +import io.joltcommunity.jolt.JsonUtils; +import tools.jackson.core.JsonParser; +import tools.jackson.core.Version; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; import org.testng.Assert; import org.testng.annotations.Test; -import java.io.IOException; import java.util.Map; public class MappingTest5 { private Diffy diffy = new Diffy(); - public static class QueryFilter5Deserializer extends JsonDeserializer { + public static class QueryFilter5Deserializer extends ValueDeserializer { /** * I tried moving this logic to be an @JsonDeserialize on the QueryFilter5 interface @@ -53,86 +54,87 @@ public static class QueryFilter5Deserializer extends JsonDeserializer() {} ); + QueryFilter5 queryFilter = jsonUtil.classpathToType(testFixture, new TypeReference() {}); // Make sure the hydrated QFilter looks right - Assert.assertTrue( queryFilter instanceof LogicalFilter5); + Assert.assertTrue(queryFilter instanceof LogicalFilter5); LogicalFilter5 andFilter = (LogicalFilter5) queryFilter; - Assert.assertEquals( Operator.AND, andFilter.getOperator() ); + Assert.assertEquals(Operator.AND, andFilter.getOperator()); Assert.assertNotNull(andFilter.getValues()); Assert.assertEquals(3, andFilter.getValues().size()); // Make sure one of the top level RealFilters looks right QueryFilter5 productIdFilter = andFilter.getValues().get(1); - Assert.assertTrue( productIdFilter instanceof StringRealFilter5); + Assert.assertTrue(productIdFilter instanceof StringRealFilter5); StringRealFilter5 stringRealProductIdFilter = (StringRealFilter5) productIdFilter; - Assert.assertEquals( Field.PRODUCTID, stringRealProductIdFilter.getField() ); - Assert.assertEquals( Operator.EQ, stringRealProductIdFilter.getOperator() ); - Assert.assertEquals( "Acme-1234", stringRealProductIdFilter.getValues().get(0) ); + Assert.assertEquals(Field.PRODUCTID, stringRealProductIdFilter.getField()); + Assert.assertEquals(Operator.EQ, stringRealProductIdFilter.getOperator()); + Assert.assertEquals("Acme-1234", stringRealProductIdFilter.getValues().get(0)); // Make sure the nested OR looks right QueryFilter5 orFilter = andFilter.getValues().get(2); - Assert.assertTrue( orFilter instanceof LogicalFilter5 ); + Assert.assertTrue(orFilter instanceof LogicalFilter5); LogicalFilter5 realOrFilter = (LogicalFilter5) orFilter; - Assert.assertEquals( Operator.OR, realOrFilter.getOperator() ); - Assert.assertEquals( 2, realOrFilter.getValues().size() ); + Assert.assertEquals(Operator.OR, realOrFilter.getOperator()); + Assert.assertEquals(2, realOrFilter.getValues().size()); // Make sure nested AND looks right QueryFilter5 nestedAndFilter = realOrFilter.getValues().get(1); - Assert.assertTrue( nestedAndFilter instanceof LogicalFilter5 ); - Assert.assertEquals( Operator.AND, nestedAndFilter.getOperator() ); - Assert.assertEquals( 3, nestedAndFilter.getValues().size() ); + Assert.assertTrue(nestedAndFilter instanceof LogicalFilter5); + Assert.assertEquals(Operator.AND, nestedAndFilter.getOperator()); + Assert.assertEquals(3, nestedAndFilter.getValues().size()); // SERIALIZE TO STRING to test serialization logic - String unitTestString = jsonUtil.toJsonString( queryFilter ); + String unitTestString = jsonUtil.toJsonString(queryFilter); // LOAD and Diffy the plain vanilla JSON versions of the documents - Map actual = JsonUtils.jsonToMap( unitTestString ); - Map expected = JsonUtils.classpathToMap( testFixture ); + Map actual = JsonUtils.jsonToMap(unitTestString); + Map expected = JsonUtils.classpathToMap(testFixture); // Diffy the vanilla versions - Diffy.Result result = diffy.diff( expected, actual ); + Diffy.Result result = diffy.diff(expected, actual); if (!result.isEmpty()) { - Assert.fail( "Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString( result.expected ) + "\n actual: " + JsonUtils.toJsonString( result.actual ) ); + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); } } } + diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/Operator.java similarity index 83% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/Operator.java index cacd1fc4..c4846d85 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/Operator.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/Operator.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; public enum Operator { diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/QueryFilter5.java similarity index 83% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/QueryFilter5.java index 8b3271c0..54c56abb 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/QueryFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/QueryFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import java.util.List; diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/RealFilter5.java similarity index 70% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/RealFilter5.java index b30069d4..cdd7fc59 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/RealFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/RealFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; @@ -22,29 +23,22 @@ /** * Tell Jackson to use the "type" field to know which subclass to initialize. - * + *

* E.g. "type" : "INTEGER" --> Deserialize a IntegerRealFilter5 */ -@JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.PROPERTY,property="type") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") @JsonSubTypes({ - @JsonSubTypes.Type(value=IntegerRealFilter5.class,name="INTEGER"), - @JsonSubTypes.Type(value=StringRealFilter5.class,name="STRING"), - @JsonSubTypes.Type(value=DateRealFilter5.class,name="DATE"), - @JsonSubTypes.Type(value=BooleanRealFilter5.class,name="BOOLEAN")}) + @JsonSubTypes.Type(value = IntegerRealFilter5.class, name = "INTEGER"), + @JsonSubTypes.Type(value = StringRealFilter5.class, name = "STRING"), + @JsonSubTypes.Type(value = DateRealFilter5.class, name = "DATE"), + @JsonSubTypes.Type(value = BooleanRealFilter5.class, name = "BOOLEAN")}) public abstract class RealFilter5 implements QueryFilter5 { - public enum Type { - STRING, - INTEGER, - BOOLEAN, - DATE - } - private final Field field; private final Operator op; public RealFilter5(Field field, - Operator op) { + Operator op) { this.field = field; this.op = op; } @@ -61,4 +55,11 @@ public Operator getOperator() { public abstract List getValues(); public abstract Type getType(); + + public enum Type { + STRING, + INTEGER, + BOOLEAN, + DATE + } } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/StringRealFilter5.java similarity index 82% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/StringRealFilter5.java index 0595dc00..fa5be65f 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/five/StringRealFilter5.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/five/StringRealFilter5.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.five; +package io.joltcommunity.jolt.jsonUtil.testdomain.five; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -27,8 +28,8 @@ public class StringRealFilter5 extends RealFilter5 { @JsonCreator public StringRealFilter5(@JsonProperty("field") Field field, - @JsonProperty("operator") Operator op, - @JsonProperty("values") List values) { + @JsonProperty("operator") Operator op, + @JsonProperty("values") List values) { super(field, op); this.values = values; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java similarity index 87% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java index 70c3a045..940f045c 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/BaseRealFilter4.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.four; +package io.joltcommunity.jolt.jsonUtil.testdomain.four; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -30,7 +31,7 @@ public abstract class BaseRealFilter4 implements QueryFilter4 { private final QueryParam queryParam; @JsonCreator - public BaseRealFilter4(@JsonProperty("queryParam") QueryParam queryParam ) { + public BaseRealFilter4(@JsonProperty("queryParam") QueryParam queryParam) { this.queryParam = queryParam; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java similarity index 80% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java index 9a432978..d3b2a87c 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/BooleanRealFilter4.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.four; +package io.joltcommunity.jolt.jsonUtil.testdomain.four; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -25,7 +26,7 @@ public class BooleanRealFilter4 extends BaseRealFilter4 { @JsonCreator public BooleanRealFilter4(@JsonProperty("queryParam") QueryParam queryParam, - @JsonProperty("value") Boolean value) { + @JsonProperty("value") Boolean value) { super(queryParam); this.value = value; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java similarity index 80% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java index 719a0929..16015969 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/IntegerRealFilter4.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.four; +package io.joltcommunity.jolt.jsonUtil.testdomain.four; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -25,7 +26,7 @@ public class IntegerRealFilter4 extends BaseRealFilter4 { @JsonCreator public IntegerRealFilter4(@JsonProperty("queryParam") QueryParam queryParam, - @JsonProperty("value") Integer value) { + @JsonProperty("value") Integer value) { super(queryParam); this.value = value; } diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/LogicalFilter4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/LogicalFilter4.java new file mode 100644 index 00000000..eac9a800 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/LogicalFilter4.java @@ -0,0 +1,103 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.jsonUtil.testdomain.four; + +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.node.ObjectNode; +import tools.jackson.databind.JsonNode; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@JsonSerialize(using = LogicalFilter4.LogicalFilter4Serializer.class) +@JsonDeserialize(using = LogicalFilter4.LogicalFilter4Deserializer.class) +public class LogicalFilter4 implements QueryFilter4 { + + public static class LogicalFilter4Serializer extends ValueSerializer { + @Override + public void serialize(LogicalFilter4 filter, JsonGenerator jgen, SerializationContext provider) throws JacksonException { + jgen.writeStartObject(); + jgen.writePOJOProperty(filter.getQueryParam().toString(), filter.getFilters().values()); + jgen.writeEndObject(); + } + } + + public static class LogicalFilter4Deserializer extends ValueDeserializer { + @Override + public LogicalFilter4 deserialize(JsonParser jp, DeserializationContext ctxt) throws JacksonException { + ObjectNode root = jp.readValueAsTree(); + + // We assume it is a LogicalFilter + Iterator iter = root.propertyNames().iterator(); + String key = iter.next(); + JsonNode arrayNode = root.iterator().next(); + + if (arrayNode == null || arrayNode.isMissingNode() || !arrayNode.isArray()) { + throw new RuntimeException("Invalid format of LogicalFilter encountered."); + } + + // pass in objectReadContext so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = arrayNode.traverse(jp.objectReadContext()); + List childrenQueryFilters = subJsonParser.readValueAs(new TypeReference>() {}); + + return new LogicalFilter4(QueryParam.valueOf(key), childrenQueryFilters); + } + } + + private final QueryParam queryParam; + private final Map filters; + + public LogicalFilter4(QueryParam queryParam, List filters) { + this.queryParam = queryParam; + this.filters = new LinkedHashMap<>(); + for (QueryFilter4 queryFilter : filters) { + this.filters.put(queryFilter.getQueryParam(), queryFilter); + } + } + + @Override + public Map getFilters() { + return filters; + } + + @Override + public QueryParam getQueryParam() { + return queryParam; + } + + @Override + public boolean isLogical() { + return true; + } + + @Override + public boolean isReal() { + return false; + } +} diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/MappingTest4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/MappingTest4.java new file mode 100644 index 00000000..7bba7e04 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/MappingTest4.java @@ -0,0 +1,141 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.jsonUtil.testdomain.four; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; +import tools.jackson.core.JsonParser; +import tools.jackson.core.Version; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Map; + +public class MappingTest4 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilter4Deserializer extends ValueDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter4 deserialize(JsonParser jp, DeserializationContext ctxt) + { + + ObjectNode root = jp.readValueAsTree(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse(jp.objectReadContext()); + + // Check if it is a "RealFilter" + JsonNode valueParam = root.get("value"); + + if (valueParam == null) { + return subJsonParser.readValueAs(LogicalFilter4.class); + } + if (valueParam.isBoolean()) { + return subJsonParser.readValueAs(BooleanRealFilter4.class); + } + else if (valueParam.isString()) { + return subJsonParser.readValueAs(StringRealFilter4.class); + } + else if (valueParam.isIntegralNumber()) { + return subJsonParser.readValueAs(IntegerRealFilter4.class); + } + else { + throw new RuntimeException("Unknown type"); + } + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer(QueryFilter4.class, new QueryFilter4Deserializer()); + + ObjectMapper mapper = JsonMapper.builder() + .addModule(testModule) + .build(); + + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil(mapper); + + String testFixture = "/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter4 queryFilter = jsonUtil.classpathToType(testFixture, new TypeReference() {}); + + // Make sure the hydrated QFilter looks right + Assert.assertTrue(queryFilter instanceof LogicalFilter4); + Assert.assertEquals(QueryParam.AND, queryFilter.getQueryParam()); + Assert.assertTrue(queryFilter.isLogical()); + Assert.assertEquals(3, queryFilter.getFilters().size()); + Assert.assertNotNull(queryFilter.getFilters().get(QueryParam.OR)); + + // Make sure one of the top level RealFilters looks right + QueryFilter4 productIdFilter = queryFilter.getFilters().get(QueryParam.PRODUCTID); + Assert.assertTrue(productIdFilter.isReal()); + Assert.assertTrue(productIdFilter instanceof StringRealFilter4); + StringRealFilter4 stringRealProductIdFilter = (StringRealFilter4) productIdFilter; + Assert.assertEquals(QueryParam.PRODUCTID, stringRealProductIdFilter.getQueryParam()); + Assert.assertEquals("Acme-1234", stringRealProductIdFilter.getValue()); + + // Make sure the nested OR looks right + QueryFilter4 orFilter = queryFilter.getFilters().get(QueryParam.OR); + Assert.assertTrue(orFilter.isLogical()); + Assert.assertEquals(QueryParam.OR, orFilter.getQueryParam()); + Assert.assertEquals(2, orFilter.getFilters().size()); + + // Make sure nested AND looks right + QueryFilter4 nestedAndFilter = orFilter.getFilters().get(QueryParam.AND); + Assert.assertTrue(nestedAndFilter.isLogical()); + Assert.assertEquals(QueryParam.AND, nestedAndFilter.getQueryParam()); + Assert.assertEquals(2, nestedAndFilter.getFilters().size()); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString(queryFilter); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap(unitTestString); + Map expected = JsonUtils.classpathToMap(testFixture); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff(expected, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/QueryFilter4.java similarity index 79% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/QueryFilter4.java index 755530f5..97e38e11 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/QueryFilter4.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/QueryFilter4.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.four; +package io.joltcommunity.jolt.jsonUtil.testdomain.four; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import java.util.Map; diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/StringRealFilter4.java similarity index 80% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/StringRealFilter4.java index 109bec12..4c2c46d2 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/four/StringRealFilter4.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/four/StringRealFilter4.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,9 +14,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.four; +package io.joltcommunity.jolt.jsonUtil.testdomain.four; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; @@ -25,7 +26,7 @@ public class StringRealFilter4 extends BaseRealFilter4 { @JsonCreator public StringRealFilter4(@JsonProperty("queryParam") QueryParam queryParam, - @JsonProperty("value") String value) { + @JsonProperty("value") String value) { super(queryParam); this.value = value; } diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/one/LogicalFilter1.java similarity index 58% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/one/LogicalFilter1.java index 917d085e..80ae75d2 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/one/LogicalFilter1.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/one/LogicalFilter1.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,41 +14,28 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.one; +package io.joltcommunity.jolt.jsonUtil.testdomain.one; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryFilter; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.Map; -public class LogicalFilter1 implements QueryFilter { - - private final QueryParam queryParam; - private final Map filters; +public record LogicalFilter1(QueryParam queryParam, Map filters) implements QueryFilter { @JsonCreator - public LogicalFilter1( @JsonProperty( "queryParam" ) QueryParam queryParam, - @JsonProperty( "filters" ) Map filters ) { + public LogicalFilter1(@JsonProperty("queryParam") QueryParam queryParam, + @JsonProperty("filters") Map filters) { this.queryParam = queryParam; this.filters = filters; } - @Override - public Map getFilters() { - return filters; - } - - @Override - public QueryParam getQueryParam() { - return queryParam; - } - @Override @JsonIgnore - public String getValue() { + public String value() { return null; } diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/one/MappingTest1.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/one/MappingTest1.java new file mode 100644 index 00000000..470ed499 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/one/MappingTest1.java @@ -0,0 +1,134 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.jsonUtil.testdomain.one; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryFilter; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.RealFilter; +import tools.jackson.core.JsonParser; +import tools.jackson.core.Version; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.Map; + +public class MappingTest1 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilter1Deserializer extends ValueDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) + { + + ObjectNode root = jp.readValueAsTree(); + + JsonNode queryParam = root.get("queryParam"); + String value = queryParam.asString(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse(jp.objectReadContext()); + + // Determine the "type" of filter we are dealing with Real or Logical and specify type + if ("OR".equals(value) || "AND".equals(value)) { + return subJsonParser.readValueAs(LogicalFilter1.class); + } + else { + return subJsonParser.readValueAs(RealFilter.class); + } + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer(QueryFilter.class, new QueryFilter1Deserializer()); + + ObjectMapper mapper = JsonMapper.builder() + .addModule(testModule) + .build(); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil(mapper); + + String testFixture = "/jsonUtils/testdomain/one/queryFilter-realAndLogical.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter queryFilter = jsonUtil.classpathToType(testFixture, new TypeReference() {}); + + // Make sure the hydrated queryFilter looks right + Assert.assertTrue(queryFilter instanceof LogicalFilter1); + Assert.assertEquals(QueryParam.AND, queryFilter.queryParam()); + Assert.assertTrue(queryFilter.isLogical()); + Assert.assertEquals(3, queryFilter.filters().size()); + Assert.assertNotNull(queryFilter.filters().get(QueryParam.OR)); + + // Make sure one of the top level RealFilters looks right + QueryFilter productIdFilter = queryFilter.filters().get(QueryParam.PRODUCTID); + Assert.assertTrue(productIdFilter.isReal()); + Assert.assertEquals(QueryParam.PRODUCTID, productIdFilter.filters()); + Assert.assertEquals("Acme-1234", productIdFilter.value()); + + // Make sure the nested OR looks right + QueryFilter orFilter = queryFilter.filters().get(QueryParam.OR); + Assert.assertTrue(orFilter.isLogical()); + Assert.assertEquals(QueryParam.OR, orFilter.queryParam()); + Assert.assertEquals(2, orFilter.filters().size()); + + // Make sure nested AND looks right + QueryFilter nestedAndFilter = orFilter.filters().get(QueryParam.AND); + Assert.assertTrue(nestedAndFilter.isLogical()); + Assert.assertEquals(QueryParam.AND, nestedAndFilter.queryParam()); + Assert.assertEquals(2, nestedAndFilter.filters().size()); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString(queryFilter); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap(unitTestString); + Map expected = JsonUtils.classpathToMap(testFixture); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff(expected, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } +} + diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/three/LogicalFilter3.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/three/LogicalFilter3.java new file mode 100644 index 00000000..022f2f81 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/three/LogicalFilter3.java @@ -0,0 +1,115 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.jsonUtil.testdomain.three; + +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryFilter; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.annotation.JsonSerialize; +import tools.jackson.databind.node.ObjectNode; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +@JsonSerialize(using = LogicalFilter3.LogicalFilter3Serializer.class) +@JsonDeserialize(using = LogicalFilter3.LogicalFilter4Deserializer.class) +public class LogicalFilter3 implements QueryFilter { + + public static class LogicalFilter3Serializer extends ValueSerializer { + + @Override + public void serialize(LogicalFilter3 filter, JsonGenerator jgen, SerializationContext provider) { + jgen.writeStartObject(); + jgen.writePOJOProperty(filter.queryParam().toString(), filter.filters().values()); + jgen.writeEndObject(); + } + } + + public static class LogicalFilter4Deserializer extends ValueDeserializer { + + @Override + public LogicalFilter3 deserialize(JsonParser jp, DeserializationContext ctxt) { + + ObjectReadContext objectCodec = jp.objectReadContext(); + ObjectNode root = jp.readValueAsTree(); + + // We assume it is a LogicalFilter + Iterator iter = root.propertyNames().iterator(); + String key = iter.next(); + + JsonNode arrayNode = root.iterator().next(); + if (arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray()) { + throw new RuntimeException("Invalid format of LogicalFilter encountered."); + } + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = arrayNode.traverse(objectCodec); + List childrenQueryFilters = subJsonParser.readValueAs(new TypeReference>() {}); + + return new LogicalFilter3(QueryParam.valueOf(key), childrenQueryFilters); + } + } + + private final QueryParam queryParam; + private final Map filters; + + public LogicalFilter3(QueryParam queryParam, List filters) { + this.queryParam = queryParam; + + this.filters = new LinkedHashMap<>(); + for (QueryFilter queryFilter : filters) { + this.filters.put(queryFilter.queryParam(), queryFilter); + } + } + + @Override + public Map filters() { + return filters; + } + + @Override + public QueryParam queryParam() { + return queryParam; + } + + @Override + public String value() { + return null; + } + + @Override + public boolean isLogical() { + return true; + } + + @Override + public boolean isReal() { + return false; + } +} + diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/three/MappingTest3.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/three/MappingTest3.java new file mode 100644 index 00000000..f090a853 --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/three/MappingTest3.java @@ -0,0 +1,129 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.jsonUtil.testdomain.three; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryFilter; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.RealFilter; +import tools.jackson.core.JsonParser; +import tools.jackson.core.Version; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Map; + +public class MappingTest3 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilterDeserializer extends ValueDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) { + + ObjectNode root = jp.readValueAsTree(); + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse(jp.objectReadContext()); + + // Check if it is a "RealFilter" + JsonNode queryParam = root.get("queryParam"); + if (queryParam != null && queryParam.isValueNode()) { + return subJsonParser.readValueAs(RealFilter.class); + } + else { + return subJsonParser.readValueAs(LogicalFilter3.class); + } + } + } + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer(QueryFilter.class, new QueryFilterDeserializer()); + + ObjectMapper mapper = JsonMapper.builder() + .addModules(testModule) + .build(); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil(mapper); + + String testFixture = "/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter queryFilter = jsonUtil.classpathToType(testFixture, new TypeReference() {}); + + // Make sure the hydrated QFilter looks right + Assert.assertTrue(queryFilter instanceof LogicalFilter3); + Assert.assertEquals(QueryParam.AND, queryFilter.queryParam()); + Assert.assertTrue(queryFilter.isLogical()); + Assert.assertEquals(3, queryFilter.filters().size()); + Assert.assertNotNull(queryFilter.filters().get(QueryParam.OR)); + + // Make sure one of the top level RealFilters looks right + QueryFilter productIdFilter = queryFilter.filters().get(QueryParam.PRODUCTID); + Assert.assertTrue(productIdFilter.isReal()); + Assert.assertEquals(QueryParam.PRODUCTID, productIdFilter.queryParam()); + Assert.assertEquals("Acme-1234", productIdFilter.value()); + + // Make sure the nested OR looks right + QueryFilter orFilter = queryFilter.filters().get(QueryParam.OR); + Assert.assertTrue(orFilter.isLogical()); + Assert.assertEquals(QueryParam.OR, orFilter.queryParam()); + Assert.assertEquals(2, orFilter.filters().size()); + + // Make sure nested AND looks right + QueryFilter nestedAndFilter = orFilter.filters().get(QueryParam.AND); + Assert.assertTrue(nestedAndFilter.isLogical()); + Assert.assertEquals(QueryParam.AND, nestedAndFilter.queryParam()); + Assert.assertEquals(2, nestedAndFilter.filters().size()); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString(queryFilter); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap(unitTestString); + Map expected = JsonUtils.classpathToMap(testFixture); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff(expected, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } +} diff --git a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/two/LogicalFilter2.java similarity index 66% rename from json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java rename to json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/two/LogicalFilter2.java index 0d81f8ce..d3e1aeb7 100644 --- a/json-utils/src/test/java/com/bazaarvoice/jolt/jsonUtil/testdomain/two/LogicalFilter2.java +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/two/LogicalFilter2.java @@ -1,5 +1,6 @@ /* - * Copyright 2014 Bazaarvoice, Inc. + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,10 +14,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.bazaarvoice.jolt.jsonUtil.testdomain.two; +package io.joltcommunity.jolt.jsonUtil.testdomain.two; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryFilter; -import com.bazaarvoice.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryFilter; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; import java.util.LinkedHashMap; import java.util.List; @@ -24,37 +25,37 @@ /** * Note this class does not have any Jackson markup as all the work is being done - * in the Jackson Module inside CustomObjectMapperTest2... - * + * in the Jackson Module inside CustomObjectMapperTest2... + *

* This is an improvement over LogicalFilter1 in that we write out an Array but still - * have a Map in memory, for easy filter lookup. + * have a Map in memory, for easy filter lookup. */ public class LogicalFilter2 implements QueryFilter { private final QueryParam queryParam; private final Map filters; - public LogicalFilter2( QueryParam queryParam, List filters ) { + public LogicalFilter2(QueryParam queryParam, List filters) { this.queryParam = queryParam; this.filters = new LinkedHashMap<>(); - for ( QueryFilter queryFilter : filters ) { - this.filters.put( queryFilter.getQueryParam(), queryFilter ); + for (QueryFilter queryFilter : filters) { + this.filters.put(queryFilter.queryParam(), queryFilter); } } @Override - public Map getFilters() { + public Map filters() { return filters; } @Override - public QueryParam getQueryParam() { + public QueryParam queryParam() { return queryParam; } @Override - public String getValue() { + public String value() { return null; } diff --git a/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/two/MappingTest2.java b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/two/MappingTest2.java new file mode 100644 index 00000000..dde0fc4d --- /dev/null +++ b/json-utils/src/test/java/io/joltcommunity/jolt/jsonUtil/testdomain/two/MappingTest2.java @@ -0,0 +1,165 @@ +/* + * Copyright 2013-2023 Bazaarvoice, Inc. + * Copyright 2025 Jolt Community + * + * 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 io.joltcommunity.jolt.jsonUtil.testdomain.two; + +import io.joltcommunity.jolt.Diffy; +import io.joltcommunity.jolt.JsonUtil; +import io.joltcommunity.jolt.JsonUtils; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryFilter; +import io.joltcommunity.jolt.jsonUtil.testdomain.QueryParam; +import io.joltcommunity.jolt.jsonUtil.testdomain.RealFilter; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.ObjectReadContext; +import tools.jackson.core.Version; +import tools.jackson.core.type.TypeReference; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.ValueSerializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.ObjectNode; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class MappingTest2 { + + private Diffy diffy = new Diffy(); + + public static class QueryFilterDeserializer extends ValueDeserializer { + + /** + * Demonstrates how to do recursive polymorphic JSON deserialization in Jackson 2.2. + * + * Aka specify a Deserializer and "catch" some input, determine what type of Class it + * should be parsed too, and then reuse the Jackson infrastructure to recursively do so. + */ + @Override + public QueryFilter deserialize(JsonParser jp, DeserializationContext ctxt) + { + + ObjectReadContext objectCodec = jp.objectReadContext(); + ObjectNode root = jp.readValueAsTree(); + + // Check if it is a "RealFilter" + JsonNode queryParam = root.get("queryParam"); + if (queryParam != null && queryParam.isValueNode()) { + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = root.traverse(objectCodec); + + return subJsonParser.readValueAs(RealFilter.class); + } + + // We assume it is a LogicalFilter + Iterator iter = root.propertyNames().iterator(); + String key = iter.next(); + + JsonNode arrayNode = root.iterator().next(); + if (arrayNode == null || arrayNode.isMissingNode() || ! arrayNode.isArray()) { + throw new RuntimeException("Invalid format of LogicalFilter encountered."); + } + + // pass in our objectCodec so that the subJsonParser knows about our configured Modules and Annotations + JsonParser subJsonParser = arrayNode.traverse(objectCodec); + List childrenQueryFilters = subJsonParser.readValueAs(new TypeReference>() {}); + + return new LogicalFilter2(QueryParam.valueOf(key), childrenQueryFilters); + } + } + + public static class LogicalFilter2Serializer extends ValueSerializer { + + @Override + public void serialize(LogicalFilter2 filter, JsonGenerator jgen, SerializationContext provider) { + jgen.writeStartObject(); + jgen.writePOJOProperty(filter.queryParam().toString(), filter.filters().values()); + jgen.writeEndObject(); + } + + } + + + @Test + public void testPolymorphicJacksonSerializationAndDeserialization() + { + + SimpleModule testModule = new SimpleModule("testModule", new Version(1, 0, 0, null, null, null)) + .addDeserializer(QueryFilter.class, new QueryFilterDeserializer()) + .addSerializer(LogicalFilter2.class, new LogicalFilter2Serializer()); + + + ObjectMapper mapper = JsonMapper.builder() + .addModule(testModule) + .build(); + + // Verifying that we can pass in a custom Mapper and create a new JsonUtil + JsonUtil jsonUtil = JsonUtils.customJsonUtil(mapper); + + String testFixture = "/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json"; + + // TEST JsonUtil and our deserialization logic + QueryFilter queryFilter = jsonUtil.classpathToType(testFixture, new TypeReference() {}); + + // Make sure the hydrated QFilter looks right + Assert.assertTrue(queryFilter instanceof LogicalFilter2); + Assert.assertEquals(QueryParam.AND, queryFilter.queryParam()); + Assert.assertTrue(queryFilter.isLogical()); + Assert.assertEquals(3, queryFilter.filters().size()); + Assert.assertNotNull(queryFilter.filters().get(QueryParam.OR)); + + // Make sure one of the top level RealFilters looks right + QueryFilter productIdFilter = queryFilter.filters().get(QueryParam.PRODUCTID); + Assert.assertTrue(productIdFilter.isReal()); + Assert.assertEquals(QueryParam.PRODUCTID, productIdFilter.queryParam()); + Assert.assertEquals("Acme-1234", productIdFilter.value()); + + // Make sure the nested OR looks right + QueryFilter orFilter = queryFilter.filters().get(QueryParam.OR); + Assert.assertTrue(orFilter.isLogical()); + Assert.assertEquals(QueryParam.OR, orFilter.queryParam()); + Assert.assertEquals(2, orFilter.filters().size()); + + // Make sure nested AND looks right + QueryFilter nestedAndFilter = orFilter.filters().get(QueryParam.AND); + Assert.assertTrue(nestedAndFilter.isLogical()); + Assert.assertEquals(QueryParam.AND, nestedAndFilter.queryParam()); + Assert.assertEquals(2, nestedAndFilter.filters().size()); + + + // SERIALIZE TO STRING to test serialization logic + String unitTestString = jsonUtil.toJsonString(queryFilter); + + // LOAD and Diffy the plain vanilla JSON versions of the documents + Map actual = JsonUtils.jsonToMap(unitTestString); + Map expected = JsonUtils.classpathToMap(testFixture); + + // Diffy the vanilla versions + Diffy.Result result = diffy.diff(expected, actual); + if (!result.isEmpty()) { + Assert.fail("Failed.\nhere is a diff:\nexpected: " + JsonUtils.toJsonString(result.expected) + "\n actual: " + JsonUtils.toJsonString(result.actual)); + } + } +} + diff --git a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/actual.json b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/actual.json index 4885c9d6..9ca0e7c7 100644 --- a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/actual.json +++ b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/actual.json @@ -1,5 +1,8 @@ { - "TagDistributionOrder": [ "ConsGames", "ProsGames" ], + "TagDistributionOrder": [ + "ConsGames", + "ProsGames" + ], "TagDistribution": { "ConsGames": { "Id": "ConsGames", @@ -8,12 +11,10 @@ { "Count": 3, "Value": "Visually Unpleasing" - }, { "Count": 2, "Value": "Poor Quality" - }, { "Count": 2, diff --git a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/expected.json b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/expected.json index 87e3a80a..13accb24 100644 --- a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/expected.json +++ b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/bugFix95/expected.json @@ -1,5 +1,8 @@ { - "TagDistributionOrder": [ "ProsGames", "ConsGames" ], + "TagDistributionOrder": [ + "ProsGames", + "ConsGames" + ], "TagDistribution": { "ProsGames": { "Id": "ProsGames", diff --git a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/actual.json b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/actual.json index 956834ce..4485cc1a 100644 --- a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/actual.json +++ b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/actual.json @@ -1,8 +1,14 @@ { "a": { "x": { - "q": [4,3], - "p": [2,1] + "q": [ + 4, + 3 + ], + "p": [ + 2, + 1 + ] }, "y": [ "", @@ -18,8 +24,14 @@ }, "b": [ { - "q": [4,3], - "p": [2,1] + "q": [ + 4, + 3 + ], + "p": [ + 2, + 1 + ] }, [ "", diff --git a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/expected.json b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/expected.json index eac1da71..f085519b 100644 --- a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/expected.json +++ b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/complexCase/expected.json @@ -1,8 +1,14 @@ { "a": { "x": { - "p": [1,2], - "q": [3,4] + "p": [ + 1, + 2 + ], + "q": [ + 3, + 4 + ] }, "y": [ "", @@ -18,8 +24,14 @@ }, "b": [ { - "p": [1,2], - "q": [3,4] + "p": [ + 1, + 2 + ], + "q": [ + 3, + 4 + ] }, [ "", diff --git a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/actual.json b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/actual.json index 54efe24c..52f8ed65 100644 --- a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/actual.json +++ b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/actual.json @@ -1,10 +1,22 @@ { "a": [ - ["q","p"], - ["y","x"] + [ + "q", + "p" + ], + [ + "y", + "x" + ] ], "b": [ - [2,1], - [4,3] + [ + 2, + 1 + ], + [ + 4, + 3 + ] ] } diff --git a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/expected.json b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/expected.json index b80005be..0317f5fa 100644 --- a/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/expected.json +++ b/json-utils/src/test/resources/jsonUtils/arrayOrderObliviousDiffy/simpleCase/expected.json @@ -1,10 +1,22 @@ { "a": [ - ["x","y"], - ["p","q"] + [ + "x", + "y" + ], + [ + "p", + "q" + ] ], "b": [ - [1,2], - [3,4] + [ + 1, + 2 + ], + [ + 3, + 4 + ] ] } diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedAOODiff.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedAOODiff.json index 0b76455a..713bf9a3 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedAOODiff.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedAOODiff.json @@ -1,4 +1,12 @@ { - "a" : [ null, null ], - "b" : [ "w", null, null, null ] + "a": [ + null, + null + ], + "b": [ + "w", + null, + null, + null + ] } diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedDiff.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedDiff.json index d378c129..9a7992b8 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedDiff.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/expectedDiff.json @@ -1,4 +1,14 @@ { - "a" : [ null, null ], - "b" : [ "w", { "x" : "x" }, "y", "z"] + "a": [ + null, + null + ], + "b": [ + "w", + { + "x": "x" + }, + "y", + "z" + ] } diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testActual.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testActual.json index c4cf93cc..3ce8d606 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testActual.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testActual.json @@ -1,4 +1,14 @@ { - "a" : [ 1, 2, 3 ], - "b" : [ { "x" : "x" }, "y", "z"] + "a": [ + 1, + 2, + 3 + ], + "b": [ + { + "x": "x" + }, + "y", + "z" + ] } diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testExpected.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testExpected.json index e2bd9dd2..840e1788 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testExpected.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/differentSizedLists/testExpected.json @@ -1,4 +1,14 @@ { - "a" : [ 1, 2 ], - "b" : [ "w", { "x" : "x" }, "y", "z"] + "a": [ + 1, + 2 + ], + "b": [ + "w", + { + "x": "x" + }, + "y", + "z" + ] } diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/expectedDiff.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/expectedDiff.json index da443aef..6e82927b 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/expectedDiff.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/expectedDiff.json @@ -1,17 +1,17 @@ { - "must" : [ + "must": [ { - "bool" : { - "should" : [ + "bool": { + "should": [ null, { - "bool" : { - "must" : [ + "bool": { + "must": [ null, { - "range" : { - "firstPublishTime" : { - "to" : null + "range": { + "firstPublishTime": { + "to": null } } } @@ -23,4 +23,4 @@ }, null ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testActual.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testActual.json index 3e5383ea..ba92e4b1 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testActual.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testActual.json @@ -1,59 +1,59 @@ { - "must" : [ + "must": [ { - "bool" : { - "should" : [ + "bool": { + "should": [ { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "term" : { - "client" : "testcustomer-concierge-synd1" + "term": { + "client": "testcustomer-concierge-synd1" } }, { - "term" : { - "subjectProduct.externalId.lc" : "common-product" + "term": { + "subjectProduct.externalId.lc": "common-product" } } ] } }, { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "terms" : { - "subjectProduct.coordinate" : [ + "terms": { + "subjectProduct.coordinate": [ "catalog:testcustomer-concierge-synd:/product::common-product" ] } }, { - "range" : { - "firstPublishTime" : { - "from" : null, - "to" : "2015-12-21T00:00:00.000-06:00", - "include_lower" : true, - "include_upper" : false + "range": { + "firstPublishTime": { + "from": null, + "to": "2015-12-21T00:00:00.000-06:00", + "include_lower": true, + "include_upper": false } } } ], - "must_not" : [ + "must_not": [ { - "term" : { - "subjectProduct.attribute.DISABLED" : true + "term": { + "subjectProduct.attribute.DISABLED": true } }, { - "term" : { - "subjectCategory.attribute.DISABLED" : true + "term": { + "subjectCategory.attribute.DISABLED": true } }, { - "terms" : { - "contentCodes" : [ + "terms": { + "contentCodes": [ "RET", "PC", "PRI", @@ -68,9 +68,9 @@ } }, { - "term" : { - "status" : "APPROVED" + "term": { + "status": "APPROVED" } } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testExpected.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testExpected.json index af6c74e2..63d06871 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testExpected.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery1/testExpected.json @@ -1,59 +1,59 @@ { - "must" : [ + "must": [ { - "bool" : { - "should" : [ + "bool": { + "should": [ { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "term" : { - "client" : "testcustomer-concierge-synd1" + "term": { + "client": "testcustomer-concierge-synd1" } }, { - "term" : { - "subjectProduct.externalId.lc" : "common-product" + "term": { + "subjectProduct.externalId.lc": "common-product" } } ] } }, { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "terms" : { - "subjectProduct.coordinate" : [ + "terms": { + "subjectProduct.coordinate": [ "catalog:testcustomer-concierge-synd:/product::common-product" ] } }, { - "range" : { - "firstPublishTime" : { - "from" : null, - "to" : null, - "include_lower" : true, - "include_upper" : false + "range": { + "firstPublishTime": { + "from": null, + "to": null, + "include_lower": true, + "include_upper": false } } } ], - "must_not" : [ + "must_not": [ { - "term" : { - "subjectProduct.attribute.DISABLED" : true + "term": { + "subjectProduct.attribute.DISABLED": true } }, { - "term" : { - "subjectCategory.attribute.DISABLED" : true + "term": { + "subjectCategory.attribute.DISABLED": true } }, { - "terms" : { - "contentCodes" : [ + "terms": { + "contentCodes": [ "RET", "PC", "PRI", @@ -68,9 +68,9 @@ } }, { - "term" : { - "status" : "APPROVED" + "term": { + "status": "APPROVED" } } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedAOODiff.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedAOODiff.json index da443aef..6e82927b 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedAOODiff.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedAOODiff.json @@ -1,17 +1,17 @@ { - "must" : [ + "must": [ { - "bool" : { - "should" : [ + "bool": { + "should": [ null, { - "bool" : { - "must" : [ + "bool": { + "must": [ null, { - "range" : { - "firstPublishTime" : { - "to" : null + "range": { + "firstPublishTime": { + "to": null } } } @@ -23,4 +23,4 @@ }, null ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedDiff.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedDiff.json index af6c74e2..63d06871 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedDiff.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/expectedDiff.json @@ -1,59 +1,59 @@ { - "must" : [ + "must": [ { - "bool" : { - "should" : [ + "bool": { + "should": [ { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "term" : { - "client" : "testcustomer-concierge-synd1" + "term": { + "client": "testcustomer-concierge-synd1" } }, { - "term" : { - "subjectProduct.externalId.lc" : "common-product" + "term": { + "subjectProduct.externalId.lc": "common-product" } } ] } }, { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "terms" : { - "subjectProduct.coordinate" : [ + "terms": { + "subjectProduct.coordinate": [ "catalog:testcustomer-concierge-synd:/product::common-product" ] } }, { - "range" : { - "firstPublishTime" : { - "from" : null, - "to" : null, - "include_lower" : true, - "include_upper" : false + "range": { + "firstPublishTime": { + "from": null, + "to": null, + "include_lower": true, + "include_upper": false } } } ], - "must_not" : [ + "must_not": [ { - "term" : { - "subjectProduct.attribute.DISABLED" : true + "term": { + "subjectProduct.attribute.DISABLED": true } }, { - "term" : { - "subjectCategory.attribute.DISABLED" : true + "term": { + "subjectCategory.attribute.DISABLED": true } }, { - "terms" : { - "contentCodes" : [ + "terms": { + "contentCodes": [ "RET", "PC", "PRI", @@ -68,9 +68,9 @@ } }, { - "term" : { - "status" : "APPROVED" + "term": { + "status": "APPROVED" } } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testActual.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testActual.json index f6870faa..ee9f3c6e 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testActual.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testActual.json @@ -1,64 +1,64 @@ { - "must" : [ + "must": [ { - "term" : { - "status" : "APPROVED" + "term": { + "status": "APPROVED" } }, { - "bool" : { - "should" : [ + "bool": { + "should": [ { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "term" : { - "client" : "testcustomer-concierge-synd1" + "term": { + "client": "testcustomer-concierge-synd1" } }, { - "term" : { - "subjectProduct.externalId.lc" : "common-product" + "term": { + "subjectProduct.externalId.lc": "common-product" } } ] } }, { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "terms" : { - "subjectProduct.coordinate" : [ + "terms": { + "subjectProduct.coordinate": [ "catalog:testcustomer-concierge-synd:/product::common-product" ] } }, { - "range" : { - "firstPublishTime" : { - "from" : null, - "to" : "2015-12-21T00:00:00.000-06:00", - "include_lower" : true, - "include_upper" : false + "range": { + "firstPublishTime": { + "from": null, + "to": "2015-12-21T00:00:00.000-06:00", + "include_lower": true, + "include_upper": false } } } ], - "must_not" : [ + "must_not": [ { - "term" : { - "subjectProduct.attribute.DISABLED" : true + "term": { + "subjectProduct.attribute.DISABLED": true } }, { - "term" : { - "subjectCategory.attribute.DISABLED" : true + "term": { + "subjectCategory.attribute.DISABLED": true } }, { - "terms" : { - "contentCodes" : [ + "terms": { + "contentCodes": [ "RET", "PRI", "PC", @@ -73,4 +73,4 @@ } } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testExpected.json b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testExpected.json index af6c74e2..63d06871 100644 --- a/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testExpected.json +++ b/json-utils/src/test/resources/jsonUtils/diffyWhenDifferent/esQuery2/testExpected.json @@ -1,59 +1,59 @@ { - "must" : [ + "must": [ { - "bool" : { - "should" : [ + "bool": { + "should": [ { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "term" : { - "client" : "testcustomer-concierge-synd1" + "term": { + "client": "testcustomer-concierge-synd1" } }, { - "term" : { - "subjectProduct.externalId.lc" : "common-product" + "term": { + "subjectProduct.externalId.lc": "common-product" } } ] } }, { - "bool" : { - "must" : [ + "bool": { + "must": [ { - "terms" : { - "subjectProduct.coordinate" : [ + "terms": { + "subjectProduct.coordinate": [ "catalog:testcustomer-concierge-synd:/product::common-product" ] } }, { - "range" : { - "firstPublishTime" : { - "from" : null, - "to" : null, - "include_lower" : true, - "include_upper" : false + "range": { + "firstPublishTime": { + "from": null, + "to": null, + "include_lower": true, + "include_upper": false } } } ], - "must_not" : [ + "must_not": [ { - "term" : { - "subjectProduct.attribute.DISABLED" : true + "term": { + "subjectProduct.attribute.DISABLED": true } }, { - "term" : { - "subjectCategory.attribute.DISABLED" : true + "term": { + "subjectCategory.attribute.DISABLED": true } }, { - "terms" : { - "contentCodes" : [ + "terms": { + "contentCodes": [ "RET", "PC", "PRI", @@ -68,9 +68,9 @@ } }, { - "term" : { - "status" : "APPROVED" + "term": { + "status": "APPROVED" } } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/jsonUtils-removeRecursive.json b/json-utils/src/test/resources/jsonUtils/jsonUtils-removeRecursive.json index c3a483a9..8aa26a65 100644 --- a/json-utils/src/test/resources/jsonUtils/jsonUtils-removeRecursive.json +++ b/json-utils/src/test/resources/jsonUtils/jsonUtils-removeRecursive.json @@ -1,28 +1,25 @@ [ { - "input" : { - "L1_A" : { - "L2_A" : { - "L3_A" : "Good", - "L3_B" : "RemoveThis" + "input": { + "L1_A": { + "L2_A": { + "L3_A": "Good", + "L3_B": "RemoveThis" }, - "L2_B" : "l2_b" + "L2_B": "l2_b" }, - "L1_B" : "l1_b", - - "L3_B" : "RemoveThis" + "L1_B": "l1_b", + "L3_B": "RemoveThis" }, - - "remove" : "L3_B", - - "expected" : { - "L1_A" : { - "L2_A" : { - "L3_A" : "Good" + "remove": "L3_B", + "expected": { + "L1_A": { + "L2_A": { + "L3_A": "Good" }, - "L2_B" : "l2_b" + "L2_B": "l2_b" }, - "L1_B" : "l1_b" + "L1_B": "l1_b" } } -] \ No newline at end of file +] diff --git a/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json b/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json index 23c53ebe..13bac18b 100644 --- a/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json +++ b/json-utils/src/test/resources/jsonUtils/queryFilter-realOnly.json @@ -1,10 +1,10 @@ { - "RATING" : { - "queryParam" : "RATING", - "value" : "3" + "RATING": { + "queryParam": "RATING", + "value": "3" }, - "PRODUCTID" : { - "queryParam" : "PRODUCTID", - "value" : "Acme-1234" + "PRODUCTID": { + "queryParam": "PRODUCTID", + "value": "Acme-1234" } -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json b/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json index 376a190a..c11cb9e1 100644 --- a/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json +++ b/json-utils/src/test/resources/jsonUtils/testdomain/five/queryFilter-realAndLogical5.json @@ -1,4 +1,3 @@ - // Even better format. Values are a list and are typed. // Now that the RealFilters values are a list, Real and Logicial Fitlers can // share a getValues( List list ) interface, which is kinda nice. @@ -16,46 +15,59 @@ // ... // }) { - "AND" : [ + "AND": [ { - "type" : "INTEGER", + "type": "INTEGER", "field": "RATING", - "operator" : "EQ", - "values": [ 3 ] + "operator": "EQ", + "values": [ + 3 + ] }, { - "type" : "STRING", + "type": "STRING", "field": "PRODUCTID", - "operator" : "EQ", - "values": [ "Acme-1234" ] + "operator": "EQ", + "values": [ + "Acme-1234" + ] }, { - "OR" : [ + "OR": [ { - "type" : "STRING", + "type": "STRING", "field": "ID", - "operator" : "EQ", - "values": [ "789" ] + "operator": "EQ", + "values": [ + "789" + ] }, { - "AND" : [ + "AND": [ { - "type" : "BOOLEAN", + "type": "BOOLEAN", "field": "ISFEATURED", - "operator" : "EQ", - "values": [ true ] + "operator": "EQ", + "values": [ + true + ] }, { - "type" : "BOOLEAN", + "type": "BOOLEAN", "field": "HASPHOTOS", - "operator" : "EQ", - "values": [ true ] + "operator": "EQ", + "values": [ + true + ] }, { - "type" : "DATE", + "type": "DATE", "field": "SUBMISSION_TIME", - "operator" : "LTE", - "values": [ "1-1-2014" ] // This is the reason the type Enum is needed + "operator": "LTE", + "values": [ + "1-1-2014" + ] + // This is the reason the type Enum is needed // as DATE and STRING both "encode" to JSON strings } ] @@ -63,4 +75,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json b/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json index d4bfdd89..06cf24cc 100644 --- a/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json +++ b/json-utils/src/test/resources/jsonUtils/testdomain/four/queryFilter-realAndLogical4.json @@ -1,10 +1,9 @@ - // Even better format. Values are typed. // This formulation requires a one to one mapping between the JSON type (String, number, boolean) // and a RealFilter. In practice this would not work as String and Date filters have // JSON values that are strings. { - "AND" : [ + "AND": [ { "queryParam": "RATING", "value": 3 @@ -14,13 +13,13 @@ "value": "Acme-1234" }, { - "OR" : [ + "OR": [ { "queryParam": "ID", "value": "789" }, { - "AND" : [ + "AND": [ { "queryParam": "ISFEATURED", "value": true @@ -34,4 +33,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json b/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json index 7413bdf0..c39a2033 100644 --- a/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json +++ b/json-utils/src/test/resources/jsonUtils/testdomain/one/queryFilter-realAndLogical.json @@ -1,11 +1,10 @@ - // This works, but overly wordy. // The "queryParam" is needlessly repeated. // // Also note all the real filter values are Strings, aka "value": "3". { "queryParam": "AND", - "filters" : { + "filters": { "RATING": { "queryParam": "RATING", "value": "3" @@ -16,19 +15,19 @@ }, "OR": { "queryParam": "OR", - "filters" : { - "ID" : { + "filters": { + "ID": { "queryParam": "ID", "value": "789" }, - "AND" : { + "AND": { "queryParam": "AND", - "filters" : { - "ISFEATURED" : { + "filters": { + "ISFEATURED": { "queryParam": "ISFEATURED", "value": "true" }, - "HASPHOTOS" : { + "HASPHOTOS": { "queryParam": "HASPHOTOS", "value": "true" } @@ -37,4 +36,4 @@ } } } -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json b/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json index 30ceccd8..04ddf9b2 100644 --- a/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json +++ b/json-utils/src/test/resources/jsonUtils/testdomain/two/queryFilter-realAndLogical2.json @@ -1,4 +1,3 @@ - // This is a much nicer formulation / format. // Silly "queryParam" is gone, but still have all values being Strings, e.g. "value": "3". // @@ -6,7 +5,7 @@ // Java "three" is nicer than "two", in that more logic was moved out of the MappingTest Module // into the LogicalFilter3 @JsonDeserializer { - "AND" : [ + "AND": [ { "queryParam": "RATING", "value": "3" @@ -16,13 +15,13 @@ "value": "Acme-1234" }, { - "OR" : [ + "OR": [ { "queryParam": "ID", "value": "789" }, { - "AND" : [ + "AND": [ { "queryParam": "ISFEATURED", "value": "true" @@ -36,4 +35,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/json-utils/src/test/resources/jsonUtils/valid.json b/json-utils/src/test/resources/jsonUtils/valid.json new file mode 100644 index 00000000..a87ae4ae --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/valid.json @@ -0,0 +1,3 @@ +{ + "foo": 123 +} diff --git a/json-utils/src/test/resources/jsonUtils/valid_list.json b/json-utils/src/test/resources/jsonUtils/valid_list.json new file mode 100644 index 00000000..6001c443 --- /dev/null +++ b/json-utils/src/test/resources/jsonUtils/valid_list.json @@ -0,0 +1 @@ +[1, 2, 3] \ No newline at end of file diff --git a/parent/pom.xml b/parent/pom.xml index 67e541cd..021d73d2 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -1,45 +1,85 @@ - + 4.0.0 - - com.bazaarvoice.commons - bv-opensource-super-pom - 1.4 - - - - - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 pom Jolt Parent + JSON to JSON transformation library written in Java where the "specification" for the transform is itself a JSON document. + https://github.com/jolt-community/jolt-community + + + bobeal + Benoit Orihuela + bobeal@pm.me + + + emmansun + Sun Yimin + emman.sun@foxmail.com + + + + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + - https://github.com/bazaarvoice/jolt - scm:git:git@github.com:bazaarvoice/jolt.git - scm:git:git@github.com:bazaarvoice/jolt.git - HEAD - + https://github.com/jolt-community/jolt-community + scm:git:git@github.com:jolt-community/jolt-community.git + scm:git:git@github.com:jolt-community/jolt-community.git + HEAD + - - 1.8 + 17 + 3.9.0 + UTF-8 + UTF-8 + + + 3.5.0 + 3.6.2 + 3.5.0 + 3.15.0 + 12.2.1 + 3.5.5 + 3.5.0 + 3.4.0 + 3.12.0 + 3.5.5 + 3.1.4 + 3.3.1 + 3.8.0 + 3.10.0 + 3.6.2 + 0.18 + 3.6.1 + 2.21.0 + 0.8.14 + 3.2.8 + 0.10.0 - 3.4 - 1 + 3.20.0 + 2.0.1.MR - 2.13.4 - 4.1.0 - 0.4.4 + 3.1.2 + 7.0.0 + 0.9.0 + 1.0.4 - 29.0-jre - 6.8.21 + 33.6.0-jre + 7.12.0 @@ -47,19 +87,21 @@ - javax.inject - javax.inject - ${javax-inject.version} + jakarta.inject + jakarta.inject-api + ${jakarta-inject.version} - com.fasterxml.jackson.core + tools.jackson.core jackson-databind ${jackson.version} + + - com.fasterxml.jackson.core + tools.jackson.core jackson-core ${jackson.version} @@ -78,6 +120,12 @@ ${argparse4j.version} + + org.reactivestreams + reactive-streams + ${reactive-streams.version} + + org.apache.commons @@ -101,73 +149,326 @@ + + + + src/main/java + + **/*.java + + + + src/main/resources + + + + + + ${project.build.testSourceDirectory} + + **/*.java + + + + src/test/resources + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + enforce-versions + initialize + + enforce + + + + + [${maven.minimum.version},) + + + ${java.minimum.version} + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.minimum.version} + ${java.minimum.version} + -Xlint:all + true + true + true + + + + + org.owasp + dependency-check-maven + + + + check + + + + + + org.apache.maven.plugins maven-source-plugin + attach-sources + verify + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadoc jar + + java + none + org.apache.rat apache-rat-plugin - 0.12 + ${maven-rat-plugin.version} - - **/*.java - + **/*.java + + **/*.md + **/OWNERS + /test-output/ + - rat-check test check - - - - - org.apache.maven.doxia - doxia-core - 1.6 - - - xerces - xercesImpl - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 + org.jacoco + jacoco-maven-plugin - attach-javadocs + report - jar + report-aggregate - - -Xdoclint:none - false - + verify - + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + org.owasp + dependency-check-maven + ${dependency-check-maven.version} + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + -Dfile.encoding=UTF-8 -Djava.awt.headless=true + + + test/integration/** + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + true + + + + + + org.apache.maven.plugins + maven-source-plugin + ${maven-source-plugin.version} + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven-failsafe-plugin.version} + + -Dfile.encoding=UTF-8 -Djava.awt.headless=true + + test/integration/** + + + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + + org.apache.maven.plugins + maven-release-plugin + ${maven-release-plugin.version} + + branch admin - + + + + org.apache.maven.plugins + maven-assembly-plugin + ${maven-assembly-plugin.version} + + + org.apache.maven.plugins + maven-dependency-plugin + ${maven-dependency-plugin.version} + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + org.codehaus.mojo + build-helper-maven-plugin + ${build-helper-maven-plugin.version} + + + org.codehaus.mojo + versions-maven-plugin + ${versions-maven-plugin.version} + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin.version} + + + + + + + release + + + performRelease + true + + + + + + org.sonatype.central + central-publishing-maven-plugin + ${central-publishing-maven-plugin.version} + true + + central + true + + + + org.apache.maven.plugins + maven-gpg-plugin + ${maven-gpg-plugin.version} + + + sign-artifacts + verify + + sign + + + + + + --pinentry-mode + loopback + + + + + + + diff --git a/pom.xml b/pom.xml index 4e3a14ad..0c96ca10 100644 --- a/pom.xml +++ b/pom.xml @@ -1,23 +1,31 @@ - + 4.0.0 - com.bazaarvoice.jolt - jolt-parent - 0.1.9-SNAPSHOT + io.github.jolt-community.jolt + jolt-community-parent + 1.2.0 parent/pom.xml - jolt + jolt-community pom Jolt + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + - https://github.com/bazaarvoice/jolt - scm:git:git@github.com:bazaarvoice/jolt.git - scm:git:git@github.com:bazaarvoice/jolt.git - HEAD - + https://github.com/jolt-community/jolt-community + scm:git:git@github.com:jolt-community/jolt-community.git + scm:git:git@github.com:jolt-community/jolt-community.git + HEAD + parent @@ -28,4 +36,4 @@ complete - \ No newline at end of file +