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)
========
+[](https://github.com/jolt-community/jolt-community/actions/workflows/ci.yml)
+[](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
-
-[](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-cliJolt Command Line Tools
+
+
+ The Apache Software License, Version 2.0
+ https://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
net.sourceforge.argparse4jargparse4j
+ ${argparse4j.version}
- com.bazaarvoice.jolt
- jolt-complete
+ io.github.jolt-community.jolt
+ jolt-community-complete${project.version}org.testngtestng
+ ${testng.version}test
@@ -39,7 +49,7 @@
org.apache.maven.pluginsmaven-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-completeJolt Completejar
+
+
+ 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.testngtestng
+ ${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