From 2b0cd1fe0d141d9e046d93f9a332babdd0ef2e9f Mon Sep 17 00:00:00 2001 From: Niels Thykier Date: Mon, 12 Feb 2024 12:24:33 +0100 Subject: [PATCH 01/14] DT-876: Support basic scenario without additional documentation Also covers DT-873 and DT-874 as a side-effect. Signed-off-by: Niels Thykier --- pint/LICENSE | 201 + pint/pom.xml | 46 + .../eblinterop/PintComponentFactory.java | 141 + .../eblinterop/PintScenarioListBuilder.java | 110 + .../eblinterop/action/PintAction.java | 124 + .../action/PintInitiateTransferAction.java | 124 + .../eblinterop/action/PintResponseCode.java | 13 + ...ScenarioParametersAndStateSetupAction.java | 53 + .../eblinterop/action/ScenarioClass.java | 8 + .../SenderSupplyScenarioParametersAction.java | 48 + .../eblinterop/checks/PintChecks.java | 248 ++ .../eblinterop/crypto/Checksums.java | 28 + .../eblinterop/crypto/JWSSignerDetails.java | 6 + .../eblinterop/crypto/PayloadSigner.java | 5 + .../crypto/PayloadSignerFactory.java | 143 + .../eblinterop/crypto/SignatureVerifier.java | 22 + .../crypto/impl/DefaultPayloadSigner.java | 23 + .../models/DynamicScenarioParameters.java | 18 + .../models/ReceiverScenarioParameters.java | 33 + .../models/SenderScenarioParameters.java | 22 + .../eblinterop/models/TDReceiveState.java | 123 + .../eblinterop/models/TransferState.java | 9 + .../party/PintReceivingPlatform.java | 197 + .../standards/eblinterop/party/PintRole.java | 23 + .../eblinterop/party/PintSendingPlatform.java | 238 ++ .../eblinterop/party/PintTransferState.java | 7 + .../pint-3.0.0-Beta-1-issuance-entry.json | 19 + .../pint-3.0.0-Beta-1-transport-document.json | 91 + .../pint/sandboxes/auto-all-in-one.json | 35 + .../auto-receivingplatform-tested-party.json | 31 + ...eceivingplatform-testing-counterparts.json | 31 + .../auto-sendingplatform-tested-party.json | 29 + ...-sendingplatform-testing-counterparts.json | 27 + ...manual-receivingplatform-tested-party.json | 27 + ...eceivingplatform-testing-counterparts.json | 26 + .../manual-sendingplatform-tested-party.json | 27 + ...-sendingplatform-testing-counterparts.json | 26 + .../pint/schemas/pint-3.0.0-Beta-1.json | 3539 +++++++++++++++++ pom.xml | 1 + sandbox/pom.xml | 5 + .../sandbox/ConformanceSandbox.java | 4 + .../springboot/ConformanceApplication.java | 2 + 42 files changed, 5933 insertions(+) create mode 100644 pint/LICENSE create mode 100644 pint/pom.xml create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintComponentFactory.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintResponseCode.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ScenarioClass.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/Checksums.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/JWSSignerDetails.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSigner.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSignerFactory.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/SignatureVerifier.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/impl/DefaultPayloadSigner.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/DynamicScenarioParameters.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/ReceiverScenarioParameters.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TransferState.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintRole.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java create mode 100644 pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintTransferState.java create mode 100644 pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-issuance-entry.json create mode 100644 pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-transport-document.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/auto-all-in-one.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-tested-party.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-testing-counterparts.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-tested-party.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-testing-counterparts.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-testing-counterparts.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-tested-party.json create mode 100644 pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-testing-counterparts.json create mode 100644 pint/src/main/resources/standards/pint/schemas/pint-3.0.0-Beta-1.json diff --git a/pint/LICENSE b/pint/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/pint/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/pint/pom.xml b/pint/pom.xml new file mode 100644 index 00000000..1b94c0e7 --- /dev/null +++ b/pint/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + org.dcsa.conformance + pint + 0.0.1-SNAPSHOT + dcsa-conformance-ebl-interop + DCSA Conformance eBL Interop + + 17 + 17 + 17 + UTF-8 + + + + org.dcsa.conformance + core + 0.0.1-SNAPSHOT + + + + org.projectlombok + lombok + 1.18.28 + true + + + + com.networknt + json-schema-validator + 1.0.86 + + + com.nimbusds + nimbus-jose-jwt + 9.24.4 + + + io.setl + canonical-json + 2.3 + + + diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintComponentFactory.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintComponentFactory.java new file mode 100644 index 00000000..0cbd05f7 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintComponentFactory.java @@ -0,0 +1,141 @@ +package org.dcsa.conformance.standards.eblinterop; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.SneakyThrows; +import org.dcsa.conformance.core.AbstractComponentFactory; +import org.dcsa.conformance.core.party.ConformanceParty; +import org.dcsa.conformance.core.party.CounterpartConfiguration; +import org.dcsa.conformance.core.party.PartyConfiguration; +import org.dcsa.conformance.core.party.PartyWebClient; +import org.dcsa.conformance.core.scenario.ScenarioListBuilder; +import org.dcsa.conformance.core.state.JsonNodeMap; +import org.dcsa.conformance.core.toolkit.JsonToolkit; +import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSignerFactory; +import org.dcsa.conformance.standards.eblinterop.party.PintReceivingPlatform; +import org.dcsa.conformance.standards.eblinterop.party.PintRole; +import org.dcsa.conformance.standards.eblinterop.party.PintSendingPlatform; + +public class PintComponentFactory extends AbstractComponentFactory { + public static final String STANDARD_NAME = "PINT"; + public static final List STANDARD_VERSIONS = List.of("3.0.0-Beta-1"); + + private static final String SENDING_PLATFORM_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); + private static final String RECEIVING_PLATFORM_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); + + private final String standardVersion; + + public PintComponentFactory(String standardVersion) { + this.standardVersion = standardVersion; + if (STANDARD_VERSIONS.stream().noneMatch(version -> version.equals(standardVersion))) { + throw new IllegalArgumentException( + "Unsupported standard version '%s'".formatted(standardVersion)); + } + } + + public List createParties( + PartyConfiguration[] partyConfigurations, + CounterpartConfiguration[] counterpartConfigurations, + JsonNodeMap persistentMap, + PartyWebClient asyncWebClient, + Map> orchestratorAuthHeader) { + Map partyConfigurationsByRoleName = + Arrays.stream(partyConfigurations) + .collect(Collectors.toMap(PartyConfiguration::getRole, Function.identity())); + Map counterpartConfigurationsByRoleName = + Arrays.stream(counterpartConfigurations) + .collect(Collectors.toMap(CounterpartConfiguration::getRole, Function.identity())); + + LinkedList parties = new LinkedList<>(); + + PartyConfiguration sendingPlatformConfiguration = + partyConfigurationsByRoleName.get(PintRole.SENDING_PLATFORM.getConfigName()); + if (sendingPlatformConfiguration != null) { + parties.add( + new PintSendingPlatform( + standardVersion, + sendingPlatformConfiguration, + counterpartConfigurationsByRoleName.get(PintRole.RECEIVING_PLATFORM.getConfigName()), + persistentMap, + asyncWebClient, + orchestratorAuthHeader, + PayloadSignerFactory.testPayloadSigner() + )); + } + + PartyConfiguration receivingPlatformConfiguration = + partyConfigurationsByRoleName.get(PintRole.RECEIVING_PLATFORM.getConfigName()); + if (receivingPlatformConfiguration != null) { + parties.add( + new PintReceivingPlatform( + standardVersion, + receivingPlatformConfiguration, + counterpartConfigurationsByRoleName.get(PintRole.SENDING_PLATFORM.getConfigName()), + persistentMap, + asyncWebClient, + orchestratorAuthHeader, + PayloadSignerFactory.testPayloadSigner() + )); + } + + return parties; + } + + public ScenarioListBuilder createScenarioListBuilder( + PartyConfiguration[] partyConfigurations, + CounterpartConfiguration[] counterpartConfigurations) { + return PintScenarioListBuilder.buildTree( + this.standardVersion, + _findPartyOrCounterpartName( + partyConfigurations, counterpartConfigurations, PintRole::isSendingPlatform), + _findPartyOrCounterpartName( + partyConfigurations, counterpartConfigurations, PintRole::isReceivingPlatform)); + } + + @Override + public SortedSet getRoleNames() { + return Arrays.stream(PintRole.values()) + .map(PintRole::getConfigName) + .collect(Collectors.toCollection(TreeSet::new)); + } + + public Set getReportRoleNames( + PartyConfiguration[] partyConfigurations, + CounterpartConfiguration[] counterpartConfigurations) { + return (partyConfigurations.length == PintRole.values().length + ? Arrays.stream(PintRole.values()).map(PintRole::getConfigName) + : Arrays.stream(counterpartConfigurations) + .map(CounterpartConfiguration::getRole) + .filter( + counterpartRole -> + Arrays.stream(partyConfigurations) + .map(PartyConfiguration::getRole) + .noneMatch(partyRole -> Objects.equals(partyRole, counterpartRole)))) + .collect(Collectors.toSet()); + } + + @SneakyThrows + public JsonNode getJsonSandboxConfigurationTemplate( + String testedPartyRole, boolean isManual, boolean isTestingCounterpartsConfig) { + return JsonToolkit.templateFileToJsonNode( + "/standards/pint/sandboxes/%s.json" + .formatted( + testedPartyRole == null + ? "auto-all-in-one" + : "%s-%s-%s" + .formatted( + isManual ? "manual" : "auto", + testedPartyRole.toLowerCase(), + isTestingCounterpartsConfig ? "testing-counterparts" : "tested-party")), + Map.ofEntries( + Map.entry("STANDARD_NAME_PLACEHOLDER", STANDARD_NAME), + Map.entry("STANDARD_VERSION_PLACEHOLDER", standardVersion), + Map.entry("SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER", SENDING_PLATFORM_AUTH_HEADER_VALUE), + Map.entry("RECEIVING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER", RECEIVING_PLATFORM_AUTH_HEADER_VALUE), + Map.entry( + "SANDBOX_ID_PREFIX", + AbstractComponentFactory._sandboxIdPrefix(STANDARD_NAME, standardVersion)))); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java new file mode 100644 index 00000000..7a6f8274 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/PintScenarioListBuilder.java @@ -0,0 +1,110 @@ +package org.dcsa.conformance.standards.eblinterop; + +import java.util.function.Function; +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.core.check.JsonSchemaValidator; +import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.core.scenario.ScenarioListBuilder; +import org.dcsa.conformance.standards.eblinterop.action.*; + +@Slf4j +public class PintScenarioListBuilder + extends ScenarioListBuilder { + + private static final ThreadLocal STANDARD_VERSION = new ThreadLocal<>(); + private static final ThreadLocal SENDING_PLATFORM_PARTY_NAME = new ThreadLocal<>(); + private static final ThreadLocal RECEIVING_PLATFORM_PARTY_NAME = new ThreadLocal<>(); + + private static final String ENVELOPE_REQUEST_SCHEMA = "EblEnvelope"; + private static final String TRANSFER_FINISHED_SIGNED_RESPONSE_SCHEMA = "EnvelopeTransferFinishedResponseSignedContent"; + + private static final String TRANSFER_STARTED_UNSIGNED_RESPONSE_SCHEMA = "EnvelopeTransferStartedResponse"; + + private static final String ENVELOPE_MANIFEST_SCHEMA = "EnvelopeManifest"; + private static final String ENVELOPE_TRANSFER_CHAIN_ENTRY_SCHEMA = "EnvelopeTransferChainEntry"; + + public static PintScenarioListBuilder buildTree( + String standardVersion, + String sendingPlatformPartyName, + String receivingPlatformPartyName) { + STANDARD_VERSION.set(standardVersion); + SENDING_PLATFORM_PARTY_NAME.set(sendingPlatformPartyName); + RECEIVING_PLATFORM_PARTY_NAME.set(receivingPlatformPartyName); + return supplyScenarioParameters().thenEither( + receiverStateSetup(ScenarioClass.NO_ISSUES) + .then(initiateTransferRequest(PintResponseCode.RECE).thenEither( + noAction(), + initiateTransferRequest(PintResponseCode.DUPE) + )), + + receiverStateSetup(ScenarioClass.INVALID_RECIPIENT).then( + initiateTransferRequest(PintResponseCode.BENV) + )/*, + scenarioType(ScenarioClass.FINISH_IMMEDIATELY, PintResponseCode.BENV)*/ + ); + } + + + private PintScenarioListBuilder( + Function actionBuilder) { + super(actionBuilder); + } + + + private static PintScenarioListBuilder supplyScenarioParameters() { + String sendingPlatform = SENDING_PLATFORM_PARTY_NAME.get(); + String receivingPlatform = RECEIVING_PLATFORM_PARTY_NAME.get(); + return new PintScenarioListBuilder( + previousAction -> + new SenderSupplyScenarioParametersAction( + receivingPlatform, + sendingPlatform, + (PintAction) previousAction + )); + } + + private static PintScenarioListBuilder receiverStateSetup(ScenarioClass scenarioClass) { + String sendingPlatform = SENDING_PLATFORM_PARTY_NAME.get(); + String receivingPlatform = RECEIVING_PLATFORM_PARTY_NAME.get(); + return new PintScenarioListBuilder( + previousAction -> new ReceiverSupplyScenarioParametersAndStateSetupAction( + receivingPlatform, + sendingPlatform, + (PintAction) previousAction, + scenarioClass)); + } + + private static PintScenarioListBuilder noAction() { + return new PintScenarioListBuilder(null); + } + + private static PintScenarioListBuilder initiateTransferRequest(PintResponseCode signedResponseCode) { + return _issuanceRequest(signedResponseCode); + } + + private static PintScenarioListBuilder _issuanceRequest( + PintResponseCode signedResponseCode + ) { + String sendingPlatform = SENDING_PLATFORM_PARTY_NAME.get(); + String receivingPlatform = RECEIVING_PLATFORM_PARTY_NAME.get(); + return new PintScenarioListBuilder( + previousAction -> + new PintInitiateTransferAction( + receivingPlatform, + sendingPlatform, + (PintAction) previousAction, + signedResponseCode, + resolveMessageSchemaValidator(ENVELOPE_REQUEST_SCHEMA), + resolveMessageSchemaValidator(TRANSFER_FINISHED_SIGNED_RESPONSE_SCHEMA), + resolveMessageSchemaValidator(ENVELOPE_MANIFEST_SCHEMA), + resolveMessageSchemaValidator(ENVELOPE_TRANSFER_CHAIN_ENTRY_SCHEMA) + )); + } + + + private static JsonSchemaValidator resolveMessageSchemaValidator(String schemaName) { + var standardVersion = STANDARD_VERSION.get(); + String schemaFilePath = "/standards/pint/schemas/pint-%s.json".formatted(standardVersion); + return JsonSchemaValidator.getInstance(schemaFilePath, schemaName); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java new file mode 100644 index 00000000..5c98549f --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintAction.java @@ -0,0 +1,124 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.core.scenario.OverwritingReference; +import org.dcsa.conformance.standards.eblinterop.models.DynamicScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.models.SenderScenarioParameters; + +public abstract class PintAction extends ConformanceAction { + protected final int expectedStatus; + private final OverwritingReference dspReference; + private final OverwritingReference rspReference; + private final OverwritingReference sspReference; + + public PintAction( + String sourcePartyName, + String targetPartyName, + PintAction previousAction, + String actionTitle, + int expectedStatus) { + super(sourcePartyName, targetPartyName, previousAction, actionTitle); + this.expectedStatus = expectedStatus; + if (previousAction == null) { + + this.dspReference = + new OverwritingReference<>( + null, new DynamicScenarioParameters()); + this.rspReference = new OverwritingReference<>(null, new ReceiverScenarioParameters("", "", "", "")); + this.sspReference = new OverwritingReference<>(null, new SenderScenarioParameters(null)); + } else { + this.dspReference = new OverwritingReference<>(previousAction.dspReference, null); + this.rspReference = new OverwritingReference<>(previousAction.rspReference, null); + this.sspReference = new OverwritingReference<>(previousAction.sspReference, null); + } + } + + + @Override + public ObjectNode exportJsonState() { + ObjectNode jsonState = super.exportJsonState(); + if (dspReference.hasCurrentValue()) { + jsonState.set("dspReference", dspReference.get().toJson()); + } + if (rspReference.hasCurrentValue()) { + jsonState.set("rspReference", rspReference.get().toJson()); + } + if (sspReference.hasCurrentValue()) { + jsonState.set("sspReference", sspReference.get().toJson()); + } + return jsonState; + } + + @Override + public void importJsonState(JsonNode jsonState) { + super.importJsonState(jsonState); + JsonNode dspNode = jsonState.get("dspReference"); + if (dspNode != null) { + dspReference.set(DynamicScenarioParameters.fromJson(dspNode)); + } + JsonNode rspNode = jsonState.get("rspReference"); + if (rspNode != null) { + rspReference.set(ReceiverScenarioParameters.fromJson(rspNode)); + } + JsonNode sspNode = jsonState.get("sspReference"); + if (sspNode != null) { + sspReference.set(SenderScenarioParameters.fromJson(sspNode)); + } + } + + + public DynamicScenarioParameters getDsp() { + var previousAction = (PintAction)this.previousAction; + if (previousAction != null) { + return previousAction.getDsp(); + } + return this.dspReference.get(); + } + + public void setDsp(DynamicScenarioParameters dsp) { + var previousAction = (PintAction)this.previousAction; + if (previousAction != null) { + previousAction.setDsp(dsp); + } else { + this.dspReference.set(dsp); + } + } + + public SenderScenarioParameters getSsp() { + var previousAction = (PintAction)this.previousAction; + if (previousAction != null) { + return previousAction.getSsp(); + } + return this.sspReference.get(); + } + + public void setSsp(SenderScenarioParameters sp) { + var previousAction = (PintAction)this.previousAction; + if (previousAction != null) { + previousAction.setSsp(sp); + } else { + this.sspReference.set(sp); + } + } + + + public ReceiverScenarioParameters getRsp() { + var previousAction = (PintAction)this.previousAction; + if (previousAction != null) { + return previousAction.getRsp(); + } + return this.rspReference.get(); + } + + public void setRsp(ReceiverScenarioParameters sp) { + var previousAction = (PintAction)this.previousAction; + if (previousAction != null) { + previousAction.setRsp(sp); + } else { + this.rspReference.set(sp); + } + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java new file mode 100644 index 00000000..87cd8c5e --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferAction.java @@ -0,0 +1,124 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +import static org.dcsa.conformance.standards.eblinterop.checks.PintChecks.validateInitiateTransferRequest; +import static org.dcsa.conformance.standards.eblinterop.checks.PintChecks.validateSignedFinishResponse; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.function.Supplier; +import java.util.stream.Stream; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.core.check.*; +import org.dcsa.conformance.core.traffic.HttpMessageType; +import org.dcsa.conformance.standards.eblinterop.checks.PintChecks; +import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSignerFactory; +import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; +import org.dcsa.conformance.standards.eblinterop.party.PintRole; + +@Getter +@Slf4j +public class PintInitiateTransferAction extends PintAction { + private final PintResponseCode pintResponseCode; + private final JsonSchemaValidator requestSchemaValidator; + private final JsonSchemaValidator responseSchemaValidator; + private final JsonSchemaValidator envelopeEnvelopeSchemaValidator; + private final JsonSchemaValidator envelopeTransferChainEntrySchemaValidator; + + public PintInitiateTransferAction( + String receivingPlatform, + String sendingPlatform, + PintAction previousAction, + PintResponseCode pintResponseCode, + JsonSchemaValidator requestSchemaValidator, + JsonSchemaValidator responseSchemaValidator, + JsonSchemaValidator envelopeEnvelopeSchemaValidator, + JsonSchemaValidator envelopeTransferChainEntrySchemaValidator + ) { + super( + sendingPlatform, + receivingPlatform, + previousAction, + "SingleRequestTransfer(%s)".formatted(pintResponseCode.name()), + 200 + ); + this.pintResponseCode = pintResponseCode; + this.requestSchemaValidator = requestSchemaValidator; + this.responseSchemaValidator = responseSchemaValidator; + this.envelopeEnvelopeSchemaValidator = envelopeEnvelopeSchemaValidator; + this.envelopeTransferChainEntrySchemaValidator = envelopeTransferChainEntrySchemaValidator; + } + + @Override + public String getHumanReadablePrompt() { + return ("Send transfer-transaction request"); + } + + @Override + public ObjectNode asJsonNode() { + return super.asJsonNode() + .put("transportDocumentReference", getSsp().transportDocumentReference()) + .set("rsp", getRsp().toJson()); + } + + @Override + public ConformanceCheck createCheck(String expectedApiVersion) { + return new ConformanceCheck(getActionTitle()) { + @Override + protected Stream createSubChecks() { + Supplier senderVerifierSupplier = () -> PayloadSignerFactory.testKeySignatureVerifier(); + return Stream.of( + new UrlPathCheck( + PintRole::isSendingPlatform, getMatchedExchangeUuid(), "/envelopes"), + new ResponseStatusCheck( + PintRole::isReceivingPlatform, getMatchedExchangeUuid(), expectedStatus), + new ApiHeaderCheck( + PintRole::isSendingPlatform, + getMatchedExchangeUuid(), + HttpMessageType.REQUEST, + expectedApiVersion), + new ApiHeaderCheck( + PintRole::isReceivingPlatform, + getMatchedExchangeUuid(), + HttpMessageType.RESPONSE, + expectedApiVersion), + new JsonSchemaCheck( + PintRole::isReceivingPlatform, + getMatchedExchangeUuid(), + HttpMessageType.RESPONSE, + responseSchemaValidator + ), + JsonAttribute.contentChecks( + PintRole::isSendingPlatform, + getMatchedExchangeUuid(), + HttpMessageType.REQUEST, + JsonAttribute.customValidator("envelopeManifestSignedContent signature could be validated", JsonAttribute.path("envelopeManifestSignedContent", PintChecks.signatureValidates(senderVerifierSupplier))), + JsonAttribute.allIndividualMatchesMustBeValid("envelopeManifestSignedContent signature could be validated", mav -> mav.submitAllMatching("envelopeTransferChain.*"), PintChecks.signatureValidates(senderVerifierSupplier)) + ), + JsonAttribute.contentChecks( + PintRole::isSendingPlatform, + getMatchedExchangeUuid(), + HttpMessageType.REQUEST, + JsonAttribute.customValidator("envelopeManifestSignedContent matches schema", JsonAttribute.path("envelopeManifestSignedContent", PintChecks.signedContentSchemaValidation(envelopeEnvelopeSchemaValidator))), + JsonAttribute.allIndividualMatchesMustBeValid("envelopeTransferChain matches schema", mav -> mav.submitAllMatching("envelopeTransferChain.*"), PintChecks.signedContentSchemaValidation(envelopeTransferChainEntrySchemaValidator)) + ), + new JsonSchemaCheck( + PintRole::isSendingPlatform, + getMatchedExchangeUuid(), + HttpMessageType.REQUEST, + requestSchemaValidator + ), + validateInitiateTransferRequest( + getMatchedExchangeUuid(), + () -> getSsp(), + () -> getRsp(), + () -> getDsp() + ), + validateSignedFinishResponse( + getMatchedExchangeUuid(), + pintResponseCode + ) + ); + } + }; + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintResponseCode.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintResponseCode.java new file mode 100644 index 00000000..94b60ee3 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintResponseCode.java @@ -0,0 +1,13 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +public enum PintResponseCode { + RECE, + DUPE, + BENV, + BSIG, + INCD, + MDOC, + DISE, + ; + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java new file mode 100644 index 00000000..9c5e0d20 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ReceiverSupplyScenarioParametersAndStateSetupAction.java @@ -0,0 +1,53 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; + +@Getter +@Slf4j +public class ReceiverSupplyScenarioParametersAndStateSetupAction extends PintAction { + private final ScenarioClass scenarioClass; + + public ReceiverSupplyScenarioParametersAndStateSetupAction( + String receivingPlatform, + String sendingPlatform, + PintAction previousAction, + ScenarioClass scenarioClass) { + super( + receivingPlatform, + sendingPlatform, + previousAction, + "ReceiverScenarioParametersAndSetup(%s)" + .formatted(scenarioClass.name()), + -1 + ); + this.scenarioClass = scenarioClass; + } + + @Override + public ObjectNode asJsonNode() { + return super.asJsonNode() + .put("scenarioClass", this.scenarioClass.name()) + .put("transportDocumentReference", this.getSsp().transportDocumentReference()); + } + + @Override + public boolean isInputRequired() { + return true; + } + + @Override + public void handlePartyInput(JsonNode partyInput) { + super.handlePartyInput(partyInput); + var rsp = ReceiverScenarioParameters.fromJson(partyInput.path("input")); + this.setRsp(rsp); + } + + @Override + public String getHumanReadablePrompt() { + return ("Setup the system for transfer and provide the following details for the sender."); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ScenarioClass.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ScenarioClass.java new file mode 100644 index 00000000..61557feb --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/ScenarioClass.java @@ -0,0 +1,8 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +public enum ScenarioClass { + + NO_ISSUES, + INVALID_RECIPIENT, + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java new file mode 100644 index 00000000..341d3bd8 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/SenderSupplyScenarioParametersAction.java @@ -0,0 +1,48 @@ +package org.dcsa.conformance.standards.eblinterop.action; + +import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.standards.eblinterop.models.SenderScenarioParameters; + +@Getter +@Slf4j +public class SenderSupplyScenarioParametersAction extends PintAction { + + public SenderSupplyScenarioParametersAction( + String platformPartyName, + String carrierPartyName, + PintAction previousAction) { + super( + carrierPartyName, + platformPartyName, + previousAction, + "SupplyScenarioParameters", + -1); + } + + @Override + public boolean isInputRequired() { + return true; + } + + @Override + public void handlePartyInput(JsonNode partyInput) { + super.handlePartyInput(partyInput); + var ssp = SenderScenarioParameters.fromJson(partyInput.path("input")); + this.setSsp(ssp); + } + + @Override + public JsonNode getJsonForHumanReadablePrompt() { + return new SenderScenarioParameters( + "TD reference" + ).toJson(); + } + + @Override + public String getHumanReadablePrompt() { + return ("Scenario details"); + } + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java new file mode 100644 index 00000000..b5883871 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/checks/PintChecks.java @@ -0,0 +1,248 @@ +package org.dcsa.conformance.standards.eblinterop.checks; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.nimbusds.jose.JWSObject; +import java.text.ParseException; +import java.util.*; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import org.dcsa.conformance.core.check.*; +import org.dcsa.conformance.core.traffic.HttpMessageType; +import org.dcsa.conformance.standards.eblinterop.action.PintResponseCode; +import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; +import org.dcsa.conformance.standards.eblinterop.models.DynamicScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.models.SenderScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.party.PintRole; + +public class PintChecks { + + private static final JsonPointer TDR_PTR = JsonPointer.compile("/transportDocument/transportDocumentReference"); + + public static JsonContentMatchedValidation anyArrayElementMatching(Predicate matcher, JsonContentMatchedValidation delegate, boolean invalidIfNoMatch) { + return (nodeToValidate,contextPath) -> { + boolean hadMatch = false; + Set issues = new LinkedHashSet<>(); + if (nodeToValidate.isArray()) { + int idx = -1; + for (var node : nodeToValidate) { + idx++; + if (matcher.test(node)) { + var r = delegate.validate(node, contextPath + "[" + idx + "]"); + issues.addAll(r); + hadMatch = true; + } + } + + if (invalidIfNoMatch && !hadMatch) { + issues.add("None of the elements in '" + contextPath + "' were the right type"); + } + } else if (invalidIfNoMatch){ + issues.add("'" + contextPath + "' as not an array"); + } + return issues; + }; + } + + public static JsonContentMatchedValidation path(int idx, JsonContentMatchedValidation delegate) { + return (nodeToValidate,contextPath) -> { + var pathSegment = "[" + idx + "]"; + var realIdx = idx; + if (realIdx == -1) { + pathSegment = "[(last)]"; + realIdx = nodeToValidate.size() - 1; + } + var node = nodeToValidate.path(realIdx); + var fullContext = contextPath.isEmpty() ? pathSegment : contextPath + pathSegment; + return delegate.validate(node, fullContext); + }; + } + + public static JsonContentMatchedValidation pathChain(JsonContentMatchedValidation delegate, Object ... paths) { + JsonContentMatchedValidation combined = delegate; + if (paths.length < 1) { + throw new IllegalArgumentException(); + } + for (int i = paths.length - 1 ; i >= 0; i--) { + var path = paths[i]; + if (path instanceof Integer pathIdx) { + combined = path(pathIdx, combined); + } else if (path instanceof String pathStr) { + combined = JsonAttribute.path(pathStr, combined); + } else { + throw new IllegalArgumentException("Only String and Integer paths are supported"); + } + } + return combined; + } + + public static JsonContentMatchedValidation signedContentValidation( + JsonContentCheck delegate) { + return (nodeToValidate,contextPath) -> delegate.validate(nodeToValidate); + } + + public static JsonContentMatchedValidation signedContentValidation( + JsonContentMatchedValidation delegate) { + return (nodeToValidate, contextPath) -> { + var content = nodeToValidate.asText(); + if (content == null || !content.contains(".")) { + return Set.of( + "The path '%s' should have been a signed payload, but was not".formatted(contextPath)); + } + JWSObject jwsObject; + try { + jwsObject = JWSObject.parse(content); + } catch (ParseException e) { + return Set.of( + "The path '%s' should have been a signed payload, but could not be parsed as a JWS." + .formatted(contextPath)); + } + JsonNode jsonBody; + try { + jsonBody = OBJECT_MAPPER.readTree(jwsObject.getPayload().toString()); + } catch (Exception e) { + return Set.of( + "The path '%s' should have been a signed payload containing Json as content, but could not be parsed: " + + e.toString()); + } + return delegate.validate(jsonBody, contextPath + "!"); + }; + } + + public static JsonContentMatchedValidation signedContentSchemaValidation( + JsonSchemaValidator schemaValidator) { + return (nodeToValidate, contextPath) -> { + var content = nodeToValidate.asText(); + if (content == null || !content.contains(".")) { + return Set.of("The path '%s' should have been a signed payload, but was not"); + } + JWSObject jwsObject = null; + try { + jwsObject = JWSObject.parse(content); + } catch (ParseException e) { + return Set.of( + "The path '%s' should have been a signed payload, but could not be parsed as a JWS."); + } + JsonNode jsonBody; + try { + jsonBody = OBJECT_MAPPER.readTree(jwsObject.getPayload().toString()); + } catch (Exception e) { + return Set.of( + "The path '%s' should have been a signed payload containing Json as content, but could not be parsed: " + + e.toString()); + } + return schemaValidator.validate(jsonBody); + }; + } + ; + + public static JsonContentMatchedValidation signatureValidates( + Supplier signatureVerifierSupplier) { + return (nodeToValidate, contextPath) -> { + var content = nodeToValidate.asText(); + if (content == null || !content.contains(".")) { + return Set.of("The path '%s' should have been a signed payload, but was not"); + } + JWSObject jwsObject; + try { + jwsObject = JWSObject.parse(content); + } catch (ParseException e) { + return Set.of( + "The path '%s' should have been a signed payload, but could not be parsed as a JWS."); + } + var signatureVerifier = signatureVerifierSupplier.get(); + if (signatureVerifier == null) { + throw new AssertionError("Missing signatureVerifier"); + } + if (!signatureVerifier.verifySignature(jwsObject)) { + return Set.of("The path '%s' was a valid JWS, but it was not signed by the expected key"); + } + return Set.of(); + }; + } + + private static void generateScenarioRelatedChecksForTransferRequest( + List checks, + Supplier sspSupplier, + Supplier rspSupplier, + Supplier dspSupplier) { + checks.add( + JsonAttribute.mustEqual( + "[Scenario] Verify that the correct 'transportDocumentReference' is used", + TDR_PTR, + delayedValue(sspSupplier, SenderScenarioParameters::transportDocumentReference))); + checks.add( + JsonAttribute.customValidator( + "[Scenario] Verify receiver EPUI is correct", + pathChain( + signedContentValidation( + pathChain( + anyArrayElementMatching( + (n) -> n.path("codeListProvider").asText("").equals("EPUI"), + JsonAttribute.path("partyCode", JsonAttribute.matchedMustEqual(delayedValue(rspSupplier, ReceiverScenarioParameters::receiverEPUI))), + true + ), + "transactions", -1, "recipient", "partyCodes") + // + ), + "envelopeTransferChain", -1 + ) + ) + ); + } + + public static ActionCheck validateSignedFinishResponse( + UUID matched, + PintResponseCode expectedResponseCode + ) { + var jsonContentChecks = new ArrayList(); + + jsonContentChecks.add( + JsonAttribute.customValidator( + "Validate that the response code was as expected", + signedContentValidation( + JsonAttribute.path("responseCode", JsonAttribute.matchedMustEqual(expectedResponseCode::name)) + ) + ) + ); + return JsonAttribute.contentChecks( + PintRole::isReceivingPlatform, + matched, + HttpMessageType.RESPONSE, + jsonContentChecks + ); + + } + + public static ActionCheck validateInitiateTransferRequest( + UUID matched, + Supplier sspSupplier, + Supplier rspSupplier, + Supplier dspSupplier + ) { + var jsonContentChecks = new ArrayList(); + + generateScenarioRelatedChecksForTransferRequest(jsonContentChecks, sspSupplier, rspSupplier, dspSupplier); + return JsonAttribute.contentChecks( + PintRole::isSendingPlatform, + matched, + HttpMessageType.REQUEST, + jsonContentChecks + ); + } + + private static Supplier delayedValue(Supplier cspSupplier, Function field) { + return () -> { + var csp = cspSupplier.get(); + if (csp == null) { + return null; + } + return field.apply(csp); + }; + } + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/Checksums.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/Checksums.java new file mode 100644 index 00000000..e0742e2b --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/Checksums.java @@ -0,0 +1,28 @@ +package org.dcsa.conformance.standards.eblinterop.crypto; + +import com.fasterxml.jackson.databind.JsonNode; +import io.setl.json.Canonical; +import io.setl.json.jackson.Convert; +import lombok.SneakyThrows; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.util.HexFormat; + +public class Checksums { + + @SneakyThrows + public static String sha256CanonicalJson(JsonNode node) { + var digester = MessageDigest.getInstance("SHA256"); + var digestBytes = digester.digest(((Canonical) Convert.toJson(node)).toCanonicalString().getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(digestBytes); + } + + @SneakyThrows + public static String sha256(String text) { + var digester = MessageDigest.getInstance("SHA256"); + var digestBytes = digester.digest(text.getBytes(StandardCharsets.UTF_8)); + return HexFormat.of().formatHex(digestBytes); + } + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/JWSSignerDetails.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/JWSSignerDetails.java new file mode 100644 index 00000000..c5c30b94 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/JWSSignerDetails.java @@ -0,0 +1,6 @@ +package org.dcsa.conformance.standards.eblinterop.crypto; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSSigner; + +public record JWSSignerDetails(JWSAlgorithm algorithm, JWSSigner signer) {} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSigner.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSigner.java new file mode 100644 index 00000000..dd83e2da --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSigner.java @@ -0,0 +1,5 @@ +package org.dcsa.conformance.standards.eblinterop.crypto; + +public interface PayloadSigner { + String sign(String payload); +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSignerFactory.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSignerFactory.java new file mode 100644 index 00000000..0316b754 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/PayloadSignerFactory.java @@ -0,0 +1,143 @@ +package org.dcsa.conformance.standards.eblinterop.crypto; + +import com.nimbusds.jose.JWSAlgorithm; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jose.crypto.RSASSASigner; +import com.nimbusds.jose.crypto.RSASSAVerifier; +import lombok.SneakyThrows; +import org.dcsa.conformance.standards.eblinterop.crypto.impl.DefaultPayloadSigner; + +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Base64; + +public class PayloadSignerFactory { + + // Generated with `openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 4 -subj "/C=US/ST=Delaware/L=Delaware/O=SELFSIGNED/CN=foo" -nodes` + // Contents in the `key.pem` + private static final String TEST_RSA_PRIVATE_KEY_PEM = """ + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCXTD3XOeBMYVZS + Pd1LmImkCzAvCqTZ/YnMh0uhYW3HUOBOdRvE++BY5uny8EZvKI4onH10SI1Wm+oy + HPBPDFA0jP4SN/v83uPke67a6IuMcoQumZVHLY5+plg/w8YehGv0+sdwPY5UuO1O + IcnJoc7b7o7elJC3alJ+hXWvATE+uaw0dcNxbQf+6GaBRY1u7iw/XI0k0LuNo1sI + EuirnoFfDWMErFOhtRJo2DNQOAACbOfM+dTIxTACKdrz3K5GdLoClIT1SoJzq5S8 + s/uc4e/CN4hJLcKFHUNQswvS7ba6SD4jf/qiGo/wUTgyaboygED9V33ZUsrfyTnY + wSoGfDvzAgMBAAECggEALI2sDEwjy/pB9Df5icBij+cnikLFJthtksgosl5BeJdN + Zm0//zL47tUZAYxWAXfc3QKwQuT2khGZ1qYE8hI7MC5wxyarUtzEGU1+wUIHjhVO + 7XYWqn403wDXLffVyLjQHbUXs+q8liBa6U4z4OeARe2rLsprD0gFAPMGI8HjIYgR + bz+953x695vbOA9DCommw3fdJLiKckLj9i/o0TyNv1aLRyQjrjCSB5JYcfo/rNjJ + eTItoox2/oJYzZeJQ3SdMLf5iqEF8AONCg1+4td25B2KiJhnnbEuHkIp4na0wqeQ + mpqjuYreTiL86vRjB+8ujaa7X2xUorFuXA9Z4qEb4QKBgQDVWFUtDXEBfYyGUU1C + Hoe/SJsvwaQ++amI0Y04rZ6pgA271PfikV2l+W1KMhkxajD8LQ5Nx7WtvhaSuBeT + TwS+1MNL7JF7HhpuoMCy4VgfPivnErnZLUkv8o0HytDK+vtq5c+BwD33NuqPKas4 + cAosNEyE4d09aYMs88Xmxbr8AwKBgQC1jCMsDkmy6JNg5e6R1UdvTAOWqFrHb0fU + S2cmyn/++kEUsUw4rvk58m4v1Ci72ZxdkhpDEJRIneoJjrYJzUUS5GuR9EYlk+tB + wjFTNRfzrGenO7F4pJUA3uGG6JMKy7LQ2d4Sm3GM8eR6PHkvO/O0+ZNSjlNDDwS7 + rhTuW4zVUQKBgDBCqBnl5X9J0ET+FTT0xQ5fNUOrUSUxwskBZinBFJgRMIoh1eU5 + ru6BqthS1uIXvHb/FjJAD/f6fQ65eBPJlzA33unI3Ov11lLaKF0OnqmKndHKqaHY + Hasr+f0eQvb3qXH4BGW8gAfxM0QpT+MXbSWsuvaARVTEDnlXt5fJeM/TAoGAB/sW + HLywDrZcrDjPaQfIMSNVUQ0rmHLS5IlACpuCTvIvZDp7EE7Y0+xNXbrk44Uoc5CV + qPcUnbCbdjoY1It6it8Rv4POhZ5gDC7+PhsqZ2Lf16EvJw+NIVGq9mRI+oOD49x/ + /69nqXuEwL7h0OrAxublzA5HqL4DRkDb2LKbmVECgYB+XvRvR0GDE6Wo1zYI2sd/ + omQJ0jJ3rT65rWPLPPHg8Pe9Z8VdK56EoJUCStlqDvF4dct+wNt4O5rUJRTpv2jh + ySiy2tAy9P+r91+PQW9Z8p9ecDi/BR2s9TLdceCGjvY518KD5HLLzP8LzG88VySr + s/bpdr+2hBUcSaTF5KXNGw== + -----END PRIVATE KEY----- + """; + + private static final String TEST_INCORRECT_RSA_PRIVATE_KEY_PEM = """ + -----BEGIN PRIVATE KEY----- + MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC588633cONawxd + r7ynVvwmeRd2KVTpskYHxn51qYwUqK1jVPxGgDy1I8j+ImWbI1Q8ZaDbvWoIHcrv + sQoO7FIDsBBb31nXbxqwI4bGop0qlbFe+lYlHaBfFe7o0Tmokb1HUUZuFk+9sgjn + AkGypVynRNQW0OGgCxzbvMS4n6DjU9TY2r9obnIz3+5dOf/MI4Y2Qv79hJxMyerj + 51u4XX1PYl0nYp6baJxwOrSe2qIGfUHUVEnTApI1pELmjph9bzYKvgjMNZndTfRP + gVC91ClZoN24/icp50dhvHKxb/7YYeTryYCIbguF4Xww+r9MQqaKCJ4ho7DAtLHk + cWuKzqZlAgMBAAECggEAPs27/zicn+pMSmYc0u3bisjyJhvujGGEKoMdWfMSFzYj + HX3qGIueNVWpQD/wzjVf8WgnrJ+sLKKXVF4YdhLV3l38IHNungbt3hiZoAPzDhtx + xRDKwI1hiUvYnXRww2C5q1klbvAFLZ3wSMln1ATqpqnl8fDJi2rFa+e1D2AGkFBA + GSSK5TmKwf7S32GOkja+JG8NG+WjQPMxU1qGcSkDVJNmOqHk1OMYgKjPlha8Dkpv + cX2pNaUyMuETogN0oYogtI3RNlBj9i0yvDkKcy2xtDFXcoBWPl0PIsgXSFW4CRJM + tFjdLfdUZnfd+mGDJUabourKwla4y1mw8v5lydI+XwKBgQDdZEpr7GnMlok0op1N + AFNuCbErpInv8e2dLZ5bW3I8FG3be+jOpPGOBiZboNUndO9UH4bHIkw20GsAbgam + 5F9JNgbLCoSQaBx5OpdsY0EGZ8fbTSPiwx2SpFfY4dT8tGFG3a+61FbYeuuuhpU1 + zlm7lMaMmZg0JK/julShg++xRwKBgQDXBUt1PdMWmtbzHYiyOHODVWlYZMjteW3g + lXI6xHMPEDA9Vu3xilat9arjno+Aa6XoP4UhXJukZCObzYsZeHYfSLOirU/KK3Rg + p7Olp2LqNHWltQCI/UPhPBeUHxuYyST/8HZ/5qNh6MBsN2BVDMEaox0koW6EIc94 + /i0iSxug8wKBgHpwnewkGrsoQgeXK7HLTVjdCVweqp7GSOiVsy/JWls53Sv20mF+ + vY0Tf6FLSLeCp1359ZsqL8Zc6+CX+RvRz5T4yTb/wSLwQVcWfWpXVj4JpXF2rzMZ + P8C7HU54T0fXJrl/n1GPX9xn1vJ1wg246s2gUVKvG4szAwfKJEYTZru/AoGAZTI8 + vUUHn8/n8iuoNhiTZPBB0DQ+zGUl7Vjolff3HtPDoFrVSaSN/vlsIAx0BUCkqJWc + loL7TXdDuwQVvzsOfNK+mIVw0/l3oDXNOt14lDl0VTTGt7JazBp4DmJFnrasDzig + zLlDk8TzKvs0/1ItX9f800yWsuEmwA8ANu+aZTkCgYByfPqSfpLdmPdLNhg6UY1D + d1UNDFXNPyFYoqHrX1NXtDfBVeW50kDBHKBROOAUgJhwKG85aFXvwkqwUKPvMszv + GVZhkK0swLScgg5I2td8jw6hmpG0DjyFx814VWPBfou+SEnyKxlYCETAWrsyvrei + qGGESdtrmEydMGuuOIZ7zA== + -----END PRIVATE KEY----- + """; + + private static final KeyPair TEST_RSA_KEY_PAIR = parsePEMRSAKey(TEST_RSA_PRIVATE_KEY_PEM); + private static final KeyPair TEST_INCORRECT_RSA_KEY_PAIR = parsePEMRSAKey(TEST_INCORRECT_RSA_PRIVATE_KEY_PEM); + private static final PayloadSigner TEST_KEY_PAYLOAD_SIGNER = rsaBasedPayloadSigner(TEST_RSA_KEY_PAIR); + private static final PayloadSigner TEST_INCORRECT_KEY_PAYLOAD_SIGNER = rsaBasedPayloadSigner(TEST_INCORRECT_RSA_KEY_PAIR); + private static final SignatureVerifier TEST_KEY_SIGNATURE_VERIFIER = singleRSAKeySignatureVerifier(TEST_RSA_KEY_PAIR); + + public static PayloadSigner testPayloadSigner() { + return TEST_KEY_PAYLOAD_SIGNER; + } + + public static PayloadSigner testIncorrectPayloadSigner() { + return TEST_INCORRECT_KEY_PAYLOAD_SIGNER; + } + + public static SignatureVerifier testKeySignatureVerifier() { + return TEST_KEY_SIGNATURE_VERIFIER; + } + + private static PayloadSigner rsaBasedPayloadSigner(KeyPair keyPair) { + return new DefaultPayloadSigner( + new JWSSignerDetails( + JWSAlgorithm.PS256, + new RSASSASigner(keyPair.getPrivate()) + ) + ); + } + + public static SignatureVerifier fromJWSVerifier(JWSVerifier jwsVerifier) { + return new SingleKeySignatureVerifier(jwsVerifier); + } + + private static SignatureVerifier singleRSAKeySignatureVerifier(KeyPair keyPair) { + return new SingleKeySignatureVerifier(new RSASSAVerifier((RSAPublicKey) keyPair.getPublic())); + } + + @SneakyThrows + private static KeyPair parsePEMRSAKey(String pem) { + String privKeyPEM = pem.replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s++", ""); + + byte [] encoded = Base64.getDecoder().decode(privKeyPEM); + + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded); + KeyFactory kf = KeyFactory.getInstance("RSA"); + var privateKey = (RSAPrivateCrtKey)kf.generatePrivate(keySpec); + var publicKey = kf.generatePublic(new RSAPublicKeySpec(privateKey.getModulus(), privateKey.getPublicExponent())); + return new KeyPair(publicKey, privateKey); + } + + private record SingleKeySignatureVerifier(JWSVerifier jwsVerifier) implements SignatureVerifier { + + @SneakyThrows + @Override + public boolean verifySignature(JWSObject jwsObject) { + return jwsObject.verify(jwsVerifier); + } + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/SignatureVerifier.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/SignatureVerifier.java new file mode 100644 index 00000000..bb2ae37e --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/SignatureVerifier.java @@ -0,0 +1,22 @@ +package org.dcsa.conformance.standards.eblinterop.crypto; + +import com.nimbusds.jose.JWSObject; + +/** + * Method for verifying signatures + * + *

+ * Abstracts away logic of trust store management behind this interface as those are generally + * implementation details. + *

+ */ +public interface SignatureVerifier { + + /** + * @param jwsObject The JWS object that has a signature to be validated + * @return true if and only the implementation deemed the JWSObject provided to have a valid + * signature from the provided platform. + */ + boolean verifySignature(JWSObject jwsObject); + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/impl/DefaultPayloadSigner.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/impl/DefaultPayloadSigner.java new file mode 100644 index 00000000..5d1c3a5c --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/crypto/impl/DefaultPayloadSigner.java @@ -0,0 +1,23 @@ +package org.dcsa.conformance.standards.eblinterop.crypto.impl; + +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jose.JWSObject; +import com.nimbusds.jose.Payload; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.dcsa.conformance.standards.eblinterop.crypto.JWSSignerDetails; +import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSigner; + +@RequiredArgsConstructor +public class DefaultPayloadSigner implements PayloadSigner { + + private final JWSSignerDetails jwsSignerDetails; + + @SneakyThrows + public String sign(String payload) { + JWSHeader header = new JWSHeader.Builder(jwsSignerDetails.algorithm()).build(); + JWSObject jwsObject = new JWSObject(header, new Payload(payload)); + jwsObject.sign(jwsSignerDetails.signer()); + return jwsObject.serialize(); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/DynamicScenarioParameters.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/DynamicScenarioParameters.java new file mode 100644 index 00000000..be936b3b --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/DynamicScenarioParameters.java @@ -0,0 +1,18 @@ +package org.dcsa.conformance.standards.eblinterop.models; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.With; + +@With +public record DynamicScenarioParameters() { + public ObjectNode toJson() { + return OBJECT_MAPPER.createObjectNode(); + } + + public static DynamicScenarioParameters fromJson(JsonNode jsonNode) { + return new DynamicScenarioParameters(); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/ReceiverScenarioParameters.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/ReceiverScenarioParameters.java new file mode 100644 index 00000000..e38d2b42 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/ReceiverScenarioParameters.java @@ -0,0 +1,33 @@ +package org.dcsa.conformance.standards.eblinterop.models; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.With; + +@With +public record ReceiverScenarioParameters( + String eblPlatform, + String receiverLegalName, + String receiverEPUI, + String receiverEPUICodeListName +) { + public ObjectNode toJson() { + return OBJECT_MAPPER + .createObjectNode() + .put("eblPlatform", eblPlatform) + .put("receiverLegalName", receiverLegalName) + .put("receiverEPUI", receiverEPUI) + .put("receiverEPUICodeListName", receiverEPUICodeListName); + } + + public static ReceiverScenarioParameters fromJson(JsonNode jsonNode) { + return new ReceiverScenarioParameters( + jsonNode.required("eblPlatform").asText(), + jsonNode.required("receiverLegalName").asText(), + jsonNode.required("receiverEPUI").asText(), + jsonNode.required("receiverEPUICodeListName").asText() + ); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java new file mode 100644 index 00000000..84ce6e5f --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/SenderScenarioParameters.java @@ -0,0 +1,22 @@ +package org.dcsa.conformance.standards.eblinterop.models; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.With; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +@With +public record SenderScenarioParameters(String transportDocumentReference) { + public ObjectNode toJson() { + return OBJECT_MAPPER + .createObjectNode() + .put("transportDocumentReference", transportDocumentReference); + } + + public static SenderScenarioParameters fromJson(JsonNode jsonNode) { + return new SenderScenarioParameters( + jsonNode.required("transportDocumentReference").asText() + ); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java new file mode 100644 index 00000000..f08863e6 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TDReceiveState.java @@ -0,0 +1,123 @@ +package org.dcsa.conformance.standards.eblinterop.models; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.nimbusds.jose.JWSObject; +import org.dcsa.conformance.core.state.JsonNodeMap; +import org.dcsa.conformance.standards.eblinterop.action.PintResponseCode; + +import java.text.ParseException; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +public class TDReceiveState { + + private static final String TRANSPORT_DOCUMENT_REFERENCE = "transportDocumentReference"; + private static final String TRANSFER_STATE = "transferState"; + private static final String EXPECTED_RECEIVER = "expectedReceiver"; + + private ObjectNode state; + + private TDReceiveState(ObjectNode state) { + this.state = state; + } + + + public TransferState getTransferState() { + var v = state.path(TRANSFER_STATE).asText(null); + if (v == null) { + return TransferState.NOT_STARTED; + } + return TransferState.valueOf(v); + } + + private void setTransferState(TransferState transferState) { + state.put(TRANSFER_STATE, transferState.name()); + } + + + public String getTransportDocumentReference() { + return state.path("transportDocumentReference").asText(); + } + + public void setExpectedReceiver(String receiverEPUI) { + this.state.put(EXPECTED_RECEIVER, receiverEPUI); + } + + + public PintResponseCode recommendedFinishTransferResponse(JsonNode initiateRequest) { + var etc = initiateRequest.path("envelopeTransferChain"); + var lastEtcEntry = etc.path(etc.size() - 1); + JsonNode etcEntryParsed; + try { + var jwsObject = JWSObject.parse(lastEtcEntry.asText()); + etcEntryParsed = OBJECT_MAPPER.readTree(jwsObject.getPayload().toString()); + } catch (ParseException | JsonProcessingException e) { + return PintResponseCode.BENV; + } + var transactions = etcEntryParsed.path("transactions"); + var lastTransactionNode = transactions.path(transactions.size() - 1); + var recipient = lastTransactionNode.path("recipient"); + var recipientPartyCodes = recipient.path("partyCodes"); + var expectedReceiver = state.path(EXPECTED_RECEIVER).asText(); + boolean hasExpectedCode = false; + for (var partyCodeNode : recipientPartyCodes) { + if (!partyCodeNode.path("codeListProvider").asText("").equals("EPUI")) { + continue; + } + + if (partyCodeNode.path("partyCode").asText("").equals(expectedReceiver)) { + hasExpectedCode = true; + break; + } + } + + if (!hasExpectedCode) { + return PintResponseCode.BENV; + } + // TODO; Add check for missing documents here. + var responseCode = PintResponseCode.RECE; + if (this.getTransferState() == TransferState.ACCEPTED) { + responseCode = PintResponseCode.DUPE; + } + return responseCode; + } + + public void updateTransferState(PintResponseCode code) { + var state = switch (code){ + case RECE, DUPE -> TransferState.ACCEPTED; + case BENV, BSIG, DISE -> TransferState.REJECTED; + case INCD, MDOC -> TransferState.INCOMPLETE; + }; + this.setTransferState(state); + } + + public static TDReceiveState fromPersistentStore(JsonNode state) { + return new TDReceiveState((ObjectNode) state); + } + + public static TDReceiveState newInstance(String transportDocumentReference) { + var state = OBJECT_MAPPER.createObjectNode() + .put(TRANSPORT_DOCUMENT_REFERENCE, transportDocumentReference) + .put(TRANSFER_STATE, TransferState.NOT_STARTED.name()); + return new TDReceiveState(state); + } + + public JsonNode asPersistentState() { + return this.state; + } + + public static TDReceiveState fromPersistentStore(JsonNodeMap jsonNodeMap, String transportDocumentReference) { + var data = jsonNodeMap.load(transportDocumentReference); + if (data == null) { + throw new IllegalArgumentException("Unknown TD Reference: " + transportDocumentReference); + } + return fromPersistentStore(data); + } + + public void save(JsonNodeMap jsonNodeMap) { + jsonNodeMap.save(getTransportDocumentReference(), asPersistentState()); + } + +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TransferState.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TransferState.java new file mode 100644 index 00000000..7ae1e8b2 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/models/TransferState.java @@ -0,0 +1,9 @@ +package org.dcsa.conformance.standards.eblinterop.models; + +public enum TransferState { + ACCEPTED, + STARTED, + REJECTED, + INCOMPLETE, + NOT_STARTED, +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java new file mode 100644 index 00000000..f553456d --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintReceivingPlatform.java @@ -0,0 +1,197 @@ +package org.dcsa.conformance.standards.eblinterop.party; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import java.util.*; +import java.util.function.Consumer; + +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.core.party.ConformanceParty; +import org.dcsa.conformance.core.party.CounterpartConfiguration; +import org.dcsa.conformance.core.party.PartyConfiguration; +import org.dcsa.conformance.core.party.PartyWebClient; +import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.core.state.JsonNodeMap; +import org.dcsa.conformance.core.traffic.ConformanceMessageBody; +import org.dcsa.conformance.core.traffic.ConformanceRequest; +import org.dcsa.conformance.core.traffic.ConformanceResponse; +import org.dcsa.conformance.standards.eblinterop.action.ReceiverSupplyScenarioParametersAndStateSetupAction; +import org.dcsa.conformance.standards.eblinterop.action.ScenarioClass; +import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSigner; +import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.models.TDReceiveState; + +@Slf4j +public class PintReceivingPlatform extends ConformanceParty { + private final Map eblStatesByTdr = new HashMap<>(); + + private final PayloadSigner payloadSigner; + + public PintReceivingPlatform( + String apiVersion, + PartyConfiguration partyConfiguration, + CounterpartConfiguration counterpartConfiguration, + JsonNodeMap persistentMap, + PartyWebClient asyncWebClient, + Map> orchestratorAuthHeader, + PayloadSigner payloadSigner) { + super( + apiVersion, + partyConfiguration, + counterpartConfiguration, + persistentMap, + asyncWebClient, + orchestratorAuthHeader); + this.payloadSigner = payloadSigner; + } + + @Override + protected void doReset() { + eblStatesByTdr.clear(); + } + + @Override + protected Map, Consumer> getActionPromptHandlers() { + return Map.ofEntries( + Map.entry(ReceiverSupplyScenarioParametersAndStateSetupAction.class, this::initiateState) + ); + } + + + private void initiateState(JsonNode actionPrompt) { + log.info("EblInteropSendingPlatform.handleScenarioTypeAction(%s)".formatted(actionPrompt.toPrettyString())); + var tdr = actionPrompt.required("transportDocumentReference").asText(); + var existing = persistentMap.load(tdr); + if (existing != null){ + throw new IllegalStateException("Please do not reuse TDRs between scenarios in the conformance test"); + } + var scenarioClass = ScenarioClass.valueOf(actionPrompt.required("scenarioClass").asText()); + var expectedRecipient = "12345-jane-doe"; + var receivingParameters = new ReceiverScenarioParameters( + "CARX", + "Jane Doe", + scenarioClass == ScenarioClass.INVALID_RECIPIENT ? "12345-invalid" : expectedRecipient, + "CargoX" + ); + + var tdState = TDReceiveState.newInstance(tdr); + tdState.setExpectedReceiver(expectedRecipient); + tdState.save(persistentMap); + asyncOrchestratorPostPartyInput( + OBJECT_MAPPER + .createObjectNode() + .put("actionId", actionPrompt.required("actionId").asText()) + .set("input", receivingParameters.toJson())); + addOperatorLogEntry( + "Finished ScenarioType"); + } + + + public ConformanceResponse handleInitiateTransferRequest(ConformanceRequest request) { + var transferRequest = request.message().body().getJsonBody(); + var tdr = transferRequest.path("transportDocument").path("transportDocumentReference").asText(); + var receiveState = TDReceiveState.fromPersistentStore(persistentMap, tdr); + var lastEnvelopeTransferChainEntrySignedContentChecksum = "..."; + var unsignedPayload = OBJECT_MAPPER.createObjectNode() + .put("lastEnvelopeTransferChainEntrySignedContentChecksum", lastEnvelopeTransferChainEntrySignedContentChecksum); + var responseCode = receiveState.recommendedFinishTransferResponse(transferRequest); + + if (responseCode != null) { + var visualizationChecksum = transferRequest.path("eBLVisualisationByCarrier") + .path("documentChecksum") + .asText(null); + var allSupportingDocuments = transferRequest.path("supportingDocuments"); + receiveState.updateTransferState(responseCode); + unsignedPayload.put("responseCode", responseCode.name()); + var receivedDocuments = unsignedPayload.putArray("receivedAdditionalDocumentChecksums"); + if (visualizationChecksum != null) { + receivedDocuments.add(visualizationChecksum); + } + for (var documentNode : allSupportingDocuments) { + var documentChecksum = documentNode.path("documentChecksum").asText(null); + if (documentChecksum != null) { + receivedDocuments.add(documentChecksum); + } + } + var signedPayload = payloadSigner.sign(unsignedPayload.toString()); + var signedPayloadJsonNode = TextNode.valueOf(signedPayload); + receiveState.save(persistentMap); + return request.createResponse( + 200, + Map.of("API-Version", List.of(apiVersion)), + new ConformanceMessageBody(signedPayloadJsonNode) + ); + } + var envelopeReference = "..."; + var transportDocumentChecksum = "..."; + unsignedPayload + .put("envelopeReference", envelopeReference) + .put("transportDocumentChecksum", transportDocumentChecksum); + unsignedPayload.putArray("missingAdditionalDocumentChecksums"); + receiveState.save(persistentMap); + return request.createResponse( + 201, + Map.of("API-Version", List.of(apiVersion)), + new ConformanceMessageBody(unsignedPayload) + ); + } + + @Override + protected void exportPartyJsonState(ObjectNode targetObjectNode) {} + + @Override + protected void importPartyJsonState(ObjectNode sourceObjectNode) {} + + @Override + public ConformanceResponse handleRequest(ConformanceRequest request) { + log.info("EblInteropPlatform.handleRequest(%s)".formatted(request)); + + var url = request.url().replaceFirst("/++$", ""); + if (url.endsWith("/envelopes")) { + return handleInitiateTransferRequest(request); + } + JsonNode jsonRequest = request.message().body().getJsonBody(); + + String tdr = jsonRequest.path("transportDocument").path("transportDocumentReference").asText(null); + + ConformanceResponse response; + if (tdr == null) { + response = + request.createResponse( + 400, + Map.of("Api-Version", List.of(apiVersion)), + new ConformanceMessageBody( + OBJECT_MAPPER + .createObjectNode() + .put( + "message", + "Rejecting issuance request as it has no transport document reference"))); + } else if (!eblStatesByTdr.containsKey(tdr)) { + eblStatesByTdr.put(tdr, PintTransferState.ISSUANCE_REQUESTED); + response = + request.createResponse( + 204, + Map.of("Api-Version", List.of(apiVersion)), + new ConformanceMessageBody(OBJECT_MAPPER.createObjectNode())); + } else { + response = + request.createResponse( + 409, + Map.of("Api-Version", List.of(apiVersion)), + new ConformanceMessageBody( + OBJECT_MAPPER + .createObjectNode() + .put( + "message", + "Rejecting issuance request for document '%s' because it is in state '%s'" + .formatted(tdr, eblStatesByTdr.get(tdr))))); + } + addOperatorLogEntry( + "Handling issuance request for eBL with transportDocumentReference '%s' (now in state '%s')" + .formatted(tdr, eblStatesByTdr.get(tdr))); + return response; + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintRole.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintRole.java new file mode 100644 index 00000000..06e42907 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintRole.java @@ -0,0 +1,23 @@ +package org.dcsa.conformance.standards.eblinterop.party; + +import lombok.Getter; + +@Getter +public enum PintRole { + SENDING_PLATFORM("SendingPlatform"), + RECEIVING_PLATFORM("ReceivingPlatform"); + + private final String configName; + + PintRole(String configName) { + this.configName = configName; + } + + public static boolean isSendingPlatform(String configName) { + return PintRole.SENDING_PLATFORM.configName.equals(configName); + } + + public static boolean isReceivingPlatform(String configName) { + return PintRole.RECEIVING_PLATFORM.configName.equals(configName); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java new file mode 100644 index 00000000..2ff4e29b --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintSendingPlatform.java @@ -0,0 +1,238 @@ +package org.dcsa.conformance.standards.eblinterop.party; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.fasterxml.jackson.databind.util.RawValue; +import java.time.Instant; +import java.util.*; +import java.util.function.Consumer; +import lombok.extern.slf4j.Slf4j; +import org.dcsa.conformance.core.party.ConformanceParty; +import org.dcsa.conformance.core.party.CounterpartConfiguration; +import org.dcsa.conformance.core.party.PartyConfiguration; +import org.dcsa.conformance.core.party.PartyWebClient; +import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.core.state.JsonNodeMap; +import org.dcsa.conformance.core.toolkit.JsonToolkit; +import org.dcsa.conformance.core.traffic.ConformanceMessageBody; +import org.dcsa.conformance.core.traffic.ConformanceRequest; +import org.dcsa.conformance.core.traffic.ConformanceResponse; +import org.dcsa.conformance.standards.eblinterop.action.PintInitiateTransferAction; +import org.dcsa.conformance.standards.eblinterop.action.SenderSupplyScenarioParametersAction; +import org.dcsa.conformance.standards.eblinterop.crypto.Checksums; +import org.dcsa.conformance.standards.eblinterop.crypto.PayloadSigner; +import org.dcsa.conformance.standards.eblinterop.models.ReceiverScenarioParameters; +import org.dcsa.conformance.standards.eblinterop.models.SenderScenarioParameters; + +@Slf4j +public class PintSendingPlatform extends ConformanceParty { + + private static final Random RANDOM = new Random(); + + private static final Map PLATFORM2CODELISTNAME = Map.ofEntries( + Map.entry("WAVE", "Wave"), + Map.entry("CARX", "CargoX"), + Map.entry("EDOX", "EdoxOnline"), + Map.entry("IQAX", "IQAX"), + Map.entry("ESSD", "EssDOCS"), + Map.entry("BOLE", "Bolero"), + Map.entry("TRGO", "TradeGO"), + Map.entry("SECR", "Secro")/*, + Map.entry("", "GSBN"), + Map.entry("", "WiseTech") + */ + ); + + private final Map eblStatesByTdr = new HashMap<>(); + + private final PayloadSigner payloadSigner; + + public PintSendingPlatform( + String apiVersion, + PartyConfiguration partyConfiguration, + CounterpartConfiguration counterpartConfiguration, + JsonNodeMap persistentMap, + PartyWebClient asyncWebClient, + Map> orchestratorAuthHeader, + PayloadSigner payloadSigner + ) { + super( + apiVersion, + partyConfiguration, + counterpartConfiguration, + persistentMap, + asyncWebClient, + orchestratorAuthHeader); + this.payloadSigner = payloadSigner; + } + + @Override + protected void exportPartyJsonState(ObjectNode targetObjectNode) {} + + @Override + protected void importPartyJsonState(ObjectNode sourceObjectNode) {} + + @Override + protected void doReset() {} + + @Override + protected Map, Consumer> getActionPromptHandlers() { + return Map.ofEntries( + Map.entry(SenderSupplyScenarioParametersAction.class, this::supplyScenarioParameters), + Map.entry(PintInitiateTransferAction.class, this::sendIssuanceRequest) + ); + } + + private static final char[] TDR_CHARS = ( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789" + // spaces - cannot come first nor last + + " " + // Symbols, also do not count as spaces. TODO, add <> when DT-902 is fixed + + "-_+!/\\`\"'*^~.:,;(){}[]@$¤&%¤&§½" + // Spec says "\S+(\s+\S+)*". Unicode smiley count as "not space". + // TODO: Add when DT-903 is fixed + //+ "✉\uD83D\uDEA2" + ).toCharArray(); + + private String generateTDR() { + var tdrChars = new StringBuilder(20); + // The breaker is sticky to limit how many times we will + // poll the random. + // Also, you might be tempted to think this is a good way + // to generate passwords/keys. You would be wrong in that + // case as this generator has bias that is not policy + // defined, and you do not want that. + int breakerLimit = 20; + for (int i = 0 ; i < 19 ; i++) { + var c = TDR_CHARS[RANDOM.nextInt(TDR_CHARS.length)]; + if ((i < 1 || i > 18)) { + while (Character.isSpaceChar(c) && breakerLimit-- > 0) { + c = TDR_CHARS[RANDOM.nextInt(TDR_CHARS.length)]; + } + + if (Character.isSpaceChar(c)) { + // In the unlikely even that random keeps pulling + // a space, we just pick the first letter and move + // on. This ensures we will not hang forever with + // a slight bias towards "A". But it is not a + // password/key, so the bias is of no consequence. + c = TDR_CHARS[0]; + assert !Character.isSpaceChar(c); + } + } + tdrChars.append(c); + } + return tdrChars.toString(); + } + + private void supplyScenarioParameters(JsonNode actionPrompt) { + log.info("EblInteropSendingPlatform.supplyScenarioParameters(%s)".formatted(actionPrompt.toPrettyString())); + var tdr = generateTDR(); + var scenarioParameters = new SenderScenarioParameters(tdr); + asyncOrchestratorPostPartyInput( + OBJECT_MAPPER + .createObjectNode() + .put("actionId", actionPrompt.required("actionId").asText()) + .set("input", scenarioParameters.toJson())); + addOperatorLogEntry( + "Provided ScenarioParameters: %s".formatted(scenarioParameters)); + } + + private void sendIssuanceRequest(JsonNode actionPrompt) { + log.info("EblInteropSendingPlatform.sendIssuanceRequest(%s)".formatted(actionPrompt.toPrettyString())); + String tdr = actionPrompt.required("transportDocumentReference").asText(); + + boolean isCorrect = actionPrompt.path("isCorrect").asBoolean(); + if (isCorrect) { + eblStatesByTdr.put(tdr, PintTransferState.ISSUANCE_REQUESTED); + } + + var body = OBJECT_MAPPER.createObjectNode(); + + var tdPayload = (ObjectNode) + JsonToolkit.templateFileToJsonNode( + "/standards/pint/messages/pint-%s-transport-document.json" + .formatted(apiVersion), + Map.of()); + // Manually set TDR. The replacement does raw text subst, which + // is then parsed as JSON. + tdPayload.put("transportDocumentReference", tdr); + body.set("transportDocument", tdPayload); + if (!isCorrect && tdPayload.path("transportDocument").has("issuingParty")) { + ((ObjectNode) tdPayload.path("transportDocument")).remove("issuingParty"); + } + var rsp = ReceiverScenarioParameters.fromJson(actionPrompt.required("rsp")); + var tdChecksum = Checksums.sha256CanonicalJson(tdPayload); + var sendingPlatform = "BOLE"; + var receivingPlatform = rsp.eblPlatform(); + var sendingEPUI = "1234"; + var sendingLegalName = "DCSA CTK tester"; + var receivingEPUI = rsp.receiverEPUI(); + var receivingLegalName = rsp.receiverLegalName(); + var receiverCodeListName = rsp.receiverEPUICodeListName(); + var latestEnvelopeTransferChainUnsigned = OBJECT_MAPPER.createObjectNode() + .put("eblPlatform", sendingPlatform) + .put("transportDocumentChecksum", tdChecksum) + .putNull("previousEnvelopeTransferChainEntrySignedContentChecksum"); + var issuingParty = OBJECT_MAPPER.createObjectNode() + .put("eblPlatform", sendingPlatform) + .put("legalName", sendingLegalName); + issuingParty.putArray("partyCodes") + .addObject() + .put("partyCode", sendingEPUI) + .put("codeListProvider", "EPUI") + .put("codeListName", PLATFORM2CODELISTNAME.get(sendingPlatform)); + var issueToParty = OBJECT_MAPPER.createObjectNode() + .put("eblPlatform", receivingPlatform) + .put("legalName", receivingLegalName); + issueToParty.putArray("partyCodes") + .addObject() + .put("partyCode", receivingEPUI) + .put("codeListProvider", "EPUI") + .put("codeListName", receiverCodeListName); + var transaction = latestEnvelopeTransferChainUnsigned + .putArray("transactions") + .addObject() + .put("action", "ISSU") + .put("timestamp", Instant.now().toEpochMilli()); + transaction.set("actor", issuingParty); + transaction.set("recipient", issueToParty); + + var latestEnvelopeTransferChainEntrySigned = payloadSigner.sign(latestEnvelopeTransferChainUnsigned.toString()); + var unsignedEnvelopeManifest = OBJECT_MAPPER.createObjectNode() + .put("transportDocumentChecksum", tdChecksum) + .put("lastEnvelopeTransferChainEntrySignedContentChecksum", Checksums.sha256(latestEnvelopeTransferChainEntrySigned)); + unsignedEnvelopeManifest.putArray("supportingDocuments"); + body.set("envelopeManifestSignedContent", TextNode.valueOf(payloadSigner.sign(unsignedEnvelopeManifest.toString()))); + body.putArray("envelopeTransferChain") + .add(latestEnvelopeTransferChainEntrySigned); + + this.syncCounterpartPost( + "/v" + apiVersion.charAt(0) + "/envelopes", + body + ); + + addOperatorLogEntry( + "Sent a %s issuance request for eBL with transportDocumentReference '%s' (now in state '%s')" + .formatted(isCorrect ? "correct" : "incorrect", tdr, eblStatesByTdr.get(tdr))); + } + + @Override + public ConformanceResponse handleRequest(ConformanceRequest request) { + log.info("EblInteropSendingPlatform.handleRequest(%s)".formatted(request)); + return request.createResponse( + 404, + Map.of("Api-Version", List.of(apiVersion)), + new ConformanceMessageBody( + OBJECT_MAPPER + .createObjectNode() + .put( + "message", + "There are no API endpoints supported. The JWKS one is not supported"))); + } +} diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintTransferState.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintTransferState.java new file mode 100644 index 00000000..9ec8ff39 --- /dev/null +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/party/PintTransferState.java @@ -0,0 +1,7 @@ +package org.dcsa.conformance.standards.eblinterop.party; + +public enum PintTransferState { + AVAILABLE_FOR_ISSUANCE, + ISSUANCE_REQUESTED, + ISSUED, +} diff --git a/pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-issuance-entry.json b/pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-issuance-entry.json new file mode 100644 index 00000000..d4ba961f --- /dev/null +++ b/pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-issuance-entry.json @@ -0,0 +1,19 @@ +{ + "documentHash": "DOCUMENT_HASH", + "previousEndorsementChainEntryHash": null, + "transactions": [ + { + "timestamp": 1658385166302442200, + "platformHost": "https://exampleblplatform.net", + "actor": { + "eBLPlatformIdentifier": "test1234@exampleblplatform.net", + "legalName": "Some carrier" + }, + "recipient": { + "eBLPlatformIdentifier": "RECEIVER_PLATFORM_IDENTIFIER", + "legalName": "RECEIVER_LEGAL_NAME" + }, + "action": "ISSU" + } + ] +} diff --git a/pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-transport-document.json b/pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-transport-document.json new file mode 100644 index 00000000..314ca69b --- /dev/null +++ b/pint/src/main/resources/standards/pint/messages/pint-3.0.0-Beta-1-transport-document.json @@ -0,0 +1,91 @@ +{ + "transportDocumentReference": "TRANSPORT_DOCUMENT_REFERENCE_PLACEHOLDER", + + "termsAndConditions": "There would normally be a long legal text here.", + "isToOrder": false, + "isElectronic": true, + "invoicePayableAt": { + "locationType": "UNCO", + "UNLocationCode": "DKCPH" + }, + "cargoMovementTypeAtOrigin": "CY", + "cargoMovementTypeAtDestination": "CY", + "deliveryTypeAtDestination": "CY", + "receiptTypeAtOrigin": "CY", + "transportDocumentTypeCode": "SWB", + "isShippedOnBoardType": true, + "freightPaymentTermCode": "PRE", + "transportDocumentStatus": "ISSU", + "utilizedTransportEquipments": [], + "transports": { + "vesselName": "Emma Maersk", + "vesselIMONumber": "1234567", + "plannedDepartureDate": "2024-02-02", + "plannedArrivalDate": "2124-02-02", + "carrierExportVoyageNumber": "2433W", + "portOfLoading": { + "locationType": "UNCO", + "UNLocationCode": "DKCPH" + }, + "portOfDischarge": { + "locationType": "UNCO", + "UNLocationCode": "NLRTM" + } + }, + "partyContactDetails": [ + { + "name": "Nobody", + "phone": "+1234567890" + } + ], + "consignmentItems": [], + "documentParties": [ + + ], + "carrierCode": "ASDF", + "carrierCodeListProvider": "NMFTA", + "issuingParty": { + "partyName": "issuing party", + "partyContactDetails": [ + { + "name": "issuing party", + "phone": "+01234567890" + } + ] + }, + "shippingInstruction": { + "shippingInstructionReference": "SHIPPING_INSTRUCTION_REFERENCE_PLACEHOLDER", + "documentStatus": "RECE", + "shippingInstructionCreatedDateTime": "2019-11-12T07:41:00+08:30", + "shippingInstructionUpdatedDateTime": "2019-11-12T07:41:00+08:30", + "isShippedOnBoardType": true, + "isElectronic": true, + "isToOrder": false, + "consignmentItems": [ + { + "descriptionOfGoods": "string", + "HSCode": "string", + "cargoItems": [ + { + "equipmentReference": "C1234567890", + "weight": 123.45, + "weightUnit": "KGM", + "numberOfPackages": 123, + "packageCode": "ABC" + } + ] + } + ], + "utilizedTransportEquipments": [ + { + "equipment": { + "equipmentReference": "C1234567890" + }, + "cargoGrossWeight": 123.45, + "cargoGrossWeightUnit": "KGM", + "isShipperOwned": true + } + ] + } +} + diff --git a/pint/src/main/resources/standards/pint/sandboxes/auto-all-in-one.json b/pint/src/main/resources/standards/pint/sandboxes/auto-all-in-one.json new file mode 100644 index 00000000..2d922eaa --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/auto-all-in-one.json @@ -0,0 +1,35 @@ +{ + "id": "SANDBOX_ID_PREFIX-auto-all-in-one", + "name": "Auto all-in-one", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": true + }, + "parties" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-all-in-one" + }, { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-all-in-one" + } ], + "counterparts" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-all-in-one/party/SendingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + }, { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-all-in-one/party/ReceivingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-tested-party.json b/pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-tested-party.json new file mode 100644 index 00000000..4903d9ba --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-tested-party.json @@ -0,0 +1,31 @@ +{ + "id": "SANDBOX_ID_PREFIX-auto-platform-tested-party", + "name": "Auto platform tested party", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "RECEIVING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": false + }, + "parties" : [ { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "orchestratorUrl": "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-platform-testing-counterparts" + } ], + "counterparts" : [ { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-platform-tested-party/party/ReceivingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "RECEIVING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + }, { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-platform-testing-counterparts/party/SendingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-testing-counterparts.json b/pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-testing-counterparts.json new file mode 100644 index 00000000..13ad4a17 --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/auto-receivingplatform-testing-counterparts.json @@ -0,0 +1,31 @@ +{ + "id": "SANDBOX_ID_PREFIX-auto-platform-testing-counterparts", + "name": "Auto platform testing counterparts", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": true + }, + "parties" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-platform-testing-counterparts" + } ], + "counterparts" : [ { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-platform-tested-party/party/ReceivingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "RECEIVING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + }, { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-platform-testing-counterparts/party/SendingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-tested-party.json b/pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-tested-party.json new file mode 100644 index 00000000..3a6d5af4 --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-tested-party.json @@ -0,0 +1,29 @@ +{ + "id": "SANDBOX_ID_PREFIX-auto-carrier-tested-party", + "name": "Auto carrier tested party", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": false + }, + "parties" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "orchestratorUrl": "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-carrier-testing-counterparts" + } ], + "counterparts" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-carrier-testing-counterparts/party/SendingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + } ,{ + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-carrier-testing-counterparts/party/ReceivingPlatform1/api" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-testing-counterparts.json b/pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-testing-counterparts.json new file mode 100644 index 00000000..2461852d --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/auto-sendingplatform-testing-counterparts.json @@ -0,0 +1,27 @@ +{ + "id": "SANDBOX_ID_PREFIX-auto-carrier-testing-counterparts", + "name": "Auto carrier testing counterparts", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": true + }, + "parties" : [ { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-carrier-testing-counterparts" + } ], + "counterparts" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-carrier-tested-party/party/SendingPlatform1/api", + "authHeaderName": "dcsa-conformance-api-key", + "authHeaderValue": "SENDING_PLATFORM_AUTH_HEADER_VALUE_PLACEHOLDER" + }, { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-auto-carrier-testing-counterparts/party/ReceivingPlatform1/api" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json b/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json new file mode 100644 index 00000000..8c9418ea --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json @@ -0,0 +1,27 @@ +{ + "id": "SANDBOX_ID_PREFIX-manual-platform-tested-party", + "name": "Manual platform tested party", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": false + }, + "parties" : [ { + "inManualMode": true, + "name" : "ReceivingPlatform", + "role" : "ReceivingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-testing-counterparts" + } ], + "counterparts" : [ { + "inManualMode": true, + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-tested-party/party/ReceivingPlatform1/api" + }, { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-testing-counterparts/party/SendingPlatform1/api" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-testing-counterparts.json b/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-testing-counterparts.json new file mode 100644 index 00000000..f6d05253 --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-testing-counterparts.json @@ -0,0 +1,26 @@ +{ + "id": "SANDBOX_ID_PREFIX-manual-platform-testing-counterparts", + "name": "Manual platform testing counterparts", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": true + }, + "parties" : [ { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-testing-counterparts" + } ], + "counterparts" : [ { + "inManualMode": true, + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-tested-party/party/ReceivingPlatform1/api" + }, { + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-testing-counterparts/party/SendingPlatform1/api" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-tested-party.json b/pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-tested-party.json new file mode 100644 index 00000000..c2564c82 --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-tested-party.json @@ -0,0 +1,27 @@ +{ + "id": "SANDBOX_ID_PREFIX-manual-carrier-tested-party", + "name": "Manual carrier tested party", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": false + }, + "parties" : [ { + "inManualMode": true, + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-carrier-testing-counterparts" + } ], + "counterparts" : [ { + "inManualMode": true, + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-carrier-tested-party/party/SendingPlatform1/api" + }, { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-carrier-testing-counterparts/party/ReceivingPlatform1/api" + } ] +} diff --git a/pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-testing-counterparts.json b/pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-testing-counterparts.json new file mode 100644 index 00000000..02880cc7 --- /dev/null +++ b/pint/src/main/resources/standards/pint/sandboxes/manual-sendingplatform-testing-counterparts.json @@ -0,0 +1,26 @@ +{ + "id": "SANDBOX_ID_PREFIX-manual-carrier-testing-counterparts", + "name": "Manual carrier testing counterparts", + "standard" : { + "name" : "STANDARD_NAME_PLACEHOLDER", + "version" : "STANDARD_VERSION_PLACEHOLDER" + }, + "orchestrator" : { + "active": true + }, + "parties" : [ { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-carrier-testing-counterparts" + } ], + "counterparts" : [ { + "inManualMode": true, + "name" : "SendingPlatform1", + "role" : "SendingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-carrier-tested-party/party/SendingPlatform1/api" + }, { + "name" : "ReceivingPlatform1", + "role" : "ReceivingPlatform", + "url" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-carrier-testing-counterparts/party/ReceivingPlatform1/api" + } ] +} diff --git a/pint/src/main/resources/standards/pint/schemas/pint-3.0.0-Beta-1.json b/pint/src/main/resources/standards/pint/schemas/pint-3.0.0-Beta-1.json new file mode 100644 index 00000000..c5e7dd28 --- /dev/null +++ b/pint/src/main/resources/standards/pint/schemas/pint-3.0.0-Beta-1.json @@ -0,0 +1,3539 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "DCSA eBL PINT API", + "description": "

DCSA OpenAPI specification for electronic Bill of Lading (eBL) Platform Interoperability (PINT) standard

\n\n

The Envelope Transfer

\n\nThe PINT API is designed to support non-repudiable transfer of eBL document and any number of additional documents between two eBL platforms. \n\nIn the most common scenario, the sending eBL platform executes the envelope transfer by:\n1. Initiating the envelope transfer using [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint\n2. Transfering the additional documents (one at the time) using [**'Transfer additional document'**](#/Transfer%20additional%20document) endpoint\n3. Completing the envelope transfer using [**'Finish envelope transfer'**](#/Finish%20envelope%20transfer) endpoint\n\n

The eBL document

\n\nThe envelope transfer from the sending eBL Platform to the receiving eBL Platform always contains the **eBL document** (transferred via [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint request body [`EblEnvelope.transportDocument`](#/EblEnvelope) schema object). The eBL document must be unchanged between different envelope transfers for the lifetime of the eBL document. \n\nThe PINT API is designed to support transfer of the following types of eBL documents:\n1. Straight eBL documents. This type of eBL document is defined by [`EblEnvelope.transportDocument.isToOrder`](#/EblEnvelope) attribute to `false`\n2. Blank-endorsed eBL documents. This type of eBL document is created by setting [`EblEnvelope.transportDocument.isToOrder`](#/EblEnvelope) attribute to `true`, and making sure that [`EblEnvelope.transportDocument.documentparties[]`](#/EblEnvelope) list does not contain document party where `DocumentParty.partyFunction` has value `END` (Endorsee)\n3. To-order/Negotiable eBL documents. This type of eBL document is created by setting the [`EblEnvelope.transportDocument.isToOrder`](#/EblEnvelope) attribute to `true`, and making sure that [`EblEnvelope.transportDocument.documentparties[]`](#/EblEnvelope) list contains document party where `DocumentParty.partyFunction` has value `END` (Endorsee). If the current endorsee party is also in possession of the eBL (possessor), this party can endorse some other party on the same eBL Platform (and make that other party new endorsee) by executing transaction with [`Transaction.action`](#/Transaction) type `ENDO` (Endorsement). IMPORTANT: To-order eBL workflows are still not fully supported in this version of PINT API.\n\n

The Additional Documents

\n\nThe envelope transfer can optionally contain one or more **additional documents** which can be transferred via [**'Transfer additional document'**](#/Transfer%20additional%20document) endpoint:\n- **'Digital copy of the original physical B/L document' document** (described via [`EnvelopeManifest.eBLVisualisationByCarrier`](#/EnvelopeManifest) schema object that has been previously transferred via [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint as JWS-signed payload of [`EblEnvelope.envelopeManifestSignedContent`](#/EblEnvelope) schema object) a.k.a. **eBLVisualisationByCarrier document**. If transferred with the initial eBL envelope transfer, eBLVisualisationByCarrier document must be transferred with every subsequent envelope transfer for the lifetime of the eBL document. Also, eBLVisualisationByCarrier document must be unchanged between different envelope transfers for the lifetime of the eBL document.\n- **Supporting document** (described via entry in the [`EnvelopeManifest.supportingDocuments[]`](#/EnvelopeManifest) list object that has been previously transferred via [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint as JWS-signed payload of [`EblEnvelope.envelopeManifestSignedContent`](#/EblEnvelope) schema object). For every envelope transfer, the sending platform can choose which supporting documents it wants to send to the receiving platform irrespective of the contents of the previously received envelope transfer. All details of the supporting documents transferred from sending to receiving platform as a part of the envelope transfer are only privy to these 2 platforms.\n\n

Non-repudiation

\n\nWhen receiving [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint request, the receiving platform should confirm the integrity of the received [`EblEnvelope`](#/EblEnvelope) schema object for non-repudiation purposes. Since it has been decided not to use JWS for signing of [`EblEnvelope`](#/EblEnvelope) (for network traffic optimization purposes) itself, the receiving platform can confirm the integrity of the envelope data by confirming integrity of the [`EblEnvelope.envelopeManifestSignedContent`](#/EblEnvelope) JWS-signed payload [`EnvelopeManifest`](#/EnvelopeManifest), and then use contents of [`EnvelopeManifest`](#/EnvelopeManifest) schema object to confirm the integrity of the other [`EblEnvelope`](#/EblEnvelope) schema object attributes (further details can be found in the description of [`EblEnvelope`](#/EblEnvelope) schema attributes).\n\nAt various stages during the envelope transfer process, the receiving platform can inform the sending platform whether envelope transfer has been accepted or rejected by sending the final response using the [`EnvelopeTransferFinishedResponse`](#/EnvelopeTransferFinishedResponse) schema object which has been wrapped in JWS-signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) schema object for non-repudiation purposes. All other response schema types are unsigned, and therefore they can not be used by the sending platform for the non-repudiation purposes.\n", + "contact": { + "name": "Digital Container Shipping Association (DCSA)", + "url": "https://dcsa.org", + "email": "info@dcsa.org" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "3.0.0-Beta-1" + }, + "servers": [ + { + "url": "/" + } + ], + "paths": { + "/v3/envelopes": { + "post": { + "tags": [ + "Start envelope transfer" + ], + "summary": "Start the eBL envelope transfer", + "description": "Start the eBL envelope transfer.\n", + "parameters": [ + { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain the **MAJOR** version number. The API-Version header **MUST** be aligned with the URI version.\n", + "required": false, + "schema": { + "type": "string", + "example": "1" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EblEnvelope" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "The Receiving platform either accepts the envelope transfer immedidately (if there are no additional documents to be transferred, or if it concludes that it is already in the possesion of all the additional documents mentioned in the [`EnvelopeManifest`](#/EnvelopeManifest)), or concludes that in has previously accepted the envelope transfer with the same contents.\n\nThe signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) that signals the accepted envelope transfer (`RECE` or `DUPE`).\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "201": { + "description": "The Receiving platform acknowledges that the envelope transfer is now active for this envelope transfer request.\n\nThe sending platform should transfer all supporting documents listed in the [`EblEnvelope.envelopeManifestSignedContent`](#/EblEnvelope) JWS-signed payload [`EnvelopeManifest.supportingDocuments`](#/EnvelopeManifest) list, as well as eBLVisualisationByCarrier document if attribute [`EnvelopeManifest.eBLVisualisationByCarrier`](#/EnvelopeManifest) was defined, prior to sending request to finish the envelope transfer.\n\nIf the sending platform attempts to start a new envelope transfer for an eBL that already has the active envelope transfer that is not yet completed, the receiving platform should assume that the sending platform is retrying the envelope transfer. The receiving platform should assume that the sending platform is aware that the previous envelope transfer failed or that the sending platform is not aware that the previous envelope transfer started.\n\nThe [`EnvelopeTransferStartedResponse`](#/EnvelopeTransferStartedResponse) response is unsigned. The sending platform is required to finish the envelope transfer via [**'Finish envelope transfer'**](#/Finish%20envelope%20transfer) endpoint in order to get a signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) with the JWS-signed payload `EnvelopeTransferFinishedResponse` carrying the information whether the receiving platform has accepted or rejected the envelope transfer.\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferStartedResponse" + } + } + } + }, + "422": { + "description": "Receiving platform rejects the envelope transfer.\n\nThe signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value that signals the rejected envelope transfer (`BENV`, `BETR` or `BSIG`).\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "default": { + "description": "Request failed for the unexpected reason. The unsigned response [`error`](https://app.swaggerhub.com/domains/dcsaorg/ERROR_DOMAIN/2.0.1#/components/schemas/error) contains all the error details.\n\nThe sending platform is required to retry the envelope transfer until they get a signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) with the JWS-signed payload [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value providing the information whether the receiving platform has accepted or rejected the envelope transfer. \n\nIn the rare corner cases, the unsigned `error` response could come from middleware and hide a true envelope transfer acceptance/rejection message. The sending platform will be liable if they act on an unsigned reponse that does *not* match the actions of the receiving platform.\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + } + } + }, + "/v3/envelopes/{envelopeReference}/additional-documents/{documentChecksum}": { + "put": { + "tags": [ + "Transfer additional document" + ], + "summary": "Transfer additional document associated with the eBL envelope transfer", + "description": "Transfer **additional document** associated with an eBL envelope transfer. The transfered document should be either one of the **supporting documents** from the [`EnvelopeManifest.supportingDocuments`](#/EnvelopeManifest) list, or **eBLVisualisationByCarrier document** if attribute [`EnvelopeManifest.eBLVisualisationByCarrier`](#/EnvelopeManifest) was defined.\n\nThe `requestBody` should contain the document being transferred. It is recommended to use the `application/octet-stream` media type for the `requestBody`. The media type provided in the initial `ISSU` (Issuance) transaction is the one that will be associated with the transferred additional document in the end.\n\nThe receiving platform should check that the transferred document has been declared at the start of the eBL envelope transfer.\nThis should be done by verifying that the `documentChecksum` URL path parameter matches either [`EnvelopeManifest.eBLVisualisationByCarrier.documentChecksum`](#/EnvelopeManifest) attribute value (in which case the receiving platform can conclude that the sending platform has transferred the **eBLVisualisationByCarrier document**), or one of the [`DocumentMetadata.documentChecksum`](#/DocumentMetadata) values from the [`EnvelopeManifest.supportingDocuments[]`](#/EnvelopeManifest) list (in which case the receiving platform can conclude that the sending platform has transferred the **supporting document**)\n \nFurthermore, the receiving platform should compute the SHA-256 checksum of the transferred additional document, and verify that it matches the value of `documentChecksum` URL path parameter. \n\nIf all the abovementioned verifications have been successfull, the receiving platform can conclude that the additional document was transferred successfully.\n", + "parameters": [ + { + "name": "envelopeReference", + "in": "path", + "description": "The receiving platform-provided unique identifier for the given eBL envelope.\n", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/EnvelopeReference" + }, + "example": "4TkP5nvgTly0MwFrDxfIkR2rvOjkUIgzibBoKABU" + }, + { + "name": "documentChecksum", + "in": "path", + "description": "The checksum of the document computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/DocumentChecksum" + }, + "example": "76a7d14c83d7268d643ae7345c448de60701f955d264a743e6928a0b8268b24f" + }, + { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain the **MAJOR** version number. The API-Version header **MUST** be aligned with the URI version.\n", + "required": false, + "schema": { + "type": "string", + "example": "1" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "The receiving platform acknowledges that the additional document was transferred successfully.\n\nIf the envelope transfer eventually gets accepted,the receiving platform will acknowledge the additional document transfer with a JWS-signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) containing JWS-signed payload [`EnvelopeTransferFinishedResponse.receivedAdditionalDocumentChecksums[]`](#/EnvelopeTransferFinishedResponse) list containing the additional document checksum. Therefore, this is unsigned response.\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + } + }, + "409": { + "description": "The Receiving platform has discovered conflict between the request and the current state of the envelope transfer.\nE.g. the checksum or the size of the transferred additional document does not match the data provided in the URL path or in the request at the start of the envelope transfer.\n\nThe signed response `EnvelopeTransferFinishedResponseSignedContent` JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value that signals the issue with the envelope transfer that does *not* reject the envelope transfer (`INCD`).\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "422": { + "description": "The Receiving platform rejects the envelope transfer.\n\nThe signed response `EnvelopeTransferFinishedResponseSignedContent` JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value that signals the rejected envelope transfer (`BENV`, `BETR` or `BSIG`).\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "default": { + "description": "Request failed for the unexpected reason. The unsigned response [`error`](https://app.swaggerhub.com/domains/dcsaorg/ERROR_DOMAIN/2.0.1#/components/schemas/error) contains all the error details.\n\nThe sending platform is required to retry the additional document transfer until they get a signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) with the JWS-signed payload [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value providing the information whether the receiving platform has accepted or rejected the envelope transfer. \n\nIn the rare corner cases, the unsigned `error` response could come from middleware and hide a true envelope transfer acceptance/rejection message. The sending platform will be liable if they act on an unsigned reponse that does *not* match the actions of the receiving platform.\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + } + } + }, + "/v3/envelopes/{envelopeReference}/finish-transfer": { + "put": { + "tags": [ + "Finish envelope transfer" + ], + "summary": "Finish the eBL envelope transfer", + "description": "Finish the eBL envelope transfer.\n\nThe sending platform believes all additional documents have been transferred and the envelope transfer can now be completed. \n\nPrior to acepting envelope transfer, the receiving platform should ensure that all supporting documents listed in the [`EnvelopeManifest.supportingDocument`](#/EnvelopeManifest) list have been successfully transferred, as well as eBLVisualisationByCarrier document if attribute [`EnvelopeManifest.eBLVisualisationByCarrier`](#/EnvelopeManifest) was defined. Otherwise, The receiving platform should reject the envelope transfer.\n", + "parameters": [ + { + "name": "envelopeReference", + "in": "path", + "description": "The receiving platform-provided unique identifier for the given eBL envelope.\n", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/EnvelopeReference" + }, + "example": "4TkP5nvgTly0MwFrDxfIkR2rvOjkUIgzibBoKABU" + }, + { + "name": "documentChecksum", + "in": "path", + "description": "The checksum of the document computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/DocumentChecksum" + }, + "example": "76a7d14c83d7268d643ae7345c448de60701f955d264a743e6928a0b8268b24f" + }, + { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain the **MAJOR** version number. The API-Version header **MUST** be aligned with the URI version.\n", + "required": false, + "schema": { + "type": "string", + "example": "1" + } + } + ], + "responses": { + "200": { + "description": "The Receiving platform accepts the envelope transfer.\n\nThe signed response `EnvelopeTransferFinishedResponseSignedContent` JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value that signals the accepted envelope transfer (`RECE` or `DUPE`).\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "409": { + "description": "The Receiving platform has discovered conflict between the request and the current state of the envelope transfer.\n\nThe signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value that signals the issue with the envelope transfer that does *not* reject the envelope transfer (`DISE` or `MDOC`).\n\nIn the case of `MDOC` [`responseCode`](#/EnvelopeTransferFinishedResponse), the receiving platform cannot accept the envelope transfer due to one or more missing additional documents. In this case, the sending platform should (re)send the missing documents (provided in [`EnvelopeTransferFinishedResponse.missingAdditionalDocumentChecksums[]`](#/EnvelopeTransferFinishedResponse) list) and then retry to finish the envelope transfer.\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "422": { + "description": "The Receiving platform rejects the envelope transfer.\n\nThe signed response `EnvelopeTransferFinishedResponseSignedContent` JWS-signed payload must contain [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value that signals the rejected envelope transfer (`BENV`, `BETR` or `BSIG`).\n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/EnvelopeTransferFinishedResponseSignedContent" + } + } + } + }, + "default": { + "description": "Request failed for the unexpected reason. The unsigned response [`error`](https://app.swaggerhub.com/domains/dcsaorg/ERROR_DOMAIN/2.0.1#/components/schemas/error) contains all the error details.\n\nThe sending platform shoud retry to finish the envelope transfer until it gets the signed response [`EnvelopeTransferFinishedResponseSignedContent`](#/EnvelopeTransferFinishedResponseSignedContent) with the JWS-signed payload [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse) attribute value providing the information whether the receiving platform has accepted or rejected the envelope transfer. \n\nIn the rare corner cases, the unsigned `error` response could come from middleware and hide a true envelope transfer acceptance/rejection message. The sending platform will be liable if they act on an unsigned reponse that does *not* match the actions of the receiving platform. \n", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/error" + } + } + } + } + } + } + }, + "/v3/.well-known/jwks.json": { + "get": { + "tags": [ + "Get JWKS (JSON Web Key Set)" + ], + "summary": "Get the public key(s) of the platform, which match the private key(s) used for the signing of the various JWS-signed content exchanged during the envelope transfer process", + "description": "Before attempting the envelope transfer, the sending platform and the receiving platform should exchange public keys that need to be used in order to confirm the integrity of the JWS-signed content for non-repudiation purposes. \n\nUpon receiving start envelope transfer request, the receiving platform needs the sending platform's public key(s) in order to be able to confirm the integrity of the contents of the [`EblEnvelope`](#/EblEnvelope).\n\nUpon receiving ['EnvelopeTransferFinishedResponseSignedContent'](#/EnvelopeTransferFinishedResponseSignedContent) response, the sending platform needs the receiving platform's public key(s) in order to be able to confirm the integrity of the response message.\n\nThis endpoint can be used to provide the JWKS containing public keys of the receiving/sending platform to the counterparty sending/receiving platform. Alternatively, the sending platform and the receiving platform can exchange public keys used for signing of the various JWS-signed content in envelope transfer process out-of-band.\n", + "parameters": [ + { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain the **MAJOR** version number. The API-Version header **MUST** be aligned with the URI version.\n", + "required": false, + "schema": { + "type": "string", + "example": "1" + } + } + ], + "responses": { + "200": { + "description": "Request successful", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JWKS" + }, + "example": [ + { + "kty": "RSA", + "x5t#S256": "exa8R4v9nLwglTQn1FKvLdUDYdPGs2Ftp-RezKSTM2A", + "e": "AQAB", + "kid": "dcsa-kid", + "x5c": [ + "MIIDfDCCAmSgAwIBAgIJAM6iTZvk8egFMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAk5MMRYwFAYDVQQIEw1Ob29yZC1Ib2xsYW5kMRIwEAYDVQQHEwlBbXN0ZXJkYW0xDTALBgNVBAoTBERDU0ExCzAJBgNVBAsTAklUMRUwEwYDVQQDEwxlYmwtZW52ZWxvcGUwHhcNMjIwNzI5MTAwNjQzWhcNMjIxMDI3MTAwNjQzWjBsMQswCQYDVQQGEwJOTDEWMBQGA1UECBMNTm9vcmQtSG9sbGFuZDESMBAGA1UEBxMJQW1zdGVyZGFtMQ0wCwYDVQQKEwREQ1NBMQswCQYDVQQLEwJJVDEVMBMGA1UEAxMMZWJsLWVudmVsb3BlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4wwZAmx07g7+rSvacPE60UvZyF02i1fTB/eg2Eu6zIuSr+np+IsEDPa5PdT4XYDSSbwa1gs1HUs7r8JY3OjUQdiXl0IgYj/YWsFJeokfne9kkSfDmFdTeiYzA+mcj68MF3wwK8UR72vprp8nWJeDxGXLjMCECr9vDGozaZ6+U15yVnAUVymBtbDlaNb2qT/OS0Nmls6pUdHfy7VM6QnYy58nBAH/PS4kdxThd1kauAN1+mUaVMYJNCbTlBSew/Y2cvC3Oh6iwe1lYkBIHMvkytGT3lbssX2mIwbht2O5EKg6b6YG2iii732w7i264i2OEKK2/+lR837Y2Es5zFR3vQIDAQABoyEwHzAdBgNVHQ4EFgQUOBRI3iOZCILGWS6ku4cdwLnyxc8wDQYJKoZIhvcNAQELBQADggEBAGIP6bQwA6FxUIJavCvmC/dLbrpkZn2b77QS9xNmDlIdtxgWHsIOK2KwOri2XqEvKhxWQ+xyEXk0F7tLmgA3soV8xDpQL32qc4/RWl3QS2dsXoRPPgMFAxY1Hu4ZAdiN8rdnWylvm7FNvTbE57PpQQXlaEF1dHR6fzBylET35W29pAuNZwdwcOyj0XzXqLHqpd87zluTq5k5JKIjjRAU2YMivLTt0986Q/ihEThVdcX00T3HiLUjRy3hE9HdbJgcdmSo094+T/sJ+rZ79SyNCnXeUYaUzFP6ZibBdf3jZhmolfqnAyJy3qHcQA61Rvn3C76bskxCG8ccspEM3VSJBzM=" + ], + "n": "4wwZAmx07g7-rSvacPE60UvZyF02i1fTB_eg2Eu6zIuSr-np-IsEDPa5PdT4XYDSSbwa1gs1HUs7r8JY3OjUQdiXl0IgYj_YWsFJeokfne9kkSfDmFdTeiYzA-mcj68MF3wwK8UR72vprp8nWJeDxGXLjMCECr9vDGozaZ6-U15yVnAUVymBtbDlaNb2qT_OS0Nmls6pUdHfy7VM6QnYy58nBAH_PS4kdxThd1kauAN1-mUaVMYJNCbTlBSew_Y2cvC3Oh6iwe1lYkBIHMvkytGT3lbssX2mIwbht2O5EKg6b6YG2iii732w7i264i2OEKK2_-lR837Y2Es5zFR3vQ" + } + ] + } + } + } + } + } + } + }, + "components": { + "schemas": { + "EnvelopeReference": { + "maxLength": 100, + "type": "string", + "description": "Opaque receiving platform-provided identifier for a given eBL envelope.\n", + "example": "4TkP5nvgTly0MwFrDxfIkR2rvOjkUIgzibBoKABU" + }, + "DocumentChecksum": { + "maxLength": 64, + "minLength": 64, + "type": "string", + "description": "The checksum of the document computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "example": "76a7d14c83d7268d643ae7345c448de60701f955d264a743e6928a0b8268b24f" + }, + "EblPlatform": { + "maxLength": 4, + "pattern": "\\S+", + "type": "string", + "description": "The EBL platform of the transaction party. \nThe value **MUST** be one of:\n- `WAVE` (Wave)\n- `CARX` (CargoX)\n- `ESSD` (EssDocs)\n- `BOLE` (Bolero)\n- `EDOX` (EdoxOnline)\n- `IQAX` (IQAX)\n- `SECR` (Secro)\n- `TRGO` (TradeGo)\n", + "example": "BOLE" + }, + "LegalName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Legal name of the party/user as shown on the envelope transfer chain.", + "example": "Digital Container Shipping Association" + }, + "RegistrationNumber": { + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Company registration number of this party. E.g. the registration number on the local chamber of commerse.", + "example": "74567837" + }, + "LocationOfRegistration": { + "maxLength": 2, + "minLength": 2, + "pattern": "^[A-Z]+$", + "type": "string", + "description": "country code of the location of registration according to ISO 3166-1 alpha-2.", + "example": "NL" + }, + "EnvelopeTransferChainEntrySignedContentChecksum": { + "maxLength": 64, + "minLength": 64, + "pattern": "^[0-9a-f]+$", + "type": "string", + "description": "The checksum of the [EnvelopeTransferChainEntrySignedContent](#/EnvelopeTransferChainEntrySignedContent). The Checksum is computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "example": "20a0257b313ae08417e07f6555c4ec829a512c083f3ead16b41158018a22abe9" + }, + "TransportDocumentChecksum": { + "maxLength": 64, + "minLength": 64, + "type": "string", + "description": "The checksum of the **eBL document** a.k.a. **transport document** computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234). \n", + "example": "583c29ab3e47f2d80899993200d3fbadb9f8a367f3a39f715935c46d7a283006" + }, + "EblEnvelope": { + "required": [ + "envelopeManifestSignedContent", + "envelopeTransferChain", + "transportDocument" + ], + "type": "object", + "properties": { + "transportDocument": { + "description": "The receiving platform is required to validate the eBL document (a.k.a. transport document) by computing the SHA-256 checksum of the `transportDocument` attribute value, and and confirming it's equal to received 'EblEnvelope.envelopeManifestSignedContent' JWS-signed payload [`EnvelopeManifest.transportDocumentChecksum`](#/EnvelopeManifest) attribute value.\n", + "allOf": [ + { + "$ref": "#/components/schemas/TransportDocument" + } + ] + }, + "envelopeManifestSignedContent": { + "$ref": "#/components/schemas/EnvelopeManifestSignedContent" + }, + "envelopeTransferChain": { + "type": "array", + "description": "The ordered list of [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entries associated with the eBL document.\n\nThe receiving platform is required to validate that all the list entries are present, valid, and they are correctly ordered by:\n1. Computing the SHA-256 checksum of the last [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the `envelopeTransferChain[]` list, and confirming it's equal to received `EblEnvelope.envelopeManifestSignedContent` JWS-signed payload [`EnvelopeManifest.lastEnvelopeTransferChainEntrySignedContentChecksum`](#/EnvelopeManifest) attribute value. \n2. Walking back through the `envelopeTransferChain[]` list from the last entry, for each previous [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the list, computing the SHA-256 checksum and confirming it's equal to the current [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry JWT-signed payload [`EnvelopeTransferChainEntry.previousEnvelopeTransferChainEntrySignedContentChecksum`](#/EnvelopeTransferChainEntry) attribute value.\n\nThe receiving platform can reject the envelope with `BENV` [`responseCode`](#/EnvelopeTransferFinishedResponse) if it concludes that the list entries are not correctly ordered by the sending platform, or discovers any other validity-related issue in the list.\n\nThe first [`EnvelopeTransferChainEntry`](#/EnvelopeTransferChainEntry) in the `envelopeTransferChain[]` list should contain the ISSU (issuance) transaction as the first transaction in the [`EnvelopeTransferChainEntry.transactions[]`](#/EnvelopeTransferChainEntry) list. \n", + "items": { + "$ref": "#/components/schemas/EnvelopeTransferChainEntrySignedContent" + } + } + } + }, + "EnvelopeManifest": { + "required": [ + "lastEnvelopeTransferChainEntrySignedContentChecksum", + "supportingDocuments", + "transportDocumentChecksum" + ], + "type": "object", + "properties": { + "transportDocumentChecksum": { + "description": "actual type: [`TransportDocumentChecksum`](#/TransportDocumentChecksum)\n\nThe checksum of the eBL document (a.k.a. transport document) computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n\nIn order to guard itself against different types of MITM attacks (e.g. 'replay attack' where last entry in the [`EblEnvelope.envelopeTransferChain`](#/EblEnvelope) list is replaced with the similar entry extracted from the another envelope transfer previously sent by the same sending platform), upon start of the envelope transfer the receiving platform should validate that the [`EnvelopeManifest.transportDocumentChecksum`](#/EnvelopeManifest) attribute and the [`EnvelopeTransferChainEntry.transportDocumentChecksum`](#/EnvelopeTransferChainEntry) attribute of the last entry in the [`EblEnvelope.envelopeTransferChain`](#/EblEnvelope) list have the same value. \n", + "allOf": [ + { + "$ref": "#/components/schemas/TransportDocumentChecksum" + } + ] + }, + "lastEnvelopeTransferChainEntrySignedContentChecksum": { + "description": "actual type: [`EnvelopeTransferChainEntrySignedContentChecksum`](#/EnvelopeTransferChainEntrySignedContentChecksum)\n \nThis attribute should contain the checksum of the last [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list. The checksum is computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "allOf": [ + { + "$ref": "#/components/schemas/EnvelopeTransferChainEntrySignedContentChecksum" + } + ] + }, + "eBLVisualisationByCarrier": { + "description": "actual type: [`DocumentMetadata`](#\\DocumentMetadata)\n\nThis attribute is used to simplify validation of the document metadata of the **'Digital copy of the original physical B/L document' document** a.k.a. **eBLVisualisationByCarrier document**. If this attribute is defined, for each [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list, JWS-signed payload [`EnvelopeTransferChainEntry.eBLVisualisationByCarrier`](#/EnvelopeTransferChainEntry) attribute value should be identical to the value of this attribute.\n", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentMetadata" + } + ] + }, + "supportingDocuments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentMetadata" + } + } + } + }, + "EnvelopeManifestSignedContent": { + "pattern": "^([a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-\\+\\/=]+)$", + "type": "string", + "description": "JWS content with compact serialization according to [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-7.1). JWS-signed payload is defined in schema [EnvelopeManifest](#/EnvelopeManifest).\n", + "example": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlVhRVdLNmt2ZkRITzNZT2NwUGl2M1RCT2JQTzk2SFZhR2U0czFhUUxBZU0ifQ.eyJkb2N1bWVudEhhc2giOiI4ZGM5OWQ4YWM5MjIyNDBjNTVjMDM4NDVmNDlkZWY2NDE4NzE0NjY1MWJhZTRmOWE2MzEzMTI3N2NmMDBkOWRmIiwicHJldmlvdXNFbnZlbG9wZUhhc2giOm51bGwsInRyYW5zYWN0aW9ucyI6W3siYWN0aW9uIjoiSVNTVSIsImNvbW1lbnRzIjoiVGhlIEIvTCBoYXMgYmVlbiBpc3N1ZWQuIiwidGltZXN0YW1wIjoxNjU4Mzg1MTY2MzAyNDQyMjAwLCJpc1RvT3JkZXIiOnRydWUsInBsYXRmb3JtSG9zdCI6ImxvY2FsaG9zdDo4NDQzIiwidHJhbnNmZXJlZSI6IjQzNTQ5ODUwMjQ4QGxvY2FsaG9zdDo4NDQzIn1dfQ.c4SJ9-61fE6RmeIuZ3EI-TSM0M6qXuOudtr3YhpDjqVMaYk_RYpaWYvw75ssTbjgGFKTBKCy5lpmOfb8Fq--Qu2k0MWbH6qdX5jTYwl0DX946RQg-hnmVTg9np3bmqVeKqKURyV-UUdG-KK_XCGzPZ-lZkeUlpMcIthQFs0pCODR9GPytv7ZXLPZFOmHM9fn3FD2yRqVhQzcs7HdcxMjCx6hkBW8Z-jW4qteVy2_E9uqjkKwlu_cQLoY83Z0mcjn0PZNQvKF10x7q1_Jjf_Su19UigTUu3pFMrzo4iPS_jcrFoIb3TSZNSzbgAwtujSBFOufPDyEmxlx1sH0ZowMvA" + }, + "EnvelopeTransferChainEntry": { + "required": [ + "eblPlatform", + "transactions", + "transportDocumentChecksum" + ], + "type": "object", + "properties": { + "eblPlatform": { + "$ref": "#/components/schemas/EblPlatform" + }, + "transportDocumentChecksum": { + "$ref": "#/components/schemas/TransportDocumentChecksum" + }, + "previousEnvelopeTransferChainEntrySignedContentChecksum": { + "maxLength": 64, + "minLength": 64, + "type": "string", + "description": "actual type: [`EnvelopeTransferChainEntrySignedContentChecksum`](#/EnvelopeTransferChainEntrySignedContentChecksum)\n\nThis attribute should *not* be defined for the first entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list. For all other entries after the first entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list, this attribute **must** be defined and contain the checksum of the previous [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list. This attribute can be used to track signed envelope transfers between platforms (for details check description of [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope)).\n\nThe checksum is computed over the entire [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry (JWS-signed payload is described in the `EnvelopeTransferChainEntry` schema). The checksum is computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "nullable": true + }, + "eBLVisualisationByCarrier": { + "description": "actual type: [`DocumentMetadata`](#\\DocumentMetadata)\n\nThis attribute is used to transfer metadata of the **'Digital copy of the original physical B/L document' document** a.k.a. **eBLVisualisationByCarrier document**. If this attribute is defined with the initial eBL envelope transfer(in the first `EnvelopeTransferChainEntrySignedContent` entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list), eBLVisualisationByCarrier document **must** be transferred with every subsequent envelope transfer for the lifetime of the eBL document. Also, eBLVisualisationByCarrier document must be unchanged between different envelope transfers for the lifetime of the eBL document (in other words, for each `EnvelopeTransferChainEntrySignedContent` entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list, JWS-signed payload `EnvelopeTransferChainEntry.eBLVisualisationByCarrier` attribute must have the identical value).\n", + "allOf": [ + { + "$ref": "#/components/schemas/DocumentMetadata" + } + ] + }, + "transactions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Transaction" + } + } + } + }, + "EnvelopeTransferChainEntrySignedContent": { + "pattern": "^([a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-\\+\\/=]+)$", + "type": "string", + "description": "JWS content with compact serialization according to [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-7.1). JWS-signed payload is defined in schema [EnvelopeTransferChainEntry](#/EnvelopeTransferChainEntry).\n", + "example": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlVhRVdLNmt2ZkRITzNZT2NwUGl2M1RCT2JQTzk2SFZhR2U0czFhUUxBZU0ifQ.eyJkb2N1bWVudEhhc2giOiI4ZGM5OWQ4YWM5MjIyNDBjNTVjMDM4NDVmNDlkZWY2NDE4NzE0NjY1MWJhZTRmOWE2MzEzMTI3N2NmMDBkOWRmIiwicHJldmlvdXNFbnZlbG9wZUhhc2giOm51bGwsInRyYW5zYWN0aW9ucyI6W3siYWN0aW9uIjoiSVNTVSIsImNvbW1lbnRzIjoiVGhlIEIvTCBoYXMgYmVlbiBpc3N1ZWQuIiwidGltZXN0YW1wIjoxNjU4Mzg1MTY2MzAyNDQyMjAwLCJpc1RvT3JkZXIiOnRydWUsInBsYXRmb3JtSG9zdCI6ImxvY2FsaG9zdDo4NDQzIiwidHJhbnNmZXJlZSI6IjQzNTQ5ODUwMjQ4QGxvY2FsaG9zdDo4NDQzIn1dfQ.c4SJ9-61fE6RmeIuZ3EI-TSM0M6qXuOudtr3YhpDjqVMaYk_RYpaWYvw75ssTbjgGFKTBKCy5lpmOfb8Fq--Qu2k0MWbH6qdX5jTYwl0DX946RQg-hnmVTg9np3bmqVeKqKURyV-UUdG-KK_XCGzPZ-lZkeUlpMcIthQFs0pCODR9GPytv7ZXLPZFOmHM9fn3FD2yRqVhQzcs7HdcxMjCx6hkBW8Z-jW4qteVy2_E9uqjkKwlu_cQLoY83Z0mcjn0PZNQvKF10x7q1_Jjf_Su19UigTUu3pFMrzo4iPS_jcrFoIb3TSZNSzbgAwtujSBFOufPDyEmxlx1sH0ZowMvA" + }, + "EnvelopeTransferStartedResponse": { + "required": [ + "envelopeReference", + "lastEnvelopeTransferChainEntrySignedContentChecksum", + "missingAdditionalDocumentChecksums", + "transportDocumentChecksum" + ], + "type": "object", + "properties": { + "envelopeReference": { + "$ref": "#/components/schemas/EnvelopeReference" + }, + "transportDocumentChecksum": { + "$ref": "#/components/schemas/TransportDocumentChecksum" + }, + "lastEnvelopeTransferChainEntrySignedContentChecksum": { + "description": "actual type: [`EnvelopeTransferChainEntrySignedContentChecksum`](#/EnvelopeTransferChainEntrySignedContentChecksum)\n\nThis attribute should contain the checksum of the last [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list received. The checksum is computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "allOf": [ + { + "$ref": "#/components/schemas/EnvelopeTransferChainEntrySignedContentChecksum" + } + ] + }, + "missingAdditionalDocumentChecksums": { + "type": "array", + "description": "The list of the checksums of the additional documents that the receiving platform expect to receive in good order before it can accept the envelope transfer.\n", + "items": { + "$ref": "#/components/schemas/DocumentChecksum" + } + } + } + }, + "EnvelopeTransferFinishedResponse": { + "required": [ + "lastEnvelopeTransferChainEntrySignedContentChecksum", + "responseCode" + ], + "type": "object", + "properties": { + "lastEnvelopeTransferChainEntrySignedContentChecksum": { + "description": "actual type: [`EnvelopeTransferChainEntrySignedContentChecksum`](#/EnvelopeTransferChainEntrySignedContentChecksum)\n\nThis attribute should contain the checksum of the last [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list received. The checksum is computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "allOf": [ + { + "$ref": "#/components/schemas/EnvelopeTransferChainEntrySignedContentChecksum" + } + ] + }, + "responseCode": { + "type": "string", + "description": "The response code can have one of the following values:\n- `RECE` (Received)\n- `DUPE` (Duplicated Envelope)\n- `BSIG` (Bad Signature)\n- `BETR` (Bad Envelope Transfer)\n- `BENV` (Bad Envelope)\n- `INCD` (Inconclusive Document)\n- `MDOC` (Missing Document)\n- `DISE` (Disputed Envelope)\n\nThe `RECE` `responseCode` is used when the receiving platform acknowledges that the envelope transfer is accepted. \nThis response code can also be used when replying to [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint request if and only if the receiving platform is already in possesion of all the additional documents listed in the `EnvelopeManifest`, and is ready to commit to accepting the envelope transfer. This should only happen in special cases (e.g. Platform A which has performed the intial envelope transfer with additional documents to platform B later during the lifetime of the same eBL receives [**'Start envelope transfer'**](#/Start%20envelope%20transfer) endpoint request from some other Platform C with `EnvelopeManifest` listing additional documents, for which it concludes (based on checksum comparison) that they are identical to the addition documents from the intial envelope transfer). The `RECE` `responseCode` should be used together with the HTTP `200 Ok` response status code.\n\nThe `DUPE` `responseCode` is used in place of `RECE` `responseCode` when the receiving platform has already previously acknowledged and accepted the transfer of this envelope. In this case, the receiving platform asserts that this envelope transfer request is invalid but the prior envelope transfer was successful. The receiving platform should include accepted version of last `EnvelopeTransferChainEntrySignedContent` entry in `EblEnvelope.envelopeTransferChain` list in the `duplicateOfAcceptedEnvelopeTransferChainEntrySignedContent` field. The receiving platform is only required to detect DUPE `responseCode` if the EnvelopeTransferChainEntrySignedContentChecksum is the same between the two transfer attempt and the sending platform is expected to reuse the same envelope. The `DUPE` `responseCode` should be used together with the HTTP `200 Ok` response status code.\n\nThe `BSIG` `responseCode` is used when the receiving platform could not process the envelope related to a signature and rejects the concrete transfer request. This response code should be used when the receiving platform decides to reject envelope transfer due to the issues with the signature used to create JWS content when starting envelope transfer (`EblEnvelope.envelopeManifestSignedContent`, and/or one or more `EnvelopeTransferChainEntrySignedContentChecksum` entries in the `EblEnvelope.envelopeTransferChain` list). Example use cases: the signature was made by an unknown key, the key or signature expired, the signed content does not match the EnvelopeTransferChainEntrySignedContentChecksum, etc. In this case, the receiving platform makes no assertions about the validity of the envelope contents. The sending platform may attempt to resolve the signature issue and retry with the same EnvelopeTransferChainEntrySignedContentChecksum. The `BSIG` `responseCode` should be used together with a HTTP `422 Unprocessable Content` response status code.\n\nThe `BETR` `responseCode` is used when the receiving platform concludes that the envelopeReference parameter provided in the URL path does not match currently active envelope transfer, and therefore it decides to reject the envelope transfer. The sending platform should start a new envelope transfer to recover. The `BETR` `responseCode` should be used together with a HTTP `422 Unprocessable Content` response status code.\n\nThe `BENV` `responseCode` is used when the receiving platform can not process the envelope and rejects the concrete envelope transfer request. This response code can be used when the receiving platform knows the transfer cannot succeed in the future and the sending platform should retain the eBL. Example use cases could be that the envelope does not list the receiving platform as the intended recipient, the transferee ID is unknown, an invalid action code was used in the transactions for this envelope transfer, etc. In this case, the receiving platform is asserting that the envelope itself is not acceptable. Basically any use case where the receiving platform decides to reject the envelope transfer for technical reasons that are not covered by either `BSIG` `responseCode` or `BETR` `responseCode`. The `BENV` `responseCode` should be used together with a HTTP `422 Unprocessable Content` response status code.\n\nThe `INCD` `responseCode` is used when the receiving platform concludes that the transferred additional document's checksum or size does not match the document checksum or size provided either directly in URL path or in `EnvelopeManifest` sent in the start envelope transfer request. The `INCD` `responseCode` does *not* reject the envelope transfer. The sending platform should try to either resend the correct document, or attempt to start new envelope transfer request with changed `EnvelopeManifest` that will contain correct document checksum and/or size for the inconclusive document. The `INCD` `responseCode` is not valid as a response to the start of a envelope transfer. The `INCD` `responseCode` should be used together with a HTTP `409 Conflict` response status code.\n\nThe `MDOC` `responseCode` is used when the receiving platform cannot accept the envelope transfer due to a missing additional document. The `MDOC` `responseCode` does *not* reject the envelope transfer. The sending platform should resend relevant documents (provided in `missingAdditionalDocumentChecksums`) and then retry finishing the envelope transfer. The `MDOC` `responseCode` is not valid as a response to the start of a envelope transfer. The `MDOC` `responseCode` should be used together with a HTTP `409 Conflict` response status code.\n\nThe `DISE` `responseCode` is used when the receiving platform has successfully parsed the envelope and validated the signatures. However, the receiving platform believes the envelope contradicts its current knowledge of the envelope transfer chain for the eBL document and there is a risk of double spending. The concrete detection method is implementation-specific. However, a method would be for the receiving platform to confirm whether it has `EnvelopeTransferChainEntrySignedContentChecksum` for the document, which are not listed in the transferred eBL envelope. Dispute resolution is not covered in the API and must be handled in via out of band process. The `DISE` `responseCode` should be used together with the HTTP `409 Conflict` response status code.\n\nUnless otherwise stated for a given response code, receiving platform will reject the active envelope transfer (if any). Some failures that result in receiving platform rejecting the envelope transfer may be retriable, in which case the sending platform can attempt a new envelope transfer for the same envelope.\n\nThe sending platform must not rely on the HTTP response status code alone as it is not covered by the signature. When there is a mismatch between the HTTP response status code and the signed response `EnvelopeTransferFinishedResponseSignedResponse` JWS-signed payload [`EnvelopeTransferFinishedResponse.responseCode`](#/EnvelopeTransferFinishedResponse), [`responseCode`](#/EnvelopeTransferFinishedResponse) decides the outcome.\n", + "example": "RECE", + "enum": [ + "RECE", + "DUPE", + "BSIG", + "BENV", + "MDOC", + "BETR", + "DISE" + ] + }, + "duplicateOfAcceptedEnvelopeTransferChainEntrySignedContent": { + "description": "actual type: [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) \n\nThis attribute should be defined only in the case that [`responseCode`](#/EnvelopeTransferFinishedResponse) attribute has `DUPE` value, indicating that receiving platform has already received and accepted envelope transfer for the eBL document that is the sending platform trying to resend in the duplicate envelope transfer.\nThis attribute should contain the last [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry in the [`EblEnvelope.envelopeTransferChain[]`](#/EblEnvelope) list received at the start of already received and accepted envelope transfer. \n\nThe provided entry might differ in the retry (e.g. due to signing with the private key that differs from the private key used to sign already received and accepted envelope transfer). \n", + "allOf": [ + { + "$ref": "#/components/schemas/EnvelopeTransferChainEntrySignedContent" + } + ] + }, + "reason": { + "maxLength": 255, + "type": "string", + "description": "Free text comment for clarifying the result or suggesting follow up actions. Should be null or omitted when [`responseCode`](#/EnvelopeTransferFinishedResponse) is `RECE`, where there is no additional information to be given.\n", + "nullable": true + }, + "missingAdditionalDocumentChecksums": { + "type": "array", + "description": "Used with the `MDOC` [`responseCode`](#/EnvelopeTransferFinishedResponse) to signal which additional documents the receiving platform believes have not been transferred.\n\nFor other response codes, this attribute should be omitted.\n", + "items": { + "$ref": "#/components/schemas/DocumentChecksum" + } + }, + "receivedAdditionalDocumentChecksums": { + "type": "array", + "description": "The receiving platform includes this attribute with the `RECE` or `DUPE` [`responseCode`](#/EnvelopeTransferFinishedResponse) to confirm all additional documents it received during the envelope transfer. This attribute must include all the additional documents included in the envelope transfer request (including the ones the receiving platform already had). This attribute provides the sending platform with a signed receipt of the documents.\n\nFor other response codes, this attribute should be omitted.\n", + "items": { + "$ref": "#/components/schemas/DocumentChecksum" + } + } + }, + "example": { + "lastEnvelopeTransferChainEntrySignedContentChecksum": "d56a93a7e9f86a2d895df818e0440bdca6ffe03246e2fee14131f2e66c84c75a", + "responseCode": "RECE" + } + }, + "EnvelopeTransferFinishedResponseSignedContent": { + "pattern": "^([a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-\\+\\/=]*)$", + "type": "string", + "description": "JWS content with compact serialization according to [RFC 7515](https://datatracker.ietf.org/doc/html/rfc7515#section-7.1). JWS-signed payload is defined in schema [EnvelopeTransferFinishedResponse](#/EnvelopeTransferFinishedResponse).\n", + "example": "eyJhbGciOiJSUzI1NiIsImtpZCI6IlVhRVdLNmt2ZkRITzNZT2NwUGl2M1RCT2JQTzk2SFZhR2U0czFhUUxBZU0ifQ.eyJyZXNwb25zZVRvRW52ZWxvcGVIYXNoIjoiZDU2YTkzYTdlOWY4NmEyZDg5NWRmODE4ZTA0NDBiZGNhNmZmZTAzMjQ2ZTJmZWUxNDEzMWYyZTY2Yzg0Yzc1YSIsInJlc3BvbnNlQ29kZSI6IlJFQ1YifQ.P2_evKyvHH25BrvW4eIxp7xo9S73oK90QomKEjZDrn3AX8drv7aeVrNYYPPh4vqK78fTVair-0Ww9G7czX9Q3xlATTPlTQrNPuThh_-nPOvNDqHBwZuq_nop6lQwIS210OQa__C4z-oGvO8m56pfXvpgfTIC9nesnIuNFtrdr1lU81q4ZQnnZI0GvWaB_4Q320PoAXmKN4EwjY5gTqdKMAvz9a5PNnWfGPkIrM-CBm_MNLpl9NAcpJONRdwI9i9zTi42NZCUnbowHuHnYNuz3WfWzVA43U4IdEBDU6XeIem0Zm331KlBAukOhZiyN8Bp7ZK7XGx50a6XcY3190lX5Q" + }, + "DocumentMetadata": { + "required": [ + "documentChecksum", + "mediaType", + "name", + "size" + ], + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Carrier rendered copy of the original B/L document.pdf" + }, + "size": { + "minimum": 1, + "pattern": "^\\d+$", + "type": "number", + "description": "the Size of the document in bytes", + "example": 4194304 + }, + "mediaType": { + "type": "string", + "description": "The Media Type (MIME type) of the document", + "example": "application/pdf" + }, + "documentChecksum": { + "$ref": "#/components/schemas/DocumentChecksum" + } + }, + "description": "The document metadata that describes the document.\n" + }, + "Transaction": { + "required": [ + "action", + "actor", + "recipient", + "timestamp" + ], + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action denotes the transaction type, which can have one of the following values:\n- `ISSU` (Issuance)\n- `TRNS` (Transfer of possession)\n- `ENDO` (Endorsement a.k.a. Transfer of title)\n- `RESD` (Request to surrender for delivery)\n- `RESA` (Request to surrender for amendment)\n- `SACC` (Surrender accepted)\n- `SREJ` (Surrender rejected)\n\nThe `ISSU` `action` is used for the eBL issuance transaction from the carrier party to the shipper party, and must appear exactly once in the first transaction of the first [`EnvelopeTransferChainEntry`](#/EnvelopeTransferChainEntry) entry (as JWS payload of [`EnvelopeTransferChainEntrySignedContent`](#/EnvelopeTransferChainEntrySignedContent) entry) in the [`EblEnvelope.envelopeTransferChain`](#/EblEnvelope) list. If the eBL document is issued to the shipper party that resides on the different eBL Platform from the carrier's platform, initial envelope transfer should contain envelope transfer chain where first entry's first transaction has `ISSU` `action` type. \n\nThe `TRNS` `action` is used for the transfer of possession of the eBL from the current possessor party to the new possessor party.\n\nThe `ENDO` `action` is used for the endorsement (a.k.a. transfer of title, or the right to take the delivery of the goods) of To-order eBL documents, where the new endorsee party is not named in the eBL document. The endorsement should always happen on the platform of the user that is not only the current endorsee, but also the current possessor of the eBL document. If the new endorsee party resides on the different eBL Platform from the current endorsee's platform, the envelope transfer process is used to notify the new endorsee of the `ENDO` `action` transaction for non-repudiation purposes, and will NOT result in the transfer of possession of the eBL documment contained in the envelope. \n\nIf the party is the current possessor, and the party is the current consignee/endorsee/shipper (or eBL document is blank-endorsed), the party may request to surrender the eBL to the carrier. This is done by using one of the Request to surrender action types (`RESD` `action` or `RESA` `action`) and with the issuing carrier or the relevant carrier agent as the `recipient` of the action. \n \n The `RESD` `action` is used when the party wants to request the delivery of the goods. If the request is accepted (for details see `SACC` `action`), the carrier and the party submitting the surrender request will negotiate how the goods will be delivered (e.g. via DCSA shipment release API).\n \n The `RESA` `action` is used when the party wants to surrender the eBL document, so that the carrier can issue an amended eBL document. If the request is accepted (for details see `SACC` `action` resonse), the alignment on the exact change(s) that need to be made to the eBL document is done outside of the PINT API (e.g. via the DCSA EBL API). If the request is accepted, the existing eBL document is voided along with its envelope transfer chain, and the amended eBL document must be reissued with a new envelope transfer chain (for details see the `ISSU` `action` type description paragraph above). The `RESA` `action` is also used for switch to paper as the DCSA process flow for switching to paper is part of the amendment process (e.g. to note how many originals and copies with and without charges should be issued).\n\nIf a surrender request (`RESD` `action` or `RESA` `action`) is not addressed to the carrier that issued the eBL document or to their legal representative, then the receiving platform should reject the envelope transfer with the [`EnvelopeTransferFinishedResponse`](#/EnvelopeTransferFinishedResponse) containing `BENV` [`responseCode`](#/EnvelopeTransferFinishedResponse).\n\nThe `SACC` `action` is used by the carrier to asynchronously accept the surrender request initiated via `RESD` `action` or `RESA` `action`. If the party that submitted the surrender request is on the different platform, the envelope transfer process is used to notify the (surrender request submitter) party of the `SACC` `action` transaction for non-repudiation purposes, and will NOT result in the transfer of possession of the eBL documment contained in the envelope. No transactions may occur after a `SACC` `action` transaction. Any envelope transfer chain changes with new transactions after a `SACC` `action` transaction are invalid and should be answered with the [`EnvelopeTransferFinishedResponse`](#/EnvelopeTransferFinishedResponse) containing `BENV` [`responseCode`](#/EnvelopeTransferFinishedResponse). \n\nThe `SREJ` `action` is used by the carrier to asynchronously reject the surrender request initiated via `RESD` `action` or `RESA` `action`, and return the eBL document possession to the party that submitted the surrender request.\n\nWhen the transaction recipient is residing on the different platform from the transaction actor's platform, the transaction should be followed by the envelope transfer from the actor's (sending) platform to the recipient's (receiving) platform. If the envelope transfer is caused by either `ENDO` `action` or `SACC` `action` transaction (where the recipient is on the different platform), the envelope transfer process is NOT causing transfer of possession of the eBL document, but is merely used as notificaiton for the non-repudiation purposes.\n", + "enum": [ + "ISSU", + "ENDO", + "TRNS", + "RESD", + "RESA", + "SACC", + "SREJ" + ] + }, + "actor": { + "$ref": "#/components/schemas/TransactionParty" + }, + "recipient": { + "$ref": "#/components/schemas/TransactionParty" + }, + "timestamp": { + "type": "number", + "description": "Unix epoch with millisecond precision of when the transaction was created.", + "example": 1658385166302442200 + }, + "comments": { + "maxLength": 255, + "type": "string", + "description": "Free text comment for the party receiving the transaction.", + "example": "The B/L has been issued." + } + } + }, + "TransactionParty": { + "required": [ + "eblPlatform", + "legalName" + ], + "type": "object", + "properties": { + "eblPlatform": { + "$ref": "#/components/schemas/EblPlatform" + }, + "legalName": { + "$ref": "#/components/schemas/LegalName" + }, + "registrationNumber": { + "$ref": "#/components/schemas/RegistrationNumber" + }, + "locationOfRegistration": { + "$ref": "#/components/schemas/LocationOfRegistration" + }, + "taxReference": { + "pattern": "^\\S+$", + "type": "string", + "description": "Tax reference used in the location of registration.", + "example": "NL859951480B01" + }, + "partyCodes": { + "$ref": "#/components/schemas/partyCodes" + } + }, + "description": "Refers to a company or a legal entity." + }, + "JWKS": { + "required": [ + "keys" + ], + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "$ref": "#/components/schemas/JWKS_keys" + } + } + }, + "description": "JSON Web Key Set to validate JWS signatures, according to [RFC 7517](https://www.ietf.org/rfc/rfc7517.txt)" + }, + "EcPublicKey": { + "type": "object", + "properties": { + "crv": { + "type": "string" + }, + "x": { + "type": "string", + "description": "The \"x\" (x coordinate) parameter contains the x coordinate for the\nElliptic Curve point. It is represented as the base64url encoding of\nthe octet string representation of the coordinate, as defined in\nSection 2.3.5 of SEC1 [SEC1]. The length of this octet string MUST\nbe the full size of a coordinate for the curve specified in the \"crv\"\nparameter. For example, if the value of \"crv\" is \"P-521\", the octet\nstring must be 66 octets long.\n", + "format": "byte" + }, + "y": { + "maxLength": 66, + "minLength": 66, + "type": "string", + "description": "The \"y\" (y coordinate) parameter contains the y coordinate for the\nElliptic Curve point. It is represented as the base64url encoding of\nthe octet string representation of the coordinate, as defined in\nSection 2.3.5 of SEC1 [SEC1]. The length of this octet string MUST\nbe the full size of a coordinate for the curve specified in the \"crv\"\nparameter. For example, if the value of \"crv\" is \"P-521\", the octet\nstring must be 66 octets long.\n", + "format": "byte" + } + } + }, + "EcPrivateKey": { + "type": "object", + "properties": { + "d": { + "type": "string", + "description": "The \"d\" (ECC private key) parameter contains the Elliptic Curve\nprivate key value. It is represented as the base64url encoding of\nthe octet string representation of the private key value, as defined\nin Section 2.3.7 of SEC1 [SEC1]. The length of this octet string\nMUST be ceiling(log-base-2(n)/8) octets (where n is the order of the\ncurve).\n", + "format": "byte" + } + } + }, + "RsaPublicKey": { + "type": "object", + "properties": { + "n": { + "type": "string", + "description": "The \"n\" (modulus) parameter contains the modulus value for the RSA\npublic key. It is represented as a Base64urlUInt-encoded value.\n\nNote that implementers have found that some cryptographic libraries\nprefix an extra zero-valued octet to the modulus representations they\nreturn, for instance, returning 257 octets for a 2048-bit key, rather\nthan 256. Implementations using such libraries will need to take\ncare to omit the extra octet from the base64url-encoded\nrepresentation.\n", + "format": "byte" + }, + "e": { + "type": "string", + "description": "The \"e\" (exponent) parameter contains the exponent value for the RSA\npublic key. It is represented as a Base64urlUInt-encoded value.\n\nFor instance, when representing the value 65537, the octet sequence\nto be base64url-encoded MUST consist of the three octets [1, 0, 1];\nthe resulting representation for this value is \"AQAB\".\n", + "format": "byte" + } + } + }, + "RsaPrivateKey": { + "type": "object", + "properties": { + "d": { + "type": "string", + "description": "The \"d\" (private exponent) parameter contains the private exponent\nvalue for the RSA private key. It is represented as a Base64urlUInt-\nencoded value.\n", + "format": "byte" + }, + "p": { + "type": "string", + "description": "The \"p\" (first prime factor) parameter contains the first prime\nfactor. It is represented as a Base64urlUInt-encoded value.\n", + "format": "byte" + }, + "q": { + "type": "string", + "description": "The \"q\" (second prime factor) parameter contains the second prime\nfactor. It is represented as a Base64urlUInt-encoded value.\n", + "format": "byte" + }, + "dp": { + "type": "string", + "description": "The \"dp\" (first factor CRT exponent) parameter contains the Chinese\nRemainder Theorem (CRT) exponent of the first factor. It is\nrepresented as a Base64urlUInt-encoded value.\n", + "format": "byte" + }, + "dq": { + "type": "string", + "description": "he \"dq\" (second factor CRT exponent) parameter contains the CRT\nexponent of the second factor. It is represented as a Base64urlUInt-\nencoded value.\n", + "format": "byte" + }, + "qi": { + "type": "string", + "description": "The \"qi\" (first CRT coefficient) parameter contains the CRT\ncoefficient of the second factor. It is represented as a\nBase64urlUInt-encoded value.\n", + "format": "byte" + }, + "oth": { + "type": "array", + "description": "The \"oth\" (other primes info) parameter contains an array of\ninformation about any third and subsequent primes, should they exist.\nWhen only two primes have been used (the normal case), this parameter\nMUST be omitted. When three or more primes have been used, the\nnumber of array elements MUST be the number of primes used minus two.\nFor more information on this case, see the description of the\nOtherPrimeInfo parameters in Appendix A.1.2 of RFC 3447 [RFC3447],\nupon which the following parameters are modeled. If the consumer of\na JWK does not support private keys with more than two primes and it\nencounters a private key that includes the \"oth\" parameter, then it\nMUST NOT use the key. Each array element MUST be an object with the\nfollowing members.\n", + "items": { + "$ref": "#/components/schemas/RsaPrivateKey_oth" + } + } + } + }, + "SymKey": { + "type": "object", + "properties": { + "k": { + "type": "string", + "description": "The \"k\" (key value) parameter contains the value of the symmetric (or\nother single-valued) key. It is represented as the base64url\nencoding of the octet sequence containing the key value.\n", + "format": "byte" + } + } + }, + "error": { + "required": [ + "errorDateTime", + "errors", + "httpMethod", + "requestUri", + "statusCode", + "statusCodeText" + ], + "type": "object", + "properties": { + "httpMethod": { + "type": "string", + "description": "The http request method type e.g. GET, POST\n", + "example": "POST", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "OPTION", + "PATCH" + ] + }, + "requestUri": { + "type": "string", + "description": "The request URI as it was sent\n", + "example": "/v1/events" + }, + "statusCode": { + "type": "integer", + "description": "The HTTP status code\n", + "format": "int32", + "example": 400 + }, + "statusCodeText": { + "maxLength": 50, + "type": "string", + "description": "The textual representation of the status code\n", + "example": "Bad Request" + }, + "errorMessage": { + "maxLength": 200, + "type": "string", + "description": "Other error information\n", + "example": "The supplied data could not be accepted" + }, + "providerCorrelationID": { + "maxLength": 100, + "type": "string", + "description": "A unique identifier for the transaction, e.g. a UUID\n", + "example": "4426d965-0dd8-4005-8c63-dc68b01c4962" + }, + "errorDateTime": { + "type": "string", + "description": "The date and time (in ISO 8601 format) the error occurred.\n", + "format": "date-time", + "example": "2019-11-12T07:41:00+08:30" + }, + "errors": { + "minItems": 1, + "type": "array", + "description": "List of detailed errors, e.g. fields that could not pass validation\n", + "items": { + "allOf": [ + { + "$ref": "#/components/schemas/detailedError" + } + ] + } + } + } + }, + "detailedError": { + "required": [ + "message", + "reason" + ], + "type": "object", + "properties": { + "errorCode": { + "maximum": 9999, + "minimum": 7000, + "type": "integer", + "description": "Standard error code see http://dcsa.org/error-codes (to be created). Examples: 7003 – out or range value, 7004 - invalid type\n", + "format": "int32", + "example": 7003 + }, + "field": { + "maxLength": 500, + "type": "string", + "description": "The field that caused the error, e.g. a failed validation. The field can be expressed as a [JSONpath](https://github.com/json-path/JsonPath)\n", + "example": "location.facilityCode" + }, + "value": { + "maxLength": 500, + "type": "string", + "description": "The value of the field that caused the error\n", + "example": "SG SIN WHS" + }, + "reason": { + "maxLength": 100, + "type": "string", + "description": "High level error message\n", + "example": "invalidData" + }, + "message": { + "maxLength": 200, + "type": "string", + "description": "Additional information as to why the error occured\n", + "example": "Spaces not allowed in facility code" + } + } + }, + "TransportDocument": { + "title": "Transport Document", + "required": [ + "cargoMovementTypeAtDestination", + "cargoMovementTypeAtOrigin", + "carrierCode", + "carrierCodeListProvider", + "consignmentItems", + "deliveryTypeAtDestination", + "documentParties", + "freightPaymentTermCode", + "invoicePayableAt", + "isElectronic", + "isShippedOnBoardType", + "isToOrder", + "issuingParty", + "partyContactDetails", + "receiptTypeAtOrigin", + "termsAndConditions", + "transportDocumentReference", + "transportDocumentStatus", + "transportDocumentTypeCode", + "transports", + "utilizedTransportEquipments" + ], + "type": "object", + "properties": { + "transportDocumentReference": { + "$ref": "#/components/schemas/transportDocumentReference" + }, + "shippingInstructionsReference": { + "$ref": "#/components/schemas/shippingInstructionsReference" + }, + "transportDocumentStatus": { + "$ref": "#/components/schemas/transportDocumentStatus" + }, + "transportDocumentTypeCode": { + "$ref": "#/components/schemas/transportDocumentTypeCode" + }, + "isShippedOnBoardType": { + "$ref": "#/components/schemas/isShippedOnBoardType" + }, + "freightPaymentTermCode": { + "$ref": "#/components/schemas/freightPaymentTermCode" + }, + "originChargesPaymentTermCode": { + "$ref": "#/components/schemas/originChargesPaymentTermCode" + }, + "destinationChargesPaymentTermCode": { + "$ref": "#/components/schemas/destinationChargesPaymentTermCode" + }, + "isElectronic": { + "$ref": "#/components/schemas/isElectronic" + }, + "isToOrder": { + "$ref": "#/components/schemas/isToOrder" + }, + "numberOfCopiesWithCharges": { + "$ref": "#/components/schemas/numberOfCopiesWithCharges" + }, + "numberOfCopiesWithoutCharges": { + "$ref": "#/components/schemas/numberOfCopiesWithoutCharges" + }, + "numberOfOriginalsWithCharges": { + "$ref": "#/components/schemas/numberOfOriginalsWithCharges" + }, + "numberOfOriginalsWithoutCharges": { + "$ref": "#/components/schemas/numberOfOriginalsWithoutCharges" + }, + "displayedNameForPlaceOfReceipt": { + "maxItems": 5, + "minItems": 1, + "type": "array", + "description": "The name to be used in order to specify how the `Place of Receipt` should be displayed on the transport document to match the name and/or address provided on the letter of credit.\n", + "items": { + "$ref": "#/components/schemas/displayedName" + } + }, + "displayedNameForPortOfLoad": { + "maxItems": 5, + "minItems": 1, + "type": "array", + "description": "The name to be used in order to specify how the `Port of Load` should be displayed on the transport document to match the name and/or address provided on the letter of credit.\n", + "items": { + "$ref": "#/components/schemas/displayedName" + } + }, + "displayedNameForPortOfDischarge": { + "maxItems": 5, + "minItems": 1, + "type": "array", + "description": "The name to be used in order to specify how the `Port of Discharge` should be displayed on the transport document to match the name and/or address provided on the letter of credit.\n", + "items": { + "$ref": "#/components/schemas/displayedName" + } + }, + "displayedNameForPlaceOfDelivery": { + "maxItems": 5, + "minItems": 1, + "type": "array", + "description": "The name to be used in order to specify how the `Place of Delivery` should be displayed on the transport document to match the name and/or address provided on the letter of credit.\n", + "items": { + "$ref": "#/components/schemas/displayedName" + } + }, + "shippedOnBoardDate": { + "$ref": "#/components/schemas/shippedOnBoardDate" + }, + "termsAndConditions": { + "$ref": "#/components/schemas/termsAndConditions" + }, + "receiptTypeAtOrigin": { + "$ref": "#/components/schemas/receiptTypeAtOrigin" + }, + "deliveryTypeAtDestination": { + "$ref": "#/components/schemas/deliveryTypeAtDestination" + }, + "cargoMovementTypeAtOrigin": { + "$ref": "#/components/schemas/cargoMovementTypeAtOrigin" + }, + "cargoMovementTypeAtDestination": { + "$ref": "#/components/schemas/cargoMovementTypeAtDestination" + }, + "issueDate": { + "$ref": "#/components/schemas/issueDate" + }, + "receivedForShipmentDate": { + "$ref": "#/components/schemas/receivedForShipmentDate" + }, + "serviceContractReference": { + "$ref": "#/components/schemas/serviceContractReference" + }, + "contractQuotationReference": { + "$ref": "#/components/schemas/contractQuotationReference" + }, + "declaredValue": { + "$ref": "#/components/schemas/declaredValue" + }, + "declaredValueCurrency": { + "$ref": "#/components/schemas/declaredValueCurrency" + }, + "carrierCode": { + "$ref": "#/components/schemas/carrierCode" + }, + "carrierCodeListProvider": { + "$ref": "#/components/schemas/carrierCodeListProvider" + }, + "issuingParty": { + "$ref": "#/components/schemas/Party" + }, + "carrierClauses": { + "type": "array", + "description": "Additional clauses for a specific shipment added by the carrier to the Bill of Lading, subject to local rules / guidelines or certain mandatory information required to be shared with the customer.\n", + "items": { + "$ref": "#/components/schemas/clauseContent" + } + }, + "numberOfRiderPages": { + "$ref": "#/components/schemas/numberOfRiderPages" + }, + "transports": { + "$ref": "#/components/schemas/Transports" + }, + "charges": { + "type": "array", + "description": "A list of `Charges`\n", + "items": { + "$ref": "#/components/schemas/Charge" + } + }, + "placeOfIssue": { + "type": "object", + "description": "General purpose object to capture where the original Transport Document (`Bill of Lading`) will be issued.\n\nThe location can be specified in **one** of the following ways: `UN Location Code` or an `Address`.\n", + "example": { + "locationName": "DCSA Headquarters", + "locationType": "UNLO", + "UNLocationCode": "NLAMS" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "ADDR": "#/components/schemas/addressLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/addressLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "invoicePayableAt": { + "type": "object", + "description": "General purpose object to capture `Invoice Payable At` location specified as: location where payment by the customer will take place. Usually refers to Basic Ocean Freight alone.\n\nThe location can be specified in **one** of the following ways: `UN Location Code` or an `Address`.\n", + "example": { + "locationName": "Eiffel Tower", + "locationType": "UNLO", + "UNLocationCode": "FRPAR" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "ADDR": "#/components/schemas/addressLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/addressLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "partyContactDetails": { + "type": "array", + "description": "The contact details of the person(s) to contact in relation to the **Transport Document** (changes, notifications etc.) \n", + "items": { + "$ref": "#/components/schemas/PartyContactDetail" + } + }, + "documentParties": { + "minLength": 1, + "type": "array", + "description": "A list of `Document Parties`\n", + "items": { + "$ref": "#/components/schemas/DocumentParty" + } + }, + "consignmentItems": { + "minLength": 1, + "type": "array", + "description": "A list of `ConsignmentItems`\n", + "items": { + "$ref": "#/components/schemas/ConsignmentItem_CAR" + } + }, + "utilizedTransportEquipments": { + "minLength": 1, + "type": "array", + "description": "A list of `Utilized Transport Equipments` describing the equipment being used.\n", + "items": { + "$ref": "#/components/schemas/UtilizedTransportEquipment_CAR" + } + }, + "references": { + "type": "array", + "description": "A list of `References`\n", + "items": { + "$ref": "#/components/schemas/Reference" + } + }, + "customsReferences": { + "type": "array", + "description": "A list of `Customs references`\n", + "items": { + "$ref": "#/components/schemas/CustomsReference" + } + } + }, + "description": "The document that governs the terms of carriage between shipper and carrier for maritime transportation. Two distinct types of transport documents exist:\n- Bill of Lading\n- Sea Waybill. \n" + }, + "transportDocumentReference": { + "maxLength": 20, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "A unique number allocated by the shipping line to the transport document and the main number used for the tracking of the status of the shipment.\n", + "example": "HHL71800000" + }, + "shippingInstructionsReference": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The identifier for a `Shipping Istructions` provided by the carrier for system purposes.\n", + "example": "e0559d83-00e2-438e-afd9-fdd610c1a008" + }, + "transportDocumentStatus": { + "maxLength": 50, + "type": "string", + "description": "The status of the `Transport Document`. Possible values are:\n- DRAFT\n- APPROVED\n- ISSUED\n- PENDING SURRENDER FOR AMENDMENT\n- SURRENDER FOR AMENDMENT\n- PENDING SURRENDER FOR DELIVERY\n- SURRENDER FOR DELIVERY\n- VOIDED\n", + "example": "DRAFT" + }, + "transportDocumentTypeCode": { + "type": "string", + "description": "Specifies the type of the transport document\n- BOL (Bill of Lading)\n- SWB (Sea Waybill)\n", + "example": "SWB", + "enum": [ + "BOL", + "SWB" + ] + }, + "isShippedOnBoardType": { + "type": "boolean", + "description": "Specifies whether the Transport document is a received for shipment, or shipped on board.", + "example": true + }, + "freightPaymentTermCode": { + "type": "string", + "description": "An indicator of whether freight and ancillary fees for the main transport are prepaid (PRE) or collect (COL). When prepaid the charges are the responsibility of the shipper or the Invoice payer on behalf of the shipper (if provided). When collect, the charges are the responsibility of the consignee or the Invoice payer on behalf of the consignee (if provided).\n- PRE (Prepaid)\n- COL (Collect)\n", + "example": "PRE", + "enum": [ + "PRE", + "COL" + ] + }, + "originChargesPaymentTermCode": { + "type": "string", + "description": "An indicator of whether origin charges are prepaid (PRE) or collect (COL). When prepaid, the charges are the responsibility of the shipper or the Invoice payer on behalf of the shipper (if provided). When collect, the charges are the responsibility of the consignee or the Invoice payer on behalf of the consignee (if provided). Examples of origin charges are customs clearance fees, documentation fees, container packing and loading charges levied at the port of origin to cover the costs of preparing the cargo for shipment. They include the cost of inland transportation to the port, when applicable.\n- PRE (Prepaid)\n- COL (Collect)\n", + "example": "PRE", + "enum": [ + "PRE", + "COL" + ] + }, + "destinationChargesPaymentTermCode": { + "type": "string", + "description": "An indicator of whether destination charges are prepaid (PRE) or collect (COL). When prepaid, the charges are the responsibility of the shipper or the Invoice payer on behalf of the shipper (if provided). When collect, the charges are the responsibility of the consignee or the Invoice payer on behalf of the consignee (if provided). Examples of destination charges are customs clearance fees, documentation fees, terminal handling fees at the destination port and the costs of inland transportation from the port to the final delivery location, when applicable.\n- PRE (Prepaid)\n- COL (Collect)\n", + "example": "PRE", + "enum": [ + "PRE", + "COL" + ] + }, + "isElectronic": { + "type": "boolean", + "description": "An indicator whether the transport document is electronically transferred.\n", + "example": true, + "default": false + }, + "isToOrder": { + "type": "boolean", + "description": "Indicates whether the B/L is issued `to order` or not. If `true`, the B/L is considered negotiable and an Endorsee party can be defined in the Document parties. If no Endorsee is defined, the B/L is blank endorsed. If `false`, the B/L is considered non-negotiable (also referred to as `straight`).\n\n`isToOrder` must be `false` if `transportDocumentTypeCode='SWB'` (Sea Waybill).\n", + "example": false + }, + "numberOfCopiesWithCharges": { + "minimum": 0, + "type": "integer", + "description": "The requested number of copies of the Transport document to be issued by the carrier including charges. Only applicable for physical (paper) documents", + "format": "int32", + "example": 2 + }, + "numberOfCopiesWithoutCharges": { + "minimum": 0, + "type": "integer", + "description": "The requested number of copies of the Transport document to be issued by the carrier **NOT** including charges. Only applicable for physical (paper) documents", + "format": "int32", + "example": 2 + }, + "numberOfOriginalsWithCharges": { + "minimum": 0, + "type": "integer", + "description": "Number of originals of the bill of lading that has been requested by the customer with charges. Only applicable for physical documents.\n", + "format": "int32", + "example": 1 + }, + "numberOfOriginalsWithoutCharges": { + "minimum": 0, + "type": "integer", + "description": "Number of originals of the bill of lading that has been requested by the customer without charges. Only applicable for physical documents.\n", + "format": "int32", + "example": 1 + }, + "displayedName": { + "maxLength": 35, + "type": "string", + "description": "A line of the address to be displayed on the transport document.\n" + }, + "shippedOnBoardDate": { + "type": "string", + "description": "Date when the last container that is linked to the transport document is physically loaded onboard the vessel indicated on the transport document.\n\nWhen provided on a transport document, the transportDocument is a `Shipped On Board` B/L.\n\nExactly one of `shippedOnBoard` and `receiveForShipmentDate` must be provided on an issued B/L.\n", + "format": "date", + "example": "2020-12-12" + }, + "termsAndConditions": { + "maxLength": 50000, + "type": "string", + "description": "Carrier terms and conditions of transport.\n" + }, + "receiptTypeAtOrigin": { + "maxLength": 3, + "type": "string", + "description": "Indicates the type of service offered at Origin. Options are defined in the Receipt/Delivery entity.\n- CY (Container yard (incl. rail ramp))\n- SD (Store Door)\n- CFS (Container Freight Station)\n", + "example": "CY", + "enum": [ + "CY", + "SD", + "CFS" + ] + }, + "deliveryTypeAtDestination": { + "maxLength": 3, + "type": "string", + "description": "Indicates the type of service offered at Destination. Options are defined in the Receipt/Delivery entity.\n- CY (Container yard (incl. rail ramp))\n- SD (Store Door)\n- CFS (Container Freight Station)\n", + "example": "CY", + "enum": [ + "CY", + "SD", + "CFS" + ] + }, + "cargoMovementTypeAtOrigin": { + "maxLength": 3, + "type": "string", + "description": "Refers to the shipment term at the loading of the cargo into the container. Options are defined in the Cargo Movement Type entity. Possible values are:\n- `FCL` (Full Container Load)\n- `LCL` (Less than Container Load)\n", + "example": "FCL" + }, + "cargoMovementTypeAtDestination": { + "maxLength": 3, + "type": "string", + "description": "Refers to the shipment term at the unloading of the cargo out of the container. Options are defined in the Cargo Movement Type entity. Possible values are:\n- `FCL` (Full Container Load)\n- `LCL` (Less than Container Load)\n", + "example": "FCL" + }, + "issueDate": { + "type": "string", + "description": "Local date when the transport document has been issued.\n\nCan be omitted on draft transport documents, but must be provided when the document has been issued.\n", + "format": "date", + "example": "2020-12-12" + }, + "receivedForShipmentDate": { + "type": "string", + "description": "Date when the last container linked to the transport document is physically in the terminal (customers cleared against the intended vessel).\n\nWhen provided on a transport document, the transportDocument is a `Received For Shipment` B/L.\n\nExactly one of `shippedOnBoard` and `receiveForShipmentDate` must be provided on an issued B/L.\n", + "format": "date", + "example": "2020-12-12" + }, + "serviceContractReference": { + "maxLength": 30, + "type": "string", + "description": "Reference number for agreement between shipper and carrier through which the shipper commits to provide a certain minimum quantity of cargo over a fixed period, and the carrier commits to a certain rate or rate schedule.", + "example": "HHL51800000" + }, + "contractQuotationReference": { + "maxLength": 35, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Information provided by the shipper to identify whether pricing for the shipment has been agreed via a contract or a quotation reference. Mandatory if service contract (owner) is not provided.\n", + "example": "HHL1401" + }, + "declaredValue": { + "minimum": 0, + "type": "number", + "description": "The value of the cargo that the shipper declares to avoid the carrier's limitation of liability and \"Ad Valorem\" freight, i.e. freight which is calculated based on the value of the goods declared by the shipper.\n", + "format": "float", + "example": 1231.1 + }, + "declaredValueCurrency": { + "maxLength": 3, + "pattern": "^[A-Z]{3}$", + "type": "string", + "description": "The currency used for the declared value, using the 3-character code defined by [ISO 4217](https://en.wikipedia.org/wiki/ISO_4217).\n", + "example": "DKK" + }, + "carrierCode": { + "maxLength": 4, + "pattern": "^\\S+$", + "type": "string", + "description": "The code containing the SCAC and/or the SMDG code to specify the issuing carrier. Details about the issuer can be given in the Document Parties entity using the party function code MS.\n", + "example": "MMCU" + }, + "carrierCodeListProvider": { + "type": "string", + "description": "The provider used for identifying the issuer Code. Possible values are:\n- SMDG (Ship Message Design Group)\n- NMFTA (National Motor Freight Traffic Association) _includes SPLC (Standard Point Location Code)_\n", + "example": "NMFTA", + "enum": [ + "SMDG", + "NMFTA" + ] + }, + "Party": { + "required": [ + "partyName" + ], + "type": "object", + "properties": { + "partyName": { + "$ref": "#/components/schemas/partyName" + }, + "address": { + "$ref": "#/components/schemas/address" + }, + "partyContactDetails": { + "minItems": 1, + "type": "array", + "description": "A list of contact details\n", + "items": { + "$ref": "#/components/schemas/PartyContactDetail" + } + }, + "identifyingCodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdentifyingCode" + } + }, + "taxLegalReferences": { + "type": "array", + "description": "A list of `Tax References` for a `Party`\n", + "items": { + "$ref": "#/components/schemas/TaxLegalReference" + } + } + }, + "description": "Refers to a company or a legal entity.\n" + }, + "partyName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Name of the party.\n", + "example": "Asseco Denmark" + }, + "address": { + "required": [ + "country", + "name" + ], + "type": "object", + "properties": { + "name": { + "$ref": "#/components/schemas/addressName" + }, + "street": { + "$ref": "#/components/schemas/streetName" + }, + "streetNumber": { + "$ref": "#/components/schemas/streetNumber" + }, + "floor": { + "$ref": "#/components/schemas/floor" + }, + "postCode": { + "$ref": "#/components/schemas/postCode" + }, + "city": { + "$ref": "#/components/schemas/cityName" + }, + "stateRegion": { + "$ref": "#/components/schemas/stateRegion" + }, + "country": { + "$ref": "#/components/schemas/country" + } + }, + "description": "An object for storing address related information\n" + }, + "addressName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Name of the address\n", + "example": "Henrik" + }, + "streetName": { + "maxLength": 100, + "type": "string", + "description": "The name of the street of the party’s address.", + "example": "Ruijggoordweg" + }, + "streetNumber": { + "maxLength": 50, + "type": "string", + "description": "The number of the street of the party’s address.", + "example": "100" + }, + "floor": { + "maxLength": 50, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The floor of the party’s street number.\n", + "example": "N/A" + }, + "postCode": { + "maxLength": 50, + "type": "string", + "description": "The post code of the party’s address.", + "example": "1047 HM" + }, + "cityName": { + "maxLength": 65, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The city name of the party’s address.\n", + "example": "Amsterdam" + }, + "stateRegion": { + "maxLength": 65, + "type": "string", + "description": "The state/region of the party’s address.", + "nullable": true, + "example": "North Holland" + }, + "country": { + "maxLength": 75, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The country of the party’s address.\n", + "example": "The Netherlands" + }, + "PartyContactDetail": { + "title": "Party Contact Detail", + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "$ref": "#/components/schemas/contactName" + } + }, + "description": "The contact details of the person to contact. It is mandatory to provide either `phone` and/or `email` along with the `name`.\n", + "example": { + "name": "Henrik", + "phone": "+45 51801234" + }, + "anyOf": [ + { + "$ref": "#/components/schemas/PhoneRequired" + }, + { + "$ref": "#/components/schemas/EmailRequired" + } + ] + }, + "PhoneRequired": { + "title": "Phone required", + "required": [ + "phone" + ], + "type": "object", + "properties": { + "phone": { + "$ref": "#/components/schemas/contactPhone" + } + }, + "description": "`Phone` is mandatory to provide\n" + }, + "contactPhone": { + "maxLength": 30, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Phone number for the contact\n", + "example": "+45 70262970" + }, + "EmailRequired": { + "title": "Email required", + "required": [ + "email" + ], + "type": "object", + "properties": { + "email": { + "$ref": "#/components/schemas/email" + } + }, + "description": "`Email` is mandatory to provide\n" + }, + "email": { + "maxLength": 100, + "pattern": "^.+@\\S+$", + "type": "string", + "description": "`E-mail` address to be used\n", + "example": "info@dcsa.org" + }, + "contactName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Name of the contact\n", + "example": "Henrik" + }, + "IdentifyingCode": { + "title": "Identifying Code", + "required": [ + "codeListProvider", + "partyCode" + ], + "type": "object", + "properties": { + "codeListProvider": { + "$ref": "#/components/schemas/codeListProvider" + }, + "partyCode": { + "$ref": "#/components/schemas/partyCode" + }, + "codeListName": { + "$ref": "#/components/schemas/codeListName" + } + } + }, + "codeListProvider": { + "maxLength": 5, + "type": "string", + "description": "A DCSA provided code for [UN/CEFACT](https://unece.org/fileadmin/DAM/trade/untdid/d16b/tred/tred3055.htm) code list providers:\n- ISO (International Standards Organization)\n- UNECE (United Nations Economic Commission for Europe)\n- LLOYD (Lloyd's register of shipping)\n- BIC (Bureau International des Containeurs)\n- IMO (International Maritime Organization)\n- SCAC (Standard Carrier Alpha Code)\n- ITIGG (International Transport Implementation Guidelines Group)\n- ITU (International Telecommunication Union)\n- SMDG (Shipplanning Message Development Group)\n- NCBH (NCB Hazcheck)\n- FMC (Federal Maritime Commission)\n- CBSA (Canada Border Services Agency)\n- DCSA (Digitial Container Shipping Association)\n- W3C (World Wide Web Consortium)\n- GLEIF (Global Legal Entity Identifier Foundation)\n- EPI (EBL Platform Identifier)\n- ZZZ (Mutually defined)\n", + "example": "SMDG" + }, + "partyCode": { + "maxLength": 100, + "type": "string", + "description": "Code to identify the party as provided by the code list provider\n", + "example": "MSK" + }, + "codeListName": { + "maxLength": 100, + "type": "string", + "description": "The name of the list, provided by the code list provider\n", + "example": "LCL" + }, + "TaxLegalReference": { + "title": "Tax & Legal Reference", + "required": [ + "countryCode", + "type", + "value" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/taxLegalReferenceType" + }, + "countryCode": { + "$ref": "#/components/schemas/countryCode" + }, + "value": { + "$ref": "#/components/schemas/taxLegalReferenceValue" + } + }, + "description": "Reference that uniquely identifies a party for tax and/or legal purposes in accordance with the relevant jurisdiction.\n\nA list of examples:\n\n| Type | Country | Description |\n|-------|:-------:|-------------|\n|PAN|IN|Goods and Services Tax Identification Number in India|\n|GSTIN|IN|Goods and Services Tax Identification Number in India|\n|IEC|IN|Importer-Exported Code in India|\n|RUC|EC|Registro Único del Contribuyente in Ecuador|\n|RUC|PE|Registro Único del Contribuyente in Peru|\n|NIF|MG|Numéro d’Identification Fiscal in Madagascar|\n|NIF|DZ|Numéro d’Identification Fiscal in Algeria|\n\nAllowed combinations of `type` and `country` are maintained in [GitHub](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/domain/documentation/reference-data/taxandlegalreferences-v300.csv).\n" + }, + "taxLegalReferenceType": { + "maxLength": 50, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The reference type code as defined by the relevant tax and/or legal authority.\n", + "example": "PAN" + }, + "countryCode": { + "maxLength": 2, + "minLength": 2, + "pattern": "^[A-Z]{2}$", + "type": "string", + "description": "The 2 characters for the country code using [ISO 3166-1 alpha-2](https://www.iso.org/obp/ui/#iso:pub:PUB500001:en)\n", + "example": "DK" + }, + "taxLegalReferenceValue": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The value of the `taxLegalReference`\n", + "example": "AAAAA0000A" + }, + "clauseContent": { + "maxLength": 20000, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The content of the clause.\n", + "example": "It is not allowed to..." + }, + "numberOfRiderPages": { + "minimum": 0, + "type": "integer", + "description": "The number of additional pages required to contain the goods description on a transport document. Only applicable for physical transport documents.", + "format": "int32", + "example": 2 + }, + "Transports": { + "required": [ + "carrierExportVoyageNumber", + "plannedArrivalDate", + "plannedDepartureDate", + "portOfDischarge", + "portOfLoading", + "vesselName" + ], + "type": "object", + "properties": { + "plannedArrivalDate": { + "$ref": "#/components/schemas/plannedArrivalDate" + }, + "plannedDepartureDate": { + "$ref": "#/components/schemas/plannedDepartureDate" + }, + "preCarriageBy": { + "maxLength": 50, + "type": "string", + "description": "Mode of transportation for pre-carriage when transport to the port of loading is organized by the carrier. If this attributes is populated, then a Place of Receipt must also be defined. The currently supported values include:\n- VESSEL\n- RAIL\n- TRUCK\n- BARGE\n", + "example": "RAIL" + }, + "onCarriageBy": { + "maxLength": 50, + "type": "string", + "description": "Mode of transportation for on-carriage when transport from the port of discharge is organized by the carrier. If this attributes is populated, then a Place of Delivery must also be defined. The currently supported values include:\n- VESSEL\n- RAIL\n- TRUCK\n- BARGE\n", + "example": "TRUCK" + }, + "placeOfReceipt": { + "$ref": "#/components/schemas/PlaceOfReceipt" + }, + "portOfLoading": { + "$ref": "#/components/schemas/PortOfLoading" + }, + "portOfDischarge": { + "$ref": "#/components/schemas/PortOfDischarge" + }, + "placeOfDelivery": { + "$ref": "#/components/schemas/PlaceOfDelivery" + }, + "onwardInlandRouting": { + "$ref": "#/components/schemas/OnwardInlandRouting" + }, + "vesselName": { + "maxLength": 35, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The name of the first sea going Vessel on board which the cargo is loaded or intended to be loaded\n", + "example": "King of the Seas" + }, + "carrierExportVoyageNumber": { + "$ref": "#/components/schemas/carrierExportVoyageNumber" + }, + "universalExportVoyageReference": { + "$ref": "#/components/schemas/universalExportVoyageReference" + } + } + }, + "plannedArrivalDate": { + "type": "string", + "description": "The planned date of arrival.\n", + "format": "date" + }, + "plannedDepartureDate": { + "type": "string", + "description": "The planned date of departure.\n", + "format": "date" + }, + "PlaceOfReceipt": { + "description": "General purpose object to capture `Place of Receipt` location specified as: the location where the cargo is handed over by the shipper, or his agent, to the shipping line. This indicates the point at which the shipping line takes on responsibility for carriage of the container.\n\n**Condition:** Only when pre-carriage is done by the carrier.\n\nThe location can be specified in **one** of the following ways: `UN Location Code`, `Facility` or an `Address`.\n", + "example": { + "locationName": "Hamburg", + "locationType": "UNLO", + "UNLocationCode": "DEHAM" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "ADDR": "#/components/schemas/addressLocation", + "FACI": "#/components/schemas/facilityLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/addressLocation" + }, + { + "$ref": "#/components/schemas/facilityLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "addressLocation": { + "required": [ + "address", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "$ref": "#/components/schemas/locationName" + }, + "locationType": { + "type": "string", + "description": "Discriminator used to identify this as a `Address Location` interface\n", + "example": "ADDR" + }, + "address": { + "$ref": "#/components/schemas/address" + } + }, + "description": "An interface used to express a location using an `Address` object\n" + }, + "locationName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The name of the location.\n", + "example": "Port of Amsterdam" + }, + "facilityLocation": { + "required": [ + "facilityCode", + "facilityCodeListProvider", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "$ref": "#/components/schemas/locationName" + }, + "locationType": { + "type": "string", + "description": "Discriminator used to identify this as a `Facility Location` interface\n", + "example": "FACI" + }, + "UNLocationCode": { + "allOf": [ + { + "$ref": "#/components/schemas/UNLocationCode" + }, + { + "description": "The UN Location code specifying where the place is located.\n\nThis field is **conditionally mandatory** depending on the value of the `facilityCodeListProvider` field.\n" + } + ] + }, + "facilityCode": { + "allOf": [ + { + "$ref": "#/components/schemas/facilityCode" + }, + { + "description": "The code used for identifying the specific facility. This code does not include the UN Location Code.\n\nThe definition of the code depends on the `facilityCodeListProvider`. As code list providers maintain multiple codeLists the following codeList is used:\n\n- for `SMDG` - the codeList used is the [SMDG Terminal Code List](https://smdg.org/wp-content/uploads/Codelists/Terminals/SMDG-Terminal-Code-List-v20210401.xlsx) \n- for `BIC` - the codeList used is the [BIC Facility Codes](https://www.bic-code.org/facility-codes/)\n" + } + ] + }, + "facilityCodeListProvider": { + "$ref": "#/components/schemas/facilityCodeListProvider" + } + }, + "description": "An interface used to express a location using a `Facility`. The facility can either be expressed using a `BIC` code or a `SMDG` code. The `facilityCode` does not contain the `UNLocationCode` - this should be provided in the `UnLocationCode` attribute.\n" + }, + "UNLocationCode": { + "maxLength": 5, + "minLength": 5, + "pattern": "^[A-Z]{2}[A-Z2-9]{3}$", + "type": "string", + "description": "The UN Location code specifying where the place is located. The pattern used must be\n- 2 characters for the country code using [ISO 3166-1 alpha-2](https://www.iso.org/obp/ui/#iso:pub:PUB500001:en)\n- 3 characters to code a location within that country. Letters A-Z and numbers from 2-9 can be used\n\nMore info can be found here: [UN/LOCODE](https://unece.org/trade/cefact/UNLOCODE-Download)\n", + "example": "FRPAR" + }, + "facilityCode": { + "maxLength": 6, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The code used for identifying the specific facility. This code does not include the UN Location Code.\n", + "nullable": false, + "example": "ADT" + }, + "facilityCodeListProvider": { + "type": "string", + "description": "The provider used for identifying the facility Code. Some facility codes are only defined in combination with an `UN Location Code`\n- BIC (Requires a UN Location Code)\n- SMDG (Requires a UN Location Code)\n", + "example": "SMDG", + "enum": [ + "BIC", + "SMDG" + ] + }, + "unLocationLocation": { + "required": [ + "UNLocationCode", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "$ref": "#/components/schemas/locationName" + }, + "locationType": { + "type": "string", + "description": "Discriminator used to identify this as a `UNLocation` location interface\n", + "example": "UNLO" + }, + "UNLocationCode": { + "$ref": "#/components/schemas/UNLocationCode" + } + }, + "description": "An interface used to express a location using a `Un Location Code`\n" + }, + "PortOfLoading": { + "description": "General purpose object to capture `Port of Loading` location specified as: the location where the cargo is loaded onto a first sea-going vessel for water transportation.\n\nThe location can be specified in **one** of the following ways: `UN Location Code` or `City and Country`.\n", + "example": { + "locationName": "Hamburg", + "locationType": "UNLO", + "UNLocationCode": "DEHAM" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "CITY": "#/components/schemas/cityLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/cityLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "cityLocation": { + "required": [ + "city", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "$ref": "#/components/schemas/locationName" + }, + "locationType": { + "type": "string", + "description": "Discriminator used to identify this as a `City Location` interface\n", + "example": "CITY" + }, + "city": { + "$ref": "#/components/schemas/city" + } + }, + "description": "An interface used to express a location using a `City`, `state/region` and `country`\n" + }, + "city": { + "required": [ + "city", + "country" + ], + "type": "object", + "properties": { + "city": { + "$ref": "#/components/schemas/cityName" + }, + "stateRegion": { + "$ref": "#/components/schemas/stateRegion" + }, + "country": { + "$ref": "#/components/schemas/country" + } + }, + "description": "An object for storing city, state/region and coutry related information\n" + }, + "PortOfDischarge": { + "description": "General purpose object to capture `Port of Discharge` location specified as: the location where the cargo is discharged from the last sea-going vessel.\n\nThe location can be specified in **one** of the following ways: `UN Location Code` or `City and Country`.\n", + "example": { + "locationName": "Hamburg", + "locationType": "UNLO", + "UNLocationCode": "DEHAM" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "CITY": "#/components/schemas/cityLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/cityLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "PlaceOfDelivery": { + "description": "General purpose object to capture `Place of Delivery` location specified as: the location where the cargo is handed over to the consignee, or his agent, by the shipping line and where responsibility of the shipping line ceases.\n\n**Condition:** Only when onward transport is done by the carrier\n\nThe location can be specified in **one** of the following ways: `UN Location Code`, `Facility` or an `Address`.\n", + "example": { + "locationName": "Hamburg", + "locationType": "UNLO", + "UNLocationCode": "DEHAM" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "ADDR": "#/components/schemas/addressLocation", + "FACI": "#/components/schemas/facilityLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/addressLocation" + }, + { + "$ref": "#/components/schemas/facilityLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "OnwardInlandRouting": { + "description": "General purpose object to capture `Onward Inland Routing` location specified as the end location of the inland movement that takes place after the container(s) being delivered to the port of discharge/place of delivery for account and risk of merchant (merchant haulage).\n\nThe location can be specified in **one** of the following ways: `UN Location Code`, `Facility` or an `Address`.\n", + "example": { + "locationName": "Hamburg", + "locationType": "UNLO", + "UNLocationCode": "DEHAM" + }, + "discriminator": { + "propertyName": "locationType", + "mapping": { + "ADDR": "#/components/schemas/addressLocation", + "FACI": "#/components/schemas/facilityLocation", + "UNLO": "#/components/schemas/unLocationLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/addressLocation" + }, + { + "$ref": "#/components/schemas/facilityLocation" + }, + { + "$ref": "#/components/schemas/unLocationLocation" + } + ] + }, + "carrierExportVoyageNumber": { + "maxLength": 50, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The identifier of an export voyage. The carrier-specific identifier of the export Voyage.\n", + "example": "2103S" + }, + "universalExportVoyageReference": { + "pattern": "^\\d{2}[0-9A-Z]{2}[NEWSR]$", + "type": "string", + "description": "A global unique voyage reference for the export Voyage, as per DCSA standard, agreed by VSA partners for the voyage. The voyage reference must match the regular expression pattern: `\\d{2}[0-9A-Z]{2}[NEWSR]`\n- `2 digits` for the year\n- `2 alphanumeric characters` for the sequence number of the voyage\n- `1 character` for the direction/haul (`N`orth, `E`ast, `W`est, `S`outh or `R`oundtrip).\n", + "example": "2103N" + }, + "Charge": { + "required": [ + "calculationBasis", + "chargeName", + "currencyAmount", + "currencyCode", + "paymentTermCode", + "quantity", + "unitPrice" + ], + "type": "object", + "properties": { + "chargeName": { + "$ref": "#/components/schemas/chargeName" + }, + "currencyAmount": { + "$ref": "#/components/schemas/currencyAmount" + }, + "currencyCode": { + "$ref": "#/components/schemas/currencyCode" + }, + "paymentTermCode": { + "$ref": "#/components/schemas/paymentTermCode" + }, + "calculationBasis": { + "$ref": "#/components/schemas/calculationBasis" + }, + "unitPrice": { + "$ref": "#/components/schemas/unitPrice" + }, + "quantity": { + "$ref": "#/components/schemas/quantity" + } + }, + "description": "addresses the monetary value of freight and other service charges for a `Transport Document`.\n" + }, + "chargeName": { + "maxLength": 50, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Free text field describing the charge to apply\n", + "example": "Documentation fee - Destination" + }, + "currencyAmount": { + "minimum": 0, + "type": "number", + "description": "The monetary value of all freight and other service charges for a transport document, with a maximum of 2-digit decimals.\n", + "format": "float", + "example": 1012.12 + }, + "currencyCode": { + "maxLength": 3, + "pattern": "^[A-Z]{3}$", + "type": "string", + "description": "The currency for the charge, using a 3-character code ([ISO 4217](https://en.wikipedia.org/wiki/ISO_4217)).\n", + "example": "DKK" + }, + "paymentTermCode": { + "type": "string", + "description": "An indicator of whether a charge is prepaid (PRE) or collect (COL). When prepaid, the charge is the responsibility of the shipper or the Invoice payer on behalf of the shipper (if provided). When collect, the charge is the responsibility of the consignee or the Invoice payer on behalf of the consignee (if provided).\n- PRE (Prepaid)\n- COL (Collect)\n", + "example": "PRE", + "enum": [ + "PRE", + "COL" + ] + }, + "calculationBasis": { + "maxLength": 50, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The code specifying the measure unit used for the corresponding unit price for this cost, such as per day, per ton, per square metre.", + "example": "Per day" + }, + "unitPrice": { + "minimum": 0, + "type": "number", + "description": "The unit price of this charge item in the currency of the charge.\n", + "format": "float", + "example": 3456.6 + }, + "quantity": { + "minimum": 0, + "type": "number", + "description": "The amount of unit for this charge item.\n", + "format": "float", + "example": 34.4 + }, + "DocumentParty": { + "title": "Document Party", + "required": [ + "isToBeNotified", + "party", + "partyFunction" + ], + "type": "object", + "properties": { + "party": { + "$ref": "#/components/schemas/Party" + }, + "partyFunction": { + "$ref": "#/components/schemas/partyFunction" + }, + "displayedAddress": { + "maxItems": 5, + "minItems": 1, + "type": "array", + "description": "The address to be displayed in the `Transport Document`. The displayed address may be used to match the address provided in the letter of credit. It is mandatory to provide a displayed address if the B/L needs to be switched to paper later in the process\n", + "items": { + "$ref": "#/components/schemas/addressLine" + } + }, + "isToBeNotified": { + "$ref": "#/components/schemas/isToBeNotified" + } + }, + "description": "Associates a Party with a role.\n" + }, + "partyFunction": { + "maxLength": 3, + "type": "string", + "description": "Specifies the role of the party in a given context. Possible values are:\n- `OS` (Original shipper)\n- `CN` (Consignee)\n- `COW` (Invoice payer on behalf of the consignor (shipper))\n- `COX` (Invoice payer on behalf of the consignee)\n- `MS` (Document/message issuer/sender)\n- `N1` (First Notify Party)\n- `N2` (Second Notify Party)\n- `NI` (Other Notify Party)\n- `DDR` (Consignor's freight forwarder)\n- `DDS` (Consignee's freight forwarder)\n- `HE` (Carrier booking office (transportation office))\n- `SCO` (Service contract owner - Defined by DCSA)\n- `BA` (Booking Agency)\n- `END` (Endorsee Party)\n", + "example": "DDS" + }, + "addressLine": { + "maxLength": 35, + "type": "string", + "description": "A single address line to be used when a B/L needs to be printed.\n", + "example": "Kronprincessegade 54" + }, + "isToBeNotified": { + "type": "boolean", + "description": "Used to decide whether the party will be notified of the arrival of the cargo.", + "example": true + }, + "ConsignmentItem_CAR": { + "title": "Consignment Item", + "required": [ + "cargoItems" + ], + "type": "object", + "properties": { + "cargoItems": { + "minItems": 1, + "type": "array", + "description": "A list of all `cargoItems`\n", + "items": { + "$ref": "#/components/schemas/CargoItem_CAR" + } + } + }, + "description": "Defines a list of `CargoItems` belonging together and the associated `Booking`. A `ConsignmentItem` can be split across multiple containers (`UtilizedTransportEquipment`) by referencing multiple `CargoItems`\n", + "allOf": [ + { + "$ref": "#/components/schemas/ConsignmentItem" + } + ] + }, + "ConsignmentItem": { + "title": "Consignment Item", + "required": [ + "HSCodes", + "cargoItems", + "carrierBookingReference", + "descriptionOfGoods", + "weight", + "weightUnit" + ], + "type": "object", + "properties": { + "carrierBookingReference": { + "maxLength": 35, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The associated booking number provided by the carrier for this `Consignment Item`.\n\nWhen multiple `carrierBookingReferences` are used then the bookings referred to must all contain the same:\n- transportPlan\n- shipmentLocations\n- receiptTypeAtOrigin\n- deliveryTypeAtDestination\n- cargoMovementTypeAtOrigin\n- cargoMovementTypeAtDestination\n- serviceContractReference\n- termsAndConditions\n- Place of B/L Issue (if provided)\n", + "example": "ABC709951" + }, + "weight": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The total weight of all the `CargoItems` listed in the `ConsignmentItem`. Excludes the tare weight of the container(s).\n", + "format": "float", + "example": 13000.3 + }, + "weightUnit": { + "$ref": "#/components/schemas/weightUnit" + }, + "volume": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The total volume of all the `CargoItems` listed in the `ConsignmentItem`.\n", + "format": "float", + "example": 12 + }, + "volumeUnit": { + "$ref": "#/components/schemas/volumeUnit" + }, + "descriptionOfGoods": { + "$ref": "#/components/schemas/descriptionOfGoods" + }, + "HSCodes": { + "minLength": 1, + "type": "array", + "description": "A list of `HS Codes` that apply to this `consignmentItem`\n", + "items": { + "$ref": "#/components/schemas/HSCode" + } + }, + "references": { + "type": "array", + "description": "A list of `References`\n", + "items": { + "$ref": "#/components/schemas/Reference" + } + }, + "customsReferences": { + "type": "array", + "description": "A list of `Customs references`\n", + "items": { + "$ref": "#/components/schemas/CustomsReference" + } + } + }, + "description": "Defines a list of `CargoItems` belonging together and the associated `Booking`. A `ConsignmentItem` can be split across multiple containers (`UtilizedTransportEquipment`) by referencing multiple `CargoItems`\n" + }, + "weightUnit": { + "type": "string", + "description": "The unit of measure which can be expressed in imperial or metric terms\n- KGM (Kilograms)\n- LBR (Pounds)\n", + "example": "KGM", + "enum": [ + "KGM", + "LBR" + ] + }, + "volumeUnit": { + "type": "string", + "description": "The unit of measure which can be expressed in either imperial or metric terms\n- FTQ (Cubic foot)\n- MTQ (Cubic meter)\n", + "example": "MTQ", + "enum": [ + "MTQ", + "FTQ" + ] + }, + "descriptionOfGoods": { + "maxLength": 5000, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The cargo description are details which accurately and properly describe the cargo being shipped in the container(s) as provided by the shipper.", + "example": "300 boxes of blue shoes size 47" + }, + "HSCode": { + "maxLength": 10, + "minLength": 6, + "pattern": "^\\d{6,10}$", + "type": "string", + "description": "Used by customs to classify the product being shipped. The type of HS code depends on country and customs requirements. The code must be at least 6 and at most 10 digits.\n\nMore information can be found here: [HS Nomenclature 2022 edition](https://www.wcoomd.org/en/topics/nomenclature/instrument-and-tools/hs-nomenclature-2022-edition/hs-nomenclature-2022-edition.aspx ).\n\nThis standard is based on the 2022 revision.\n", + "example": "851713" + }, + "Reference": { + "required": [ + "type", + "value" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/referenceType" + }, + "value": { + "$ref": "#/components/schemas/referenceValue" + } + }, + "description": "References provided by the shipper or freight forwarder at the time of `Booking` or at the time of providing `Shipping Instructions`. Carriers share it back when providing track and trace event updates, some are also printed on the B/L. Customers can use these references to track shipments in their internal systems.\n" + }, + "referenceType": { + "maxLength": 3, + "type": "string", + "description": "The reference type codes defined by DCSA. Possible values are:\n- FF (Freight Forwarder’s Reference)\n- SI (Shipper’s Reference)\n- SPO (Shippers Purchase Order Reference)\n- CPO (Consignees Purchase Order Reference)\n- CR (Customer’s Reference)\n- AAO (Consignee’s Reference)\n- ECR (Empty container release reference)\n- CSI (Customer shipment ID)\n- BPR (Booking party reference number)\n- BID (Booking Request ID)\n- SAC (Shipping Agency Code)\n", + "example": "FF" + }, + "referenceValue": { + "maxLength": 100, + "type": "string", + "description": "The actual value of the reference. \n", + "example": "HHL00103004" + }, + "CustomsReference": { + "title": "Custom Reference", + "required": [ + "countryCode", + "type", + "value" + ], + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/customsReferenceType" + }, + "countryCode": { + "$ref": "#/components/schemas/countryCode" + }, + "value": { + "$ref": "#/components/schemas/customsReferenceValue" + } + }, + "description": "Reference associated with customs and/or excise purposes required by the relevant authorities for the import, export, or transit of the goods.\n\nA (small) list of examples:\n\n| Type | Country | Description |\n|-------|:-------:|-------------|\n|ACID|EG|Advance Cargo Information Declaration in Egypt|\n|CERS|CA|Canadian Export Reporting System|\n|ITN|US|Internal Transaction Number in US|\n|PEB|ID|PEB reference number|\n|CSN|IN|Cargo Summary Notification (CSN)|\n\nAllowed combinations of `type` and `country` are maintained in [GitHub](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/domain/documentation/reference-data/customsreferences-v300.csv).\n" + }, + "customsReferenceType": { + "maxLength": 50, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The reference type code as defined in the relevant customs jurisdiction.\n", + "example": "ACID" + }, + "customsReferenceValue": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The value of the `customsReference`\n", + "example": "4988470982020120017" + }, + "CargoItem_CAR": { + "title": "Cargo Item", + "required": [ + "equipmentReference", + "outerPackaging", + "weight", + "weightUnit" + ], + "type": "object", + "properties": { + "shippingMarks": { + "$ref": "#/components/schemas/ShippingMarks" + }, + "equipmentReference": { + "$ref": "#/components/schemas/equipmentReference" + }, + "weight": { + "$ref": "#/components/schemas/weight" + }, + "volume": { + "$ref": "#/components/schemas/volume" + }, + "weightUnit": { + "$ref": "#/components/schemas/weightUnit" + }, + "volumeUnit": { + "$ref": "#/components/schemas/volumeUnit" + }, + "outerPackaging": { + "$ref": "#/components/schemas/OuterPackaging_CAR" + }, + "customsReferences": { + "type": "array", + "description": "A list of `Customs references`\n", + "items": { + "$ref": "#/components/schemas/CustomsReference" + } + } + }, + "description": "A `cargoItem` is the smallest unit used by stuffing. A `cargoItem` cannot be split across containers.\n" + }, + "ShippingMarks": { + "minItems": 1, + "type": "array", + "description": "A list of the `ShippingMarks` applicable to this `cargoItem`\n", + "items": { + "maxLength": 500, + "type": "string", + "description": "The identifying details of a package or the actual markings that appear on the package(s). This information is provided by the customer.\n", + "example": "Made in China" + } + }, + "equipmentReference": { + "maxLength": 11, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The unique identifier for the equipment, which should follow the BIC ISO Container Identification Number where possible.\nAccording to [ISO 6346](https://en.wikipedia.org/wiki/ISO_6346), a container identification code consists of a 4-letter prefix and a 7-digit number (composed of a 3-letter owner code, a category identifier, a serial number, and a check-digit).\n\nIf a container does not comply with [ISO 6346](https://en.wikipedia.org/wiki/ISO_6346), it is suggested to follow [Recommendation #2: Containers with non-ISO identification](https://smdg.org/documents/smdg-recommendations) from SMDG.\n", + "example": "APZU4812090" + }, + "weight": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The total weight of the cargo including packaging items being carried in the container(s). Excludes the tare weight of the container(s).\n", + "format": "float", + "example": 13000.3 + }, + "volume": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "Calculated by multiplying the width, height, and length of the packed cargo.\n", + "format": "float", + "example": 12 + }, + "OuterPackaging_CAR": { + "title": "Outer Packaging", + "required": [ + "description" + ], + "type": "object", + "properties": { + "imoPackagingCode": { + "pattern": "^[A-Z0-9]{1,5}$", + "type": "string", + "description": "The code of the packaging as per IMO.\n\n**Condition:** only applicable to dangerous goods if specified in the IMO IMDG code amendment version 41-22. If not available, the package code as per UN recommendation 21 should be used.\n", + "example": "1A2" + }, + "dangerousGoods": { + "type": "array", + "description": "A list of `Dangerous Goods`\n", + "items": { + "$ref": "#/components/schemas/DangerousGoods_CAR" + } + } + }, + "description": "Object for outer packaging/overpack specification. Examples of overpacks are a number of packages stacked on to a pallet and secured by strapping or placed in a protective outer packaging such as a box or crate to form one unit for the convenience of handling and stowage during transport. It is an array of the attributes below.\n", + "allOf": [ + { + "$ref": "#/components/schemas/OuterPackaging_SHI" + } + ] + }, + "OuterPackaging_SHI": { + "title": "Outer Packaging", + "required": [ + "numberOfPackages" + ], + "type": "object", + "properties": { + "packageCode": { + "$ref": "#/components/schemas/packageCode" + }, + "numberOfPackages": { + "minimum": 1, + "type": "integer", + "description": "Specifies the number of outer packagings/overpacks associated with this `Cargo Item`.\n", + "format": "int32", + "example": 18 + }, + "description": { + "maxLength": 100, + "type": "string", + "description": "Description of the outer packaging/overpack.\n", + "example": "Drum, steel" + } + }, + "description": "Object for outer packaging/overpack specification. Examples of overpacks are a number of packages stacked on to a pallet and secured by strapping or placed in a protective outer packaging such as a box or crate to form one unit for the convenience of handling and stowage during transport. It is an array of the attributes below.\n" + }, + "packageCode": { + "pattern": "^[A-Z0-9]{2}$", + "type": "string", + "description": "A code identifying the outer packaging/overpack. `PackageCode` must follow the codes specified in [Recommendation N°21 - Revision 12 Annexes V and VI](https://unece.org/sites/default/files/2021-06/rec21_Rev12e_Annex-V-VI_2021.xls)\n\n**Condition:** only applicable to dangerous goods if the `IMO packaging code` is not available.\n", + "example": "5H" + }, + "DangerousGoods_CAR": { + "title": "Dangerous Goods", + "required": [ + "imoClass", + "properShippingName" + ], + "type": "object", + "description": "Specification for `Dangerous Goods`. It is mandatory to either provide the `unNumber` or the `naNumber`. Dangerous Goods is based on **IMDG Amendment Version 41-22**.\n", + "allOf": [ + { + "$ref": "#/components/schemas/DangerousGoods" + } + ] + }, + "DangerousGoods": { + "title": "Dangerous Goods", + "type": "object", + "properties": { + "codedVariantList": { + "$ref": "#/components/schemas/codedVariantList" + }, + "properShippingName": { + "$ref": "#/components/schemas/properShippingName" + }, + "technicalName": { + "$ref": "#/components/schemas/technicalName" + }, + "imoClass": { + "$ref": "#/components/schemas/imoClass" + }, + "subsidiaryRisk1": { + "$ref": "#/components/schemas/subsidiaryRisk" + }, + "subsidiaryRisk2": { + "$ref": "#/components/schemas/subsidiaryRisk" + }, + "isMarinePollutant": { + "$ref": "#/components/schemas/isMarinePollutant" + }, + "packingGroup": { + "$ref": "#/components/schemas/packingGroup" + }, + "isLimitedQuantity": { + "$ref": "#/components/schemas/isLimitedQuantity" + }, + "isExceptedQuantity": { + "$ref": "#/components/schemas/isExceptedQuantity" + }, + "isSalvagePackings": { + "$ref": "#/components/schemas/isSalvagePackings" + }, + "isEmptyUncleanedResidue": { + "$ref": "#/components/schemas/isEmptyUncleanedResidue" + }, + "isWaste": { + "$ref": "#/components/schemas/isWaste" + }, + "isHot": { + "$ref": "#/components/schemas/isHot" + }, + "isCompetentAuthorityApprovalProvided": { + "$ref": "#/components/schemas/isCompetentAuthorityApprovalProvided" + }, + "competentAuthorityApproval": { + "$ref": "#/components/schemas/competentAuthorityApproval" + }, + "segregationGroups": { + "type": "array", + "description": "List of the segregation groups applicable to specific hazardous goods according to the IMO IMDG Code.\n\n**Condition:** only applicable to specific hazardous goods.\n", + "items": { + "maxLength": 2, + "type": "string", + "description": "Grouping of Dangerous Goods having certain similar chemical properties. Possible values are:\n\n- `1` (Acids)\n- `2` (Ammonium Compounds)\n- `3` (Bromates)\n- `4` (Chlorates)\n- `5` (Chlorites)\n- `6` (Cyanides)\n- `7` (Heavy metals and their salts)\n- `8` (Hypochlorites)\n- `9` (Lead and its compounds)\n- `10` (Liquid halogenated hydrocarbons)\n- `11` (Mercury and mercury compounds)\n- `12` (Nitrites and their mixtures)\n- `13` (Perchlorates)\n- `14` (Permanganates)\n- `15` (Powdered metals)\n- `16` (Peroxides),\n- `17` (Azides)\n- `18` (Alkalis)\n", + "example": "12" + } + }, + "innerPackagings": { + "minLength": 1, + "type": "array", + "description": "A list of `Inner Packings` contained inside this `outer packaging/overpack`.\n", + "items": { + "$ref": "#/components/schemas/InnerPackaging" + } + }, + "emergencyContactDetails": { + "$ref": "#/components/schemas/EmergencyContactDetails" + }, + "EMSNumber": { + "$ref": "#/components/schemas/EMSNumber" + }, + "endOfHoldingTime": { + "$ref": "#/components/schemas/endOfHoldingTime" + }, + "fumigationDateTime": { + "$ref": "#/components/schemas/fumigationDateTime" + }, + "isReportableQuantity": { + "$ref": "#/components/schemas/isReportableQuantity" + }, + "inhalationZone": { + "$ref": "#/components/schemas/inhalationZone" + }, + "grossWeight": { + "$ref": "#/components/schemas/DangerousGoods_grossWeight" + }, + "netWeight": { + "$ref": "#/components/schemas/DangerousGoods_netWeight" + }, + "netExplosiveContent": { + "$ref": "#/components/schemas/DangerousGoods_netExplosiveContent" + }, + "volume": { + "$ref": "#/components/schemas/DangerousGoods_volume" + }, + "limits": { + "$ref": "#/components/schemas/Limits" + } + }, + "oneOf": [ + { + "title": "UN Number", + "required": [ + "unNumber" + ], + "type": "object", + "properties": { + "unNumber": { + "$ref": "#/components/schemas/unNumber" + } + } + }, + { + "title": "NA Number", + "required": [ + "naNumber" + ], + "type": "object", + "properties": { + "naNumber": { + "$ref": "#/components/schemas/naNumber" + } + } + } + ] + }, + "codedVariantList": { + "pattern": "^[0-3][0-9A-Z]{3}$", + "type": "string", + "description": "Four-character code supplied by Exis Technologies that assists to remove ambiguities when identifying a variant within a single UN number or NA number that may occur when two companies exchange DG information.\n\nCharacter | Valid Characters | Description\n:--------:|------------------|------------\n1| 0, 1, 2, 3|The packing group. Code 0 indicates there is no packing group\n2|0 to 9 and A to Z|A sequence letter for the PSN, or 0 if there were no alternative PSNs\n3 and 4|0 to 9 and A to Z|Two sequence letters for other information, for the cases where the variant is required because of different in subrisks, packing instruction etc.\n", + "example": "2200" + }, + "properShippingName": { + "maxLength": 250, + "type": "string", + "description": "The proper shipping name for goods under IMDG Code, or the product name for goods under IBC Code and IGC Code, or the bulk cargo shipping name for goods under IMSBC Code, or the name of oil for goods under Annex I to the MARPOL Convention.\n", + "example": "Chromium Trioxide, anhydrous" + }, + "technicalName": { + "maxLength": 250, + "type": "string", + "description": "The recognized chemical or biological name or other name currently used for the referenced dangerous goods as described in chapter 3.1.2.8 of the IMDG Code.\n" + }, + "imoClass": { + "maxLength": 4, + "type": "string", + "description": "The hazard class code of the referenced dangerous goods according to the specified regulation. Examples of possible values are:\n \n - `1.1A` (Substances and articles which have a mass explosion hazard)\n - `1.6N` (Extremely insensitive articles which do not have a mass explosion hazard)\n - `2.1` (Flammable gases)\n - `8` (Corrosive substances)\n\nThe value must comply with one of the values in the [DG IMO Class value table](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/domain/dcsa/reference-data/imoclasses-v3.1.0.csv)\n", + "example": "1.4S" + }, + "subsidiaryRisk": { + "pattern": "^[0-9](\\.[0-9])?$", + "type": "string", + "description": "Any risk in addition to the class of the referenced dangerous goods according to the IMO IMDG Code.\n", + "example": "1.2" + }, + "isMarinePollutant": { + "type": "boolean", + "description": "Indicates if the goods belong to the classification of Marine Pollutant.\n", + "example": false + }, + "packingGroup": { + "maximum": 3, + "minimum": 1, + "type": "integer", + "description": "The packing group according to the UN Recommendations on the Transport of Dangerous Goods and IMO IMDG Code.\n", + "format": "int32", + "example": 3 + }, + "isLimitedQuantity": { + "type": "boolean", + "description": "Indicates if the dangerous goods can be transported as limited quantity in accordance with Chapter 3.4 of the IMO IMDG Code.\n", + "example": false + }, + "isExceptedQuantity": { + "type": "boolean", + "description": "Indicates if the dangerous goods can be transported as excepted quantity in accordance with Chapter 3.5 of the IMO IMDG Code.\n", + "example": false + }, + "isSalvagePackings": { + "type": "boolean", + "description": "Indicates if the cargo has special packaging for the transport, recovery or disposal of damaged, defective, leaking or nonconforming hazardous materials packages, or hazardous materials that have spilled or leaked.\n", + "example": false + }, + "isEmptyUncleanedResidue": { + "type": "boolean", + "description": "Indicates if the cargo is residue.\n", + "example": false + }, + "isWaste": { + "type": "boolean", + "description": "Indicates if waste is being shipped\n", + "example": false + }, + "isHot": { + "type": "boolean", + "description": "Indicates if high temperature cargo is shipped.\n", + "example": false + }, + "isCompetentAuthorityApprovalProvided": { + "type": "boolean", + "description": "Indicates if the cargo require approval from authorities\n", + "example": false + }, + "competentAuthorityApproval": { + "maxLength": 70, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Name and reference number of the competent authority providing the approval.\n", + "example": "{Name and reference...}" + }, + "InnerPackaging": { + "title": "Inner Packaging", + "required": [ + "description", + "material", + "quantity" + ], + "type": "object", + "properties": { + "quantity": { + "type": "integer", + "description": "Count of `Inner Packagings` of the referenced `Dangerous Goods`.\n", + "format": "int32", + "example": 20 + }, + "material": { + "maxLength": 100, + "type": "string", + "description": "The `material` used for the `Inner Packaging` of the referenced `Dangerous Goods`.\n", + "example": "Plastic" + }, + "description": { + "maxLength": 100, + "type": "string", + "description": "Description of the packaging.\n", + "example": "Wowen plastic water resistant Bag" + } + }, + "description": "Object for inner packaging specification\n" + }, + "EmergencyContactDetails": { + "title": "Emergency Contact Details", + "required": [ + "contact", + "phone" + ], + "type": "object", + "properties": { + "contact": { + "maxLength": 255, + "type": "string", + "description": "Name of the Contact person during an emergency.\n", + "example": "Henrik Larsen" + }, + "provider": { + "maxLength": 255, + "type": "string", + "description": "Name of the third party vendor providing emergency support\n", + "example": "GlobeTeam" + }, + "phone": { + "$ref": "#/components/schemas/contactPhone" + }, + "referenceNumber": { + "maxLength": 255, + "type": "string", + "description": "Contract reference for the emergency support provided by an external third party vendor.\n", + "example": "12234" + } + }, + "description": "24 hr emergency contact details\n" + }, + "EMSNumber": { + "maxLength": 7, + "type": "string", + "description": "The emergency schedule identified in the IMO EmS Guide – Emergency Response Procedures for Ships Carrying Dangerous Goods. Comprises 2 values; 1 for spillage and 1 for fire. Possible values spillage: S-A to S-Z. Possible values fire: F-A to F-Z.\n", + "example": "F-A S-Q" + }, + "endOfHoldingTime": { + "type": "string", + "description": "Date by when the refrigerated liquid needs to be delivered.\n", + "format": "date", + "example": "2021-09-03" + }, + "fumigationDateTime": { + "type": "string", + "description": "Date & time when the container was fumigated\n", + "format": "date-time", + "example": "2021-09-03T09:03:00-02:00" + }, + "isReportableQuantity": { + "type": "boolean", + "description": "Indicates if a container of hazardous material is at the reportable quantity level. If `TRUE`, a report to the relevant authority must be made in case of spill.\n", + "example": false + }, + "inhalationZone": { + "maxLength": 1, + "minLength": 1, + "type": "string", + "description": "The zone classification of the toxicity of the inhalant. Possible values are:\n- `A` (Hazard Zone A) can be asigned to specific gases and liquids\n- `B` (Hazard Zone B) can be asigned to specific gases and liquids\n- `C` (Hazard Zone C) can **only** be asigned to specific gases\n- `D` (Hazard Zone D) can **only** be asigned to specific gases\n", + "example": "A" + }, + "dgGrossWeight": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The grand total weight of the DG cargo and weight per UNNumber/NANumber including packaging items being carried, which can be expressed in imperial or metric terms, as provided by the shipper.\n", + "format": "float", + "example": 12000 + }, + "netWeight": { + "type": "number", + "description": "Total weight of the goods carried, excluding packaging.\n", + "format": "float", + "example": 2.4 + }, + "netWeightUnit": { + "type": "string", + "description": "Unit of measure used to describe the `netWeight`. Possible values are\n- KGM (Kilograms)\n- LBR (Pounds)\n", + "example": "KGM", + "enum": [ + "KGM", + "LBR" + ] + }, + "netExplosiveContent": { + "type": "number", + "description": "The total weight of the explosive substances, without the packaging’s, casings, etc.\n", + "format": "float", + "example": 2.4 + }, + "netExplosiveContentUnit": { + "type": "string", + "description": "Unit of measure used to describe the `netExplosiveWeight`. Possible values are\n- KGM (Kilograms)\n- GRM (Grams)\n", + "example": "KGM", + "enum": [ + "KGM", + "GRM" + ] + }, + "dgVolume": { + "type": "number", + "description": "The volume of the referenced dangerous goods.\n", + "format": "float", + "example": 2.4 + }, + "volumeUnitDG": { + "type": "string", + "description": "The unit of measure which can be expressed in either imperial or metric terms\n- FTQ (Cubic foot)\n- MTQ (Cubic meter)\n- LTR (Litre)\n", + "example": "MTQ", + "enum": [ + "MTQ", + "FTQ", + "LTR" + ] + }, + "Limits": { + "required": [ + "temperatureUnit" + ], + "type": "object", + "properties": { + "temperatureUnit": { + "type": "string", + "description": "The unit for **all attributes in the limits structure** in Celsius or Fahrenheit\n\n- CEL (Celsius)\n- FAH (Fahrenheit)\n", + "example": "CEL", + "enum": [ + "CEL", + "FAH" + ] + }, + "flashPoint": { + "$ref": "#/components/schemas/flashPoint" + }, + "transportControlTemperature": { + "$ref": "#/components/schemas/transportControlTemperature" + }, + "transportEmergencyTemperature": { + "$ref": "#/components/schemas/transportEmergencyTemperature" + }, + "SADT": { + "$ref": "#/components/schemas/sadt" + }, + "SAPT": { + "$ref": "#/components/schemas/sapt" + } + }, + "description": "Limits for the `Dangerous Goods`. The same `Temperature Unit` needs to apply to all attributes in this structure.\n" + }, + "flashPoint": { + "type": "number", + "description": "Lowest temperature at which a chemical can vaporize to form an ignitable mixture in air. Condition: only applicable to specific hazardous goods according to the IMO IMDG Code amendment version 41-22.\n", + "format": "float", + "example": 42 + }, + "transportControlTemperature": { + "type": "number", + "description": "Maximum temperature at which certain substance (such as organic peroxides and self-reactive and related substances) can be safely transported for a prolonged period.\n", + "format": "float", + "example": 24.1 + }, + "transportEmergencyTemperature": { + "type": "number", + "description": "Temperature at which emergency procedures shall be implemented\n", + "format": "float", + "example": 74.1 + }, + "sadt": { + "type": "number", + "description": "Lowest temperature in which self-accelerating decomposition may occur in a substance\n", + "format": "float", + "example": 54.1 + }, + "sapt": { + "type": "number", + "description": "Lowest temperature in which self-accelerating polymerization may occur in a substance\n", + "format": "float", + "example": 70 + }, + "UtilizedTransportEquipment_CAR": { + "title": "Utilized Transport Equipment", + "required": [ + "cargoGrossWeight", + "cargoGrossWeightUnit", + "equipment", + "isShipperOwned", + "seals" + ], + "type": "object", + "properties": { + "equipment": { + "$ref": "#/components/schemas/Equipment" + }, + "cargoGrossWeight": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The grand total weight of the cargo and weight per container including packaging, which can be expressed in imperial or metric terms, as provided by the shipper. Excludes the tare weight of the container.\n", + "format": "float", + "example": 12000 + }, + "cargoGrossWeightUnit": { + "$ref": "#/components/schemas/weightUnit" + }, + "cargoGrossVolume": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The grand total volume of the cargo per container, which can be expressed in imperial or metric terms, as provided by the shipper.\n", + "format": "float", + "example": 120 + }, + "cargoGrossVolumeUnit": { + "type": "string", + "description": "The unit of measure which can be expressed in either imperial or metric terms\n- FTQ (Cubic foot)\n- MTQ (Cubic meter)\n\n**Conditional:** if `cargoGrossVolume` is provided then `cargoGrossVolumeUnit` is required\n", + "example": "MTQ", + "enum": [ + "MTQ", + "FTQ" + ] + }, + "isShipperOwned": { + "$ref": "#/components/schemas/isShipperOwned" + }, + "isNonOperatingReefer": { + "$ref": "#/components/schemas/isNonOperatingReefer" + }, + "activeReeferSettings": { + "$ref": "#/components/schemas/ActiveReeferSettings" + }, + "seals": { + "minLength": 1, + "type": "array", + "description": "A list of `Seals`\n", + "items": { + "$ref": "#/components/schemas/Seal" + } + }, + "references": { + "type": "array", + "description": "A list of `References`\n", + "items": { + "$ref": "#/components/schemas/Reference" + } + }, + "customsReferences": { + "type": "array", + "description": "A list of `Customs references`\n", + "items": { + "$ref": "#/components/schemas/CustomsReference" + } + } + }, + "description": "Specifies the container (`equipment`), the total `weight`, total `volume`, possible `ActiveReeferSettings`, `seals` and `references`\n" + }, + "Equipment": { + "required": [ + "equipmentReference" + ], + "type": "object", + "properties": { + "equipmentReference": { + "$ref": "#/components/schemas/equipmentReference" + }, + "ISOEquipmentCode": { + "$ref": "#/components/schemas/ISOEquipmentCode" + }, + "tareWeight": { + "$ref": "#/components/schemas/tareWeight" + }, + "weightUnit": { + "type": "string", + "description": "The unit of measure which can be expressed in imperial or metric terms\n- KGM (Kilograms)\n- LBR (Pounds)\n\n**Conditional:** Mandatory to provide if `tareWeight` is provided\n", + "example": "KGM", + "enum": [ + "KGM", + "LBR" + ] + } + }, + "description": "Used for storing cargo in/on during transport. The equipment size/type is defined by the ISO 6346 code. The most common equipment size/type is 20'/40'/45' DRY Freight Container, but several different versions exist.\n" + }, + "ISOEquipmentCode": { + "maxLength": 4, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Unique code for the different equipment size and type used to transport commodities. The code can refer to either the ISO size type (e.g. 22G1) or the ISO type group (e.g. 22GP) following the [ISO 6346](https://en.wikipedia.org/wiki/ISO_6346) standard.\n", + "example": "22GP" + }, + "tareWeight": { + "minimum": 0, + "exclusiveMinimum": true, + "type": "number", + "description": "The weight of an empty container (gross container weight).\n", + "format": "float", + "example": 4800 + }, + "isShipperOwned": { + "type": "boolean", + "description": "Indicates whether the container is shipper owned (SOC).", + "example": true + }, + "isNonOperatingReefer": { + "type": "boolean", + "description": "If the equipment is a Reefer Container then setting this attribute will indicate that the container should be treated as a `DRY` container.\n\n**Condition:** Only applicable if `ISOEquipmentCode` shows a Reefer type.\n", + "example": false + }, + "ActiveReeferSettings": { + "title": "Active Reefer Settings", + "type": "object", + "properties": { + "temperatureSetpoint": { + "$ref": "#/components/schemas/temperatureSetpoint" + }, + "temperatureUnit": { + "type": "string", + "description": "The unit for temperature in Celsius or Fahrenheit\n\n- CEL (Celsius)\n- FAH (Fahrenheit)\n\n**Condition:** Mandatory to provide if `temperatureSetpoint` is provided\n", + "example": "CEL", + "enum": [ + "CEL", + "FAH" + ] + }, + "o2Setpoint": { + "$ref": "#/components/schemas/o2Setpoint" + }, + "co2Setpoint": { + "$ref": "#/components/schemas/co2Setpoint" + }, + "humiditySetpoint": { + "$ref": "#/components/schemas/humiditySetpoint" + }, + "airExchangeSetpoint": { + "$ref": "#/components/schemas/airExchangeSetpoint" + }, + "airExchangeUnit": { + "$ref": "#/components/schemas/airExchangeUnit" + }, + "isVentilationOpen": { + "$ref": "#/components/schemas/isVentilationOpen" + }, + "isDrainholesOpen": { + "$ref": "#/components/schemas/isDrainholesOpen" + }, + "isBulbMode": { + "$ref": "#/components/schemas/isBulbMode" + }, + "isColdTreatmentRequired": { + "$ref": "#/components/schemas/isColdTreatmentRequired" + }, + "isControlledAtmosphereRequired": { + "$ref": "#/components/schemas/isControlledAtmosphereRequired" + } + }, + "description": "The specifications for a Reefer equipment.\n\n**Condition:** Only applicable when`isNonOperatingReefer` is set to `false`\n" + }, + "temperatureSetpoint": { + "type": "number", + "description": "Target value of the temperature for the Reefer based on the cargo requirement.\n", + "format": "float", + "example": -15 + }, + "o2Setpoint": { + "maximum": 100, + "minimum": 0, + "type": "number", + "description": "The percentage of the controlled atmosphere O2 target value\n", + "format": "float", + "example": 75.3 + }, + "co2Setpoint": { + "maximum": 100, + "minimum": 0, + "type": "number", + "description": "The percentage of the controlled atmosphere CO2 target value\n", + "format": "float", + "example": 25 + }, + "humiditySetpoint": { + "maximum": 100, + "minimum": 0, + "type": "number", + "description": "The percentage of the controlled atmosphere humidity target value\n", + "format": "float", + "example": 95.6 + }, + "airExchangeSetpoint": { + "minimum": 0, + "type": "number", + "description": "Target value for the air exchange rate which is the rate at which outdoor air replaces indoor air within a Reefer container\n", + "format": "float", + "example": 15.4 + }, + "airExchangeUnit": { + "type": "string", + "description": "The unit for `airExchange` in metrics- or imperial- units per hour\n\n- MQH (Cubic metre per hour)\n- FQH (Cubic foot per hour)\n\n**NB:** This is a conditional field. If `airExchange` is specified then this field is required\n", + "example": "MQH", + "enum": [ + "MQH", + "FQH" + ] + }, + "isVentilationOpen": { + "type": "boolean", + "description": "If `true` the ventilation orifice is `Open` - if `false` the ventilation orifice is `closed`\n", + "example": true + }, + "isDrainholesOpen": { + "type": "boolean", + "description": "Is drainholes open on the container\n", + "example": true + }, + "isBulbMode": { + "type": "boolean", + "description": "Is special container setting for handling flower bulbs active\n", + "example": true + }, + "isColdTreatmentRequired": { + "type": "boolean", + "description": "Indicator whether cargo requires cold treatment prior to loading at origin or during transit, but prior arrival at POD\n", + "example": true + }, + "isControlledAtmosphereRequired": { + "type": "boolean", + "description": "Indicator of whether cargo requires Controlled Atmosphere.\n", + "example": true + }, + "Seal": { + "required": [ + "number" + ], + "type": "object", + "properties": { + "number": { + "$ref": "#/components/schemas/sealNumber" + }, + "source": { + "$ref": "#/components/schemas/sealSource" + }, + "type": { + "$ref": "#/components/schemas/sealType" + } + }, + "description": "Addresses the seal-related information associated with the shipment equipment. A seal is put on a shipment equipment once it is loaded. This `Seal` is meant to stay on until the shipment equipment reaches its final destination.\n" + }, + "sealNumber": { + "maxLength": 15, + "type": "string", + "description": "Identifies a seal affixed to the container." + }, + "sealSource": { + "maxLength": 5, + "type": "string", + "description": "The source of the seal, namely who has affixed the seal. This attribute links to the Seal Source ID defined in the Seal Source reference data entity. Possible values are:\n- CAR (Carrier)\n- SHI (Shipper)\n- PHY (Phytosanitary)\n- VET (Veterinary)\n- CUS (Customs)\n\n**Condition:** Conditional on type of commodity\n", + "example": "CUS" + }, + "sealType": { + "maxLength": 5, + "type": "string", + "description": "The type of seal. This attribute links to the Seal Type ID defined in the Seal Type reference data entity. Possible values are:\n- KLP (Keyless padlock)\n- BLT (Bolt)\n- WIR (Wire)\n", + "example": "WIR" + }, + "partyCodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/partyCodes_inner" + } + }, + "unNumber": { + "pattern": "^\\d{4}$", + "type": "string", + "description": "United Nations Dangerous Goods Identifier (UNDG) assigned by the UN Sub-Committee of Experts on the Transport of Dangerous Goods and shown in the IMO IMDG.\n", + "example": "1463" + }, + "naNumber": { + "pattern": "^\\d{4}$", + "type": "string", + "description": "Four-digit number that is assigned to dangerous, hazardous, and harmful substances by the United States Department of Transportation.\n", + "example": "9037" + }, + "JWKS_keys": { + "required": [ + "kty" + ], + "type": "object", + "properties": { + "kty": { + "type": "string", + "description": "The \"kty\" (key type) parameter identifies the cryptographic algorithm family used with the key, such as \"RSA\" or \"EC\". \"kty\" values should either be registered in the IANA \"JSON Web Key Types\" registry established by [JWA] or be a value that contains a Collision- Resistant Name. The \"kty\" value is a case-sensitive string. This member MUST be present in a JWK." + }, + "use": { + "type": "string", + "description": "The \"use\" (public key use) parameter identifies the intended use of\nthe public key. The \"use\" parameter is employed to indicate whether\na public key is used for encrypting data or verifying the signature\non data.\nvalues defined are:\n * sig (signature)\n * enc (encryption)\n", + "enum": [ + "sig", + "enc" + ] + }, + "key_ops": { + "type": "string", + "description": "The \"key_ops\" (key operations) parameter identifies the operation(s)\nfor which the key is intended to be used. The \"key_ops\" parameter is\nintended for use cases in which public, private, or symmetric keys\nmay be present.\n\nIts value is an array of key operation values. Values defined by\nthis specification are:\n\n* sign (compute digital signature or MAC)\n* verify (verify digital signature or MAC)\n* encrypt (encrypt content)\n* decrypt (decrypt content and validate decryption, if applicable)\n* wrapKey (encrypt key)\n* unwrapKey (decrypt key and validate decryption, if applicable)\n* deriveKey (derive key)\n* deriveBits (derive bits not to be used as a key)\n", + "enum": [ + "sign", + "verify", + "encrypt", + "decrypt", + "wrapKey", + "unwrapKey", + "deriveKey", + "deriveBits" + ] + }, + "alg": { + "type": "string", + "description": "The \"alg\" (algorithm) parameter identifies the algorithm intended for use with the key. The values used should either be registered in the IANA \"JSON Web Signature and Encryption Algorithms\" registry established by [JWA] or be a value that contains a Collision- Resistant Name. The \"alg\" value is a case-sensitive ASCII string. Use of this member is OPTIONAL." + }, + "kid": { + "type": "string", + "description": "The \"kid\" (key ID) parameter is used to match a specific key. This is used, for instance, to choose among a set of keys within a JWK Set during key rollover. The structure of the \"kid\" value is unspecified. When \"kid\" values are used within a JWK Set, different keys within the JWK Set SHOULD use distinct \"kid\" values. (One example in which different keys might use the same \"kid\" value is if they have different \"kty\" (key type) values but are considered to be equivalent alternatives by the application using them.) The \"kid\" value is a case-sensitive string. Use of this member is OPTIONAL. When used with JWS or JWE, the \"kid\" value is used to match a JWS or JWE \"kid\" Header Parameter value." + }, + "x5u": { + "type": "string", + "description": "The \"x5u\" (X.509 URL) parameter is a URI [RFC3986] that refers to a resource for an X.509 public key certificate or certificate chain [RFC5280]. The identified resource MUST provide a representation of the certificate or certificate chain that conforms to RFC 5280 [RFC5280] in PEM-encoded form, with each certificate delimited as specified in Section 6.1 of RFC 4945 [RFC4945]. The key in the first certificate MUST match the public key represented by other members of the JWK. The protocol used to acquire the resource MUST provide integrity protection; an HTTP GET request to retrieve the certificate MUST use TLS [RFC2818] [RFC5246]; the identity of the server MUST be validated, as per Section 6 of RFC 6125 [RFC6125]. Use of this member is OPTIONAL.", + "format": "url" + }, + "x5c": { + "type": "array", + "description": "The \"x5c\" (X.509 certificate chain) parameter contains a chain of one\nor more PKIX certificates [RFC5280]. The certificate chain is\nrepresented as a JSON array of certificate value strings. Each\nstring in the array is a base64-encoded (Section 4 of [RFC4648] --\nnot base64url-encoded) DER [ITU.X690.1994] PKIX certificate value.\nThe PKIX certificate containing the key value MUST be the first\ncertificate. This MAY be followed by additional certificates, with\neach subsequent certificate being the one used to certify the\nprevious one. The key in the first certificate MUST match the public\nkey represented by other members of the JWK. Use of this member is\nOPTIONAL.\n", + "items": { + "type": "string" + } + }, + "x5t": { + "type": "string", + "description": "The \"x5t\" (X.509 certificate SHA-1 thumbprint) parameter is a\nbase64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER\nencoding of an X.509 certificate [RFC5280]. Note that certificate\nthumbprints are also sometimes known as certificate fingerprints.\nThe key in the certificate MUST match the public key represented by\nother members of the JWK. Use of this member is OPTIONAL.\n" + } + }, + "description": "The value of the \"keys\" parameter is an array of JWK values. By default, the order of the JWK values within the array does not imply an order of preference among them, although applications of JWK Sets can choose to assign a meaning to the order for their purposes, if desired." + }, + "RsaPrivateKey_oth": { + "type": "object", + "properties": { + "r": { + "type": "string", + "description": "The \"r\" (prime factor) parameter within an \"oth\" array member\nrepresents the value of a subsequent prime factor. It is represented\nas a Base64urlUInt-encoded value.\n", + "format": "byte" + }, + "d": { + "type": "string", + "description": "The \"d\" (factor CRT exponent) parameter within an \"oth\" array member\nrepresents the CRT exponent of the corresponding prime factor. It is\nrepresented as a Base64urlUInt-encoded value.\n", + "format": "byte" + }, + "t": { + "type": "string", + "description": "The \"t\" (factor CRT coefficient) parameter within an \"oth\" array\nmember represents the CRT coefficient of the corresponding prime\nfactor. It is represented as a Base64urlUInt-encoded value.\n", + "format": "byte" + } + } + }, + "DangerousGoods_grossWeight": { + "required": [ + "unit", + "value" + ], + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/dgGrossWeight" + }, + "unit": { + "$ref": "#/components/schemas/weightUnit" + } + }, + "description": "Total weight of the goods carried, including packaging.\n" + }, + "DangerousGoods_netWeight": { + "required": [ + "unit", + "value" + ], + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/netWeight" + }, + "unit": { + "$ref": "#/components/schemas/netWeightUnit" + } + }, + "description": "Total weight of the goods carried, excluding packaging.\n" + }, + "DangerousGoods_netExplosiveContent": { + "required": [ + "unit", + "value" + ], + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/netExplosiveContent" + }, + "unit": { + "$ref": "#/components/schemas/netExplosiveContentUnit" + } + }, + "description": "The total weight of the explosive substances, without the packaging’s, casings, etc.\n" + }, + "DangerousGoods_volume": { + "type": "object", + "properties": { + "value": { + "$ref": "#/components/schemas/dgVolume" + }, + "unit": { + "$ref": "#/components/schemas/volumeUnitDG" + } + }, + "description": "The volume of the referenced dangerous goods.\n\n**Condition:** only applicable to liquids and gas.\n" + }, + "partyCodes_inner": { + "required": [ + "codeListProvider", + "partyCode" + ], + "type": "object", + "properties": { + "partyCode": { + "maxLength": 100, + "minLength": 1, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "Code to identify the party as provided by the `partyCodeListProvider` and `codeListName`\n", + "example": "529900T8BM49AURSDO55" + }, + "codeListProvider": { + "type": "string", + "description": "Describes the organisation that provides the party code.\n\n - `EPUI`:The party code is an EBL Platform User Identifier (that is, an identifier provided by a platform, used to transfer eBLs). EPIU should be combined with the `codeListName`, to identify the platform that issued the identifier.\n - `GLEIF`: The party code is issued by Global Legal Entity Identifier Foundation (GLEIF). See https://www.gleif.org/en. The `codeNameList` (if omitted) defaults to `LEI`.\n - `W3C`: The party code is issued by a standard created by World Wide Web Consortium (W3C). See https://www.w3.org/. The `codeNameList` (if omitted) defaults to `DID`.\n", + "example": "EPIU", + "enum": [ + "GLEIF", + "W3C", + "EPUI" + ] + }, + "codeListName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The name of the code list / code generation mechanism / code authority for the party code.\n\nFor `EPUI`:\n * `Wave`: An identifier provided by Wave BL.\n * `CargoX`: An identifier provided by CargoX\n * `EdoxOnline`: An identifier provided by EdoxOnline\n * `IQAX`: An identifier provided by IQAX\n * `EssDOCS`: An identifier provided by essDOCS\n * `Bolero`: An identifier provided by Bolero\n * `TradeGO`: An identifierprovided by TradeGo\n * `Secro`: An identifier provided by Secro\n * `GSBN`: An identifier provided by GSBN\n * `WiseTech`: An identifier provided by WiseTech\n\nFor `W3C`:\n * `DID`: The party code is a Decentralized Identifier (see https://www.w3.org/TR/did-core/).\n\nFor `GLEIF`:\n * `LEI`: The party code is a Legal Entity Identifier (LEI) as issued by Global Legal Entity Identifier Foundation (GLEIF). See https://www.gleif.org/en\n", + "example": "Bolero" + } + } + } + }, + "parameters": { + "envelopeReference": { + "name": "envelopeReference", + "in": "path", + "description": "The receiving platform-provided unique identifier for the given eBL envelope.\n", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/EnvelopeReference" + }, + "example": "4TkP5nvgTly0MwFrDxfIkR2rvOjkUIgzibBoKABU" + }, + "documentChecksum": { + "name": "documentChecksum", + "in": "path", + "description": "The checksum of the document computed using SHA-256 hash algorithm according to [RFC 6234](https://datatracker.ietf.org/doc/html/rfc6234).\n", + "required": true, + "style": "simple", + "explode": false, + "schema": { + "$ref": "#/components/schemas/DocumentChecksum" + }, + "example": "76a7d14c83d7268d643ae7345c448de60701f955d264a743e6928a0b8268b24f" + }, + "Api-Version-Major": { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain the **MAJOR** version number. The API-Version header **MUST** be aligned with the URI version.\n", + "required": false, + "schema": { + "type": "string", + "example": "1" + } + } + }, + "headers": { + "API-Version": { + "description": "SemVer used to indicate the version of the contract (API version) returned.\n", + "schema": { + "type": "string", + "example": "1.0.0" + } + } + } + } +} diff --git a/pom.xml b/pom.xml index cc7ce8fb..bbf53109 100644 --- a/pom.xml +++ b/pom.xml @@ -11,6 +11,7 @@ core booking ebl + pint ebl-issuance ebl-surrender ovs diff --git a/sandbox/pom.xml b/sandbox/pom.xml index b0cec891..767d8598 100644 --- a/sandbox/pom.xml +++ b/sandbox/pom.xml @@ -29,6 +29,11 @@ ebl 0.0.1-SNAPSHOT + + org.dcsa.conformance + pint + 0.0.1-SNAPSHOT + org.dcsa.conformance ebl-issuance diff --git a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java index f63259ce..d6d31450 100644 --- a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java +++ b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java @@ -26,6 +26,7 @@ import org.dcsa.conformance.sandbox.state.ConformancePersistenceProvider; import org.dcsa.conformance.standards.booking.BookingComponentFactory; import org.dcsa.conformance.standards.ebl.EblComponentFactory; +import org.dcsa.conformance.standards.eblinterop.PintComponentFactory; import org.dcsa.conformance.standards.eblissuance.EblIssuanceComponentFactory; import org.dcsa.conformance.standards.eblsurrender.EblSurrenderComponentFactory; import org.dcsa.conformance.standards.ovs.OvsComponentFactory; @@ -798,6 +799,9 @@ private static AbstractComponentFactory _createComponentFactory( if (EblComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { return new EblComponentFactory(standardConfiguration.getVersion()); } + if (PintComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { + return new PintComponentFactory(standardConfiguration.getVersion()); + } if (EblIssuanceComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { return new EblIssuanceComponentFactory(standardConfiguration.getVersion()); } diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java index eaa79e2c..14e66cad 100644 --- a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java @@ -27,6 +27,7 @@ import org.dcsa.conformance.sandbox.state.DynamoDbSortedPartitionsNonLockingMap; import org.dcsa.conformance.standards.booking.BookingComponentFactory; import org.dcsa.conformance.standards.ebl.EblComponentFactory; +import org.dcsa.conformance.standards.eblinterop.PintComponentFactory; import org.dcsa.conformance.standards.eblissuance.EblIssuanceComponentFactory; import org.dcsa.conformance.standards.eblsurrender.EblSurrenderComponentFactory; import org.dcsa.conformance.standards.ovs.OvsComponentFactory; @@ -148,6 +149,7 @@ public ConformanceApplication(ConformanceConfiguration conformanceConfiguration) BookingComponentFactory.STANDARD_VERSIONS.stream() .map(BookingComponentFactory::new), EblComponentFactory.STANDARD_VERSIONS.stream().map(EblComponentFactory::new), + PintComponentFactory.STANDARD_VERSIONS.stream().map(PintComponentFactory::new), EblIssuanceComponentFactory.STANDARD_VERSIONS.stream() .map(EblIssuanceComponentFactory::new), EblSurrenderComponentFactory.STANDARD_VERSIONS.stream() From f07bc4ad1d51e206a96f1ad7a81131f090a18676 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 12 Feb 2024 17:33:38 +0100 Subject: [PATCH 02/14] DT-903 Tomcat no longer appending the wrong charset when left unspecified for application/json --- .../springboot/ConformanceTomcatConfig.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceTomcatConfig.java diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceTomcatConfig.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceTomcatConfig.java new file mode 100644 index 00000000..c11277f7 --- /dev/null +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceTomcatConfig.java @@ -0,0 +1,18 @@ +package org.dcsa.conformance.springboot; + +import org.apache.catalina.connector.Connector; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ConformanceTomcatConfig { + + @Bean + public WebServerFactoryCustomizer servletContainerCustomizer() { + return factory -> + factory.addConnectorCustomizers( + (Connector connector) -> connector.setEnforceEncodingInGetWriter(false)); + } +} From 955bbf76ff7966bf480feb7c0767562d3d229137 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:05:22 +0100 Subject: [PATCH 03/14] DT-903 Explicitly writing the Tomcat servlet output as UTF-8 --- .../springboot/ConformanceApplication.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java index 14e66cad..a500c1ca 100644 --- a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java @@ -3,8 +3,10 @@ import com.fasterxml.jackson.databind.JsonNode; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.PrintWriter; + +import java.io.OutputStreamWriter; import java.net.URI; +import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -290,9 +292,10 @@ private static void _writeResponse( (headerName, headerValues) -> headerValues.forEach( headerValue -> servletResponse.setHeader(headerName, headerValue))); - PrintWriter writer = servletResponse.getWriter(); - writer.write(stringBody); - writer.flush(); + OutputStreamWriter outputStreamWriter = + new OutputStreamWriter(servletResponse.getOutputStream(), StandardCharsets.UTF_8); + outputStreamWriter.write(stringBody); + outputStreamWriter.flush(); } @SneakyThrows From 2ccd601c6c7d8231e41b02d29b8b8cd52c0bfb9b Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:09:38 +0100 Subject: [PATCH 04/14] DT-902 HTML-escaped report text, fixed manual PINT sandbox creation --- .../conformance/core/report/ConformanceReport.java | 12 +++++++----- .../manual-receivingplatform-tested-party.json | 2 +- .../dcsa/conformance/sandbox/ConformanceSandbox.java | 6 +++--- .../conformance/sandbox/ConformanceWebuiHandler.java | 7 +++++++ .../springboot/ConformanceApplication.java | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/core/src/main/java/org/dcsa/conformance/core/report/ConformanceReport.java b/core/src/main/java/org/dcsa/conformance/core/report/ConformanceReport.java index eac0ceed..619b31d6 100644 --- a/core/src/main/java/org/dcsa/conformance/core/report/ConformanceReport.java +++ b/core/src/main/java/org/dcsa/conformance/core/report/ConformanceReport.java @@ -10,6 +10,7 @@ import java.util.stream.Collectors; import lombok.Getter; import lombok.SneakyThrows; +import org.apache.commons.text.StringEscapeUtils; import org.dcsa.conformance.core.check.ConformanceCheck; import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; @@ -147,7 +148,7 @@ private static String scenarioAsHtmlBlock(ConformanceReport report, int level, b level * 2, printable ? " open" : "", getConformanceIcon(report.conformanceStatus), - report.title, + StringEscapeUtils.escapeHtml4(report.title), getErrors(report), report.subReports.stream() .map(subReport -> renderReport(subReport, level + 1, printable)) @@ -160,7 +161,7 @@ private static String scenarioDetailsAsHtmlBlock(ConformanceReport report, int l .formatted( level * 2, getConformanceIcon(report.conformanceStatus), - report.title.trim(), + StringEscapeUtils.escapeHtml4(report.title.trim()), getConformanceLabel(report.conformanceStatus)); } return "
%n%s %s%n
%s
%n%s%n
%n" @@ -168,7 +169,7 @@ private static String scenarioDetailsAsHtmlBlock(ConformanceReport report, int l level * 2, printable ? " open" : "", getConformanceIcon(report.conformanceStatus), - report.title, + StringEscapeUtils.escapeHtml4(report.title), getErrors(report), report.subReports.stream() .map(subReport -> renderReport(subReport, level + 1, printable)) @@ -179,7 +180,7 @@ private static String scenarioListAsHtmlBlock(ConformanceReport report, int leve return "
%n

%s

%n
%s
%n
%n%s%n" .formatted( level * 2, - report.title, + StringEscapeUtils.escapeHtml4(report.title), getErrors(report), report.subReports.stream() .map(subReport -> renderReport(subReport, level + 1, printable)) @@ -190,7 +191,7 @@ private static String asHtmlBlock(ConformanceReport report, int level, boolean p return "
%n

%s

%n
%s %s %s
%n
%s
%n
%n%s%n" .formatted( level * 2, - report.title, + StringEscapeUtils.escapeHtml4(report.title), getConformanceIcon(report.conformanceStatus), getConformanceLabel(report.conformanceStatus), getExchangesDetails(report), @@ -240,6 +241,7 @@ private static String getExchangesDetails(ConformanceReport report) { private static String getErrors(ConformanceReport report) { return report.errorMessages.stream() + .map(StringEscapeUtils::escapeHtml4) .map("\n
%s
"::formatted) .collect(Collectors.joining()); } diff --git a/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json b/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json index 8c9418ea..98d060dd 100644 --- a/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json +++ b/pint/src/main/resources/standards/pint/sandboxes/manual-receivingplatform-tested-party.json @@ -10,7 +10,7 @@ }, "parties" : [ { "inManualMode": true, - "name" : "ReceivingPlatform", + "name" : "ReceivingPlatform1", "role" : "ReceivingPlatform", "orchestratorUrl" : "http://localhost:8080/conformance/sandbox/SANDBOX_ID_PREFIX-manual-platform-testing-counterparts" } ], diff --git a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java index d6d31450..78a28b2f 100644 --- a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java +++ b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java @@ -799,9 +799,6 @@ private static AbstractComponentFactory _createComponentFactory( if (EblComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { return new EblComponentFactory(standardConfiguration.getVersion()); } - if (PintComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { - return new PintComponentFactory(standardConfiguration.getVersion()); - } if (EblIssuanceComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { return new EblIssuanceComponentFactory(standardConfiguration.getVersion()); } @@ -811,6 +808,9 @@ private static AbstractComponentFactory _createComponentFactory( if (OvsComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { return new OvsComponentFactory(standardConfiguration.getVersion()); } + if (PintComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { + return new PintComponentFactory(standardConfiguration.getVersion()); + } if (TntComponentFactory.STANDARD_NAME.equals(standardConfiguration.getName())) { return new TntComponentFactory(standardConfiguration.getVersion()); } diff --git a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceWebuiHandler.java b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceWebuiHandler.java index a2a3f41f..d3c3c8d8 100644 --- a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceWebuiHandler.java +++ b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceWebuiHandler.java @@ -16,6 +16,7 @@ import org.dcsa.conformance.sandbox.state.ConformancePersistenceProvider; import org.dcsa.conformance.standards.booking.BookingComponentFactory; import org.dcsa.conformance.standards.ebl.EblComponentFactory; +import org.dcsa.conformance.standards.eblinterop.PintComponentFactory; import org.dcsa.conformance.standards.eblissuance.EblIssuanceComponentFactory; import org.dcsa.conformance.standards.eblsurrender.EblSurrenderComponentFactory; import org.dcsa.conformance.standards.ovs.OvsComponentFactory; @@ -66,6 +67,12 @@ public class ConformanceWebuiHandler { OvsComponentFactory.STANDARD_VERSIONS.stream() .collect( Collectors.toMap(Function.identity(), OvsComponentFactory::new)))), + Map.entry( + PintComponentFactory.STANDARD_NAME, + new TreeMap<>( + PintComponentFactory.STANDARD_VERSIONS.stream() + .collect( + Collectors.toMap(Function.identity(), PintComponentFactory::new)))), Map.entry( TntComponentFactory.STANDARD_NAME, new TreeMap<>( diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java index a500c1ca..d85a5615 100644 --- a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java @@ -151,12 +151,12 @@ public ConformanceApplication(ConformanceConfiguration conformanceConfiguration) BookingComponentFactory.STANDARD_VERSIONS.stream() .map(BookingComponentFactory::new), EblComponentFactory.STANDARD_VERSIONS.stream().map(EblComponentFactory::new), - PintComponentFactory.STANDARD_VERSIONS.stream().map(PintComponentFactory::new), EblIssuanceComponentFactory.STANDARD_VERSIONS.stream() .map(EblIssuanceComponentFactory::new), EblSurrenderComponentFactory.STANDARD_VERSIONS.stream() .map(EblSurrenderComponentFactory::new), OvsComponentFactory.STANDARD_VERSIONS.stream().map(OvsComponentFactory::new), + PintComponentFactory.STANDARD_VERSIONS.stream().map(PintComponentFactory::new), TntComponentFactory.STANDARD_VERSIONS.stream().map(TntComponentFactory::new)) .flatMap(Function.identity()); componentFactories.forEach( From 7cd1f2f7581ef59ab09fec0d392ff46da89de40e Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 19 Feb 2024 12:21:06 +0100 Subject: [PATCH 05/14] OVS 3.0.0-Beta-2 schema and response --- .../standards/ovs/OvsComponentFactory.java | 5 +- .../standards/ovs/party/OvsPublisher.java | 4 +- ...sponse.json => ovs-300beta1-response.json} | 0 .../ovs/messages/ovs-300beta2-response.json | 41 + ...isher.json => ovs-300beta1-publisher.json} | 0 .../ovs/schemas/ovs-300beta2-publisher.json | 831 ++++++++++++++++++ 6 files changed, 878 insertions(+), 3 deletions(-) rename ovs/src/main/resources/standards/ovs/messages/{ovs-v30-response.json => ovs-300beta1-response.json} (100%) create mode 100644 ovs/src/main/resources/standards/ovs/messages/ovs-300beta2-response.json rename ovs/src/main/resources/standards/ovs/schemas/{ovs-v30-publisher.json => ovs-300beta1-publisher.json} (100%) create mode 100644 ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java index c625ce15..77f30b13 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java @@ -20,7 +20,7 @@ public class OvsComponentFactory extends AbstractComponentFactory { public static final String STANDARD_NAME = "OVS"; - public static final List STANDARD_VERSIONS = List.of("3.0.0-Beta1"); + public static final List STANDARD_VERSIONS = List.of("3.0.0-Beta1", "3.0.0-Beta2"); private static final String PUBLISHER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); private static final String SUBSCRIBER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); @@ -116,7 +116,8 @@ public JsonSchemaValidator getMessageSchemaValidator(String apiProviderRole, boo String schemaFilePath = "/standards/ovs/schemas/ovs-%s-%s.json" .formatted( - standardVersion.startsWith("2") ? "v22" : "v30", apiProviderRole.toLowerCase()); + standardVersion.toLowerCase().replaceAll("[.-]", ""), + apiProviderRole.toLowerCase()); String schemaName = OvsRole.isPublisher(apiProviderRole) ? (forRequest ? null : "serviceSchedules") : null; return JsonSchemaValidator.getInstance(schemaFilePath, schemaName); diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java index 2f6f7d14..e30d81de 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java @@ -55,7 +55,9 @@ public ConformanceResponse handleRequest(ConformanceRequest request) { JsonNode jsonResponseBody = JsonToolkit.templateFileToJsonNode( - "/standards/ovs/messages/ovs-v30-response.json", Map.ofEntries()); + "/standards/ovs/messages/ovs-%s-response.json" + .formatted(apiVersion.toLowerCase().replaceAll("[.-]", "")), + Map.ofEntries()); return request.createResponse( 200, diff --git a/ovs/src/main/resources/standards/ovs/messages/ovs-v30-response.json b/ovs/src/main/resources/standards/ovs/messages/ovs-300beta1-response.json similarity index 100% rename from ovs/src/main/resources/standards/ovs/messages/ovs-v30-response.json rename to ovs/src/main/resources/standards/ovs/messages/ovs-300beta1-response.json diff --git a/ovs/src/main/resources/standards/ovs/messages/ovs-300beta2-response.json b/ovs/src/main/resources/standards/ovs/messages/ovs-300beta2-response.json new file mode 100644 index 00000000..e401d972 --- /dev/null +++ b/ovs/src/main/resources/standards/ovs/messages/ovs-300beta2-response.json @@ -0,0 +1,41 @@ +[ + { + "carrierServiceName": "Great Lion Service", + "carrierServiceCode": "FE1", + "universalServiceReference": "SR12345A", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "HLC", + "vesselIMONumber": "9321483", + "vesselName": "King of the Seas", + "vesselCallSign": "NCVV", + "isDummyVessel": true, + "transportCalls": [ + { + "portVisitReference": "NLAMS1234589", + "transportCallReference": "SR11111X-9321483-2107W-NLAMS-ACT-1-1", + "carrierImportVoyageNumber": "2103N", + "carrierExportVoyageNumber": "2103S", + "universalImportVoyageReference": "2103N", + "universalExportVoyageReference": "2103N", + "location": { + "locationName": "Port of Amsterdam", + "locationType": "UNLO", + "UNLocationCode": "NLAMS" + }, + "statusCode": "OMIT", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "PLN", + "eventDateTime": "2025-01-14T09:21:00+01:00", + "delayReasonCode": "WEA", + "changeRemark": "Bad weather" + } + ] + } + ] + } + ] + } +] diff --git a/ovs/src/main/resources/standards/ovs/schemas/ovs-v30-publisher.json b/ovs/src/main/resources/standards/ovs/schemas/ovs-300beta1-publisher.json similarity index 100% rename from ovs/src/main/resources/standards/ovs/schemas/ovs-v30-publisher.json rename to ovs/src/main/resources/standards/ovs/schemas/ovs-300beta1-publisher.json diff --git a/ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json b/ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json new file mode 100644 index 00000000..c85900eb --- /dev/null +++ b/ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json @@ -0,0 +1,831 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Operational Vessel Schedules", + "description": "API specification issued by DCSA.org\r\n\r\nThis API supports OVS (Operational Vessel Schedules)\r\n\r\nThe Interface Standards for OVS and other documentation can be found on the [DCSA Website](https://dcsa.org/standards/operational-vessel-schedules/).\r\n\r\nFor explanation to specific values or objects please refer to the [Information Model](https://dcsa.org/wp-content/uploads/2024/01/DCSA-Information-Model-2023.Q4.pdf).\r\n\r\n### Stats API\r\nThe Stats API offers crucial statistical information for both API providers and consumers to enhance their services and helps DCSA to understand and scale the ecosystem. We expect you to invoke the Stats API for every request made to the Operational Vessel Schedules API. Further details can be found [here](https://developer.dcsa.org/#/http/guides/api-guides/stats-api)\r\n\r\nFor a changelog please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/ovs/v3#v300B2). Please [create a GitHub issue](https://github.com/dcsaorg/DCSA-OpenAPI/issues/new)", + "contact": { + "name": "Digital Container Shipping Association (DCSA)", + "url": "https://dcsa.org", + "email": "info@dcsa.org" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "3.0.0-Beta-2" + }, + "servers": [ + { + "url": "http://localhost:3000" + } + ], + "tags": [ + { + "name": "Operational Vessel Schedules", + "description": " " + } + ], + "paths": { + "/v3/service-schedules": { + "get": { + "tags": [ + "Operational Vessel Schedules" + ], + "summary": "Get a list of Schedules", + "description": "Get a list of service schedules. The result is `Vessel-Centric` - this means that the `Vessel` is in the top of the hierarchy of the response structure. A service is a heirarchical structure with the following elements:\r\n- One or more `Services` which can contain one or more `Vessels`\r\n- A `Vessel` which can call multiple `Ports` (`TransportCalls`).\r\n- A `Port` (`TransportCall`) can contain one or more `TimeStamps`.\r\n\r\nThe number of service schedules in the list can be narrowed down by providing filter parameters. The resulting payload will always include **entire voyage(s) being matched**. This means that even though a filter only matches a single `Port` in a `Voyage` or a single `Timestamp` within a `Port` in a `Voyage` - **the entire Voyage matched** is returned. If the `carrierImportVoyageNumber` of the `Port` differs from the `carrierExportVoyageNumber` of the `Port` then the **entire Voyage** for both these Voyage numbers are included.\r\n\r\nAn example of this is when `&UNLocationCode=DEHAM` is used as a filter parameter. In this case **entire Voyages** would be listed where `DEHAM` is a `Port`.\r\n\r\nBe aware that it is possible to specify filters that are mutially exclusive resulting in an empty response list. An example of this could be when both using `vesselIMONumber` and `vesselName` filters at the same time:\r\n\r\n&vesselIMONumber=9321483&vesselName=King of the Seas\r\n\r\nIf no `Vessel` exists where `vesselIMONumber` is **9321483** and `vesselName` is **King of the Seas** then the result will be an empty list\r\n\r\nIf no `startDate` filter is provided then **3 months** prior to the request data is used. If no `endDate` filters is provided then **6 months** after the request date is used.", + "operationId": "get-v3-service-schedules", + "parameters": [ + { + "name": "carrierServiceName", + "in": "query", + "description": "The carrier service name to filter by. The result will only return schedules including the service name.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 50, + "type": "string", + "example": "Great Lion Service" + } + }, + { + "name": "carrierServiceCode", + "in": "query", + "description": "The carrier specific service code to filter by. The result will only return schedules including the service code.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 11, + "type": "string", + "example": "FE1" + } + }, + { + "name": "universalServiceReference", + "in": "query", + "description": "The **U**niversal **S**ervice **R**eference (`USR`) as defined by **DCSA** to filter by. The service code must match the regular expression pattern: `SR\\d{5}[A-Z]`. The letters `SR` followed by `5 digits`, followed by a checksum-character as a capital letter from `A to Z`. The result will only return schedules including the service reference", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 8, + "pattern": "^SR\\d{5}[A-Z]$", + "type": "string", + "example": "SR12345A" + } + }, + { + "name": "vesselIMONumber", + "in": "query", + "description": "The identifier of a vessel. The result will only return schedules including the vessel with the specified IMO number. It is not a requirement for dummy vessels to have an `IMO Number`. In this case filtering by `vesselName` should be used.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 7, + "pattern": "^\\d{7}$", + "type": "string", + "example": "9321483" + } + }, + { + "name": "vesselName", + "in": "query", + "description": "The name of a vessel. The result will only return schedules including the vessel with the specified name. Be aware that the `vesselName` is not unique and might match multiple vessels. If possible, filtering by `IMO Number` is preferred. In case of dummy vessels an `IMO Number` might not exist in which case this filter is to be used.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 35, + "type": "string", + "example": "King of the Seas" + } + }, + { + "name": "carrierVoyageNumber", + "in": "query", + "description": "The carrier specific identifier of a `Voyage` - can be both **importVoyageNumber** and **exportVoyageNumber**. The result will only return schedules including the `Ports` where `carrierVoyageNumber` is either `carrierImportVoyageNumber` or `carrierExportVoyageNumber`", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 50, + "type": "string", + "example": "2103S" + } + }, + { + "name": "universalVoyageReference", + "in": "query", + "description": "The Universal Reference of a `Voyage` - can be both **importUniversalVoyageReference** and **exportUniversalVoyageReference**. The result will only return schedules including the `Ports` where `universalVoyageReference` is either `importUniversalVoyageReference` or `exportUniversalVoyageReference`", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 5, + "pattern": "^\\d{2}[0-9A-Z]{2}[NEWSR]$", + "type": "string", + "example": "2201N" + } + }, + { + "name": "UNLocationCode", + "in": "query", + "description": "The `UN Location Code` specifying where a port is located. Specifying this filter will only return schedules including **entire Voyages** related to this particular `UN Location Code`.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 5, + "minLength": 5, + "pattern": "^[A-Z]{2}[A-Z2-9]{3}$", + "type": "string", + "example": "NLAMS" + } + }, + { + "name": "facilitySMDGCode", + "in": "query", + "description": "The `facilitySMDGCode` specifying a specific facility (using SMDG Code). Be aware that the `facilitySMDGCode` does not contain a `UNLocationCode` - this must be specified in the `UNLocationCode` filter. Specifying this filter will only return schedules including **entire Voyages** related to this particular `facilitySMDGCode`.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "maxLength": 6, + "type": "string", + "example": "APM" + } + }, + { + "name": "startDate", + "in": "query", + "description": "The start date of the period for which schedule information is requested. If a date of any Timestamp (`ATA`, `ETA` or `PTA`) inside a `PortCall` matches a date on or after (`≥`) the `startDate` the **entire Voyage** (import- and export-Voyage) matching the `PortCall` will be included in the result. All matching is done towards local Date at the place of the port call. If this filter is not provided the default value is **3 months** prior to request time. The value is populated in `ISO 8601` date format.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "format": "date", + "example": "2020-04-06" + } + }, + { + "name": "endDate", + "in": "query", + "description": "The end date of the period for which schedule information is requested. If a date of any Timestamp (`ATA`, `ETA` or `PTA`) inside a `PortCall` matches a date on or before (`≤`) the `endDate` the **entire Voyage**(import- and export-Voyage) matching the `PortCall` will be included in the result. All matching is done towards local Date at the place of the port call. If this filter is not provided the default value is **6 months** after request time. The value is populated in `ISO 8601` date format.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "type": "string", + "format": "date", + "example": "2020-04-10" + } + }, + { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain **MAJOR** version. API-Version header **MUST** be aligned with the URI version.", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string", + "example": "3" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of items to return.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 0, + "type": "number", + "default": 100 + } + } + ], + "responses": { + "200": { + "description": "OK", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceSchedule" + } + } + } + } + }, + "400": { + "description": "Bad Request", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "httpMethod": "GET", + "requestUri": "https://dcsa.org/ovs/v3/service-schedules", + "statusCode": 400, + "statusCodeText": "Bad Request", + "providerCorrelationReference": "4426d965-0dd8-4005-8c63-dc68b01c4962", + "errorDateTime": "2019-11-12T07:41:00+08:30", + "errors": [ + { + "errorCode": 7007, + "property": "UNLocationCode", + "value": "NA", + "errorCodeText": "invalidQuery", + "erorCodeMessage": "UNLocationCode does not exist" + } + ] + } + } + } + } + } + }, + "500": { + "description": "Internal Server Error", + "headers": { + "API-Version": { + "$ref": "#/components/headers/API-Version" + } + }, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + }, + "examples": { + "Example 1": { + "value": { + "httpMethod": "GET", + "requestUri": "https://dcsa.org/ovs/v3/service-schedules", + "statusCode": 500, + "statusCodeText": "Internal Server Error", + "statusCodeMessage": "Cannot process request.", + "providerCorrelationReference": "4426d965-0dd8-4005-8c63-dc68b01c4962", + "errorDateTime": "2019-11-12T07:41:00+08:30", + "errors": [] + } + } + } + } + } + } + }, + "servers": [ + { + "url": "http://localhost:3000" + } + ] + } + } + }, + "components": { + "schemas": { + "serviceSchedules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ServiceSchedule" + } + }, + "ServiceSchedule": { + "title": "ServiceSchedule", + "required": [ + "carrierServiceCode", + "carrierServiceName" + ], + "type": "object", + "properties": { + "carrierServiceName": { + "maxLength": 50, + "type": "string", + "description": "The name of the service.", + "example": "Great Lion Service" + }, + "carrierServiceCode": { + "maxLength": 11, + "type": "string", + "description": "The carrier-specific code of the service for which the schedule details are published.", + "example": "FE1" + }, + "universalServiceReference": { + "maxLength": 8, + "pattern": "^SR\\d{5}[A-Z]$", + "type": "string", + "description": "A global unique service reference, as per DCSA standard, agreed by VSA partners for the service. The service reference must match the regular expression pattern: `SR\\d{5}[A-Z]`. The letters `SR` followed by `5 digits`, followed by a checksum-character as a capital letter from `A to Z`.", + "example": "SR12345A" + }, + "vesselSchedules": { + "type": "array", + "items": { + "$ref": "#/components/schemas/VesselSchedule" + } + } + } + }, + "VesselSchedule": { + "title": "VesselSchedule", + "required": [ + "isDummyVessel", + "vesselOperatorSMDGLinerCode" + ], + "type": "object", + "properties": { + "vesselOperatorSMDGLinerCode": { + "maxLength": 10, + "type": "string", + "description": "The carrier who is in charge of the vessel operation based on the SMDG code.\r\nIf not available at the moment of sharing the schedule, use TBD (To be defined).\r\nIn the case an operator is still using a code not from SMDG, use the available code.", + "example": "HLC" + }, + "vesselIMONumber": { + "maxLength": 7, + "pattern": "^\\d{7}$", + "type": "string", + "description": "The unique reference for a registered Vessel. The reference is the International Maritime Organisation (IMO) number, also sometimes known as the Lloyd's register code, which does not change during the lifetime of the vessel\r\n\r\nCondition: If the vessel is not dummy, there needs to be an IMO. If the vessel is dummy, the IMO is optional.", + "example": "9321483" + }, + "vesselName": { + "maxLength": 35, + "type": "string", + "description": "The name of the Vessel given by the Vessel Operator and registered with IMO.", + "example": "King of the Seas" + }, + "vesselCallSign": { + "maxLength": 10, + "type": "string", + "description": "A unique alphanumeric identity that belongs to the vessel and is assigned by the International Telecommunication Union (ITU). It consists of a threeletter alphanumeric prefix that indicates nationality, followed by one to four characters to identify the individual vessel. For instance, vessels registered under Denmark are assigned the prefix ranges 5PA-5QZ, OUAOZZ, and XPA-XPZ. The Call Sign changes whenever a vessel changes its flag.", + "example": "NCVV" + }, + "isDummyVessel": { + "type": "boolean", + "description": "Is this a dummy vessel. In case no vessel has been asigned yet - this property can be set to `true` indicating that the vesselIMONumber does not exist." + }, + "transportCalls": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TransportCall" + } + } + } + }, + "TransportCall": { + "title": "TransportCall", + "required": [ + "carrierImportVoyageNumber", + "transportCallReference" + ], + "type": "object", + "properties": { + "portVisitReference": { + "maxLength": 50, + "type": "string", + "description": "The unique reference that can be used to link different `transportCallReferences` to the same port visit. The reference is provided by the port to uniquely identify a port call", + "example": "NLAMS1234589" + }, + "transportCallReference": { + "maxLength": 100, + "type": "string", + "description": "The unique reference for a transport call. It’s the vessel operator’s responsibility to provide the Transport Call Reference, other parties are obliged to pick it up and use it. It can take the form of Port Call References as defined in OVS Definitions Document, or alternatively a reference as defined by the vessel operator.", + "example": "SR11111X-9321483-2107W-NLAMS-ACT-1-1" + }, + "carrierImportVoyageNumber": { + "maxLength": 50, + "type": "string", + "description": "The identifier of an import voyage. The carrier-specific identifier of the import Voyage.", + "example": "2103N" + }, + "carrierExportVoyageNumber": { + "maxLength": 50, + "type": "string", + "description": "The identifier of an export voyage. The carrier-specific identifier of the export Voyage.", + "example": "2103S" + }, + "universalImportVoyageReference": { + "pattern": "^\\d{2}[0-9A-Z]{2}[NEWSR]$", + "type": "string", + "description": "A global unique voyage reference for the import Voyage, as per DCSA standard, agreed by VSA partners for the voyage. The voyage reference must match the regular expression pattern: `\\d{2}[0-9A-Z]{2}[NEWSR]`\r\n- `2 digits` for the year\r\n- `2 alphanumeric characters` for the sequence number of the voyage\r\n- `1 character` for the direction/haul (`N`orth, `E`ast, `W`est, `S`outh or `R`oundtrip).", + "example": "2103N" + }, + "universalExportVoyageReference": { + "pattern": "^\\d{2}[0-9A-Z]{2}[NEWSR]$", + "type": "string", + "description": "A global unique voyage reference for the export Voyage, as per DCSA standard, agreed by VSA partners for the voyage. The voyage reference must match the regular expression pattern: `\\d{2}[0-9A-Z]{2}[NEWSR]`\r\n- `2 digits` for the year\r\n- `2 alphanumeric characters` for the sequence number of the voyage\r\n- `1 character` for the direction/haul (`N`orth, `E`ast, `W`est, `S`outh or `R`oundtrip).", + "example": "2103N" + }, + "location": { + "description": "General purpose object to capture location-related data, the location can be specified in **one** of the following ways: `UN Location Code`, a `Facility` or an `Address`.", + "discriminator": { + "propertyName": "locationType", + "mapping": { + "UNLO": "#/components/schemas/UNLocationLocation", + "FACS": "#/components/schemas/FacilitySMDGLocation", + "ADDR": "#/components/schemas/AddressLocation" + } + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UNLocationLocation" + }, + { + "$ref": "#/components/schemas/FacilitySMDGLocation" + }, + { + "$ref": "#/components/schemas/AddressLocation" + } + ] + }, + "statusCode": { + "type": "string", + "description": "The set of codes in `Status Code` are ONLY meant to communicate any change / exception to the published schedule. This is not required in case of normal schedule. Possible values are:\r\n\r\n- OMIT (Omit)\r\n- BLNK (Blank)\r\n- ADHO (Ad Hoc)\r\n- PHOT (Phase Out)\r\n- PHIN (Phase In)\r\n- SLID (Sliding)\r\n- ROTC (Rotation Change)\r\n- CUTR (Cut and Run)\r\n\r\nMore details can be found on [GitHub](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/ovs/v3/reference-data/portcallstatuscodes-v300.csv)", + "example": "OMIT" + }, + "timestamps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Timestamp" + } + } + }, + "description": "\t\nA transportCall in the schedule. A transportCall can be either just a Port or further specified as a terminalCall.\n\nThe order of the list is the sequence of the list" + }, + "UNLocationLocation": { + "title": "UNLocationLocation", + "required": [ + "UNLocationCode", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "maxLength": 100, + "pattern": "^\\S+(\\s+\\S+)*$", + "type": "string", + "description": "The name of the location.", + "example": "Port of Amsterdam" + }, + "locationType": { + "maxLength": 4, + "type": "string", + "description": "Discriminator used to identify this as a `UNLocation` location interface.", + "example": "UNLO" + }, + "UNLocationCode": { + "maxLength": 5, + "minLength": 5, + "pattern": "^[A-Z]{2}[A-Z2-9]{3}$", + "type": "string", + "description": "The UN Location code specifying where the place is located. The pattern used must be\n\n - 2 characters for the country code using [ISO 3166-1 alpha-2](https://www.iso.org/obp/ui/#iso:pub:PUB500001:en)\n - 3 characters to code a location within that country. Letters A-Z and numbers from 2-9 can be used\n\nMore info can be found here: [UN/LOCODE](https://unece.org/trade/cefact/UNLOCODE-Download)", + "example": "NLAMS" + } + }, + "description": "An interface used to express a location using a `Un Location Code`." + }, + "FacilitySMDGLocation": { + "title": "FacilitySMDGLocation", + "required": [ + "UNLocationCode", + "facilitySMDGCode", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "maxLength": 100, + "type": "string", + "description": "The name of the location.", + "example": "Port of Amsterdam" + }, + "locationType": { + "maxLength": 4, + "type": "string", + "description": "Discriminator used to identify this as a `Facility Location` interface only using `SMDG` code list.", + "example": "FACS" + }, + "UNLocationCode": { + "maxLength": 5, + "minLength": 5, + "pattern": "^[A-Z]{2}[A-Z2-9]{3}$", + "type": "string", + "description": "The UN Location code specifying where the place is located. The pattern used must be\n\n - 2 characters for the country code using [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)\n - 3 characters to code a location within that country. Letters A-Z and numbers from 2-9 can be used\n\nMore info can be found here: [UN/LOCODE](https://en.wikipedia.org/wiki/UN/LOCODE)", + "example": "NLAMS" + }, + "facilitySMDGCode": { + "maxLength": 6, + "type": "string", + "description": "The code used for identifying the specific facility. This code does not include the UN Location Code.\n\nThe codeList used by SMDG is the [SMDG Terminal Code List](https://smdg.org/wp-content/uploads/Codelists/Terminals/SMDG-Terminal-Code-List-v20210401.xlsx)", + "example": "ACT" + } + }, + "description": "An interface used to express a location using a `Facility` by the `SMDG` code list. The `facilitySMDGCode` does not contain the `UNLocationCode` - this should be provided in the `UnLocationCode` attribute." + }, + "AddressLocation": { + "title": "AddressLocation", + "required": [ + "address", + "locationType" + ], + "type": "object", + "properties": { + "locationName": { + "maxLength": 100, + "type": "string", + "description": "The name of the location.", + "example": "Port of Amsterdam" + }, + "locationType": { + "maxLength": 4, + "type": "string", + "description": "Discriminator used to identify this as an `Address` location interface.", + "example": "ADDR" + }, + "address": { + "$ref": "#/components/schemas/AddressLocation_address" + } + }, + "description": "An interface used to express a location using an `Address` object." + }, + "Timestamp": { + "title": "Timestamp", + "required": [ + "eventClassifierCode", + "eventDateTime", + "eventTypeCode" + ], + "type": "object", + "properties": { + "eventTypeCode": { + "type": "string", + "description": "Identifier for type of `transportEvent`\r\n\r\n- ARRI (Arrived)\r\n- DEPA (Departed)\r\n\r\nMore details can be found on [GitHub](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/ovs/v3/reference-data/transporteventtypecodes-v300.csv).", + "enum": [ + "ARRI", + "DEPA" + ] + }, + "eventClassifierCode": { + "type": "string", + "description": "Code for the event classifier. Values can vary depending on eventType.\n\nPossible values are:\n- ACT (Actual)\n- EST (Estimated)\n- PLN (Planned)\n", + "enum": [ + "PLN", + "EST", + "ACT" + ] + }, + "eventDateTime": { + "type": "string", + "description": "Time in the timestamp", + "format": "date-time", + "example": "2025-01-14T09:21:00+01:00" + }, + "delayReasonCode": { + "maxLength": 3, + "type": "string", + "description": "Reason code for the delay. See SMDG [Code list DELAY](https://smdg.org/documents/smdg-code-lists/delay-reason-and-port-call-activity/) for a list of valid codes to be used for this attribute.", + "example": "WEA" + }, + "changeRemark": { + "maxLength": 250, + "type": "string", + "description": "Free text field to provide information as to why the `TransportEvent` was sent.", + "example": "Bad weather" + } + }, + "description": "\t\nA timestamp for a port." + }, + "ErrorResponse": { + "title": "ErrorResponse", + "required": [ + "errorDateTime", + "errors", + "httpMethod", + "requestUri", + "statusCode", + "statusCodeText" + ], + "type": "object", + "properties": { + "httpMethod": { + "type": "string", + "description": "The HTTP method used to make the request e.g. `GET`, `POST`, etc\n", + "example": "POST", + "enum": [ + "GET", + "HEAD", + "POST", + "PUT", + "DELETE", + "OPTION", + "PATCH" + ] + }, + "requestUri": { + "type": "string", + "description": "The URI that was requested.\n", + "example": "/v1/events" + }, + "statusCode": { + "type": "integer", + "description": "The HTTP status code returned.\n", + "format": "int32", + "example": 400 + }, + "statusCodeText": { + "maxLength": 50, + "type": "string", + "description": "A standard short description corresponding to the HTTP status code.\n", + "example": "Bad Request" + }, + "statusCodeMessage": { + "maxLength": 200, + "type": "string", + "description": "A long description corresponding to the HTTP status code with additional information.\n", + "example": "The supplied data could not be accepted" + }, + "providerCorrelationReference": { + "maxLength": 100, + "type": "string", + "description": "A unique identifier to the HTTP request within the scope of the API provider.\n", + "example": "4426d965-0dd8-4005-8c63-dc68b01c4962" + }, + "errorDateTime": { + "type": "string", + "description": "The DateTime corresponding to the error occuring. Must be formatted using [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) format.\n", + "format": "date-time", + "example": "2019-11-12T07:41:00+08:30" + }, + "errors": { + "type": "array", + "description": "An array of errors provding more detail about the root cause.\n", + "items": { + "$ref": "#/components/schemas/DetailedError" + } + } + }, + "description": "Unexpected error" + }, + "AddressLocation_address": { + "required": [ + "name" + ], + "type": "object", + "properties": { + "name": { + "maxLength": 100, + "type": "string", + "description": "Name of the address.", + "example": "John" + }, + "street": { + "maxLength": 100, + "type": "string", + "description": "The name of the street of the party’s address.", + "example": "Ruijggoordweg" + }, + "streetNumber": { + "maxLength": 50, + "type": "string", + "description": "The number of the street of the party’s address.", + "example": "100" + }, + "floor": { + "maxLength": 50, + "type": "string", + "description": "The floor of the party’s street number.", + "example": "N/A" + }, + "postCode": { + "maxLength": 50, + "type": "string", + "description": "The post code of the party’s address.", + "example": "1047 HM" + }, + "city": { + "maxLength": 65, + "type": "string", + "description": "The city name of the party’s address.", + "example": "Amsterdam" + }, + "stateRegion": { + "maxLength": 65, + "type": "string", + "description": "The state/region of the party’s address.", + "example": "North Holland" + }, + "country": { + "maxLength": 75, + "type": "string", + "description": "The country of the party’s address.", + "example": "The Netherlands" + } + }, + "description": "An object for storing address related information." + }, + "DetailedError": { + "title": "DetailedError", + "required": [ + "errorCodeText" + ], + "type": "object", + "properties": { + "errorCode": { + "maximum": 9999, + "minimum": 7000, + "type": "integer", + "description": "The detailed error code returned.\n\n - `7000-7999` Technical error codes\n - `8000-8999` Functional error codes\n - `9000-9999` API provider-specific error codes \n\n[Error codes as specified by DCSA](https://dcsa.atlassian.net/wiki/spaces/DTG/pages/197132308/Standard+Error+Codes).\n", + "format": "int32", + "example": 7003 + }, + "property": { + "maxLength": 100, + "type": "string", + "description": "The name of the property causing the error.\n", + "example": "facilityCode" + }, + "value": { + "maxLength": 500, + "type": "string", + "description": "The value of the property causing the error serialised as a string exactly as in the original request.\n", + "example": "SG SIN WHS" + }, + "jsonPath": { + "maxLength": 500, + "type": "string", + "description": "A path to the property causing the error, formatted according to [JSONpath](https://github.com/json-path/JsonPath).\n", + "example": "$.location.facilityCode" + }, + "errorCodeText": { + "maxLength": 100, + "type": "string", + "description": "A standard short description corresponding to the `errorCode`.\n", + "example": "invalidData" + }, + "erorCodeMessage": { + "maxLength": 200, + "type": "string", + "description": "A long description corresponding to the `errorCode` with additional information.\n", + "example": "Spaces not allowed in facility code" + } + } + } + }, + "parameters": { + "Limit": { + "name": "limit", + "in": "query", + "description": "Maximum number of items to return.", + "required": false, + "style": "form", + "explode": true, + "schema": { + "minimum": 0, + "type": "number", + "default": 100 + } + }, + "APIVersionMajor": { + "name": "API-Version", + "in": "header", + "description": "An API-Version header **MAY** be added to the request (optional); if added it **MUST** only contain **MAJOR** version. API-Version header **MUST** be aligned with the URI version.", + "required": false, + "style": "simple", + "explode": false, + "schema": { + "type": "string", + "example": "3" + } + } + }, + "headers": { + "API-Version": { + "description": "SemVer used to indicate the version of the contract (API version) returned.\n", + "style": "simple", + "explode": false, + "schema": { + "type": "string", + "example": "3.0.0" + } + } + } + } +} From dbd9b0ed3b5b209860ca0297d929e873394d4111 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:13:09 +0100 Subject: [PATCH 06/14] OVS filtering with publisher-supplied scenario parameters --- .../standards/ovs/OvsComponentFactory.java | 3 + .../standards/ovs/OvsScenarioListBuilder.java | 21 ++++- .../standards/ovs/action/OvsAction.java | 14 ++++ .../ovs/action/OvsGetSchedulesAction.java | 12 ++- .../SupplyScenarioParametersAction.java | 84 +++++++++++++++++++ .../ovs/party/OvsFilterParameter.java | 36 ++++++++ .../standards/ovs/party/OvsPublisher.java | 52 +++++++++++- .../standards/ovs/party/OvsSubscriber.java | 14 +++- .../ovs/party/SuppliedScenarioParameters.java | 44 ++++++++++ 9 files changed, 272 insertions(+), 8 deletions(-) create mode 100644 ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java create mode 100644 ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java create mode 100644 ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/SuppliedScenarioParameters.java diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java index 77f30b13..08aebfcd 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java @@ -4,6 +4,8 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; + +import lombok.Getter; import lombok.SneakyThrows; import org.dcsa.conformance.core.AbstractComponentFactory; import org.dcsa.conformance.core.check.JsonSchemaValidator; @@ -25,6 +27,7 @@ public class OvsComponentFactory extends AbstractComponentFactory { private static final String PUBLISHER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); private static final String SUBSCRIBER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); + @Getter private final String standardVersion; public OvsComponentFactory(String standardVersion) { diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java index a72d72dc..33389543 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java @@ -4,8 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.scenario.ConformanceAction; import org.dcsa.conformance.core.scenario.ScenarioListBuilder; -import org.dcsa.conformance.standards.ovs.action.OvsAction; import org.dcsa.conformance.standards.ovs.action.OvsGetSchedulesAction; +import org.dcsa.conformance.standards.ovs.action.SupplyScenarioParametersAction; +import org.dcsa.conformance.standards.ovs.party.OvsFilterParameter; import org.dcsa.conformance.standards.ovs.party.OvsRole; @Slf4j @@ -20,7 +21,13 @@ public static OvsScenarioListBuilder buildTree( threadLocalComponentFactory.set(componentFactory); threadLocalPublisherPartyName.set(publisherPartyName); threadLocalSubscriberPartyName.set(subscriberPartyName); - return noAction().then(getEventsRequest()); + if ("3.0.0-Beta1".equals(componentFactory.getStandardVersion())) { + return noAction().then(getEventsRequest()); + } else { + return supplyScenarioParameters( + OvsFilterParameter.LIMIT, OvsFilterParameter.CARRIER_VOYAGE_NUMBER) + .then(getEventsRequest()); + } } private OvsScenarioListBuilder(Function actionBuilder) { @@ -31,6 +38,14 @@ private static OvsScenarioListBuilder noAction() { return new OvsScenarioListBuilder(null); } + private static OvsScenarioListBuilder supplyScenarioParameters( + OvsFilterParameter... ovsFilterParameters) { + String publisherPartyName = threadLocalPublisherPartyName.get(); + return new OvsScenarioListBuilder( + previousAction -> + new SupplyScenarioParametersAction(publisherPartyName, ovsFilterParameters)); + } + private static OvsScenarioListBuilder getEventsRequest() { OvsComponentFactory componentFactory = threadLocalComponentFactory.get(); String publisherPartyName = threadLocalPublisherPartyName.get(); @@ -40,7 +55,7 @@ private static OvsScenarioListBuilder getEventsRequest() { new OvsGetSchedulesAction( subscriberPartyName, publisherPartyName, - (OvsAction) previousAction, + previousAction, componentFactory.getMessageSchemaValidator( OvsRole.PUBLISHER.getConfigName(), false))); } diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsAction.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsAction.java index c21087e5..6646d6fe 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsAction.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsAction.java @@ -1,8 +1,13 @@ package org.dcsa.conformance.standards.ovs.action; import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.standards.ovs.party.SuppliedScenarioParameters; + +import java.util.Map; +import java.util.function.Supplier; public abstract class OvsAction extends ConformanceAction { + protected final Supplier sspSupplier; protected final int expectedStatus; public OvsAction( @@ -12,6 +17,15 @@ public OvsAction( String actionTitle, int expectedStatus) { super(sourcePartyName, targetPartyName, previousAction, actionTitle); + this.sspSupplier = _getSspSupplier(previousAction); this.expectedStatus = expectedStatus; } + + private Supplier _getSspSupplier(ConformanceAction previousAction) { + return previousAction instanceof SupplyScenarioParametersAction supplyAvailableTdrAction + ? supplyAvailableTdrAction::getSuppliedScenarioParameters + : previousAction == null + ? () -> SuppliedScenarioParameters.fromMap(Map.ofEntries()) + : _getSspSupplier(previousAction.getPreviousAction()); + } } diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsGetSchedulesAction.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsGetSchedulesAction.java index b962c0c3..b783785f 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsGetSchedulesAction.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/OvsGetSchedulesAction.java @@ -1,9 +1,12 @@ package org.dcsa.conformance.standards.ovs.action; import java.util.stream.Stream; + +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.check.*; +import org.dcsa.conformance.core.scenario.ConformanceAction; import org.dcsa.conformance.core.traffic.HttpMessageType; import org.dcsa.conformance.standards.ovs.party.OvsRole; @@ -15,7 +18,7 @@ public class OvsGetSchedulesAction extends OvsAction { public OvsGetSchedulesAction( String subscriberPartyName, String publisherPartyName, - OvsAction previousAction, + ConformanceAction previousAction, JsonSchemaValidator responseSchemaValidator) { super(subscriberPartyName, publisherPartyName, previousAction, "GetSchedules", 200); this.responseSchemaValidator = responseSchemaValidator; @@ -23,7 +26,8 @@ public OvsGetSchedulesAction( @Override public String getHumanReadablePrompt() { - return "Send a GET schedules request"; + return "Send a GET schedules request with the following parameters: " + + sspSupplier.get().toJson().toPrettyString(); } @Override @@ -42,4 +46,8 @@ protected Stream createSubChecks() { } }; } + + public ObjectNode asJsonNode() { + return super.asJsonNode().set("suppliedScenarioParameters", sspSupplier.get().toJson()); + } } diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java new file mode 100644 index 00000000..4f610912 --- /dev/null +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java @@ -0,0 +1,84 @@ +package org.dcsa.conformance.standards.ovs.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.LinkedHashSet; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.Getter; +import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.standards.ovs.party.OvsFilterParameter; +import org.dcsa.conformance.standards.ovs.party.SuppliedScenarioParameters; + +@Getter +public class SupplyScenarioParametersAction extends ConformanceAction { + private SuppliedScenarioParameters suppliedScenarioParameters = null; + private final LinkedHashSet ovsFilterParameters; + + public SupplyScenarioParametersAction( + String publisherPartyName, OvsFilterParameter... ovsFilterParameters) { + super(publisherPartyName, null, null, "SupplyScenarioParameters"); + this.ovsFilterParameters = + Stream.of(ovsFilterParameters).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public void reset() { + super.reset(); + suppliedScenarioParameters = null; + } + + @Override + public ObjectNode exportJsonState() { + ObjectNode jsonState = super.exportJsonState(); + if (suppliedScenarioParameters != null) { + jsonState.set("suppliedScenarioParameters", suppliedScenarioParameters.toJson()); + } + return jsonState; + } + + @Override + public void importJsonState(JsonNode jsonState) { + super.importJsonState(jsonState); + if (jsonState.has("suppliedScenarioParameters")) { + suppliedScenarioParameters = + SuppliedScenarioParameters.fromJson(jsonState.required("suppliedScenarioParameters")); + } + } + + @Override + public ObjectNode asJsonNode() { + ObjectNode objectNode = super.asJsonNode(); + ArrayNode jsonOvsFilterParameters = objectNode.putArray("ovsFilterParametersQueryParamNames"); + ovsFilterParameters.forEach( + ovsFilterParameter -> jsonOvsFilterParameters.add(ovsFilterParameter.getQueryParamName())); + return objectNode; + } + + @Override + public String getHumanReadablePrompt() { + return "Use the following format to provide the values of the specified query parameters" + + " for which your party can successfully process a GET request:"; + } + + @Override + public JsonNode getJsonForHumanReadablePrompt() { + return SuppliedScenarioParameters.fromMap( + ovsFilterParameters.stream() + .collect(Collectors.toMap(Function.identity(), ignoredKey -> "TODO"))) + .toJson(); + } + + @Override + public boolean isInputRequired() { + return true; + } + + @Override + public void handlePartyInput(JsonNode partyInput) { + super.handlePartyInput(partyInput); + suppliedScenarioParameters = SuppliedScenarioParameters.fromJson(partyInput.get("input")); + } +} diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java new file mode 100644 index 00000000..fdb2abe2 --- /dev/null +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsFilterParameter.java @@ -0,0 +1,36 @@ +package org.dcsa.conformance.standards.ovs.party; + +import lombok.Getter; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum OvsFilterParameter { + CARRIER_SERVICE_NAME("carrierServiceName"), + CARRIER_SERVICE_CODE("carrierServiceCode"), + UNIVERSAL_SERVICE_REFERENCE("universalServiceReference"), + VESSEL_IMO_NUMBER("vesselIMONumber"), + VESSEL_NAME("vesselName"), + CARRIER_VOYAGE_NUMBER("carrierVoyageNumber"), + UNIVERSAL_VOYAGE_REFERENCE("universalVoyageReference"), + UN_LOCATION_CODE("UNLocationCode"), + FACILITY_SMDG_CODE("facilitySMDGCode"), + START_DATE("startDate"), + END_DATE("endDate"), + LIMIT("limit"), + ; + + public static final Map byQueryParamName = + Arrays.stream(values()) + .collect( + Collectors.toUnmodifiableMap( + OvsFilterParameter::getQueryParamName, Function.identity())); + + @Getter private final String queryParamName; + + OvsFilterParameter(String queryParamName) { + this.queryParamName = queryParamName; + } +} diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java index e30d81de..e2344591 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java @@ -1,9 +1,16 @@ package org.dcsa.conformance.standards.ovs.party; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.text.SimpleDateFormat; import java.util.*; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.party.ConformanceParty; import org.dcsa.conformance.core.party.CounterpartConfiguration; @@ -15,6 +22,7 @@ import org.dcsa.conformance.core.traffic.ConformanceMessageBody; import org.dcsa.conformance.core.traffic.ConformanceRequest; import org.dcsa.conformance.core.traffic.ConformanceResponse; +import org.dcsa.conformance.standards.ovs.action.SupplyScenarioParametersAction; @Slf4j public class OvsPublisher extends ConformanceParty { @@ -46,7 +54,49 @@ protected void doReset() {} @Override protected Map, Consumer> getActionPromptHandlers() { - return Map.ofEntries(); + return Map.ofEntries( + Map.entry(SupplyScenarioParametersAction.class, this::supplyScenarioParameters)); + } + + private void supplyScenarioParameters(JsonNode actionPrompt) { + log.info("OvsPublisher.supplyScenarioParameters(%s)".formatted(actionPrompt.toPrettyString())); + + SuppliedScenarioParameters responseSsp = + SuppliedScenarioParameters.fromMap( + StreamSupport.stream( + actionPrompt.required("ovsFilterParametersQueryParamNames").spliterator(), + false) + .map( + jsonOvsFilterParameter -> + OvsFilterParameter.byQueryParamName.get(jsonOvsFilterParameter.asText())) + .collect( + Collectors.toMap( + Function.identity(), + ovsFilterParameter -> + switch (ovsFilterParameter) { + case CARRIER_SERVICE_NAME -> "Great Lion Service"; + case CARRIER_SERVICE_CODE -> "FE1"; + case UNIVERSAL_SERVICE_REFERENCE -> "SR12345A"; + case VESSEL_IMO_NUMBER -> "9321483"; + case VESSEL_NAME -> "King of the Seas"; + case CARRIER_VOYAGE_NUMBER -> "2103S"; + case UNIVERSAL_VOYAGE_REFERENCE -> "2201N"; + case UN_LOCATION_CODE -> "NLAMS"; + case FACILITY_SMDG_CODE -> "APM"; + case START_DATE, END_DATE -> + new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + case LIMIT -> "100"; + }))); + + asyncOrchestratorPostPartyInput( + new ObjectMapper() + .createObjectNode() + .put("actionId", actionPrompt.required("actionId").asText()) + .set("input", responseSsp.toJson())); + + addOperatorLogEntry( + "Submitting SuppliedScenarioParameters: %s" + .formatted(responseSsp.toJson().toPrettyString())); } @Override diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsSubscriber.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsSubscriber.java index 71a452bc..4a5e64ee 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsSubscriber.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsSubscriber.java @@ -4,6 +4,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.*; import java.util.function.Consumer; +import java.util.stream.Collectors; + import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.party.ConformanceParty; import org.dcsa.conformance.core.party.CounterpartConfiguration; @@ -50,11 +52,19 @@ protected Map, Consumer> getActionP private void getSchedules(JsonNode actionPrompt) { log.info("OvsSubscriber.getSchedules(%s)".formatted(actionPrompt.toPrettyString())); + SuppliedScenarioParameters ssp = + SuppliedScenarioParameters.fromJson(actionPrompt.get("suppliedScenarioParameters")); syncCounterpartGet( - "/%s/service-schedules".formatted(apiVersion.startsWith("2") ? "v2" : "v3"), Map.ofEntries()); + "/v3/service-schedules", + ssp.getMap().entrySet().stream() + .collect( + Collectors.toMap( + entry -> entry.getKey().getQueryParamName(), + entry -> Set.of(entry.getValue())))); - addOperatorLogEntry("Sent GET schedules request"); + addOperatorLogEntry( + "Sent GET schedules request with parameters %s".formatted(ssp.toJson().toPrettyString())); } @Override diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/SuppliedScenarioParameters.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/SuppliedScenarioParameters.java new file mode 100644 index 00000000..b85aa777 --- /dev/null +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/SuppliedScenarioParameters.java @@ -0,0 +1,44 @@ +package org.dcsa.conformance.standards.ovs.party; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Getter; + +@Getter +public class SuppliedScenarioParameters { + + private final Map map; + + private SuppliedScenarioParameters(Map map) { + this.map = Collections.unmodifiableMap(map); + } + + public ObjectNode toJson() { + ObjectNode objectNode = new ObjectMapper().createObjectNode(); + map.forEach( + (ovsFilterParameter, value) -> + objectNode.put(ovsFilterParameter.getQueryParamName(), value)); + return objectNode; + } + + public static SuppliedScenarioParameters fromMap(Map map) { + return new SuppliedScenarioParameters(map); + } + + public static SuppliedScenarioParameters fromJson(JsonNode jsonNode) { + return new SuppliedScenarioParameters( + Arrays.stream(OvsFilterParameter.values()) + .filter(ovsFilterParameter -> jsonNode.has(ovsFilterParameter.getQueryParamName())) + .collect( + Collectors.toUnmodifiableMap( + Function.identity(), + ovsFilterParameter -> + jsonNode.required(ovsFilterParameter.getQueryParamName()).asText()))); + } +} From c789923c4479ec50796fac214bada94324ce99ce Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 19 Feb 2024 20:58:12 +0100 Subject: [PATCH 07/14] OVS scenarios for the basic common filters --- .../standards/ovs/OvsScenarioListBuilder.java | 46 +++++++++++++++++-- .../SupplyScenarioParametersAction.java | 26 ++++++++++- .../standards/ovs/party/OvsPublisher.java | 5 +- 3 files changed, 68 insertions(+), 9 deletions(-) diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java index 33389543..9d82d8da 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java @@ -1,5 +1,7 @@ package org.dcsa.conformance.standards.ovs; +import static org.dcsa.conformance.standards.ovs.party.OvsFilterParameter.*; + import java.util.function.Function; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.scenario.ConformanceAction; @@ -22,11 +24,31 @@ public static OvsScenarioListBuilder buildTree( threadLocalPublisherPartyName.set(publisherPartyName); threadLocalSubscriberPartyName.set(subscriberPartyName); if ("3.0.0-Beta1".equals(componentFactory.getStandardVersion())) { - return noAction().then(getEventsRequest()); + return noAction().then(getSchedules()); } else { - return supplyScenarioParameters( - OvsFilterParameter.LIMIT, OvsFilterParameter.CARRIER_VOYAGE_NUMBER) - .then(getEventsRequest()); + return noAction() + .thenEither( + scenarioWithFilterBy(CARRIER_SERVICE_NAME), + scenarioWithFilterByDatesAnd(CARRIER_SERVICE_NAME), + scenarioWithFilterBy(CARRIER_SERVICE_CODE), + scenarioWithFilterByDatesAnd(CARRIER_SERVICE_CODE), + scenarioWithFilterBy(UNIVERSAL_SERVICE_REFERENCE), + scenarioWithFilterByDatesAnd(UNIVERSAL_SERVICE_REFERENCE), + scenarioWithFilterBy(VESSEL_IMO_NUMBER), + scenarioWithFilterByDatesAnd(VESSEL_IMO_NUMBER), + scenarioWithFilterBy(VESSEL_NAME), + scenarioWithFilterByDatesAnd(VESSEL_NAME), + scenarioWithFilterBy(CARRIER_VOYAGE_NUMBER), + scenarioWithFilterByDatesAnd(CARRIER_VOYAGE_NUMBER), + scenarioWithFilterBy(UNIVERSAL_VOYAGE_REFERENCE), + scenarioWithFilterByDatesAnd(UNIVERSAL_VOYAGE_REFERENCE), + scenarioWithFilterBy(UN_LOCATION_CODE), + scenarioWithFilterByDatesAnd(UN_LOCATION_CODE), + scenarioWithFilterBy(FACILITY_SMDG_CODE), + scenarioWithFilterByDatesAnd(FACILITY_SMDG_CODE), + scenarioWithFilterBy(START_DATE), + scenarioWithFilterBy(END_DATE), + scenarioWithFilterBy(START_DATE, END_DATE)); } } @@ -38,6 +60,20 @@ private static OvsScenarioListBuilder noAction() { return new OvsScenarioListBuilder(null); } + private static OvsScenarioListBuilder scenarioWithFilterBy(OvsFilterParameter parameter1) { + return supplyScenarioParameters(LIMIT, parameter1).then(getSchedules()); + } + + private static OvsScenarioListBuilder scenarioWithFilterByDatesAnd( + OvsFilterParameter parameter1) { + return supplyScenarioParameters(LIMIT, START_DATE, END_DATE, parameter1).then(getSchedules()); + } + + private static OvsScenarioListBuilder scenarioWithFilterBy( + OvsFilterParameter parameter1, OvsFilterParameter parameter2) { + return supplyScenarioParameters(LIMIT, parameter1, parameter2).then(getSchedules()); + } + private static OvsScenarioListBuilder supplyScenarioParameters( OvsFilterParameter... ovsFilterParameters) { String publisherPartyName = threadLocalPublisherPartyName.get(); @@ -46,7 +82,7 @@ private static OvsScenarioListBuilder supplyScenarioParameters( new SupplyScenarioParametersAction(publisherPartyName, ovsFilterParameters)); } - private static OvsScenarioListBuilder getEventsRequest() { + private static OvsScenarioListBuilder getSchedules() { OvsComponentFactory componentFactory = threadLocalComponentFactory.get(); String publisherPartyName = threadLocalPublisherPartyName.get(); String subscriberPartyName = threadLocalSubscriberPartyName.get(); diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java index 4f610912..8325da52 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/action/SupplyScenarioParametersAction.java @@ -3,6 +3,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; import java.util.LinkedHashSet; import java.util.function.Function; import java.util.stream.Collectors; @@ -14,12 +18,22 @@ @Getter public class SupplyScenarioParametersAction extends ConformanceAction { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + private SuppliedScenarioParameters suppliedScenarioParameters = null; private final LinkedHashSet ovsFilterParameters; public SupplyScenarioParametersAction( String publisherPartyName, OvsFilterParameter... ovsFilterParameters) { - super(publisherPartyName, null, null, "SupplyScenarioParameters"); + super( + publisherPartyName, + null, + null, + "SupplyScenarioParameters(%s)" + .formatted( + Arrays.stream(ovsFilterParameters) + .map(OvsFilterParameter::getQueryParamName) + .collect(Collectors.joining(", ")))); this.ovsFilterParameters = Stream.of(ovsFilterParameters).collect(Collectors.toCollection(LinkedHashSet::new)); } @@ -67,7 +81,15 @@ public String getHumanReadablePrompt() { public JsonNode getJsonForHumanReadablePrompt() { return SuppliedScenarioParameters.fromMap( ovsFilterParameters.stream() - .collect(Collectors.toMap(Function.identity(), ignoredKey -> "TODO"))) + .collect( + Collectors.toMap( + Function.identity(), + ovsFilterParameter -> + switch (ovsFilterParameter) { + case LIMIT -> "100"; + case START_DATE, END_DATE -> DATE_FORMAT.format(new Date()); + default -> "TODO"; + }))) .toJson(); } diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java index e2344591..a7489eda 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/party/OvsPublisher.java @@ -27,6 +27,8 @@ @Slf4j public class OvsPublisher extends ConformanceParty { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); + public OvsPublisher( String apiVersion, PartyConfiguration partyConfiguration, @@ -83,8 +85,7 @@ private void supplyScenarioParameters(JsonNode actionPrompt) { case UNIVERSAL_VOYAGE_REFERENCE -> "2201N"; case UN_LOCATION_CODE -> "NLAMS"; case FACILITY_SMDG_CODE -> "APM"; - case START_DATE, END_DATE -> - new SimpleDateFormat("yyyy-MM-dd").format(new Date()); + case START_DATE, END_DATE -> DATE_FORMAT.format(new Date()); case LIMIT -> "100"; }))); From 4854cf0a55a4bf46f7737f884de468c51b10690f Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Tue, 20 Feb 2024 10:12:48 +0100 Subject: [PATCH 08/14] TNT 220 implementation now in line with OVS 300b2 --- .../standards/tnt/TntComponentFactory.java | 18 +-- .../standards/tnt/TntScenarioListBuilder.java | 72 +++++++++++- .../SupplyScenarioParametersAction.java | 110 ++++++++++++++++++ .../standards/tnt/action/TntAction.java | 13 +++ .../tnt/action/TntGetEventsAction.java | 75 +++--------- .../tnt/party/SuppliedScenarioParameters.java | 44 +++++++ .../tnt/party/TntFilterParameter.java | 43 +++++++ .../standards/tnt/party/TntPublisher.java | 67 ++++++++++- .../standards/tnt/party/TntSubscriber.java | 13 ++- ...22-response.json => tnt-220-response.json} | 0 ...-publisher.json => tnt-220-publisher.json} | 0 11 files changed, 373 insertions(+), 82 deletions(-) create mode 100644 tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/SupplyScenarioParametersAction.java create mode 100644 tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/SuppliedScenarioParameters.java create mode 100644 tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntFilterParameter.java rename tnt/src/main/resources/standards/tnt/messages/{tnt-v22-response.json => tnt-220-response.json} (100%) rename tnt/src/main/resources/standards/tnt/schemas/{tnt-v22-publisher.json => tnt-220-publisher.json} (100%) diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntComponentFactory.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntComponentFactory.java index 9349dcba..f9253aac 100644 --- a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntComponentFactory.java +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntComponentFactory.java @@ -14,7 +14,6 @@ import org.dcsa.conformance.core.scenario.ScenarioListBuilder; import org.dcsa.conformance.core.state.JsonNodeMap; import org.dcsa.conformance.core.toolkit.JsonToolkit; -import org.dcsa.conformance.standards.tnt.action.TntEventType; import org.dcsa.conformance.standards.tnt.party.TntPublisher; import org.dcsa.conformance.standards.tnt.party.TntRole; import org.dcsa.conformance.standards.tnt.party.TntSubscriber; @@ -113,18 +112,11 @@ public Set getReportRoleNames( .collect(Collectors.toSet()); } - public Map getEventSchemaValidators() { - String schemaFilePath = "/standards/tnt/schemas/tnt-v22-publisher.json"; - return Map.ofEntries( - Map.entry( - TntEventType.EQUIPMENT, - JsonSchemaValidator.getInstance(schemaFilePath, "equipmentEvent")), - Map.entry( - TntEventType.SHIPMENT, - JsonSchemaValidator.getInstance(schemaFilePath, "shipmentEvent")), - Map.entry( - TntEventType.TRANSPORT, - JsonSchemaValidator.getInstance(schemaFilePath, "transportEvent"))); + public JsonSchemaValidator getMessageSchemaValidator(String apiProviderRole, boolean forRequest) { + String schemaFilePath = "/standards/tnt/schemas/tnt-220-publisher.json"; + String schemaName = + TntRole.isPublisher(apiProviderRole) ? (forRequest ? null : "events") : null; + return JsonSchemaValidator.getInstance(schemaFilePath, schemaName); } @SneakyThrows diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntScenarioListBuilder.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntScenarioListBuilder.java index b96ac3e7..79359d79 100644 --- a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntScenarioListBuilder.java +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/TntScenarioListBuilder.java @@ -1,11 +1,15 @@ package org.dcsa.conformance.standards.tnt; +import static org.dcsa.conformance.standards.tnt.party.TntFilterParameter.*; + import java.util.function.Function; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.scenario.ConformanceAction; import org.dcsa.conformance.core.scenario.ScenarioListBuilder; -import org.dcsa.conformance.standards.tnt.action.TntAction; +import org.dcsa.conformance.standards.tnt.action.SupplyScenarioParametersAction; import org.dcsa.conformance.standards.tnt.action.TntGetEventsAction; +import org.dcsa.conformance.standards.tnt.party.TntFilterParameter; +import org.dcsa.conformance.standards.tnt.party.TntRole; @Slf4j public class TntScenarioListBuilder extends ScenarioListBuilder { @@ -19,7 +23,40 @@ public static TntScenarioListBuilder buildTree( threadLocalComponentFactory.set(componentFactory); threadLocalPublisherPartyName.set(publisherPartyName); threadLocalSubscriberPartyName.set(subscriberPartyName); - return noAction().then(getEventsRequest()); + return noAction() + .thenEither( + scenarioWithFilterBy(EVENT_CREATED_DATE_TIME), + scenarioWithFilterBy(EVENT_CREATED_DATE_TIME_EQ), + scenarioWithFilterBy(EVENT_CREATED_DATE_TIME_GT, EVENT_CREATED_DATE_TIME_LT), + scenarioWithFilterBy(EVENT_CREATED_DATE_TIME_GT, EVENT_CREATED_DATE_TIME_LTE), + scenarioWithFilterBy(EVENT_CREATED_DATE_TIME_GTE, EVENT_CREATED_DATE_TIME_LT), + scenarioWithFilterBy(EVENT_CREATED_DATE_TIME_GTE, EVENT_CREATED_DATE_TIME_LTE), + scenarioWithFilterBy(EVENT_TYPE), + scenarioWithFilterByDateTimesAnd(EVENT_TYPE), + scenarioWithFilterBy(SHIPMENT_EVENT_TYPE_CODE), + scenarioWithFilterByDateTimesAnd(SHIPMENT_EVENT_TYPE_CODE), + scenarioWithFilterBy(DOCUMENT_TYPE_CODE), + scenarioWithFilterByDateTimesAnd(DOCUMENT_TYPE_CODE), + scenarioWithFilterBy(CARRIER_BOOKING_REFERENCE), + scenarioWithFilterByDateTimesAnd(CARRIER_BOOKING_REFERENCE), + scenarioWithFilterBy(TRANSPORT_DOCUMENT_REFERENCE), + scenarioWithFilterByDateTimesAnd(TRANSPORT_DOCUMENT_REFERENCE), + scenarioWithFilterBy(TRANSPORT_EVENT_TYPE_CODE), + scenarioWithFilterByDateTimesAnd(TRANSPORT_EVENT_TYPE_CODE), + scenarioWithFilterBy(TRANSPORT_CALL_ID), + scenarioWithFilterByDateTimesAnd(TRANSPORT_CALL_ID), + scenarioWithFilterBy(VESSEL_IMO_NUMBER), + scenarioWithFilterByDateTimesAnd(VESSEL_IMO_NUMBER), + scenarioWithFilterBy(EXPORT_VOYAGE_NUMBER), + scenarioWithFilterByDateTimesAnd(EXPORT_VOYAGE_NUMBER), + scenarioWithFilterBy(CARRIER_SERVICE_CODE), + scenarioWithFilterByDateTimesAnd(CARRIER_SERVICE_CODE), + scenarioWithFilterBy(UN_LOCATION_CODE), + scenarioWithFilterByDateTimesAnd(UN_LOCATION_CODE), + scenarioWithFilterBy(EQUIPMENT_EVENT_TYPE_CODE), + scenarioWithFilterByDateTimesAnd(EQUIPMENT_EVENT_TYPE_CODE), + scenarioWithFilterBy(EQUIPMENT_REFERENCE), + scenarioWithFilterByDateTimesAnd(EQUIPMENT_REFERENCE)); } private TntScenarioListBuilder(Function actionBuilder) { @@ -30,7 +67,31 @@ private static TntScenarioListBuilder noAction() { return new TntScenarioListBuilder(null); } - private static TntScenarioListBuilder getEventsRequest() { + private static TntScenarioListBuilder scenarioWithFilterBy(TntFilterParameter parameter1) { + return supplyScenarioParameters(LIMIT, parameter1).then(getEvents()); + } + + private static TntScenarioListBuilder scenarioWithFilterByDateTimesAnd( + TntFilterParameter parameter1) { + return supplyScenarioParameters( + LIMIT, EVENT_CREATED_DATE_TIME_GTE, EVENT_CREATED_DATE_TIME_LT, parameter1) + .then(getEvents()); + } + + private static TntScenarioListBuilder scenarioWithFilterBy( + TntFilterParameter parameter1, TntFilterParameter parameter2) { + return supplyScenarioParameters(LIMIT, parameter1, parameter2).then(getEvents()); + } + + private static TntScenarioListBuilder supplyScenarioParameters( + TntFilterParameter... TntFilterParameters) { + String publisherPartyName = threadLocalPublisherPartyName.get(); + return new TntScenarioListBuilder( + previousAction -> + new SupplyScenarioParametersAction(publisherPartyName, TntFilterParameters)); + } + + private static TntScenarioListBuilder getEvents() { TntComponentFactory componentFactory = threadLocalComponentFactory.get(); String publisherPartyName = threadLocalPublisherPartyName.get(); String subscriberPartyName = threadLocalSubscriberPartyName.get(); @@ -39,7 +100,8 @@ private static TntScenarioListBuilder getEventsRequest() { new TntGetEventsAction( subscriberPartyName, publisherPartyName, - (TntAction) previousAction, - componentFactory.getEventSchemaValidators())); + previousAction, + componentFactory.getMessageSchemaValidator( + TntRole.PUBLISHER.getConfigName(), false))); } } diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/SupplyScenarioParametersAction.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/SupplyScenarioParametersAction.java new file mode 100644 index 00000000..6997b496 --- /dev/null +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/SupplyScenarioParametersAction.java @@ -0,0 +1,110 @@ +package org.dcsa.conformance.standards.tnt.action; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import lombok.Getter; +import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.standards.tnt.party.SuppliedScenarioParameters; +import org.dcsa.conformance.standards.tnt.party.TntFilterParameter; + +@Getter +public class SupplyScenarioParametersAction extends ConformanceAction { + private SuppliedScenarioParameters suppliedScenarioParameters = null; + private final LinkedHashSet TntFilterParameters; + + public SupplyScenarioParametersAction( + String publisherPartyName, TntFilterParameter... TntFilterParameters) { + super( + publisherPartyName, + null, + null, + "SupplyScenarioParameters(%s)" + .formatted( + Arrays.stream(TntFilterParameters) + .map(TntFilterParameter::getQueryParamName) + .collect(Collectors.joining(", ")))); + this.TntFilterParameters = + Stream.of(TntFilterParameters).collect(Collectors.toCollection(LinkedHashSet::new)); + } + + @Override + public void reset() { + super.reset(); + suppliedScenarioParameters = null; + } + + @Override + public ObjectNode exportJsonState() { + ObjectNode jsonState = super.exportJsonState(); + if (suppliedScenarioParameters != null) { + jsonState.set("suppliedScenarioParameters", suppliedScenarioParameters.toJson()); + } + return jsonState; + } + + @Override + public void importJsonState(JsonNode jsonState) { + super.importJsonState(jsonState); + if (jsonState.has("suppliedScenarioParameters")) { + suppliedScenarioParameters = + SuppliedScenarioParameters.fromJson(jsonState.required("suppliedScenarioParameters")); + } + } + + @Override + public ObjectNode asJsonNode() { + ObjectNode objectNode = super.asJsonNode(); + ArrayNode jsonTntFilterParameters = objectNode.putArray("tntFilterParametersQueryParamNames"); + TntFilterParameters.forEach( + TntFilterParameter -> jsonTntFilterParameters.add(TntFilterParameter.getQueryParamName())); + return objectNode; + } + + @Override + public String getHumanReadablePrompt() { + return "Use the following format to provide the values of the specified query parameters" + + " for which your party can successfully process a GET request:"; + } + + @Override + public JsonNode getJsonForHumanReadablePrompt() { + return SuppliedScenarioParameters.fromMap( + TntFilterParameters.stream() + .collect( + Collectors.toMap( + Function.identity(), + TntFilterParameter -> + switch (TntFilterParameter) { + case LIMIT -> "100"; + case EVENT_CREATED_DATE_TIME, + EVENT_CREATED_DATE_TIME_EQ, + EVENT_CREATED_DATE_TIME_GT, + EVENT_CREATED_DATE_TIME_GTE, + EVENT_CREATED_DATE_TIME_LT, + EVENT_CREATED_DATE_TIME_LTE -> + ZonedDateTime.now() + .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + default -> "TODO"; + }))) + .toJson(); + } + + @Override + public boolean isInputRequired() { + return true; + } + + @Override + public void handlePartyInput(JsonNode partyInput) { + super.handlePartyInput(partyInput); + suppliedScenarioParameters = SuppliedScenarioParameters.fromJson(partyInput.get("input")); + } +} diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntAction.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntAction.java index 558622ec..d08ccf8d 100644 --- a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntAction.java +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntAction.java @@ -1,8 +1,12 @@ package org.dcsa.conformance.standards.tnt.action; +import java.util.Map; +import java.util.function.Supplier; import org.dcsa.conformance.core.scenario.ConformanceAction; +import org.dcsa.conformance.standards.tnt.party.SuppliedScenarioParameters; public abstract class TntAction extends ConformanceAction { + protected final Supplier sspSupplier; protected final int expectedStatus; public TntAction( @@ -12,6 +16,15 @@ public TntAction( String actionTitle, int expectedStatus) { super(sourcePartyName, targetPartyName, previousAction, actionTitle); + this.sspSupplier = _getSspSupplier(previousAction); this.expectedStatus = expectedStatus; } + + private Supplier _getSspSupplier(ConformanceAction previousAction) { + return previousAction instanceof SupplyScenarioParametersAction supplyAvailableTdrAction + ? supplyAvailableTdrAction::getSuppliedScenarioParameters + : previousAction == null + ? () -> SuppliedScenarioParameters.fromMap(Map.ofEntries()) + : _getSspSupplier(previousAction.getPreviousAction()); + } } diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntGetEventsAction.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntGetEventsAction.java index 4ec28470..a644cc6d 100644 --- a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntGetEventsAction.java +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/action/TntGetEventsAction.java @@ -1,33 +1,32 @@ package org.dcsa.conformance.standards.tnt.action; -import com.fasterxml.jackson.databind.JsonNode; -import java.util.*; -import java.util.function.Function; +import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.stream.Stream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.check.*; -import org.dcsa.conformance.core.traffic.ConformanceExchange; +import org.dcsa.conformance.core.scenario.ConformanceAction; import org.dcsa.conformance.core.traffic.HttpMessageType; import org.dcsa.conformance.standards.tnt.party.TntRole; @Getter @Slf4j public class TntGetEventsAction extends TntAction { - private final Map eventSchemaValidators; + private final JsonSchemaValidator responseSchemaValidator; public TntGetEventsAction( String subscriberPartyName, String publisherPartyName, - TntAction previousAction, - Map eventSchemaValidators) { + ConformanceAction previousAction, + JsonSchemaValidator responseSchemaValidator) { super(subscriberPartyName, publisherPartyName, previousAction, "GetEvents", 200); - this.eventSchemaValidators = eventSchemaValidators; + this.responseSchemaValidator = responseSchemaValidator; } @Override public String getHumanReadablePrompt() { - return "Send a GET events request"; + return "Send a GET events request with the following parameters: " + + sspSupplier.get().toJson().toPrettyString(); } @Override @@ -38,60 +37,16 @@ protected Stream createSubChecks() { return Stream.of( new UrlPathCheck(TntRole::isSubscriber, getMatchedExchangeUuid(), "/events"), new ResponseStatusCheck(TntRole::isPublisher, getMatchedExchangeUuid(), expectedStatus), - new ActionCheck( - "The HTTP %s matches the standard JSON schema" - .formatted(HttpMessageType.RESPONSE.name().toLowerCase()), + new JsonSchemaCheck( TntRole::isPublisher, getMatchedExchangeUuid(), - HttpMessageType.RESPONSE) { - - private boolean isEventNode(JsonNode jsonNode) { - return jsonNode.isObject() && !jsonNode.path("eventType").isMissingNode(); - } - - private ArrayList _findEventNodes( - ArrayList foundEventNodes, JsonNode searchInJsonNode) { - if (isEventNode(searchInJsonNode)) { - foundEventNodes.add(searchInJsonNode); - } else { - searchInJsonNode.forEach( - elementNode -> _findEventNodes(foundEventNodes, elementNode)); - } - return foundEventNodes; - } - - @Override - protected Set checkConformance(Function getExchangeByUuid) { - ConformanceExchange exchange = getExchangeByUuid.apply(matchedExchangeUuid); - JsonNode jsonResponse = exchange.getMessage(httpMessageType).body().getJsonBody(); - LinkedHashSet validationErrors = new LinkedHashSet<>(); - if (!jsonResponse.isArray()) { - validationErrors.add("The root JSON response must be an array of events"); - } - ArrayList eventNodes = _findEventNodes(new ArrayList<>(), jsonResponse); - int eventCount = eventNodes.size(); - for (int eventIndex = 0; eventIndex < eventCount; ++eventIndex) { - JsonNode eventNode = eventNodes.get(eventIndex); - JsonNode eventTypeNode = eventNode.path("eventType"); - TntEventType eventType; - String eventTypeText = eventTypeNode.asText().toUpperCase(); - try { - eventType = TntEventType.valueOf(eventTypeText); - } catch (RuntimeException e) { - validationErrors.add( - "Event #%d: incorrect eventType attribute: %s" - .formatted(eventIndex, eventTypeNode)); - continue; - } - JsonSchemaValidator eventSchemaValidator = eventSchemaValidators.get(eventType); - for (String validationError : eventSchemaValidator.validate(eventNode)) { - validationErrors.add("Event #%d: %s".formatted(eventIndex, validationError)); - } - } - return validationErrors; - } - }); + HttpMessageType.RESPONSE, + responseSchemaValidator)); } }; } + + public ObjectNode asJsonNode() { + return super.asJsonNode().set("suppliedScenarioParameters", sspSupplier.get().toJson()); + } } diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/SuppliedScenarioParameters.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/SuppliedScenarioParameters.java new file mode 100644 index 00000000..10528ec6 --- /dev/null +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/SuppliedScenarioParameters.java @@ -0,0 +1,44 @@ +package org.dcsa.conformance.standards.tnt.party; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Getter; + +@Getter +public class SuppliedScenarioParameters { + + private final Map map; + + private SuppliedScenarioParameters(Map map) { + this.map = Collections.unmodifiableMap(map); + } + + public ObjectNode toJson() { + ObjectNode objectNode = new ObjectMapper().createObjectNode(); + map.forEach( + (ovsFilterParameter, value) -> + objectNode.put(ovsFilterParameter.getQueryParamName(), value)); + return objectNode; + } + + public static SuppliedScenarioParameters fromMap(Map map) { + return new SuppliedScenarioParameters(map); + } + + public static SuppliedScenarioParameters fromJson(JsonNode jsonNode) { + return new SuppliedScenarioParameters( + Arrays.stream(TntFilterParameter.values()) + .filter(ovsFilterParameter -> jsonNode.has(ovsFilterParameter.getQueryParamName())) + .collect( + Collectors.toUnmodifiableMap( + Function.identity(), + ovsFilterParameter -> + jsonNode.required(ovsFilterParameter.getQueryParamName()).asText()))); + } +} diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntFilterParameter.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntFilterParameter.java new file mode 100644 index 00000000..cd0977e9 --- /dev/null +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntFilterParameter.java @@ -0,0 +1,43 @@ +package org.dcsa.conformance.standards.tnt.party; + +import java.util.Arrays; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import lombok.Getter; + +public enum TntFilterParameter { + EVENT_TYPE("eventType"), + SHIPMENT_EVENT_TYPE_CODE("shipmentEventTypeCode"), + DOCUMENT_TYPE_CODE("documentTypeCode"), + CARRIER_BOOKING_REFERENCE("carrierBookingReference"), + TRANSPORT_DOCUMENT_REFERENCE("transportDocumentReference"), + TRANSPORT_EVENT_TYPE_CODE("transportEventTypeCode"), + TRANSPORT_CALL_ID("transportCallID"), + VESSEL_IMO_NUMBER("vesselIMONumber"), + EXPORT_VOYAGE_NUMBER("exportVoyageNumber"), + CARRIER_SERVICE_CODE("carrierServiceCode"), + UN_LOCATION_CODE("UNLocationCode"), + EQUIPMENT_EVENT_TYPE_CODE("equipmentEventTypeCode"), + EQUIPMENT_REFERENCE("equipmentReference"), + EVENT_CREATED_DATE_TIME("eventCreatedDateTime"), + EVENT_CREATED_DATE_TIME_GTE("eventCreatedDateTime:gte"), + EVENT_CREATED_DATE_TIME_GT("eventCreatedDateTime:gt"), + EVENT_CREATED_DATE_TIME_LTE("eventCreatedDateTime:lte"), + EVENT_CREATED_DATE_TIME_LT("eventCreatedDateTime:lt"), + EVENT_CREATED_DATE_TIME_EQ("eventCreatedDateTime:eq"), + LIMIT("limit"), + ; + + public static final Map byQueryParamName = + Arrays.stream(values()) + .collect( + Collectors.toUnmodifiableMap( + TntFilterParameter::getQueryParamName, Function.identity())); + + @Getter private final String queryParamName; + + TntFilterParameter(String queryParamName) { + this.queryParamName = queryParamName; + } +} diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntPublisher.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntPublisher.java index d32b9fa9..5e352cf2 100644 --- a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntPublisher.java +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntPublisher.java @@ -1,9 +1,15 @@ package org.dcsa.conformance.standards.tnt.party; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.party.ConformanceParty; import org.dcsa.conformance.core.party.CounterpartConfiguration; @@ -15,6 +21,7 @@ import org.dcsa.conformance.core.traffic.ConformanceMessageBody; import org.dcsa.conformance.core.traffic.ConformanceRequest; import org.dcsa.conformance.core.traffic.ConformanceResponse; +import org.dcsa.conformance.standards.tnt.action.SupplyScenarioParametersAction; @Slf4j public class TntPublisher extends ConformanceParty { @@ -46,7 +53,63 @@ protected void doReset() {} @Override protected Map, Consumer> getActionPromptHandlers() { - return Map.ofEntries(); + return Map.ofEntries( + Map.entry(SupplyScenarioParametersAction.class, this::supplyScenarioParameters)); + } + + private void supplyScenarioParameters(JsonNode actionPrompt) { + log.info("TntPublisher.supplyScenarioParameters(%s)".formatted(actionPrompt.toPrettyString())); + + SuppliedScenarioParameters responseSsp = + SuppliedScenarioParameters.fromMap( + StreamSupport.stream( + actionPrompt.required("tntFilterParametersQueryParamNames").spliterator(), + false) + .map( + jsonOvsFilterParameter -> + TntFilterParameter.byQueryParamName.get(jsonOvsFilterParameter.asText())) + .collect( + Collectors.toMap( + Function.identity(), + tntFilterParameter -> + switch (tntFilterParameter) { + case EVENT_TYPE -> "SHIPMENT,EQUIPMENT"; + case SHIPMENT_EVENT_TYPE_CODE -> + "RECE,DRFT,PENA,PENU,REJE,APPR,ISSU,SURR,SUBM,VOID,CONF,REQS,CMPL,HOLD,RELS"; + case DOCUMENT_TYPE_CODE -> + "CBR,BKG,SHI,SRM,TRD,ARN,VGM,CAS,CUS,DGD,OOG"; + case CARRIER_BOOKING_REFERENCE -> "ABC709951"; + case TRANSPORT_DOCUMENT_REFERENCE -> + UUID.randomUUID().toString().replaceAll("-", "").substring(0, 20); + case TRANSPORT_EVENT_TYPE_CODE -> "ARRI,DEPA"; + case TRANSPORT_CALL_ID -> UUID.randomUUID().toString(); + case VESSEL_IMO_NUMBER -> "9321483"; + case EXPORT_VOYAGE_NUMBER -> "2103S"; + case CARRIER_SERVICE_CODE -> "FE1"; + case UN_LOCATION_CODE -> "FRPAR"; + case EQUIPMENT_EVENT_TYPE_CODE -> + "LOAD,DISC,GTIN,GTOT,STUF,STRP,PICK,DROP,INSP,RSEA,RMVD"; + case EQUIPMENT_REFERENCE -> "APZU4812090"; + case EVENT_CREATED_DATE_TIME, + EVENT_CREATED_DATE_TIME_GTE, + EVENT_CREATED_DATE_TIME_GT, + EVENT_CREATED_DATE_TIME_LTE, + EVENT_CREATED_DATE_TIME_LT, + EVENT_CREATED_DATE_TIME_EQ -> + ZonedDateTime.now() + .format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); + case LIMIT -> "100"; + }))); + + asyncOrchestratorPostPartyInput( + new ObjectMapper() + .createObjectNode() + .put("actionId", actionPrompt.required("actionId").asText()) + .set("input", responseSsp.toJson())); + + addOperatorLogEntry( + "Submitting SuppliedScenarioParameters: %s" + .formatted(responseSsp.toJson().toPrettyString())); } @Override @@ -55,7 +118,7 @@ public ConformanceResponse handleRequest(ConformanceRequest request) { JsonNode jsonResponseBody = JsonToolkit.templateFileToJsonNode( - "/standards/tnt/messages/tnt-v22-response.json", Map.ofEntries()); + "/standards/tnt/messages/tnt-220-response.json", Map.ofEntries()); return request.createResponse( 200, diff --git a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntSubscriber.java b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntSubscriber.java index ac6ac9e8..10fefa03 100644 --- a/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntSubscriber.java +++ b/tnt/src/main/java/org/dcsa/conformance/standards/tnt/party/TntSubscriber.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.*; import java.util.function.Consumer; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.party.ConformanceParty; import org.dcsa.conformance.core.party.CounterpartConfiguration; @@ -50,11 +51,19 @@ protected Map, Consumer> getActionP private void getEvents(JsonNode actionPrompt) { log.info("TntSubscriber.getEvents(%s)".formatted(actionPrompt.toPrettyString())); + SuppliedScenarioParameters ssp = + SuppliedScenarioParameters.fromJson(actionPrompt.get("suppliedScenarioParameters")); syncCounterpartGet( - "/%s/events".formatted(apiVersion.startsWith("2") ? "v2" : "v3"), Map.ofEntries()); + "/v2/events", + ssp.getMap().entrySet().stream() + .collect( + Collectors.toMap( + entry -> entry.getKey().getQueryParamName(), + entry -> Set.of(entry.getValue())))); - addOperatorLogEntry("Sent GET events request"); + addOperatorLogEntry( + "Sent GET events request with parameters %s".formatted(ssp.toJson().toPrettyString())); } @Override diff --git a/tnt/src/main/resources/standards/tnt/messages/tnt-v22-response.json b/tnt/src/main/resources/standards/tnt/messages/tnt-220-response.json similarity index 100% rename from tnt/src/main/resources/standards/tnt/messages/tnt-v22-response.json rename to tnt/src/main/resources/standards/tnt/messages/tnt-220-response.json diff --git a/tnt/src/main/resources/standards/tnt/schemas/tnt-v22-publisher.json b/tnt/src/main/resources/standards/tnt/schemas/tnt-220-publisher.json similarity index 100% rename from tnt/src/main/resources/standards/tnt/schemas/tnt-v22-publisher.json rename to tnt/src/main/resources/standards/tnt/schemas/tnt-220-publisher.json From 762a0d80d708bf6f52a1d4d647b50878ed5d5aa9 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:27:14 +0100 Subject: [PATCH 09/14] Replacing OVS 300b2 with 300 final --- .../dcsa/conformance/standards/ovs/OvsComponentFactory.java | 2 +- .../{ovs-300beta2-response.json => ovs-300-response.json} | 0 .../{ovs-300beta2-publisher.json => ovs-300-publisher.json} | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) rename ovs/src/main/resources/standards/ovs/messages/{ovs-300beta2-response.json => ovs-300-response.json} (100%) rename ovs/src/main/resources/standards/ovs/schemas/{ovs-300beta2-publisher.json => ovs-300-publisher.json} (99%) diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java index 08aebfcd..2ae74b60 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java @@ -22,7 +22,7 @@ public class OvsComponentFactory extends AbstractComponentFactory { public static final String STANDARD_NAME = "OVS"; - public static final List STANDARD_VERSIONS = List.of("3.0.0-Beta1", "3.0.0-Beta2"); + public static final List STANDARD_VERSIONS = List.of("3.0.0", "3.0.0-Beta1"); private static final String PUBLISHER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); private static final String SUBSCRIBER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); diff --git a/ovs/src/main/resources/standards/ovs/messages/ovs-300beta2-response.json b/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json similarity index 100% rename from ovs/src/main/resources/standards/ovs/messages/ovs-300beta2-response.json rename to ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json diff --git a/ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json b/ovs/src/main/resources/standards/ovs/schemas/ovs-300-publisher.json similarity index 99% rename from ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json rename to ovs/src/main/resources/standards/ovs/schemas/ovs-300-publisher.json index c85900eb..5f7031bb 100644 --- a/ovs/src/main/resources/standards/ovs/schemas/ovs-300beta2-publisher.json +++ b/ovs/src/main/resources/standards/ovs/schemas/ovs-300-publisher.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "Operational Vessel Schedules", - "description": "API specification issued by DCSA.org\r\n\r\nThis API supports OVS (Operational Vessel Schedules)\r\n\r\nThe Interface Standards for OVS and other documentation can be found on the [DCSA Website](https://dcsa.org/standards/operational-vessel-schedules/).\r\n\r\nFor explanation to specific values or objects please refer to the [Information Model](https://dcsa.org/wp-content/uploads/2024/01/DCSA-Information-Model-2023.Q4.pdf).\r\n\r\n### Stats API\r\nThe Stats API offers crucial statistical information for both API providers and consumers to enhance their services and helps DCSA to understand and scale the ecosystem. We expect you to invoke the Stats API for every request made to the Operational Vessel Schedules API. Further details can be found [here](https://developer.dcsa.org/#/http/guides/api-guides/stats-api)\r\n\r\nFor a changelog please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/ovs/v3#v300B2). Please [create a GitHub issue](https://github.com/dcsaorg/DCSA-OpenAPI/issues/new)", + "description": "API specification issued by DCSA.org\r\n\r\nThis API supports OVS (Operational Vessel Schedules)\r\n\r\nThe Interface Standards for OVS and other documentation can be found on the [DCSA Website](https://dcsa.org/standards/operational-vessel-schedules/).\r\n\r\nFor explanation to specific values or objects please refer to the [Information Model](https://dcsa.org/wp-content/uploads/2024/01/DCSA-Information-Model-2023.Q4.pdf).\r\n\r\n### Stats API\r\nThe Stats API offers crucial statistical information for both API providers and consumers to enhance their services and helps DCSA to understand and scale the ecosystem. We expect you to invoke the Stats API for every request made to the Operational Vessel Schedules API. Further details can be found [here](https://developer.dcsa.org/#/http/guides/api-guides/stats-api)\r\n\r\nFor a changelog please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/blob/master/ovs/v3#v300). Please [create a GitHub issue](https://github.com/dcsaorg/DCSA-OpenAPI/issues/new)", "contact": { "name": "Digital Container Shipping Association (DCSA)", "url": "https://dcsa.org", @@ -12,7 +12,7 @@ "name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html" }, - "version": "3.0.0-Beta-2" + "version": "3.0.0" }, "servers": [ { @@ -356,7 +356,7 @@ "vesselOperatorSMDGLinerCode": { "maxLength": 10, "type": "string", - "description": "The carrier who is in charge of the vessel operation based on the SMDG code.\r\nIf not available at the moment of sharing the schedule, use TBD (To be defined).\r\nIn the case an operator is still using a code not from SMDG, use the available code.", + "description": "The carrier who is in charge of the vessel operation based on the SMDG code.\r\nIf not available at the moment of sharing the schedule, use `TBD` (To be defined).\r\nIn the case an operator is still using a code not from SMDG, use the available code.", "example": "HLC" }, "vesselIMONumber": { From 5cf6301097eb14f7b6bda27b11eef09de827fdf4 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:43:10 +0100 Subject: [PATCH 10/14] Updated OVS 3.0.0 scenario list --- .../standards/ovs/OvsComponentFactory.java | 2 +- .../standards/ovs/OvsScenarioListBuilder.java | 63 +++++++------------ 2 files changed, 25 insertions(+), 40 deletions(-) diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java index 2ae74b60..2d1fb938 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsComponentFactory.java @@ -22,7 +22,7 @@ public class OvsComponentFactory extends AbstractComponentFactory { public static final String STANDARD_NAME = "OVS"; - public static final List STANDARD_VERSIONS = List.of("3.0.0", "3.0.0-Beta1"); + public static final List STANDARD_VERSIONS = List.of("3.0.0"); private static final String PUBLISHER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); private static final String SUBSCRIBER_AUTH_HEADER_VALUE = UUID.randomUUID().toString(); diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java index 9d82d8da..59d2e485 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/OvsScenarioListBuilder.java @@ -23,33 +23,27 @@ public static OvsScenarioListBuilder buildTree( threadLocalComponentFactory.set(componentFactory); threadLocalPublisherPartyName.set(publisherPartyName); threadLocalSubscriberPartyName.set(subscriberPartyName); - if ("3.0.0-Beta1".equals(componentFactory.getStandardVersion())) { - return noAction().then(getSchedules()); - } else { - return noAction() - .thenEither( - scenarioWithFilterBy(CARRIER_SERVICE_NAME), - scenarioWithFilterByDatesAnd(CARRIER_SERVICE_NAME), - scenarioWithFilterBy(CARRIER_SERVICE_CODE), - scenarioWithFilterByDatesAnd(CARRIER_SERVICE_CODE), - scenarioWithFilterBy(UNIVERSAL_SERVICE_REFERENCE), - scenarioWithFilterByDatesAnd(UNIVERSAL_SERVICE_REFERENCE), - scenarioWithFilterBy(VESSEL_IMO_NUMBER), - scenarioWithFilterByDatesAnd(VESSEL_IMO_NUMBER), - scenarioWithFilterBy(VESSEL_NAME), - scenarioWithFilterByDatesAnd(VESSEL_NAME), - scenarioWithFilterBy(CARRIER_VOYAGE_NUMBER), - scenarioWithFilterByDatesAnd(CARRIER_VOYAGE_NUMBER), - scenarioWithFilterBy(UNIVERSAL_VOYAGE_REFERENCE), - scenarioWithFilterByDatesAnd(UNIVERSAL_VOYAGE_REFERENCE), - scenarioWithFilterBy(UN_LOCATION_CODE), - scenarioWithFilterByDatesAnd(UN_LOCATION_CODE), - scenarioWithFilterBy(FACILITY_SMDG_CODE), - scenarioWithFilterByDatesAnd(FACILITY_SMDG_CODE), - scenarioWithFilterBy(START_DATE), - scenarioWithFilterBy(END_DATE), - scenarioWithFilterBy(START_DATE, END_DATE)); - } + return noAction() + .thenEither( + scenarioWithParameters(CARRIER_SERVICE_CODE), + scenarioWithParameters(CARRIER_SERVICE_CODE, LIMIT), + scenarioWithParameters(UNIVERSAL_SERVICE_REFERENCE), + scenarioWithParameters(UNIVERSAL_SERVICE_REFERENCE, LIMIT), + scenarioWithParameters(VESSEL_IMO_NUMBER), + scenarioWithParameters(VESSEL_IMO_NUMBER, LIMIT), + scenarioWithParameters(UN_LOCATION_CODE), + scenarioWithParameters(UN_LOCATION_CODE, LIMIT), + scenarioWithParameters(UN_LOCATION_CODE, FACILITY_SMDG_CODE), + scenarioWithParameters(UN_LOCATION_CODE, FACILITY_SMDG_CODE, LIMIT), + scenarioWithParameters(CARRIER_VOYAGE_NUMBER, CARRIER_SERVICE_CODE), + scenarioWithParameters(CARRIER_VOYAGE_NUMBER, CARRIER_SERVICE_CODE, LIMIT), + scenarioWithParameters(CARRIER_VOYAGE_NUMBER, UNIVERSAL_SERVICE_REFERENCE), + scenarioWithParameters(CARRIER_VOYAGE_NUMBER, UNIVERSAL_SERVICE_REFERENCE, LIMIT), + scenarioWithParameters(UNIVERSAL_VOYAGE_REFERENCE, CARRIER_SERVICE_CODE), + scenarioWithParameters(UNIVERSAL_VOYAGE_REFERENCE, CARRIER_SERVICE_CODE, LIMIT), + scenarioWithParameters(UNIVERSAL_VOYAGE_REFERENCE, UNIVERSAL_SERVICE_REFERENCE), + scenarioWithParameters( + UNIVERSAL_VOYAGE_REFERENCE, UNIVERSAL_SERVICE_REFERENCE, LIMIT)); } private OvsScenarioListBuilder(Function actionBuilder) { @@ -60,18 +54,9 @@ private static OvsScenarioListBuilder noAction() { return new OvsScenarioListBuilder(null); } - private static OvsScenarioListBuilder scenarioWithFilterBy(OvsFilterParameter parameter1) { - return supplyScenarioParameters(LIMIT, parameter1).then(getSchedules()); - } - - private static OvsScenarioListBuilder scenarioWithFilterByDatesAnd( - OvsFilterParameter parameter1) { - return supplyScenarioParameters(LIMIT, START_DATE, END_DATE, parameter1).then(getSchedules()); - } - - private static OvsScenarioListBuilder scenarioWithFilterBy( - OvsFilterParameter parameter1, OvsFilterParameter parameter2) { - return supplyScenarioParameters(LIMIT, parameter1, parameter2).then(getSchedules()); + private static OvsScenarioListBuilder scenarioWithParameters( + OvsFilterParameter... ovsFilterParameters) { + return supplyScenarioParameters(ovsFilterParameters).then(getSchedules()); } private static OvsScenarioListBuilder supplyScenarioParameters( From a8fe9965c57af52e02774948e0eb429c4c4bbe94 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:47:55 +0100 Subject: [PATCH 11/14] Java 21 with no more Content-Length header in HttpClient GET --- booking/pom.xml | 8 ++-- cdk/pom.xml | 15 ++++---- .../conformance/cdk/ConformanceStack.java | 8 ++-- core/pom.xml | 8 ++-- .../MemorySortedPartitionsLockingMap.java | 2 +- ebl-issuance/pom.xml | 8 ++-- ebl-surrender/pom.xml | 8 ++-- ebl/pom.xml | 8 ++-- lambda/pom.xml | 10 ++--- ovs/pom.xml | 8 ++-- sandbox/pom.xml | 10 ++--- .../sandbox/ConformanceSandbox.java | 37 ++++++++++++------- .../DynamoDbSortedPartitionsLockingMap.java | 22 +++++------ spring-boot/pom.xml | 10 ++--- .../springboot/ConformanceApplication.java | 1 + tnt/pom.xml | 8 ++-- webui/src/app/app.component.html | 2 +- .../app/pages/report/report.component.html | 10 ++++- 18 files changed, 101 insertions(+), 82 deletions(-) diff --git a/booking/pom.xml b/booking/pom.xml index c876c993..0f2662c2 100644 --- a/booking/pom.xml +++ b/booking/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-booking DCSA Conformance Booking - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/cdk/pom.xml b/cdk/pom.xml index 1c6442d3..64276e6a 100644 --- a/cdk/pom.xml +++ b/cdk/pom.xml @@ -9,7 +9,6 @@ DCSA Conformance CDK UTF-8 - 2.94.0 5.10.0 @@ -19,8 +18,8 @@ maven-compiler-plugin 3.10.1 - 17 - 17 + 21 + 21 @@ -37,29 +36,29 @@ software.amazon.awscdk aws-cdk-lib - ${cdk.version} + 2.130.0 software.amazon.awscdk apigatewayv2-authorizers-alpha - 2.94.0-alpha.0 + 2.114.0-alpha.0 software.amazon.awscdk apigatewayv2-integrations-alpha - 2.94.0-alpha.0 + 2.114.0-alpha.0 software.amazon.awsconstructs cloudfronts3 - 2.42.0 + 2.53.0 software.constructs constructs - 10.2.70 + 10.3.0 diff --git a/cdk/src/main/java/org/dcsa/conformance/cdk/ConformanceStack.java b/cdk/src/main/java/org/dcsa/conformance/cdk/ConformanceStack.java index cbbb9dda..031387f3 100644 --- a/cdk/src/main/java/org/dcsa/conformance/cdk/ConformanceStack.java +++ b/cdk/src/main/java/org/dcsa/conformance/cdk/ConformanceStack.java @@ -115,7 +115,7 @@ public ConformanceStack( "/bin/sh", "-c", "mvn clean install && cp /asset-input/target/conformance-lambda.jar /asset-output/")) - .image(Runtime.JAVA_17.getBundlingImage()) + .image(Runtime.JAVA_21.getBundlingImage()) .volumes( singletonList( // Mount local .m2 repo to avoid download all the @@ -135,7 +135,7 @@ public ConformanceStack( prefix + "SandboxTaskLambda", FunctionProps.builder() .functionName(prefix + "SandboxTaskLambda") - .runtime(Runtime.JAVA_17) + .runtime(Runtime.JAVA_21) .code(assetCode) .handler("org.dcsa.conformance.lambda.SandboxTaskLambda") .memorySize(1024) @@ -150,7 +150,7 @@ public ConformanceStack( prefix + "ApiLambda", FunctionProps.builder() .functionName(prefix + "ApiLambda") - .runtime(Runtime.JAVA_17) + .runtime(Runtime.JAVA_21) .code(assetCode) .handler("org.dcsa.conformance.lambda.ApiLambda") .memorySize(1024) @@ -165,7 +165,7 @@ public ConformanceStack( prefix + "WebuiLambda", FunctionProps.builder() .functionName(prefix + "WebuiLambda") - .runtime(Runtime.JAVA_17) + .runtime(Runtime.JAVA_21) .code(assetCode) .handler("org.dcsa.conformance.lambda.WebuiLambda") .memorySize(1024) diff --git a/core/pom.xml b/core/pom.xml index c2cced78..ec73c9c9 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -8,16 +8,16 @@ dcsa-conformance-core DCSA Conformance Core - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/core/src/main/java/org/dcsa/conformance/core/state/MemorySortedPartitionsLockingMap.java b/core/src/main/java/org/dcsa/conformance/core/state/MemorySortedPartitionsLockingMap.java index 3ef34d9d..7c0a022b 100644 --- a/core/src/main/java/org/dcsa/conformance/core/state/MemorySortedPartitionsLockingMap.java +++ b/core/src/main/java/org/dcsa/conformance/core/state/MemorySortedPartitionsLockingMap.java @@ -19,7 +19,7 @@ private static class MemoryMapItem { private final HashMap> memoryMap = new HashMap<>(); public MemorySortedPartitionsLockingMap() { - super(5 * 1000, 100, 10 * 1000); + super(60 * 1000, 100, 60 * 1000); } private MemoryMapItem _getOrCreateItem(String partitionKey, String sortKey) { diff --git a/ebl-issuance/pom.xml b/ebl-issuance/pom.xml index 50480944..0a21a176 100644 --- a/ebl-issuance/pom.xml +++ b/ebl-issuance/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-ebl-issuance DCSA Conformance eBL Issuance - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/ebl-surrender/pom.xml b/ebl-surrender/pom.xml index 0e5f0439..20bfeb2f 100644 --- a/ebl-surrender/pom.xml +++ b/ebl-surrender/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-ebl-surrender DCSA Conformance eBL Surrender - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/ebl/pom.xml b/ebl/pom.xml index ce411acf..4295fea2 100644 --- a/ebl/pom.xml +++ b/ebl/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-ebl DCSA Conformance eBL - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/lambda/pom.xml b/lambda/pom.xml index 79a53908..8af2d7b2 100644 --- a/lambda/pom.xml +++ b/lambda/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-lambda DCSA Conformance Lambda - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -47,7 +47,7 @@ com.amazonaws aws-java-sdk-lambda - 1.12.552 + 1.12.665 @@ -65,7 +65,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/ovs/pom.xml b/ovs/pom.xml index 65c3eb4b..e779cc57 100644 --- a/ovs/pom.xml +++ b/ovs/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-ovs DCSA Conformance OVS - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/sandbox/pom.xml b/sandbox/pom.xml index b0cec891..d3f2a774 100644 --- a/sandbox/pom.xml +++ b/sandbox/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-sandbox DCSA Conformance Sandbox - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -53,7 +53,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true @@ -61,7 +61,7 @@ software.amazon.awssdk dynamodb - 2.20.149 + 2.24.9 diff --git a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java index 58114bbb..0149a544 100644 --- a/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java +++ b/sandbox/src/main/java/org/dcsa/conformance/sandbox/ConformanceSandbox.java @@ -543,7 +543,8 @@ public static void executeDeferredTask( } catch (Exception e) { log.error( "Deferred task execution failed: %s" - .formatted(jsonNode == null ? null : jsonNode.toPrettyString()), e); + .formatted(jsonNode == null ? null : jsonNode.toPrettyString()), + e); } } @@ -553,21 +554,30 @@ private static ConformanceResponse _syncHttpRequest(ConformanceRequest conforman log.info( "ConformanceSandbox.syncHttpRequest(%s) request: %s" .formatted(uri, conformanceRequest.toJson().toPrettyString())); + HttpRequest.Builder httpRequestBuilder = - HttpRequest.newBuilder() - .uri(uri) - .method( - conformanceRequest.method(), - HttpRequest.BodyPublishers.ofString( - conformanceRequest.message().body().getStringBody())) - .timeout(Duration.ofHours(1)); + "GET".equals(conformanceRequest.method()) + ? HttpRequest.newBuilder().uri(uri).GET() + : HttpRequest.newBuilder() + .uri(uri) + .method( + conformanceRequest.method(), + HttpRequest.BodyPublishers.ofString( + conformanceRequest.message().body().getStringBody())); + + httpRequestBuilder.timeout(Duration.ofHours(1)); + conformanceRequest .message() .headers() .forEach((name, values) -> values.forEach(value -> httpRequestBuilder.header(name, value))); - HttpResponse httpResponse = - HttpClient.newHttpClient() - .send(httpRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + + HttpResponse httpResponse; + try (HttpClient httpClient = + HttpClient.newBuilder().followRedirects(HttpClient.Redirect.NORMAL).build()) { + httpResponse = + httpClient.send(httpRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + } ConformanceResponse conformanceResponse = conformanceRequest.createResponse( httpResponse.statusCode(), @@ -622,8 +632,9 @@ private static void _syncSendOutboundWebRequest(ConformanceWebRequest conformanc .headers() .forEach( (name, values) -> values.forEach(value -> httpRequestBuilder.header(name, value))); - HttpClient.newHttpClient() - .send(httpRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + try (HttpClient httpClient = HttpClient.newHttpClient()) { + httpClient.send(httpRequestBuilder.build(), HttpResponse.BodyHandlers.ofString()); + } } catch (Exception e) { log.error( "Failed to send outbound request: %s" diff --git a/sandbox/src/main/java/org/dcsa/conformance/sandbox/state/DynamoDbSortedPartitionsLockingMap.java b/sandbox/src/main/java/org/dcsa/conformance/sandbox/state/DynamoDbSortedPartitionsLockingMap.java index ed8fa9db..03868511 100644 --- a/sandbox/src/main/java/org/dcsa/conformance/sandbox/state/DynamoDbSortedPartitionsLockingMap.java +++ b/sandbox/src/main/java/org/dcsa/conformance/sandbox/state/DynamoDbSortedPartitionsLockingMap.java @@ -21,7 +21,7 @@ public class DynamoDbSortedPartitionsLockingMap extends SortedPartitionsLockingM private final String tableName; public DynamoDbSortedPartitionsLockingMap(DynamoDbClient dynamoDbClient, String tableName) { - super(5 * 1000, 500, 10 * 1000); + super(60 * 1000, 500, 60 * 1000); this.dynamoDbClient = dynamoDbClient; this.tableName = tableName; } @@ -37,7 +37,7 @@ protected void _saveItem(String lockedBy, String partitionKey, String sortKey, J Map.entry("PK", AttributeValue.fromS(partitionKey)), Map.entry("SK", AttributeValue.fromS(sortKey))); try { - retryTransactWriteItems( + _retryTransactWriteItems( TransactWriteItemsRequest.builder() .transactItems( TransactWriteItem.builder() @@ -107,7 +107,7 @@ private void _createOrLockItem( try { String lockedUntil = Instant.ofEpochMilli(Instant.now().toEpochMilli() + lockDurationMillis).toString(); - retryTransactWriteItems( + _retryTransactWriteItems( TransactWriteItemsRequest.builder() .transactItems( TransactWriteItem.builder() @@ -126,7 +126,7 @@ private void _createOrLockItem( .build()); } catch (TransactionCanceledException transactionCanceledException) { if ("ConditionalCheckFailed" - .equals(transactionCanceledException.cancellationReasons().get(0).code())) { + .equals(transactionCanceledException.cancellationReasons().getFirst().code())) { log.debug( "CCF _createOrLockItem(LB=%s, PK=%s, SK=%s, ...)" .formatted(lockedBy, partitionKey, sortKey)); @@ -160,7 +160,7 @@ private void _lockExistingItem( String newLockedUntil = Instant.ofEpochMilli(Instant.now().toEpochMilli() + lockDurationMillis).toString(); try { - retryTransactWriteItems( + _retryTransactWriteItems( TransactWriteItemsRequest.builder() .transactItems( TransactWriteItem.builder() @@ -185,7 +185,7 @@ private void _lockExistingItem( .build()); } catch (TransactionCanceledException transactionCanceledException) { if ("ConditionalCheckFailed" - .equals(transactionCanceledException.cancellationReasons().get(0).code())) { + .equals(transactionCanceledException.cancellationReasons().getFirst().code())) { log.warn( "CCF _lockExistingItem(LB=%s, PK=%s, SK=%s, ...)" .formatted(lockedBy, partitionKey, sortKey)); @@ -226,7 +226,7 @@ private JsonNode _loadLockedItem( .build()) .build()) .responses() - .get(0) + .getFirst() .item() .get("value"), AttributeValue.fromS("{}")) @@ -261,7 +261,7 @@ protected void _unlockItem(String lockedBy, String partitionKey, String sortKey) Map.entry("PK", AttributeValue.fromS(partitionKey)), Map.entry("SK", AttributeValue.fromS(sortKey))); try { - retryTransactWriteItems( + _retryTransactWriteItems( TransactWriteItemsRequest.builder() .transactItems( TransactWriteItem.builder() @@ -321,12 +321,12 @@ private TransactGetItemsResponse retryTransactGetItems( latestTransactionCanceledException); } - private TransactWriteItemsResponse retryTransactWriteItems( - TransactWriteItemsRequest transactWriteItemsRequest) { + private void _retryTransactWriteItems(TransactWriteItemsRequest transactWriteItemsRequest) { TransactionCanceledException latestTransactionCanceledException = null; for (int retriesLeft = MAX_TRANSACTION_RETRY_COUNT - 1; retriesLeft >= 0; --retriesLeft) { try { - return dynamoDbClient.transactWriteItems(transactWriteItemsRequest); + dynamoDbClient.transactWriteItems(transactWriteItemsRequest); + return; } catch (TransactionCanceledException transactionCanceledException) { if (transactionCanceledException.cancellationReasons().stream() .noneMatch(reason -> "TransactionConflict".equals(reason.code()))) { diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml index 8e476b2f..1ed3cfeb 100644 --- a/spring-boot/pom.xml +++ b/spring-boot/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.1.1 + 3.2.3 org.dcsa.conformance @@ -14,15 +14,15 @@ dcsa-conformance-spring-boot DCSA Conformance Spring Boot - 17 + 21 2022.0.3 - 17 - 17 + 21 + 21 2.19.0 UTF-8 - + org.springframework.boot spring-boot-starter-actuator diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java index b924420f..a9a8b335 100644 --- a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java @@ -339,6 +339,7 @@ private String _buildHomeSandboxSection(String sandboxId) { } public static void main(String[] args) { +// System.setProperty("javax.net.debug", "ssl:all"); SpringApplication.run(ConformanceApplication.class, args); } } diff --git a/tnt/pom.xml b/tnt/pom.xml index bbea2071..b45cc3ef 100644 --- a/tnt/pom.xml +++ b/tnt/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-tnt DCSA Conformance TnT - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true diff --git a/webui/src/app/app.component.html b/webui/src/app/app.component.html index f8c22760..aff61a3a 100644 --- a/webui/src/app/app.component.html +++ b/webui/src/app/app.component.html @@ -1,6 +1,6 @@
-
+
diff --git a/webui/src/app/pages/report/report.component.html b/webui/src/app/pages/report/report.component.html index a3f2a8ea..5e8de1cd 100644 --- a/webui/src/app/pages/report/report.component.html +++ b/webui/src/app/pages/report/report.component.html @@ -3,9 +3,17 @@ class="conformanceStatus" [title]="getConformanceStatusTitle(report.status)" >{{getConformanceStatusEmoji(report.status)}} - + {{report.title}} +
+ 🚫 + {{errorMessage}} +
+
Date: Mon, 26 Feb 2024 17:00:24 +0100 Subject: [PATCH 12/14] Also using Java 21 in the pipeline --- Dockerfile | 6 +++--- .../conformance/springboot/ConformanceApplication.java | 10 ++++++++-- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index 763f1584..e2438f30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM maven:3.8.5-openjdk-17-slim as builder +FROM maven:3.9.6-eclipse-temurin-21-alpine as builder WORKDIR /build @@ -6,11 +6,11 @@ COPY . . RUN mvn install -DskipTests -U -B -FROM openjdk:17-alpine3.14 +FROM eclipse-temurin:21 EXPOSE 8080 VOLUME /tmp COPY --from=builder /build/spring-boot/target/*.jar app.jar -ENTRYPOINT ["java","-jar","/app.jar"] +ENTRYPOINT ["java", "-jar", "/app.jar", "ExitOnStartup"] diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java index a9a8b335..22bf87d5 100644 --- a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java @@ -35,6 +35,7 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.bind.annotation.*; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; @@ -339,7 +340,12 @@ private String _buildHomeSandboxSection(String sandboxId) { } public static void main(String[] args) { -// System.setProperty("javax.net.debug", "ssl:all"); - SpringApplication.run(ConformanceApplication.class, args); + // System.setProperty("javax.net.debug", "ssl:all"); + ConfigurableApplicationContext applicationContext = + SpringApplication.run(ConformanceApplication.class, args); + if (List.of(args).contains("ExitOnStartup")) { + System.out.println("ExitOnStartup requested; exiting..."); + applicationContext.close(); + } } } From cf5fe4a9365828e1a74cc653738a62deed563853 Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 26 Feb 2024 17:59:11 +0100 Subject: [PATCH 13/14] Java 21 in one more place in the pipeline --- .github/workflows/master.yml | 2 +- Dockerfile | 2 +- .../conformance/springboot/ConformanceApplication.java | 8 +------- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index c6c06198..a3eff303 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -26,7 +26,7 @@ jobs: uses: actions/setup-java@v3 with: distribution: 'temurin' - java-version: 17 + java-version: 21 cache: 'maven' - name: Build Conformance-Gateway diff --git a/Dockerfile b/Dockerfile index e2438f30..06dddf01 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,4 +13,4 @@ EXPOSE 8080 VOLUME /tmp COPY --from=builder /build/spring-boot/target/*.jar app.jar -ENTRYPOINT ["java", "-jar", "/app.jar", "ExitOnStartup"] +ENTRYPOINT ["java", "-jar", "/app.jar"] diff --git a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java index 22bf87d5..3b543724 100644 --- a/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java +++ b/spring-boot/src/main/java/org/dcsa/conformance/springboot/ConformanceApplication.java @@ -35,7 +35,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.ConfigurationPropertiesScan; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.web.bind.annotation.*; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; @@ -341,11 +340,6 @@ private String _buildHomeSandboxSection(String sandboxId) { public static void main(String[] args) { // System.setProperty("javax.net.debug", "ssl:all"); - ConfigurableApplicationContext applicationContext = - SpringApplication.run(ConformanceApplication.class, args); - if (List.of(args).contains("ExitOnStartup")) { - System.out.println("ExitOnStartup requested; exiting..."); - applicationContext.close(); - } + SpringApplication.run(ConformanceApplication.class, args); } } From c78ca69c6ba893cda2f57f74e7b50b5a5fa8f62e Mon Sep 17 00:00:00 2001 From: gj0dcsa <135594855+gj0dcsa@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:29:50 +0100 Subject: [PATCH 14/14] Using Java 21 for PINT as well --- pint/pom.xml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pint/pom.xml b/pint/pom.xml index 1b94c0e7..e3a07d74 100644 --- a/pint/pom.xml +++ b/pint/pom.xml @@ -8,9 +8,9 @@ dcsa-conformance-ebl-interop DCSA Conformance eBL Interop - 17 - 17 - 17 + 21 + 21 + 21 UTF-8 @@ -23,7 +23,7 @@ org.projectlombok lombok - 1.18.28 + 1.18.30 true