diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..94c9ce4
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,50 @@
+name: Build
+
+on:
+ push:
+ branches: [ main ]
+ pull_request:
+ branches: [ main ]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: build-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ build:
+ name: Build and test
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Check out the code
+ uses: actions/checkout@v4
+
+ - name: Set up JDK 25
+ uses: actions/setup-java@v4
+ with:
+ java-version: '25'
+ distribution: 'temurin'
+ cache: maven
+
+ - name: Build and run tests
+ env:
+ GITLAB_MAVEN_URL: ${{ secrets.GITLAB_MAVEN_URL }}
+ GITLAB_MAVEN_USERNAME: ${{ secrets.GITLAB_DEPLOY_USER }}
+ GITLAB_MAVEN_TOKEN: ${{ secrets.GITLAB_DEPLOY_TOKEN }}
+ run: >
+ mvn -s .mvn/settings.xml
+ --batch-mode --no-transfer-progress --update-snapshots
+ -P gitlab-maven
+ clean verify
+
+ - name: Upload test reports if the build failed
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: surefire-reports
+ path: '**/target/surefire-reports/**'
+ retention-days: 7
\ No newline at end of file
diff --git a/.mvn/jvm.config b/.mvn/jvm.config
new file mode 100644
index 0000000..7cafa1e
--- /dev/null
+++ b/.mvn/jvm.config
@@ -0,0 +1 @@
+--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.text=ALL-UNNAMED --add-opens=java.desktop/java.awt.font=ALL-UNNAMED
diff --git a/.mvn/settings.xml b/.mvn/settings.xml
new file mode 100644
index 0000000..2547128
--- /dev/null
+++ b/.mvn/settings.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+ gitlab-maven
+ ${env.GITLAB_MAVEN_USERNAME}
+ ${env.GITLAB_MAVEN_TOKEN}
+
+
+
+
+
+
+ gitlab-maven
+
+ true
+
+
+
+
+ gitlab-maven
+ ${env.GITLAB_MAVEN_URL}
+
+ true
+
+
+ true
+
+
+
+
+
+
+ gitlab-maven
+ ${env.GITLAB_MAVEN_URL}
+
+ true
+
+
+ true
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..dd5b3a5
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,174 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
diff --git a/README.md b/README.md
index 6e64432..be70aa9 100644
--- a/README.md
+++ b/README.md
@@ -1,35 +1,199 @@
-# openepcis-document-validation-service
+
+
+
-An open-source service to validate EPCIS documents ensuring compliance with EPCIS standards.
+[](LICENSE)
+[](https://github.com/openepcis/openepcis-document-validation-service/releases)
+[](https://github.com/openepcis/openepcis-document-validation-service)
-## Introduction
+
OpenEPCIS Document Validation Service
-The `openepcis-document-validation-service` is designed to validate EPCIS documents to ensure they meet the required standards. EPCIS (Electronic Product Code Information Services) facilitates the exchange of visibility data within and across enterprises.
+Validates if a provided [EPCIS](https://www.gs1.org/standards/epcis) document follows the GS1 standard, and
+informs in plain language what is wrong when it does not. Works with EPCIS 2.0 JSON/JSON-LD and with
+EPCIS 1.2 and 2.0 XML.
-## Key Features
+## Why
-- Validates EPCIS documents against EPCIS 2.0 standards.
-- Supports both XML and JSON/JSON-LD formats.
-- Provides detailed validation error messages.
-- RESTful API for easy integration.
+An EPCIS document that looks fine can still be rejected, and the reason is usually buried in schema. A validator that responds with `does not match the uri pattern` leaves users
+guessing which field/attribute is not adhering to the standard. This service answers with the field, the users provided, and where in the document issue lies: For example if EPCIS
+document contains invalid `bizStep` such as `transforming` then tool responds with:
-## Getting Started
+```
+"transforming" is not a standard EPCIS business step for bizStep in event 1 (ObjectEvent).
+Did you mean "transporting"? Use one of the 41 standard steps (accepting, arriving, assembling,
+collecting, commissioning, consigning and 35 more), or, for a step of your own, a full web address
+such as https://example.com/bizStep/transforming.
+```
-### Prerequisites
+Each invalid value corresponds to one error to avoid chain of errors due to one field violation. All validation processing occur locally nothing reaches network, since every schema
+is
+bundled in the jar.
-- Java 11 or higher
-- Maven 3.6 or higher
-- Docker (optional, for containerized deployment)
+## Getting started
-### Installation
+Run the demo service in dev mode. It listens on port 9000, with Swagger UI at
+[localhost:9000/q/swagger-ui](http://localhost:9000/q/swagger-ui):
-1. Clone the repository:
- ```sh
- git clone https://github.com/openepcis/openepcis-document-validation-service.git
- cd openepcis-document-validation-service
- ```
+```bash
+git clone https://github.com/openepcis/openepcis-document-validation-service.git
+cd openepcis-document-validation-service
+mvn -pl quarkus/rest-app -am quarkus:dev
+```
-2. Build the project using Maven:
- ```sh
- mvn clean install
- ```
+Send it a document:
+
+```bash
+curl -X POST 'http://localhost:9000/api/events/validate' \
+ -H 'Content-Type: application/json' \
+ -H 'GS1-EPCIS-Version: 2.0.0' \
+ --data-binary @my-epcis-document.json
+```
+
+An empty `200` means the document is valid. A `400` returns the list of problems.
+
+## The API
+
+`POST /api/events/validate`
+
+| | |
+|-----------------------------|---------------------------------------------------------------------------|
+| Request body | the EPCIS document |
+| `Content-Type` | `application/json`, `application/ld+json` or `application/xml` |
+| `GS1-EPCIS-Version` header | `2.0.0` (default) or `1.2.0` |
+| `epcisDocumentSchema` query | `capture` (default) or `query` |
+| `Accept` | `application/json` for a JSON array, `application/xml` for a JAXB wrapper |
+
+| Response | Meaning |
+|----------|-------------------------------------------|
+| `200` | valid, empty body |
+| `400` | not valid, body lists the problems |
+| `415` | media type or EPCIS version not supported |
+
+### An example error format
+
+```json
+[
+ {
+ "type": "enum",
+ "line": "https://ref.gs1.org/standards/epcis/epcis-json-schema.json#/definitions/bizStep/anyOf/1/enum",
+ "location": "$.epcisBody.eventList[0].bizStep",
+ "message": "\"transforming\" is not a standard EPCIS business step for bizStep in event 1 (ObjectEvent). Did you mean \"transporting\"? Use one of the 41 standard steps (accepting, arriving, assembling, collecting, commissioning, consigning and 35 more), or, for a step of your own, a full web address such as https://example.com/bizStep/transforming.",
+ "field": "bizStep",
+ "value": "transforming",
+ "eventIndex": 0,
+ "eventType": "ObjectEvent",
+ "suggestion": "transporting",
+ "allowedValues": [
+ "accepting",
+ "arriving",
+ "assembling",
+ "collecting",
+ "commissioning",
+ "..."
+ ]
+ }
+]
+```
+
+`message` is written for a human to read. The other fields are there so a UI can do more with it:
+
+- `location` is a JSON path, so an editor can jump straight to the line. For XML, `line` carries the
+ line number instead.
+- `field`, `value`, `eventIndex` and `eventType` let you say "event 6, bizStep" without parsing text.
+- `suggestion` is the closest standard value, when the wrong one is a near miss.
+- `allowedValues` is the full list, kept out of the message so it can sit behind a "show all" toggle.
+
+## Use it as a library
+
+Add the module you need. Versions come from the OpenEPCIS BOM.
+
+```xml
+
+
+ io.openepcis
+ openepcis-document-validation-service
+
+
+
+
+io.openepcis
+openepcis-document-validation-rest-api
+
+
+
+
+io.openepcis
+openepcis-document-validation-servlet-api
+
+
+
+
+io.openepcis.quarkus
+quarkus-document-validation-service
+
+```
+
+Calling the validator directly:
+
+```java
+// SchemaValidator is @RequestScoped, so inject it instead of keeping one instance around
+@Inject
+SchemaValidator schemaValidator;
+
+final Multi errors = schemaValidator.validate(
+ document, // InputStream
+ "application/json", // media type decides JSON or XML
+ EPCISDocumentType.CAPTURE, // or QUERY
+ EPCISVersion.VERSION_2_0_0); // or VERSION_1_2_0
+```
+
+The stream stays empty when the document is valid. Each problem arrives as an item, so you can react to
+the first one without waiting for the rest. It only fails when validation itself broke, for example on
+malformed input.
+
+## Project layout
+
+| Module | What it does |
+|-----------------------------------------|------------------------------------------------------------|
+| `core` | the validator and the bundled schemas. No web dependencies |
+| `rest-api` | JAX-RS resource returning `Uni>` |
+| `servlet-api` | the same endpoint as a plain servlet |
+| `quarkus/runtime`, `quarkus/deployment` | Quarkus extension pair |
+| `quarkus/rest-app` | runnable demo service |
+| `restassured` | shared HTTP test suite, so both front ends behave the same |
+
+Error wording lives in `core/src/main/resources/validation-messages/epcis-validation-messages.yaml`.
+Rewording a message is a change to that file, not to Java.
+
+## Building
+
+Java 21 and Maven. The parent POM is the external OpenEPCIS BOM, so the first build downloads it.
+
+```bash
+mvn clean install # everything
+mvn -pl core test # one module
+mvn -pl core test -Dtest=JsonValidationMessageTest # one test class
+mvn -pl quarkus/rest-app -am verify -Pnative # native image
+```
+
+Test documents come from
+[openepcis-test-resources](https://github.com/openepcis/openepcis-test-resources), including the
+deliberately broken ones used to check the error messages.
+
+## Contributing
+
+Bug reports, new test documents and better error wording are all welcome. If you hit a message that
+left you guessing, that is worth an issue on its own, since clear messages are the whole point of this
+project.
+
+## Related
+
+- [OpenEPCIS Tools](https://tools.openepcis.io/) - open source EPCIS 2.0 tools and services
+- [OpenEPCIS](https://openepcis.io/) - Read more about OpenEPCIS
+- [OpenEPCIS Test Resources](https://github.com/openepcis/openepcis-test-resources) - EPCIS documents used for testing
+- [benelog GmbH & Co. KG](https://www.benelog.com/) - Company behind the OpenEPCIS
+- [GS1 EPCIS Standard](https://www.gs1.org/standards/epcis) - Learn more about EPCIS
+
+## License
+
+Licensed under the [Apache License 2.0](LICENSE).
diff --git a/core/pom.xml b/core/pom.xml
index 7db4c06..2d603ea 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -1,7 +1,7 @@
- io.quarkus
- quarkus-core
+ io.openepcis
+ openepcis-epcis-constants
+ compile
+
io.openepcis
- openepcis-epcis-constants
- compile
+ openepcis-test-resources
+ test
-
- io.netty
- netty-buffer
+ io.openepcis
+ openepcis-reactive-event-publisher
- -->
-
+
- org.projectlombok
- lombok
+ io.quarkus
+ quarkus-core
@@ -93,6 +95,10 @@
org.apache.commonscommons-collections4
+
+ org.apache.commons
+ commons-text
+ jakarta.xml.bind
diff --git a/core/src/main/java/io/openepcis/validation/SchemaValidator.java b/core/src/main/java/io/openepcis/validation/SchemaValidator.java
index 2455fb2..3815219 100644
--- a/core/src/main/java/io/openepcis/validation/SchemaValidator.java
+++ b/core/src/main/java/io/openepcis/validation/SchemaValidator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,72 +17,75 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchemaFactory;
+import io.openepcis.constants.EPCIS;
import io.openepcis.constants.EPCISDocumentType;
import io.openepcis.constants.EPCISVersion;
+import io.openepcis.validation.config.ValidationDefaults;
import io.openepcis.validation.json.JsonSchemaValidator;
import io.openepcis.validation.model.ValidationError;
import io.openepcis.validation.xml.Xml12SchemaValidator;
import io.openepcis.validation.xml.Xml20SchemaValidator;
import io.smallrye.mutiny.Multi;
+
import java.io.InputStream;
+import java.util.Locale;
public class SchemaValidator {
- private final JsonSchemaValidator jsonSchemaValidator;
- private final Xml20SchemaValidator xml20SchemaValidator;
- private final Xml12SchemaValidator xml12SchemaValidator;
-
- public SchemaValidator(final ObjectMapper mapper, final JsonSchemaFactory factory) {
- this.jsonSchemaValidator = new JsonSchemaValidator(mapper, factory);
- this.xml20SchemaValidator = new Xml20SchemaValidator();
- this.xml12SchemaValidator = new Xml12SchemaValidator();
- }
-
- /**
- * Method that accepts the EPCIS XML or JSON document/event and checks if it adheres to respective
- * schema or not.
- *
- * @param epcisInputData input EPCIS document/event that needs to be validated against schema.
- * @param mediaType format type of the input document: application/xml, application/json,
- * application/json+ld, etc.
- * @param epcisDocumentSchema type of EPCIS Document schema against which the document needs to be
- * validated against: Capture or Query schema.
- */
- public Multi validate(
- final InputStream epcisInputData,
- final String mediaType,
- final EPCISDocumentType epcisDocumentSchema,
- final EPCISVersion epcisVersion) {
- Validator validator = getValidator(mediaType, epcisVersion);
+ private final JsonSchemaValidator jsonSchemaValidator;
+ private final Xml20SchemaValidator xml20SchemaValidator;
+ private final Xml12SchemaValidator xml12SchemaValidator;
- if (epcisDocumentSchema.equals(EPCISDocumentType.CAPTURE)) {
- return validator.validateAgainstCaptureSchema(epcisInputData);
+ public SchemaValidator(final ObjectMapper mapper, final JsonSchemaFactory factory) {
+ this(mapper, factory, ValidationDefaults.MAX_REPORTED_ERRORS);
}
- if (epcisDocumentSchema.equals(EPCISDocumentType.QUERY)) {
- return validator.validateAgainstQuerySchema(epcisInputData);
+ public SchemaValidator(final ObjectMapper mapper, final JsonSchemaFactory factory, final int maxReportedErrors) {
+ this.jsonSchemaValidator = new JsonSchemaValidator(mapper, factory, maxReportedErrors);
+ // same limit for XML and JSON, so the two front ends behave alike
+ this.xml20SchemaValidator = new Xml20SchemaValidator(maxReportedErrors);
+ this.xml12SchemaValidator = new Xml12SchemaValidator(maxReportedErrors);
}
- throw new UnsupportedOperationException("Unsupported schema type : " + epcisDocumentSchema);
- }
+ /**
+ * Method that accepts the EPCIS XML or JSON document/event and checks if it adheres to respective schema or not.
+ *
+ * @param epcisInputData input EPCIS document/event that needs to be validated against schema.
+ * @param mediaType format type of the input document: application/xml, application/json, pplication/json+ld, etc.
+ * @param epcisDocumentSchema type of EPCIS Document schema against which the document needs to be validated against: Capture or Query schema.
+ */
+ public Multi validate(final InputStream epcisInputData,
+ final String mediaType,
+ final EPCISDocumentType epcisDocumentSchema,
+ final EPCISVersion epcisVersion) {
+ Validator validator = getValidator(mediaType, epcisVersion);
- private Validator getValidator(final String mediaType, EPCISVersion epcisVersion) {
+ if (epcisDocumentSchema.equals(EPCISDocumentType.CAPTURE)) {
+ return validator.validateAgainstCaptureSchema(epcisInputData);
+ }
- if (mediaType.toLowerCase().contains("json")) {
- return jsonSchemaValidator;
+ if (epcisDocumentSchema.equals(EPCISDocumentType.QUERY)) {
+ return validator.validateAgainstQuerySchema(epcisInputData);
+ }
+
+ throw new UnsupportedOperationException("Unsupported schema type : " + epcisDocumentSchema);
}
- if (mediaType.toLowerCase().contains("xml")) {
+ private Validator getValidator(final String mediaType, EPCISVersion epcisVersion) {
+ if (mediaType.toLowerCase(Locale.ROOT).contains(EPCIS.JSON_FORMAT)) {
+ return jsonSchemaValidator;
+ }
- if (EPCISVersion.VERSION_2_0_0.getSchemaVersion().equals(epcisVersion.getSchemaVersion())) {
- return xml20SchemaValidator;
- }
+ if (mediaType.toLowerCase(Locale.ROOT).contains(EPCIS.XML_FORMAT)) {
+ if (EPCISVersion.VERSION_2_0_0.getSchemaVersion().equals(epcisVersion.getSchemaVersion())) {
+ return xml20SchemaValidator;
+ }
- if (EPCISVersion.VERSION_1_2_0.getSchemaVersion().equals(epcisVersion.getSchemaVersion())) {
- return xml12SchemaValidator;
- }
- }
+ if (EPCISVersion.VERSION_1_2_0.getSchemaVersion().equals(epcisVersion.getSchemaVersion())) {
+ return xml12SchemaValidator;
+ }
+ }
- throw new UnsupportedOperationException("Unsupported media type : " + mediaType);
- }
+ throw new UnsupportedOperationException("Unsupported media type : " + mediaType);
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/Validator.java b/core/src/main/java/io/openepcis/validation/Validator.java
index 97c8f0a..502293a 100644
--- a/core/src/main/java/io/openepcis/validation/Validator.java
+++ b/core/src/main/java/io/openepcis/validation/Validator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,21 +18,22 @@
import io.openepcis.validation.exception.SchemaValidationException;
import io.openepcis.validation.model.ValidationError;
import io.smallrye.mutiny.Multi;
+
import java.io.InputStream;
public interface Validator {
- Multi validateAgainstCaptureSchema(final InputStream epcisInputData)
- throws SchemaValidationException;
+ Multi validateAgainstCaptureSchema(final InputStream epcisInputData) throws SchemaValidationException;
+
+ Multi validateAgainstQuerySchema(final InputStream epcisInputData) throws SchemaValidationException;
+
+ default InputStream getResourceAsStream(final String name) {
+ final InputStream inputStream = getClass().getResourceAsStream(name);
- Multi validateAgainstQuerySchema(final InputStream epcisInputData)
- throws SchemaValidationException;
+ if (inputStream == null) {
+ return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
+ }
- default InputStream getResourceAsStream(final String name) {
- InputStream inputStream = getClass().getResourceAsStream(name);
- if (inputStream == null) {
- return Thread.currentThread().getContextClassLoader().getResourceAsStream(name);
+ return inputStream;
}
- return inputStream;
- }
}
diff --git a/core/src/main/java/io/openepcis/validation/config/ValidationDefaults.java b/core/src/main/java/io/openepcis/validation/config/ValidationDefaults.java
new file mode 100644
index 0000000..c869957
--- /dev/null
+++ b/core/src/main/java/io/openepcis/validation/config/ValidationDefaults.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.config;
+
+/**
+ * Configuration for validation settings
+ */
+public class ValidationDefaults {
+ // Most problems reported per document; the last slot says the limit was reached.
+ public static final String MAX_REPORTED_ERRORS_TEXT = "1000";
+ public static final int MAX_REPORTED_ERRORS = Integer.parseInt(MAX_REPORTED_ERRORS_TEXT);
+
+ private ValidationDefaults() {
+ }
+}
diff --git a/core/src/main/java/io/openepcis/validation/exception/SchemaValidationException.java b/core/src/main/java/io/openepcis/validation/exception/SchemaValidationException.java
index 8fe7e97..a654df2 100644
--- a/core/src/main/java/io/openepcis/validation/exception/SchemaValidationException.java
+++ b/core/src/main/java/io/openepcis/validation/exception/SchemaValidationException.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,11 +17,11 @@
public class SchemaValidationException extends RuntimeException {
- public SchemaValidationException(String msg) {
- super(msg);
- }
+ public SchemaValidationException(String msg) {
+ super(msg);
+ }
- public SchemaValidationException(String msg, Throwable cause) {
- super(msg, cause);
- }
+ public SchemaValidationException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/json/JsonErrorHandler.java b/core/src/main/java/io/openepcis/validation/json/JsonErrorHandler.java
deleted file mode 100644
index e5b33c9..0000000
--- a/core/src/main/java/io/openepcis/validation/json/JsonErrorHandler.java
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.openepcis.validation.json;
-
-import com.networknt.schema.ValidationMessage;
-import io.openepcis.validation.model.ValidationError;
-import java.util.Collection;
-
-/**
- * Class to format the validation errors occur during the validation of JSON Schema Capture/Query
- * document/event.
- */
-public class JsonErrorHandler {
-
- @lombok.Getter private final java.util.List exceptions;
-
- public JsonErrorHandler() {
- this.exceptions = new java.util.ArrayList<>();
- }
-
- public void jsonValidationFormat(final Collection messages) {
- for (final ValidationMessage errorMessage : messages) {
- final ValidationError errorFormat = new ValidationError();
- errorFormat.setType(errorMessage.getType());
- errorFormat.setLine(errorMessage.getSchemaLocation().toString());
- errorFormat.setLocation(errorMessage.getInstanceLocation().toString());
- errorFormat.setMessage(errorFormat.filterErrorMessage(errorMessage.getMessage()));
- exceptions.add(errorFormat);
- }
- }
-}
diff --git a/core/src/main/java/io/openepcis/validation/json/JsonSchemaValidator.java b/core/src/main/java/io/openepcis/validation/json/JsonSchemaValidator.java
index 95dae78..6042825 100644
--- a/core/src/main/java/io/openepcis/validation/json/JsonSchemaValidator.java
+++ b/core/src/main/java/io/openepcis/validation/json/JsonSchemaValidator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,90 +15,240 @@
*/
package io.openepcis.validation.json;
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.networknt.schema.*;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.ValidationMessage;
+import io.openepcis.constants.EPCIS;
+import io.openepcis.reactive.publisher.ObjectNodePublisher;
import io.openepcis.validation.Validator;
+import io.openepcis.validation.config.ValidationDefaults;
import io.openepcis.validation.exception.SchemaValidationException;
+import io.openepcis.validation.message.JsonErrorFormatter;
+import io.openepcis.validation.message.MessageTemplates;
import io.openepcis.validation.model.ValidationError;
import io.quarkus.runtime.annotations.RegisterForReflection;
import io.smallrye.mutiny.Multi;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
-import lombok.extern.slf4j.Slf4j;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
-@Slf4j
@RegisterForReflection(targets = JsonSchemaFactory.class)
public class JsonSchemaValidator implements Validator {
- private final ObjectMapper mapper;
- private final JsonSchemaFactory validatorFactory;
- private final JsonSchema captureJsonSchema;
- private final JsonSchema queryJsonSchema;
- private final JsonErrorHandler jsonErrorHandler;
-
- public JsonSchemaValidator(
- final ObjectMapper objectMapper, final JsonSchemaFactory validatorFactory) {
- this.mapper = objectMapper;
- this.validatorFactory = validatorFactory;
- this.captureJsonSchema = loadSchema("/json-schema/modular-EPCIS-JSON-schema.json");
- this.queryJsonSchema = loadSchema("/json-schema/modified-query-schema.json");
- this.jsonErrorHandler = new JsonErrorHandler();
- }
-
- private JsonSchema loadSchema(final String path) {
- return validatorFactory.getSchema(getResourceAsStream(path));
- }
-
- @Override
- public Multi validateAgainstCaptureSchema(final InputStream epcisInputData)
- throws SchemaValidationException {
- return Multi.createFrom()
- .emitter(
- multiEmitter -> {
- try {
- final Set captureErrors =
- captureJsonSchema.validate(mapper.readValue(epcisInputData, JsonNode.class));
-
- // If there are any validation errors then throw the errors
- if (!captureErrors.isEmpty()) {
- jsonErrorHandler.jsonValidationFormat(captureErrors);
+ // Errors below here belong to the body, which is validated event by event instead.
+ private static final String BODY_PREFIX = "$." + EPCIS.EPCIS_BODY_IN_CAMEL_CASE;
+ private static final String EVENT_SCHEMA_REF = "{\"$ref\":\"resource:/json-schema/epcis-json-schema.json#/definitions/EPCIS-Document-Event\"}";
+ private static final int PRESCAN_LIMIT = 65536;
+ private static final JsonErrorFormatter FORMATTER = new JsonErrorFormatter();
+
+ private final ObjectMapper mapper;
+ private final JsonFactory prescanFactory;
+ private final JsonSchema documentSchema; // switches on type: EPCISDocument or EPCISQueryDocument
+ private final JsonSchema queryParameterSchema; // GE_/LT_/EQ_ style query, not a document
+ private final JsonSchema eventSchema;
+ private final int maxReportedErrors;
+
+ // Kept so existing callers and tests compile without passing configuration.
+ public JsonSchemaValidator(final ObjectMapper objectMapper, final JsonSchemaFactory validatorFactory) {
+ this(objectMapper, validatorFactory, ValidationDefaults.MAX_REPORTED_ERRORS);
+ }
+
+ public JsonSchemaValidator(final ObjectMapper objectMapper,
+ final JsonSchemaFactory validatorFactory,
+ final int maxReportedErrors) {
+ this.mapper = objectMapper;
+ this.maxReportedErrors = maxReportedErrors > 0 ? maxReportedErrors : ValidationDefaults.MAX_REPORTED_ERRORS;
+
+ // the pre-scan must not close the body, the publisher still has to read it
+ this.prescanFactory = new JsonFactory().disable(JsonParser.Feature.AUTO_CLOSE_SOURCE);
+
+ // every schema is a bundled classpath resource, so validation never reaches the network
+ this.documentSchema = loadSchema(validatorFactory, "/json-schema/modular-EPCIS-JSON-schema.json");
+ this.queryParameterSchema = loadSchema(validatorFactory, "/json-schema/modified-query-schema.json");
+ this.eventSchema = validatorFactory.getSchema(EVENT_SCHEMA_REF);
+ }
+
+ private JsonSchema loadSchema(final JsonSchemaFactory validationFactory, final String path) {
+ return validationFactory.getSchema(getResourceAsStream(path));
+ }
+
+ @Override
+ public Multi validateAgainstCaptureSchema(final InputStream epcisInputData) throws SchemaValidationException {
+ return validate(epcisInputData, false, "Capture");
+ }
+
+ @Override
+ public Multi validateAgainstQuerySchema(final InputStream epcisInputData) throws SchemaValidationException {
+ return validate(epcisInputData, true, "Query");
+ }
+
+ private Multi validate(final InputStream epcisInputData,
+ final boolean queryRequested,
+ final String label) {
+ // lazy read so nothing read until subscription
+ return Multi.createFrom().deferred(() -> {
+ try {
+ final BufferedInputStream body = new BufferedInputStream(epcisInputData);
+ final String type = prescanType(body);
+
+ // there is no type EPCISDocument/EPCISQueryDocument: a literal query when one was asked for, else a document missing its type
+ if (type == null) {
+ return single(body, queryRequested ? queryParameterSchema : documentSchema);
+ }
+
+ // if type is EPCISDocument/EPCISQueryDocument then use ObjectNodePublisher to stream and validate event-by-event from eventsList
+ if (EPCIS.EPCIS_DOCUMENT.equals(type) || EPCIS.EPCIS_QUERY_DOCUMENT.equals(type)) {
+ return streamDocument(body, type);
}
- this.jsonErrorHandler.getExceptions().stream().forEach(multiEmitter::emit);
- multiEmitter.complete();
- } catch (Exception exception) {
- multiEmitter.fail(
- new SchemaValidationException(
- "Exception occurred during validation against EPCIS JSON Capture schema : "
- + exception.getMessage()));
- }
- });
- }
-
- @Override
- public Multi validateAgainstQuerySchema(final InputStream document)
- throws SchemaValidationException {
- return Multi.createFrom()
- .emitter(
- multiEmitter -> {
- try {
- final Set queryErrors =
- queryJsonSchema.validate(mapper.readValue(document, JsonNode.class));
-
- // If there are any validation errors then throw the errors
- if (!queryErrors.isEmpty()) {
- jsonErrorHandler.jsonValidationFormat(queryErrors);
+ // If none match then treat is as bareEvent
+ return single(body, documentSchema);
+ } catch (Exception exception) {
+ return Multi.createFrom().failure(new SchemaValidationException("Exception occurred during validation against EPCIS JSON " + label + " schema : " + exception.getMessage(), exception));
+ }
+ });
+ }
+
+ // Reads the initial token to find the top level type then revert so the further stream reads from start
+ private String prescanType(final BufferedInputStream body) throws IOException {
+ body.mark(PRESCAN_LIMIT);
+ try (JsonParser parser = prescanFactory.createParser(body)) {
+ if (parser.nextToken() != JsonToken.START_OBJECT) {
+ return null;
+ }
+ int depth = 1;
+
+ while (depth > 0) {
+ final JsonToken token = parser.nextToken();
+ if (token == null) {
+ return null;
+ }
+
+ if (token == JsonToken.FIELD_NAME) {
+ if (depth == 1 && EPCIS.TYPE.equals(parser.currentName())) {
+ parser.nextToken();
+ // only the top level type counts
+ return parser.getValueAsString();
+ }
+ } else if (token == JsonToken.START_OBJECT || token == JsonToken.START_ARRAY) {
+ depth++;
+ } else if (token == JsonToken.END_OBJECT || token == JsonToken.END_ARRAY) {
+ depth--;
}
+ }
+ return null;
+ } catch (Exception exception) {
+ return null;
+ } finally {
+ // hand the untouched stream on
+ body.reset();
+ }
+ }
+
+ // A single object: a literal query, or one bare event. Never an event list.
+ private Multi single(final InputStream body, final JsonSchema schema) throws IOException {
+ final JsonNode node = mapper.readTree(body);
+ final List errors = cap(FORMATTER.format(node, schema.validate(node)));
+ return Multi.createFrom().iterable(errors);
+ }
+
+ // Stream EPCISDocument/EPCISQueryDocument to validate event-by-event from eventsList
+ private Multi streamDocument(final InputStream body, final String documentType) throws IOException {
+ // Path to read get the eventsList
+ final String eventListPath = EPCIS.EPCIS_QUERY_DOCUMENT.equals(documentType) ? EPCIS.QUERY_EVENT_LIST_PATH : EPCIS.CAPTURE_EVENT_LIST_PATH;
+ final AtomicInteger nextEvent = new AtomicInteger();
+ final AtomicLong nodes = new AtomicLong();
+ final AtomicLong kept = new AtomicLong();
+ final int reportable = Math.max(1, maxReportedErrors - 1);
+
+ return Multi.createFrom()
+ .publisher(ObjectNodePublisher.fromInputStream(body))
+ .onItem()
+ .transformToMultiAndConcatenate(node -> {
+ nodes.incrementAndGet();
+ final String nodeType = node.path(EPCIS.TYPE).asText("");
+ //Validate header or event depending on type separately
+ return Multi.createFrom().iterable(
+ EPCIS.EPCIS_DOCUMENT.equals(nodeType) || EPCIS.EPCIS_QUERY_DOCUMENT.equals(nodeType)
+ ? validateHeader(node)
+ : validateEvent(node, eventListPath, nextEvent.getAndIncrement()));
+ })
+ // nothing parsed at all must never report like a clean document
+ .onCompletion().continueWith(() -> nodes.get() == 0 ? List.of(unreadable()) : List.of())
+ // one more than we show, which is what proves more exist without counting
+ .select().first(maxReportedErrors)
+ // that extra one is dropped, it only served as proof that there were more than the limit, so report a single limitExceeded error instead
+ .select().where(error -> kept.incrementAndGet() <= reportable)
+ .onCompletion().continueWith(() -> kept.get() > reportable ? List.of(limitExceeded(reportable)) : List.of());
+ }
+
+ // Validates the header/envelope against GS1 schema by adding empty eventsList so it validates
+ private List validateHeader(final ObjectNode header) {
+ header.putObject(EPCIS.EPCIS_BODY_IN_CAMEL_CASE);
+ final List envelope = new ArrayList<>();
+ for (final ValidationError error : FORMATTER.format(null, documentSchema.validate(header))) {
+ final String at = error.getLocation();
+ if (at == null || !at.startsWith(BODY_PREFIX)) {
+ envelope.add(error);
+ }
+ }
+ return envelope;
+ }
+
+
+ private List validateEvent(final ObjectNode event,
+ final String eventListPath,
+ final int index) {
+ final Set messages = eventSchema.validate(event);
+ // formatEvent turns $.eventTime back into $.epcisBody.eventList[3].eventTime
+ final String eventType = event.path(EPCIS.TYPE).asText(null);
+ return messages.isEmpty() ? List.of() : FORMATTER.formatEvent(eventListPath, index, eventType, messages);
+ }
+
+ // Applies the limit to a single object result, where there is no stream to cut short.
+ private List cap(final List all) {
+ final int reportable = Math.max(1, maxReportedErrors - 1);
+ if (all.size() <= reportable) {
+ return all;
+ }
+ final List capped = new ArrayList<>(all.subList(0, reportable));
+ capped.add(limitExceeded(reportable));
+ return capped;
+ }
+
+ // The parser produced nothing so was unable to read the documents report it instead of reporting a clean document.
+ private ValidationError unreadable() {
+ final ValidationError error = new ValidationError();
+ error.setType("required");
+ error.setLocation("$");
+ error.setField("@context");
+ error.setMessage(MessageTemplates.hasKeyword("document.unreadable")
+ ? MessageTemplates.keyword("document.unreadable")
+ : "The document could not be read. Check that @context is present and appears before epcisBody.");
+ return error;
+ }
+
+ // The last entry in errors when limit is exceeded, so a capped list is never mistaken for a complete one.
+ private ValidationError limitExceeded(final int limit) {
+ final ValidationError error = new ValidationError();
+ error.setType("limitExceeded");
+ error.setMessage(MessageTemplates.hasKeyword("limit.exceeded")
+ ? MessageTemplates.render(MessageTemplates.keyword("limit.exceeded"), Map.of("limit", String.valueOf(limit)))
+ : "Validation stopped after " + limit + " problems. This document has more.");
+ return error;
- this.jsonErrorHandler.getExceptions().stream().forEach(multiEmitter::emit);
- multiEmitter.complete();
- } catch (Exception exception) {
- multiEmitter.fail(
- new SchemaValidationException(
- "Exception occurred during validation against EPCIS JSON Query schema : "
- + exception.getMessage()));
- }
- });
- }
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/message/JsonErrorFormatter.java b/core/src/main/java/io/openepcis/validation/message/JsonErrorFormatter.java
new file mode 100644
index 0000000..4ee6d24
--- /dev/null
+++ b/core/src/main/java/io/openepcis/validation/message/JsonErrorFormatter.java
@@ -0,0 +1,539 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.message;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.networknt.schema.ValidationMessage;
+import io.openepcis.validation.model.ValidationError;
+import org.apache.commons.collections4.CollectionUtils;
+
+import java.util.*;
+
+/**
+ * Turns the raw networknt messages into one readable error per place in the document.
+ */
+public final class JsonErrorFormatter {
+
+ //When one position produced several messages, lead with the most useful keyword.
+ private static final List KEYWORD_PRIORITY =
+ List.of(
+ "required", "additionalProperties", "propertyNames", "enum", "type", "format", "pattern",
+ "minItems", "maxItems", "uniqueItems", "minimum", "maximum", "minLength");
+ // Keeps a 41 value vocabulary out of the sentence. The full list travels in allowedValues.
+ private static final int ALLOWED_SAMPLE_SIZE = 6;
+
+ /**
+ * Where a single streamed event sits in the document. Null whenever the whole document was
+ * parsed, because then the paths networknt reports are already absolute.
+ *
+ * @param prefix the absolute path of this event, for example $.epcisBody.eventList[3]
+ * @param index its zero based position in the event list
+ * @param type its EPCIS event type, or null when the document did not declare one
+ */
+ private record EventContext(String prefix, int index, String type) {
+ }
+
+ /**
+ * Groups and words every failure of a whole parsed document.
+ */
+ public List format(final JsonNode document, final Collection messages) {
+ return format(document, messages, null); // no context, the reported paths are already absolute
+ }
+
+ /**
+ * Groups and words the failures of one streamed event.
+ *
+ *
A streamed event is validated on its own, so networknt reports $.eventTime where the caller
+ * needs $.epcisBody.eventList[3].eventTime. The context restores that, which is what keeps the
+ * location usable for jumping to a line in an editor.
+ *
+ * @param eventListPath absolute path of the list holding the event, without the index
+ * @param eventIndex zero based position of the event in that list
+ * @param eventType EPCIS event type of the event, may be null
+ */
+ public List formatEvent(final String eventListPath,
+ final int eventIndex,
+ final String eventType,
+ final Collection messages) {
+ // the index is appended here so a caller cannot get the bracket syntax wrong
+ final String prefix = eventListPath + "[" + eventIndex + "]";
+ // no document tree exists while streaming, the context carries what would be read from it
+ return format(null, messages, new EventContext(prefix, eventIndex, eventType));
+ }
+
+ private List format(final JsonNode document,
+ final Collection messages,
+ final EventContext context) {
+
+ if (CollectionUtils.isEmpty(messages)) {
+ // valid document, so no allocation at all
+ return List.of();
+ }
+
+ final Map> byLocation = new LinkedHashMap<>();
+ for (final ValidationMessage message : messages) {
+ // one bucket per position, which is what merges the duplicates
+ byLocation.computeIfAbsent(message.getInstanceLocation().toString(), key -> new ArrayList<>()).add(message);
+ }
+
+ // where each anyOf/oneOf choice failed, so a choice can tell a missing field from a bad one
+ final Map> branchLocations = branchLocations(messages);
+ final List results = new ArrayList<>(byLocation.size());
+ byLocation.forEach((location, group) -> results.addAll(buildAll(document, location, group, branchLocations, context)));
+ results.sort(Comparator.comparing(error -> error.getLocation() == null ? "" : error.getLocation()));
+ return results;
+ }
+
+ /**
+ * Records every position at which each anyOf/oneOf choice failed, keyed by the choice.
+ *
+ *
A branch that fails at the position of the choice itself was rejected because a field is
+ * absent. A branch that fails deeper got past that presence check, so the field was supplied and
+ * only its contents are wrong. Telling those two apart is what {@link
+ * #choiceWasSatisfiedButInvalid} needs.
+ */
+ private Map> branchLocations(final Collection messages) {
+ final Map> byBranch = new HashMap<>();
+ for (final ValidationMessage message : messages) {
+ final String branch = branchGroupOf(message);
+ if (branch != null) {
+ byBranch.computeIfAbsent(branch, key -> new HashSet<>())
+ .add(message.getInstanceLocation().toString());
+ }
+ }
+ return byBranch;
+ }
+
+ /**
+ * Whether the document already satisfied this choice and only got the contents wrong.
+ *
+ *
An event carrying a quantityList with an unknown field inside it fails that branch deep in
+ * the subtree, and the sibling branches then report their own fields as missing. Saying "epcList
+ * or sensorElementList is missing" would be wrong, so the choice is dropped and the precise error
+ * inside quantityList is kept instead.
+ *
+ *
Only failures of this same choice count. That is what stops an unrelated problem, such as a
+ * malformed eventTime, from ever hiding a genuinely missing field.
+ */
+ private boolean choiceWasSatisfiedButInvalid(final String branch,
+ final String location,
+ final Map> branchLocations) {
+ final Set inGroup = branchLocations.get(branch);
+ if (inGroup == null) {
+ return false; // nothing recorded for this choice, so keep the error
+ }
+ for (final String other : inGroup) {
+ if (other.length() > location.length() // strictly longer means deeper than the choice
+ && other.startsWith(location)
+ && (other.charAt(location.length()) == '.' || other.charAt(location.length()) == '[')) {
+ return true; // a branch of this choice got past the presence check
+ }
+ }
+ return false;
+ }
+
+ // Which anyOf or oneOf produced it, so sibling branches of the same choice can be merged.
+ private String branchGroupOf(final ValidationMessage message) {
+ final String path = message.getEvaluationPath().toString();
+ int index = path.lastIndexOf("anyOf[");
+ if (index < 0) {
+ index = path.lastIndexOf("oneOf[");
+ }
+ // the shared prefix identifies the choice
+ return index < 0 ? null : path.substring(0, index);
+ }
+
+ /**
+ * One position can still hold genuinely different problems. Alternative required fields coming
+ * from sibling branches become one "at least one of" sentence, everything else is kept separate.
+ */
+ private List buildAll(final JsonNode document,
+ final String location,
+ final List group,
+ final Map> branchLocations,
+ final EventContext context) {
+
+ // Every "required" message is grouped by the choice it came out of. The empty key collects the
+ // unconditional ones, where each named field really is missing rather than being an alternative.
+ final Map> requiredByBranch = new LinkedHashMap<>();
+ final List others = new ArrayList<>();
+ for (final ValidationMessage message : group) {
+ if ("required".equals(message.getType())) {
+ final String branch = branchGroupOf(message); // null when the rule is unconditional
+ requiredByBranch
+ .computeIfAbsent(branch == null ? "" : branch, key -> new ArrayList<>())
+ .add(message);
+ } else {
+ others.add(message);
+ }
+ }
+
+ final List results = new ArrayList<>();
+ // field name to the message that reported it, so nothing is lost and duplicates collapse
+ final Map leftoverRequired = new LinkedHashMap<>();
+
+ requiredByBranch.forEach(
+ (branch, siblings) -> {
+ final Set fields = new LinkedHashSet<>(); // ordered, so wording is stable
+ for (final ValidationMessage message : siblings) {
+ if (message.getProperty() != null) {
+ fields.add(message.getProperty()); // getProperty holds the missing field name
+ }
+ }
+
+ // sibling branches offer a choice, so they become one "at least one of" sentence
+ if (!branch.isEmpty() && fields.size() > 1 && MessageTemplates.hasKeyword("required.alternatives")) {
+ if (choiceWasSatisfiedButInvalid(branch, location, branchLocations)) {
+ return; // the precise error inside that field is the real one
+ }
+
+ results.add(buildRequiredGroup(document, location, siblings, fields, "required.alternatives", context));
+ return;
+ }
+
+ for (final ValidationMessage message : siblings) {
+ if (message.getProperty() == null) {
+ others.add(message); // no field name to group by, report it on its own
+ } else {
+ leftoverRequired.putIfAbsent(message.getProperty(), message);
+ }
+ }
+ });
+
+ // unconditional rules mean all of these fields are missing, so every one must be named
+ if (leftoverRequired.size() > 1 && MessageTemplates.hasKeyword("required.multiple")) {
+ results.add(
+ buildRequiredGroup(
+ document,
+ location,
+ List.copyOf(leftoverRequired.values()),
+ leftoverRequired.keySet(),
+ "required.multiple",
+ context));
+ } else {
+ others.addAll(leftoverRequired.values()); // a single missing field stays an ordinary message
+ }
+
+ if (!others.isEmpty()) {
+ results.add(build(document, location, others, context));
+ }
+ return results;
+ }
+
+ /**
+ * Builds one error that names several fields at once.
+ *
+ * @param templateKey required.alternatives when the fields are a choice, required.multiple when
+ * every one of them is genuinely missing
+ */
+ private ValidationError buildRequiredGroup(
+ final JsonNode document,
+ final String location,
+ final List siblings,
+ final Set fields,
+ final String templateKey,
+ final EventContext context) {
+ final ValidationError error = new ValidationError();
+ error.setType("required");
+ error.setSchemaPath(siblings.getFirst().getSchemaLocation().toString());
+ error.setLocation(absolute(location, context)); // full document path even while streaming
+
+ final String joined = String.join(", ", fields);
+ error.setField(joined);
+ applyEventContext(error, document, location, context);
+ error.setAllowedValues(List.copyOf(fields));
+ error.setMessage(MessageTemplates.render(MessageTemplates.keyword(templateKey), Map.of("where", whereLabel(error), "fields", joined)));
+
+ return error;
+ }
+
+ /**
+ * Builds one error from a group of messages that all describe the same position.
+ */
+ private ValidationError build(final JsonNode document,
+ final String location,
+ final List group,
+ final EventContext context) {
+
+ final ValidationMessage primary = pickPrimary(group); // the most informative of the group
+ final ValidationError error = new ValidationError();
+
+ error.setType(primary.getType()); // unchanged, the front end may switch on it
+ error.setSchemaPath(primary.getSchemaLocation().toString()); // unchanged
+ error.setLocation(absolute(location, context)); // full document path even while streaming
+
+ // the field name is the last path segment, except for required, additionalProperties and
+ // propertyNames, where the path points at the parent and the name is in getProperty
+ error.setField(primary.getProperty() != null ? primary.getProperty() : lastSegment(location));
+ error.setValue(plainValue(primary.getInstanceNode())); // the value sent, previously discarded
+ applyEventContext(error, document, location, context);
+
+ final List allowed = allowedValuesIn(group); // read from the schema, never hard coded
+ if (!allowed.isEmpty()) {
+ error.setAllowedValues(allowed); // full list as data so the UI can reveal it on demand
+ error.setSuggestion(ValueSuggester.closest(error.getValue(), allowed)); // commons-text
+ }
+
+ error.setMessage(renderMessage(primary, group, error, allowed));
+ return error;
+ }
+
+ /**
+ * Chooses the wording and fills in every placeholder.
+ */
+ private String renderMessage(
+ final ValidationMessage primary,
+ final List group,
+ final ValidationError error,
+ final List allowed) {
+
+ final Map values = new HashMap<>(); // one small map per reported error
+ values.put("field", error.getField() == null ? "this field" : error.getField());
+ values.put("value", error.getValue() == null ? "" : error.getValue());
+ values.put("where", whereLabel(error));
+ values.put("allowedCount", String.valueOf(allowed.size()));
+ values.put("allowedSample", sample(allowed));
+ values.put(
+ "suggestion",
+ error.getSuggestion() == null
+ ? "" // no close match, so the sentence simply omits the hint
+ : MessageTemplates.fill(
+ MessageTemplates.fragment("suggestionHint", " Did you mean \"{best}\"?"),
+ Map.of("best", error.getSuggestion())));
+
+ // keyword specific extras, reusing the arguments networknt already computed for us
+ final Object[] arguments = primary.getArguments();
+ if (arguments != null && arguments.length > 0) {
+ if ("type".equals(primary.getType()) && arguments.length >= 2) {
+ values.put("actual", String.valueOf(arguments[0])); // for type the order is found, expected
+ values.put("expected", String.valueOf(arguments[1]));
+ } else {
+ values.put("limit", String.valueOf(arguments[0])); // min/max style keywords
+ if (arguments.length >= 2) {
+ values.put("actual", String.valueOf(arguments[1]));
+ }
+ }
+ }
+
+ final String rendered = MessageTemplates.render(MessageTemplates.templateFor(primary), values);
+ final String base = rendered != null ? rendered : primary.getError(); // layer 3 safety net
+
+ final String extras = otherReasons(primary, group);
+ if (extras.isEmpty()) {
+ return MessageTemplates.tidy(base); // the common case
+ }
+ return MessageTemplates.tidy(
+ base
+ + MessageTemplates.fill(
+ MessageTemplates.fragment("reasonMore", " Also: {reasons}"),
+ Map.of("reasons", extras)));
+ }
+
+ /**
+ * Any genuinely different reason at the same position, so nothing is silently dropped.
+ */
+ private String otherReasons(final ValidationMessage primary, final List group) {
+ if (group.size() < 2) {
+ return ""; // nothing else to report
+ }
+ final Set keywords = new HashSet<>();
+ for (final ValidationMessage message : group) {
+ keywords.add(message.getType());
+ }
+ // repeated branches of one keyword, and the vocabulary pair of enum plus uri, are one mistake
+ if (keywords.size() <= 1
+ || (keywords.size() == 2
+ && keywords.contains("enum")
+ && (keywords.contains("format") || keywords.contains("pattern")))) {
+ return "";
+ }
+ final List extras = new ArrayList<>();
+ final Set seen = new HashSet<>();
+ seen.add(primary.getType()); // the primary reason is already in the sentence
+ for (final ValidationMessage message : group) {
+ if (seen.add(message.getType())) {
+ extras.add(message.getError()); // one entry per distinct remaining keyword
+ }
+ }
+ return String.join("; ", extras);
+ }
+
+ /**
+ * Picks the message whose keyword explains the mistake best.
+ */
+ private ValidationMessage pickPrimary(final List group) {
+ ValidationMessage best = group.get(0);
+ int bestRank = rank(best.getType());
+ for (final ValidationMessage message : group) {
+ final int rank = rank(message.getType());
+ if (rank < bestRank) {
+ bestRank = rank;
+ best = message;
+ }
+ }
+ return best;
+ }
+
+ /**
+ * Lower is better. Unknown keywords sort last rather than being dropped.
+ */
+ private int rank(final String keyword) {
+ final int index = KEYWORD_PRIORITY.indexOf(keyword);
+ return index < 0 ? KEYWORD_PRIORITY.size() : index;
+ }
+
+ /**
+ * Reads the allowed values from the schema node itself. This is why no EPCIS vocabulary list is
+ * ever copied into Java or into the catalogue, and why a GS1 schema update needs no code change.
+ */
+ private List allowedValuesIn(final List group) {
+ for (final ValidationMessage message : group) {
+ if ("enum".equals(message.getType())) {
+ final JsonNode schemaNode = message.getSchemaNode(); // the enum array from the schema
+ if (schemaNode != null && schemaNode.isArray()) {
+ final List values = new ArrayList<>(schemaNode.size()); // exact size
+ schemaNode.forEach(node -> values.add(node.asText()));
+ return values;
+ }
+ }
+ }
+ return List.of(); // shared empty list, no allocation
+ }
+
+ /**
+ * A short sample, so a 41 value vocabulary never floods the sentence.
+ */
+ private String sample(final List allowed) {
+ if (allowed.isEmpty()) {
+ return "";
+ }
+ if (allowed.size() <= ALLOWED_SAMPLE_SIZE) {
+ return String.join(", ", allowed);
+ }
+ return String.join(", ", allowed.subList(0, ALLOWED_SAMPLE_SIZE)) // subList is a view, no copy
+ + " and "
+ + (allowed.size() - ALLOWED_SAMPLE_SIZE)
+ + " more";
+ }
+
+ /**
+ * Records which event holds the problem, so a message can say "event 6 (TransformationEvent)".
+ */
+ private void applyEventContext(final ValidationError error,
+ final JsonNode document,
+ final String location,
+ final EventContext context) {
+ if (context != null) {
+ // streaming already knows which event this is, so no path parsing and no tree lookup
+ error.setEventIndex(context.index());
+ error.setEventType(context.type());
+ return;
+ }
+ final Integer index = eventIndexOf(location);
+ if (index == null) {
+ return; // the problem is outside eventList, for example at the document root
+ }
+ error.setEventIndex(index);
+ if (document == null) {
+ return; // still useful without the type
+ }
+ final JsonNode events = document.path("epcisBody").path("eventList"); // path never throws
+ if (events.isArray() && index < events.size()) {
+ final JsonNode type = events.get(index).path("type");
+ if (type.isTextual()) {
+ error.setEventType(type.asText()); // read from the parsed tree, no second parse
+ }
+ }
+ }
+
+ /**
+ * Rewrites an event relative path into a full document path, so a caller of {@link #formatEvent}
+ * gets the same location it would have got from validating the whole document.
+ */
+ private String absolute(final String location, final EventContext context) {
+ if (context == null || location == null) {
+ return location; // whole document mode, the path is already absolute
+ }
+ // $ alone means the event itself, anything else keeps its tail after the leading $
+ return "$".equals(location) ? context.prefix() : context.prefix() + location.substring(1);
+ }
+
+ /**
+ * Renders {where}: the document, or an event number with its type when known.
+ */
+ private String whereLabel(final ValidationError error) {
+ if (error.getEventIndex() == null) {
+ return MessageTemplates.fragment("whereDocument", "the document");
+ }
+ final String number = String.valueOf(error.getEventIndex() + 1); // humans count from 1
+ if (error.getEventType() != null) {
+ return MessageTemplates.fill(
+ MessageTemplates.fragment("whereEventTyped", "event {number} ({type})"),
+ Map.of("number", number, "type", error.getEventType()));
+ }
+ return MessageTemplates.fill(
+ MessageTemplates.fragment("whereEvent", "event {number}"), Map.of("number", number));
+ }
+
+ /**
+ * Reads the zero based index out of a path segment such as eventList[5].
+ */
+ private Integer eventIndexOf(final String location) {
+ final int marker = location.indexOf("eventList[");
+ if (marker < 0) {
+ return null;
+ }
+ final int start = marker + "eventList[".length();
+ final int end = location.indexOf(']', start);
+ if (end < 0) {
+ return null;
+ }
+ try {
+ return Integer.valueOf(location.substring(start, end));
+ } catch (NumberFormatException e) {
+ return null; // an unexpected path must never break a validation response
+ }
+ }
+
+ /**
+ * The field name at the end of a path, ignoring any array indexes.
+ */
+ private String lastSegment(final String location) {
+ if (location == null || location.isEmpty() || "$".equals(location)) {
+ return null; // the document root has no field name
+ }
+ String path = location;
+ while (path.endsWith("]")) { // strip trailing indexes, e.g. epcList[0] gives epcList
+ final int open = path.lastIndexOf('[');
+ if (open < 0) {
+ break;
+ }
+ path = path.substring(0, open);
+ }
+ final int dot = path.lastIndexOf('.');
+ return dot < 0 ? path : path.substring(dot + 1);
+ }
+
+ /**
+ * The value without JSON quoting. Objects and arrays are skipped so nothing floods a message.
+ */
+ private String plainValue(final JsonNode node) {
+ if (node == null || node.isMissingNode() || node.isNull() || !node.isValueNode()) {
+ return null;
+ }
+ return node.asText();
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/io/openepcis/validation/message/MessageTemplates.java b/core/src/main/java/io/openepcis/validation/message/MessageTemplates.java
new file mode 100644
index 0000000..b0573e5
--- /dev/null
+++ b/core/src/main/java/io/openepcis/validation/message/MessageTemplates.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.message;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.networknt.schema.ValidationMessage;
+import org.apache.commons.collections4.MapUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.util.Map;
+
+/**
+ * Reads the message catalogue and decides which sentence describes a validation failure.
+ */
+public class MessageTemplates {
+ // Read the catalog of error validation messages
+ private static final String LOCATION = "/validation-messages/epcis-validation-messages.yaml";
+ // Loaded exactly once, when this class is first touched.
+ private static final Catalogue CATALOGUE = load();
+
+ // Hold the 4 types of validation message from epcis-validation-messages.yaml
+ private record Catalogue(Map schemaLocations, Map keywords, Map fragments, Map xsdTypes) {
+ }
+
+ private static Catalogue load() {
+ try (InputStream stream = MessageTemplates.class.getResourceAsStream(LOCATION)) {
+ if (stream == null) {
+ throw new IllegalStateException("Validation message catalogue not found at " + LOCATION);
+ }
+ final Catalogue parsed = new ObjectMapper(new YAMLFactory()).disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES).readValue(stream, Catalogue.class);
+ // rebuild with truly immutable maps, no wrapper indirection
+ return new Catalogue(immutable(parsed.schemaLocations()), immutable(parsed.keywords()), immutable(parsed.fragments()), immutable(parsed.xsdTypes()));
+ } catch (IOException e) {
+ throw new UncheckedIOException("Could not read validation message catalogue", e);
+ }
+ }
+
+ // Map.copyOf gives a real immutable map. A missing YAML section becomes empty, never null
+ private static Map immutable(final Map source) {
+ return MapUtils.isEmpty(source) ? Map.of() : Map.copyOf(source);
+ }
+
+ /**
+ * The template for this message, or null when the caller should fall back to getError().
+ */
+ public static String templateFor(final ValidationMessage message) {
+ final String location = message.getSchemaLocation().toString();
+ final int hash = location.indexOf('#');
+ String pointer = hash < 0 ? "#" : location.substring(hash);
+ final String keyword = message.getType();
+ while (true) {
+ String template = CATALOGUE.schemaLocations().get(pointer + "." + keyword);
+ if (template != null) {
+ return template;
+ }
+ template = CATALOGUE.schemaLocations().get(pointer);
+ if (template != null) {
+ return template;
+ }
+ final int slash = pointer.lastIndexOf('/');
+ if (slash <= 0) {
+ break;
+ }
+ pointer = pointer.substring(0, slash);
+ }
+ final String byMessageKey = CATALOGUE.keywords().get(message.getMessageKey());
+ return byMessageKey != null ? byMessageKey : CATALOGUE.keywords().get(keyword);
+ }
+
+ /**
+ * A keyword template by name, for synthetic messages such as required.alternatives.
+ */
+ public static String keyword(final String name) {
+ return CATALOGUE.keywords().get(name);
+ }
+
+ /**
+ * Whether a keyword template exists, so callers can choose a different strategy if not.
+ */
+ public static boolean hasKeyword(final String name) {
+ return CATALOGUE.keywords().containsKey(name);
+ }
+
+ /**
+ * A reusable sentence piece, with a hard coded fallback so wording is never missing entirely.
+ */
+ public static String fragment(final String name, final String fallback) {
+ final String value = CATALOGUE.fragments().get(name);
+ return value != null ? value : fallback;
+ }
+
+ /**
+ * Maps an XSD rule code such as cvc-enumeration-valid to the type field of the response.
+ */
+ public static String xsdType(final String code) {
+ return CATALOGUE.xsdTypes().get(code);
+ }
+
+ /**
+ * Fills a template and tidies the finished sentence. Null template gives null, not a crash.
+ */
+ public static String render(final String template, final Map values) {
+ return template == null ? null : tidy(fill(template, values));
+ }
+
+ /**
+ * Collapses runs of spaces left behind by empty placeholders and trims the ends.
+ */
+ public static String tidy(final String message) {
+ if (message == null) {
+ return null;
+ }
+ final StringBuilder out = new StringBuilder(message.length());
+ boolean previousWasSpace = false;
+ for (int i = 0; i < message.length(); i++) {
+ final char c = message.charAt(i);
+ final boolean isSpace = c == ' ' || c == '\t';
+ if (isSpace) {
+ if (!previousWasSpace) {
+ out.append(' '); // keep the first space of a run, drop the rest
+ }
+ } else {
+ out.append(c);
+ }
+ previousWasSpace = isSpace;
+ }
+ int start = 0; // trim leading spaces without allocating another String
+ int end = out.length();
+ while (start < end && out.charAt(start) == ' ') {
+ start++;
+ }
+ while (end > start && out.charAt(end - 1) == ' ') {
+ end--;
+ }
+ return out.substring(start, end);
+ }
+
+ /**
+ * Replaces every {name} with its value. An unknown placeholder becomes empty.
+ */
+ public static String fill(final String template, final Map values) {
+ if (template.indexOf('{') < 0) {
+ // nothing to substitute, so allocate nothing at all
+ return template;
+ }
+ final StringBuilder out = new StringBuilder(template.length() + 64);
+ int i = 0;
+ while (i < template.length()) {
+ final int open = template.indexOf('{', i);
+ if (open < 0) {
+ // no placeholder left, copy the tail
+ out.append(template, i, template.length());
+ break;
+ }
+ final int close = template.indexOf('}', open);
+ if (close < 0) {
+ // unbalanced brace, copy verbatim, never throw
+ out.append(template, i, template.length());
+ break;
+ }
+ // the literal text before the placeholder
+ out.append(template, i, open);
+ final String value = values.get(template.substring(open + 1, close));
+ if (value != null) {
+ // a missing value contributes nothing, tidy() closes the gap
+ out.append(value);
+ }
+ i = close + 1;
+ }
+ return out.toString();
+ }
+
+ private MessageTemplates() {
+ }
+}
diff --git a/core/src/main/java/io/openepcis/validation/message/ValueSuggester.java b/core/src/main/java/io/openepcis/validation/message/ValueSuggester.java
new file mode 100644
index 0000000..db21ce8
--- /dev/null
+++ b/core/src/main/java/io/openepcis/validation/message/ValueSuggester.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.message;
+
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.similarity.LevenshteinDistance;
+
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Finds the closest allowed value to what the user typed, so we can offer a "did you mean" option in the validation error message.
+ */
+public class ValueSuggester {
+ private static final LevenshteinDistance LEVENSHTEIN = LevenshteinDistance.getDefaultInstance();
+
+ /**
+ * Returns the closest candidate or null when nothing is close enough. Uses distance and LEVENSHTEIN.
+ */
+ public static String closest(final String wrongValue, final List candidates) {
+ if (StringUtils.isBlank(wrongValue) || CollectionUtils.isEmpty(candidates)) {
+ return null;
+ }
+ final String needle = wrongValue.toLowerCase(Locale.ROOT);
+ final int needleLength = needle.length();
+ final int maxDistance = Math.max(1, needleLength / 3);
+ String best = null;
+ int bestDistance = Integer.MAX_VALUE;
+ for (final String candidate : candidates) {
+ // Combined guard checks using short-circuit logic instead of separate continue statements
+ if (StringUtils.isNotBlank(candidate) && Math.abs(needleLength - candidate.length()) <= maxDistance) {
+ // Execution: Reuses the stateless global engine
+ final int distance = LEVENSHTEIN.apply(needle, candidate.toLowerCase(Locale.ROOT));
+ if (distance < bestDistance) {
+ bestDistance = distance;
+ best = candidate;
+ // Performance optimization: Stop searching immediately upon an exact match
+ if (bestDistance == 0) {
+ return best;
+ }
+ }
+ }
+ }
+ return bestDistance <= maxDistance ? best : null;
+ }
+
+ private ValueSuggester() {
+ }
+}
diff --git a/core/src/main/java/io/openepcis/validation/model/ValidationError.java b/core/src/main/java/io/openepcis/validation/model/ValidationError.java
index ee56633..4b60cee 100644
--- a/core/src/main/java/io/openepcis/validation/model/ValidationError.java
+++ b/core/src/main/java/io/openepcis/validation/model/ValidationError.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,29 +15,246 @@
*/
package io.openepcis.validation.model;
-import jakarta.xml.bind.annotation.XmlAttribute;
+import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlElementWrapper;
import jakarta.xml.bind.annotation.XmlRootElement;
import jakarta.xml.bind.annotation.XmlType;
-import lombok.Data;
+import org.apache.commons.lang3.StringUtils;
-@Data
+import java.util.List;
+import java.util.regex.Pattern;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
@XmlRootElement(name = "ValidationError")
-@XmlType(
- name = "ValidationError",
- propOrder = {"type", "line", "location", "message"})
+@XmlType(name = "ValidationError", propOrder = {"type", "line", "eventIndex", "eventType", "column", "location", "schemaPath", "message", "field", "value", "suggestion", "allowedValues"})
public class ValidationError {
+ @XmlElement
+ private String type;
+ // XML only: line number in submitted document
+ @XmlElement
+ private Integer line;
+ // XML only: column number of the error
+ @XmlElement
+ private Integer column;
+ // JSON only: problem in document like $.epcisBody.eventList[5].bizStep
+ @XmlElement
+ private String location;
+ // Which schema rules got rejected, for diagnostics only not for the display on UI
+ @XmlElement
+ private String schemaPath;
+ @XmlElement
+ private String message;
+ // Indicate which field failed
+ @XmlElement
+ private String field;
+ // User provided values
+ @XmlElement
+ private String value;
+ // zero based event index
+ @XmlElement
+ private Integer eventIndex;
+ // eventType for quick reference to users
+ @XmlElement
+ private String eventType;
+ // closest standard value, or null for suggesting to user
+ @XmlElement
+ private String suggestion;
+ // full list as data, UI can collapse it
+ @XmlElementWrapper(name = "allowedValues")
+ @XmlElement(name = "allowedValue")
+ private List allowedValues;
+ // Strips only a leading XSD rule code such as "cvc-complex-type.2.4.a: ".
+ private static final Pattern XSD_ERROR_CODE = Pattern.compile("^(cvc|src|sch)-[A-Za-z0-9_.\\-]+:\\s*");
+
+ public String filterErrorMessage(final String message) {
+ if (StringUtils.isBlank(message)) {
+ return message;
+ }
+ return XSD_ERROR_CODE.matcher(message).replaceFirst("");
+ }
+
+ public ValidationError() {
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public Integer getLine() {
+ return this.line;
+ }
+
+ public Integer getColumn() {
+ return this.column;
+ }
+
+ public String getLocation() {
+ return this.location;
+ }
+
+ public String getSchemaPath() {
+ return this.schemaPath;
+ }
+
+ public String getMessage() {
+ return this.message;
+ }
+
+ public String getField() {
+ return this.field;
+ }
+
+ public String getValue() {
+ return this.value;
+ }
+
+ public Integer getEventIndex() {
+ return this.eventIndex;
+ }
+
+ public String getEventType() {
+ return this.eventType;
+ }
+
+ public String getSuggestion() {
+ return this.suggestion;
+ }
+
+ public List getAllowedValues() {
+ return this.allowedValues;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public void setLine(Integer line) {
+ this.line = line;
+ }
+
+ public void setColumn(Integer column) {
+ this.column = column;
+ }
+
+ public void setLocation(String location) {
+ this.location = location;
+ }
+
+ public void setSchemaPath(String schemaPath) {
+ this.schemaPath = schemaPath;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ public void setField(String field) {
+ this.field = field;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public void setEventIndex(Integer eventIndex) {
+ this.eventIndex = eventIndex;
+ }
+
+ public void setEventType(String eventType) {
+ this.eventType = eventType;
+ }
+
+ public void setSuggestion(String suggestion) {
+ this.suggestion = suggestion;
+ }
- @XmlAttribute private String type;
+ public void setAllowedValues(List allowedValues) {
+ this.allowedValues = allowedValues;
+ }
- @XmlElement private String line;
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof ValidationError)) return false;
+ ValidationError other = (ValidationError) o;
+ if (!other.canEqual((Object) this)) return false;
+ Object this$line = this.getLine();
+ Object other$line = other.getLine();
+ if (this$line == null ? other$line != null : !this$line.equals(other$line)) return false;
+ Object this$column = this.getColumn();
+ Object other$column = other.getColumn();
+ if (this$column == null ? other$column != null : !this$column.equals(other$column)) return false;
+ Object this$eventIndex = this.getEventIndex();
+ Object other$eventIndex = other.getEventIndex();
+ if (this$eventIndex == null ? other$eventIndex != null : !this$eventIndex.equals(other$eventIndex)) return false;
+ Object this$type = this.getType();
+ Object other$type = other.getType();
+ if (this$type == null ? other$type != null : !this$type.equals(other$type)) return false;
+ Object this$location = this.getLocation();
+ Object other$location = other.getLocation();
+ if (this$location == null ? other$location != null : !this$location.equals(other$location)) return false;
+ Object this$schemaPath = this.getSchemaPath();
+ Object other$schemaPath = other.getSchemaPath();
+ if (this$schemaPath == null ? other$schemaPath != null : !this$schemaPath.equals(other$schemaPath)) return false;
+ Object this$message = this.getMessage();
+ Object other$message = other.getMessage();
+ if (this$message == null ? other$message != null : !this$message.equals(other$message)) return false;
+ Object this$field = this.getField();
+ Object other$field = other.getField();
+ if (this$field == null ? other$field != null : !this$field.equals(other$field)) return false;
+ Object this$value = this.getValue();
+ Object other$value = other.getValue();
+ if (this$value == null ? other$value != null : !this$value.equals(other$value)) return false;
+ Object this$eventType = this.getEventType();
+ Object other$eventType = other.getEventType();
+ if (this$eventType == null ? other$eventType != null : !this$eventType.equals(other$eventType)) return false;
+ Object this$suggestion = this.getSuggestion();
+ Object other$suggestion = other.getSuggestion();
+ if (this$suggestion == null ? other$suggestion != null : !this$suggestion.equals(other$suggestion)) return false;
+ Object this$allowedValues = this.getAllowedValues();
+ Object other$allowedValues = other.getAllowedValues();
+ if (this$allowedValues == null ? other$allowedValues != null : !this$allowedValues.equals(other$allowedValues)) return false;
+ return true;
+ }
- @XmlElement private String location;
+ protected boolean canEqual(Object other) {
+ return other instanceof ValidationError;
+ }
- @XmlElement private String message;
+ @Override
+ public int hashCode() {
+ int PRIME = 59;
+ int result = 1;
+ Object $line = this.getLine();
+ result = result * PRIME + ($line == null ? 43 : $line.hashCode());
+ Object $column = this.getColumn();
+ result = result * PRIME + ($column == null ? 43 : $column.hashCode());
+ Object $eventIndex = this.getEventIndex();
+ result = result * PRIME + ($eventIndex == null ? 43 : $eventIndex.hashCode());
+ Object $type = this.getType();
+ result = result * PRIME + ($type == null ? 43 : $type.hashCode());
+ Object $location = this.getLocation();
+ result = result * PRIME + ($location == null ? 43 : $location.hashCode());
+ Object $schemaPath = this.getSchemaPath();
+ result = result * PRIME + ($schemaPath == null ? 43 : $schemaPath.hashCode());
+ Object $message = this.getMessage();
+ result = result * PRIME + ($message == null ? 43 : $message.hashCode());
+ Object $field = this.getField();
+ result = result * PRIME + ($field == null ? 43 : $field.hashCode());
+ Object $value = this.getValue();
+ result = result * PRIME + ($value == null ? 43 : $value.hashCode());
+ Object $eventType = this.getEventType();
+ result = result * PRIME + ($eventType == null ? 43 : $eventType.hashCode());
+ Object $suggestion = this.getSuggestion();
+ result = result * PRIME + ($suggestion == null ? 43 : $suggestion.hashCode());
+ Object $allowedValues = this.getAllowedValues();
+ result = result * PRIME + ($allowedValues == null ? 43 : $allowedValues.hashCode());
+ return result;
+ }
- public String filterErrorMessage(String message) {
- String[] messageParts = message.split(":", 2);
- return messageParts.length > 1 ? messageParts[1].trim() : message;
- }
+ @Override
+ public String toString() {
+ return "ValidationError(type=" + this.getType() + ", line=" + this.getLine() + ", column=" + this.getColumn() + ", location=" + this.getLocation() + ", schemaPath=" + this.getSchemaPath() + ", message=" + this.getMessage() + ", field=" + this.getField() + ", value=" + this.getValue() + ", eventIndex=" + this.getEventIndex() + ", eventType=" + this.getEventType() + ", suggestion=" + this.getSuggestion() + ", allowedValues=" + this.getAllowedValues() + ")";
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/model/xml/ValidationResult.java b/core/src/main/java/io/openepcis/validation/model/xml/ValidationResult.java
index 7c613dc..5a293be 100644
--- a/core/src/main/java/io/openepcis/validation/model/xml/ValidationResult.java
+++ b/core/src/main/java/io/openepcis/validation/model/xml/ValidationResult.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,62 @@
package io.openepcis.validation.model.xml;
import io.openepcis.validation.model.ValidationError;
-import jakarta.xml.bind.annotation.*;
+import jakarta.xml.bind.annotation.XmlElement;
+import jakarta.xml.bind.annotation.XmlElementWrapper;
+import jakarta.xml.bind.annotation.XmlRootElement;
+import jakarta.xml.bind.annotation.XmlType;
+
import java.util.List;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
@XmlRootElement(name = "ValidationResult")
@XmlType(name = "ValidationResult")
-@NoArgsConstructor
-@AllArgsConstructor
-@Data
public class ValidationResult {
-
@XmlElementWrapper(name = "errors")
@XmlElement(name = "ValidationError")
private List errors;
+
+ public ValidationResult() {
+ }
+
+ public ValidationResult(List errors) {
+ this.errors = errors;
+ }
+
+ public List getErrors() {
+ return this.errors;
+ }
+
+ public void setErrors(List errors) {
+ this.errors = errors;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) return true;
+ if (!(o instanceof ValidationResult)) return false;
+ ValidationResult other = (ValidationResult) o;
+ if (!other.canEqual((Object) this)) return false;
+ Object this$errors = this.getErrors();
+ Object other$errors = other.getErrors();
+ if (this$errors == null ? other$errors != null : !this$errors.equals(other$errors)) return false;
+ return true;
+ }
+
+ protected boolean canEqual(Object other) {
+ return other instanceof ValidationResult;
+ }
+
+ @Override
+ public int hashCode() {
+ int PRIME = 59;
+ int result = 1;
+ Object $errors = this.getErrors();
+ result = result * PRIME + ($errors == null ? 43 : $errors.hashCode());
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "ValidationResult(errors=" + this.getErrors() + ")";
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/AbstractXmlSchemaValidator.java b/core/src/main/java/io/openepcis/validation/xml/AbstractXmlSchemaValidator.java
index 01b5b13..319083e 100644
--- a/core/src/main/java/io/openepcis/validation/xml/AbstractXmlSchemaValidator.java
+++ b/core/src/main/java/io/openepcis/validation/xml/AbstractXmlSchemaValidator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,78 +16,75 @@
package io.openepcis.validation.xml;
import io.openepcis.validation.Validator;
+import io.openepcis.validation.config.ValidationDefaults;
import io.openepcis.validation.exception.SchemaValidationException;
import io.openepcis.validation.model.ValidationError;
import io.smallrye.mutiny.Multi;
-import java.io.IOException;
-import java.io.InputStream;
+import org.xml.sax.SAXException;
+
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
-import org.xml.sax.SAXException;
+import java.io.IOException;
+import java.io.InputStream;
public abstract class AbstractXmlSchemaValidator implements Validator {
- protected Schema captureXmlSchema;
- protected Schema queryXmlSchema;
- protected EventModifier eventModifier = new EventModifier();
- protected XmlErrorHandler xsdErrorHandler;
- protected SchemaFactory schemaFactory;
+ protected int maxReportedErrors = ValidationDefaults.MAX_REPORTED_ERRORS;
+ protected Schema captureXmlSchema;
+ protected Schema queryXmlSchema;
+ protected EventModifier eventModifier = new EventModifier();
+ protected SchemaFactory schemaFactory;
+
+ @Override
+ public Multi validateAgainstCaptureSchema(final InputStream captureInput) {
+ return Multi.createFrom()
+ .emitter(
+ multiEmitter -> {
+ try {
+ final InputStream inputEvent = eventModifier.modifyEvent(captureInput);
+ final javax.xml.validation.Validator validator = captureXmlSchema.newValidator();
+ // Fresh handler to avoid reused validation polluting with old errors
+ final XmlErrorHandler errorHandler = new XmlErrorHandler(maxReportedErrors);
- @Override
- public Multi validateAgainstCaptureSchema(final InputStream captureInput) {
- return Multi.createFrom()
- .emitter(
- multiEmitter -> {
- try {
- final InputStream inputEvent = eventModifier.modifyEvent(captureInput);
- final javax.xml.validation.Validator validator = captureXmlSchema.newValidator();
- validator.setErrorHandler(this.xsdErrorHandler);
- validator.validate(new StreamSource(inputEvent));
- this.xsdErrorHandler.getExceptions().forEach(multiEmitter::emit);
- multiEmitter.complete();
- } catch (Exception e) {
- multiEmitter.fail(
- new SchemaValidationException(
- "Exception occurred during the validation of Capture XML document/event against XSD : "
- + e.getMessage(),
- e));
- }
- });
- }
+ validator.setErrorHandler(errorHandler);
+ validator.validate(new StreamSource(inputEvent));
+ errorHandler.getExceptions().forEach(multiEmitter::emit);
+ multiEmitter.complete();
+ } catch (Exception e) {
+ multiEmitter.fail(new SchemaValidationException("Failed to validate the Capture XML document against the XSD : " + e.getMessage(), e));
+ }
+ });
+ }
- @Override
- public Multi validateAgainstQuerySchema(InputStream queryInput)
- throws SchemaValidationException {
- return Multi.createFrom()
- .emitter(
- multiEmitter -> {
- final javax.xml.validation.Validator validator = queryXmlSchema.newValidator();
- validator.setErrorHandler(this.xsdErrorHandler);
- try {
- validator.validate(new StreamSource(queryInput));
- this.xsdErrorHandler.getExceptions().forEach(multiEmitter::emit);
- multiEmitter.complete();
- } catch (SAXException | IOException e) {
- multiEmitter.fail(
- new SchemaValidationException(
- "Exception occurred during the validation of Query XML document/event against XSD : "
- + e.getMessage()
- + e));
- }
- });
- }
+ @Override
+ public Multi validateAgainstQuerySchema(InputStream queryInput)
+ throws SchemaValidationException {
+ return Multi.createFrom()
+ .emitter(
+ multiEmitter -> {
+ final javax.xml.validation.Validator validator = queryXmlSchema.newValidator();
+ // Fresh handler to avoid reused validation polluting with old errors
+ final XmlErrorHandler errorHandler = new XmlErrorHandler(maxReportedErrors);
+ validator.setErrorHandler(errorHandler);
+
+ try {
+ validator.validate(new StreamSource(queryInput));
+ errorHandler.getExceptions().forEach(multiEmitter::emit);
+ multiEmitter.complete();
+ } catch (SAXException | IOException e) {
+ multiEmitter.fail(new SchemaValidationException("Failed to validate the Query XML document against the XSD : " + e.getMessage(), e));
+ }
+ });
+ }
- protected Schema loadSchema(final String name) {
- Schema schema;
- try {
- schema = schemaFactory.newSchema(new StreamSource(getClass().getResourceAsStream(name)));
- } catch (Exception e) {
- throw new SchemaValidationException(
- "Exception occurred during the loading of the XSD document to schema : "
- + e.getMessage()
- + e);
+ protected Schema loadSchema(final String name) {
+ Schema schema;
+ try {
+ schema = schemaFactory.newSchema(new StreamSource(getClass().getResourceAsStream(name)));
+ } catch (Exception e) {
+ throw new SchemaValidationException("Failed to load XSD schema : " + e.getMessage(), e);
+ }
+ return schema;
}
- return schema;
- }
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/EventModifier.java b/core/src/main/java/io/openepcis/validation/xml/EventModifier.java
index 0220c76..b0df467 100644
--- a/core/src/main/java/io/openepcis/validation/xml/EventModifier.java
+++ b/core/src/main/java/io/openepcis/validation/xml/EventModifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,176 +15,162 @@
*/
package io.openepcis.validation.xml;
+import io.openepcis.constants.EPCIS;
+import io.openepcis.constants.EPCISVersion;
import io.openepcis.validation.exception.SchemaValidationException;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.xml.stream.*;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.time.Instant;
-import java.util.Arrays;
-import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.regex.Pattern;
-import javax.xml.stream.*;
public class EventModifier {
- private final ExecutorService executorService;
- private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
- private final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
- private static final List EPCIS_EVENT_TYPES =
- Arrays.asList(
- "ObjectEvent",
- "AggregationEvent",
- "TransactionEvent",
- "TransformationEvent",
- "AssociationEvent");
-
- private static final String EPCIS_DOCUMENT_NAMESPACE = "urn:epcglobal:epcis:xsd:2";
- private static final String EPCIS = "epcis";
- private static final String FIRST_TAG_REGEX = "<(?!\\?|\\!--)([\\S\\s]*?)>";
- private static final Pattern FIRST_TAG_PATTERN = Pattern.compile(FIRST_TAG_REGEX);
-
- public EventModifier(final ExecutorService executorService) {
- this.executorService = executorService;
- }
-
- public EventModifier() {
- this(Executors.newWorkStealingPool());
- }
-
- public InputStream modifyEvent(final InputStream input) {
- final BufferedInputStream captureInput =
- BufferedInputStream.class.isAssignableFrom(input.getClass())
- ? (BufferedInputStream) input
- : new BufferedInputStream(input);
- try {
- final String preScan = XMLTagPreScanUtil.scanFirstTag(captureInput);
- final PipedOutputStream pipedOutputStream = new PipedOutputStream();
- final PipedInputStream pipe = new PipedInputStream(pipedOutputStream);
- executorService.execute(
- () -> {
- try {
- copy(captureInput, pipedOutputStream);
- } catch (Exception e) {
- throw new SchemaValidationException(
- "Exception occurred during reading of schema version from input document : "
- + e.getMessage(),
- e);
- }
- });
-
- if (preScan.contains("EPCISDocument")) {
- return pipe;
- } else if (EPCIS_EVENT_TYPES.stream().anyMatch(preScan::contains)) {
- return addDocumentWrapper(pipe); // method to add the epcis document wrapper to single event
- }
- } catch (Exception e) {
- e.printStackTrace();
+ private final ExecutorService executorService;
+ private final XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+ private final XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
+
+ public EventModifier(final ExecutorService executorService) {
+ this.executorService = executorService;
}
- return captureInput;
- }
-
- public void copy(final InputStream inputStream, final OutputStream outputStream) {
- try (ReadableByteChannel src = Channels.newChannel(inputStream);
- WritableByteChannel dest = Channels.newChannel(outputStream)) {
- final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
- while (src.read(buffer) != -1) {
- buffer.flip();
- dest.write(buffer);
- buffer.compact();
- }
- buffer.flip();
- while (buffer.hasRemaining()) {
- dest.write(buffer);
- }
- } catch (Exception e) {
- throw new SchemaValidationException(
- "Exception occurred during the copy of OutputStream to InputStream : " + e.getMessage(),
- e);
+
+ public EventModifier() {
+ this(Executors.newWorkStealingPool());
}
- }
-
- // Add the document wrapper to single event
- public InputStream addDocumentWrapper(final InputStream captureInput) {
- try {
- final XMLStreamReader reader = inputFactory.createXMLStreamReader(captureInput);
- final PipedOutputStream pipedOutputStream = new PipedOutputStream();
- final PipedInputStream pipe = new PipedInputStream(pipedOutputStream);
-
- executorService.execute(
- () -> {
- try {
- XMLStreamWriter writer = outputFactory.createXMLStreamWriter(pipedOutputStream);
-
- writer.writeStartDocument();
-
- writer.writeStartElement(EPCIS, "EPCISDocument", EPCIS_DOCUMENT_NAMESPACE);
- writer.writeAttribute("creationDate", Instant.now().toString());
- writer.writeAttribute("schemaVersion", "2.0");
- writer.writeNamespace(EPCIS, EPCIS_DOCUMENT_NAMESPACE);
- writer.writeNamespace("xsi", "http://www.w3.org/2001/XMLSchema-instance");
-
- writer.writeStartElement("EPCISBody");
- writer.writeStartElement("EventList");
-
- // read the event and append to writer
- copyXmlEvent(reader, writer);
-
- writer.writeEndElement(); // end EventList
- writer.writeEndElement(); // end EPCISBody
- writer.writeEndElement(); // end EPCISDocument
-
- writer.writeEndDocument();
- pipedOutputStream.close();
- } catch (Exception e) {
- try {
- pipedOutputStream.write(e.getMessage().getBytes());
- pipedOutputStream.close();
- throw new SchemaValidationException(
- "Exception occurred during the adding of document wrapper to EPCIS event : "
- + e.getMessage(),
- e);
- } catch (IOException ex) {
- ex.printStackTrace();
- }
+
+ public InputStream modifyEvent(final InputStream input) {
+ final BufferedInputStream captureInput = BufferedInputStream.class.isAssignableFrom(input.getClass())
+ ? (BufferedInputStream) input
+ : new BufferedInputStream(input);
+ try {
+ // scanFirstTag to determine if a single event or an epcis document. Checks the first element with mark/reset, so captureInput stays readable
+ final String preScan = XMLTagPreScanUtil.scanFirstTag(captureInput);
+
+ // Only for bare XML event document wrapper needs to be added rest return as is
+ if (EPCIS.EPCIS_EVENT_TYPES.stream().anyMatch(preScan::contains) && !preScan.contains(EPCIS.EPCIS_DOCUMENT) && !preScan.contains(EPCIS.EPCIS_QUERY_DOCUMENT)) {
+ // Only bareEvent so add wrapper
+ return addDocumentWrapper(captureInput);
}
- });
- return pipe;
- } catch (Exception e) {
- throw new SchemaValidationException(
- "Exception occurred during the addition of Document wrapper to event : " + e.getMessage(),
- e);
+
+ // for EPCISDocument or EPCISQueryDocument return as is
+ return captureInput;
+ } catch (Exception e) {
+ throw new SchemaValidationException("Exception occurred during the modification of EPCIS event : " + e.getMessage(), e);
+ }
}
- }
-
- private void copyXmlEvent(final XMLStreamReader reader, final XMLStreamWriter writer) {
- try {
- while (reader.hasNext()) {
- int event = reader.next();
-
- if (event == XMLStreamConstants.START_ELEMENT) {
- writer.writeStartElement(reader.getLocalName());
-
- // Add all the attributes associated with xml tag
- for (int i = 0; i < reader.getAttributeCount(); i++) {
- writer.writeAttribute(reader.getAttributeLocalName(i), reader.getAttributeValue(i));
- }
-
- // Add all the namespaces associated with xml tag
- for (int i = 0; i < reader.getNamespaceCount(); i++) {
- writer.writeNamespace(reader.getNamespacePrefix(i), reader.getNamespaceURI(i));
- }
-
- } else if (event == XMLStreamConstants.END_ELEMENT) {
- writer.writeEndElement();
- } else if (event == XMLStreamConstants.CHARACTERS) {
- writer.writeCharacters(reader.getText());
+
+ public void copy(final InputStream inputStream, final OutputStream outputStream) {
+ try (ReadableByteChannel src = Channels.newChannel(inputStream);
+ WritableByteChannel dest = Channels.newChannel(outputStream)) {
+ final ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
+ while (src.read(buffer) != -1) {
+ buffer.flip();
+ dest.write(buffer);
+ buffer.compact();
+ }
+ buffer.flip();
+ while (buffer.hasRemaining()) {
+ dest.write(buffer);
+ }
+ } catch (Exception e) {
+ throw new SchemaValidationException("Exception occurred during the copy of OutputStream to InputStream : " + e.getMessage(), e);
+ }
+ }
+
+ // Add the document wrapper to single event
+ public InputStream addDocumentWrapper(final InputStream captureInput) {
+ try {
+ final XMLStreamReader reader = inputFactory.createXMLStreamReader(captureInput);
+ final PipedOutputStream pipedOutputStream = new PipedOutputStream();
+ final PipedInputStream pipe = new PipedInputStream(pipedOutputStream);
+
+ executorService.execute(
+ () -> {
+ try {
+ XMLStreamWriter writer = outputFactory.createXMLStreamWriter(pipedOutputStream);
+
+ writer.writeStartDocument();
+
+ writer.writeStartElement(EPCIS.EPCIS_NAMESPACE_PREFIX, EPCIS.EPCIS_DOCUMENT, EPCIS.EPCIS_2_0_XMLNS);
+ writer.writeAttribute(EPCIS.CREATION_DATE, Instant.now().toString());
+ writer.writeAttribute(EPCIS.SCHEMA_VERSION, EPCISVersion.VERSION_2_0_0.getSchemaVersion());
+ writer.writeNamespace(EPCIS.EPCIS_NAMESPACE_PREFIX, EPCIS.EPCIS_2_0_XMLNS);
+ writer.writeNamespace(EPCIS.XSI, EPCIS.XML_SCHEMA_INSTANCE);
+
+ writer.writeStartElement(EPCIS.EPCIS_BODY);
+ writer.writeStartElement(EPCIS.EVENT_LIST);
+
+ // read the event and append to writer
+ copyXmlEvent(reader, writer);
+
+ writer.writeEndElement(); // end EventList
+ writer.writeEndElement(); // end EPCISBody
+ writer.writeEndElement(); // end EPCISDocument
+
+ writer.writeEndDocument();
+ pipedOutputStream.close();
+ } catch (Exception e) {
+ try {
+ pipedOutputStream.write(e.getMessage().getBytes());
+ pipedOutputStream.close();
+ throw new SchemaValidationException("Exception occurred during the adding of document wrapper to EPCIS event : " + e.getMessage(), e);
+ } catch (IOException ex) {
+ throw new SchemaValidationException("Exception occurred during the adding of document wrapper to EPCIS event : " + ex.getMessage(), ex);
+ }
+ }
+ });
+ return pipe;
+ } catch (Exception e) {
+ throw new SchemaValidationException("Exception occurred during the addition of Document wrapper to event : " + e.getMessage(), e);
+ }
+ }
+
+ private void copyXmlEvent(final XMLStreamReader reader, final XMLStreamWriter writer) {
+ try {
+ while (reader.hasNext()) {
+ int event = reader.next();
+
+ if (event == XMLStreamConstants.START_ELEMENT) {
+ // Get namespace of the element Null or empty means direct event (not
+ final String elementNs = reader.getNamespaceURI();
+
+ // if empty directly add unqualified name
+ if (StringUtils.isBlank(elementNs)) {
+ writer.writeStartElement(reader.getLocalName());
+ } else {
+ final String prefix = reader.getPrefix();
+ writer.writeStartElement(prefix == null ? "" : prefix, reader.getLocalName(), elementNs);
+ }
+
+ // Add all the namespaces associated with xml tag
+ for (int i = 0; i < reader.getNamespaceCount(); i++) {
+ final String nsPrefix = reader.getNamespacePrefix(i);
+ writer.writeNamespace(nsPrefix == null ? "" : nsPrefix, reader.getNamespaceURI(i));
+ }
+
+ // Add all the attributes associated with xml tag
+ for (int i = 0; i < reader.getAttributeCount(); i++) {
+ final String attrNs = reader.getAttributeNamespace(i);
+
+ if (StringUtils.isEmpty(attrNs)) {
+ writer.writeAttribute(reader.getAttributeLocalName(i), reader.getAttributeValue(i));
+ } else {
+ writer.writeAttribute(reader.getAttributePrefix(i), attrNs, reader.getAttributeLocalName(i), reader.getAttributeValue(i));
+ }
+ }
+ } else if (event == XMLStreamConstants.END_ELEMENT) {
+ writer.writeEndElement();
+ } else if (event == XMLStreamConstants.CHARACTERS) {
+ writer.writeCharacters(reader.getText());
+ }
+ }
+ } catch (Exception e) {
+ throw new SchemaValidationException("Exception occurred during the copying of EPCIS event : " + e.getMessage(), e);
}
- }
- } catch (Exception e) {
- e.printStackTrace();
}
- }
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/XMLTagPreScanUtil.java b/core/src/main/java/io/openepcis/validation/xml/XMLTagPreScanUtil.java
index e37d9c7..c304770 100644
--- a/core/src/main/java/io/openepcis/validation/xml/XMLTagPreScanUtil.java
+++ b/core/src/main/java/io/openepcis/validation/xml/XMLTagPreScanUtil.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,29 +23,33 @@
public class XMLTagPreScanUtil {
- private static final String FIRST_TAG_REGEX = "<(?!\\?|\\!--)([\\S\\s]*?)>";
- private static final Pattern FIRST_TAG_PATTERN = Pattern.compile(FIRST_TAG_REGEX);
- private static final int READ_LIMIT = 4096;
-
- public static final String scanFirstTag(final BufferedInputStream input) throws IOException {
- input.mark(READ_LIMIT);
- try {
- final StringBuilder sb = new StringBuilder();
- final byte[] buffer = new byte[64];
- int len = -1;
- int bytesReceived = 0;
- Matcher matcher = FIRST_TAG_PATTERN.matcher(sb.toString());
- while (!matcher.find(0) && bytesReceived < READ_LIMIT && (len = input.read(buffer)) != -1) {
- sb.append(new String(buffer, 0, len, StandardCharsets.UTF_8));
- bytesReceived += len;
- matcher = FIRST_TAG_PATTERN.matcher(sb.toString());
- }
- if (matcher.find(0)) {
- return matcher.group(0);
- }
- return "";
- } finally {
- input.reset();
+ private static final String FIRST_TAG_REGEX = "<(?!\\?|\\!--)([\\S\\s]*?)>";
+ private static final Pattern FIRST_TAG_PATTERN = Pattern.compile(FIRST_TAG_REGEX);
+ private static final int READ_LIMIT = 4096;
+
+ public static String scanFirstTag(final BufferedInputStream input) throws IOException {
+ input.mark(READ_LIMIT);
+
+ try {
+ final StringBuilder sb = new StringBuilder();
+ final byte[] buffer = new byte[64];
+ int len = -1;
+ int bytesReceived = 0;
+ Matcher matcher = FIRST_TAG_PATTERN.matcher(sb.toString());
+
+ while (!matcher.find(0) && bytesReceived < READ_LIMIT && (len = input.read(buffer)) != -1) {
+ sb.append(new String(buffer, 0, len, StandardCharsets.UTF_8));
+ bytesReceived += len;
+ matcher = FIRST_TAG_PATTERN.matcher(sb.toString());
+ }
+
+ if (matcher.find(0)) {
+ return matcher.group(0);
+ }
+
+ return "";
+ } finally {
+ input.reset();
+ }
}
- }
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/Xml12SchemaValidator.java b/core/src/main/java/io/openepcis/validation/xml/Xml12SchemaValidator.java
index 6cf52d4..8d606ed 100644
--- a/core/src/main/java/io/openepcis/validation/xml/Xml12SchemaValidator.java
+++ b/core/src/main/java/io/openepcis/validation/xml/Xml12SchemaValidator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,17 +15,27 @@
*/
package io.openepcis.validation.xml;
+import io.openepcis.validation.config.ValidationDefaults;
import io.openepcis.validation.xml.resolver.ClasspathResourceResolver;
+
import javax.xml.XMLConstants;
import javax.xml.validation.SchemaFactory;
public class Xml12SchemaValidator extends AbstractXmlSchemaValidator {
- public Xml12SchemaValidator() {
- SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
- schemaFactory.setResourceResolver(new ClasspathResourceResolver("xsd/1_2_0"));
- this.schemaFactory = schemaFactory;
- this.captureXmlSchema = loadSchema("/xsd/1_2_0/EPCglobal-epcis-1_2.xsd");
- this.queryXmlSchema = loadSchema("/xsd/1_2_0/EPCglobal-epcis-query-1_2.xsd");
- this.xsdErrorHandler = new XmlErrorHandler();
- }
+
+
+ public Xml12SchemaValidator() {
+ // keep the existing callers and tests unchanged
+ this(ValidationDefaults.MAX_REPORTED_ERRORS); // keeps existing callers and tests unchanged
+ }
+
+
+ public Xml12SchemaValidator(final int maxReportedErrors) {
+ final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ schemaFactory.setResourceResolver(new ClasspathResourceResolver("xsd/1_2_0"));
+ this.schemaFactory = schemaFactory;
+ this.captureXmlSchema = loadSchema("/xsd/1_2_0/EPCglobal-epcis-1_2.xsd");
+ this.queryXmlSchema = loadSchema("/xsd/1_2_0/EPCglobal-epcis-query-1_2.xsd");
+ this.maxReportedErrors = maxReportedErrors;
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/Xml20SchemaValidator.java b/core/src/main/java/io/openepcis/validation/xml/Xml20SchemaValidator.java
index 89198bf..ddc5c11 100644
--- a/core/src/main/java/io/openepcis/validation/xml/Xml20SchemaValidator.java
+++ b/core/src/main/java/io/openepcis/validation/xml/Xml20SchemaValidator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,17 +15,27 @@
*/
package io.openepcis.validation.xml;
+import io.openepcis.validation.config.ValidationDefaults;
import io.openepcis.validation.xml.resolver.ClasspathResourceResolver;
+
import javax.xml.XMLConstants;
import javax.xml.validation.SchemaFactory;
public class Xml20SchemaValidator extends AbstractXmlSchemaValidator {
- public Xml20SchemaValidator() {
- SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
- schemaFactory.setResourceResolver(new ClasspathResourceResolver("xsd/2_0_0"));
- this.schemaFactory = schemaFactory;
- this.captureXmlSchema = loadSchema("/xsd/2_0_0/EPCglobal-epcis-2_0.xsd");
- this.queryXmlSchema = loadSchema("/xsd/2_0_0/EPCglobal-epcis-query-2_0.xsd");
- this.xsdErrorHandler = new XmlErrorHandler();
- }
+
+ public Xml20SchemaValidator() {
+ // keep the existing callers and tests unchanged
+ this(ValidationDefaults.MAX_REPORTED_ERRORS); // keeps existing callers and tests unchanged
+ }
+
+
+ public Xml20SchemaValidator(final int maxReportedErrors) {
+ final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+ schemaFactory.setResourceResolver(new ClasspathResourceResolver("xsd/2_0_0"));
+ this.schemaFactory = schemaFactory;
+ this.captureXmlSchema = loadSchema("/xsd/2_0_0/EPCglobal-epcis-2_0.xsd");
+ this.queryXmlSchema = loadSchema("/xsd/2_0_0/EPCglobal-epcis-query-2_0.xsd");
+ this.maxReportedErrors = maxReportedErrors;
+ }
+
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/XmlErrorHandler.java b/core/src/main/java/io/openepcis/validation/xml/XmlErrorHandler.java
index b186aeb..3cce594 100644
--- a/core/src/main/java/io/openepcis/validation/xml/XmlErrorHandler.java
+++ b/core/src/main/java/io/openepcis/validation/xml/XmlErrorHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,40 +15,161 @@
*/
package io.openepcis.validation.xml;
+import io.openepcis.validation.config.ValidationDefaults;
+import io.openepcis.validation.message.MessageTemplates;
+import io.openepcis.validation.message.ValueSuggester;
import io.openepcis.validation.model.ValidationError;
+import org.apache.commons.lang3.StringUtils;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXParseException;
-/** custom error handler while validating xml against xsd */
+import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * custom error handler while validating xml against xsd
+ */
public class XmlErrorHandler implements ErrorHandler {
- @lombok.Getter private final java.util.List exceptions;
-
- public XmlErrorHandler() {
- this.exceptions = new java.util.ArrayList<>();
- }
-
- @Override
- public void warning(final SAXParseException exception) {
- handleError(exception);
- }
-
- @Override
- public void error(final SAXParseException exception) {
- handleError(exception);
- }
-
- @Override
- public void fatalError(final SAXParseException exception) {
- handleError(exception);
- }
-
- public void handleError(SAXParseException e) {
- // This will allow the validation to continue even if there are errors.
- final ValidationError errorFormat = new ValidationError();
- errorFormat.setType("required");
- errorFormat.setLine(Integer.toString(e.getLineNumber()));
- errorFormat.setLocation(Integer.toString(e.getColumnNumber()));
- errorFormat.setMessage(errorFormat.filterErrorMessage(e.getMessage()));
- this.exceptions.add(errorFormat);
- }
+ private final List exceptions;
+ // Max number of validation errors that can be added
+ private final int maxReportedErrors;
+ // Pattern to extract the values from Xerces validation errors
+ private static final Pattern ENUM_VALUES = Pattern.compile("enumeration \'\\[([^\\]]*)]\'");
+ private static final Pattern FACET_VALUE = Pattern.compile("Value \'([^\']*)\' is not");
+ private static final Pattern DATATYPE_VALUE = Pattern.compile("\'([^\']*)\' is not a valid value for");
+ private static final Pattern VALUE_OF_ELEMENT = Pattern.compile("The value \'([^\']*)\' of element \'([^\']*)\' is not valid");
+ private static final Pattern ELEMENT_NAME = Pattern.compile("element \'\\{?[^\']*?[:\"]?([A-Za-z_][A-Za-z0-9_.\\-]*)}?\'");
+
+ public XmlErrorHandler() {
+ this(ValidationDefaults.MAX_REPORTED_ERRORS);
+ }
+
+ public XmlErrorHandler(final int maxReportedErrors) {
+ this.exceptions = new ArrayList<>();
+ // passed in from SchemaValidator, so core needs no configuration framework of its own
+ this.maxReportedErrors = maxReportedErrors > 0 ? maxReportedErrors : ValidationDefaults.MAX_REPORTED_ERRORS;
+ }
+
+ @Override
+ public void warning(final SAXParseException exception) {
+ handleError(exception);
+ }
+
+ @Override
+ public void error(final SAXParseException exception) {
+ handleError(exception);
+ }
+
+ @Override
+ public void fatalError(final SAXParseException exception) {
+ handleError(exception);
+ }
+
+ public void handleError(final SAXParseException e) {
+ if (this.exceptions.size() >= maxReportedErrors) {
+ return; // the marker is already the last entry, nothing further is recorded
+ }
+ if (this.exceptions.size() == maxReportedErrors - 1) {
+ this.exceptions.add(limitExceeded(maxReportedErrors - 1)); // same wording as the JSON path
+ return;
+ }
+ final String raw = e.getMessage() == null ? "" : e.getMessage(); // never NPE on a null message
+ final Integer line = e.getLineNumber() > 0 ? e.getLineNumber() : null;
+ final Integer column = e.getColumnNumber() > 0 ? e.getColumnNumber() : null;
+ // Xerces reports one mistake twice, for example "not facet-valid" then "value of element is not valid".
+ // Same line and column is the same mistake, so keep the first, more specific one.
+ for (final ValidationError existing : this.exceptions) {
+ if (Objects.equals(line, existing.getLine()) && Objects.equals(column, existing.getColumn())) {
+ // duplicate contains the values and messages so extract the values to include in errors
+ enrich(existing, raw);
+ return;
+ }
+ }
+ final ValidationError errorFormat = new ValidationError();
+ errorFormat.setType(typeOf(raw)); // a real type, instead of always reporting "required"
+ errorFormat.setLine(line); // unchanged, the front end uses this for XML highlighting
+ errorFormat.setColumn(column); // unchanged
+ errorFormat.setSchemaPath(schemaRuleOf(raw)); //the cvc code, for diagnostics
+ errorFormat.setMessage(errorFormat.filterErrorMessage(raw));
+ enrich(errorFormat, raw); // extract the values and field names from the raw message
+ this.exceptions.add(errorFormat);
+ }
+
+ //The leading W3C rule code, for example cvc-enumeration-valid, kept for diagnostics.
+ private String schemaRuleOf(final String raw) {
+ final int colon = raw.indexOf(':');
+ return colon > 0 ? raw.substring(0, colon) : null;
+ }
+
+ /**
+ * Maps the leading W3C XSD rule code to a response type via the catalogue. Longest match wins.
+ */
+ private String typeOf(final String raw) {
+ final int colon = raw.indexOf(':');
+ if (colon > 0) {
+ final String code = raw.substring(0, colon); // e.g. cvc-complex-type.2.4.a
+ for (int cut = code.length(); cut > 0; cut = code.lastIndexOf('.', cut - 1)) {
+ final String mapped = MessageTemplates.xsdType(code.substring(0, cut));
+ if (mapped != null) {
+ return mapped; // prefers cvc-complex-type.2.4 over cvc-complex-type
+ }
+ }
+ }
+ return "invalid"; // an honest default, better than claiming a field was required
+ }
+
+ // The last entry in errors when limit is exceeded, so a capped list is never mistaken for a complete one.
+ private ValidationError limitExceeded(final int limit) {
+ final ValidationError error = new ValidationError();
+ error.setType("limitExceeded");
+ error.setMessage(MessageTemplates.hasKeyword("limit.exceeded") ? MessageTemplates.render(MessageTemplates.keyword("limit.exceeded"), Map.of("limit", String.valueOf(limit))) : "Validation stopped after " + limit + " problems. This document has more.");
+ return error;
+ }
+
+ // Get the values matching the error model to populate in errors from the Xerces validation errors
+ private void enrich(final ValidationError error, final String raw) {
+ if (StringUtils.isBlank(error.getValue())) {
+ // User provided value which is invalid
+ error.setValue(firstGroup(FACET_VALUE, raw));
+ }
+ if (StringUtils.isBlank(error.getValue())) {
+ // 'not-a-date' is not a valid value for
+ error.setValue(firstGroup(DATATYPE_VALUE, raw));
+ }
+ final Matcher valueOfElement = VALUE_OF_ELEMENT.matcher(raw);
+ if (valueOfElement.find()) {
+ if (StringUtils.isBlank(error.getValue())) {
+ error.setValue(valueOfElement.group(1));
+ }
+ if (StringUtils.isBlank(error.getField())) {
+ error.setField(valueOfElement.group(2)); // the element name, only this phrasing has it
+ }
+ }
+ if (StringUtils.isBlank(error.getField())) {
+ error.setField(firstGroup(ELEMENT_NAME, raw));
+ }
+ if (error.getAllowedValues() == null) {
+ final String list = firstGroup(ENUM_VALUES, raw);
+ if (list != null) {
+ final List allowed = Arrays.stream(list.split(",")).map(String::trim).filter(value -> !value.isEmpty()).toList();
+ if (!allowed.isEmpty()) {
+ error.setAllowedValues(allowed);
+ }
+ }
+ }
+ // only worth suggesting once both halves are known, and never overwrite an existing one
+ if (error.getSuggestion() == null && error.getValue() != null && error.getAllowedValues() != null) {
+ error.setSuggestion(ValueSuggester.closest(error.getValue(), error.getAllowedValues()));
+ }
+ }
+
+ private String firstGroup(final Pattern pattern, final String raw) {
+ final Matcher matcher = pattern.matcher(raw);
+ return matcher.find() ? matcher.group(1) : null;
+ }
+
+ public List getExceptions() {
+ return this.exceptions;
+ }
}
diff --git a/core/src/main/java/io/openepcis/validation/xml/resolver/ClasspathResourceResolver.java b/core/src/main/java/io/openepcis/validation/xml/resolver/ClasspathResourceResolver.java
index f90759b..30b7e4f 100644
--- a/core/src/main/java/io/openepcis/validation/xml/resolver/ClasspathResourceResolver.java
+++ b/core/src/main/java/io/openepcis/validation/xml/resolver/ClasspathResourceResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,11 +15,12 @@
*/
package io.openepcis.validation.xml.resolver;
-import java.io.InputStream;
-import java.io.InputStreamReader;
import org.w3c.dom.ls.LSInput;
import org.w3c.dom.ls.LSResourceResolver;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
public class ClasspathResourceResolver implements LSResourceResolver {
private final String base_path;
diff --git a/core/src/main/java/io/openepcis/validation/xml/resolver/LSInputImpl.java b/core/src/main/java/io/openepcis/validation/xml/resolver/LSInputImpl.java
index 843f25c..4adf259 100644
--- a/core/src/main/java/io/openepcis/validation/xml/resolver/LSInputImpl.java
+++ b/core/src/main/java/io/openepcis/validation/xml/resolver/LSInputImpl.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,14 +15,11 @@
*/
package io.openepcis.validation.xml.resolver;
+import org.w3c.dom.ls.LSInput;
+
import java.io.InputStream;
import java.io.Reader;
-import lombok.Getter;
-import lombok.Setter;
-import org.w3c.dom.ls.LSInput;
-@Getter
-@Setter
public class LSInputImpl implements LSInput {
private Reader characterStream;
private InputStream byteStream;
@@ -37,4 +34,64 @@ public class LSInputImpl implements LSInput {
public boolean getCertifiedText() {
return certifiedText;
}
+
+ public Reader getCharacterStream() {
+ return this.characterStream;
+ }
+
+ public InputStream getByteStream() {
+ return this.byteStream;
+ }
+
+ public String getStringData() {
+ return this.stringData;
+ }
+
+ public String getSystemId() {
+ return this.systemId;
+ }
+
+ public String getPublicId() {
+ return this.publicId;
+ }
+
+ public String getBaseURI() {
+ return this.baseURI;
+ }
+
+ public String getEncoding() {
+ return this.encoding;
+ }
+
+ public void setCharacterStream(Reader characterStream) {
+ this.characterStream = characterStream;
+ }
+
+ public void setByteStream(InputStream byteStream) {
+ this.byteStream = byteStream;
+ }
+
+ public void setStringData(String stringData) {
+ this.stringData = stringData;
+ }
+
+ public void setSystemId(String systemId) {
+ this.systemId = systemId;
+ }
+
+ public void setPublicId(String publicId) {
+ this.publicId = publicId;
+ }
+
+ public void setBaseURI(String baseURI) {
+ this.baseURI = baseURI;
+ }
+
+ public void setEncoding(String encoding) {
+ this.encoding = encoding;
+ }
+
+ public void setCertifiedText(boolean certifiedText) {
+ this.certifiedText = certifiedText;
+ }
}
diff --git a/core/src/main/resources/json-schema/modified-query-schema.json b/core/src/main/resources/json-schema/modified-query-schema.json
index 154c76d..e76b810 100644
--- a/core/src/main/resources/json-schema/modified-query-schema.json
+++ b/core/src/main/resources/json-schema/modified-query-schema.json
@@ -1,1278 +1,960 @@
{
- "title": "EPCIS Query parameters",
- "type": "object",
- "properties": {
- "@context": {
- "anyOf": [
- {
- "type": "string",
- "format": "uri"
- },
- {
- "type": "object"
- },
- {
- "type": "array",
- "uniqueItems": true,
- "items": {
- "anyOf": [
- {
- "type": "string",
- "format": "uri"
- },
- {
- "type": "object"
- }
- ]
- }
- }
- ]
- },
- "eventType": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "ObjectEvent",
- "AggregationEvent",
- "AssociationEvent",
- "TransformationEvent",
- "TransactionEvent",
- "all"
- ]
- }
- },
- "GE_eventTime": {
- "type": "string",
- "format": "date-time"
- },
- "LT_eventTime": {
- "type": "string",
- "format": "date-time"
- },
- "GE_recordTime": {
- "type": "string",
- "format": "date-time"
- },
- "LT_recordTime": {
- "type": "string",
- "format": "date-time"
- },
- "EQ_action": {
- "type": "array",
- "items": {
- "type": "string",
- "enum": [
- "ADD",
- "OBSERVE",
- "DELETE"
- ]
- }
- },
- "EQ_bizStep": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_disposition": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_setPersistentDisposition": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_unsetPersistentDisposition": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_readPoint": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "WD_readPoint": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_bizLocation": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "WD_bizLocation": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_bizTransaction_type": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_source_type": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_destination_type": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_transformationID": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_epc": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_parentID": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_inputEPC": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_outputEPC": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_anyEPC": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_epcClass": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_inputEPCClass": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_outputEPCClass": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "MATCH_anyEPCClass": {
- "type": "array",
- "items": {
- "type": "string",
- "format": "uri"
- }
- },
- "EQ_quantity": {
- "type": "integer"
- },
- "GT_quantity": {
- "type": "integer"
- },
- "GE_quantity": {
- "type": "integer"
- },
- "LT_quantity": {
- "type": "integer"
- },
- "LE_quantity": {
- "type": "integer"
- },
- "EQ_eventID": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "GE_errorDeclaration_Time": {
- "type": "string",
- "format": "date-time"
- },
- "LT_errorDeclaration_Time": {
- "type": "string",
- "format": "date-time"
- },
- "EQ_errorReason": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_correctiveEventID": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "orderBy": {
- "type": "string"
- },
- "orderDirection": {
- "type": "string"
- },
- "eventCountLimit": {
- "type": "integer"
- },
- "maxEventCount": {
- "type": "integer"
- },
- "EXISTS_sensorElementList": {
- "type": "integer",
- "nullable": true
- },
- "GE_time": {
- "type": "string",
- "format": "date-time"
- },
- "LT_time": {
- "type": "string",
- "format": "date-time"
- },
- "GE_startTime": {
- "type": "string",
- "format": "date-time"
- },
- "LT_startTime": {
- "type": "string",
- "format": "date-time"
- },
- "GE_endTime": {
- "type": "string",
- "format": "date-time"
- },
- "LT_endTime": {
- "type": "string",
- "format": "date-time"
- },
- "EQ_type": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_deviceID": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_dataProcessingMethod": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_microorganism": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_chemicalSubstance": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_bizRules": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_stringValue": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_booleanValue": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_hexBinaryValue": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "EQ_uriValue": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "GE_sDev": {
- "type": "number"
- },
- "LT_sDev": {
- "type": "number"
- },
- "GE_percRank": {
- "type": "number"
- },
- "LT_percRank": {
- "type": "number"
- },
- "GE_percValue": {
- "type": "number"
- },
- "LT_percValue": {
- "type": "number"
- }
- },
- "patternProperties": {
- "^EQ_value_[A-Z0-9]{2,4}$": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "^GE_minValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LT_minValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GT_minValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LE_minValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GE_maxValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LT_maxValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GT_maxValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LE_maxValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GE_meanValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LT_meanValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GT_meanValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LE_meanValue_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GE_value_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LT_value_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GT_value_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LE_value_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^EQ_quantity_[A-Z0-9]{2,4}$": {
- "type": "array",
- "items": {
- "type": "number"
- }
- },
- "^GE_quantity_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LT_quantity_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^GT_quantity_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^LE_quantity_[A-Z0-9]{2,4}$": {
- "type": "number"
- },
- "^EQ_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "EPCIS Query parameters",
+ "$comment": "Describes a literal EPCIS query (GE_eventTime, EQ_action, MATCH_epc, orderBy), not a document. Taken from the official query-schema.json EPCIS-Query definition, flattened to the document root and carrying GS1's own definitions so every $ref resolves locally and no vocabulary is restated here. Do not hand edit: regenerate from https://ref.gs1.org/standards/epcis/query-schema.json",
+ "type": "object",
+ "properties": {
+ "@context": {
+ "$ref": "#/definitions/%40context"
+ },
+ "eventType": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/eventType"
}
- ]
- }
- ]
- },
- "^GT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LE_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "GE_eventTime": {
+ "$ref": "#/definitions/time"
+ },
+ "LT_eventTime": {
+ "$ref": "#/definitions/time"
+ },
+ "GE_recordTime": {
+ "$ref": "#/definitions/time"
+ },
+ "LT_recordTime": {
+ "$ref": "#/definitions/time"
+ },
+ "EQ_action": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/action"
}
- ]
- }
- ]
- },
- "^GT_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LE_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_INNER_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_bizStep": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/bizStep"
}
- ]
- }
- ]
- },
- "^GT_INNER_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_INNER_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_INNER_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LE_INNER_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_INNER_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "boolean"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_disposition": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/disposition"
}
- ]
- }
- ]
- },
- "^GT_INNER_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_INNER_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_INNER_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LE_INNER_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EXISTS_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^EXISTS_INNER_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^EXISTS_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^EXISTS_INNER_ILMD_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^HASATTR_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^EQ_ATTR_[a-z]\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^EQ_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_persistentDisposition_set": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/disposition"
}
- ]
- }
- ]
- },
- "^GT_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LE_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^GT_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^GE_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^LT_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^LE_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^EXISTS_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^EXISTS_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^EQ_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_persistentDisposition_unset": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/disposition"
}
- ]
- }
- ]
- },
- "^GT_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "number"
- }
- ]
- },
- "^LE_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_quantity": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/decimal"
}
- ]
- }
- ]
- },
- "^GT_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "number"
- }
- ]
- },
- "^LE_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "GT_quantity": {
+ "$ref": "#/definitions/decimal"
+ },
+ "GE_quantity": {
+ "$ref": "#/definitions/decimal"
+ },
+ "LT_quantity": {
+ "$ref": "#/definitions/decimal"
+ },
+ "LE_quantity": {
+ "$ref": "#/definitions/decimal"
+ },
+ "EQ_readPoint": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
}
- ]
- }
- ]
- },
- "^GT_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "number"
- }
- ]
- },
- "^LE_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "WD_readPoint": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
}
- ]
- }
- ]
- },
- "^GT_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "number"
- }
- ]
- },
- "^LE_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_bizLocation": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
}
- ]
- }
- ]
- },
- "^GT_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^LT_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "number"
- }
- ]
- },
- "^LE_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^EQ_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "array",
- "items": {
+ },
+ "WD_bizLocation": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_transformationID": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "MATCH_epc": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/id"
+ }
+ },
+ "MATCH_parentID": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/id"
+ }
+ },
+ "MATCH_inputEPC": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/id"
+ }
+ },
+ "MATCH_outputEPC": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/id"
+ }
+ },
+ "MATCH_anyEPC": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/id"
+ }
+ },
+ "MATCH_epcClass": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "MATCH_inputEPCClass": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "MATCH_outputEPCClass": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "MATCH_anyEPCClass": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_eventID": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/eventID"
+ }
+ },
+ "EXISTS_errorDeclaration": {
+ "type": "integer",
+ "nullable": true
+ },
+ "GE_errorDeclaration_Time": {
+ "$ref": "#/definitions/time"
+ },
+ "LT_errorDeclaration_Time": {
+ "$ref": "#/definitions/time"
+ },
+ "EQ_errorReason": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/error-reason"
+ }
+ },
+ "EQ_correctiveEventID": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/eventID"
+ }
+ },
+ "orderBy": {
"type": "string"
- }
- },
- {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "orderDirection": {
+ "type": "string",
+ "enum": [
+ "ASC",
+ "DESC"
+ ]
+ },
+ "eventCountLimit": {
+ "type": "integer"
+ },
+ "maxEventCount": {
+ "type": "integer"
+ },
+ "EXISTS_sensorElementList": {
+ "type": "integer",
+ "nullable": true
+ },
+ "GE_time": {
+ "$ref": "#/definitions/time"
+ },
+ "LT_time": {
+ "$ref": "#/definitions/time"
+ },
+ "GE_startTime": {
+ "$ref": "#/definitions/time"
+ },
+ "LT_startTime": {
+ "$ref": "#/definitions/time"
+ },
+ "GE_endTime": {
+ "$ref": "#/definitions/time"
+ },
+ "LT_endTime": {
+ "$ref": "#/definitions/time"
+ },
+ "EQ_type": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/measurementType"
}
- ]
- }
- ]
- },
- "^GT_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
- }
- ]
- },
- "^GE_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ },
+ "EQ_deviceID": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_dataProcessingMethod": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_microorganism": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_chemicalSubstance": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_bizRules": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "EQ_stringValue": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "EQ_booleanValue": {
+ "type": "boolean"
+ },
+ "EQ_hexBinaryValue": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "EQ_uriValue": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "GE_percRank": {
+ "type": "number"
+ },
+ "LT_percRank": {
+ "type": "number"
+ },
+ "GT_percRank": {
+ "type": "number"
+ },
+ "LE_percRank": {
+ "type": "number"
}
- ]
},
- "^LT_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "string",
- "format": "date-time"
- },
- {
- "type": "number"
+ "patternProperties": {
+ "^EQ_bizTransaction_[a-z][a-zA-Z0-9_]+$": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "^EQ_source_[a-z][a-zA-Z0-9_]+$": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "^EQ_destination_[a-z][a-zA-Z0-9_]+$": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/uri"
+ }
+ },
+ "^EQ_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_INNER_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_INNER_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_INNER_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_INNER_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_INNER_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_INNER_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_INNER_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_INNER_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_INNER_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_INNER_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EXISTS_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EXISTS_INNER_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EXISTS_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EXISTS_INNER_ILMD_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^HASATTR_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "^EQ_ATTR_\\\\w+_\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^EQ_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EXISTS_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EXISTS_INNER_ERROR_DECLARATION_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EQ_value_[A-Z0-9]{2,4}$": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "^GE_minValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_minValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_minValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_minValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GE_maxValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_maxValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_maxValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_maxValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GE_meanValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_meanValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_meanValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_meanValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GE_sDev_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_sDev_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_sDev_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_sDev_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GE_value_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_value_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_value_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_value_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GE_percValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_percValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_percValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_percValue_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^EQ_quantity_[A-Z0-9]{2,4}$": {
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ "^GE_quantity_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LT_quantity_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^GT_quantity_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^LE_quantity_[A-Z0-9]{2,4}$": {
+ "type": "number"
+ },
+ "^EQ_readPoint_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^EQ_INNER_readPoint_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^EQ_bizLocation_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^EQ_INNER_bizLocation_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^EQ_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_INNER_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_INNER_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EQ_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/string-array-or-date-string-or-number"
+ },
+ "^GT_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^GE_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LT_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^LE_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "$ref": "#/definitions/date-string-or-number"
+ },
+ "^EXISTS_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EXISTS_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
+ },
+ "^EXISTS_SENSORREPORT_[a-z][a-zA-Z0-9]*\\\\:\\\\w+$": {
+ "type": "integer",
+ "nullable": true
}
- ]
},
- "^LE_INNER_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "oneOf": [
- {
- "type": "number"
- },
- {
- "type": "string",
- "format": "date-time"
+ "additionalProperties": false,
+ "definitions": {
+ "date-string-or-number": {
+ "oneOf": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string",
+ "format": "date-time"
+ }
+ ]
+ },
+ "string-array-or-date-string-or-number": {
+ "oneOf": [
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "$ref": "#/definitions/date-string-or-number"
+ }
+ ]
+ },
+ "uri": {
+ "type": "string",
+ "format": "uri"
+ },
+ "eventType": {
+ "anyOf": [
+ {
+ "type": "string",
+ "enum": [
+ "ObjectEvent",
+ "AggregationEvent",
+ "AssociationEvent",
+ "TransformationEvent",
+ "TransactionEvent"
+ ]
+ },
+ {
+ "type": "string",
+ "format": "uri"
+ }
+ ]
+ },
+ "disposition": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/vocab-other-uri"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "active",
+ "container_closed",
+ "damaged",
+ "destroyed",
+ "dispensed",
+ "disposed",
+ "encoded",
+ "expired",
+ "in_progress",
+ "in_transit",
+ "inactive",
+ "no_pedigree_match",
+ "non_sellable_other",
+ "partially_dispensed",
+ "recalled",
+ "reserved",
+ "retail_sold",
+ "returned",
+ "sellable_accessible",
+ "sellable_not_accessible",
+ "stolen",
+ "unknown",
+ "available",
+ "completeness_verified",
+ "completeness_inferred",
+ "conformant",
+ "container_open",
+ "mismatch_instance",
+ "mismatch_class",
+ "mismatch_quantity",
+ "needs_replacement",
+ "non_conformant",
+ "unavailable"
+ ]
+ }
+ ]
+ },
+ "vocab-other-uri": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^(?!(urn:epcglobal:cbv|https?:\\/\\/ns\\.gs1\\.org/cbv\\/))"
+ },
+ "time": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "error-reason": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/vocab-other-uri"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "did_not_occur",
+ "incorrect_data"
+ ]
+ }
+ ]
+ },
+ "eventID": {
+ "$ref": "#/definitions/uri"
+ },
+ "vocab-uri": {
+ "type": "string",
+ "format": "uri"
+ },
+ "decimal": {
+ "type": "number"
+ },
+ "uom": {
+ "type": "string",
+ "pattern": "^[A-Z0-9]{2,3}$"
+ },
+ "measurementType": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/vocab-nonGS1WebVoc-uri"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "AbsoluteHumidity",
+ "AbsorbedDose",
+ "AbsorbedDoseRate",
+ "Acceleration",
+ "Radioactivity",
+ "Altitude",
+ "AmountOfSubstance",
+ "AmountOfSubstancePerUnitVolume",
+ "Angle",
+ "AngularAcceleration",
+ "AngularMomentum",
+ "AngularVelocity",
+ "Area",
+ "Capacitance",
+ "Conductance",
+ "Conductivity",
+ "Count",
+ "Density",
+ "Dimensionless",
+ "DoseEquivalent",
+ "DoseEquivalentRate",
+ "DynamicViscosity",
+ "ElectricCharge",
+ "ElectricCurrent",
+ "ElectricCurrentDensity",
+ "ElectricFieldStrength",
+ "Energy",
+ "Exposure",
+ "Force",
+ "Frequency",
+ "Illuminance",
+ "Inductance",
+ "Irradiance",
+ "KinematicViscosity",
+ "Length",
+ "LinearMomentum",
+ "Luminance",
+ "LuminousFlux",
+ "LuminousIntensity",
+ "MagneticFlux",
+ "MagneticFluxDensity",
+ "MagneticVectorPotential",
+ "Mass",
+ "MassConcentration",
+ "MassFlowRate",
+ "MassPerAreaTime",
+ "MemoryCapacity",
+ "MolalityOfSolute",
+ "MolarEnergy",
+ "MolarMass",
+ "MolarVolume",
+ "Power",
+ "Pressure",
+ "RadiantFlux",
+ "RadiantIntensity",
+ "RelativeHumidity",
+ "Resistance",
+ "Resistivity",
+ "SolidAngle",
+ "SpecificVolume",
+ "Speed",
+ "SurfaceDensity",
+ "SurfaceTension",
+ "Temperature",
+ "Time",
+ "Torque",
+ "Voltage",
+ "Volume",
+ "VolumeFlowRate",
+ "VolumeFraction",
+ "VolumetricFlux",
+ "Wavenumber"
+ ]
+ }
+ ]
+ },
+ "vocab-nonGS1WebVoc-uri": {
+ "type": "string",
+ "format": "uri",
+ "pattern": "^(?!(https?:\\/\\/gs1\\.org\\/voc\\/|https?:\\/\\/www\\.gs1\\.org\\/voc\\/))"
+ },
+ "@context": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uri"
+ },
+ {
+ "type": "object"
+ },
+ {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "anyOf": [
+ {
+ "type": "string",
+ "format": "uri"
+ },
+ {
+ "type": "object"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "action": {
+ "type": "string",
+ "enum": [
+ "OBSERVE",
+ "ADD",
+ "DELETE"
+ ]
+ },
+ "bizStep": {
+ "anyOf": [
+ {
+ "$ref": "#/definitions/vocab-other-uri"
+ },
+ {
+ "type": "string",
+ "enum": [
+ "accepting",
+ "arriving",
+ "assembling",
+ "collecting",
+ "commissioning",
+ "consigning",
+ "creating_class_instance",
+ "cycle_counting",
+ "decommissioning",
+ "departing",
+ "destroying",
+ "disassembling",
+ "dispensing",
+ "encoding",
+ "entering_exiting",
+ "holding",
+ "inspecting",
+ "installing",
+ "killing",
+ "loading",
+ "other",
+ "packing",
+ "picking",
+ "receiving",
+ "removing",
+ "repackaging",
+ "repairing",
+ "replacing",
+ "reserving",
+ "retail_selling",
+ "shipping",
+ "staging_outbound",
+ "stock_taking",
+ "stocking",
+ "storing",
+ "transporting",
+ "unloading",
+ "unpacking",
+ "void_shipping",
+ "sensor_reporting",
+ "sampling"
+ ]
+ }
+ ]
+ },
+ "id": {
+ "type": "string",
+ "format": "uri"
}
- ]
- },
- "EXISTS_SENSORELEMENT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "EXISTS_SENSORMETADATA_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "EXISTS_SENSORREPORT_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "integer",
- "nullable": true
- },
- "^EQ_readPoint_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^EQ_INNER_readPoint_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^EQ_bizLocation_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "^EQ_INNER_bizLocation_[a-z][a-zA-Z0-9]*\\:\\w+$": {
- "type": "array",
- "items": {
- "type": "string"
- }
}
- },
- "additionalProperties": false,
- "$schema": "http://json-schema.org/draft-07/schema#"
-}
\ No newline at end of file
+}
diff --git a/core/src/main/resources/validation-messages/epcis-validation-messages.yaml b/core/src/main/resources/validation-messages/epcis-validation-messages.yaml
new file mode 100644
index 0000000..0b4f0af
--- /dev/null
+++ b/core/src/main/resources/validation-messages/epcis-validation-messages.yaml
@@ -0,0 +1,344 @@
+#
+# Copyright 2022-2024 benelog GmbH & Co. KG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+# =============================================================================
+# EPCIS VALIDATION MESSAGE CATALOGUE
+# =============================================================================
+#
+# Every user facing validation message lives in this file. No message text
+# belongs in Java code. Adding or rewording a message is a change here only.
+#
+# -----------------------------------------------------------------------------
+# HOW A MESSAGE IS CHOSEN (three layers, most specific wins)
+# -----------------------------------------------------------------------------
+#
+# Layer 1 schemaLocations Keyed by JSON Pointer into the EPCIS schema.
+# The pointer is walked upwards until an entry
+# matches, so one entry covers every event type and
+# every occurrence in the document.
+#
+# For a wrong bizStep the engine reports
+# #/definitions/bizStep/anyOf/1/enum
+# and the lookup tries, in order:
+# #/definitions/bizStep/anyOf/1/enum.enum
+# #/definitions/bizStep/anyOf/1/enum
+# #/definitions/bizStep/anyOf/1.enum
+# #/definitions/bizStep/anyOf/1
+# #/definitions/bizStep/anyOf.enum
+# #/definitions/bizStep/anyOf
+# #/definitions/bizStep.enum
+# #/definitions/bizStep <-- matches here
+#
+# Layer 2 keywords Keyed by JSON Schema keyword. The safety net for
+# anything not named in layer 1.
+#
+# Layer 3 (not in this file) Whatever the library itself says, taken from
+# ValidationMessage.getError(). Reached only when
+# layers 1 and 2 both miss, so a message is never
+# lost and an unknown keyword can never break a
+# response.
+#
+# -----------------------------------------------------------------------------
+# PLACEHOLDERS
+# -----------------------------------------------------------------------------
+#
+# {field} name of the offending field, for example bizStep
+# {value} the value the user actually sent
+# {where} friendly position, for example "event 6 (TransformationEvent)"
+# {suggestion} " Did you mean ...?" when a close match exists, else empty
+# {allowedCount} how many values the standard allows at this place
+# {allowedSample} a short sample of the allowed values, never the whole list
+# {expected} the type the schema wants (keyword: type)
+# {actual} the type or count actually supplied (keywords: type, min/maxItems)
+# {limit} the boundary the schema sets (keywords: min*, max*)
+# {fields} a comma separated field list (required.alternatives)
+#
+# Unknown placeholders render as empty and the surrounding gap is collapsed,
+# so a template can safely mention a placeholder that a keyword does not supply.
+#
+# -----------------------------------------------------------------------------
+# RULES WHEN EDITING
+# -----------------------------------------------------------------------------
+#
+# 1. Quote every key in schemaLocations. A JSON Pointer starts with "#", and
+# an unquoted "#" would be read as a YAML comment. This is exactly why the
+# catalogue is YAML and not a .properties file, where such a key is
+# silently swallowed with no error at all.
+# 2. Wrap message text in single quotes so that ":" and "{" stay literal.
+# Write a literal single quote as two single quotes.
+# 3. Never paste the full list of allowed values into a message. Use
+# {allowedSample} and let the API return the complete list separately in
+# the allowedValues field, so the UI can reveal it on demand.
+# 4. Keep the wording free of schema jargon. The reader is a supply chain user,
+# not a JSON Schema author.
+# 5. The full 41 value CBV list, the 33 dispositions and so on are read from
+# the schema at runtime. Never copy such a list into this file.
+#
+# =============================================================================
+
+
+# =============================================================================
+# LAYER 1 ... PER SCHEMA LOCATION
+# =============================================================================
+schemaLocations:
+
+ # ---------------------------------------------------------------------------
+ # CBV vocabularies
+ #
+ # Each of these is declared in the schema as anyOf(custom URI, standard
+ # value). A plain word that is not a standard value therefore fails both
+ # branches, which is why one mistake used to arrive as two errors. One entry
+ # here replaces both of them with a single sentence that explains the choice.
+ # ---------------------------------------------------------------------------
+
+ "#/definitions/bizStep": '"{value}" is not a standard EPCIS business step for {field} in {where}.{suggestion} Use one of the {allowedCount} standard steps ({allowedSample}), or, for a step of your own, a full web address such as https://example.com/bizStep/{value}.'
+
+ # Also covers persistentDisposition, whose set and unset entries reuse this definition.
+ "#/definitions/disposition": '"{value}" is not a standard EPCIS disposition for {field} in {where}.{suggestion} Use one of the {allowedCount} standard dispositions ({allowedSample}), or, for a disposition of your own, a full web address such as https://example.com/disposition/{value}.'
+
+ "#/definitions/bizTransaction-type": '"{value}" is not a standard EPCIS business transaction type for {field} in {where}.{suggestion} Use one of the {allowedCount} standard types ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/source-dest-type": '"{value}" is not a standard EPCIS source or destination type for {field} in {where}.{suggestion} Use one of the {allowedCount} standard types ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/error-reason": '"{value}" is not a standard EPCIS error reason for {field} in {where}.{suggestion} Use one of the {allowedCount} standard reasons ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/measurementType": '"{value}" is not a standard EPCIS measurement type for {field} in {where}.{suggestion} Use one of the {allowedCount} standard types ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/sensorAlertType": '"{value}" is not a standard EPCIS sensor alert type for {field} in {where}.{suggestion} Use one of the {allowedCount} standard types ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/component": '"{value}" is not a standard EPCIS address component for {field} in {where}.{suggestion} Use one of the {allowedCount} standard components ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/eventType": '"{value}" is not a standard EPCIS event type for {field} in {where}.{suggestion} Use one of the {allowedCount} standard types ({allowedSample}), or a full web address of your own.'
+
+ "#/definitions/common-event-properties": '"{value}" is not a value EPCIS recognises for {field} in {where}.{suggestion} Use one of the {allowedCount} standard values ({allowedSample}), or a full web address of your own.'
+
+ # ---------------------------------------------------------------------------
+ # The custom vocabulary branch
+ #
+ # Shared by every vocabulary above as the "or a URI of your own" alternative.
+ # Grouping normally leads with the enum branch, so this wording appears only
+ # when the enum branch did not report anything.
+ # ---------------------------------------------------------------------------
+
+ "#/definitions/vocab-other-uri": '{field} in {where} must be either a standard EPCIS value or a full web address of your own, for example https://example.com/vocabulary/{value}. It cannot be a plain word. You sent "{value}".'
+
+ "#/definitions/vocab-uri": '{field} in {where} must be a recognised EPCIS name or a full web address. You sent "{value}".'
+
+ # ---------------------------------------------------------------------------
+ # Fixed value fields
+ #
+ # action is a plain enum of exactly three values with no custom URI option,
+ # so the wording here deliberately does not offer one.
+ # ---------------------------------------------------------------------------
+
+ "#/definitions/action": '"{value}" is not a valid action for {where}. An action must be exactly one of {allowedSample}, in capital letters.'
+
+ # ---------------------------------------------------------------------------
+ # Identifiers
+ # ---------------------------------------------------------------------------
+
+ "#/definitions/uri": '{field} in {where} must be a full web address or a URN. Examples: https://id.gs1.org/01/09520123456788 or urn:epc:id:sgtin:0614141.107346.2017. You sent "{value}".'
+
+ "#/definitions/id": '{field} in {where} must be a full identifier, for example https://id.gs1.org/414/4012345000054 or urn:epc:id:sgln:0614141.00001.0. You sent "{value}".'
+
+ # ---------------------------------------------------------------------------
+ # Dates and times
+ #
+ # One shared definition backs eventTime, recordTime, creationDate,
+ # declarationTime and the sensor metadata times, so the wording stays generic
+ # and lets {field} name the one that failed.
+ # ---------------------------------------------------------------------------
+
+ "#/definitions/time": '{field} in {where} must be a date and time that includes a time zone. Example: 2026-07-22T09:20:00+02:00. You sent "{value}".'
+
+ "#/definitions/Event/properties/eventTimeZoneOffset": 'eventTimeZoneOffset in {where} must be a time zone offset such as +02:00 or -05:30. You sent "{value}".'
+
+ # ---------------------------------------------------------------------------
+ # Numbers
+ # ---------------------------------------------------------------------------
+
+ "#/definitions/decimal": '{field} in {where} must be a number written without quotation marks, for example 19.3 rather than "19.3". You sent "{value}".'
+
+
+# =============================================================================
+# LAYER 2 ... PER JSON SCHEMA KEYWORD
+#
+# Reached when no schema location above matches. Wording stays general here,
+# because the same keyword can fail anywhere in the document.
+# =============================================================================
+keywords:
+
+ # --- Missing, unknown and misnamed fields ----------------------------------
+
+ required: '{field} is required but missing in {where}.'
+
+ # Synthetic key, produced when sibling schema branches each demand a different
+ # field. Instead of several "X is required" errors the user gets one choice.
+ required.alternatives: '{where} must contain at least one of these fields: {fields}.'
+
+ # Synthetic key, produced when one position is missing several fields that are all
+ # unconditionally required. Every field is named, so nothing is hidden from the user.
+ required.multiple: 'These required fields are missing in {where}: {fields}.'
+
+ additionalProperties: '{field} is not a field EPCIS recognises in {where}. Remove it, or move it to a namespace of your own such as ex:{field}.'
+
+ propertyNames: '"{field}" is not a valid field name in {where}. A field of your own needs a namespace prefix, for example ex:{field}.'
+
+ notAllowed: '{field} is not allowed in {where}. Remove it.'
+
+ dependentRequired: '{field} in {where} may only be used together with the field it depends on.'
+
+ dependencies: '{field} in {where} is missing another field that it depends on.'
+
+ # --- Wrong value -----------------------------------------------------------
+
+ enum: '"{value}" is not an accepted value for {field} in {where}.{suggestion} Accepted values: {allowedSample}.'
+
+ const: '{field} in {where} must be exactly "{limit}". You sent "{value}".'
+
+ pattern: '"{value}" is not correctly formatted for {field} in {where}.'
+
+ type: '{field} in {where} must be {expected}, but {actual} was supplied.'
+
+ unionType: '{field} in {where} must be {expected}, but {actual} was supplied.'
+
+ # --- Formats ---------------------------------------------------------------
+
+ format: '{field} in {where} is not in the format EPCIS expects. You sent "{value}".'
+ format.uri: '{field} in {where} must be a full web address or URN, for example https://id.gs1.org/01/09520123456788. You sent "{value}".'
+ format.uri-reference: '{field} in {where} must be a web address. You sent "{value}".'
+ format.date-time: '{field} in {where} must be a date and time that includes a time zone. Example: 2026-07-22T09:20:00+02:00. You sent "{value}".'
+ format.date: '{field} in {where} must be a date such as 2026-07-22. You sent "{value}".'
+ format.time: '{field} in {where} must be a time such as 09:20:00. You sent "{value}".'
+ format.duration: '{field} in {where} must be a length of time such as PT30M for thirty minutes. You sent "{value}".'
+ format.uuid: '{field} in {where} must be a UUID such as 3f2504e0-4f89-11d3-9a0c-0305e82c3301. You sent "{value}".'
+ format.email: '{field} in {where} must be an email address. You sent "{value}".'
+ format.hostname: '{field} in {where} must be a host name such as example.com. You sent "{value}".'
+ format.regex: '{field} in {where} must be a valid regular expression. You sent "{value}".'
+ format.unknown: '{field} in {where} uses a format EPCIS does not know how to check.'
+
+ # --- Sizes and counts ------------------------------------------------------
+
+ minItems: '{field} in {where} needs at least {limit} entries, but has {actual}.'
+ maxItems: '{field} in {where} allows at most {limit} entries, but has {actual}.'
+ uniqueItems: '{field} in {where} lists the same entry more than once. Remove the duplicates.'
+ contains: '{field} in {where} must contain at least one entry of the expected kind.'
+ contains.min: '{field} in {where} must contain at least {limit} entries of the expected kind.'
+ contains.max: '{field} in {where} must contain at most {limit} entries of the expected kind.'
+ minLength: '{field} in {where} must be at least {limit} characters long. You sent "{value}".'
+ maxLength: '{field} in {where} must be at most {limit} characters long.'
+ minProperties: '{field} in {where} must have at least {limit} fields.'
+ maxProperties: '{field} in {where} must have at most {limit} fields.'
+
+ # --- Numeric ranges --------------------------------------------------------
+
+ minimum: '{field} in {where} must be {limit} or greater. You sent "{value}".'
+ maximum: '{field} in {where} must be {limit} or less. You sent "{value}".'
+ exclusiveMinimum: '{field} in {where} must be greater than {limit}. You sent "{value}".'
+ exclusiveMaximum: '{field} in {where} must be less than {limit}. You sent "{value}".'
+ multipleOf: '{field} in {where} must be a multiple of {limit}. You sent "{value}".'
+
+ # --- Structure -------------------------------------------------------------
+ #
+ # These fire when a value matches none of the shapes EPCIS allows. They are
+ # the least helpful messages in the catalogue by nature, because the schema
+ # itself cannot know which shape was intended. Grouping usually surfaces a
+ # more precise error from inside the failing branch instead.
+
+ anyOf: '{field} in {where} does not match any of the shapes EPCIS allows here. Check the fields of this {field} against the EPCIS standard.'
+ oneOf: '{field} in {where} matches more than one of the shapes EPCIS allows, or none of them. Check the fields of this {field} against the EPCIS standard.'
+ allOf: '{field} in {where} does not meet all of the rules for this part of an EPCIS document.'
+ not: '{field} in {where} uses a value that is not allowed here.'
+
+ unevaluatedProperties: '{field} is not a field EPCIS recognises in {where}. Remove it, or move it to a namespace of your own such as ex:{field}.'
+ unevaluatedItems: '{where} contains an entry at position {limit} that EPCIS does not expect here.'
+
+ # --- Rarely seen -----------------------------------------------------------
+
+ readOnly: '{field} in {where} is set by the repository and cannot be supplied.'
+ writeOnly: '{field} in {where} cannot be returned in a response.'
+ contentEncoding: '{field} in {where} is not encoded the way EPCIS expects.'
+ contentMediaType: '{field} in {where} is not of the media type EPCIS expects.'
+ false: '{field} is not allowed in {where}.'
+ id: '"{value}" is not a valid identifier for {field} in {where}.'
+
+ # Appended as the final entry when the list was capped
+ limit.exceeded: 'Validation stopped after {limit} problems. This document has more, so the list above is not complete. Fix these first, then validate again.'
+
+ # Emitted when the parser produced nothing at all, so an unreadable document is never reported clean.
+ document.unreadable: 'The document could not be read. Check that @context is present and appears before epcisBody.'
+
+# =============================================================================
+# REUSABLE SENTENCE PIECES
+#
+# Leading spaces matter here and YAML keeps them, which is why these are
+# single quoted. A template embeds {suggestion} with no space of its own.
+# =============================================================================
+fragments:
+
+ # Appended only when a close match to the wrong value was found.
+ suggestionHint: ' Did you mean "{best}"?'
+
+ # Appended when one position really does have several unrelated problems.
+ reasonMore: ' Also: {reasons}'
+
+ # Fills {where}. eventIndex is zero based internally and shown from 1.
+ whereDocument: 'the document'
+ whereEvent: 'event {number}'
+ whereEventTyped: 'event {number} ({type})'
+
+
+# =============================================================================
+# XML ... XSD VALIDATION RULE CODES
+#
+# Xerces prefixes each message with the identifier of the W3C XML Schema
+# validation rule that failed, for example "cvc-enumeration-valid". Those names
+# come from the specification itself, so they are stable to key on. They are
+# mapped here to the type field of the response, which previously reported
+# "required" for every XML error regardless of what actually went wrong.
+#
+# The longest matching prefix wins, so cvc-complex-type.2.4 is preferred over
+# cvc-complex-type when both are present.
+# =============================================================================
+xsdTypes:
+ cvc-enumeration-valid: 'enum'
+ cvc-datatype-valid: 'format'
+ cvc-pattern-valid: 'pattern'
+ cvc-maxLength-valid: 'maxLength'
+ cvc-minLength-valid: 'minLength'
+ cvc-length-valid: 'length'
+ cvc-minInclusive-valid: 'minimum'
+ cvc-maxInclusive-valid: 'maximum'
+ cvc-minExclusive-valid: 'exclusiveMinimum'
+ cvc-maxExclusive-valid: 'exclusiveMaximum'
+ cvc-totalDigits-valid: 'format'
+ cvc-fractionDigits-valid: 'format'
+ cvc-complex-type.2.1: 'unexpectedContent'
+ cvc-complex-type.2.2: 'unexpectedContent'
+ cvc-complex-type.2.3: 'unexpectedContent'
+ cvc-complex-type.2.4: 'unexpectedElement'
+ cvc-complex-type.3: 'attribute'
+ cvc-complex-type.4: 'required'
+ cvc-complex-type: 'invalidContent'
+ cvc-type.3.1.1: 'attribute'
+ cvc-type.3.1.3: 'format'
+ cvc-attribute: 'attribute'
+ cvc-elt.1: 'unknownElement'
+ cvc-elt.3: 'nilNotAllowed'
+ cvc-elt.4: 'invalidType'
+ cvc-identity-constraint: 'duplicateKey'
+ cvc-au: 'attribute'
+ src-resolve: 'schemaResolution'
diff --git a/core/src/test/java/io/openepcis/validation/test/EPCISQueryTest.java b/core/src/test/java/io/openepcis/validation/test/EPCISQueryTest.java
new file mode 100644
index 0000000..7370ad0
--- /dev/null
+++ b/core/src/test/java/io/openepcis/validation/test/EPCISQueryTest.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.serialization.JsonNodeReader;
+import io.openepcis.constants.EPCISVersion;
+import io.openepcis.resources.util.Commons;
+import io.openepcis.validation.SchemaValidator;
+import io.openepcis.validation.model.ValidationError;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.List;
+
+import static io.openepcis.constants.EPCISDocumentType.QUERY;
+import static org.junit.jupiter.api.Assertions.*;
+
+class EPCISQueryTest {
+
+ private static final String QUERIES = "2.0/EPCIS/JSON/Query/Queries/";
+ private static final String QUERIES_INVALID = QUERIES + "Invalid/";
+
+ private final ObjectMapper mapper = new ObjectMapper();
+ private final SchemaValidator validator =
+ new SchemaValidator(mapper, JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+ .jsonNodeReader(JsonNodeReader.builder().jsonMapper(mapper).build()).build());
+
+ @Test
+ @DisplayName("valid: a literal query with GE_ and LT_ filters reports no errors")
+ void validLiteralQuery() {
+ assertEquals(0, validate(QUERIES + "SimpleEventQuery.json").size());
+ }
+
+
+ @Test
+ @DisplayName("invalid: a literal query with a bad action and a bad date reports both")
+ void literalQueryReportsEveryBadValue() {
+ final List errors = validate(QUERIES_INVALID + "Query_with_invalid_values.json");
+
+ assertEquals(3, errors.size()); // neither problem hides the other
+ assertTrue(errors.stream().anyMatch(e -> "EQ_action".equals(e.getField())));
+ assertTrue(errors.stream().anyMatch(e -> "GE_eventTime".equals(e.getField())));
+ }
+
+ @Test
+ @DisplayName("valid: a bare event with its own @context reports no errors")
+ void validBareEvent() {
+ assertEquals(0, validate("2.0/EPCIS/JSON/Capture/Events/ObjectEvent.json").size());
+ }
+
+ @Test
+ @DisplayName("valid: a query document is validated as a document, not as a query")
+ void queryDocumentIsValidatedAsADocument() {
+ assertEquals(0, validate("2.0/EPCIS/JSON/Query/queryDocument.json").size());
+ }
+
+ @Test
+ @DisplayName("invalid: an event inside a query document reports its full nested path")
+ void queryDocumentEventKeepsItsNestedPath() {
+ final ValidationError error = validate("2.0/EPCIS/JSON/Query/Invalid/QueryDocument_with_invalid_action.json").getFirst();
+
+ // the path the front end maps to an editor line must keep its exact shape
+ assertTrue(error.getLocation().startsWith("$.epcisBody.queryResults.resultsBody.eventList["));
+ }
+
+ @Test
+ @DisplayName("invalid: an unknown query parameter is named in the message")
+ void unknownQueryParameterIsNamed() {
+ final ValidationError error = validate(QUERIES_INVALID + "Query_with_unknown_parameter.json").getFirst();
+ assertEquals("GE_eventDate", error.getField());
+ }
+
+ // Validates one fixture and returns the errors.
+ private List validate(final String resourcePath) {
+ final InputStream document = Commons.getInputStream(resourcePath);
+ assertNotNull(document, "missing shared test resource " + resourcePath);
+ return validator
+ .validate(document, "application/json", QUERY, EPCISVersion.VERSION_2_0_0)
+ .subscribe()
+ .asStream()
+ .toList();
+ }
+}
diff --git a/core/src/test/java/io/openepcis/validation/test/JsonDocumentTest.java b/core/src/test/java/io/openepcis/validation/test/JsonDocumentTest.java
index 1e5fca4..8bb2ab3 100644
--- a/core/src/test/java/io/openepcis/validation/test/JsonDocumentTest.java
+++ b/core/src/test/java/io/openepcis/validation/test/JsonDocumentTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,91 +15,75 @@
*/
package io.openepcis.validation.test;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
+import com.networknt.schema.serialization.JsonNodeReader;
import io.openepcis.constants.EPCISDocumentType;
import io.openepcis.constants.EPCISVersion;
+import io.openepcis.resources.util.Commons;
import io.openepcis.validation.SchemaValidator;
import io.openepcis.validation.model.ValidationError;
import io.smallrye.mutiny.Multi;
-import java.io.InputStream;
import org.junit.jupiter.api.Test;
-public class JsonDocumentTest {
+import java.io.InputStream;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+class JsonDocumentTest {
- private ObjectMapper mapper = new ObjectMapper();
- private SchemaValidator validator =
- new SchemaValidator(
- mapper,
- JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
- .jsonMapper(mapper)
- .build());
+ // paths of the shared documents in openepcis-test-resources
+ private static final String QUERY = "2.0/EPCIS/JSON/Query/";
+ private static final String QUERY_INVALID = "2.0/EPCIS/JSON/Query/Invalid/";
+ private static final String INVALID = "2.0/EPCIS/JSON/Capture/Invalid/";
- @Test
- public void validateCaptureDocumentTest() {
- final InputStream inputStream = getClass().getResourceAsStream("/json/CaptureDocument.json");
- final Multi jsonCaptureErrors =
- validator.validate(
- inputStream, "application/json", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
- assertTrue(jsonCaptureErrors.subscribe().asStream().toList().size() > 0);
- }
+ private final ObjectMapper mapper = new ObjectMapper();
+ private final SchemaValidator validator =
+ new SchemaValidator(mapper, JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+ .jsonNodeReader(JsonNodeReader.builder().jsonMapper(mapper).build()).build());
- @Test
- public void validateCaptureDocumentWithCertificationTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/json/CaptureDocumentWithCertification.json");
- final Multi jsonCaptureErrors =
- validator.validate(
- inputStream, "application/json", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
- assertTrue(jsonCaptureErrors.subscribe().asStream().toList().size() > 0);
- /*
- jsonCaptureErrors.subscribe().with(error -> {
- System.out.println(error);
- });
+ /**
+ * A fixture that is not on the classpath comes back as null, which the validator only reports
+ * much later as "Stream closed". Naming the missing file here keeps that diagnosable.
*/
- }
+ private InputStream open(final String resourcePath) {
+ final InputStream document = Commons.getInputStream(resourcePath);
+ assertNotNull(document, "missing shared test resource " + resourcePath);
+ return document;
+ }
- @Test
- public void validateQueryDocumentWithEventTypeTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/json/QueryDocumentWithEventType.json");
- final Multi jsonQueryErrors =
- validator.validate(
- inputStream, "application/json", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, jsonQueryErrors.subscribe().asStream().toList().size());
- }
+ @Test
+ void validateCaptureDocumentTest() {
+ final InputStream inputStream = open(INVALID + "Combination_of_events_with_missing_mandatory_fields.json");
+ final Multi jsonCaptureErrors = validator.validate(inputStream, "application/json", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
+ assertFalse(jsonCaptureErrors.subscribe().asStream().toList().isEmpty());
+ }
- @Test
- public void validateQueryDocumentWithEventTimeTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/json/queryDocumentWithEventTime.json");
- final Multi jsonQueryErrors =
- validator.validate(
- inputStream, "application/json", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, jsonQueryErrors.subscribe().asStream().toList().size());
- }
+ @Test
+ void validateCaptureDocumentWithCertificationTest() {
+ final InputStream inputStream = open(INVALID + "ObjectEvent_with_invalid_certificationInfo.json");
+ final Multi jsonCaptureErrors = validator.validate(inputStream, "application/json", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
+ assertFalse(jsonCaptureErrors.subscribe().asStream().toList().isEmpty());
+ }
- @Test
- public void validateQueryDocumentWithActionTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/json/queryDocumentWithAction.json");
- final Multi jsonQueryErrors =
- validator.validate(
- inputStream, "application/json", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, jsonQueryErrors.subscribe().asStream().toList().size());
- }
+ @Test
+ void validateQueryDocumentTest() {
+ final InputStream inputStream = open(QUERY + "queryDocument.json");
+ final Multi jsonQueryErrors = validator.validate(inputStream, "application/json", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
+ final List errors = jsonQueryErrors.subscribe().asStream().toList();
+ assertEquals(0, errors.size());
+ }
- @Test
- public void validateQueryDocumentWithInValidEventTypeTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/json/queryDocumentWithInvalidEventType.json");
- final Multi jsonQueryErrors =
- validator.validate(
- inputStream, "application/json", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertTrue(jsonQueryErrors.subscribe().asStream().toList().size() > 0);
- }
+ @Test
+ void validateQueryDocumentWithInValidEventTypeTest() {
+ final InputStream inputStream = open(QUERY_INVALID + "QueryDocument_with_invalid_eventType.json");
+ final Multi jsonQueryErrors = validator.validate(inputStream, "application/json", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
+ final List errors = jsonQueryErrors.subscribe().asStream().toList();
+ assertFalse(errors.isEmpty());
+ assertEquals(1, errors.size());
+ }
}
diff --git a/core/src/test/java/io/openepcis/validation/test/JsonValidationMessageTest.java b/core/src/test/java/io/openepcis/validation/test/JsonValidationMessageTest.java
new file mode 100644
index 0000000..ab925be
--- /dev/null
+++ b/core/src/test/java/io/openepcis/validation/test/JsonValidationMessageTest.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.serialization.JsonNodeReader;
+import io.openepcis.constants.EPCISDocumentType;
+import io.openepcis.constants.EPCISVersion;
+import io.openepcis.resources.util.Commons;
+import io.openepcis.validation.SchemaValidator;
+import io.openepcis.validation.config.ValidationDefaults;
+import io.openepcis.validation.model.ValidationError;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Validates the quality of JSON validation messages provided to the users
+ * Validates: that one mistake produces exactly one error, and the message names the field, the value and the event in plain language.
+ */
+class JsonValidationMessageTest {
+
+ // path for valid and invalid documents from the test-resources
+ private static final String VALID = "2.0/EPCIS/JSON/Capture/Documents/";
+ private static final String INVALID = "2.0/EPCIS/JSON/Capture/Invalid/";
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ * Validates one fixture and returns the errors.
+ */
+ private List validate(final String resourcePath) {
+ final InputStream document = Commons.getInputStream(resourcePath);
+ assertNotNull(document, "missing shared test resource " + resourcePath);
+ return validate(document);
+ }
+
+ private List validate(final InputStream document) {
+ final SchemaValidator validator = new SchemaValidator(
+ mapper,
+ JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+ .jsonNodeReader(JsonNodeReader.builder().jsonMapper(mapper).build())
+ .build());
+
+ return validator
+ .validate(document, "application/json", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0)
+ .subscribe()
+ .asStream()
+ .toList();
+ }
+
+ // ---------------------------------------------------------------------------
+ // Valid documents must stay silent
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName(VALID + "ObjectEvent.json")
+ void validObjectEvent() {
+ // Valid document carries two events and a standard bizStep
+ assertEquals(0, validate(VALID + "ObjectEvent.json").size());
+ }
+
+ @Test
+ @DisplayName(VALID + "ObjectEvent_with_custom_bizStep_uri.json")
+ void validCustomBizStepUri() {
+ // Valid document with custom bizStep URI
+ assertEquals(0, validate(VALID + "ObjectEvent_with_custom_bizStep_uri.json").size());
+ }
+
+ @Test
+ @DisplayName(VALID + "Combination_of_different_event.json")
+ void validMultipleEvents() {
+ assertEquals(0, validate(VALID + "Combination_of_different_event.json").size());
+ }
+
+ // ---------------------------------------------------------------------------
+ // One issue or wrong format in event should produce exactly one error instead of multiple chained errors.
+ // Ex: one wrong bizStep used to produce two errors
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName(INVALID + "ObjectEvent_with_non_standard_bizStep.json")
+ void wrongBizStepGivesSingleFriendlyError() {
+ final List errors = validate(INVALID + "ObjectEvent_with_non_standard_bizStep.json");
+
+ // the schema fails both anyOf branches, the response must still show one mistake
+ assertEquals(1, errors.size());
+ final ValidationError error = errors.getFirst();
+
+ // line is a line number or nothing at all: JSON line numbers belong to the client, which owns
+ // the text it renders. The schema rule that rejected the value travels in schemaPath.
+ assertNull(error.getLine());
+ assertNull(error.getColumn());
+ assertNotNull(error.getSchemaPath());
+ assertTrue(error.getSchemaPath().contains("/definitions/bizStep"));
+
+ assertEquals(
+ "\"transforming\" is not a standard EPCIS business step for bizStep in event 1 "
+ + "(ObjectEvent). Did you mean \"transporting\"? Use one of the 41 standard steps "
+ + "(accepting, arriving, assembling, collecting, commissioning, consigning and 35 more), "
+ + "or, for a step of your own, a full web address such as "
+ + "https://example.com/bizStep/transforming.",
+ error.getMessage());
+
+ assertEquals("bizStep", error.getField());
+ assertEquals("transforming", error.getValue());
+ assertEquals("transporting", error.getSuggestion());
+ assertEquals(Integer.valueOf(0), error.getEventIndex());
+ assertEquals("ObjectEvent", error.getEventType());
+ assertEquals(41, error.getAllowedValues().size()); // full list travels as data for the UI
+
+ // the path the front end maps to an editor line must keep its exact shape
+ assertEquals("$.epcisBody.eventList[0].bizStep", error.getLocation());
+ }
+
+ @Test
+ @DisplayName(INVALID + "ObjectEvent_with_non_standard_bizStep.json")
+ void messageStaysShortEvenForLargeVocabularies() {
+ final ValidationError error = validate(INVALID + "ObjectEvent_with_non_standard_bizStep.json").getFirst();
+
+ // the old message was over 600 characters of raw enum dump
+ assertTrue(error.getMessage().length() < 400, "message too long: " + error.getMessage().length());
+ assertTrue(error.getMessage().contains("and 35 more"));
+ }
+
+ @Test
+ @DisplayName("invalid: a wrong disposition suggests the closest standard disposition")
+ void wrongDispositionSuggestsClosest() {
+ final List errors = validate(INVALID + "ObjectEvent_with_non_standard_disposition.json");
+ assertEquals(1, errors.size());
+ assertEquals("in_transit", errors.getFirst().getSuggestion());
+ assertTrue(errors.getFirst().getMessage().contains("Did you mean \"in_transit\"?"));
+ }
+
+ // ---------------------------------------------------------------------------
+ // Grouping: several raw messages for one mistake
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("invalid: a wrong action gives ONE error, and offers no custom URI")
+ void wrongActionGivesSingleError() {
+ // the raw engine reports four messages here, one enum plus three branch patterns
+ final List errors = validate(INVALID + "ObjectEvent_with_invalid_action.json");
+ assertEquals(1, errors.size());
+
+ final ValidationError error = errors.getFirst();
+ assertEquals("action", error.getField());
+ assertEquals("CREATE", error.getValue());
+ assertTrue(error.getMessage().contains("OBSERVE, ADD, DELETE"));
+
+ // action has no custom URI branch, so suggesting a web address would be wrong advice
+ assertFalse(error.getMessage().contains("web address"), "action must not be offered a custom URI: " + error.getMessage());
+ }
+
+ @Test
+ @DisplayName("invalid: an unknown field wins over the misleading branch errors")
+ void unknownFieldReportedInsteadOfBranchNoise() {
+ // the raw engine reports five messages, three of them wrongly claiming epcList is missing
+ final List errors = validate(INVALID + "ObjectEvent_with_unknown_field_in_quantityList.json");
+
+ assertEquals(1, errors.size());
+ assertEquals("weight", errors.getFirst().getField());
+ assertTrue(errors.getFirst().getMessage().contains("ex:weight")); // suggest how to fix it
+ }
+
+ @Test
+ @DisplayName("invalid: an event missing every identifier list gets one 'at least one of' message")
+ void missingIdentifierListsGivesOneChoice() {
+ final List errors = validate(INVALID + "ObjectEvent_without_identifier_list.json");
+
+ assertEquals(1, errors.size());
+ assertTrue(errors.getFirst().getMessage().contains("at least one of"));
+ assertTrue(errors.getFirst().getMessage().contains("epcList"));
+ }
+
+ @Test
+ @DisplayName("invalid: several genuinely missing fields are ALL named, none is dropped")
+ void everyMissingRequiredFieldIsNamed() {
+ // regression guard: grouping must never hide a second missing field at the same position
+ final List errors = validate(INVALID + "ObjectEvent_without_action_and_eventTime.json");
+ final String combined = String.join(" | ", errors.stream().map(ValidationError::getMessage).toList());
+
+ assertTrue(combined.contains("action"), "action not reported: " + combined);
+ assertTrue(combined.contains("eventTime"), "eventTime not reported: " + combined);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Other common mistakes
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("invalid: a bad eventTime explains the expected format with an example")
+ void badEventTimeShowsExample() {
+ final List errors = validate(INVALID + "ObjectEvent_with_invalid_eventTime_format.json");
+
+ assertEquals(1, errors.size());
+ assertEquals("eventTime", errors.getFirst().getField());
+ assertTrue(errors.getFirst().getMessage().contains("2026-07-22T09:20:00+02:00"));
+ assertTrue(errors.getFirst().getMessage().contains("22-07-2026"));
+ }
+
+ @Test
+ @DisplayName("invalid: a text quantity says a number is expected")
+ void quantityMustBeNumber() {
+ final List errors = validate(INVALID + "ObjectEvent_with_quantity_as_text.json");
+
+ assertEquals(1, errors.size());
+ assertEquals("quantity", errors.getFirst().getField());
+ assertTrue(errors.getFirst().getMessage().contains("number"));
+ }
+
+ @Test
+ @DisplayName("invalid: an ilmd field without a namespace is told to add one")
+ void ilmdFieldNeedsNamespace() {
+ final List errors = validate(INVALID + "ObjectEvent_with_ilmd_field_without_namespace.json");
+
+ assertEquals(1, errors.size());
+ assertEquals("materialType", errors.getFirst().getField());
+ assertTrue(errors.getFirst().getMessage().contains("ex:materialType"));
+ }
+
+ @Test
+ @DisplayName("invalid: a missing @context is reported against the document, not an event")
+ void missingContextIsReportedAtDocumentLevel() {
+ final List errors = validate(INVALID + "EPCISDocument_without_context.json");
+ assertEquals(1, errors.size());
+ assertEquals("@context", errors.getFirst().getField());
+ assertNull(errors.getFirst().getEventIndex());
+ assertTrue(errors.getFirst().getMessage().contains("The document could not be read"));
+ }
+
+ @Test
+ @DisplayName("invalid: the event number in the message is one based and names the event type")
+ void eventNumberIsHumanReadable() {
+ final List errors = validate(INVALID + "ObjectEvent_with_non_standard_bizStep_in_second_event.json");
+
+ assertEquals(1, errors.size());
+ assertEquals(Integer.valueOf(1), errors.getFirst().getEventIndex());
+ assertTrue(errors.getFirst().getMessage().contains("event 2 (ObjectEvent)"));
+ }
+
+ // ---------------------------------------------------------------------------
+ // Contract guards: the front end depends on these
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("contract: every error carries a message, a type and a JSON path location")
+ void everyErrorIsUsableByTheFrontEnd() {
+ final List errors = validate(INVALID + "Combination_of_multiple_invalid_fields.json");
+ assertTrue(errors.size() > 1);
+
+ for (final ValidationError error : errors) {
+ assertNotNull(error.getMessage(), "message missing");
+ assertFalse(error.getMessage().isBlank(), "message blank");
+ assertNotNull(error.getType(), "type missing");
+ assertNotNull(error.getLocation(), "location missing");
+
+ // the front end strips the leading $ and maps the rest to an editor line
+ assertTrue(error.getLocation().startsWith("$"), "unexpected location: " + error.getLocation());
+ }
+ }
+
+ @Test
+ @DisplayName("contract: errors come back in document order")
+ void errorsAreInDocumentOrder() {
+ final List errors = validate(INVALID + "Combination_of_non_standard_bizStep_and_disposition.json");
+
+ assertEquals(2, errors.size());
+ assertEquals(Integer.valueOf(0), errors.get(0).getEventIndex());
+ assertEquals(Integer.valueOf(1), errors.get(1).getEventIndex());
+ }
+
+ // Test to print every message for manual review.
+ @Test
+ @DisplayName("review: print the messages for a document with many different mistakes")
+ void printMessagesForReview() {
+ final List errors = validate(INVALID + "Combination_of_multiple_invalid_fields.json");
+
+ System.out.println("---- JSON validation messages for review ----");
+ errors.forEach(error -> System.out.printf("%-22s %-46s %s%n", error.getType(), error.getLocation(), error.getMessage()));
+ assertTrue(errors.size() >= 5);
+ }
+
+ // Test if the validation message limit is working and capping correctly
+ @Test
+ @DisplayName("invalid: a document with more problems than the limit is capped and informs about the limit")
+ void errorListIsCappedTest() throws Exception {
+ final int events = ValidationDefaults.MAX_REPORTED_ERRORS + 500;
+ // Generate dummy event with more errors
+ final StringBuilder document = new StringBuilder()
+ .append("{\"@context\":[\"https://ref.gs1.org/standards/epcis/epcis-context.jsonld\"],")
+ .append("\"type\":\"EPCISDocument\",\"schemaVersion\":\"2.0\",")
+ .append("\"creationDate\":\"2026-07-22T10:30:00Z\",\"epcisBody\":{\"eventList\":[");
+
+ // Generate eventList with number of events exceeding the MAX_REPORTED_ERRORS limit
+ for (int i = 0; i < events / 2; i++) {
+ if (i > 0) {
+ document.append(',');
+ }
+ // 2 clear mistake per event: bizStep is not a standard value and eventTime in wrong format
+ document.append("{\"type\":\"ObjectEvent\",\"eventTime\":\"2026-07-T06:45:00+02:00\",")
+ .append("\"eventTimeZoneOffset\":\"+02:00\",\"action\":\"ADD\",")
+ .append("\"bizStep\":\"transforming\",")
+ .append("\"epcList\":[\"https://id.gs1.org/01/09521141012345/21/2017\"]}");
+ }
+ document.append("]}}");
+ final List errors = validate(new ByteArrayInputStream(document.toString().getBytes(StandardCharsets.UTF_8)));
+
+ // ValidationDefaults.MAX_REPORTED_ERRORS - 1 real problems plus one entry saying the list is incomplete
+ assertEquals(ValidationDefaults.MAX_REPORTED_ERRORS, errors.size());
+ assertEquals("limitExceeded", errors.getLast().getType());
+ assertTrue(errors.getLast().getMessage().contains(String.valueOf(ValidationDefaults.MAX_REPORTED_ERRORS - 1)));
+
+ // the reported ones are the first events, in order
+ assertEquals("$.epcisBody.eventList[0].bizStep", errors.getFirst().getLocation());
+ assertEquals("$.epcisBody.eventList[0].eventTime", errors.get(1).getLocation());
+ }
+}
diff --git a/core/src/test/java/io/openepcis/validation/test/MessageCatalogueTest.java b/core/src/test/java/io/openepcis/validation/test/MessageCatalogueTest.java
new file mode 100644
index 0000000..7d90a0b
--- /dev/null
+++ b/core/src/test/java/io/openepcis/validation/test/MessageCatalogueTest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.networknt.schema.JsonSchema;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.ValidationMessage;
+import com.networknt.schema.serialization.JsonNodeReader;
+import io.openepcis.validation.message.MessageTemplates;
+import io.openepcis.validation.model.ValidationError;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Guards the message catalogue itself. The error message itself can contain wrong information or missing information.
+ * These tests are present to prevent any missing/wrong information within the error message itself.
+ */
+class MessageCatalogueTest {
+
+ private static final String CATALOGUE = "/validation-messages/epcis-validation-messages.yaml";
+
+ // Every placeholder the formatter is able to supply. Anything else is a typo.
+ private static final Set KNOWN_PLACEHOLDERS =
+ Set.of("field", "value", "where", "suggestion", "allowedCount", "allowedSample",
+ "expected", "actual", "limit", "fields", "best", "reasons", "number", "type");
+ private static final Pattern PLACEHOLDER = Pattern.compile("\\{([^}]*)}");
+
+ @SuppressWarnings("unchecked")
+ private Map> readCatalogue() throws Exception {
+ try (InputStream stream = getClass().getResourceAsStream(CATALOGUE)) {
+ assertNotNull(stream, "catalogue not found on the classpath at " + CATALOGUE);
+ return new ObjectMapper(new YAMLFactory()).readValue(stream, Map.class);
+ }
+ }
+
+ // ---------------------------------------------------------------------------
+ // Content integrity
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("catalogue: parses and contains all four sections")
+ void catalogueParses() throws Exception {
+ final Map> catalogue = readCatalogue();
+ assertTrue(catalogue.containsKey("schemaLocations"));
+ assertTrue(catalogue.containsKey("keywords"));
+ assertTrue(catalogue.containsKey("fragments"));
+ assertTrue(catalogue.containsKey("xsdTypes"));
+ assertFalse(catalogue.get("schemaLocations").isEmpty());
+ assertFalse(catalogue.get("keywords").isEmpty());
+ }
+
+ @Test
+ @DisplayName("catalogue: schema pointer keys survived YAML parsing")
+ void schemaPointerKeysAreNotSwallowed() throws Exception {
+ // a JSON Pointer starts with '#', which YAML reads as a comment unless the key is quoted.
+ final Map locations = readCatalogue().get("schemaLocations");
+ assertTrue(locations.containsKey("#/definitions/bizStep"), "bizStep pointer key missing, is it quoted in the YAML? keys=" + locations.keySet());
+ locations.keySet()
+ .forEach(key -> assertTrue(key.startsWith("#/"), "not a JSON Pointer key: " + key));
+ }
+
+ @Test
+ @DisplayName("catalogue: every placeholder is one the formatter can fill")
+ void noUnknownPlaceholders() throws Exception {
+ final Map offenders = new LinkedHashMap<>();
+ readCatalogue()
+ .forEach(
+ (section, entries) ->
+ entries.forEach(
+ (key, template) -> {
+ final Matcher matcher = PLACEHOLDER.matcher(template);
+ while (matcher.find()) {
+ if (!KNOWN_PLACEHOLDERS.contains(matcher.group(1))) {
+ offenders.put(section + " -> " + key, matcher.group(0));
+ }
+ }
+ }));
+ assertTrue(offenders.isEmpty(), "unknown placeholders: " + offenders);
+ }
+
+ @Test
+ @DisplayName("catalogue: braces are balanced in every template")
+ void bracesAreBalanced() throws Exception {
+ readCatalogue()
+ .forEach(
+ (section, entries) ->
+ entries.forEach(
+ (key, template) ->
+ assertEquals(
+ template.chars().filter(c -> c == '{').count(),
+ template.chars().filter(c -> c == '}').count(),
+ "unbalanced braces in " + section + " -> " + key)));
+ }
+
+ @Test
+ @DisplayName("catalogue: the fragments that need a leading space still have one")
+ void leadingSpacesArePreserved() throws Exception {
+ // these are appended straight onto a sentence, so losing the space runs words together.
+ final Map fragments = readCatalogue().get("fragments");
+ assertTrue(fragments.get("suggestionHint").startsWith(" "), "suggestionHint lost its space");
+ assertTrue(fragments.get("reasonMore").startsWith(" "), "reasonMore lost its space");
+ }
+
+ // ---------------------------------------------------------------------------
+ // Lookup behaviour
+ // ---------------------------------------------------------------------------
+
+ /**
+ * Produces the real messages for one broken document, so lookup is tested against real input.
+ */
+ private Set messagesFor(final String document) throws Exception {
+ final ObjectMapper mapper = new ObjectMapper();
+ final JsonSchemaFactory factory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+ .jsonNodeReader(JsonNodeReader.builder().jsonMapper(mapper).build())
+ .build();
+ try (InputStream schema = getClass().getResourceAsStream("/json-schema/modular-EPCIS-JSON-schema.json")) {
+ final JsonSchema captureSchema = factory.getSchema(schema);
+ final JsonNode node = mapper.readTree(document);
+ return captureSchema.validate(node);
+ }
+ }
+
+ private static String captureWith(final String extraField) {
+ return "{\"@context\":[\"https://ref.gs1.org/standards/epcis/epcis-context.jsonld\"],"
+ + "\"type\":\"EPCISDocument\",\"schemaVersion\":\"2.0\","
+ + "\"creationDate\":\"2026-07-22T10:30:00Z\",\"epcisBody\":{\"eventList\":["
+ + "{\"type\":\"ObjectEvent\",\"eventTime\":\"2026-07-22T06:45:00+02:00\","
+ + "\"eventTimeZoneOffset\":\"+02:00\",\"action\":\"ADD\","
+ + "\"epcList\":[\"https://id.gs1.org/00/038123450000000129\"],"
+ + extraField
+ + "}]}}";
+ }
+
+ @Test
+ @DisplayName("lookup: a bizStep failure resolves the layer 1 entry by walking the schema pointer")
+ void schemaPointerWalkResolvesVocabulary() throws Exception {
+ // the engine reports #/definitions/bizStep/anyOf/1/enum, the catalogue holds #/definitions/bizStep
+ final ValidationMessage enumMessage =
+ messagesFor(captureWith("\"bizStep\":\"transforming\"")).stream()
+ .filter(message -> "enum".equals(message.getType()))
+ .findFirst()
+ .orElseThrow();
+ final String template = MessageTemplates.templateFor(enumMessage);
+ assertNotNull(template);
+ assertTrue(
+ template.contains("business step"),
+ "expected the bizStep wording, got: " + template);
+ }
+
+ @Test
+ @DisplayName("lookup: a keyword with no schema entry falls back to the layer 2 wording")
+ void keywordFallbackIsUsed() throws Exception {
+ final ValidationMessage required =
+ messagesFor(
+ "{\"@context\":[\"https://ref.gs1.org/standards/epcis/epcis-context.jsonld\"],"
+ + "\"type\":\"EPCISDocument\",\"schemaVersion\":\"2.0\","
+ + "\"creationDate\":\"2026-07-22T10:30:00Z\",\"epcisBody\":{\"eventList\":["
+ + "{\"type\":\"ObjectEvent\",\"eventTimeZoneOffset\":\"+02:00\",\"action\":\"ADD\","
+ + "\"epcList\":[\"https://id.gs1.org/00/038123450000000129\"]}]}}")
+ .stream()
+ .filter(message -> "required".equals(message.getType()))
+ .findFirst()
+ .orElseThrow();
+ assertEquals(MessageTemplates.keyword("required"), MessageTemplates.templateFor(required));
+ }
+
+ @Test
+ @DisplayName("lookup: an unknown keyword returns null so the library wording is used instead")
+ void unknownKeywordFallsThrough() {
+ assertNull(MessageTemplates.keyword("thisKeywordDoesNotExist"));
+ assertFalse(MessageTemplates.hasKeyword("thisKeywordDoesNotExist"));
+ }
+
+ @Test
+ @DisplayName("lookup: both synthetic required templates exist")
+ void syntheticRequiredTemplatesExist() {
+ // one for a choice between fields, one for several fields that are all missing
+ assertTrue(MessageTemplates.hasKeyword("required.alternatives"));
+ assertTrue(MessageTemplates.hasKeyword("required.multiple"));
+ }
+
+ // ---------------------------------------------------------------------------
+ // Rendering
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("render: placeholders are replaced and unknown ones leave no gap")
+ void fillAndTidy() {
+ assertEquals("bizStep is wrong", MessageTemplates.render("{field} is wrong{suggestion}", Map.of("field", "bizStep")));
+ assertEquals("bizStep is wrong. Did you mean \"shipping\"?",
+ MessageTemplates.render(
+ "{field} is wrong.{suggestion}",
+ Map.of("field", "bizStep", "suggestion", " Did you mean \"shipping\"?")));
+ }
+
+ @Test
+ @DisplayName("render: runs of spaces collapse and the ends are trimmed")
+ void tidyCollapsesWhitespace() {
+ assertEquals("a b c", MessageTemplates.tidy(" a b \t c "));
+ }
+
+ @Test
+ @DisplayName("render: a template with no placeholder is returned untouched")
+ void fillShortCircuits() {
+ final String plain = "nothing to substitute here";
+ assertEquals(plain, MessageTemplates.fill(plain, Map.of()));
+ }
+
+ @Test
+ @DisplayName("render: an unbalanced brace is copied verbatim rather than throwing")
+ void unbalancedBraceIsSafe() {
+ assertEquals("a {field", MessageTemplates.fill("a {field", Map.of("field", "x")));
+ }
+
+ // ---------------------------------------------------------------------------
+ // XML rule code mapping
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("xsd: rule codes map to a meaningful type")
+ void xsdCodesAreMapped() {
+ assertEquals("enum", MessageTemplates.xsdType("cvc-enumeration-valid"));
+ assertEquals("pattern", MessageTemplates.xsdType("cvc-pattern-valid"));
+ assertEquals("unexpectedElement", MessageTemplates.xsdType("cvc-complex-type.2.4"));
+ assertEquals("invalidContent", MessageTemplates.xsdType("cvc-complex-type"));
+ assertNull(MessageTemplates.xsdType("cvc-does-not-exist"));
+ }
+
+ // ---------------------------------------------------------------------------
+ // The message trimming that used to corrupt XML text
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("trim: an XSD rule code prefix is removed")
+ void ruleCodePrefixIsStripped() {
+ final ValidationError error = new ValidationError();
+ assertEquals(
+ "Invalid content was found starting with element 'action'.",
+ error.filterErrorMessage("cvc-complex-type.2.4.a: Invalid content was found starting with element 'action'."));
+ }
+
+ @Test
+ @DisplayName("trim: a colon inside the text is left alone")
+ void colonInsideTextIsNotCut() {
+ final ValidationError error = new ValidationError();
+ final String message = "The prefix \"epcis\" for element \"epcis:EPCISDocument\" is not bound.";
+ assertEquals(message, error.filterErrorMessage(message));
+ }
+
+ @Test
+ @DisplayName("trim: a null or blank message is handled without throwing")
+ void blankMessageIsSafe() {
+ final ValidationError error = new ValidationError();
+ assertNull(error.filterErrorMessage(null));
+ assertEquals("", error.filterErrorMessage(""));
+ }
+}
diff --git a/core/src/test/java/io/openepcis/validation/test/ValueSuggesterTest.java b/core/src/test/java/io/openepcis/validation/test/ValueSuggesterTest.java
new file mode 100644
index 0000000..09819d1
--- /dev/null
+++ b/core/src/test/java/io/openepcis/validation/test/ValueSuggesterTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.test;
+
+import io.openepcis.validation.message.ValueSuggester;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * Validate the "did you mean" suggestions. Also avoid wrong suggestions based on attributes or values.
+ */
+class ValueSuggesterTest {
+
+ // The CBV business steps enum that is the largest vocabulary in the EPCIS schema.
+ private static final List BIZ_STEPS =
+ List.of(
+ "accepting", "arriving", "assembling", "collecting", "commissioning", "consigning",
+ "creating_class_instance", "cycle_counting", "decommissioning", "departing", "destroying",
+ "disassembling", "dispensing", "encoding", "entering_exiting", "holding", "inspecting",
+ "installing", "killing", "loading", "other", "packing", "picking", "receiving", "removing",
+ "repackaging", "repairing", "replacing", "reserving", "retail_selling", "shipping",
+ "staging_outbound", "stock_taking", "stocking", "storing", "transporting", "unloading",
+ "unpacking", "void_shipping", "sensor_reporting", "sampling");
+
+ private static final List ACTIONS = List.of("OBSERVE", "ADD", "DELETE");
+
+ // Map of user provided values and potential suggestions
+ @ParameterizedTest(name = "\"{0}\" should suggest \"{1}\"")
+ @CsvSource({
+ "transforming, transporting", // the value from the original user report
+ "shiping, shipping", // one missing letter
+ "commisioning, commissioning", // one missing letter in a long word
+ "recieving, receiving", // two letters transposed
+ "SHIPPING, shipping", // right word, wrong case
+ "Packing, packing", // capitalized first letter
+ "unpackng, unpacking" // one dropped letter
+ })
+ @DisplayName("suggests the closest business step for a near miss")
+ void suggestsClosestBizStep(final String wrongValue, final String expected) {
+ assertEquals(expected, ValueSuggester.closest(wrongValue, BIZ_STEPS));
+ }
+
+ @ParameterizedTest(name = "\"{0}\" should suggest nothing")
+ @CsvSource({
+ "CREATE", // a plausible looking action that is nowhere near a business step
+ "totally_unrelated_word",
+ "xyz",
+ "a"
+ })
+ @DisplayName("stays quiet when nothing is close enough to be useful")
+ void staysQuietWhenNothingIsClose(final String wrongValue) {
+ assertNull(ValueSuggester.closest(wrongValue, BIZ_STEPS));
+ }
+
+ @Test
+ @DisplayName("stays quiet for a wrong action, because none of the three is close to CREATE")
+ void noSuggestionForWrongAction() {
+ assertNull(ValueSuggester.closest("CREATE", ACTIONS));
+ }
+
+ @Test
+ @DisplayName("suggests the right action when only the case is wrong")
+ void suggestsActionOnCaseMismatch() {
+ assertEquals("OBSERVE", ValueSuggester.closest("observe", ACTIONS));
+ }
+
+ @Test
+ @DisplayName("handles null and empty input without throwing")
+ void handlesMissingInput() {
+ assertNull(ValueSuggester.closest(null, BIZ_STEPS));
+ assertNull(ValueSuggester.closest("", BIZ_STEPS));
+ assertNull(ValueSuggester.closest(" ", BIZ_STEPS));
+ assertNull(ValueSuggester.closest("shiping", null));
+ assertNull(ValueSuggester.closest("shiping", List.of()));
+ }
+
+ @Test
+ @DisplayName("a longer wrong value tolerates proportionally more difference")
+ void toleranceScalesWithLength() {
+ // the threshold is a third of the length, so a long value may differ by more characters
+ assertEquals("creating_class_instance", ValueSuggester.closest("creating_class_instanc", BIZ_STEPS));
+ // but a short value must be almost exact
+ assertNull(ValueSuggester.closest("add", List.of("OBSERVE", "DELETE")));
+ }
+}
diff --git a/core/src/test/java/io/openepcis/validation/test/XmlDocumentTest.java b/core/src/test/java/io/openepcis/validation/test/XmlDocumentTest.java
index 3084e5f..986bcdc 100644
--- a/core/src/test/java/io/openepcis/validation/test/XmlDocumentTest.java
+++ b/core/src/test/java/io/openepcis/validation/test/XmlDocumentTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2024 benelog GmbH & Co. KG
+ * Copyright 2022-2026 benelog GmbH & Co. KG
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,96 +15,70 @@
*/
package io.openepcis.validation.test;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
+import com.networknt.schema.serialization.JsonNodeReader;
import io.openepcis.constants.EPCISDocumentType;
import io.openepcis.constants.EPCISVersion;
+import io.openepcis.resources.util.Commons;
import io.openepcis.validation.SchemaValidator;
import io.openepcis.validation.model.ValidationError;
import io.smallrye.mutiny.Multi;
-import java.io.InputStream;
-import org.junit.jupiter.api.Test;
-
-public class XmlDocumentTest {
- private ObjectMapper mapper = new ObjectMapper();
- private SchemaValidator validator =
- new SchemaValidator(
- mapper,
- JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
- .jsonMapper(mapper)
- .build());
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
- @Test
- public void captureDocumentWithBareEventTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/xml/captureDocument-independent-object-event.xml");
- final Multi xsdCaptureErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
- var errors = xsdCaptureErrors.subscribe().asStream().toList();
- assertEquals(1, errors.size());
- }
-
- @Test
- public void captureDocumentWithComment() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/xml/captureDocumentWithComment.xml");
- final Multi xsdCaptureErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_1_2_0);
- var errors = xsdCaptureErrors.subscribe().asStream().toList();
- assertEquals(0, errors.size());
- }
+import java.io.InputStream;
+import java.util.stream.Stream;
- @Test
- public void captureDocumentCompleteEPCISDocumentTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/xml/captureDocument-complete-epcis-20-document.xml");
- final Multi xsdCaptureErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, xsdCaptureErrors.subscribe().asStream().toList().size());
- }
+import static io.openepcis.constants.EPCISDocumentType.CAPTURE;
+import static io.openepcis.constants.EPCISDocumentType.QUERY;
+import static io.openepcis.constants.EPCISVersion.VERSION_1_2_0;
+import static io.openepcis.constants.EPCISVersion.VERSION_2_0_0;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
- @Test
- public void queryDocumentWithEventTimeTest() {
- final InputStream inputStream = getClass().getResourceAsStream("/xml/queryDocument.xml");
- final Multi xsdQueryErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, xsdQueryErrors.subscribe().asStream().toList().size());
- }
+class XmlDocumentTest {
+ private final ObjectMapper mapper = new ObjectMapper();
+ private final SchemaValidator validator =
+ new SchemaValidator(
+ mapper,
+ JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+ .jsonNodeReader(JsonNodeReader.builder().jsonMapper(mapper).build())
+ .build());
+ private static final String INVALID_PATH = "2.0/EPCIS/XML/Capture/Invalid";
+ private static final String QUERY_PATH = "2.0/EPCIS/XML/Query";
+ private static final String CAPTURE_PATH = "2.0/EPCIS/XML/Capture/Documents";
+ private static final String CAPTURE_PATH_EVENT = "2.0/EPCIS/XML/Capture/Events";
+ private static final String CAPTURE_1_2_PATH = "1.2/EPCIS/XML/Capture/Documents";
- @Test
- public void queryDocumentWithEventTypeTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/xml/queryDocumentWithEventType.xml");
- final Multi xsdQueryErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, xsdQueryErrors.subscribe().asStream().toList().size());
- }
+ @ParameterizedTest(name = "{index}: {0}")
+ @MethodSource("validationTestCases")
+ void validateDocuments(String testName, String resource, EPCISDocumentType documentType, EPCISVersion version, int expectedErrors) {
+ final InputStream inputStream = Commons.getInputStream(resource);
+ // a fixture that is not on the classpath comes back as null and only surfaces deep inside
+ // EventModifier as an NPE, so name the missing file here instead
+ assertNotNull(inputStream, "missing shared test resource " + resource);
+ final Multi validationErrorMulti = validator.validate(inputStream, "application/xml", documentType, version);
+ var errors = validationErrorMulti.subscribe().asStream().toList();
+ assertEquals(expectedErrors, errors.size());
+ }
- @Test
- public void queryDocumentWithReadPointTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/xml/queryDocumentWithReadPoint.xml");
- final Multi xsdQueryErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(0, xsdQueryErrors.subscribe().asStream().toList().size());
- }
+ static Stream validationTestCases() {
+ return Stream.of(
+ Arguments.of("Capture document with bare event", INVALID_PATH + "/ObjectEvent_with_invalid_action.xml", CAPTURE, VERSION_2_0_0, 1),
+ Arguments.of("Capture document with comment", CAPTURE_1_2_PATH + "/EPCISDocument_with_comment.xml", CAPTURE, VERSION_1_2_0, 0),
+ Arguments.of("Complete EPCIS capture document", CAPTURE_PATH + "/Combination_of_different_event.xml", CAPTURE, VERSION_2_0_0, 0),
+ Arguments.of("Simple Query document", QUERY_PATH + "/queryDocument.xml", QUERY, VERSION_2_0_0, 0),
+ Arguments.of("Query document with multiple events", QUERY_PATH + "/QueryDocumentWithMultipleEvents.xml", QUERY, VERSION_2_0_0, 0),
+ Arguments.of("Query document with missing value", QUERY_PATH + "/Invalid/QueryDocument_with_missing_value.xml", QUERY, VERSION_2_0_0, 1),
+ Arguments.of("Query document sent to the capture path", QUERY_PATH + "/queryDocument.xml", CAPTURE, VERSION_2_0_0, 1),
+ Arguments.of("EPCIS bare event with no error", CAPTURE_PATH_EVENT + "/TransactionEvent.xml", CAPTURE, VERSION_2_0_0, 0),
- @Test
- public void queryDocumentWithMissingValueTest() {
- final InputStream inputStream =
- getClass().getResourceAsStream("/xml/queryDocumentWithMissingValue.xml");
- final Multi xsdQueryErrors =
- validator.validate(
- inputStream, "application/xml", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0);
- assertEquals(1, xsdQueryErrors.subscribe().asStream().toList().size());
- }
+ // XML Queries
+ Arguments.of("Literal query with GE_ and LT_ params", QUERY_PATH + "/Queries/SimpleEventQuery.xml", QUERY, VERSION_2_0_0, 0),
+ Arguments.of("Literal query without queryName", QUERY_PATH + "/Queries/Invalid/Query_without_queryName.xml", QUERY, VERSION_2_0_0, 1)
+ );
+ }
}
diff --git a/core/src/test/java/io/openepcis/validation/test/XmlValidationMessageTest.java b/core/src/test/java/io/openepcis/validation/test/XmlValidationMessageTest.java
new file mode 100644
index 0000000..63193eb
--- /dev/null
+++ b/core/src/test/java/io/openepcis/validation/test/XmlValidationMessageTest.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2022-2026 benelog GmbH & Co. KG
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.openepcis.validation.test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.networknt.schema.JsonSchemaFactory;
+import com.networknt.schema.SpecVersion;
+import com.networknt.schema.serialization.JsonNodeReader;
+import io.openepcis.constants.EPCISDocumentType;
+import io.openepcis.constants.EPCISVersion;
+import io.openepcis.resources.util.Commons;
+import io.openepcis.validation.SchemaValidator;
+import io.openepcis.validation.model.ValidationError;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Checks the XML documents of validation reporting.
+ * Xerces reports one mistake twice, once as a facet failure and once as an element failure, so the response used to contain duplicates.
+ */
+class XmlValidationMessageTest {
+
+ // path for valid and invalid documents from the test-resources
+ private static final String VALID = "2.0/EPCIS/XML/Capture/Documents/";
+ private static final String INVALID = "2.0/EPCIS/XML/Capture/Invalid/";
+
+ private static final String VALID_12 = "1.2/EPCIS/XML/Capture/Documents/";
+ private static final String QUERY = "2.0/EPCIS/XML/Query/";
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ /**
+ * Validates one fixture and returns the errors.
+ */
+ private List validate(final String resourcePath, final EPCISDocumentType schema, final EPCISVersion version) {
+ final InputStream document = Commons.getInputStream(resourcePath);
+ assertNotNull(document, "missing test resource " + resourcePath);
+
+ final SchemaValidator validator = new SchemaValidator(
+ mapper,
+ JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
+ .jsonNodeReader(JsonNodeReader.builder().jsonMapper(mapper).build()).build());
+
+ return validator
+ .validate(document, "application/xml", schema, version)
+ .subscribe()
+ .asStream()
+ .toList();
+ }
+
+ // Util method for the validation of EPCIS 2.0 documents and return errors
+ private List validateCapture(final String fixture) {
+ return validate(fixture, EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_2_0_0);
+ }
+
+ // ---------------------------------------------------------------------------
+ // Valid documents must stay silent
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("valid: a complete EPCIS 2.0 XML capture document reports no errors")
+ void validCompleteDocument() {
+ assertEquals(0, validateCapture(VALID + "Combination_of_different_event.xml").size());
+ }
+
+ @Test
+ @DisplayName("valid: a correctly ordered ObjectEvent reports no errors")
+ void validObjectEvent() {
+ assertEquals(0, validateCapture(VALID + "ObjectEvent.xml").size());
+ }
+
+ @Test
+ @DisplayName("valid: an EPCIS 1.2 document with comments reports no errors")
+ void validVersion12Document() {
+ assertEquals(0,
+ validate(VALID_12 + "EPCISDocument_with_comment.xml", EPCISDocumentType.CAPTURE, EPCISVersion.VERSION_1_2_0).size());
+ }
+
+ @Test
+ @DisplayName("valid: a query document reports no errors")
+ void validQueryDocument() {
+ assertEquals(0,
+ validate(QUERY + "queryDocument.xml", EPCISDocumentType.QUERY, EPCISVersion.VERSION_2_0_0).size());
+ }
+
+ // ---------------------------------------------------------------------------
+ // Duplicates: Xerces reports one mistake twice
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("invalid: a wrong action gives ONE error, not the two Xerces reports")
+ void wrongActionIsNotDuplicated() {
+ // Xerces emits "not facet-valid" and "value of element is not valid" at the same position
+ final List errors = validateCapture(INVALID + "ObjectEvent_with_invalid_action.xml");
+ assertEquals(1, errors.size());
+
+ final ValidationError error = errors.getFirst();
+ assertEquals("enum", error.getType());
+ assertNotEquals("required", error.getType());
+ assertTrue(error.getMessage().contains("CREATE")); // echoes what user sent
+ assertTrue(error.getMessage().contains("ADD")); // and lists what is allowed
+ }
+
+ @Test
+ @DisplayName("invalid: a bad eventTime gives ONE error typed as a pattern failure")
+ void badEventTimeIsNotDuplicated() {
+ final List errors = validateCapture(INVALID + "ObjectEvent_with_invalid_eventTime_format.xml");
+ assertEquals(1, errors.size());
+ assertEquals("pattern", errors.getFirst().getType());
+ assertTrue(errors.getFirst().getMessage().contains("22-07-2026"));
+ }
+
+ // ---------------------------------------------------------------------------
+ // The error type must describe what actually went wrong
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("invalid: elements in the wrong order are typed as an unexpected element")
+ void wrongElementOrderIsTyped() {
+ final List errors = validateCapture(INVALID + "ObjectEvent_with_wrong_element_order.xml");
+ assertEquals(1, errors.size());
+ assertEquals("unexpectedElement", errors.getFirst().getType());
+ assertTrue(errors.getFirst().getMessage().contains("action"));
+ }
+
+ @Test
+ @DisplayName("invalid: an element EPCIS does not know is typed as an unexpected element")
+ void unknownElementIsTyped() {
+ final List errors = validateCapture(INVALID + "ObjectEvent_with_unknown_element.xml");
+ assertEquals(1, errors.size());
+ assertEquals("unexpectedElement", errors.getFirst().getType());
+ assertTrue(errors.getFirst().getMessage().contains("madeUpField"));
+ }
+
+ @Test
+ @DisplayName("invalid: a missing mandatory element is reported")
+ void missingMandatoryElementIsReported() {
+ final List errors = validateCapture(INVALID + "ObjectEvent_without_eventTime.xml");
+ assertEquals(1, errors.size());
+ assertTrue(errors.getFirst().getMessage().contains("eventTime")); // names what is expected
+ }
+
+ // ---------------------------------------------------------------------------
+ // Contract guards: the front end depends on these
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("contract: the XSD rule code prefix is stripped from the message")
+ void ruleCodePrefixIsRemoved() {
+ final List errors = validateCapture(INVALID + "ObjectEvent_with_unknown_element.xml");
+ // "cvc-complex-type.2.4.a: " is noise for a reader and must not survive into the response
+ assertFalse(errors.getFirst().getMessage().startsWith("cvc-"), "rule code leaked: " + errors.getFirst().getMessage());
+ }
+
+ @Test
+ @DisplayName("contract: line and column are present, because the XML editor highlights by line")
+ void lineAndColumnArePresent() {
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_unknown_element.xml").getFirst();
+ // for XML the front end reads line, and location carries the column
+ System.out.println(error);
+ assertNotNull(error.getLine());
+ assertNotNull(error.getColumn());
+ assertTrue(error.getLine() > 0);
+ assertTrue(error.getColumn() > 0);
+ }
+
+ @Test
+ @DisplayName("Check if proper field name and value are included in error")
+ void checkFieldAndValueTest() {
+ // Confirm field name and value is included in error
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_invalid_eventTime_format.xml").getFirst();
+ assertNotNull(error.getField());
+ assertNotNull(error.getValue());
+ assertNotNull(error.getLine());
+ assertNotNull(error.getColumn());
+ }
+
+ @Test
+ @DisplayName("Check if proper field , value and suggestions are included in error")
+ void checkFieldValueAndSuggestionTest() {
+ // Confirm field name and value is included in error
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_invalid_action.xml").getFirst();
+ assertNotNull(error.getField());
+ assertNotNull(error.getValue());
+ assertNotNull(error.getAllowedValues());
+ assertFalse(error.getAllowedValues().isEmpty());
+ assertNotNull(error.getLine());
+ assertNotNull(error.getColumn());
+ }
+
+ @Test
+ @DisplayName("contract: a bare ObjectEvent is wrapped and validated, keeping existing behaviour")
+ void bareEventStillValidates() {
+ // the wrapping path in EventModifier must keep working, this fixture reports exactly one error
+ assertEquals(1, validateCapture(INVALID + "ObjectEvent_bare_event_without_action.xml").size());
+ }
+
+ // ---------------------------------------------------------------------------
+ // The reported fields must mean the same thing for XML as they do for JSON
+ // ---------------------------------------------------------------------------
+
+ @Test
+ @DisplayName("invalid: an XML enum failure carries the field, the value and the accepted values")
+ void xmlEnumErrorCarriesFieldValueAndAllowedValues() {
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_invalid_action.xml").getFirst();
+
+ assertEquals("enum", error.getType());
+ // the element name only appears in the duplicate message Xerces emits, so this proves the
+ // duplicate is mined for detail instead of being discarded
+ assertEquals("action", error.getField());
+ assertEquals("CREATE", error.getValue());
+ assertEquals(List.of("ADD", "OBSERVE", "DELETE"), error.getAllowedValues());
+ }
+
+ @Test
+ @DisplayName("invalid: a near miss XML value suggests the closest accepted value")
+ void xmlNearMissSuggestsClosestValue() {
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_near_miss_action.xml").getFirst();
+
+ assertEquals("OBSERV", error.getValue());
+ assertEquals("OBSERVE", error.getSuggestion()); // one edit away, so worth offering
+ }
+
+ @Test
+ @DisplayName("invalid: a distant XML value offers no suggestion rather than a guess")
+ void xmlDistantValueOffersNoSuggestion() {
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_invalid_action.xml").getFirst();
+
+ // CREATE is four edits from DELETE, past the threshold, so suggesting anything would be noise
+ assertNull(error.getSuggestion());
+ }
+
+ @Test
+ @DisplayName("invalid: line and column are numbers and the schema rule travels in schemaPath")
+ void xmlErrorReportsRealLineAndColumn() {
+ final ValidationError error = validateCapture(INVALID + "ObjectEvent_with_invalid_action.xml").getFirst();
+
+ // line used to hold a schema URI for JSON and a line number for XML; it is now always a number
+ assertNotNull(error.getLine());
+ assertTrue(error.getLine() > 0, "line must be a real line number");
+ assertNotNull(error.getColumn());
+ // the instance path is a JSON concept, so XML leaves it empty rather than smuggling a column in
+ assertNull(error.getLocation());
+ assertEquals("cvc-enumeration-valid", error.getSchemaPath());
+ }
+
+ /**
+ * Prints the XML messages so the current wording can be reviewed without starting the service.
+ */
+ @Test
+ @DisplayName("review: print the XML messages a user would see")
+ void printMessagesForReview() {
+ final String[] fixtures = {
+ INVALID + "ObjectEvent_with_invalid_action.xml",
+ INVALID + "ObjectEvent_with_invalid_eventTime_format.xml",
+ INVALID + "ObjectEvent_with_unknown_element.xml",
+ INVALID + "ObjectEvent_with_wrong_element_order.xml",
+ INVALID + "ObjectEvent_without_eventTime.xml"
+ };
+
+ System.out.println("---- XML validation messages for review ----");
+ for (final String fixture : fixtures) {
+ for (final ValidationError error : validateCapture(fixture)) {
+ System.out.printf("%-45s type=%-18s line=%s col=%s%n %s%n", fixture, error.getType(), error.getLine(), error.getLocation(), error.getMessage());
+ }
+ }
+ }
+}
diff --git a/core/src/test/resources/json/CaptureDocument.json b/core/src/test/resources/json/CaptureDocument.json
deleted file mode 100644
index d1f983b..0000000
--- a/core/src/test/resources/json/CaptureDocument.json
+++ /dev/null
@@ -1,48 +0,0 @@
-{
- "@context": [
- "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld"
- ],
- "type": "EPCISDocument",
- "schemaVersion": "2.0",
- "creationDate": "2005-07-11T11:30:47.0Z",
- "epcisBody": {
- "eventList": [
- {
- "type": "ObjectEvent",
- "eventTime": "2005-04-03T20:33:31.116-06:00",
- "eventTimeZoneOffset": "-06:00",
- "epcList": [
- "urn:epc:id:sgtin:0614141.107346.2017",
- "urn:epc:id:sgtin:0614141.107346.2018"
- ],
- "bizStep": "shipping",
- "disposition": "in_transit",
- "readPoint": {
- "id": "urn:epc:id:sgln:0614141.07346.1234"
- },
- "bizTransactionList": [
- {
- "type": "po",
- "bizTransaction": "http://transaction.acme.com/po/12345678"
- }
- ]
- },
- {
- "type": "ObjectEvent",
- "eventTime": "2005-04-03T20:33:31.116-06:00",
- "bizStep": "commissioning",
- "disposition": "in_transit",
- "readPoint": {
- "id": "urn:epc:id:sgln:0614141.07346.1234"
- },
- "quantityList": [
- {
- "epcClass": "urn:epc:class:lgtin:4023333.002000.2019-10-07",
- "quantity": 150,
- "uom": "KGM"
- }
- ]
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/core/src/test/resources/json/CaptureDocumentWithCertification.json b/core/src/test/resources/json/CaptureDocumentWithCertification.json
deleted file mode 100644
index ddcd1ad..0000000
--- a/core/src/test/resources/json/CaptureDocumentWithCertification.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "@context": [
- "https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld"
- ],
- "type": "EPCISDocument",
- "schemaVersion": "2.0",
- "creationDate": "2005-07-11T11:30:47.0Z",
- "epcisBody": {
- "eventList": [
- {
- "type": "ObjectEvent",
- "eventTime": "2005-04-03T20:33:31.116-06:00",
- "eventTimeZoneOffset": "-06:00",
- "action": "ADD",
- "bizStep": "commissioning",
- "disposition": "in_transit",
- "readPoint": {
- "id": "urn:epc:id:sgln:0614141.07346.1234"
- },
- "quantityList": [
- {
- "epcClass": "urn:epc:class:lgtin:4023333.002000.2019-10-07",
- "quantity": 150,
- "uom": "KGM"
- }
- ],
- "certificationInfo": [
- {
- "gs1:CertificationDetails": {
- "gs1:certificationAgency": "testingAgency",
- "gs1:certificationAuditDate": "2023-12-20"
- }
- },
- {
- "gs1:CertificationDetails": {
- "gs1:certificationAuditDate": "wrong date format"
- }
- }
- ]
- }
- ]
- }
-}
\ No newline at end of file
diff --git a/core/src/test/resources/json/QueryDocumentWithEventType.json b/core/src/test/resources/json/QueryDocumentWithEventType.json
deleted file mode 100644
index eb30416..0000000
--- a/core/src/test/resources/json/QueryDocumentWithEventType.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "@context": "https://ref.gs1.org/standards/epcis/2.0.0/query-schema.json",
- "eventType": [
- "ObjectEvent"
- ]
-}
\ No newline at end of file
diff --git a/core/src/test/resources/json/queryDocumentWithAction.json b/core/src/test/resources/json/queryDocumentWithAction.json
deleted file mode 100644
index a49af49..0000000
--- a/core/src/test/resources/json/queryDocumentWithAction.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "@context": "https://ref.gs1.org/standards/epcis/2.0.0/query-schema.json",
- "eventType": ["ObjectEvent"],
- "EQ_action": ["ADD"]
-}
\ No newline at end of file
diff --git a/core/src/test/resources/json/queryDocumentWithEventTime.json b/core/src/test/resources/json/queryDocumentWithEventTime.json
deleted file mode 100644
index b31a9ca..0000000
--- a/core/src/test/resources/json/queryDocumentWithEventTime.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "@context": "https://ref.gs1.org/standards/epcis/2.0.0/query-schema.json",
- "GE_eventTime": "2022-01-01T00:00:00Z",
- "LT_eventTime": "2022-12-31T23:59:59Z"
-}
\ No newline at end of file
diff --git a/core/src/test/resources/json/queryDocumentWithInvalidEventType.json b/core/src/test/resources/json/queryDocumentWithInvalidEventType.json
deleted file mode 100644
index 4d2833a..0000000
--- a/core/src/test/resources/json/queryDocumentWithInvalidEventType.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "@context": "https://ref.gs1.org/standards/epcis/2.0.0/query-schema.json",
- "eventType": ["AssertType"],
- "EQ_action": ["ADD"]
-}
\ No newline at end of file
diff --git a/core/src/test/resources/xml/captureDocument-complete-epcis-20-document.xml b/core/src/test/resources/xml/captureDocument-complete-epcis-20-document.xml
deleted file mode 100644
index f35218f..0000000
--- a/core/src/test/resources/xml/captureDocument-complete-epcis-20-document.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-
-
-
-
- 2005-04-03T20:33:31.116-06:00
- -06:00
-
- urn:epc:id:sgtin:0614141.107346.2017
- urn:epc:id:sgtin:0614141.107346.2018
-
- OBSERVE
- urn:epcglobal:cbv:bizstep:shipping
- urn:epcglobal:cbv:disp:in_transit
-
- urn:epc:id:sgln:0614141.07346.1234
-
-
- http://transaction.acme.com/po/12345678
-
-
-
- 2005-04-04T20:33:31.116-06:00
- -06:00
-
- urn:epc:id:sgtin:0614141.107346.2018
-
- OBSERVE
- urn:epcglobal:cbv:bizstep:receiving
- urn:epcglobal:cbv:disp:in_progress
-
- urn:epc:id:sgln:0012345.11111.400
-
-
- urn:epc:id:sgln:0012345.11111.0
-
-
- http://transaction.acme.com/po/12345678
- urn:epcglobal:cbv:bt:0614141073467:1152
-
- Example of a vendor/user extension
-
-
-
-
\ No newline at end of file
diff --git a/core/src/test/resources/xml/captureDocument-independent-object-event.xml b/core/src/test/resources/xml/captureDocument-independent-object-event.xml
deleted file mode 100644
index 7d167d7..0000000
--- a/core/src/test/resources/xml/captureDocument-independent-object-event.xml
+++ /dev/null
@@ -1,16 +0,0 @@
-
- 2005-04-03T20:33:31.116-06:00
- -06:00
-
- urn:epc:id:sgtin:0614141.107346.2017
- urn:epc:id:sgtin:0614141.107346.2018
-
- urn:epcglobal:cbv:bizstep:shipping
- urn:epcglobal:cbv:disp:in_transit
-
- urn:epc:id:sgln:0614141.07346.1234
-
-
- http://transaction.acme.com/po/12345678
-
-
\ No newline at end of file
diff --git a/core/src/test/resources/xml/captureDocumentWithComment.xml b/core/src/test/resources/xml/captureDocumentWithComment.xml
deleted file mode 100644
index 0fe6840..0000000
--- a/core/src/test/resources/xml/captureDocumentWithComment.xml
+++ /dev/null
@@ -1,300 +0,0 @@
-
-
-
-
-
-
- 1.0
-
- urn:epc:id:sgln:030001.111111.0
-
-
- urn:epc:id:sgln:039999.999999.0
-
-
- EPCglobal
- 1.0
- 1100220001
- Events
- 2023-04-01T08:45:16Z
-
-
-
-
-
-
-
-
-
- 0001-0123-45
- US_FDA_NDC
- Epcistra
- GS1 Pharma LLC
- PILL
- 100mg
- 500 pills
-
-
-
- 0001-0123-45
- US_FDA_NDC
- Epcistra
- GS1 Pharma LLC
- PILL
- 100mg
- 500 pills
-
-
-
-
-
-
-
- GS1 Pharma
- 1 Biopharm Ct
- Suite A
- Thousand Oaks
- CA
- 91320
- US
-
-
-
- GS1 Pharma DC
- 2806 Agoura Rd
- Suite 500
- Westlake Village
- CA
- 91361
- US
-
-
-
- GS1 Drug Distributor
- 230 Park Ave S
- Room 378
- New York
- NY
- 10003-1502
- US
-
-
-
- GS1 Drug Distributor Warehouse
- 136 Main St
- Suite 100
- Louisville
- KY
- 40218
- US
-
-
-
-
-
-
- GS1 US DSCSA R1.3
-
- true
- Seller has complied with each applicable subsection of FDCA Sec. 581(27)(A)-(G).
-
-
-
-
-
-
- 2023-03-27T06:45:16Z
- -05:00
-
- urn:epc:id:sgtin:030001.0012345.11
- urn:epc:id:sgtin:030001.0012345.12
- urn:epc:id:sgtin:030001.0012345.13
- urn:epc:id:sgtin:030001.0012345.14
- urn:epc:id:sgtin:030001.0012345.15
- urn:epc:id:sgtin:030001.0012345.16
- urn:epc:id:sgtin:030001.0012345.17
- urn:epc:id:sgtin:030001.0012345.18
- urn:epc:id:sgtin:030001.0012345.19
- urn:epc:id:sgtin:030001.0012345.20
- urn:epc:id:sgtin:030001.0012345.21
- urn:epc:id:sgtin:030001.0012345.22
-
- ADD
- urn:epcglobal:cbv:bizstep:commissioning
- urn:epcglobal:cbv:disp:active
-
- urn:epc:id:sgln:030001.111111.0
-
-
- urn:epc:id:sgln:030001.111111.0
-
-
-
- A123
- 2025-03-27
-
-
-
-
-
- 2023-03-27T06:46:16Z
- -05:00
-
- urn:epc:id:sgtin:030001.1012345.110
- urn:epc:id:sgtin:030001.1012345.111
- urn:epc:id:sgtin:030001.1012345.121
-
- ADD
- urn:epcglobal:cbv:bizstep:commissioning
- urn:epcglobal:cbv:disp:active
-
- urn:epc:id:sgln:030001.111111.0
-
-
- urn:epc:id:sgln:030001.111111.0
-
-
-
- A123
- 2025-03-27
-
-
-
-
-
- 2023-03-27T06:50:16Z
- -05:00
- urn:epc:id:sgtin:030001.1012345.110
-
- urn:epc:id:sgtin:030001.0012345.11
- urn:epc:id:sgtin:030001.0012345.12
- urn:epc:id:sgtin:030001.0012345.13
- urn:epc:id:sgtin:030001.0012345.14
-
- ADD
- urn:epcglobal:cbv:bizstep:packing
- urn:epcglobal:cbv:disp:in_progress
-
- urn:epc:id:sgln:030001.111111.0
-
-
- urn:epc:id:sgln:030001.111111.0
-
-
-
-
- 2023-03-27T06:51:16Z
- -05:00
- urn:epc:id:sgtin:030001.1012345.111
-
- urn:epc:id:sgtin:030001.0012345.15
- urn:epc:id:sgtin:030001.0012345.16
- urn:epc:id:sgtin:030001.0012345.17
- urn:epc:id:sgtin:030001.0012345.18
-
- ADD
- urn:epcglobal:cbv:bizstep:packing
- urn:epcglobal:cbv:disp:in_progress
-
- urn:epc:id:sgln:030001.111111.0
-
-
- urn:epc:id:sgln:030001.111111.0
-
-
-
-
- 2023-03-27T06:52:16Z
- -05:00
- urn:epc:id:sgtin:030001.1012345.121
-
- urn:epc:id:sgtin:030001.0012345.19
- urn:epc:id:sgtin:030001.0012345.20
- urn:epc:id:sgtin:030001.0012345.21
- urn:epc:id:sgtin:030001.0012345.22
-
- ADD
- urn:epcglobal:cbv:bizstep:packing
- urn:epcglobal:cbv:disp:in_progress
-
- urn:epc:id:sgln:030001.111111.0
-
-
- urn:epc:id:sgln:030001.111111.0
-
-
-
-
- 2023-04-01T06:47:16Z
- -05:00
-
- urn:epc:id:sscc:030001.41234567890
-
- ADD
- urn:epcglobal:cbv:bizstep:commissioning
- urn:epcglobal:cbv:disp:active
-
- urn:epc:id:sgln:030001.111121.0
-
-
- urn:epc:id:sgln:030001.111121.0
-
-
-
-
- 2023-04-01T06:48:16Z
- -05:00
- urn:epc:id:sscc:030001.41234567890
-
- urn:epc:id:sgtin:030001.1012345.110
- urn:epc:id:sgtin:030001.1012345.111
- urn:epc:id:sgtin:030001.1012345.121
-
- ADD
- urn:epcglobal:cbv:bizstep:packing
- urn:epcglobal:cbv:disp:in_progress
-
- urn:epc:id:sgln:030001.111121.0
-
-
- urn:epc:id:sgln:030001.111121.0
-
-
-
-
- 2023-04-01T07:48:16Z
- -05:00
-
- urn:epc:id:sscc:030001.41234567890
-
- OBSERVE
- urn:epcglobal:cbv:bizstep:shipping
- urn:epcglobal:cbv:disp:in_transit
-
- urn:epc:id:sgln:030001.111121.0
-
-
-
- urn:epcglobal:cbv:bt:0399999999991:XYZPO189
- urn:epcglobal:cbv:bt:0300011111215:DEL100
-
-
-
-
- urn:epc:id:sgln:030001.111111.0
- urn:epc:id:sgln:030001.111121.0
-
-
-
- urn:epc:id:sgln:039999.999999.0
- urn:epc:id:sgln:039999.345678.0
-
-
- false
-
-
-
-
diff --git a/core/src/test/resources/xml/queryDocument.xml b/core/src/test/resources/xml/queryDocument.xml
deleted file mode 100644
index 276a7b1..0000000
--- a/core/src/test/resources/xml/queryDocument.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
- SimpleEventQuery
-
-
- GE_eventTime
- 2022-09-30T22:00:00Z
-
-
- LT_eventTime
- 2022-12-30T23:00:00Z
-
-
-
-
-
\ No newline at end of file
diff --git a/core/src/test/resources/xml/queryDocumentWithEventType.xml b/core/src/test/resources/xml/queryDocumentWithEventType.xml
deleted file mode 100644
index f4432fe..0000000
--- a/core/src/test/resources/xml/queryDocumentWithEventType.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
- SimpleEventQuery
-
-
- eventType
-
- ObjectEvent
-
-
-
- EQ_action
-
- ADD
-
-
-
-
-
-
\ No newline at end of file
diff --git a/core/src/test/resources/xml/queryDocumentWithMissingValue.xml b/core/src/test/resources/xml/queryDocumentWithMissingValue.xml
deleted file mode 100644
index b2fcb52..0000000
--- a/core/src/test/resources/xml/queryDocumentWithMissingValue.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
- SimpleEventQuery
-
-
- eventType
-
- ObjectEvent
- AggregationEvent
-
-
-
- EQ_readPoint
-
-
-
-
-
\ No newline at end of file
diff --git a/core/src/test/resources/xml/queryDocumentWithReadPoint.xml b/core/src/test/resources/xml/queryDocumentWithReadPoint.xml
deleted file mode 100644
index 2ed3b07..0000000
--- a/core/src/test/resources/xml/queryDocumentWithReadPoint.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
- SimpleEventQuery
-
-
- eventType
-
- ObjectEvent
- AggregationEvent
-
-
-
- EQ_readPoint
-
- urn:epc:id:sgln:73784378.7838.78437834
-
-
-
-
-
-
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 7d7d293..741dfc5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,7 @@
- 4.0.0
-
- io.openepcis
- openepcis-bom
- 999-SNAPSHOT
-
- openepcis-document-validation-parent
-
- openepcis-document-validation-parent
- pom
- Service to validate EPCIS 2.0 documents and identifiers ensuring complicante with EPCIS standards.
- https://github.com/openepcis/openepcis-document-validation-service
-
-
-
- The Apache Software License, Version 2.0
- http://www.apache.org/licenses/LICENSE-2.0.txt
-
-
-
-
-
- Sven Boeckelmann
- sven.boeckelmann@benelog.com
- benelog GmbH & Co. KG
- https://www.benelog.com
-
-
-
-
- scm:git:git://github.com/openepcis/openepcis-document-validation-service.git
- scm:git:ssh://github.com:openepcis/openepcis-document-validation-service.git
- https://github.com/openepcis/openepcis-document-validation-service/tree/main
-
-
-
-
- core
- restassured
- rest-api
- servlet-api
- quarkus
-
-
-
-
- io.openepcis
- openepcis-document-validation-service
- ${project.parent.version}
-
-
- io.openepcis
- openepcis-document-validation-rest-api
- ${project.parent.version}
-
-
- io.openepcis
- openepcis-document-validation-servlet-api
- ${project.parent.version}
-
-
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+ 4.0.0
+ io.openepcis
- openepcis-document-validation-restassured
- ${project.parent.version}
-
-
- io.openepcis.quarkus
- quarkus-document-validation-service
- ${project.parent.version}
-
-
- io.openepcis.quarkus
- quarkus-document-validation-service-deployment
- ${project.parent.version}
-
-
- io.openepcis.quarkus
- quarkus-openepcis-model-deployment
- ${project.parent.version}
-
-
- io.openepcis.quarkus
- quarkus-test-resources-deployment
- ${project.parent.version}
-
-
-
-
- io.openepcis
- openepcis-epcis-constants
- ${project.parent.version}
-
-
- io.openepcis
- openepcis-model-rest
- ${project.parent.version}
-
-
- io.openepcis
- openepcis-model-epcis
- ${project.parent.version}
-
+ openepcis-bom
+ 999-SNAPSHOT
+
+ openepcis-document-validation-parent
-
- io.openepcis
- openepcis-test-resources
- ${project.parent.version}
-
-
-
- io.openepcis.quarkus
- quarkus-test-resources
- ${project.parent.version}
-
-
-
- io.openepcis.quarkus
- quarkus-openepcis-model
- ${project.parent.version}
-
-
-
-
-
-
-
-
- central-snapshots
- https://central.sonatype.com/repository/maven-snapshots/
-
- true
-
-
- false
-
-
-
+ openepcis-document-validation-parent
+ pom
+ Service to validate EPCIS 2.0 documents and identifiers ensuring complicante with EPCIS standards.
+ https://github.com/openepcis/openepcis-document-validation-service
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+
+
+
+
+
+ Sven Boeckelmann
+ sven.boeckelmann@benelog.com
+ benelog GmbH & Co. KG
+ https://www.benelog.com
+
+
+ Aravinda Baligab
+ benelog GmbH & Co. KG
+ https://www.benelog.com
+
+
+
+
+ scm:git:git://github.com/openepcis/openepcis-document-validation-service.git
+ scm:git:ssh://github.com:openepcis/openepcis-document-validation-service.git
+ https://github.com/openepcis/openepcis-document-validation-service/tree/main
+
+
+
+
+ core
+ restassured
+ rest-api
+ servlet-api
+ quarkus
+
+
+
+
+
+ io.openepcis
+ openepcis-document-validation-service
+ ${project.parent.version}
+
+
+ io.openepcis
+ openepcis-document-validation-rest-api
+ ${project.parent.version}
+
+
+ io.openepcis
+ openepcis-document-validation-servlet-api
+ ${project.parent.version}
+
+
+ io.openepcis
+ openepcis-document-validation-restassured
+ ${project.parent.version}
+
+
+ io.openepcis.quarkus
+ quarkus-document-validation-service
+ ${project.parent.version}
+
+
+ io.openepcis.quarkus
+ quarkus-document-validation-service-deployment
+ ${project.parent.version}
+
+
+ io.openepcis.quarkus
+ quarkus-openepcis-model-deployment
+ ${project.parent.version}
+
+
+ io.openepcis.quarkus
+ quarkus-test-resources-deployment
+ ${project.parent.version}
+
+
+
+ io.openepcis
+ openepcis-reactive-event-publisher
+ ${project.parent.version}
+
+
+
+
+ io.openepcis
+ openepcis-epcis-constants
+ ${project.parent.version}
+
+
+ io.openepcis
+ openepcis-model-rest
+ ${project.parent.version}
+
+
+ io.openepcis
+ openepcis-model-epcis
+ ${project.parent.version}
+
+
+
+ io.openepcis
+ openepcis-test-resources
+ ${project.parent.version}
+
+
+
+ io.openepcis.quarkus
+ quarkus-test-resources
+ ${project.parent.version}
+
+
+
+ io.openepcis.quarkus
+ quarkus-openepcis-model
+ ${project.parent.version}
+
+
+
+
+
+
+
+
+ central-snapshots
+ https://central.sonatype.com/repository/maven-snapshots/
+
+ true
+
+
+ false
+
+
+
diff --git a/quarkus/deployment/pom.xml b/quarkus/deployment/pom.xml
index 1d6891c..749b6d6 100644
--- a/quarkus/deployment/pom.xml
+++ b/quarkus/deployment/pom.xml
@@ -1,7 +1,7 @@