├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature-request.md └── workflows │ ├── commit.yml │ └── deploy.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── pom.xml └── src ├── main ├── assemblies │ └── plugin.xml ├── java │ ├── io │ │ └── zentity │ │ │ ├── common │ │ │ ├── AbstractGroupedActionListener.java │ │ │ ├── AsyncCollectionRunner.java │ │ │ ├── Json.java │ │ │ ├── Patterns.java │ │ │ └── StreamUtil.java │ │ │ ├── model │ │ │ ├── Attribute.java │ │ │ ├── Index.java │ │ │ ├── IndexField.java │ │ │ ├── Matcher.java │ │ │ ├── Model.java │ │ │ ├── Resolver.java │ │ │ └── ValidationException.java │ │ │ └── resolution │ │ │ ├── Job.java │ │ │ ├── Query.java │ │ │ └── input │ │ │ ├── Attribute.java │ │ │ ├── Input.java │ │ │ ├── Term.java │ │ │ ├── scope │ │ │ ├── Exclude.java │ │ │ ├── Include.java │ │ │ ├── Scope.java │ │ │ └── ScopeField.java │ │ │ └── value │ │ │ ├── BooleanValue.java │ │ │ ├── DateValue.java │ │ │ ├── NumberValue.java │ │ │ ├── StringValue.java │ │ │ ├── Value.java │ │ │ └── ValueInterface.java │ └── org │ │ └── elasticsearch │ │ └── plugin │ │ └── zentity │ │ ├── BulkAction.java │ │ ├── HomeAction.java │ │ ├── ModelsAction.java │ │ ├── ParamsUtil.java │ │ ├── ResolutionAction.java │ │ ├── SetupAction.java │ │ └── ZentityPlugin.java └── resources │ ├── license-header-notice.xml │ ├── license-header.txt │ ├── notice-template.ftl │ ├── plugin-descriptor.properties │ └── zentity.properties └── test ├── java ├── io │ └── zentity │ │ ├── common │ │ ├── AsyncCollectionRunnerTest.java │ │ └── StreamUtilTest.java │ │ ├── model │ │ ├── AttributeTest.java │ │ ├── IndexFieldTest.java │ │ ├── IndexTest.java │ │ ├── MatcherTest.java │ │ ├── ModelTest.java │ │ └── ResolverTest.java │ │ └── resolution │ │ ├── JobTest.java │ │ └── input │ │ ├── InputTest.java │ │ └── TermTest.java └── org │ └── elasticsearch │ └── plugin │ └── zentity │ ├── AbstractIT.java │ ├── HomeActionIT.java │ ├── ModelsActionIT.java │ ├── ResolutionActionIT.java │ ├── SetupActionIT.java │ └── ZentityPluginIT.java └── resources ├── TestData.txt ├── TestDataArrays.txt ├── TestDataObjectArrays.txt ├── TestEntityModelA.json ├── TestEntityModelArrays.json ├── TestEntityModelB.json ├── TestEntityModelElasticsearchError.json ├── TestEntityModelObjectArrays.json ├── TestEntityModelZentityError.json ├── TestIndex.json ├── TestIndexArrays.json ├── TestIndexObjectArrays.json ├── docker-compose.yml └── log4j.xml /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[Bug] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | 12 | ### Environment 13 | - zentity version: 14 | - Elasticsearch version: 15 | 16 | ### Describe the bug 17 | 18 | A brief description of the behavior you observed. 19 | 20 | ### Expected behavior 21 | 22 | A brief description of what you expected to happen. 23 | 24 | ### Steps to reproduce 25 | 26 | 1. ... 27 | 2. ... 28 | 3. ... 29 | 30 | ### Additional context 31 | 32 | The following information is helpful in many cases: 33 | 34 | - Entity model(s): `GET _zentity/models/ENTITY_TYPE` 35 | - Index mappings and settings: `GET INDEX_NAME` 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an enhancement for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/commit.yml: -------------------------------------------------------------------------------- 1 | # Build and test zentity for each supported version of Elasticsearch whenever pushing a commit or submitting a pull request. 2 | name: Commit 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - "zentity-*.*.*" 9 | - "zentity-*.*.*-*" 10 | pull_request: 11 | branches: 12 | - '**' 13 | tags-ignore: 14 | - "zentity-*.*.*" 15 | - "zentity-*.*.*-*" 16 | 17 | jobs: 18 | build: 19 | strategy: 20 | 21 | # Attempt to run all tests. 22 | fail-fast: false 23 | 24 | # Build and test zentity for each version of Elasticsearch that its APIs support. 25 | matrix: 26 | elasticsearch: 27 | - 8.17.0 28 | - 8.16.2 29 | - 8.16.1 30 | - 8.16.0 31 | - 8.15.5 32 | - 8.15.4 33 | - 8.15.3 34 | - 8.15.2 35 | - 8.15.1 36 | - 8.15.0 37 | - 8.14.3 38 | - 8.14.2 39 | - 8.14.1 40 | - 8.14.0 41 | - 8.13.3 42 | - 8.13.2 43 | - 8.13.1 44 | - 8.13.0 45 | 46 | # Use the latest LTS OS. 47 | runs-on: ubuntu-20.04 48 | 49 | steps: 50 | 51 | # Checkout the repository. 52 | # Uses: https://github.com/actions/checkout 53 | - name: Checkout 54 | uses: actions/checkout@v2 55 | 56 | # Configure the JDK. 57 | # Uses: https://github.com/actions/setup-java 58 | # Elasticsearch JVM support matrix: https://www.elastic.co/support/matrix#matrix_jvm 59 | - name: Set up JDK 60 | uses: actions/setup-java@v1 61 | with: 62 | java-version: 17 63 | 64 | # Cache Maven packages. 65 | # Uses: https://github.com/actions/cache 66 | - name: Cache packages 67 | uses: actions/cache@v2 68 | with: 69 | path: ~/.m2 70 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 71 | restore-keys: ${{ runner.os }}-m2 72 | 73 | # Build and test zentity for a given version of Elasticsearch. 74 | - name: Build and test 75 | env: 76 | ELASTICSEARCH_VERSION: ${{ matrix.elasticsearch }} 77 | run: mvn --batch-mode clean install "-Delasticsearch.version=$ELASTICSEARCH_VERSION" --file pom.xml -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | # Build, test, and deploy zentity for each supported version of Elasticsearch whenever pushing a release tag. 2 | name: Deploy 3 | on: 4 | push: 5 | tags: 6 | - "zentity-*.*.*" 7 | - "zentity-*.*.*-*" 8 | 9 | jobs: 10 | create_release: 11 | name: Create Release 12 | 13 | # Use the latest LTS OS. 14 | runs-on: ubuntu-20.04 15 | outputs: 16 | upload_url: ${{ steps.create_release.outputs.upload_url }} 17 | release_tag: ${{ steps.get_tag.outputs.tag }} 18 | zentity_version: ${{ steps.get_zentity_version.outputs.first_match }} 19 | steps: 20 | 21 | # Checkout the repository. 22 | # Uses: https://github.com/actions/checkout 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | 26 | # Get the release tag. 27 | - name: Get release tag 28 | id: get_tag 29 | run: echo ::set-output name=tag::${GITHUB_REF#refs/*/} 30 | 31 | # Get the zentity version by excluding the zentity- prefix in the release tag. 32 | # Uses: https://github.com/AsasInnab/regex-action 33 | - name: Get zentity version 34 | id: get_zentity_version 35 | uses: AsasInnab/regex-action@v1 36 | with: 37 | regex_pattern: "(?<=^zentity-).*" 38 | regex_flags: "i" 39 | search_string: "${{ steps.get_tag.outputs.tag }}" 40 | 41 | # Create the release. 42 | # Uses: https://github.com/actions/create-release 43 | - name: Create Release 44 | id: create_release 45 | uses: actions/create-release@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | tag_name: ${{ github.ref }} 50 | release_name: ${{ github.ref }} 51 | draft: false 52 | prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') || contains(github.ref, 'experimental') }} 53 | body: "Releasing zentity-${{ steps.get_zentity_version.outputs.first_match }}" 54 | 55 | build_and_upload_artifacts: 56 | name: Build and Upload Artifacts 57 | needs: create_release 58 | 59 | # Use the latest LTS OS. 60 | runs-on: ubuntu-20.04 61 | 62 | # Set the zentity version as the value of the release tag. 63 | env: 64 | ZENTITY_VERSION: ${{ needs.create_release.outputs.zentity_version }} 65 | strategy: 66 | 67 | # Build and test zentity for each version of Elasticsearch that its APIs support. 68 | matrix: 69 | elasticsearch: 70 | - 8.17.0 71 | - 8.16.2 72 | - 8.16.1 73 | - 8.16.0 74 | - 8.15.5 75 | - 8.15.4 76 | - 8.15.3 77 | - 8.15.2 78 | - 8.15.1 79 | - 8.15.0 80 | - 8.14.3 81 | - 8.14.2 82 | - 8.14.1 83 | - 8.14.0 84 | - 8.13.3 85 | - 8.13.2 86 | - 8.13.1 87 | - 8.13.0 88 | steps: 89 | 90 | # Checkout the repository. 91 | # Uses: https://github.com/actions/checkout 92 | - name: Checkout 93 | uses: actions/checkout@v2 94 | 95 | # Configure the JDK. 96 | # Uses: https://github.com/actions/setup-java 97 | # Elasticsearch JVM support matrix: https://www.elastic.co/support/matrix#matrix_jvm 98 | - name: Set up JDK 99 | uses: actions/setup-java@v1 100 | with: 101 | java-version: 17 102 | 103 | # Cache Maven packages. 104 | # Uses: https://github.com/actions/cache 105 | - name: Cache packages 106 | uses: actions/cache@v2 107 | with: 108 | path: ~/.m2 109 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} 110 | restore-keys: ${{ runner.os }}-m2 111 | 112 | # Build and test zentity for a given version of Elasticsearch. 113 | - name: Build and test 114 | env: 115 | ELASTICSEARCH_VERSION: ${{ matrix.elasticsearch }} 116 | run: mvn --batch-mode clean install -Dzentity.version=$ZENTITY_VERSION -Delasticsearch.version=$ELASTICSEARCH_VERSION --file pom.xml 117 | 118 | # Set the artifact name using the given versions of zentity and Elasticsearch. 119 | - name: Set artifact name 120 | id: set_artifact_name 121 | env: 122 | ELASTICSEARCH_VERSION: ${{ matrix.elasticsearch }} 123 | run: echo ::set-output name=name::zentity-$ZENTITY_VERSION-elasticsearch-$ELASTICSEARCH_VERSION.zip 124 | 125 | # Upload the release asset to: https://github.com/zentity-io/zentity/releases 126 | # Uses: https://github.com/actions/upload-release-asset 127 | - name: Upload release asset 128 | id: upload_release_asset 129 | uses: actions/upload-release-asset@v1 130 | env: 131 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 132 | with: 133 | upload_url: ${{ needs.create_release.outputs.upload_url }} 134 | asset_path: ./target/releases/${{ steps.set_artifact_name.outputs.name }} 135 | asset_name: ${{ steps.set_artifact_name.outputs.name }} 136 | asset_content_type: application/zip -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | *.iws 5 | 6 | # Maven 7 | log/ 8 | target/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to zentity 2 | 3 | 4 | ## Feature requests and bug reports 5 | 6 | Open an issue on the [issue list](https://github.com/zentity-io/zentity/issues). 7 | 8 | 9 | ## Development 10 | 11 | ### Setting up the development environment 12 | 13 | zentity was developed in IntelliJ IDEA and uses Maven to manage dependencies, tests, and builds. 14 | 15 | **Prepare your environment:** 16 | 17 | 1. Install [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) 18 | 2. Install [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) 19 | 3. Install [JDK 17](https://www.oracle.com/java/technologies/downloads/#java17) 20 | 21 | **Create the project on IntelliJ IDEA:** 22 | 23 | 1. File > New > Project from Version Control 24 | - Version Control: Git 25 | - URL: https://www.github.com/zentity-io/zentity 26 | - Directory: Your local repository directory 27 | 28 | **Configure the project to use JDK 11:** 29 | 30 | 1. Navigate to File > Project Structure 31 | 2. Navigate to Project Settings > Project Settings 32 | 3. Set the Project SDK to the home path of JDK 11 33 | 34 | **Configure the Maven Run Configurations:** 35 | 36 | 1. Navigate to Run > Edit Configurations 37 | 2. Navigate to Maven 38 | 3. Create configurations with the following values for "Command line": 39 | - `clean install` - Runs all tests and builds zentity locally. 40 | - `clean install -e -X -Dtests.security.manager=false` - Runs all tests and builds zentity locally with extra debugging details. 41 | - `test -DskipIntegTests=true` - Runs unit tests locally. 42 | 4. Run these frequently to ensure that your tests continue to pass as you modify the code. 43 | 44 | **Known issues:** 45 | 46 | - When integration tests fail, the spawned Elasticsearch server will continue to run. You must find the process PID running on port 9400 and terminate it before you can run integration tests again. 47 | 48 | 49 | ### Important files 50 | 51 | - **.travis.yml** - Build matrix for travis-ci.org 52 | - **pom.xml** - Maven configuration file that defines project settings, dependency versions, build behavior, and high level test behavior. 53 | - **src/main/resources/plugin-descriptor.properties** - File required by all Elasticsearch plugins ([source](https://www.elastic.co/guide/en/elasticsearch/plugins/current/plugin-authors.html#_plugin_descriptor_file)). 54 | - **src/test/ant/integration-tests.xml** - Ant script that controls the integration tests. 55 | - **src/test/java** - Code for unit tests and integration tests. Unit tests are suffixed with `Test`. Integration test classes are suffixed with `IT`. 56 | - **src/test/resources** - Data, entity models, and configurations used by tests. 57 | 58 | 59 | ### Software design choices 60 | 61 | - **Outputs must be deterministic.** Use `TreeMap` and `TreeSet` instead of `HashMap` and `HashSet` for properties that will be serialized to JSON. This ensures that identical inputs produce identical outputs down to the last byte. 62 | - **Input and outputs should mimic the experience of Elasticsearch.** This will make it easier for Elasticsearch users to work with the zentity. Here are some examples: 63 | - zentity and Elasticsearch both display results under `hits.hits`. 64 | - zentity and Elasticsearch use the `pretty` URL parameter to format JSON. 65 | 66 | 67 | ### Code formatting conventions 68 | 69 | - Line separator: LF (`\n`) 70 | - File encoding: UTF-8 71 | - Indentation: 4 spaces 72 | - Automatically reformat code with your IDE before committing. zentity uses the default reformatting configuration of IntelliJ IDEA. 73 | - Break up large chunks of code into smaller chunks, and preface each code chunk with a brief comment that explains its purpose. 74 | 75 | 76 | ### Submitting contributions 77 | 78 | 1. Create a branch. 79 | 2. Develop your changes. 80 | 3. Rebase your changes with the master branch. 81 | 4. Test your changes. 82 | 5. Submit a [pull request](https://github.com/zentity-io/zentity/pulls). If your contribution addresses a feature or bug from the 83 | [issues list](https://github.com/zentity-io/zentity/issues), please link your pull request to the issue. 84 | 85 | 86 | ## Contacting the author 87 | 88 | zentity is maintained by [davemoore-](https://github.com/davemoore-) who can help you with anything you need regarding this project. 89 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | zentity 2 | Copyright © 2018-2025 Dave Moore 3 | https://zentity.io 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | zentity 17 | Copyright © 2018-2024 Dave Moore 18 | https://zentity.io 19 | 20 | Licensed under the Apache License, Version 2.0 (the "License"); 21 | you may not use this file except in compliance with the License. 22 | You may obtain a copy of the License at 23 | 24 | http://www.apache.org/licenses/LICENSE-2.0 25 | 26 | Unless required by applicable law or agreed to in writing, software 27 | distributed under the License is distributed on an "AS IS" BASIS, 28 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 29 | See the License for the specific language governing permissions and 30 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/zentity-io/zentity/workflows/Commit/badge.svg)](https://github.com/zentity-io/zentity/actions) 2 | 3 | # zentity 4 | 5 | zentity is an **[Elasticsearch](https://www.elastic.co/products/elasticsearch)** plugin for entity resolution. 6 | 7 | zentity aims to be: 8 | 9 | - **Simple** - Entity resolution is hard. zentity makes it easy. 10 | - **Fast** - Get results in real-time. From milliseconds to low seconds. 11 | - **Generic** - Resolve anything. People, companies, locations, sessions, and more. 12 | - **Transitive** - Resolve over multiple hops. Recursion finds dynamic identities. 13 | - **Multi-source** - Resolve over multiple indices with disparate mappings. 14 | - **Accommodating** - Operate on data as it exists. No changing or reindexing data. 15 | - **Logical** - Logic is easier to read, troubleshoot, and optimize than statistics. 16 | - **100% Elasticsearch** - Elasticsearch is a great foundation for entity resolution. 17 | 18 | 19 | ## Documentation 20 | 21 | Documentation is hosted at [https://zentity.io/docs](https://zentity.io/docs) 22 | 23 | 24 | ## Quick start 25 | 26 | Once you have installed Elasticsearch, you can install zentity from a remote URL or a local file. 27 | 28 | 1. Browse the [releases](https://zentity.io/releases). 29 | 2. Find a release that matches your version of Elasticsearch. Copy the name of the .zip file. 30 | 3. Install the plugin using the `elasticsearch-plugin` script that comes with Elasticsearch. 31 | 32 | Example: 33 | 34 | `elasticsearch-plugin install https://zentity.io/releases/zentity-1.8.3-elasticsearch-8.17.0.zip` 35 | 36 | Read the [installation](https://zentity.io/docs/installation) docs for more details. 37 | 38 | 39 | ## Next steps 40 | 41 | Read the [documentation](https://zentity.io/docs/basic-usage) to learn about [entity models](https://zentity.io/docs/entity-models), 42 | how to [manage entity models](https://zentity.io/docs/rest-apis/models-api), and how to [resolve entities](https://zentity.io/docs/rest-apis/resolution-api). 43 | 44 | 45 | ## License 46 | 47 | ``` 48 | This software is licensed under the Apache License, version 2 ("ALv2"), quoted below. 49 | 50 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 51 | use this file except in compliance with the License. You may obtain a copy of 52 | the License at 53 | 54 | http://www.apache.org/licenses/LICENSE-2.0 55 | 56 | Unless required by applicable law or agreed to in writing, software 57 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 58 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 59 | License for the specific language governing permissions and limitations under 60 | the License. 61 | ``` 62 | -------------------------------------------------------------------------------- /src/main/assemblies/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin 4 | 5 | zip 6 | 7 | false 8 | 9 | 10 | ${project.basedir}/src/main/resources/plugin-descriptor.properties 11 | / 12 | true 13 | 14 | 15 | ${project.build.directory}/generated-sources/NOTICE 16 | / 17 | true 18 | 19 | 20 | ${project.basedir}/LICENSE 21 | / 22 | true 23 | 24 | 25 | 26 | 27 | / 28 | true 29 | true 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/common/AbstractGroupedActionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | 21 | import org.elasticsearch.action.ActionListener; 22 | import org.elasticsearch.action.support.GroupedActionListener; 23 | import org.elasticsearch.common.util.concurrent.AtomicArray; 24 | import org.elasticsearch.common.util.concurrent.CountDown; 25 | 26 | import java.util.Collection; 27 | import java.util.Collections; 28 | import java.util.List; 29 | import java.util.concurrent.atomic.AtomicReference; 30 | 31 | /** 32 | * A base {@link GroupedActionListener} that is thread-safe, allows failing quickly on error, 33 | * and generally is more extensible to subclasses. It guarentees that the delegate's {@link ActionListener#onResponse} 34 | * and {@link ActionListener#onFailure} methods will be called at-most-once and exclusively. 35 | * 36 | * @param 37 | */ 38 | public abstract class AbstractGroupedActionListener implements ActionListener { 39 | private final CountDown countDown; 40 | private final AtomicArray results; 41 | private final ActionListener> delegate; 42 | private final AtomicReference failure = new AtomicReference<>(); 43 | private final boolean failFast; 44 | private boolean responseSent = false; 45 | protected final int groupSize; 46 | 47 | public AbstractGroupedActionListener(ActionListener> delegate, int groupSize) { 48 | this(delegate, groupSize, false); 49 | } 50 | 51 | public AbstractGroupedActionListener(ActionListener> delegate, int groupSize, boolean failFast) { 52 | if (groupSize <= 0) { 53 | throw new IllegalArgumentException("groupSize must be greater than 0 but was " + groupSize); 54 | } 55 | this.groupSize = groupSize; 56 | this.results = new AtomicArray<>(groupSize); 57 | this.countDown = new CountDown(groupSize); 58 | this.delegate = delegate; 59 | this.failFast = failFast; 60 | } 61 | 62 | public boolean failFast() { 63 | return failFast; 64 | } 65 | 66 | private void sendResponse() { 67 | synchronized (this) { 68 | if (responseSent) { 69 | return; 70 | } 71 | this.responseSent = true; 72 | } 73 | 74 | if (this.failure.get() != null) { 75 | this.delegate.onFailure(this.failure.get()); 76 | } else { 77 | List collect = Collections.unmodifiableList(this.results.asList()); 78 | this.delegate.onResponse(collect); 79 | } 80 | } 81 | 82 | protected void setResultAndCountDown(int index, T element) { 83 | this.results.setOnce(index, element); 84 | if (this.countDown.countDown()) { 85 | sendResponse(); 86 | } 87 | } 88 | 89 | public abstract void onResponse(T element); 90 | 91 | public void onFailure(Exception e) { 92 | if (!this.failure.compareAndSet(null, e)) { 93 | this.failure.accumulateAndGet(e, (current, update) -> { 94 | if (update != current) { 95 | current.addSuppressed(update); 96 | } 97 | 98 | return current; 99 | }); 100 | } 101 | 102 | if (this.countDown.countDown() || this.failFast) { 103 | sendResponse(); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/common/AsyncCollectionRunner.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | import org.elasticsearch.action.ActionListener; 21 | 22 | import java.util.Collection; 23 | import java.util.Deque; 24 | import java.util.concurrent.ConcurrentLinkedDeque; 25 | import java.util.function.BiConsumer; 26 | import java.util.stream.IntStream; 27 | 28 | /** 29 | * A utility class that runs items in a collection asynchronously and collects their results in order. 30 | * 31 | * @param 32 | * @param 33 | */ 34 | public class AsyncCollectionRunner { 35 | private final BiConsumer> itemRunner; 36 | private final Deque items; 37 | private final boolean failFast; 38 | private final int concurrency; 39 | private final int size; 40 | 41 | // state 42 | private IndexedGroupedActionListener groupedListener; 43 | private boolean hasStarted = false; 44 | private boolean hasFailure = false; 45 | 46 | public AsyncCollectionRunner(Collection items, BiConsumer> itemRunner) { 47 | this(items, itemRunner, 1, false); 48 | } 49 | 50 | public AsyncCollectionRunner(Collection items, BiConsumer> itemRunner, int concurrency) { 51 | this(items, itemRunner, concurrency, false); 52 | } 53 | 54 | public AsyncCollectionRunner(Collection items, BiConsumer> itemRunner, int concurrency, boolean failFast) { 55 | this.items = new ConcurrentLinkedDeque<>(items); 56 | this.itemRunner = itemRunner; 57 | this.size = items.size(); 58 | this.concurrency = concurrency; 59 | this.failFast = failFast; 60 | } 61 | 62 | private void runNextItem() { 63 | if (hasFailure && groupedListener.failFast()) { 64 | // Don't continue running if there is already a failure 65 | // and the failure has already been delegated 66 | return; 67 | } 68 | 69 | T nextItem; 70 | final int resultIndex; 71 | 72 | synchronized (items) { 73 | if (items.isEmpty()) { 74 | return; 75 | } 76 | resultIndex = size - items.size(); 77 | nextItem = items.pop(); 78 | } 79 | 80 | ActionListener resultListener = ActionListener.wrap( 81 | (result) -> groupedListener.onResponse(resultIndex, result), 82 | (ex) -> { 83 | hasFailure = true; 84 | groupedListener.onFailure(ex); 85 | } 86 | ); 87 | 88 | itemRunner.accept(nextItem, ActionListener.runAfter( 89 | resultListener, 90 | this::runNextItem)); 91 | } 92 | 93 | /** 94 | * Run the collection and listen for the results. 95 | * 96 | * @param onComplete The result listener. 97 | */ 98 | public void run(ActionListener> onComplete) { 99 | synchronized (this) { 100 | if (hasStarted) { 101 | throw new IllegalStateException("Runner has already been started. Instances cannot be reused."); 102 | } 103 | hasStarted = true; 104 | } 105 | 106 | groupedListener = new IndexedGroupedActionListener<>(onComplete, items.size(), failFast); 107 | 108 | IntStream.range(0, concurrency) 109 | .forEach((i) -> runNextItem()); 110 | } 111 | 112 | static class IndexedGroupedActionListener extends AbstractGroupedActionListener { 113 | IndexedGroupedActionListener(ActionListener> delegate, int groupSize, boolean failFast) { 114 | super(delegate, groupSize, failFast); 115 | } 116 | 117 | void onResponse(int index, ResultT element) { 118 | this.setResultAndCountDown(index, element); 119 | } 120 | 121 | @Override 122 | public void onResponse(ResultT element) { 123 | throw new IllegalStateException("Should call onResponse(int index, ResultT element) instead."); 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/common/Json.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | import com.fasterxml.jackson.core.JsonProcessingException; 21 | import com.fasterxml.jackson.core.io.JsonStringEncoder; 22 | import com.fasterxml.jackson.databind.JsonNode; 23 | import com.fasterxml.jackson.databind.ObjectMapper; 24 | import com.fasterxml.jackson.databind.SerializationFeature; 25 | 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | import java.util.TreeMap; 29 | 30 | public class Json { 31 | 32 | public static final ObjectMapper MAPPER = new ObjectMapper(); 33 | public static final ObjectMapper ORDERED_MAPPER = new ObjectMapper().configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true); 34 | private static final JsonStringEncoder STRING_ENCODER = new JsonStringEncoder(); 35 | 36 | public static String quoteString(String value) { 37 | return jsonStringFormat(value); 38 | } 39 | 40 | private static String jsonStringEscape(String value) { 41 | if (value == null) 42 | return "null"; // Prevents NullPointerException on STRING_ENCODER.quoteAsString() 43 | return new String(STRING_ENCODER.quoteAsString(value)); 44 | } 45 | 46 | private static String jsonStringQuote(String value) { 47 | return "\"" + value + "\""; 48 | } 49 | 50 | private static String jsonStringFormat(String value) { 51 | return jsonStringQuote(jsonStringEscape(value)); 52 | } 53 | 54 | 55 | /** 56 | * Converts an object {@link JsonNode JsonNode's} fields iterator to a {@link Map} of strings. 57 | * 58 | * @param iterator The object iterator. 59 | * @return The node's map representation. 60 | * @throws JsonProcessingException If the object cannot be written as a string. 61 | */ 62 | public static Map toStringMap(Iterator> iterator) throws JsonProcessingException { 63 | Map map = new TreeMap<>(); 64 | while (iterator.hasNext()) { 65 | Map.Entry paramNode = iterator.next(); 66 | String paramField = paramNode.getKey(); 67 | JsonNode paramValue = paramNode.getValue(); 68 | if (paramValue.isObject() || paramValue.isArray()) { 69 | map.put(paramField, MAPPER.writeValueAsString(paramValue)); 70 | } else if (paramValue.isNull()) { 71 | map.put(paramField, "null"); 72 | } else { 73 | map.put(paramField, paramValue.asText()); 74 | } 75 | } 76 | return map; 77 | } 78 | 79 | /** 80 | * Converts an object {@link JsonNode} to a {@link Map} of strings. 81 | * 82 | * @param node The object node. 83 | * @return The node's map representation. 84 | * @throws JsonProcessingException If the object cannot be written as a string. 85 | */ 86 | public static Map toStringMap(JsonNode node) throws JsonProcessingException { 87 | if (!node.isObject()) { 88 | throw new IllegalArgumentException("Can only convert JSON objects to maps"); 89 | } 90 | return toStringMap(node.fields()); 91 | } 92 | 93 | /** 94 | * Converts an object JSON {@link String} to a {@link Map} of strings. 95 | * 96 | * @param jsonString The object node string. 97 | * @return The node's map representation. 98 | * @throws JsonProcessingException If the object cannot be written/ parsed as a string. 99 | */ 100 | public static Map toStringMap(String jsonString) throws JsonProcessingException { 101 | return toStringMap(Json.MAPPER.readTree(jsonString)); 102 | } 103 | 104 | /** 105 | * Re-serialize a JSON string with pretty-printing. 106 | * 107 | * @param json The JSON string. 108 | * @return The pretty JSON string. 109 | * @throws JsonProcessingException If there is an issue parsing the input. 110 | */ 111 | public static String pretty(String json) throws JsonProcessingException { 112 | return Json.ORDERED_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(Json.ORDERED_MAPPER.readTree(json)); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/common/Patterns.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | import java.util.regex.Pattern; 21 | 22 | /** 23 | * Regular expression patterns to compile once when the zentity plugin loads. 24 | */ 25 | public class Patterns { 26 | 27 | public static final Pattern COLON = Pattern.compile(":"); 28 | public static final Pattern EMPTY_STRING = Pattern.compile("^\\s*$"); 29 | public static final Pattern NEWLINE = Pattern.compile("\\r?\\n"); 30 | public static final Pattern NUMBER_STRING = Pattern.compile("^-?\\d*\\.{0,1}\\d+$"); 31 | public static final Pattern PERIOD = Pattern.compile("\\."); 32 | public static final Pattern VARIABLE = Pattern.compile("\\{\\{\\s*([^\\s{}]+)\\s*}}"); 33 | public static final Pattern VARIABLE_PARAMS = Pattern.compile("^params\\.(.+)"); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/common/StreamUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | import org.elasticsearch.core.Tuple; 21 | 22 | import java.util.concurrent.atomic.AtomicLong; 23 | import java.util.concurrent.atomic.AtomicReference; 24 | import java.util.function.Function; 25 | import java.util.stream.Stream; 26 | 27 | public class StreamUtil { 28 | /** 29 | * Constructs a stateful function to be used in {@link Stream#flatMap} that buffers items into {@link Tuple Tuples} 30 | * of a single type. This is not re-usable between streams, as it is stateful. Parallel streams also might be 31 | * prone to issues with ordering. 32 | * 33 | * @param The type of each item. 34 | * @return A function for flat mapping a single stream. 35 | */ 36 | public static Function>> tupleFlatmapper() { 37 | final AtomicLong idxCounter = new AtomicLong(0); 38 | 39 | AtomicReference v1 = new AtomicReference<>(); 40 | 41 | return (T item) -> { 42 | int index = (int) (idxCounter.getAndIncrement() % 2); 43 | 44 | if (index == 0) { 45 | v1.set(item); 46 | return Stream.empty(); 47 | } 48 | 49 | return Stream.of(Tuple.tuple(v1.get(), item)); 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/model/Attribute.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import com.fasterxml.jackson.core.JsonProcessingException; 21 | import com.fasterxml.jackson.databind.JsonNode; 22 | import io.zentity.common.Json; 23 | import io.zentity.common.Patterns; 24 | 25 | import java.io.IOException; 26 | import java.util.Arrays; 27 | import java.util.Iterator; 28 | import java.util.Map; 29 | import java.util.Set; 30 | import java.util.TreeMap; 31 | import java.util.TreeSet; 32 | 33 | public class Attribute { 34 | 35 | public static final Set VALID_TYPES = new TreeSet<>( 36 | Arrays.asList("boolean", "date", "number", "string") 37 | ); 38 | 39 | private final String name; 40 | private String[] nameFields; 41 | private Map params = new TreeMap<>(); 42 | private Double score; 43 | private String type = "string"; 44 | private boolean validateRunnable = false; 45 | 46 | public Attribute(String name, JsonNode json) throws ValidationException, JsonProcessingException { 47 | validateName(name); 48 | this.name = name; 49 | this.nameFields = this.parseNameFields(name); 50 | this.deserialize(json); 51 | } 52 | 53 | public Attribute(String name, String json) throws ValidationException, IOException { 54 | validateName(name); 55 | this.name = name; 56 | this.nameFields = this.parseNameFields(name); 57 | this.deserialize(json); 58 | } 59 | 60 | public Attribute(String name, JsonNode json, boolean validateRunnable) throws ValidationException, JsonProcessingException { 61 | validateName(name); 62 | this.name = name; 63 | this.nameFields = this.parseNameFields(name); 64 | this.validateRunnable = validateRunnable; 65 | this.deserialize(json); 66 | } 67 | 68 | public Attribute(String name, String json, boolean validateRunnable) throws ValidationException, IOException { 69 | validateName(name); 70 | this.name = name; 71 | this.nameFields = this.parseNameFields(name); 72 | this.validateRunnable = validateRunnable; 73 | this.deserialize(json); 74 | } 75 | 76 | public String name() { 77 | return this.name; 78 | } 79 | 80 | public String[] nameFields() { 81 | return this.nameFields; 82 | } 83 | 84 | public Map params() { 85 | return this.params; 86 | } 87 | 88 | public Double score() { 89 | return this.score; 90 | } 91 | 92 | public String type() { 93 | return this.type; 94 | } 95 | 96 | public void score(JsonNode value) throws ValidationException { 97 | validateScore(value); 98 | this.score = value.doubleValue(); 99 | } 100 | 101 | public void type(JsonNode value) throws ValidationException { 102 | validateType(value); 103 | this.type = value.textValue(); 104 | } 105 | 106 | /** 107 | * 108 | * @param name The name of the attribute. 109 | * @return 110 | */ 111 | private String[] parseNameFields(String name) throws ValidationException { 112 | String[] nameFields = Patterns.PERIOD.split(name, -1); 113 | this.validateNameFields(nameFields); 114 | return nameFields; 115 | } 116 | 117 | private void validateName(String value) throws ValidationException { 118 | Model.validateStrictName(value); 119 | } 120 | 121 | private void validateNameFields(String[] nameFields) throws ValidationException { 122 | for (String nameField : nameFields) 123 | Model.validateStrictName(nameField); 124 | } 125 | 126 | private void validateScore(JsonNode value) throws ValidationException { 127 | String errorMessage = "'attributes." + this.name + ".score' must be a floating point number in the range of 0.0 - 1.0. Integer values of 0 or 1 are acceptable."; 128 | if (!value.isNull() && !value.isNumber()) 129 | throw new ValidationException(errorMessage); 130 | if (value.isNumber() && (value.floatValue() < 0.0 || value.floatValue() > 1.0)) 131 | throw new ValidationException(errorMessage); 132 | } 133 | 134 | /** 135 | * Validate the value of "attributes".ATTRIBUTE_NAME."type". 136 | * Must be a non-empty string containing a recognized type. 137 | * 138 | * @param value The value of "attributes".ATTRIBUTE_NAME."type". 139 | * @throws ValidationException 140 | */ 141 | private void validateType(JsonNode value) throws ValidationException { 142 | if (!value.isTextual()) 143 | throw new ValidationException("'attributes." + this.name + ".type' must be a string."); 144 | if (Patterns.EMPTY_STRING.matcher(value.textValue()).matches()) 145 | throw new ValidationException("'attributes." + this.name + ".type'' must not be empty."); 146 | if (!VALID_TYPES.contains(value.textValue())) 147 | throw new ValidationException("'attributes." + this.name + ".type' has an unrecognized type '" + value.textValue() + "'."); 148 | } 149 | 150 | /** 151 | * Validate the value of "attributes".ATTRIBUTE_NAME."params". 152 | * Must be an object. 153 | * 154 | * @param value The value of "attributes".ATTRIBUTE_NAME."params". 155 | * @throws ValidationException 156 | */ 157 | private void validateParams(JsonNode value) throws ValidationException { 158 | if (!value.isObject()) 159 | throw new ValidationException("'attributes." + this.name + ".params' must be an object."); 160 | } 161 | 162 | /** 163 | * Validate the value of "attributes".ATTRIBUTE_NAME. 164 | * Must be an object. 165 | * 166 | * @param object The value of "attributes".ATTRIBUTE_NAME. 167 | * @throws ValidationException 168 | */ 169 | private void validateObject(JsonNode object) throws ValidationException { 170 | if (!object.isObject()) 171 | throw new ValidationException("'attributes." + this.name + "' must be an object."); 172 | } 173 | 174 | /** 175 | * Deserialize, validate, and hold the state of an attribute object of an entity model. 176 | * Expected structure of the json variable: 177 | *
178 |      * {
179 |      *   "type": ATTRIBUTE_TYPE
180 |      * }
181 |      * 
182 | * 183 | * @param json Attribute object of an entity model. 184 | * @throws ValidationException 185 | * @throws JsonProcessingException 186 | */ 187 | public void deserialize(JsonNode json) throws ValidationException, JsonProcessingException { 188 | validateObject(json); 189 | 190 | // Validate and hold the state of fields. 191 | Iterator> fields = json.fields(); 192 | while (fields.hasNext()) { 193 | Map.Entry field = fields.next(); 194 | String name = field.getKey(); 195 | JsonNode value = field.getValue(); 196 | switch (name) { 197 | case "type": 198 | this.type(value); 199 | break; 200 | case "params": 201 | // Set any params that were specified in the input, with the values serialized as strings. 202 | if (!value.isObject()) 203 | throw new ValidationException("'attributes." + this.name + ".params' must be an object."); 204 | Iterator> paramsNode = value.fields(); 205 | while (paramsNode.hasNext()) { 206 | Map.Entry paramNode = paramsNode.next(); 207 | String paramField = paramNode.getKey(); 208 | JsonNode paramValue = paramNode.getValue(); 209 | if (paramValue.isObject() || paramValue.isArray()) 210 | this.params().put(paramField, Json.MAPPER.writeValueAsString(paramValue)); 211 | else if (paramValue.isNull()) 212 | this.params().put(paramField, "null"); 213 | else 214 | this.params().put(paramField, paramValue.asText()); 215 | } 216 | break; 217 | case "score": 218 | this.score(value); 219 | break; 220 | default: 221 | throw new ValidationException("'attributes." + this.name + "." + name + "' is not a recognized field."); 222 | } 223 | } 224 | } 225 | 226 | public void deserialize(String json) throws ValidationException, IOException { 227 | deserialize(Json.MAPPER.readTree(json)); 228 | } 229 | 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/model/Index.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import io.zentity.common.Patterns; 23 | 24 | import java.io.IOException; 25 | import java.util.Arrays; 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.TreeMap; 30 | import java.util.TreeSet; 31 | 32 | public class Index { 33 | 34 | public static final Set REQUIRED_FIELDS = new TreeSet<>( 35 | Arrays.asList("fields") 36 | ); 37 | 38 | private final String name; 39 | private Map fields; 40 | private Map> attributeIndexFieldsMap = new TreeMap<>(); 41 | private boolean validateRunnable = false; 42 | 43 | public Index(String name, JsonNode json) throws ValidationException { 44 | validateName(name); 45 | this.name = name; 46 | this.deserialize(json); 47 | } 48 | 49 | public Index(String name, String json) throws ValidationException, IOException { 50 | validateName(name); 51 | this.name = name; 52 | this.deserialize(json); 53 | } 54 | 55 | public Index(String name, JsonNode json, boolean validateRunnable) throws ValidationException { 56 | validateName(name); 57 | this.name = name; 58 | this.validateRunnable = validateRunnable; 59 | this.deserialize(json); 60 | } 61 | 62 | public Index(String name, String json, boolean validateRunnable) throws ValidationException, IOException { 63 | validateName(name); 64 | this.name = name; 65 | this.validateRunnable = validateRunnable; 66 | this.deserialize(json); 67 | } 68 | 69 | public String name() { 70 | return this.name; 71 | } 72 | 73 | public Map> attributeIndexFieldsMap() { 74 | return this.attributeIndexFieldsMap; 75 | } 76 | 77 | public Map fields() { 78 | return this.fields; 79 | } 80 | 81 | public void fields(JsonNode value) throws ValidationException { 82 | validateFields(value); 83 | Map fields = new TreeMap<>(); 84 | Iterator> children = value.fields(); 85 | while (children.hasNext()) { 86 | Map.Entry child = children.next(); 87 | String fieldName = child.getKey(); 88 | JsonNode fieldObject = child.getValue(); 89 | validateField(fieldName, fieldObject); 90 | fields.put(fieldName, new IndexField(this.name, fieldName, fieldObject)); 91 | } 92 | this.fields = fields; 93 | this.rebuildAttributeIndexFieldsMap(); 94 | } 95 | 96 | private void validateName(String value) throws ValidationException { 97 | if (Patterns.EMPTY_STRING.matcher(value).matches()) 98 | throw new ValidationException("'indices' has an index with an empty name."); 99 | } 100 | 101 | private void validateField(String fieldName, JsonNode fieldObject) throws ValidationException { 102 | if (fieldName.equals("")) 103 | throw new ValidationException("'indices." + this.name + ".fields' has a field with an empty name."); 104 | } 105 | 106 | private void validateFields(JsonNode value) throws ValidationException { 107 | if (!value.isObject()) 108 | throw new ValidationException("'indices." + this.name + ".fields' must be an object."); 109 | if (this.validateRunnable) { 110 | if (value.size() == 0) { 111 | // Clarifying "in the entity model" because this exception likely will appear only for resolution requests, 112 | // and the user might think that the message is referring to the input instead of the entity model. 113 | throw new ValidationException("'indices." + this.name + ".fields' must not be empty in the entity model."); 114 | } 115 | } 116 | } 117 | 118 | private void validateObject(JsonNode object) throws ValidationException { 119 | if (!object.isObject()) 120 | throw new ValidationException("'indices." + this.name + "' must be an object."); 121 | if (this.validateRunnable) { 122 | if (object.size() == 0) { 123 | // Clarifying "in the entity model" because this exception likely will appear only for resolution requests, 124 | // and the user might think that the message is referring to the input instead of the entity model. 125 | throw new ValidationException("'indices." + this.name + "' must not be empty in the entity model."); 126 | } 127 | } 128 | } 129 | 130 | /** 131 | * Create a reverse index of attribute names to index fields for faster lookup of index fields by attributes 132 | * during a resolution job. 133 | */ 134 | private void rebuildAttributeIndexFieldsMap() { 135 | this.attributeIndexFieldsMap = new TreeMap<>(); 136 | for (String indexFieldName : this.fields().keySet()) { 137 | String attributeName = this.fields().get(indexFieldName).attribute(); 138 | if (!this.attributeIndexFieldsMap.containsKey(attributeName)) 139 | this.attributeIndexFieldsMap.put(attributeName, new TreeMap<>()); 140 | if (!this.attributeIndexFieldsMap.get(attributeName).containsKey(indexFieldName)) 141 | this.attributeIndexFieldsMap.get(attributeName).put(indexFieldName, this.fields.get(indexFieldName)); 142 | } 143 | } 144 | 145 | /** 146 | * Deserialize, validate, and hold the state of an index object of an entity model. 147 | * Expected structure of the json variable: 148 | *
149 |      * {
150 |      *   "fields": {
151 |      *     INDEX_FIELD_NAME: INDEX_FIELD_OBJECT
152 |      *     ...
153 |      *   }
154 |      * }
155 |      * 
156 | * 157 | * @param json Index object of an entity model. 158 | * @throws ValidationException 159 | */ 160 | public void deserialize(JsonNode json) throws ValidationException { 161 | validateObject(json); 162 | 163 | // Validate the existence of required fields. 164 | for (String field : REQUIRED_FIELDS) 165 | if (!json.has(field)) 166 | throw new ValidationException("'indices." + this.name + "' is missing required field '" + field + "'."); 167 | 168 | // Validate and hold the state of fields. 169 | Iterator> fields = json.fields(); 170 | while (fields.hasNext()) { 171 | Map.Entry field = fields.next(); 172 | String name = field.getKey(); 173 | JsonNode value = field.getValue(); 174 | switch (name) { 175 | case "fields": 176 | this.fields(value); 177 | break; 178 | default: 179 | throw new ValidationException("'indices." + this.name + "." + name + "' is not a recognized field."); 180 | } 181 | } 182 | } 183 | 184 | public void deserialize(String json) throws ValidationException, IOException { 185 | deserialize(Json.MAPPER.readTree(json)); 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/model/IndexField.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import io.zentity.common.Patterns; 23 | 24 | import java.io.IOException; 25 | import java.util.Arrays; 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.TreeSet; 30 | 31 | public class IndexField { 32 | 33 | public static final Set REQUIRED_FIELDS = new TreeSet<>( 34 | Arrays.asList("attribute") 35 | ); 36 | 37 | private final String index; 38 | private final String name; 39 | private String[] path; 40 | private String attribute; 41 | private String matcher; 42 | private Double quality; 43 | private boolean validateRunnable = false; 44 | 45 | public IndexField(String index, String name, JsonNode json) throws ValidationException { 46 | validateName(name); 47 | this.index = index; 48 | this.name = name; 49 | this.nameToPath(name); 50 | this.deserialize(json); 51 | } 52 | 53 | public IndexField(String index, String name, String json) throws ValidationException, IOException { 54 | validateName(name); 55 | this.index = index; 56 | this.name = name; 57 | this.nameToPath(name); 58 | this.deserialize(json); 59 | } 60 | 61 | public IndexField(String index, String name, JsonNode json, boolean validateRunnable) throws ValidationException { 62 | validateName(name); 63 | this.index = index; 64 | this.name = name; 65 | this.validateRunnable = validateRunnable; 66 | this.nameToPath(name); 67 | this.deserialize(json); 68 | } 69 | 70 | public IndexField(String index, String name, String json, boolean validateRunnable) throws ValidationException, IOException { 71 | validateName(name); 72 | this.index = index; 73 | this.name = name; 74 | this.validateRunnable = validateRunnable; 75 | this.nameToPath(name); 76 | this.deserialize(json); 77 | } 78 | 79 | public String index() { 80 | return this.index; 81 | } 82 | 83 | public String name() { 84 | return this.name; 85 | } 86 | 87 | public String[] path() { 88 | return this.path; 89 | } 90 | 91 | public String attribute() { 92 | return this.attribute; 93 | } 94 | 95 | public void attribute(JsonNode value) throws ValidationException { 96 | validateAttribute(value); 97 | this.attribute = value.textValue(); 98 | } 99 | 100 | public String matcher() { 101 | return this.matcher; 102 | } 103 | 104 | public void matcher(JsonNode value) throws ValidationException { 105 | validateMatcher(value); 106 | this.matcher = value.textValue(); 107 | } 108 | 109 | private void nameToPath(String name) { 110 | this.path = Patterns.PERIOD.split(name); 111 | } 112 | 113 | public Double quality() { 114 | return this.quality; 115 | } 116 | 117 | public void quality(JsonNode value) throws ValidationException { 118 | validateQuality(value); 119 | this.quality = value.doubleValue(); 120 | } 121 | 122 | private void validateName(String value) throws ValidationException { 123 | if (Patterns.EMPTY_STRING.matcher(value).matches()) 124 | throw new ValidationException("'indices." + this.index + "' has a field with an empty name."); 125 | } 126 | 127 | private void validateAttribute(JsonNode value) throws ValidationException { 128 | if (!value.isTextual()) 129 | throw new ValidationException("'indices." + this.index + ".fields." + this.name + ".attribute' must be a string."); 130 | if (Patterns.EMPTY_STRING.matcher(value.textValue()).matches()) 131 | throw new ValidationException("'indices." + this.index + ".fields." + this.name + ".attribute' must not be empty."); 132 | } 133 | 134 | private void validateMatcher(JsonNode value) throws ValidationException { 135 | if (!value.isNull() && !value.isTextual()) 136 | throw new ValidationException("'indices." + this.index + "." + this.name + ".matcher' must be a string."); 137 | if (value.isTextual() && Patterns.EMPTY_STRING.matcher(value.textValue()).matches()) 138 | throw new ValidationException("'indices." + this.index + ".fields." + this.name + ".matcher' must not be empty."); 139 | } 140 | 141 | private void validateObject(JsonNode object) throws ValidationException { 142 | if (!object.isObject()) 143 | throw new ValidationException("'indices." + this.index + ".fields." + this.name + "' must be an object."); 144 | } 145 | 146 | private void validateQuality(JsonNode value) throws ValidationException { 147 | String errorMessage = "'indices." + this.index + ".fields." + this.name + ".quality' must be a floating point number in the range of 0.0 - 1.0. Integer values of 0 or 1 are acceptable."; 148 | if (!value.isNull() && !value.isNumber()) 149 | throw new ValidationException(errorMessage); 150 | if (value.isNumber() && (value.floatValue() < 0.0 || value.floatValue() > 1.0)) 151 | throw new ValidationException(errorMessage); 152 | } 153 | 154 | /** 155 | * Deserialize, validate, and hold the state of a field object of an index field object of an entity model. 156 | * Expected structure of the json variable: 157 | *
158 |      * {
159 |      *   "attribute": ATTRIBUTE_NAME,
160 |      *   "matcher": MATCHER_NAME
161 |      * }
162 |      * 
163 | * 164 | * @param json Index object of an entity model. 165 | * @throws ValidationException 166 | */ 167 | public void deserialize(JsonNode json) throws ValidationException { 168 | validateObject(json); 169 | 170 | // Validate the existence of required fields. 171 | for (String field : REQUIRED_FIELDS) 172 | if (!json.has(field)) 173 | throw new ValidationException("'indices." + this.index + ".fields." + this.name + "' is missing required field '" + field + "'."); 174 | 175 | // Validate and hold the state of fields. 176 | Iterator> fields = json.fields(); 177 | while (fields.hasNext()) { 178 | Map.Entry field = fields.next(); 179 | String name = field.getKey(); 180 | JsonNode value = field.getValue(); 181 | switch (name) { 182 | case "attribute": 183 | this.attribute(value); 184 | break; 185 | case "matcher": 186 | this.matcher(value); 187 | break; 188 | case "quality": 189 | this.quality(value); 190 | break; 191 | default: 192 | throw new ValidationException("'indices." + this.index + ".fields." + this.name + "." + name + "' is not a recognized field."); 193 | } 194 | } 195 | } 196 | 197 | public void deserialize(String json) throws ValidationException, IOException { 198 | deserialize(Json.MAPPER.readTree(json)); 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/model/Matcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import com.fasterxml.jackson.core.JsonProcessingException; 21 | import com.fasterxml.jackson.databind.JsonNode; 22 | import io.zentity.common.Json; 23 | import io.zentity.common.Patterns; 24 | 25 | import java.io.IOException; 26 | import java.util.Arrays; 27 | import java.util.Iterator; 28 | import java.util.Map; 29 | import java.util.Set; 30 | import java.util.TreeMap; 31 | import java.util.TreeSet; 32 | import java.util.regex.Pattern; 33 | 34 | public class Matcher { 35 | 36 | public static final Set REQUIRED_FIELDS = new TreeSet<>( 37 | Arrays.asList("clause") 38 | ); 39 | 40 | private final String name; 41 | private String clause; 42 | private Map params = new TreeMap<>(); 43 | private Double quality; 44 | private boolean validateRunnable = false; 45 | private Map variables = new TreeMap<>(); 46 | 47 | public Matcher(String name, JsonNode json) throws ValidationException, JsonProcessingException { 48 | validateName(name); 49 | this.name = name; 50 | this.deserialize(json); 51 | } 52 | 53 | public Matcher(String name, String json) throws ValidationException, IOException { 54 | validateName(name); 55 | this.name = name; 56 | this.deserialize(json); 57 | } 58 | 59 | public Matcher(String name, JsonNode json, boolean validateRunnable) throws ValidationException, JsonProcessingException { 60 | validateName(name); 61 | this.name = name; 62 | this.validateRunnable = validateRunnable; 63 | this.deserialize(json); 64 | } 65 | 66 | public Matcher(String name, String json, boolean validateRunnable) throws ValidationException, IOException { 67 | validateName(name); 68 | this.name = name; 69 | this.validateRunnable = validateRunnable; 70 | this.deserialize(json); 71 | } 72 | 73 | /** 74 | * Extract the names of the variables expressed in a clause as {{ variable }}. 75 | * These will be used when populating the matcher clause to prevent redundant regular expression replacements. 76 | * 77 | * @param clause Clause serialized as a string. 78 | * @return 79 | */ 80 | public static Map parseVariables(String clause) { 81 | java.util.regex.Matcher m = Patterns.VARIABLE.matcher(clause); 82 | Map variables = new TreeMap<>(); 83 | while (m.find()) { 84 | String variable = m.group(1); 85 | Pattern pattern = Pattern.compile("\\{\\{\\s*(" + Pattern.quote(variable) + ")\\s*}}"); 86 | variables.put(variable, pattern); 87 | } 88 | return variables; 89 | } 90 | 91 | public String name() { 92 | return this.name; 93 | } 94 | 95 | public String clause() { 96 | return this.clause; 97 | } 98 | 99 | public Map params() { 100 | return this.params; 101 | } 102 | 103 | public Double quality() { 104 | return this.quality; 105 | } 106 | 107 | public Map variables() { 108 | return this.variables; 109 | } 110 | 111 | public void clause(JsonNode value) throws ValidationException, JsonProcessingException { 112 | validateClause(value); 113 | this.clause = Json.MAPPER.writeValueAsString(value); 114 | this.variables = parseVariables(this.clause); 115 | } 116 | 117 | public void quality(JsonNode value) throws ValidationException { 118 | validateQuality(value); 119 | this.quality = value.doubleValue(); 120 | } 121 | 122 | private void validateName(String value) throws ValidationException { 123 | Model.validateStrictName(value); 124 | } 125 | 126 | private void validateClause(JsonNode value) throws ValidationException { 127 | if (!value.isObject()) 128 | throw new ValidationException("'matchers." + this.name + ".clause' must be an object."); 129 | if (value.size() == 0) 130 | throw new ValidationException("'matchers." + this.name + ".clause' must not be empty."); 131 | } 132 | 133 | private void validateObject(JsonNode object) throws ValidationException { 134 | if (!object.isObject()) 135 | throw new ValidationException("'matchers." + this.name + "' must be an object."); 136 | if (this.validateRunnable) { 137 | if (object.size() == 0) { 138 | // Clarifying "in the entity model" because this exception likely will appear only for resolution requests, 139 | // and the user might think that the message is referring to the input instead of the entity model. 140 | throw new ValidationException("'matchers." + this.name + "' must not be empty in the entity model."); 141 | } 142 | } 143 | } 144 | 145 | private void validateQuality(JsonNode value) throws ValidationException { 146 | String errorMessage = "'matchers." + this.name + ".quality' must be a floating point number in the range of 0.0 - 1.0. Integer values of 0 or 1 are acceptable."; 147 | if (!value.isNull() && !value.isNumber()) 148 | throw new ValidationException(errorMessage); 149 | if (value.isNumber() && (value.floatValue() < 0.0 || value.floatValue() > 1.0)) 150 | throw new ValidationException(errorMessage); 151 | } 152 | 153 | /** 154 | * Deserialize, validate, and hold the state of a matcher object of an entity model. 155 | * Expected structure of the json variable: 156 | *
157 |      * {
158 |      *   "clause": MATCHER_CLAUSE,
159 |      *   "params": MATCHER_PARAMS,
160 |      *   "quality": MATCHER_QUALITY
161 |      * }
162 |      * 
163 | * 164 | * @param json Matcher object of an entity model. 165 | * @throws ValidationException 166 | */ 167 | public void deserialize(JsonNode json) throws ValidationException, JsonProcessingException { 168 | validateObject(json); 169 | 170 | // Validate the existence of required fields. 171 | for (String field : REQUIRED_FIELDS) 172 | if (!json.has(field)) 173 | throw new ValidationException("'matchers." + this.name + "' is missing required field '" + field + "'."); 174 | 175 | // Validate and hold the state of fields. 176 | Iterator> fields = json.fields(); 177 | while (fields.hasNext()) { 178 | Map.Entry field = fields.next(); 179 | String name = field.getKey(); 180 | JsonNode value = field.getValue(); 181 | switch (name) { 182 | case "clause": 183 | this.clause(value); 184 | break; 185 | case "params": 186 | // Set any params that were specified in the input, with the values serialized as strings. 187 | if (value.isNull()) 188 | break; 189 | if (!value.isObject()) 190 | throw new ValidationException("'matchers." + this.name + ".params' must be an object."); 191 | Iterator> paramsNode = value.fields(); 192 | while (paramsNode.hasNext()) { 193 | Map.Entry paramNode = paramsNode.next(); 194 | String paramField = paramNode.getKey(); 195 | JsonNode paramValue = paramNode.getValue(); 196 | if (paramValue.isObject() || paramValue.isArray()) 197 | this.params().put(paramField, Json.MAPPER.writeValueAsString(paramValue)); 198 | else if (paramValue.isNull()) 199 | this.params().put(paramField, "null"); 200 | else 201 | this.params().put(paramField, paramValue.asText()); 202 | } 203 | break; 204 | case "quality": 205 | this.quality(value); 206 | break; 207 | default: 208 | throw new ValidationException("'matchers." + this.name + "." + name + "' is not a recognized field."); 209 | } 210 | } 211 | } 212 | 213 | public void deserialize(String json) throws ValidationException, IOException { 214 | deserialize(Json.MAPPER.readTree(json)); 215 | } 216 | 217 | } 218 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/model/Resolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import io.zentity.common.Patterns; 23 | 24 | import java.io.IOException; 25 | import java.util.Arrays; 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.TreeSet; 30 | 31 | public class Resolver { 32 | 33 | public static final Set REQUIRED_FIELDS = new TreeSet<>( 34 | Arrays.asList("attributes") 35 | ); 36 | 37 | private final String name; 38 | private Set attributes = new TreeSet<>(); 39 | private boolean validateRunnable = false; 40 | private int weight = 0; 41 | 42 | public Resolver(String name, JsonNode json) throws ValidationException { 43 | validateName(name); 44 | this.name = name; 45 | this.deserialize(json); 46 | } 47 | 48 | public Resolver(String name, String json) throws ValidationException, IOException { 49 | validateName(name); 50 | this.name = name; 51 | this.deserialize(json); 52 | } 53 | 54 | public Resolver(String name, JsonNode json, boolean validateRunnable) throws ValidationException { 55 | validateName(name); 56 | this.name = name; 57 | this.validateRunnable = validateRunnable; 58 | this.deserialize(json); 59 | } 60 | 61 | public Resolver(String name, String json, boolean validateRunnable) throws ValidationException, IOException { 62 | validateName(name); 63 | this.name = name; 64 | this.validateRunnable = validateRunnable; 65 | this.deserialize(json); 66 | } 67 | 68 | public String name() { 69 | return this.name; 70 | } 71 | 72 | public Set attributes() { 73 | return this.attributes; 74 | } 75 | 76 | public int weight () { return this.weight; } 77 | 78 | public void attributes(JsonNode value) throws ValidationException { 79 | validateAttributes(value); 80 | Set attributes = new TreeSet<>(); 81 | for (JsonNode attribute : value) 82 | attributes.add(attribute.textValue()); 83 | this.attributes = attributes; 84 | } 85 | 86 | public void weight(JsonNode value) throws ValidationException { 87 | validateWeight(value); 88 | this.weight = value.asInt(); 89 | } 90 | 91 | private void validateName(String value) throws ValidationException { 92 | Model.validateStrictName(value); 93 | } 94 | 95 | private void validateAttributes(JsonNode value) throws ValidationException { 96 | if (!value.isArray()) 97 | throw new ValidationException("'resolvers." + this.name + ".attributes' must be an array of strings."); 98 | if (value.size() == 0) 99 | throw new ValidationException("'resolvers." + this.name + ".attributes' must not be empty."); 100 | for (JsonNode attribute : value) { 101 | if (!attribute.isTextual()) 102 | throw new ValidationException("'resolvers." + this.name + ".attributes' must be an array of strings."); 103 | String attributeName = attribute.textValue(); 104 | if (attributeName == null || Patterns.EMPTY_STRING.matcher(attributeName).matches()) 105 | throw new ValidationException("'resolvers." + this.name + ".attributes' must be an array of non-empty strings."); 106 | } 107 | } 108 | 109 | private void validateWeight(JsonNode value) throws ValidationException { 110 | // Allow floats only if the decimal value is ###.0 111 | if (value.isNumber() && value.floatValue() % 1 != 0.0) 112 | throw new ValidationException("'resolvers." + this.name + ".weight' must be an integer."); 113 | if (!value.isNull() && !value.isNumber()) 114 | throw new ValidationException("'resolvers." + this.name + ".weight' must be an integer."); 115 | } 116 | 117 | private void validateObject(JsonNode object) throws ValidationException { 118 | if (!object.isObject()) 119 | throw new ValidationException("'resolvers." + this.name + "' must be an object."); 120 | if (this.validateRunnable) { 121 | if (object.size() == 0) { 122 | // Clarifying "in the entity model" because this exception likely will appear only for resolution requests, 123 | // and the user might think that the message is referring to the input instead of the entity model. 124 | throw new ValidationException("'resolvers." + this.name + "' must not be empty in the entity model."); 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Deserialize, validate, and hold the state of a resolver object of an entity model. 131 | * Expected structure of the json variable: 132 | *
133 |      * {
134 |      *   "attributes": [
135 |      *      ATTRIBUTE_NAME,
136 |      *      ...
137 |      *    ],
138 |      *   "weight": INTEGER
139 |      * }
140 |      * 
141 | * 142 | * @param json Resolver object of an entity model. 143 | * @throws ValidationException 144 | */ 145 | public void deserialize(JsonNode json) throws ValidationException { 146 | validateObject(json); 147 | 148 | // Validate the existence of required fields. 149 | for (String field : REQUIRED_FIELDS) 150 | if (!json.has(field)) 151 | throw new ValidationException("'resolvers." + this.name + "' is missing required field '" + field + "'."); 152 | 153 | // Validate and hold the state of fields. 154 | Iterator> fields = json.fields(); 155 | while (fields.hasNext()) { 156 | Map.Entry field = fields.next(); 157 | String name = field.getKey(); 158 | JsonNode value = field.getValue(); 159 | switch (name) { 160 | case "attributes": 161 | this.attributes(value); 162 | break; 163 | case "weight": 164 | this.weight(value); 165 | break; 166 | default: 167 | throw new ValidationException("'resolvers." + this.name + "." + name + "' is not a recognized field."); 168 | } 169 | } 170 | } 171 | 172 | public void deserialize(String json) throws ValidationException, IOException { 173 | deserialize(Json.MAPPER.readTree(json)); 174 | } 175 | 176 | } 177 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/model/ValidationException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | public class ValidationException extends Exception { 21 | public ValidationException(String message) { 22 | super(message); 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/Attribute.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input; 19 | 20 | import com.fasterxml.jackson.core.JsonProcessingException; 21 | import com.fasterxml.jackson.databind.JsonNode; 22 | import com.fasterxml.jackson.databind.util.ClassUtil; 23 | import io.zentity.common.Json; 24 | import io.zentity.common.Patterns; 25 | import io.zentity.model.ValidationException; 26 | import io.zentity.resolution.input.value.Value; 27 | 28 | import java.io.IOException; 29 | import java.util.Iterator; 30 | import java.util.Map; 31 | import java.util.Set; 32 | import java.util.TreeMap; 33 | import java.util.TreeSet; 34 | 35 | public class Attribute { 36 | 37 | private final String name; 38 | private Map params = new TreeMap<>(); 39 | private String type; 40 | private Set values = new TreeSet<>(); 41 | 42 | public Attribute(String name, String type, JsonNode json) throws ValidationException, JsonProcessingException { 43 | validateName(name); 44 | validateType(type); 45 | this.name = name; 46 | this.type = type; 47 | this.deserialize(json); 48 | } 49 | 50 | public Attribute(String name, String type, String json) throws ValidationException, IOException { 51 | validateName(name); 52 | validateType(type); 53 | this.name = name; 54 | this.type = type; 55 | this.deserialize(json); 56 | } 57 | 58 | public Attribute(String name, String type) throws ValidationException { 59 | validateName(name); 60 | validateType(type); 61 | this.name = name; 62 | this.type = type; 63 | } 64 | 65 | public String name() { 66 | return this.name; 67 | } 68 | 69 | public Map params() { 70 | return this.params; 71 | } 72 | 73 | public String type() { 74 | return this.type; 75 | } 76 | 77 | public Set values() { 78 | return this.values; 79 | } 80 | 81 | private void validateName(String value) throws ValidationException { 82 | if (Patterns.EMPTY_STRING.matcher(value).matches()) 83 | throw new ValidationException("'attributes' has an attribute with empty name."); 84 | } 85 | 86 | /** 87 | * Validate the value of "attributes".ATTRIBUTE_NAME."type". 88 | * Must be a non-empty string containing a recognized type. 89 | * 90 | * @param value The value of "attributes".ATTRIBUTE_NAME."type". 91 | * @throws ValidationException 92 | */ 93 | private void validateType(String value) throws ValidationException { 94 | if (Patterns.EMPTY_STRING.matcher(value).matches()) 95 | throw new ValidationException("'attributes." + this.name + ".type'' must not be empty."); 96 | if (!io.zentity.model.Attribute.VALID_TYPES.contains(value)) 97 | throw new ValidationException("'attributes." + this.name + ".type' has an unrecognized type '" + value + "'."); 98 | } 99 | 100 | /** 101 | * Parse a single input attribute value. The following examples are all valid attribute structures, although the 102 | * first example would be converted to the second example. 103 | *
104 |      *  {
105 |      *   ATTRIBUTE_NAME: [
106 |      *     ATTRIBUTE_VALUE,
107 |      *     ...
108 |      *   ]
109 |      * }
110 |      *
111 |      * {
112 |      *   ATTRIBUTE_NAME: {
113 |      *     "values": [
114 |      *       ATTRIBUTE_VALUE,
115 |      *       ...
116 |      *     ]
117 |      *   }
118 |      * }
119 |      *
120 |      * {
121 |      *   ATTRIBUTE_NAME: {
122 |      *     "values": [
123 |      *       ATTRIBUTE_VALUE,
124 |      *       ...
125 |      *     ],
126 |      *     "params": {
127 |      *       ATTRIBUTE_PARAM_FIELD: ATTRIBUTE_PARAM_VALUE,
128 |      *       ...
129 |      *     }
130 |      *   }
131 |      * }
132 |      *
133 |      * {
134 |      *   ATTRIBUTE_NAME: {
135 |      *     "params": {
136 |      *       ATTRIBUTE_PARAM_FIELD: ATTRIBUTE_PARAM_VALUE,
137 |      *       ...
138 |      *     }
139 |      *   }
140 |      * }
141 |      * 
142 | * 143 | * @param json Attribute object of an entity model. 144 | * @throws ValidationException 145 | */ 146 | public void deserialize(JsonNode json) throws ValidationException, JsonProcessingException { 147 | if (json.isNull()) 148 | return; 149 | if (!json.isObject() && !json.isArray()) 150 | throw new ValidationException("'attributes." + this.name + "' must be an object or array."); 151 | 152 | Iterator valuesNode = ClassUtil.emptyIterator(); 153 | Iterator> paramsNode = ClassUtil.emptyIterator(); 154 | 155 | // Parse values from array 156 | if (json.isArray()) { 157 | valuesNode = json.elements(); 158 | 159 | } else if (json.isObject()) { 160 | 161 | // Parse values from object 162 | if (json.has("values")) { 163 | if (!json.get("values").isArray()) 164 | throw new ValidationException("'attributes." + this.name + ".values' must be an array."); 165 | valuesNode = json.get("values").elements(); 166 | } 167 | 168 | // Parse params from object 169 | if (json.has("params")) { 170 | if (!json.get("params").isObject()) 171 | throw new ValidationException("'attributes." + this.name + ".params' must be an object."); 172 | paramsNode = json.get("params").fields(); 173 | } 174 | } else { 175 | throw new ValidationException("'attributes." + this.name + "' must be an object or array."); 176 | } 177 | 178 | // Set any values or params that were specified in the input. 179 | while (valuesNode.hasNext()) { 180 | JsonNode valueNode = valuesNode.next(); 181 | this.values().add(Value.create(this.type, valueNode)); 182 | } 183 | 184 | // Set any params that were specified in the input, with the values serialized as strings. 185 | while (paramsNode.hasNext()) { 186 | Map.Entry paramNode = paramsNode.next(); 187 | String paramField = paramNode.getKey(); 188 | JsonNode paramValue = paramNode.getValue(); 189 | if (paramValue.isObject() || paramValue.isArray()) 190 | this.params().put(paramField, Json.MAPPER.writeValueAsString(paramValue)); 191 | else if (paramValue.isNull()) 192 | this.params().put(paramField, "null"); 193 | else 194 | this.params().put(paramField, paramValue.asText()); 195 | } 196 | } 197 | 198 | public void deserialize(String json) throws ValidationException, IOException { 199 | deserialize(Json.MAPPER.readTree(json)); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/Term.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import io.zentity.common.Patterns; 23 | import io.zentity.model.ValidationException; 24 | import io.zentity.resolution.input.value.BooleanValue; 25 | import io.zentity.resolution.input.value.DateValue; 26 | import io.zentity.resolution.input.value.NumberValue; 27 | import io.zentity.resolution.input.value.StringValue; 28 | 29 | import java.io.IOException; 30 | import java.text.ParseException; 31 | import java.text.SimpleDateFormat; 32 | 33 | public class Term implements Comparable { 34 | 35 | private final String term; 36 | private Boolean isBoolean; 37 | private Boolean isDate; 38 | private Boolean isNumber; 39 | private BooleanValue booleanValue; 40 | private DateValue dateValue; 41 | private NumberValue numberValue; 42 | private StringValue stringValue; 43 | 44 | public Term(String term) throws ValidationException { 45 | validateTerm(term); 46 | this.term = term; 47 | } 48 | 49 | private void validateTerm(String term) throws ValidationException { 50 | if (Patterns.EMPTY_STRING.matcher(term).matches()) 51 | throw new ValidationException("A term must be a non-empty string."); 52 | } 53 | 54 | public String term() { return this.term; } 55 | 56 | public static boolean isBoolean(String term) { 57 | String termLowerCase = term.toLowerCase(); 58 | return termLowerCase.equals("true") || termLowerCase.equals("false"); 59 | } 60 | 61 | public static boolean isDate(String term, String format) { 62 | try { 63 | SimpleDateFormat formatter = new SimpleDateFormat(format); 64 | formatter.setLenient(false); 65 | formatter.parse(term); 66 | } catch (ParseException e) { 67 | return false; 68 | } 69 | return true; 70 | } 71 | 72 | public static boolean isNumber(String term) { 73 | return Patterns.NUMBER_STRING.matcher(term).matches(); 74 | } 75 | 76 | /** 77 | * Check if the term string is a boolean value. 78 | * Lazily store the decision and then return the decision. 79 | * 80 | * @return 81 | */ 82 | public boolean isBoolean() { 83 | if (this.isBoolean == null) 84 | this.isBoolean = isBoolean(this.term); 85 | return this.isBoolean; 86 | } 87 | 88 | /** 89 | * Check if the term string is a date value. 90 | * Lazily store the decision and then return the decision. 91 | * 92 | * @return 93 | */ 94 | public boolean isDate(String format) { 95 | if (this.isDate == null) 96 | this.isDate = isDate(this.term, format); 97 | return this.isDate; 98 | } 99 | 100 | /** 101 | * Convert the term to a BooleanValue. 102 | * Lazily store the value and then return it. 103 | * 104 | * @return 105 | */ 106 | public BooleanValue booleanValue() throws IOException, ValidationException { 107 | if (this.booleanValue == null) { 108 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + this.term + "}").get("value"); 109 | this.booleanValue = new BooleanValue(value); 110 | } 111 | return this.booleanValue; 112 | } 113 | 114 | /** 115 | * Check if the term string is a number value. 116 | * Lazily store the decision and then return the decision. 117 | * 118 | * @return 119 | */ 120 | public boolean isNumber() { 121 | if (this.isNumber == null) 122 | this.isNumber = isNumber(this.term); 123 | return this.isNumber; 124 | } 125 | 126 | /** 127 | * Convert the term to a DateValue. 128 | * Lazily store the value and then return it. 129 | * 130 | * @return 131 | */ 132 | public DateValue dateValue() throws IOException, ValidationException { 133 | if (this.dateValue == null) { 134 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + Json.quoteString(this.term) + "}").get("value"); 135 | this.dateValue = new DateValue(value); 136 | } 137 | return this.dateValue; 138 | } 139 | 140 | /** 141 | * Convert the term to a NumberValue. 142 | * Lazily store the value and then return it. 143 | * 144 | * @return 145 | */ 146 | public NumberValue numberValue() throws IOException, ValidationException { 147 | if (this.numberValue == null) { 148 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + this.term + "}").get("value"); 149 | this.numberValue = new NumberValue(value); 150 | } 151 | return this.numberValue; 152 | } 153 | 154 | /** 155 | * Convert the term to a StringValue. 156 | * Lazily store the value and then return it. 157 | * 158 | * @return 159 | */ 160 | public StringValue stringValue() throws IOException, ValidationException { 161 | if (this.stringValue == null) { 162 | JsonNode value = Json.MAPPER.readTree("{\"value\":" + Json.quoteString(this.term) + "}").get("value"); 163 | this.stringValue = new StringValue(value); 164 | } 165 | return this.stringValue; 166 | } 167 | 168 | @Override 169 | public int compareTo(Term o) { 170 | return this.term.compareTo(o.term); 171 | } 172 | 173 | @Override 174 | public String toString() { 175 | return this.term; 176 | } 177 | 178 | @Override 179 | public boolean equals(Object o) { return this.hashCode() == o.hashCode(); } 180 | 181 | @Override 182 | public int hashCode() { return this.term.hashCode(); } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/scope/Exclude.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.scope; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.Model; 22 | import io.zentity.model.ValidationException; 23 | 24 | import java.io.IOException; 25 | import java.util.Iterator; 26 | import java.util.Map; 27 | 28 | public class Exclude extends ScopeField { 29 | 30 | public Exclude() { 31 | super(); 32 | } 33 | 34 | @Override 35 | public void deserialize(JsonNode json, Model model) throws ValidationException, IOException { 36 | if (!json.isNull() && !json.isObject()) 37 | throw new ValidationException("The 'scope.exclude' field of the request body must be an object."); 38 | 39 | // Parse and validate the "scope.exclude" fields of the request body. 40 | Iterator> fields = json.fields(); 41 | while (fields.hasNext()) { 42 | Map.Entry field = fields.next(); 43 | String name = field.getKey(); 44 | switch (name) { 45 | case "attributes": 46 | this.attributes = parseAttributes("exclude", model, json.get("attributes")); 47 | break; 48 | case "resolvers": 49 | this.resolvers = parseResolvers("exclude", json.get("resolvers")); 50 | break; 51 | case "indices": 52 | this.indices = parseIndices("exclude", json.get("indices")); 53 | break; 54 | default: 55 | throw new ValidationException("'scope.exclude." + name + "' is not a recognized field."); 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/scope/Include.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.scope; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.Model; 22 | import io.zentity.model.ValidationException; 23 | 24 | import java.io.IOException; 25 | import java.util.Iterator; 26 | import java.util.Map; 27 | 28 | public class Include extends ScopeField { 29 | 30 | public Include() { 31 | super(); 32 | } 33 | 34 | @Override 35 | public void deserialize(JsonNode json, Model model) throws ValidationException, IOException { 36 | if (!json.isNull() && !json.isObject()) 37 | throw new ValidationException("The 'scope.include' field of the request body must be an object."); 38 | 39 | // Parse and validate the "scope.include" fields of the request body. 40 | Iterator> fields = json.fields(); 41 | while (fields.hasNext()) { 42 | Map.Entry field = fields.next(); 43 | String name = field.getKey(); 44 | switch (name) { 45 | case "attributes": 46 | this.attributes = parseAttributes("include", model, json.get("attributes")); 47 | break; 48 | case "resolvers": 49 | this.resolvers = parseResolvers("include", json.get("resolvers")); 50 | break; 51 | case "indices": 52 | this.indices = parseIndices("include", json.get("indices")); 53 | break; 54 | default: 55 | throw new ValidationException("'scope.include." + name + "' is not a recognized field."); 56 | } 57 | } 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/scope/Scope.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.scope; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import io.zentity.model.Model; 23 | import io.zentity.model.ValidationException; 24 | 25 | import java.io.IOException; 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | 29 | public class Scope { 30 | 31 | private Exclude exclude = new Exclude(); 32 | private Include include = new Include(); 33 | 34 | public Scope() { 35 | } 36 | 37 | public Exclude exclude() { 38 | return this.exclude; 39 | } 40 | 41 | public Include include() { 42 | return this.include; 43 | } 44 | 45 | public void deserialize(JsonNode json, Model model) throws ValidationException, IOException { 46 | if (!json.isNull() && !json.isObject()) 47 | throw new ValidationException("The 'scope' field of the request body must be an object."); 48 | 49 | // Parse and validate the "scope.exclude" and "scope.include" fields of the request body. 50 | Iterator> fields = json.fields(); 51 | while (fields.hasNext()) { 52 | Map.Entry field = fields.next(); 53 | String name = field.getKey(); 54 | switch (name) { 55 | case "exclude": 56 | this.exclude.deserialize(json.get("exclude"), model); 57 | break; 58 | case "include": 59 | this.include.deserialize(json.get("include"), model); 60 | break; 61 | default: 62 | throw new ValidationException("'scope." + name + "' is not a recognized field."); 63 | } 64 | } 65 | 66 | } 67 | 68 | public void deserialize(String json, Model model) throws ValidationException, IOException { 69 | deserialize(Json.MAPPER.readTree(json), model); 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/scope/ScopeField.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.scope; 19 | 20 | import com.fasterxml.jackson.core.JsonProcessingException; 21 | import com.fasterxml.jackson.databind.JsonNode; 22 | import io.zentity.common.Json; 23 | import io.zentity.model.Model; 24 | import io.zentity.model.ValidationException; 25 | import io.zentity.resolution.input.Attribute; 26 | 27 | import java.io.IOException; 28 | import java.util.Iterator; 29 | import java.util.Map; 30 | import java.util.Set; 31 | import java.util.TreeMap; 32 | import java.util.TreeSet; 33 | 34 | public abstract class ScopeField { 35 | 36 | protected Map attributes = new TreeMap<>(); 37 | protected Set indices = new TreeSet<>(); 38 | protected Set resolvers = new TreeSet<>(); 39 | 40 | public ScopeField() { 41 | } 42 | 43 | /** 44 | * Parse and validate the "scope.*.attributes" field of the request body or URL. 45 | * 46 | * @param scopeType "exclude" or "include". 47 | * @param model The entity model. 48 | * @param scopeAttributes The "attributes" object of "scope.exclude" or "scope.include". 49 | * @return Names and values of attributes to include in the entity model. 50 | * @throws ValidationException 51 | * @throws JsonProcessingException 52 | */ 53 | public static Map parseAttributes(String scopeType, Model model, JsonNode scopeAttributes) throws ValidationException, JsonProcessingException { 54 | Map attributesObj = new TreeMap<>(); 55 | if (scopeAttributes.isNull()) 56 | return attributesObj; 57 | if (!scopeAttributes.isObject()) 58 | throw new ValidationException("'scope." + scopeType + ".attributes' must be an object."); 59 | Iterator> attributeNodes = scopeAttributes.fields(); 60 | while (attributeNodes.hasNext()) { 61 | Map.Entry attribute = attributeNodes.next(); 62 | String attributeName = attribute.getKey(); 63 | 64 | // Validate that the attribute exists in the entity model. 65 | if (!model.attributes().containsKey(attributeName)) 66 | throw new ValidationException("'" + attributeName + "' is not defined in the entity model."); 67 | 68 | // Parse the attribute values. 69 | String attributeType = model.attributes().get(attributeName).type(); 70 | JsonNode valuesNode = scopeAttributes.get(attributeName); 71 | if (!valuesNode.isNull()) 72 | attributesObj.put(attributeName, new Attribute(attributeName, attributeType, valuesNode)); 73 | } 74 | return attributesObj; 75 | } 76 | 77 | /** 78 | * Parse and validate the "scope.*.indices" field of the request body or URL. 79 | * 80 | * @param scopeType "include" or "exclude". 81 | * @param scopeIndices The "indices" object of "scope.exclude" or "scope.include". 82 | * @return Names of indices to include in the entity model. 83 | * @throws ValidationException 84 | */ 85 | public static Set parseIndices(String scopeType, JsonNode scopeIndices) throws ValidationException { 86 | Set indices = new TreeSet<>(); 87 | if (scopeIndices.isNull()) 88 | return indices; 89 | if (scopeIndices.isTextual()) { 90 | if (scopeIndices.asText().equals("")) 91 | throw new ValidationException("'scope." + scopeType + ".indices' must not have non-empty strings."); 92 | String index = scopeIndices.asText(); 93 | indices.add(index); 94 | } else if (scopeIndices.isArray()) { 95 | for (JsonNode indexNode : scopeIndices) { 96 | if (!indexNode.isTextual()) 97 | throw new ValidationException("'scope." + scopeType + ".indices' must be a string or an array of strings."); 98 | String index = indexNode.asText(); 99 | if (index == null || index.equals("")) 100 | throw new ValidationException("'scope." + scopeType + ".indices' must not have non-empty strings."); 101 | indices.add(index); 102 | } 103 | } else { 104 | throw new ValidationException("'scope." + scopeType + ".indices' must be a string or an array of strings."); 105 | } 106 | return indices; 107 | } 108 | 109 | /** 110 | * Parse and validate the "scope.*.resolvers" field of the request body or URL. 111 | * 112 | * @param scopeType "include" or "exclude". 113 | * @param scopeResolvers The "resolvers" object of "scope.exclude" or "scope.include". 114 | * @return Names of resolvers to exclude from the entity model. 115 | * @throws ValidationException 116 | */ 117 | public static Set parseResolvers(String scopeType, JsonNode scopeResolvers) throws ValidationException { 118 | Set resolvers = new TreeSet<>(); 119 | if (scopeResolvers.isNull()) 120 | return resolvers; 121 | if (scopeResolvers.isTextual()) { 122 | if (scopeResolvers.asText().equals("")) 123 | throw new ValidationException("'scope." + scopeType + ".resolvers' must not have non-empty strings."); 124 | String resolver = scopeResolvers.asText(); 125 | resolvers.add(resolver); 126 | } else if (scopeResolvers.isArray()) { 127 | for (JsonNode resolverNode : scopeResolvers) { 128 | if (!resolverNode.isTextual()) 129 | throw new ValidationException("'scope." + scopeType + ".resolvers' must be a string or an array of strings."); 130 | String resolver = resolverNode.asText(); 131 | if (resolver == null || resolver.equals("")) 132 | throw new ValidationException("'scope." + scopeType + ".resolvers' must not have non-empty strings."); 133 | resolvers.add(resolver); 134 | } 135 | } else { 136 | throw new ValidationException("'scope." + scopeType + ".resolvers' must be a string or an array of strings."); 137 | } 138 | return resolvers; 139 | } 140 | 141 | public Map attributes() { 142 | return this.attributes; 143 | } 144 | 145 | public void attributes(Map attributes) { 146 | this.attributes = attributes; 147 | } 148 | 149 | public Set indices() { 150 | return this.indices; 151 | } 152 | 153 | public void indices(Set indices) { 154 | this.indices = indices; 155 | } 156 | 157 | public Set resolvers() { 158 | return this.resolvers; 159 | } 160 | 161 | public void resolvers(Set resolvers) { 162 | this.resolvers = resolvers; 163 | } 164 | 165 | public abstract void deserialize(JsonNode json, Model model) throws ValidationException, IOException; 166 | 167 | public void deserialize(String json, Model model) throws ValidationException, IOException { 168 | deserialize(Json.MAPPER.readTree(json), model); 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/value/BooleanValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.value; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.ValidationException; 22 | 23 | public class BooleanValue extends Value { 24 | 25 | public final String type = "boolean"; 26 | 27 | public BooleanValue(JsonNode value) throws ValidationException { 28 | super(value); 29 | } 30 | 31 | /** 32 | * Serialize the attribute value from a JsonNode object to a String object. 33 | * 34 | * @return 35 | */ 36 | @Override 37 | public String serialize(JsonNode value) { 38 | if (value.isNull()) 39 | return "null"; 40 | return value.asText(); 41 | } 42 | 43 | /** 44 | * Validate the value. 45 | * 46 | * @param value Attribute value. 47 | * @throws ValidationException 48 | */ 49 | @Override 50 | public void validate(JsonNode value) throws ValidationException { 51 | if (!value.isBoolean() && !value.isNull()) 52 | throw new ValidationException("Expected '" + this.type + "' attribute data type."); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/value/DateValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.value; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.ValidationException; 22 | 23 | public class DateValue extends StringValue { 24 | 25 | public final String type = "date"; 26 | 27 | public DateValue(JsonNode value) throws ValidationException { 28 | super(value); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/value/NumberValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.value; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.ValidationException; 22 | 23 | public class NumberValue extends Value { 24 | 25 | public final String type = "number"; 26 | 27 | public NumberValue(JsonNode value) throws ValidationException { 28 | super(value); 29 | } 30 | 31 | /** 32 | * Serialize the attribute value from a JsonNode object to a String object. 33 | * 34 | * @return 35 | */ 36 | @Override 37 | public String serialize(JsonNode value) { 38 | if (value.isNull()) 39 | return "null"; 40 | else if (value.isIntegralNumber()) 41 | return value.bigIntegerValue().toString(); 42 | else if (value.isFloatingPointNumber()) 43 | return String.valueOf(value.doubleValue()); 44 | else 45 | return value.numberValue().toString(); 46 | } 47 | 48 | /** 49 | * Validate the value. 50 | * 51 | * @param value Attribute value. 52 | * @throws ValidationException 53 | */ 54 | @Override 55 | public void validate(JsonNode value) throws ValidationException { 56 | if (!value.isNumber() && !value.isNull()) 57 | throw new ValidationException("Expected '" + this.type + "' attribute data type."); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/value/StringValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.value; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.ValidationException; 22 | 23 | public class StringValue extends Value { 24 | 25 | public final String type = "string"; 26 | 27 | public StringValue(JsonNode value) throws ValidationException { 28 | super(value); 29 | } 30 | 31 | /** 32 | * Serialize the attribute value from a JsonNode object to a String object. 33 | * 34 | * @return 35 | */ 36 | @Override 37 | public String serialize(JsonNode value) { 38 | if (value.isNull()) 39 | return "null"; 40 | return value.textValue(); 41 | } 42 | 43 | /** 44 | * Validate the value. 45 | * 46 | * @param value Attribute value. 47 | * @throws ValidationException 48 | */ 49 | @Override 50 | public void validate(JsonNode value) throws ValidationException { 51 | if (!value.isTextual() && !value.isNull()) 52 | throw new ValidationException("Expected '" + this.type + "' attribute data type."); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/value/Value.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.value; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.ValidationException; 22 | 23 | public abstract class Value implements ValueInterface { 24 | 25 | protected final String type = "value"; 26 | protected final JsonNode value; 27 | protected final String serialized; 28 | 29 | /** 30 | * Validate and hold the object of a value. 31 | * 32 | * @param value Attribute value. 33 | */ 34 | Value(JsonNode value) throws ValidationException { 35 | this.validate(value); 36 | this.value = value.isNull() ? null : value; 37 | this.serialized = this.serialize(value); 38 | } 39 | 40 | /** 41 | * Factory method to construct a Value. 42 | * 43 | * @param attributeType Attribute type. 44 | * @param value Attribute value. 45 | * @return 46 | * @throws ValidationException 47 | */ 48 | public static Value create(String attributeType, JsonNode value) throws ValidationException { 49 | switch (attributeType) { 50 | case "boolean": 51 | return new BooleanValue(value); 52 | case "date": 53 | return new DateValue(value); 54 | case "number": 55 | return new NumberValue(value); 56 | case "string": 57 | return new StringValue(value); 58 | default: 59 | throw new ValidationException("'" + attributeType + " is not a recognized attribute type."); 60 | } 61 | } 62 | 63 | @Override 64 | public abstract String serialize(JsonNode value); 65 | 66 | @Override 67 | public abstract void validate(JsonNode value) throws ValidationException; 68 | 69 | @Override 70 | public Object type() { 71 | return this.type; 72 | } 73 | 74 | @Override 75 | public JsonNode value() { 76 | return this.value; 77 | } 78 | 79 | @Override 80 | public String serialized() { 81 | return this.serialized; 82 | } 83 | 84 | @Override 85 | public int compareTo(Value o) { 86 | return this.serialized.compareTo(o.serialized); 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return this.serialized; 92 | } 93 | 94 | @Override 95 | public boolean equals(Object o) { return this.hashCode() == o.hashCode(); } 96 | 97 | @Override 98 | public int hashCode() { return this.serialized.hashCode(); } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/io/zentity/resolution/input/value/ValueInterface.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input.value; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.model.ValidationException; 22 | 23 | public interface ValueInterface extends Comparable { 24 | 25 | /** 26 | * Validate the attribute value. Throw an exception on validation error. Pass on success. 27 | * 28 | * @param value Attribute value. 29 | */ 30 | void validate(JsonNode value) throws ValidationException; 31 | 32 | /** 33 | * Serialize the attribute value from a JsonNode object to a String object. 34 | */ 35 | String serialize(JsonNode value); 36 | 37 | /** 38 | * Return the attribute type. 39 | * 40 | * @return 41 | */ 42 | Object type(); 43 | 44 | /** 45 | * Return the attribute value. 46 | * 47 | * @return 48 | */ 49 | Object value(); 50 | 51 | /** 52 | * Return the serialized attribute value. 53 | * 54 | * @return 55 | */ 56 | String serialized(); 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/plugin/zentity/BulkAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import io.zentity.common.Json; 21 | import io.zentity.common.Patterns; 22 | import io.zentity.common.StreamUtil; 23 | import joptsimple.internal.Strings; 24 | import org.elasticsearch.core.Tuple; 25 | 26 | import java.util.Arrays; 27 | import java.util.List; 28 | import java.util.stream.Collectors; 29 | 30 | public class BulkAction { 31 | 32 | public static final int MAX_CONCURRENT_OPERATIONS_PER_REQUEST = 100; 33 | 34 | /** 35 | * Split an NDJSON-formatted string into pairs of params and payloads. 36 | * 37 | * @param body NDJSON-formatted string. 38 | * @return 39 | */ 40 | static List> splitBulkEntries(String body) { 41 | String[] lines = Patterns.NEWLINE.split(body); 42 | if (lines.length % 2 != 0) 43 | throw new BadRequestException("Bulk request must have repeating pairs of params and payloads on separate lines."); 44 | return Arrays.stream(lines) 45 | .flatMap(StreamUtil.tupleFlatmapper()) 46 | .collect(Collectors.toList()); 47 | } 48 | 49 | /** 50 | * Serialize the response of a bulk request. 51 | * 52 | * @param result The result of a bulk request. 53 | * @return 54 | */ 55 | static String bulkResultToJson(BulkResult result) { 56 | return "{" + 57 | Json.quoteString("took") + ":" + result.took + 58 | "," + Json.quoteString("errors") + ":" + result.errors + 59 | "," + Json.quoteString("items") + ":" + "[" + Strings.join(result.items, ",") + "]" + 60 | "}"; 61 | } 62 | 63 | /** 64 | * Small wrapper around a single response for a bulk request. 65 | */ 66 | static final class SingleResult { 67 | final String response; 68 | final boolean failed; 69 | 70 | SingleResult(String response, boolean failed) { 71 | this.response = response; 72 | this.failed = failed; 73 | } 74 | } 75 | 76 | /** 77 | * A wrapper for a collection of single responses for a bulk request. 78 | */ 79 | static final class BulkResult { 80 | final List items; 81 | final boolean errors; 82 | final long took; 83 | 84 | BulkResult(List items, boolean errors, long took) { 85 | this.items = items; 86 | this.errors = errors; 87 | this.took = took; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/plugin/zentity/HomeAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import org.elasticsearch.client.internal.node.NodeClient; 21 | import org.elasticsearch.xcontent.XContentBuilder; 22 | import org.elasticsearch.xcontent.XContentFactory; 23 | import org.elasticsearch.rest.BaseRestHandler; 24 | import org.elasticsearch.rest.RestResponse; 25 | import org.elasticsearch.rest.RestRequest; 26 | import org.elasticsearch.rest.RestStatus; 27 | 28 | import java.util.List; 29 | import java.util.Properties; 30 | 31 | import static org.elasticsearch.rest.RestRequest.Method.GET; 32 | 33 | 34 | public class HomeAction extends BaseRestHandler { 35 | 36 | @Override 37 | public List routes() { 38 | return List.of( 39 | new Route(GET, "_zentity") 40 | ); 41 | } 42 | 43 | @Override 44 | public String getName() { 45 | return "zentity_plugin_action"; 46 | } 47 | 48 | @Override 49 | protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) { 50 | 51 | Properties props = ZentityPlugin.properties(); 52 | Boolean pretty = restRequest.paramAsBoolean("pretty", false); 53 | return channel -> { 54 | XContentBuilder content = XContentFactory.jsonBuilder(); 55 | if (pretty) 56 | content.prettyPrint(); 57 | content.startObject(); 58 | content.field("name", props.getProperty("name")); 59 | content.field("description", props.getProperty("description")); 60 | content.field("website", props.getProperty("zentity.website")); 61 | content.startObject("version"); 62 | content.field("zentity", props.getProperty("zentity.version")); 63 | content.field("elasticsearch", props.getProperty("elasticsearch.version")); 64 | content.endObject(); 65 | content.endObject(); 66 | channel.sendResponse(new RestResponse(RestStatus.OK, content)); 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/plugin/zentity/ParamsUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import org.elasticsearch.core.Booleans; 21 | import org.elasticsearch.core.TimeValue; 22 | import org.elasticsearch.rest.RestRequest; 23 | 24 | import java.util.Map; 25 | import java.util.Optional; 26 | import java.util.TreeMap; 27 | import java.util.function.Function; 28 | 29 | public class ParamsUtil { 30 | /** 31 | * Parse a string as a boolean, where empty values are interpreted as "true". Similar to 32 | * {@link RestRequest#paramAsBoolean}. 33 | * 34 | * @param val The raw param value. 35 | * @return The parsed bool. 36 | */ 37 | private static boolean asBoolean(String val) { 38 | // Treat empty string as true because that allows the presence of the url parameter to mean "turn this on" 39 | if (val != null && val.length() == 0) { 40 | return true; 41 | } else { 42 | return Booleans.parseBoolean(val); 43 | } 44 | } 45 | 46 | /** 47 | * Get a parameter, which may or may not be present, from multiple sets of parameters. 48 | * 49 | * @param key The parameter key. 50 | * @param params The primary set of parameters. 51 | * @param defaultParams A backup set of parameters, if the parameter is not found in the primary. 52 | * @return An optional string of the parameter's value. 53 | */ 54 | private static Optional opt(String key, Map params, Map defaultParams) { 55 | return Optional 56 | .ofNullable(params.get(key)) 57 | .or(() -> Optional.ofNullable(defaultParams.get(key))); 58 | } 59 | 60 | private static T opt(String key, T defaultValue, Function mapper, Map params, Map defaultParams) { 61 | return opt(key, params, defaultParams) 62 | .map((val) -> { 63 | try { 64 | return mapper.apply(val); 65 | } catch (Exception ex) { 66 | throw new BadRequestException("Failed to parse parameter [" + key + "] with value [" + val + "]", ex); 67 | } 68 | }) 69 | .orElse(defaultValue); 70 | } 71 | 72 | public static String optString(String key, String defaultValue, Map params, Map defaultParams) { 73 | return opt(key, params, defaultParams).orElse(defaultValue); 74 | } 75 | 76 | public static Boolean optBoolean(String key, Boolean defaultValue, Map params, Map defaultParams) { 77 | return opt(key, defaultValue, ParamsUtil::asBoolean, params, defaultParams); 78 | } 79 | 80 | public static Integer optInteger(String key, Integer defaultValue, Map params, Map defaultParams) { 81 | return opt(key, defaultValue, Integer::parseInt, params, defaultParams); 82 | } 83 | 84 | public static TimeValue optTimeValue(String key, TimeValue defaultValue, Map params, Map defaultParams) { 85 | return opt(key, defaultValue, s -> TimeValue.parseTimeValue(s, key), params, defaultParams); 86 | } 87 | 88 | /** 89 | * Read many parameters from a {@link RestRequest} into a {@link Map}. It is necessary to read all possible params 90 | * in a {@link org.elasticsearch.rest.BaseRestHandler BaseRestHandler's} prepare method to avoid throwing 91 | * a validation error. 92 | * 93 | * @param req A request. 94 | * @param params All the parameters to read from the request. 95 | * @return A map from the parameter name to the parameter value in the request. 96 | */ 97 | public static Map readAll(RestRequest req, String ...params) { 98 | Map paramsMap = new TreeMap<>(); 99 | 100 | for (String param : params) { 101 | paramsMap.put(param, req.param(param)); 102 | } 103 | 104 | return paramsMap; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/plugin/zentity/SetupAction.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import org.apache.logging.log4j.LogManager; 21 | import org.apache.logging.log4j.Logger; 22 | import org.elasticsearch.ElasticsearchSecurityException; 23 | import org.elasticsearch.action.ActionListener; 24 | import org.elasticsearch.action.admin.indices.create.CreateIndexResponse; 25 | import org.elasticsearch.client.internal.node.NodeClient; 26 | import org.elasticsearch.common.settings.Settings; 27 | import org.elasticsearch.xcontent.XContentBuilder; 28 | import org.elasticsearch.xcontent.XContentFactory; 29 | import org.elasticsearch.rest.BaseRestHandler; 30 | import org.elasticsearch.rest.RestRequest; 31 | import org.elasticsearch.rest.RestResponse; 32 | import org.elasticsearch.rest.RestStatus; 33 | 34 | import java.util.List; 35 | 36 | import static org.elasticsearch.rest.RestRequest.Method; 37 | import static org.elasticsearch.rest.RestRequest.Method.POST; 38 | 39 | public class SetupAction extends BaseRestHandler { 40 | 41 | private static final Logger logger = LogManager.getLogger(SetupAction.class); 42 | 43 | public static final int DEFAULT_NUMBER_OF_SHARDS = 1; 44 | public static final int DEFAULT_NUMBER_OF_REPLICAS = 1; 45 | public static final String INDEX_MAPPING = "{\n" + 46 | " \"dynamic\": \"strict\",\n" + 47 | " \"properties\": {\n" + 48 | " \"attributes\": {\n" + 49 | " \"type\": \"object\",\n" + 50 | " \"enabled\": false\n" + 51 | " },\n" + 52 | " \"resolvers\": {\n" + 53 | " \"type\": \"object\",\n" + 54 | " \"enabled\": false\n" + 55 | " },\n" + 56 | " \"matchers\": {\n" + 57 | " \"type\": \"object\",\n" + 58 | " \"enabled\": false\n" + 59 | " },\n" + 60 | " \"indices\": {\n" + 61 | " \"type\": \"object\",\n" + 62 | " \"enabled\": false\n" + 63 | " }\n" + 64 | " }\n" + 65 | "}"; 66 | 67 | @Override 68 | public List routes() { 69 | return List.of( 70 | new Route(POST, "_zentity/_setup") 71 | ); 72 | } 73 | 74 | /** 75 | * Create the .zentity-models index. 76 | * 77 | * @param client The client that will communicate with Elasticsearch. 78 | * @param numberOfShards The value of index.number_of_shards. 79 | * @param numberOfReplicas The value of index.number_of_replicas. 80 | * @param onComplete Action to perform after index creation request completes. 81 | */ 82 | public static void createIndex(NodeClient client, int numberOfShards, int numberOfReplicas, ActionListener onComplete) { 83 | client.admin().indices().prepareCreate(ModelsAction.INDEX_NAME) 84 | .setSettings(Settings.builder() 85 | .put("index.hidden", true) 86 | .put("index.number_of_shards", numberOfShards) 87 | .put("index.number_of_replicas", numberOfReplicas) 88 | ) 89 | .setMapping(INDEX_MAPPING) 90 | .execute(onComplete); 91 | } 92 | 93 | /** 94 | * Create the .zentity-models index using the default settings. 95 | * 96 | * @param client The client that will communicate with Elasticsearch. 97 | * @param onComplete The action to perform after the index creation request completes. 98 | */ 99 | public static void createIndex(NodeClient client, ActionListener onComplete) { 100 | createIndex(client, DEFAULT_NUMBER_OF_SHARDS, DEFAULT_NUMBER_OF_REPLICAS, onComplete); 101 | } 102 | 103 | @Override 104 | public String getName() { 105 | return "zentity_setup_action"; 106 | } 107 | 108 | @Override 109 | protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) { 110 | 111 | // Parse request 112 | Boolean pretty = restRequest.paramAsBoolean("pretty", false); 113 | int numberOfShards = restRequest.paramAsInt("number_of_shards", DEFAULT_NUMBER_OF_SHARDS); 114 | int numberOfReplicas = restRequest.paramAsInt("number_of_replicas", DEFAULT_NUMBER_OF_REPLICAS); 115 | Method method = restRequest.method(); 116 | 117 | return channel -> { 118 | try { 119 | if (method == POST) { 120 | createIndex(client, numberOfShards, numberOfReplicas, new ActionListener<>() { 121 | 122 | @Override 123 | public void onResponse(CreateIndexResponse response) { 124 | try { 125 | 126 | // The .zentity-models index was created. Send the response. 127 | XContentBuilder content = XContentFactory.jsonBuilder(); 128 | if (pretty) 129 | content.prettyPrint(); 130 | content.startObject().field("acknowledged", true).endObject(); 131 | channel.sendResponse(new RestResponse(RestStatus.OK, content)); 132 | } catch (Exception e) { 133 | 134 | // An error occurred when sending the response. 135 | ZentityPlugin.sendResponseError(channel, logger, e); 136 | } 137 | } 138 | 139 | @Override 140 | public void onFailure(Exception e) { 141 | 142 | // An error occurred when creating the .zentity-models index. 143 | if (e.getClass() == ElasticsearchSecurityException.class) { 144 | 145 | // The error was a security exception. 146 | // Log the error message as it was received from Elasticsearch. 147 | logger.debug(e.getMessage()); 148 | 149 | // Return a more descriptive error message for the user. 150 | ZentityPlugin.sendResponseError(channel, logger, new ForbiddenException("The '" + ModelsAction.INDEX_NAME + "' index cannot be created. This action requires the 'create_index' privilege for the '" + ModelsAction.INDEX_NAME + "' index. Your role does not have this privilege.")); 151 | } else { 152 | 153 | // The error was unexpected. 154 | ZentityPlugin.sendResponseError(channel, logger, e); 155 | } 156 | } 157 | }); 158 | 159 | } else { 160 | throw new NotImplementedException("Method and endpoint not implemented."); 161 | } 162 | } catch (NotImplementedException e) { 163 | channel.sendResponse(new RestResponse(channel, RestStatus.NOT_IMPLEMENTED, e)); 164 | } 165 | }; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/org/elasticsearch/plugin/zentity/ZentityPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import io.zentity.common.Json; 21 | import io.zentity.model.ValidationException; 22 | import org.apache.logging.log4j.Logger; 23 | import org.elasticsearch.ElasticsearchException; 24 | import org.elasticsearch.ElasticsearchSecurityException; 25 | import org.elasticsearch.ElasticsearchStatusException; 26 | import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver; 27 | import org.elasticsearch.cluster.node.DiscoveryNodes; 28 | import org.elasticsearch.common.Strings; 29 | import org.elasticsearch.common.io.stream.NamedWriteableRegistry; 30 | import org.elasticsearch.common.settings.ClusterSettings; 31 | import org.elasticsearch.common.settings.IndexScopedSettings; 32 | import org.elasticsearch.common.settings.Settings; 33 | import org.elasticsearch.common.settings.SettingsFilter; 34 | import org.elasticsearch.features.NodeFeature; 35 | import org.elasticsearch.rest.RestChannel; 36 | import org.elasticsearch.rest.RestController; 37 | import org.elasticsearch.rest.RestHandler; 38 | import org.elasticsearch.rest.RestResponse; 39 | import org.elasticsearch.rest.RestStatus; 40 | import org.elasticsearch.xcontent.XContentBuilder; 41 | import org.elasticsearch.plugins.ActionPlugin; 42 | import org.elasticsearch.plugins.Plugin; 43 | 44 | import java.io.IOException; 45 | import java.io.InputStream; 46 | import java.util.Arrays; 47 | import java.util.List; 48 | import java.util.Properties; 49 | import java.util.function.Predicate; 50 | import java.util.function.Supplier; 51 | 52 | class NotFoundException extends Exception { 53 | public NotFoundException(String message) { 54 | super(message); 55 | } 56 | } 57 | 58 | class NotImplementedException extends Exception { 59 | NotImplementedException(String message) { 60 | super(message); 61 | } 62 | } 63 | 64 | class ForbiddenException extends ElasticsearchSecurityException { 65 | public ForbiddenException(String message) { 66 | super(message); 67 | } 68 | } 69 | 70 | class BadRequestException extends ElasticsearchStatusException { 71 | public BadRequestException(String message) { 72 | this(message, null); 73 | } 74 | 75 | public BadRequestException(String message, Throwable cause) { 76 | super(message, RestStatus.BAD_REQUEST, cause); 77 | } 78 | } 79 | 80 | public class ZentityPlugin extends Plugin implements ActionPlugin { 81 | 82 | private static final Properties properties = new Properties(); 83 | 84 | public ZentityPlugin() throws IOException { 85 | Properties zentityProperties = new Properties(); 86 | Properties pluginDescriptorProperties = new Properties(); 87 | InputStream zentityStream = this.getClass().getResourceAsStream("/zentity.properties"); 88 | InputStream pluginDescriptorStream = this.getClass().getResourceAsStream("/plugin-descriptor.properties"); 89 | zentityProperties.load(zentityStream); 90 | pluginDescriptorProperties.load(pluginDescriptorStream); 91 | properties.putAll(zentityProperties); 92 | properties.putAll(pluginDescriptorProperties); 93 | } 94 | 95 | public static Properties properties() { 96 | return properties; 97 | } 98 | 99 | public String version() { 100 | return properties.getProperty("version"); 101 | } 102 | 103 | @Override 104 | public List getRestHandlers( 105 | Settings settings, 106 | NamedWriteableRegistry namedWriteableRegistry, 107 | RestController restController, 108 | ClusterSettings clusterSettings, 109 | IndexScopedSettings indexScopedSettings, 110 | SettingsFilter settingsFilter, 111 | IndexNameExpressionResolver indexNameExpressionResolver, 112 | Supplier nodesInCluster, 113 | Predicate clusterSupportsFeature) { 114 | return Arrays.asList( 115 | new HomeAction(), 116 | new ModelsAction(), 117 | new ResolutionAction(), 118 | new SetupAction() 119 | ); 120 | } 121 | 122 | /** 123 | * Return an error response through a RestChannel. 124 | * This method is used by the action classes in org.elasticsearch.plugin.zentity. 125 | * 126 | * @param channel The REST channel to return the response through. 127 | * @param e The exception object to process and return. 128 | */ 129 | protected static void sendResponseError(RestChannel channel, Logger logger, Exception e) { 130 | try { 131 | 132 | // Handle known types of errors. 133 | if (e instanceof ForbiddenException) { 134 | channel.sendResponse(new RestResponse(channel, RestStatus.FORBIDDEN, e)); 135 | } else if (e instanceof ValidationException) { 136 | channel.sendResponse(new RestResponse(channel, RestStatus.BAD_REQUEST, e)); 137 | } else if (e instanceof NotFoundException) { 138 | channel.sendResponse(new RestResponse(channel, RestStatus.NOT_FOUND, e)); 139 | } else if (e instanceof NotImplementedException) { 140 | channel.sendResponse(new RestResponse(channel, RestStatus.NOT_IMPLEMENTED, e)); 141 | } else if (e instanceof ElasticsearchException) { 142 | // Any other ElasticsearchException which has its own status code. 143 | channel.sendResponse(new RestResponse(channel, ((ElasticsearchException) e).status(), e)); 144 | } else { 145 | // Log the stack trace for unexpected types of errors. 146 | logger.catching(e); 147 | channel.sendResponse(new RestResponse(channel, RestStatus.INTERNAL_SERVER_ERROR, e)); 148 | } 149 | } catch (Exception ee) { 150 | 151 | // Unexpected error when processing the exception object. 152 | // Since BytesRestResponse throws IOException, build this response object as a string 153 | // to handle any IOException that find its way here. 154 | logger.catching(ee); 155 | String message = "{\"error\":{\"root_cause\":[{\"type\":\"exception\",\"reason\":" + Json.quoteString(ee.getMessage()) + "}],\"type\":\"exception\",\"reason\":" + Json.quoteString(ee.getMessage()) + "},\"status\":500}"; 156 | channel.sendResponse(new RestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); 157 | } 158 | } 159 | 160 | /** 161 | * Return a response through a RestChannel. 162 | * This method is used by the action classes in org.elasticsearch.plugin.zentity. 163 | * 164 | * @param channel The REST channel to return the response through. 165 | * @param content The content to process and return. 166 | */ 167 | protected static void sendResponse(RestChannel channel, RestStatus statusCode, XContentBuilder content) { 168 | channel.sendResponse(new RestResponse(statusCode, content)); 169 | } 170 | 171 | /** 172 | * Return a response through a RestChannel. 173 | * This method is used by the action classes in org.elasticsearch.plugin.zentity. 174 | * 175 | * @param channel The REST channel to return the response through. 176 | * @param content The content to process and return. 177 | */ 178 | protected static void sendResponse(RestChannel channel, XContentBuilder content) { 179 | sendResponse(channel, RestStatus.OK, Strings.toString(content)); 180 | } 181 | 182 | /** 183 | * Return a response through a RestChannel. 184 | * This method is used by the action classes in org.elasticsearch.plugin.zentity. 185 | * 186 | * @param channel The REST channel to return the response through. 187 | * @param json The JSON string to process and return. 188 | */ 189 | protected static void sendResponse(RestChannel channel, RestStatus statusCode, String json) { 190 | channel.sendResponse(new RestResponse(statusCode, "application/json", json)); 191 | } 192 | 193 | /** 194 | * Return a response through a RestChannel. 195 | * This method is used by the action classes in org.elasticsearch.plugin.zentity. 196 | * 197 | * @param channel The REST channel to return the response through. 198 | * @param json The JSON string to process and return. 199 | */ 200 | protected static void sendResponse(RestChannel channel, String json) { 201 | sendResponse(channel, RestStatus.OK, json); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/main/resources/license-header-notice.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ^\s*$ 5 | ^\s*$ 6 | true 7 | true 8 | false 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/license-header.txt: -------------------------------------------------------------------------------- 1 | ${project.name} 2 | Copyright © ${project.inceptionYear}-${current.year} ${owner} 3 | ${project.url} 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. -------------------------------------------------------------------------------- /src/main/resources/notice-template.ftl: -------------------------------------------------------------------------------- 1 | <#-- To render the third-party file. 2 | Available context : 3 | - dependencyMap a collection of Map.Entry with 4 | key are dependencies (as a MavenProject) (from the maven project) 5 | values are licenses of each dependency (array of string) 6 | - licenseMap a collection of Map.Entry with 7 | key are licenses of each dependency (array of string) 8 | values are all dependencies using this license 9 | --> 10 | 11 | <#function projectLicense licenses> 12 | <#if dependencyMap?size == 0> 13 | <#list licenses as license> 14 | <#return license> 15 | 16 | <#else> 17 | <#assign result = ""/> 18 | <#list licenses as license> 19 | <#if result == ""> 20 | <#assign result = license/> 21 | <#else> 22 | <#assign result = result + ", " + license/> 23 | 24 | 25 | <#return result> 26 | 27 | 28 | 29 | <#function projectName p> 30 | <#if p.name?index_of('Unnamed') > -1> 31 | <#return p.artifactId> 32 | <#else> 33 | <#return p.name> 34 | 35 | 36 | 37 | <#if dependencyMap?size == 0> 38 | The project has no dependencies. 39 | <#else> 40 | 41 | ================================================================================ 42 | 43 | zentity uses the following third-party dependencies: 44 | 45 | <#list dependencyMap as e> 46 | <#assign project = e.getKey()/> 47 | <#assign licenses = e.getValue()/> 48 | -------------------------------------------------------------------------------- 49 | 50 | Name: ${projectName(project)} 51 | Artifact: ${project.groupId}:${project.artifactId}:${project.version} 52 | URL: ${project.url!"-"} 53 | License: ${projectLicense(licenses)} 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- 58 | -------------------------------------------------------------------------------- /src/main/resources/plugin-descriptor.properties: -------------------------------------------------------------------------------- 1 | classname=${zentity.classname} 2 | description=${project.description} 3 | elasticsearch.version=${elasticsearch.version} 4 | java.version=${jdk.version} 5 | name=${project.artifactId} 6 | version=${project.version} 7 | -------------------------------------------------------------------------------- /src/main/resources/zentity.properties: -------------------------------------------------------------------------------- 1 | zentity.author=${zentity.author} 2 | zentity.website=${zentity.website} 3 | zentity.version=${zentity.version} -------------------------------------------------------------------------------- /src/test/java/io/zentity/common/AsyncCollectionRunnerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | import org.elasticsearch.action.ActionListener; 21 | import org.junit.Test; 22 | 23 | import java.util.Collection; 24 | import java.util.List; 25 | import java.util.concurrent.CompletableFuture; 26 | import java.util.concurrent.ExecutionException; 27 | import java.util.concurrent.Executor; 28 | import java.util.function.BiConsumer; 29 | import java.util.stream.Collectors; 30 | import java.util.stream.IntStream; 31 | 32 | import static org.junit.Assert.assertEquals; 33 | 34 | public class AsyncCollectionRunnerTest { 35 | static final Executor THREAD_PER_TASK_EXECUTOR = (command) -> new Thread(command).start(); 36 | 37 | void quietSleep(long millis) { 38 | try { 39 | Thread.sleep(millis); 40 | } catch (InterruptedException ignored) { 41 | } 42 | } 43 | 44 | @Test 45 | public void testRunSerial() throws InterruptedException, ExecutionException { 46 | List items = List.of(0, 1, 2, 3, 4); 47 | 48 | BiConsumer> itemRunner = (num, listener) -> THREAD_PER_TASK_EXECUTOR.execute(() -> { 49 | quietSleep(1); 50 | listener.onResponse(num); 51 | }); 52 | 53 | CompletableFuture> doneFut = new CompletableFuture<>(); 54 | 55 | AsyncCollectionRunner runner = new AsyncCollectionRunner<>( 56 | items, 57 | itemRunner); 58 | 59 | runner.run(ActionListener.wrap(doneFut::complete, doneFut::completeExceptionally)); 60 | 61 | Collection results = doneFut.get(); 62 | assertEquals(items, results); 63 | } 64 | 65 | @Test 66 | public void testRunParallel() throws InterruptedException, ExecutionException { 67 | int size = 1_000; 68 | List items = IntStream.range(0, size) 69 | .boxed() 70 | .collect(Collectors.toList()); 71 | 72 | BiConsumer> itemRunner = (num, listener) -> THREAD_PER_TASK_EXECUTOR.execute(() -> { 73 | quietSleep(1); 74 | listener.onResponse(num); 75 | }); 76 | 77 | CompletableFuture> doneFut = new CompletableFuture<>(); 78 | 79 | AsyncCollectionRunner runner = new AsyncCollectionRunner<>( 80 | items, 81 | itemRunner, 82 | 50); 83 | 84 | runner.run( 85 | ActionListener.wrap(doneFut::complete, doneFut::completeExceptionally)); 86 | 87 | Collection results = doneFut.get(); 88 | assertEquals(items, results); 89 | } 90 | 91 | @Test 92 | public void testRunHigherConcurrencyThanItems() throws InterruptedException, ExecutionException { 93 | int size = 4; 94 | List items = IntStream.range(0, size) 95 | .boxed() 96 | .collect(Collectors.toList()); 97 | 98 | BiConsumer> itemRunner = (num, listener) -> THREAD_PER_TASK_EXECUTOR.execute(() -> { 99 | quietSleep(1); 100 | listener.onResponse(num); 101 | }); 102 | 103 | CompletableFuture> doneFut = new CompletableFuture<>(); 104 | 105 | AsyncCollectionRunner runner = new AsyncCollectionRunner<>( 106 | items, 107 | itemRunner, 108 | 50); 109 | 110 | runner.run( 111 | ActionListener.wrap(doneFut::complete, doneFut::completeExceptionally)); 112 | 113 | Collection results = doneFut.get(); 114 | assertEquals(items, results); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/test/java/io/zentity/common/StreamUtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.common; 19 | 20 | import org.elasticsearch.core.Tuple; 21 | import org.junit.Test; 22 | 23 | import java.util.List; 24 | import java.util.stream.Collectors; 25 | import java.util.stream.Stream; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | 29 | public class StreamUtilTest { 30 | @Test 31 | public void testTupleFlatmapper() { 32 | Stream stream = Stream.of("0a", "0b", "1a", "1b", "2a", "2b"); 33 | 34 | List> tuples = stream 35 | .flatMap(StreamUtil.tupleFlatmapper()) 36 | .collect(Collectors.toList()); 37 | 38 | assertEquals(3, tuples.size()); 39 | 40 | for (int i = 0; i < tuples.size(); i++) { 41 | Tuple tup = tuples.get(i); 42 | assertEquals(i + "a", tup.v1()); 43 | assertEquals(i + "b", tup.v2()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/java/io/zentity/model/IndexFieldTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import org.junit.Test; 21 | 22 | public class IndexFieldTest { 23 | 24 | public final static String VALID_OBJECT = "{\"attribute\":\"foo\",\"matcher\":\"bar\",\"quality\":1.0}"; 25 | 26 | //// "indices".INDEX_NAME."fields" /////////////////////////////////////////////////////////////////////////////// 27 | 28 | @Test 29 | public void testValid() throws Exception { 30 | new IndexField("index_name", "index_field_name", VALID_OBJECT); 31 | } 32 | 33 | @Test(expected = ValidationException.class) 34 | public void testInvalidUnexpectedField() throws Exception { 35 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":\"bar\",\"quality\":1.0,\"foo\":\"bar\"}"); 36 | } 37 | 38 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME ////////////////////////////////////////////////////////////// 39 | 40 | @Test(expected = ValidationException.class) 41 | public void testInvalidNameEmpty() throws Exception { 42 | new IndexField("index_name", " ", VALID_OBJECT); 43 | } 44 | 45 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME."attribute" ////////////////////////////////////////////////// 46 | 47 | @Test(expected = ValidationException.class) 48 | public void testInvalidAttributeMissing() throws Exception { 49 | new IndexField("index_name", "index_field_name", "{\"matcher\":\"bar\"}"); 50 | } 51 | 52 | @Test(expected = ValidationException.class) 53 | public void testInvalidAttributeEmpty() throws Exception { 54 | new IndexField("index_name", "index_field_name", "{\"attribute\":\" \",\"matcher\":\"bar\"}"); 55 | } 56 | 57 | @Test(expected = ValidationException.class) 58 | public void testInvalidAttributeTypeArray() throws Exception { 59 | new IndexField("index_name", "index_field_name", "{\"attribute\":[],\"matcher\":\"bar\"}"); 60 | } 61 | 62 | @Test(expected = ValidationException.class) 63 | public void testInvalidAttributeTypeBoolean() throws Exception { 64 | new IndexField("index_name", "index_field_name", "{\"attribute\":true,\"matcher\":\"bar\"}"); 65 | } 66 | 67 | @Test(expected = ValidationException.class) 68 | public void testInvalidAttributeTypeFloat() throws Exception { 69 | new IndexField("index_name", "index_field_name", "{\"attribute\":1.0,\"matcher\":\"bar\"}"); 70 | } 71 | 72 | @Test(expected = ValidationException.class) 73 | public void testInvalidAttributeTypeInteger() throws Exception { 74 | new IndexField("index_name", "index_field_name", "{\"attribute\":1,\"matcher\":\"bar\"}"); 75 | } 76 | 77 | @Test(expected = ValidationException.class) 78 | public void testInvalidAttributeTypeNull() throws Exception { 79 | new IndexField("index_name", "index_field_name", "{\"attribute\":null,\"matcher\":\"bar\"}"); 80 | } 81 | 82 | @Test(expected = ValidationException.class) 83 | public void testInvalidAttributeTypeObject() throws Exception { 84 | new IndexField("index_name", "index_field_name", "{\"attribute\":{},\"matcher\":\"bar\"}"); 85 | } 86 | 87 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME."matcher" //////////////////////////////////////////////////// 88 | 89 | /** 90 | * Valid because matchers are optional for index fields. 91 | * See: https://zentity.io/docs/advanced-usage/payload-attributes/ 92 | */ 93 | @Test 94 | public void testValidMatcherMissing() throws Exception { 95 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\"}"); 96 | } 97 | 98 | /** 99 | * Valid because matchers are optional for index fields. 100 | * See: https://zentity.io/docs/advanced-usage/payload-attributes/ 101 | */ 102 | @Test 103 | public void testValidMatcherTypeNull() throws Exception { 104 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":null}"); 105 | } 106 | 107 | @Test(expected = ValidationException.class) 108 | public void testInvalidMatcherEmpty() throws Exception { 109 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":\" \"}"); 110 | } 111 | 112 | @Test(expected = ValidationException.class) 113 | public void testInvalidMatcherTypeArray() throws Exception { 114 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":[]}"); 115 | } 116 | 117 | @Test(expected = ValidationException.class) 118 | public void testInvalidMatcherTypeBoolean() throws Exception { 119 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":true}"); 120 | } 121 | 122 | @Test(expected = ValidationException.class) 123 | public void testInvalidMatcherTypeFloat() throws Exception { 124 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":1.0}"); 125 | } 126 | 127 | @Test(expected = ValidationException.class) 128 | public void testInvalidMatcherTypeInteger() throws Exception { 129 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":1}"); 130 | } 131 | 132 | @Test(expected = ValidationException.class) 133 | public void testInvalidMatcherTypeObject() throws Exception { 134 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"matcher\":{}}"); 135 | } 136 | 137 | //// "indices".INDEX_NAME."fields".INDEX_FIELD_NAME."quality" //////////////////////////////////////////////////// 138 | 139 | @Test 140 | public void testValidQualityValue() throws Exception { 141 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0.0}"); 142 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0.5}"); 143 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":1.0}"); 144 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0}"); 145 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":1}"); 146 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":null}"); 147 | } 148 | 149 | /** 150 | * Valid because the "quality" field is optional. 151 | */ 152 | @Test 153 | public void testValidQualityMissing() throws Exception { 154 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\"}"); 155 | } 156 | 157 | /** 158 | * Valid because the "quality" field is optional. 159 | */ 160 | @Test 161 | public void testValidQualityTypeNull() throws Exception { 162 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":null}"); 163 | } 164 | 165 | @Test 166 | public void testValidQualityTypeIntegerOne() throws Exception { 167 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":1}"); 168 | } 169 | 170 | @Test 171 | public void testValidQualityTypeIntegerZero() throws Exception { 172 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":0}"); 173 | } 174 | 175 | @Test(expected = ValidationException.class) 176 | public void testInvalidQualityTypeArray() throws Exception { 177 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":[]}"); 178 | } 179 | 180 | @Test(expected = ValidationException.class) 181 | public void testInvalidQualityTypeBoolean() throws Exception { 182 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":true}"); 183 | } 184 | 185 | @Test(expected = ValidationException.class) 186 | public void testInvalidQualityTypeInteger() throws Exception { 187 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":10}"); 188 | } 189 | 190 | @Test(expected = ValidationException.class) 191 | public void testInvalidQualityTypeFloatNegative() throws Exception { 192 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":-1.0}"); 193 | } 194 | 195 | @Test(expected = ValidationException.class) 196 | public void testInvalidQualityTypeObject() throws Exception { 197 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":{}}"); 198 | } 199 | 200 | @Test(expected = ValidationException.class) 201 | public void testInvalidQualityValueTooHigh() throws Exception { 202 | new IndexField("index_name", "index_field_name", "{\"attribute\":\"foo\",\"quality\":100.0}"); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/test/java/io/zentity/model/IndexTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import org.junit.Test; 21 | 22 | public class IndexTest { 23 | 24 | public final static String VALID_OBJECT = "{\"fields\":{\"index_field_name\":" + IndexFieldTest.VALID_OBJECT + "}}"; 25 | 26 | //// "indices" /////////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | @Test 29 | public void testValid() throws Exception { 30 | new Index("index_name", VALID_OBJECT); 31 | } 32 | 33 | @Test(expected = ValidationException.class) 34 | public void testInvalidUnexpectedField() throws Exception { 35 | new Index("index_name", "{\"fields\":{\"index_field_name\":" + IndexFieldTest.VALID_OBJECT + "},\"foo\":\"bar\"}"); 36 | } 37 | 38 | //// "indices".INDEX_NAME //////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | @Test(expected = ValidationException.class) 41 | public void testInvalidNameEmpty() throws Exception { 42 | new Index(" ", VALID_OBJECT); 43 | } 44 | 45 | //// "indices".INDEX_NAME."fields" /////////////////////////////////////////////////////////////////////////////// 46 | 47 | @Test(expected = ValidationException.class) 48 | public void testInvalidFieldsMissing() throws Exception { 49 | new Index("index_name", "{}"); 50 | } 51 | 52 | @Test 53 | public void testValidFieldsEmpty() throws Exception { 54 | new Index("index_name", "{\"fields\":{}}"); 55 | } 56 | 57 | @Test(expected = ValidationException.class) 58 | public void testInvalidFieldsEmptyRunnable() throws Exception { 59 | new Index("index_name", "{\"fields\":{}}", true); 60 | } 61 | 62 | @Test(expected = ValidationException.class) 63 | public void testInvalidFieldsTypeArray() throws Exception { 64 | new Index("index_name", "{\"fields\":[]}"); 65 | } 66 | 67 | @Test(expected = ValidationException.class) 68 | public void testInvalidFieldsTypeBoolean() throws Exception { 69 | new Index("index_name", "{\"fields\":true}"); 70 | } 71 | 72 | @Test(expected = ValidationException.class) 73 | public void testInvalidFieldsTypeFloat() throws Exception { 74 | new Index("index_name", "{\"fields\":1.0}"); 75 | } 76 | 77 | @Test(expected = ValidationException.class) 78 | public void testInvalidFieldsTypeInteger() throws Exception { 79 | new Index("index_name", "{\"fields\":1}"); 80 | } 81 | 82 | @Test(expected = ValidationException.class) 83 | public void testInvalidFieldsTypeNull() throws Exception { 84 | new Index("index_name", "{\"fields\":null}"); 85 | } 86 | 87 | @Test(expected = ValidationException.class) 88 | public void testInvalidFieldsTypeString() throws Exception { 89 | new Index("index_name", "{\"fields\":\"foobar\"}"); 90 | } 91 | } -------------------------------------------------------------------------------- /src/test/java/io/zentity/model/MatcherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.model; 19 | 20 | import org.junit.Test; 21 | 22 | import java.util.Collections; 23 | 24 | public class MatcherTest { 25 | 26 | public final static String VALID_OBJECT = "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}}}"; 27 | 28 | //// "matchers" ////////////////////////////////////////////////////////////////////////////////////////////////// 29 | 30 | @Test 31 | public void testValid() throws Exception { 32 | new Matcher("matcher_name", VALID_OBJECT); 33 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0.5}"); 34 | } 35 | 36 | @Test(expected = ValidationException.class) 37 | public void testInvalidUnexpectedField() throws Exception { 38 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"foo\":\"bar\"}"); 39 | } 40 | 41 | //// "matchers".MATCHER_NAME ///////////////////////////////////////////////////////////////////////////////////// 42 | 43 | @Test(expected = ValidationException.class) 44 | public void testInvalidNameEmpty() throws Exception { 45 | new Matcher(" ", VALID_OBJECT); 46 | } 47 | 48 | @Test(expected = ValidationException.class) 49 | public void testInvalidNameContainsAsterisk() throws Exception { 50 | new Matcher("selectivemploymentax*", VALID_OBJECT); 51 | } 52 | 53 | @Test(expected = ValidationException.class) 54 | public void testInvalidNameContainsHash() throws Exception { 55 | new Matcher("c#ke", VALID_OBJECT); 56 | } 57 | 58 | @Test(expected = ValidationException.class) 59 | public void testInvalidNameContainsColon() throws Exception { 60 | new Matcher("p:psi", VALID_OBJECT); 61 | } 62 | 63 | @Test(expected = ValidationException.class) 64 | public void testInvalidNameStartsWithUnderscore() throws Exception { 65 | new Matcher("_fanta", VALID_OBJECT); 66 | } 67 | 68 | @Test(expected = ValidationException.class) 69 | public void testInvalidNameStartsWithDash() throws Exception { 70 | new Matcher("-fanta", VALID_OBJECT); 71 | } 72 | 73 | @Test(expected = ValidationException.class) 74 | public void testInvalidNameStartsWithPlus() throws Exception { 75 | new Matcher("+fanta", VALID_OBJECT); 76 | } 77 | 78 | @Test(expected = ValidationException.class) 79 | public void testInvalidNameStartsTooLong() throws Exception { 80 | new Matcher(String.join("", Collections.nCopies(100, "sprite")), VALID_OBJECT); 81 | } 82 | 83 | @Test(expected = ValidationException.class) 84 | public void testInvalidNameIsDot() throws Exception { 85 | new Matcher(".", VALID_OBJECT); 86 | } 87 | 88 | @Test(expected = ValidationException.class) 89 | public void testInvalidNameIsDotDot() throws Exception { 90 | new Matcher("..", VALID_OBJECT); 91 | } 92 | 93 | @Test(expected = ValidationException.class) 94 | public void testInvalidNameIsNotLowercase() throws Exception { 95 | new Matcher("MELLO_yello", VALID_OBJECT); 96 | } 97 | 98 | @Test 99 | public void testValidNames() throws Exception { 100 | new Matcher("hello", VALID_OBJECT); 101 | new Matcher(".hello", VALID_OBJECT); 102 | new Matcher("..hello", VALID_OBJECT); 103 | new Matcher("hello.world", VALID_OBJECT); 104 | new Matcher("hello_world", VALID_OBJECT); 105 | new Matcher("hello-world", VALID_OBJECT); 106 | new Matcher("hello+world", VALID_OBJECT); 107 | new Matcher("您好", VALID_OBJECT); 108 | } 109 | 110 | //// "matchers".MATCHER_NAME."clause" //////////////////////////////////////////////////////////////////////////// 111 | 112 | @Test(expected = ValidationException.class) 113 | public void testInvalidClauseEmpty() throws Exception { 114 | new Matcher("matcher_name", "{\"clause\":{}}"); 115 | } 116 | 117 | @Test(expected = ValidationException.class) 118 | public void testInvalidClauseTypeArray() throws Exception { 119 | new Matcher("matcher_name", "{\"clause\":[]}"); 120 | } 121 | 122 | @Test(expected = ValidationException.class) 123 | public void testInvalidClauseTypeBoolean() throws Exception { 124 | new Matcher("matcher_name", "{\"clause\":true}"); 125 | } 126 | 127 | @Test(expected = ValidationException.class) 128 | public void testInvalidClauseTypeFloat() throws Exception { 129 | new Matcher("matcher_name", "{\"clause\":1.0}"); 130 | } 131 | 132 | @Test(expected = ValidationException.class) 133 | public void testInvalidClauseTypeInteger() throws Exception { 134 | new Matcher("matcher_name", "{\"clause\":1}"); 135 | } 136 | 137 | @Test(expected = ValidationException.class) 138 | public void testInvalidClauseTypeNull() throws Exception { 139 | new Matcher("matcher_name", "{\"clause\":null}"); 140 | } 141 | 142 | @Test(expected = ValidationException.class) 143 | public void testInvalidClauseTypeString() throws Exception { 144 | new Matcher("matcher_name", "{\"clause\":\"foobar\"}"); 145 | } 146 | 147 | //// "matchers".MATCHER_NAME."quality" /////////////////////////////////////////////////////////////////////////// 148 | 149 | @Test 150 | public void testValidQualityValue() throws Exception { 151 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0.0}"); 152 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0.5}"); 153 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":1.0}"); 154 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0}"); 155 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":1}"); 156 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":null}"); 157 | } 158 | 159 | /** 160 | * Valid because the "quality" field is optional. 161 | */ 162 | @Test 163 | public void testValidQualityMissing() throws Exception { 164 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}}}"); 165 | } 166 | 167 | /** 168 | * Valid because the "quality" field is optional. 169 | */ 170 | @Test 171 | public void testValidQualityTypeNull() throws Exception { 172 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":null}"); 173 | } 174 | 175 | @Test 176 | public void testValidQualityTypeIntegerOne() throws Exception { 177 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":1}"); 178 | } 179 | 180 | @Test 181 | public void testValidQualityTypeIntegerZero() throws Exception { 182 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":0}"); 183 | } 184 | 185 | @Test(expected = ValidationException.class) 186 | public void testInvalidQualityTypeArray() throws Exception { 187 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":[]}"); 188 | } 189 | 190 | @Test(expected = ValidationException.class) 191 | public void testInvalidQualityTypeBoolean() throws Exception { 192 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":true}"); 193 | } 194 | 195 | @Test(expected = ValidationException.class) 196 | public void testInvalidQualityTypeInteger() throws Exception { 197 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":10}"); 198 | } 199 | 200 | @Test(expected = ValidationException.class) 201 | public void testInvalidQualityTypeFloatNegative() throws Exception { 202 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":-1.0}"); 203 | } 204 | 205 | @Test(expected = ValidationException.class) 206 | public void testInvalidQualityTypeObject() throws Exception { 207 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":{}}"); 208 | } 209 | 210 | @Test(expected = ValidationException.class) 211 | public void testInvalidQualityValueTooHigh() throws Exception { 212 | new Matcher("matcher_name", "{\"clause\":{\"match\":{\"{{ field }}\":\"{{ value }}\"}},\"quality\":100.0}"); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/test/java/io/zentity/resolution/input/TermTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package io.zentity.resolution.input; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import io.zentity.model.ValidationException; 23 | import io.zentity.resolution.input.value.Value; 24 | import org.junit.Assert; 25 | import org.junit.Test; 26 | 27 | public class TermTest { 28 | 29 | //// "terms".TERM //////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | @Test(expected = ValidationException.class) 32 | public void testInvalidTypeEmptyString() throws Exception { 33 | new Term(""); 34 | } 35 | @Test(expected = ValidationException.class) 36 | public void testInvalidTypeEmptyStringWhitespace() throws Exception { 37 | new Term(" "); 38 | } 39 | 40 | //// Value type detection //////////////////////////////////////////////////////////////////////////////////////// 41 | 42 | @Test 43 | public void testValidTypeBooleanFalse() throws Exception { 44 | Term term = new Term("false"); 45 | Assert.assertTrue(term.isBoolean()); 46 | Assert.assertFalse(term.isNumber()); 47 | } 48 | 49 | @Test 50 | public void testValidTypeBooleanTrue() throws Exception { 51 | Term term = new Term("true"); 52 | Assert.assertTrue(term.isBoolean()); 53 | Assert.assertFalse(term.isNumber()); 54 | } 55 | 56 | @Test 57 | public void testValidTypeDate() throws Exception { 58 | Term term = new Term("2019-12-31 12:45:00"); 59 | Assert.assertFalse(term.isBoolean()); 60 | Assert.assertTrue(term.isDate("yyyy-MM-dd HH:mm:ss")); 61 | Assert.assertFalse(term.isNumber()); 62 | } 63 | 64 | @Test 65 | public void testInvalidTypeDate() throws Exception { 66 | Term term = new Term("2019-12-31 12:45:00"); 67 | Assert.assertFalse(term.isDate("yyyyMMdd")); 68 | } 69 | 70 | @Test 71 | public void testValidTypeNumberIntegerLongNegative() throws Exception { 72 | Term term = new Term("-922337203685477"); 73 | Assert.assertFalse(term.isBoolean()); 74 | Assert.assertTrue(term.isNumber()); 75 | } 76 | 77 | @Test 78 | public void testValidTypeNumberIntegerLongPositive() throws Exception { 79 | Term term = new Term("922337203685477"); 80 | Assert.assertFalse(term.isBoolean()); 81 | Assert.assertTrue(term.isNumber()); 82 | } 83 | 84 | @Test 85 | public void testValidTypeNumberIntegerShortNegative() throws Exception { 86 | Term term = new Term("-1"); 87 | Assert.assertFalse(term.isBoolean()); 88 | Assert.assertTrue(term.isNumber()); 89 | } 90 | 91 | @Test 92 | public void testValidTypeNumberIntegerShortPositive() throws Exception { 93 | Term term = new Term("1"); 94 | Assert.assertFalse(term.isBoolean()); 95 | Assert.assertTrue(term.isNumber()); 96 | } 97 | 98 | @Test 99 | public void testValidTypeNumberFloatLongNegative() throws Exception { 100 | Term term = new Term("-3.141592653589793"); 101 | Assert.assertFalse(term.isBoolean()); 102 | Assert.assertTrue(term.isNumber()); 103 | } 104 | 105 | @Test 106 | public void testValidTypeNumberFloatLongPositive() throws Exception { 107 | Term term = new Term("3.141592653589793"); 108 | Assert.assertFalse(term.isBoolean()); 109 | Assert.assertTrue(term.isNumber()); 110 | } 111 | 112 | @Test 113 | public void testValidTypeNumberFloatShortNegative() throws Exception { 114 | Term term = new Term("-1.0"); 115 | Assert.assertFalse(term.isBoolean()); 116 | Assert.assertTrue(term.isNumber()); 117 | } 118 | 119 | @Test 120 | public void testValidTypeNumberFloatShortPositive() throws Exception { 121 | Term term = new Term("1.0"); 122 | Assert.assertFalse(term.isBoolean()); 123 | Assert.assertTrue(term.isNumber()); 124 | } 125 | 126 | //// Value conversion //////////////////////////////////////////////////////////////////////////////////////////// 127 | 128 | @Test 129 | public void testValueConversionBooleanFalse() throws Exception { 130 | Term term = new Term("false"); 131 | JsonNode value = Json.MAPPER.readTree("{\"value\":false}").get("value"); 132 | Assert.assertEquals(term.booleanValue(), Value.create("boolean", value)); 133 | } 134 | 135 | @Test 136 | public void testValueConversionBooleanTrue() throws Exception { 137 | Term term = new Term("true"); 138 | JsonNode value = Json.MAPPER.readTree("{\"value\":true}").get("value"); 139 | Assert.assertEquals(term.booleanValue(), Value.create("boolean", value)); 140 | } 141 | 142 | @Test 143 | public void testValueConversionDate() throws Exception { 144 | Term term = new Term("2019-12-31 12:45:00"); 145 | JsonNode value = Json.MAPPER.readTree("{\"value\":\"2019-12-31 12:45:00\"}").get("value"); 146 | Assert.assertEquals(term.dateValue(), Value.create("date", value)); 147 | } 148 | 149 | @Test 150 | public void testValueConversionNumberIntegerLongNegative() throws Exception { 151 | Term term = new Term("-922337203685477"); 152 | JsonNode value = Json.MAPPER.readTree("{\"value\":-922337203685477}").get("value"); 153 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 154 | } 155 | 156 | @Test 157 | public void testValueConversionNumberIntegerLongPositive() throws Exception { 158 | Term term = new Term("922337203685477"); 159 | JsonNode value = Json.MAPPER.readTree("{\"value\":922337203685477}").get("value"); 160 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 161 | } 162 | 163 | @Test 164 | public void testValueConversionNumberIntegerShortNegative() throws Exception { 165 | Term term = new Term("-1"); 166 | JsonNode value = Json.MAPPER.readTree("{\"value\":-1}").get("value"); 167 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 168 | } 169 | 170 | @Test 171 | public void testValueConversionNumberIntegerShortPositive() throws Exception { 172 | Term term = new Term("1"); 173 | JsonNode value = Json.MAPPER.readTree("{\"value\":1}").get("value"); 174 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 175 | } 176 | 177 | @Test 178 | public void testValueConversionNumberFloatLongNegative() throws Exception { 179 | Term term = new Term("-3.141592653589793"); 180 | JsonNode value = Json.MAPPER.readTree("{\"value\":-3.141592653589793}").get("value"); 181 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 182 | } 183 | 184 | @Test 185 | public void testValueConversionNumberFloatLongPositive() throws Exception { 186 | Term term = new Term("3.141592653589793"); 187 | JsonNode value = Json.MAPPER.readTree("{\"value\":3.141592653589793}").get("value"); 188 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 189 | } 190 | 191 | @Test 192 | public void testValueConversionNumberFloatShortNegative() throws Exception { 193 | Term term = new Term("-1.0"); 194 | JsonNode value = Json.MAPPER.readTree("{\"value\":-1.0}").get("value"); 195 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 196 | } 197 | 198 | @Test 199 | public void testValueConversionNumberFloatShortPositive() throws Exception { 200 | Term term = new Term("1.0"); 201 | JsonNode value = Json.MAPPER.readTree("{\"value\":1.0}").get("value"); 202 | Assert.assertEquals(term.numberValue(), Value.create("number", value)); 203 | } 204 | 205 | @Test 206 | public void testValueConversionString() throws Exception { 207 | Term term = new Term("abc"); 208 | JsonNode value = Json.MAPPER.readTree("{\"value\":\"abc\"}").get("value"); 209 | Assert.assertEquals(term.stringValue(), Value.create("string", value)); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/test/java/org/elasticsearch/plugin/zentity/AbstractIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import org.apache.http.HttpHost; 23 | import org.elasticsearch.client.Request; 24 | import org.elasticsearch.client.Response; 25 | import org.elasticsearch.client.RestClient; 26 | import org.junit.AfterClass; 27 | import org.junit.BeforeClass; 28 | import org.junit.Ignore; 29 | import org.testcontainers.containers.DockerComposeContainer; 30 | import org.testcontainers.containers.wait.strategy.Wait; 31 | 32 | import java.io.File; 33 | import java.io.IOException; 34 | import java.nio.file.Path; 35 | import java.nio.file.Paths; 36 | import java.time.Duration; 37 | 38 | import static org.junit.Assert.*; 39 | import static org.junit.Assume.assumeFalse; 40 | 41 | @Ignore("Base class") 42 | public abstract class AbstractIT { 43 | 44 | public final static String SERVICE_NAME = "es01"; 45 | public final static int SERVICE_PORT = 9400; 46 | 47 | // A docker-compose cluster used for all test cases in the test class. 48 | private static DockerComposeContainer cluster; 49 | 50 | // Client that communicates with the docker-compose cluster. 51 | private static RestClient client; 52 | 53 | @BeforeClass 54 | public static void setup() throws Exception { 55 | createCluster(); 56 | createClient(); 57 | } 58 | 59 | @AfterClass 60 | public static void tearDown() throws IOException { 61 | destroyClient(); 62 | destroyCluster(); 63 | } 64 | 65 | /** 66 | * Create the cluster if it doesn't exist. 67 | */ 68 | public static void createCluster() { 69 | Path path = Paths.get(System.getenv("BUILD_DIRECTORY"), "test-classes", "docker-compose.yml"); 70 | cluster = new DockerComposeContainer(new File(path.toString())) 71 | .withEnv("BUILD_DIRECTORY", System.getenv("BUILD_DIRECTORY")) 72 | .withEnv("ELASTICSEARCH_VERSION", System.getenv("ELASTICSEARCH_VERSION")) 73 | .withEnv("ZENTITY_VERSION", System.getenv("ZENTITY_VERSION")) 74 | .withExposedService(SERVICE_NAME, SERVICE_PORT, 75 | Wait.forHttp("/_cat/health") 76 | .forStatusCodeMatching(it -> it >= 200 && it < 300) 77 | .withReadTimeout(Duration.ofSeconds(120))); 78 | cluster.start(); 79 | } 80 | 81 | /** 82 | * Destroy the cluster if it exists. 83 | */ 84 | public static void destroyCluster() { 85 | if (cluster != null) { 86 | cluster.stop(); 87 | cluster = null; 88 | } 89 | } 90 | 91 | /** 92 | * Create the client if it doesn't exist. 93 | */ 94 | public static void createClient() throws IOException { 95 | 96 | // The client configuration depends on the cluster implementation, 97 | // so create the cluster first if it hasn't already been created. 98 | if (cluster == null) 99 | createCluster(); 100 | 101 | try { 102 | 103 | // Create the client. 104 | String host = cluster.getServiceHost(SERVICE_NAME, SERVICE_PORT); 105 | Integer port = cluster.getServicePort(SERVICE_NAME, SERVICE_PORT); 106 | client = RestClient.builder(new HttpHost(host, port)).build(); 107 | 108 | // Verify if the client can establish a connection to the cluster. 109 | Response response = client.performRequest(new Request("GET", "/")); 110 | JsonNode json = Json.MAPPER.readTree(response.getEntity().getContent()); 111 | assertEquals("You Know, for Search", json.get("tagline").textValue()); 112 | 113 | } catch (IOException e) { 114 | 115 | // If we have an exception here, let's ignore the test. 116 | destroyClient(); 117 | assumeFalse("Integration tests are skipped", e.getMessage().contains("Connection refused")); 118 | fail("Something wrong is happening. REST Client seemed to raise an exception: " + e.getMessage()); 119 | } 120 | } 121 | 122 | /** 123 | * Destroy the client if it exists. 124 | */ 125 | public static void destroyClient() throws IOException { 126 | if (client != null) { 127 | client.close(); 128 | client = null; 129 | } 130 | } 131 | 132 | /** 133 | * Return the client to be used in test cases. 134 | * 135 | * @return The client. 136 | */ 137 | public static RestClient client() throws IOException { 138 | if (client == null) 139 | createClient(); 140 | return client; 141 | } 142 | } -------------------------------------------------------------------------------- /src/test/java/org/elasticsearch/plugin/zentity/HomeActionIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import org.elasticsearch.client.Request; 23 | import org.elasticsearch.client.Response; 24 | import org.junit.Test; 25 | 26 | import java.io.InputStream; 27 | import java.util.Properties; 28 | 29 | import static org.junit.Assert.assertEquals; 30 | 31 | public class HomeActionIT extends AbstractIT { 32 | 33 | @Test 34 | public void testHomeAction() throws Exception { 35 | 36 | // Get plugin properties 37 | Properties props = new Properties(); 38 | Properties zentityProperties = new Properties(); 39 | Properties pluginDescriptorProperties = new Properties(); 40 | InputStream zentityStream = ZentityPlugin.class.getResourceAsStream("/zentity.properties"); 41 | InputStream pluginDescriptorStream = ZentityPlugin.class.getResourceAsStream("/plugin-descriptor.properties"); 42 | zentityProperties.load(zentityStream); 43 | pluginDescriptorProperties.load(pluginDescriptorStream); 44 | props.putAll(zentityProperties); 45 | props.putAll(pluginDescriptorProperties); 46 | 47 | // Verify if the plugin properties match the output of GET _zentity 48 | Response response = client().performRequest(new Request("GET", "_zentity")); 49 | JsonNode json = Json.MAPPER.readTree(response.getEntity().getContent()); 50 | assertEquals(json.get("name").asText(), props.getProperty("name")); 51 | assertEquals(json.get("description").asText(), props.getProperty("description")); 52 | assertEquals(json.get("website").asText(), props.getProperty("zentity.website")); 53 | assertEquals(json.get("version").get("zentity").asText(), props.getProperty("zentity.version")); 54 | assertEquals(json.get("version").get("elasticsearch").asText(), props.getProperty("elasticsearch.version")); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/org/elasticsearch/plugin/zentity/SetupActionIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import org.elasticsearch.client.Request; 23 | import org.elasticsearch.client.Response; 24 | import org.elasticsearch.client.ResponseException; 25 | import org.junit.Test; 26 | 27 | import static org.junit.Assert.*; 28 | 29 | public class SetupActionIT extends AbstractIT { 30 | 31 | public static void destroyTestResources() throws Exception { 32 | try { 33 | client().performRequest(new Request("DELETE", ModelsAction.INDEX_NAME)); 34 | } catch (ResponseException e) { 35 | // Destroy the test index if it already exists, otherwise continue. 36 | } 37 | } 38 | 39 | @Test 40 | public void testSetupDefault() throws Exception { 41 | destroyTestResources(); 42 | try { 43 | 44 | // Run setup with default settings 45 | Response setupResponse = client().performRequest(new Request("POST", "_zentity/_setup")); 46 | JsonNode setupJson = Json.MAPPER.readTree(setupResponse.getEntity().getContent()); 47 | 48 | // The response should be { "acknowledged": true } 49 | JsonNode acknowledged = setupJson.get("acknowledged"); 50 | assertTrue(acknowledged.isBoolean() && acknowledged.asBoolean()); 51 | 52 | // Get the index settings and mapping 53 | Response getIndexResponse = client().performRequest(new Request("GET", ModelsAction.INDEX_NAME)); 54 | JsonNode getIndexJson = Json.MAPPER.readTree(getIndexResponse.getEntity().getContent()); 55 | 56 | // Verify if the mapping matches the default mapping 57 | JsonNode mappingJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("mappings"); 58 | assertEquals(mappingJson.get("dynamic").asText(), "strict"); 59 | assertEquals(mappingJson.get("properties").get("attributes").get("type").asText(), "object"); 60 | assertFalse(mappingJson.get("properties").get("attributes").get("enabled").booleanValue()); 61 | assertEquals(mappingJson.get("properties").get("resolvers").get("type").asText(), "object"); 62 | assertFalse(mappingJson.get("properties").get("resolvers").get("enabled").booleanValue()); 63 | assertEquals(mappingJson.get("properties").get("matchers").get("type").asText(), "object"); 64 | assertFalse(mappingJson.get("properties").get("matchers").get("enabled").booleanValue()); 65 | assertEquals(mappingJson.get("properties").get("indices").get("type").asText(), "object"); 66 | assertFalse(mappingJson.get("properties").get("indices").get("enabled").booleanValue()); 67 | 68 | // Verify if the settings match the default settings 69 | JsonNode settingsJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("settings"); 70 | assertEquals(settingsJson.get("index").get("number_of_shards").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_SHARDS)); 71 | assertEquals(settingsJson.get("index").get("number_of_replicas").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_REPLICAS)); 72 | 73 | } finally { 74 | destroyTestResources(); 75 | } 76 | } 77 | 78 | @Test 79 | public void testSetupCustom() throws Exception { 80 | destroyTestResources(); 81 | try { 82 | 83 | // Run setup with custom settings 84 | Response setupResponse = client().performRequest(new Request("POST", "_zentity/_setup?number_of_shards=2&number_of_replicas=2")); 85 | JsonNode setupJson = Json.MAPPER.readTree(setupResponse.getEntity().getContent()); 86 | 87 | // The response should be { "acknowledged": true } 88 | JsonNode acknowledged = setupJson.get("acknowledged"); 89 | assertTrue(acknowledged.isBoolean() && acknowledged.asBoolean()); 90 | 91 | // Get the index settings and mapping 92 | Response getIndexResponse = client().performRequest(new Request("GET", ModelsAction.INDEX_NAME)); 93 | JsonNode getIndexJson = Json.MAPPER.readTree(getIndexResponse.getEntity().getContent()); 94 | 95 | // Verify if the settings match the default settings 96 | JsonNode settingsJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("settings"); 97 | assertEquals(settingsJson.get("index").get("number_of_shards").asText(), "2"); 98 | assertEquals(settingsJson.get("index").get("number_of_replicas").asText(), "2"); 99 | 100 | } finally { 101 | destroyTestResources(); 102 | } 103 | } 104 | 105 | @Test 106 | public void testSetupDeconflict() throws Exception { 107 | destroyTestResources(); 108 | try { 109 | 110 | // Run setup with default settings 111 | Response setupDefaultResponse = client().performRequest(new Request("POST", "_zentity/_setup")); 112 | JsonNode setupDefaultJson = Json.MAPPER.readTree(setupDefaultResponse.getEntity().getContent()); 113 | 114 | // The response should be { "acknowledged": true } 115 | JsonNode acknowledged = setupDefaultJson.get("acknowledged"); 116 | assertTrue(acknowledged.isBoolean() && acknowledged.asBoolean()); 117 | 118 | // Run setup again with custom settings 119 | try { 120 | client().performRequest(new Request("POST", "_zentity/_setup?number_of_shards=2&number_of_replicas=2")); 121 | } catch (ResponseException e) { 122 | 123 | // The response should be an error 124 | JsonNode setupCustomJson = Json.MAPPER.readTree(e.getResponse().getEntity().getContent()); 125 | assertEquals(e.getResponse().getStatusLine().getStatusCode(), 400); 126 | assertEquals(setupCustomJson.get("error").get("type").asText(), "resource_already_exists_exception"); 127 | } 128 | 129 | // Get the index settings and mapping 130 | Response getIndexResponse = client().performRequest(new Request("GET", ModelsAction.INDEX_NAME)); 131 | JsonNode getIndexJson = Json.MAPPER.readTree(getIndexResponse.getEntity().getContent()); 132 | 133 | // Verify if the settings match the default settings and not the custom settings 134 | JsonNode settingsJson = getIndexJson.get(ModelsAction.INDEX_NAME).get("settings"); 135 | assertEquals(settingsJson.get("index").get("number_of_shards").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_SHARDS)); 136 | assertEquals(settingsJson.get("index").get("number_of_replicas").asText(), Integer.toString(SetupAction.DEFAULT_NUMBER_OF_REPLICAS)); 137 | 138 | } finally { 139 | destroyTestResources(); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /src/test/java/org/elasticsearch/plugin/zentity/ZentityPluginIT.java: -------------------------------------------------------------------------------- 1 | /* 2 | * zentity 3 | * Copyright © 2018-2025 Dave Moore 4 | * https://zentity.io 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.elasticsearch.plugin.zentity; 19 | 20 | import com.fasterxml.jackson.databind.JsonNode; 21 | import io.zentity.common.Json; 22 | import org.elasticsearch.client.Request; 23 | import org.elasticsearch.client.Response; 24 | import org.junit.Test; 25 | 26 | import java.util.Iterator; 27 | import java.util.Map; 28 | 29 | import static org.junit.Assert.assertTrue; 30 | 31 | public class ZentityPluginIT extends AbstractIT { 32 | 33 | @Test 34 | public void testPluginIsLoaded() throws Exception { 35 | Response response = client().performRequest(new Request("GET", "_nodes/plugins")); 36 | JsonNode json = Json.MAPPER.readTree(response.getEntity().getContent()); 37 | Iterator> nodes = json.get("nodes").fields(); 38 | while (nodes.hasNext()) { 39 | Map.Entry entry = nodes.next(); 40 | JsonNode node = entry.getValue(); 41 | boolean pluginFound = false; 42 | for (JsonNode plugin : node.get("plugins")) { 43 | String pluginName = plugin.get("name").textValue(); 44 | if (pluginName.equals("zentity")) { 45 | pluginFound = true; 46 | break; 47 | } 48 | } 49 | assertTrue(pluginFound); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /src/test/resources/TestDataArrays.txt: -------------------------------------------------------------------------------- 1 | { "index" : { "_index" : "zentity_test_index_arrays", "_id" : "1" }} 2 | { "string" : "abc", "array_1": [ "111" ], "array_2" : ["222",null,"222"], "array_3" : [],"array_4" : [ "222", "333", "444" ]} 3 | { "index" : { "_index" : "zentity_test_index_arrays", "_id" : "2" }} 4 | { "string" : "xyz", "array_1": [ "444" ], "array_2" : null, "array_4" : [ "555" ]} 5 | -------------------------------------------------------------------------------- /src/test/resources/TestDataObjectArrays.txt: -------------------------------------------------------------------------------- 1 | { "index" : { "_index" : "zentity_test_index_object_arrays", "_id": "1" }} 2 | { "first_name" : "alice", "last_name" : "jones", "phone" : [{ "number" : "555-123-4567", "type" : "home" }, { "number" : "555-987-6543", "type" : "mobile" }]} 3 | { "index" : { "_index" : "zentity_test_index_object_arrays", "_id": "2" }} 4 | { "first_name" : "allison", "last_name" : "jones", "phone" : [{ "number" : "555-987-6543", "type" : "mobile" }]} 5 | -------------------------------------------------------------------------------- /src/test/resources/TestEntityModelArrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "string": {}, 4 | "array": {} 5 | }, 6 | "resolvers": { 7 | "string": { 8 | "attributes": [ 9 | "string" 10 | ] 11 | }, 12 | "array": { 13 | "attributes": [ 14 | "array" 15 | ] 16 | } 17 | }, 18 | "matchers": { 19 | "exact": { 20 | "clause": { 21 | "term": { 22 | "{{ field }}": "{{ value }}" 23 | } 24 | } 25 | } 26 | }, 27 | "indices": { 28 | "zentity_test_index_arrays": { 29 | "fields": { 30 | "string": { 31 | "attribute": "string", 32 | "matcher": "exact" 33 | }, 34 | "array_1": { 35 | "attribute": "array", 36 | "matcher": "exact" 37 | }, 38 | "array_2": { 39 | "attribute": "array", 40 | "matcher": "exact" 41 | }, 42 | "array_3": { 43 | "attribute": "array", 44 | "matcher": "exact" 45 | }, 46 | "array_4": { 47 | "attribute": "array", 48 | "matcher": "exact" 49 | } 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/test/resources/TestEntityModelB.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "attribute_a": { 4 | "type": "string" 5 | }, 6 | "attribute_b": { 7 | "type": "string" 8 | }, 9 | "attribute_c": { 10 | "type": "string" 11 | }, 12 | "attribute_d": { 13 | "type": "string" 14 | }, 15 | "attribute_x": { 16 | "type": "string" 17 | }, 18 | "attribute_unused": { 19 | "type": "string" 20 | } 21 | }, 22 | "resolvers": { 23 | "resolver_ab": { 24 | "attributes": [ 25 | "attribute_a", "attribute_b" 26 | ], 27 | "weight": -1 28 | }, 29 | "resolver_ac": { 30 | "attributes": [ 31 | "attribute_a", "attribute_c" 32 | ], 33 | "weight": -1 34 | }, 35 | "resolver_bc": { 36 | "attributes": [ 37 | "attribute_b", "attribute_c" 38 | ], 39 | "weight": 1 40 | }, 41 | "resolver_cd": { 42 | "attributes": [ 43 | "attribute_c", "attribute_d" 44 | ], 45 | "weight": -1 46 | }, 47 | "resolver_x": { 48 | "attributes": [ 49 | "attribute_x" 50 | ] 51 | }, 52 | "resolver_unused": { 53 | "attributes": [ 54 | "attribute_unused" 55 | ] 56 | } 57 | }, 58 | "matchers": { 59 | "matcher_a": { 60 | "clause": { 61 | "match": { 62 | "{{ field }}": "{{ value }}" 63 | } 64 | } 65 | }, 66 | "matcher_b": { 67 | "clause": { 68 | "term": { 69 | "{{ field }}": "{{ value }}" 70 | } 71 | } 72 | } 73 | }, 74 | "indices": { 75 | "zentity_test_index_a": { 76 | "fields": { 77 | "field_a.clean": { 78 | "attribute": "attribute_a", 79 | "matcher": "matcher_a" 80 | }, 81 | "field_b.clean": { 82 | "attribute": "attribute_b", 83 | "matcher": "matcher_a" 84 | }, 85 | "field_c.clean": { 86 | "attribute": "attribute_c", 87 | "matcher": "matcher_a" 88 | }, 89 | "field_d.clean": { 90 | "attribute": "attribute_d", 91 | "matcher": "matcher_a" 92 | }, 93 | "object.a.b.c.keyword": { 94 | "attribute": "attribute_x", 95 | "matcher": "matcher_b" 96 | }, 97 | "unused": { 98 | "attribute": "attribute_unused", 99 | "matcher": "matcher_b" 100 | }, 101 | "unused_matcher": { 102 | "attribute": "attribute_unused" 103 | }, 104 | "unused_matcher_null": { 105 | "attribute": "attribute_unused", 106 | "matcher": null 107 | } 108 | } 109 | } 110 | } 111 | } -------------------------------------------------------------------------------- /src/test/resources/TestEntityModelElasticsearchError.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "attribute_a": { 4 | "type": "string" 5 | }, 6 | "attribute_b": { 7 | "type": "string" 8 | }, 9 | "attribute_c": { 10 | "type": "string" 11 | }, 12 | "attribute_d": { 13 | "type": "string" 14 | }, 15 | "attribute_x": { 16 | "type": "string" 17 | }, 18 | "attribute_unused": { 19 | "type": "string" 20 | } 21 | }, 22 | "resolvers": { 23 | "resolver_ab": { 24 | "attributes": [ 25 | "attribute_a", "attribute_b" 26 | ], 27 | "weight": -1 28 | }, 29 | "resolver_ac": { 30 | "attributes": [ 31 | "attribute_a", "attribute_c" 32 | ], 33 | "weight": -1 34 | }, 35 | "resolver_bc": { 36 | "attributes": [ 37 | "attribute_b", "attribute_c" 38 | ], 39 | "weight": 1 40 | }, 41 | "resolver_cd": { 42 | "attributes": [ 43 | "attribute_c", "attribute_d" 44 | ], 45 | "weight": -1 46 | }, 47 | "resolver_x": { 48 | "attributes": [ 49 | "attribute_x" 50 | ] 51 | }, 52 | "resolver_unused": { 53 | "attributes": [ 54 | "attribute_unused" 55 | ] 56 | } 57 | }, 58 | "matchers": { 59 | "matcher_a": { 60 | "clause": { 61 | "match": { 62 | "{{ field }}": "{{ value }}" 63 | } 64 | } 65 | }, 66 | "matcher_b": { 67 | "clause": { 68 | "example_malformed_query": { 69 | "{{ field }}": "{{ value }}" 70 | } 71 | } 72 | } 73 | }, 74 | "indices": { 75 | "zentity_test_index_a": { 76 | "fields": { 77 | "field_a.clean": { 78 | "attribute": "attribute_a", 79 | "matcher": "matcher_a" 80 | }, 81 | "field_b.clean": { 82 | "attribute": "attribute_b", 83 | "matcher": "matcher_a" 84 | }, 85 | "field_c.clean": { 86 | "attribute": "attribute_c", 87 | "matcher": "matcher_a" 88 | }, 89 | "field_d.clean": { 90 | "attribute": "attribute_d", 91 | "matcher": "matcher_a" 92 | }, 93 | "object.a.b.c.keyword": { 94 | "attribute": "attribute_x", 95 | "matcher": "matcher_b" 96 | }, 97 | "unused": { 98 | "attribute": "attribute_unused", 99 | "matcher": "matcher_b" 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/test/resources/TestEntityModelObjectArrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "first_name": {}, 4 | "last_name": {}, 5 | "phone": {} 6 | }, 7 | "resolvers": { 8 | "name_phone": { 9 | "attributes": [ 10 | "last_name", 11 | "phone" 12 | ] 13 | } 14 | }, 15 | "matchers": { 16 | "exact": { 17 | "clause": { 18 | "term": { 19 | "{{ field }}": "{{ value }}" 20 | } 21 | } 22 | }, 23 | "exact_phone": { 24 | "clause": { 25 | "nested": { 26 | "path": "phone", 27 | "query": { 28 | "term": { 29 | "{{ field }}": "{{ value }}" 30 | } 31 | } 32 | } 33 | } 34 | } 35 | }, 36 | "indices": { 37 | "zentity_test_index_object_arrays": { 38 | "fields": { 39 | "first_name": { 40 | "attribute": "first_name", 41 | "matcher": "exact" 42 | }, 43 | "last_name": { 44 | "attribute": "last_name", 45 | "matcher": "exact" 46 | }, 47 | "phone.number": { 48 | "attribute": "phone", 49 | "matcher": "exact_phone" 50 | } 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/test/resources/TestEntityModelZentityError.json: -------------------------------------------------------------------------------- 1 | { 2 | "attributes": { 3 | "attribute_a": { 4 | "type": "string" 5 | }, 6 | "attribute_b": { 7 | "type": "string" 8 | }, 9 | "attribute_c": { 10 | "type": "string" 11 | }, 12 | "attribute_d": { 13 | "type": "string" 14 | }, 15 | "attribute_x": { 16 | "type": "number" 17 | }, 18 | "attribute_unused": { 19 | "type": "string" 20 | } 21 | }, 22 | "resolvers": { 23 | "resolver_ab": { 24 | "attributes": [ 25 | "attribute_a", "attribute_b" 26 | ], 27 | "weight": -1 28 | }, 29 | "resolver_ac": { 30 | "attributes": [ 31 | "attribute_a", "attribute_c" 32 | ], 33 | "weight": -1 34 | }, 35 | "resolver_bc": { 36 | "attributes": [ 37 | "attribute_b", "attribute_c" 38 | ], 39 | "weight": 1 40 | }, 41 | "resolver_cd": { 42 | "attributes": [ 43 | "attribute_c", "attribute_d" 44 | ], 45 | "weight": -1 46 | }, 47 | "resolver_x": { 48 | "attributes": [ 49 | "attribute_x" 50 | ] 51 | }, 52 | "resolver_unused": { 53 | "attributes": [ 54 | "attribute_unused" 55 | ] 56 | } 57 | }, 58 | "matchers": { 59 | "matcher_a": { 60 | "clause": { 61 | "match": { 62 | "{{ field }}": "{{ value }}" 63 | } 64 | } 65 | }, 66 | "matcher_b": { 67 | "clause": { 68 | "term": { 69 | "{{ field }}": "{{ value }}" 70 | } 71 | } 72 | } 73 | }, 74 | "indices": { 75 | "zentity_test_index_a": { 76 | "fields": { 77 | "field_a.clean": { 78 | "attribute": "attribute_a", 79 | "matcher": "matcher_a" 80 | }, 81 | "field_b.clean": { 82 | "attribute": "attribute_b", 83 | "matcher": "matcher_a" 84 | }, 85 | "field_c.clean": { 86 | "attribute": "attribute_c", 87 | "matcher": "matcher_a" 88 | }, 89 | "field_d.clean": { 90 | "attribute": "attribute_d", 91 | "matcher": "matcher_a" 92 | }, 93 | "object.a.b.c.keyword": { 94 | "attribute": "attribute_x", 95 | "matcher": "matcher_b" 96 | }, 97 | "unused": { 98 | "attribute": "attribute_unused", 99 | "matcher": "matcher_b" 100 | } 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /src/test/resources/TestIndex.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "number_of_shards": 1, 4 | "number_of_replicas": 2, 5 | "analysis": { 6 | "filter": { 7 | "strip_punct": { 8 | "type": "pattern_replace", 9 | "pattern": "[^a-zA-Z0-9]", 10 | "replacement": "" 11 | } 12 | }, 13 | "analyzer": { 14 | "strip_punct": { 15 | "filter": [ 16 | "strip_punct" 17 | ], 18 | "tokenizer": "keyword" 19 | } 20 | } 21 | } 22 | }, 23 | "mappings": { 24 | "properties": { 25 | "field_a": { 26 | "type": "text", 27 | "fields": { 28 | "clean": { 29 | "type": "text", 30 | "analyzer": "strip_punct" 31 | }, 32 | "keyword": { 33 | "type": "keyword" 34 | } 35 | } 36 | }, 37 | "field_b": { 38 | "type": "text", 39 | "fields": { 40 | "clean": { 41 | "type": "text", 42 | "analyzer": "strip_punct" 43 | }, 44 | "keyword": { 45 | "type": "keyword" 46 | } 47 | } 48 | }, 49 | "field_c": { 50 | "type": "text", 51 | "fields": { 52 | "clean": { 53 | "type": "text", 54 | "analyzer": "strip_punct" 55 | }, 56 | "keyword": { 57 | "type": "keyword" 58 | } 59 | } 60 | }, 61 | "field_d": { 62 | "type": "text", 63 | "fields": { 64 | "clean": { 65 | "type": "text", 66 | "analyzer": "strip_punct" 67 | }, 68 | "keyword": { 69 | "type": "keyword" 70 | } 71 | } 72 | }, 73 | "type_boolean": { 74 | "type": "boolean" 75 | }, 76 | "type_date": { 77 | "type": "date", 78 | "format": "yyyy-MM-dd'T'HH:mm:ss.SSS" 79 | }, 80 | "type_double": { 81 | "type": "double" 82 | }, 83 | "type_float": { 84 | "type": "float" 85 | }, 86 | "type_integer": { 87 | "type": "integer" 88 | }, 89 | "type_long": { 90 | "type": "long" 91 | }, 92 | "type_string": { 93 | "type": "keyword" 94 | }, 95 | "type_string_null": { 96 | "type": "keyword" 97 | }, 98 | "object": { 99 | "properties": { 100 | "a": { 101 | "properties": { 102 | "b": { 103 | "properties": { 104 | "c": { 105 | "type": "text", 106 | "fields": { 107 | "keyword": { 108 | "type": "keyword" 109 | } 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | } 117 | }, 118 | "unused": { 119 | "type": "keyword" 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/test/resources/TestIndexArrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "number_of_shards": 1, 4 | "number_of_replicas": 2 5 | }, 6 | "mappings" : { 7 | "properties" : { 8 | "array_1" : { 9 | "type" : "keyword" 10 | }, 11 | "array_2" : { 12 | "type" : "keyword" 13 | }, 14 | "array_3" : { 15 | "type" : "keyword" 16 | }, 17 | "array_4" : { 18 | "type" : "keyword" 19 | }, 20 | "string" : { 21 | "type" : "keyword" 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /src/test/resources/TestIndexObjectArrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "number_of_shards": 1, 4 | "number_of_replicas": 2 5 | }, 6 | "mappings": { 7 | "properties": { 8 | "first_name": { 9 | "type": "text" 10 | }, 11 | "last_name": { 12 | "type": "text" 13 | }, 14 | "phone": { 15 | "type": "nested", 16 | "properties": { 17 | "number": { 18 | "type": "keyword" 19 | }, 20 | "type": { 21 | "type": "keyword" 22 | } 23 | } 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/test/resources/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | es01: 4 | image: "docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}" 5 | user: elasticsearch 6 | command: 7 | - /bin/bash 8 | - -c 9 | - "elasticsearch-plugin install --batch file:///releases/zentity-${ZENTITY_VERSION}-elasticsearch-${ELASTICSEARCH_VERSION}.zip && elasticsearch" 10 | environment: 11 | - node.name=es01 12 | - cluster.name=zentity-test-cluster 13 | - cluster.initial_master_nodes=es01 14 | #- cluster.initial_master_nodes=es01,es02,es03 15 | #- discovery.seed_hosts=es02,es03 16 | - http.port=9400 17 | - bootstrap.memory_lock=true 18 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m -ea" 19 | - xpack.security.enabled=false 20 | - action.destructive_requires_name=false 21 | ulimits: 22 | memlock: 23 | soft: -1 24 | hard: -1 25 | volumes: 26 | - data01:/usr/share/elasticsearch/data 27 | - ${BUILD_DIRECTORY}/releases/:/releases 28 | ports: 29 | - 9400:9400 30 | networks: 31 | - elastic 32 | # es02: 33 | # image: "docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}" 34 | # user: elasticsearch 35 | # command: 36 | # - /bin/bash 37 | # - -c 38 | # - "elasticsearch-plugin install --batch file:///releases/zentity-${ZENTITY_VERSION}-elasticsearch-${ELASTICSEARCH_VERSION}.zip && elasticsearch" 39 | # environment: 40 | # - node.name=es02 41 | # - cluster.name=zentity-test-cluster 42 | # - cluster.initial_master_nodes=es01,es02,es03 43 | # - discovery.seed_hosts=es01,es03 44 | # - http.port=9400 45 | # - bootstrap.memory_lock=true 46 | # - "ES_JAVA_OPTS=-Xms512m -Xmx512m -ea" 47 | # - xpack.security.enabled=false 48 | # - action.destructive_requires_name=false 49 | # ulimits: 50 | # memlock: 51 | # soft: -1 52 | # hard: -1 53 | # volumes: 54 | # - data02:/usr/share/elasticsearch/data 55 | # - ${BUILD_DIRECTORY}/releases/:/releases 56 | # networks: 57 | # - elastic 58 | # es03: 59 | # image: "docker.elastic.co/elasticsearch/elasticsearch:${ELASTICSEARCH_VERSION}" 60 | # user: elasticsearch 61 | # command: 62 | # - /bin/bash 63 | # - -c 64 | # - "elasticsearch-plugin install --batch file:///releases/zentity-${ZENTITY_VERSION}-elasticsearch-${ELASTICSEARCH_VERSION}.zip && elasticsearch" 65 | # environment: 66 | # - node.name=es03 67 | # - cluster.name=zentity-test-cluster 68 | # - cluster.initial_master_nodes=es01,es02,es03 69 | # - discovery.seed_hosts=es01,es02 70 | # - http.port=9400 71 | # - bootstrap.memory_lock=true 72 | # - "ES_JAVA_OPTS=-Xms512m -Xmx512m -ea" 73 | # - xpack.security.enabled=false 74 | # - action.destructive_requires_name=false 75 | # ulimits: 76 | # memlock: 77 | # soft: -1 78 | # hard: -1 79 | # volumes: 80 | # - data03:/usr/share/elasticsearch/data 81 | # - ${BUILD_DIRECTORY}/releases/:/releases 82 | # networks: 83 | # - elastic 84 | 85 | volumes: 86 | data01: 87 | driver: local 88 | # data02: 89 | # driver: local 90 | # data03: 91 | # driver: local 92 | 93 | networks: 94 | elastic: 95 | driver: bridge -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------