diff --git a/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java b/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java index 75062c54..ef02e31a 100644 --- a/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java +++ b/booking/src/main/java/org/dcsa/conformance/standards/booking/checks/BookingChecks.java @@ -508,6 +508,34 @@ private static JsonNode getShipmentLocationTypeCode(JsonNode body, @NonNull Stri return issues; }); + static final JsonContentCheck CHECK_CARGO_GROSS_WEIGHT_CONDITIONS = + JsonAttribute.allIndividualMatchesMustBeValid( + "Check Cargo Gross Weight conditions", + mav -> mav.submitAllMatching("requestedEquipments.*"), + (nodeToValidate, contextPath) -> { + var issues = new LinkedHashSet(); + var cargoGrossWeight = nodeToValidate.path("cargoGrossWeight"); + if (cargoGrossWeight.isMissingNode() || cargoGrossWeight.isNull()) { + var commodities = nodeToValidate.path("commodities"); + if (!(commodities.isMissingNode() || commodities.isNull()) && commodities.isArray()) { + AtomicInteger commodityCounter = new AtomicInteger(0); + StreamSupport.stream(commodities.spliterator(), false) + .forEach( + commodity -> { + var commodityGrossWeight = commodity.path("cargoGrossWeight"); + int currentCommodityCount = commodityCounter.getAndIncrement(); + if (commodityGrossWeight.isMissingNode() + || commodityGrossWeight.isNull()) { + issues.add( + "The '%s' must have cargo gross weight at commodities position %s" + .formatted(contextPath, currentCommodityCount)); + } + }); + } + } + return issues; + }); + private static Supplier delayedValue(Supplier cspSupplier, Function field) { return () -> { var csp = cspSupplier.get(); @@ -611,6 +639,7 @@ private static void generateScenarioRelatedChecks(List checks, VALIDATE_SHIPPER_MINIMUM_REQUEST_FIELDS, VALIDATE_DOCUMENT_PARTY, NATIONAL_COMMODITY_TYPE_CODE_VALIDATION, + CHECK_CARGO_GROSS_WEIGHT_CONDITIONS, JsonAttribute.atLeastOneOf( JsonPointer.compile("/expectedDepartureDate"), JsonPointer.compile("/expectedArrivalAtPlaceOfDeliveryStartDate"), diff --git a/booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java b/booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java new file mode 100644 index 00000000..676850f2 --- /dev/null +++ b/booking/src/test/java/org/dcsa/conformance/standards/booking/checks/BookingChecksTest.java @@ -0,0 +1,96 @@ +package org.dcsa.conformance.standards.booking.checks; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.dcsa.conformance.core.check.JsonContentCheck; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BookingChecksTest { + + private ObjectNode booking; + private ObjectNode requestedEquipment; + private ArrayNode requestedEquipments; + private ObjectNode commodity; + private ArrayNode commodities; + + @BeforeEach + void setUp() { + booking = OBJECT_MAPPER.createObjectNode(); + requestedEquipment = OBJECT_MAPPER.createObjectNode(); + requestedEquipments = OBJECT_MAPPER.createArrayNode(); + commodity = OBJECT_MAPPER.createObjectNode(); + commodities = OBJECT_MAPPER.createArrayNode(); + } + + @Test + void testCargoGrossWeightPresentAtRequestedEquipment_valid() { + requestedEquipment.set("cargoGrossWeight", OBJECT_MAPPER.createObjectNode()); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set error = check.validate(booking); + assertTrue(error.isEmpty()); + } + + @Test + void testCargoGrossWeightMissingAtRequestedEquipmentNoCommodities_valid() { + + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertTrue(errors.isEmpty()); + } + + @Test + void testCargoGrossWeightMissingAtRequestedEquipmentPresentAtCommodities_valid() { + commodity.set("cargoGrossWeight", OBJECT_MAPPER.createObjectNode()); + commodities.add(commodity); + requestedEquipment.set("commodities", commodities); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertTrue(errors.isEmpty()); + } + + @Test + void testCargoGrossWeightMissingInRequestedEquipmentsAndAlsoInCommodities_invalid() { + + commodities.add(commodity); + requestedEquipment.set("commodities", commodities); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertEquals(1, errors.size()); + assertTrue( + errors.contains( + "The 'requestedEquipments[0]' must have cargo gross weight at commodities position 0")); + } + + @Test + void testCargoGrossWeightMissingInRequestedEquipmentsAndInOneOfTheCommodities_invalid() { + + ObjectNode commodity2 = OBJECT_MAPPER.createObjectNode(); + commodity.set("cargoGrossWeight", OBJECT_MAPPER.createObjectNode()); + commodities.add(commodity2); + commodities.add(commodity); + requestedEquipment.set("commodities", commodities); + requestedEquipments.add(requestedEquipment); + booking.set("requestedEquipments", requestedEquipments); + JsonContentCheck check = BookingChecks.CHECK_CARGO_GROSS_WEIGHT_CONDITIONS; + Set errors = check.validate(booking); + assertEquals(1, errors.size()); + assertTrue( + errors.contains( + "The 'requestedEquipments[0]' must have cargo gross weight at commodities position 0")); + } +} diff --git a/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml b/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml index ff1f1115..b8a459f6 100644 --- a/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml +++ b/ebl-issuance/src/main/resources/standards/eblissuance/schemas/EBL_ISS_v3.0.0.yaml @@ -6,11 +6,11 @@ info: version: 3.0.0 title: DCSA eBL Issuance API description: | -

DCSA OpenAPI specification for the issuance of an electronic Bill of Lading (eBL) via an eBL Solution Provider

+

DCSA OpenAPI specification for the issuance of an electronic Bill of Lading (referred to as `eBL`) via an eBL Solution Provider

- This API is intended as an API between a carrier and a EBL Solution Platform. + This API is intended as an API between a carrier and a eBL Solution Platform. - The EBL Solution Provider is to implement + The eBL Solution Provider is to implement PUT /v3/ebl-issuance-requests @@ -18,9 +18,9 @@ info: POST /v3/ebl-issuance-responses - for the EBL Solution Provider to call. + for the eBL Solution Provider to call. - When the document is to be surrendered, it should happen via a version of the [DCSA EBL Surrender](https://app.swaggerhub.com/apis-docs/dcsaorg/DCSA_EBL_SUR/3.0.0) API. + When the document is to be surrendered, it should happen via a version of the [DCSA eBL Surrender](https://app.swaggerhub.com/apis-docs/dcsaorg/DCSA_EBL_SUR/3.0.0) API. ### API Design & Implementation Principles This API follows the guidelines defined in version 2.1 of the API Design & Implementation Principles which can be found on the [DCSA Developer page](https://developer.dcsa.org/api_design) @@ -29,20 +29,20 @@ info: Please look at the following implementation guide for how to create [Digital Signatures](https://developer.dcsa.org/implementing-digital-signatures-for-transport-documents). ### Changelog and GitHub - For a changelog, please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/tree/master/ebl/v3/issuance#v300). Please [create a GitHub issue](https://github.com/dcsaorg/DCSA-OpenAPI/issues/new) if you have any questions/comments. + For a changelog, please click [here](https://github.com/dcsaorg/DCSA-OpenAPI/tree/master/ebl/v3/issuance#v300). If you have any questions, feel free to [Contact Us](https://dcsa.org/get-involved/contact-us). API specification issued by [DCSA.org](https://dcsa.org/). license: name: Apache 2.0 - url: 'http://www.apache.org/licenses/LICENSE-2.0.html' + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' contact: name: Digital Container Shipping Association (DCSA) url: 'https://dcsa.org' email: info@dcsa.org tags: - - name: Issuance EBL + - name: Issuance eBL description: | - Issuance EBL **implemented** by EBL Solution Platform + Issuance eBL **implemented** by eBL Solution Platform - name: Issuance Response description: | Issuance Response **implemented** by carrier @@ -50,13 +50,13 @@ paths: /v3/ebl-issuance-requests: put: tags: - - Issuance EBL + - Issuance eBL operationId: put-ebl-issuance-requests - summary: Request issuance of an EBL + summary: Request issuance of an eBL description: | - Submit a transport document (EBL) for issuance + Submit a transport document (eBL) for issuance - **This endPoint is to be implemented by an EBL Solution Provider for the carrier to call** + **This endPoint is to be implemented by an eBL Solution Provider for the carrier to call** parameters: - $ref: '#/components/parameters/Api-Version-Major' requestBody: @@ -78,7 +78,7 @@ paths: description: | An Issuance Request is made with a `Transport Document Reference` (TDR), that was used previously to request the issuance of a `Transport Document` (TD). The document is either already issued or an TD with the same TDR. - The eBL platform will inform the carrier when the carrier needs to act on this document again. If the issuance is pending, then the carrier will be notified via the DCSA_EBL_ISS_RSP API once the issuance process completes. If the issuance has already succeeded, the eBL platform will notify the carrier when there is a surrender request via the DCSA_EBL_SUR API. + The eBL platform will inform the carrier when the carrier needs to act on this document again. If the issuance is pending, then the carrier will be notified via the `/v3/ebl-issuance-responses` endPoint once the issuance process completes. If the issuance has already succeeded, the eBL platform will notify the carrier when there is a surrender request via the DCSA_EBL_SUR API. headers: API-Version: $ref: '#/components/headers/API-Version' @@ -124,7 +124,7 @@ paths: description: | Submit a response to a carrier issuance request. - **This endPoint is to be implemented by a carrier for the EBL Solution Provider to call** + **This endPoint is to be implemented by a carrier for the eBL Solution Provider to call** parameters: - $ref: '#/components/parameters/Api-Version-Major' requestBody: @@ -402,7 +402,7 @@ components: pattern: ^\S+$ maxLength: 4 description: | - The EBL platform of the transaction party. + The eBL platform of the transaction party. The value **MUST** be one of: - `WAVE` (Wave) - `CARX` (CargoX) @@ -3235,7 +3235,7 @@ components: issuanceResponseCode: type: string description: | - The platforms verdict on the issuance of the EBL identified by the `transportDocumentReference` + The platforms verdict on the issuance of the eBL identified by the `transportDocumentReference` Options are: - `ISSU`: The document was successfully `ISSU` and successfully delivered to the initial possessor. @@ -3274,7 +3274,7 @@ components: description: | An object to capture where the original Transport Document (`Bill of Lading`) will be issued. - The location can be specified as one of `UN Location Code` or `CountryCode`. + **Condition:** The location can be specified as one of `UN Location Code` or `CountryCode`, but not both. properties: locationName: type: string @@ -3450,7 +3450,7 @@ components: pattern: ^\S(?:.*\S)?$ maxLength: 35 description: | - Reference number assigned to an `Import License` or permit, issued by countries exercising import controls that athorizes the importation of the articles stated in the license. The `Import License` must be valid at time of arrival. + Reference number assigned to an `Import License` or permit, issued by countries exercising import controls that authorizes the importation of the articles stated in the license. The `Import License` must be valid at time of arrival. example: EMC007123 issueDate: type: string diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java index 5c846917..451529ce 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/checks/EBLChecks.java @@ -5,6 +5,8 @@ import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.EXEMPT_PACKAGE_CODES; import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.MODE_OF_TRANSPORT; import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.NATIONAL_COMMODITY_CODES; +import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.PARTY_FUNCTION_CODE; +import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.PARTY_FUNCTION_CODE_HBL; import static org.dcsa.conformance.standards.ebl.checks.EblDatasets.REQUESTED_CARRIER_CLAUSES; @@ -666,6 +668,19 @@ private static Consumer allDg(Consumer mav.submitAllMatching("documentParties.other.*.partyFunction"), + JsonAttribute.matchedMustBeDatasetKeywordIfPresent(PARTY_FUNCTION_CODE)); + + static final JsonRebaseableContentCheck VALID_PARTY_FUNCTION_HBL = + JsonAttribute.allIndividualMatchesMustBeValid( + "The partyFunction in OtherDocumentParty of houseBillOfLadings is valid", + mav -> + mav.submitAllMatching("houseBillOfLadings.*.documentParties.other.*.partyFunction"), + JsonAttribute.matchedMustBeDatasetKeywordIfPresent(PARTY_FUNCTION_CODE_HBL)); + private static final List STATIC_SI_CHECKS = Arrays.asList( JsonAttribute.mustBeDatasetKeywordIfPresent( SI_REQUEST_SEND_TO_PLATFORM, @@ -684,6 +699,8 @@ private static Consumer allDg(Consumer allDg(Consumer "SHA256withECDSA"; + case RSAPrivateKey ignored -> "SHA256withRSA"; + default -> throw new UnsupportedOperationException("Unsupported key"); + }; + final ContentSigner signer = new JcaContentSignerBuilder(algo).build(keyPair.getPrivate()); + return certBuilder.build(signer); + } catch (Exception e) { + throw new UserFacingException("Error while generating a self-certificate: " + e.getMessage(), e); + } + } } diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java deleted file mode 100644 index 546b5c20..00000000 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/RSAPayloadSigner.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.dcsa.conformance.standards.ebl.crypto.impl; - -import org.dcsa.conformance.standards.ebl.crypto.JWSSignerDetails; -import org.dcsa.conformance.standards.ebl.crypto.PayloadSignerWithKey; - -import java.security.interfaces.RSAPublicKey; - -import static org.dcsa.conformance.standards.ebl.crypto.PayloadSignerFactory.pemEncodeKey; - -public class RSAPayloadSigner extends DefaultPayloadSigner implements PayloadSignerWithKey { - - private final RSAPublicKey rsaPublicKey; - - public RSAPayloadSigner(JWSSignerDetails jwsSignerDetails, RSAPublicKey rsaPublicKey) { - super(jwsSignerDetails); - this.rsaPublicKey = rsaPublicKey; - } - - @Override - public String getPublicKeyInPemFormat() { - return pemEncodeKey(this.rsaPublicKey); - } -} diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java new file mode 100644 index 00000000..4953b8f1 --- /dev/null +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/crypto/impl/X509BackedPayloadSigner.java @@ -0,0 +1,22 @@ +package org.dcsa.conformance.standards.ebl.crypto.impl; + +import org.bouncycastle.cert.X509CertificateHolder; +import org.dcsa.conformance.standards.ebl.crypto.JWSSignerDetails; +import org.dcsa.conformance.standards.ebl.crypto.PayloadSignerWithKey; + +import static org.dcsa.conformance.standards.ebl.crypto.PayloadSignerFactory.pemEncodeCertificate; + +public class X509BackedPayloadSigner extends DefaultPayloadSigner implements PayloadSignerWithKey { + + private final X509CertificateHolder x509Cert; + + public X509BackedPayloadSigner(JWSSignerDetails jwsSignerDetails, X509CertificateHolder x509Cert) { + super(jwsSignerDetails); + this.x509Cert = x509Cert; + } + + @Override + public String getPublicKeyInPemFormat() { + return pemEncodeCertificate(this.x509Cert); + } +} diff --git a/ebl/src/main/resources/standards/ebl/messages/ebl-api-3.0.0-request.json b/ebl/src/main/resources/standards/ebl/messages/ebl-api-3.0.0-request.json index 373a3a11..2d6a407e 100644 --- a/ebl/src/main/resources/standards/ebl/messages/ebl-api-3.0.0-request.json +++ b/ebl/src/main/resources/standards/ebl/messages/ebl-api-3.0.0-request.json @@ -148,7 +148,7 @@ }, "issueTo": { "partyName": "DCSA CTK", - "identifyingCodes" : [ + "identifyingCodes": [ { "codeListProvider": "W3C", "codeListName": "LCL", diff --git a/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml b/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml index 2fa70d4d..f1e99802 100644 --- a/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml +++ b/ebl/src/main/resources/standards/ebl/schemas/EBL_v3.0.0.yaml @@ -8511,7 +8511,7 @@ components: ########################################## DocumentPartiesShippingInstructions: type: object - title: Document Parties + title: Document Parties (Shipping Instructions) description: | All `Parties` with associated roles. @@ -8553,7 +8553,7 @@ components: ############################ DocumentPartiesHouseBL: type: object - title: Document Parties + title: Document Parties (House B/L) description: | All `Parties` with associated roles for this `House Bill of Lading`. diff --git a/ebl/src/test/java/org/dcsa/conformance/standards/ebl/checks/EBLChecksTest.java b/ebl/src/test/java/org/dcsa/conformance/standards/ebl/checks/EBLChecksTest.java index 3bf8914e..59241b4b 100644 --- a/ebl/src/test/java/org/dcsa/conformance/standards/ebl/checks/EBLChecksTest.java +++ b/ebl/src/test/java/org/dcsa/conformance/standards/ebl/checks/EBLChecksTest.java @@ -19,6 +19,8 @@ import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.LOCATION_NAME_CONDITIONAL_VALIDATION_POA; import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.LOCATION_NAME_CONDITIONAL_VALIDATION_POFD; import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.VALID_CONSIGMENT_ITEMS_REFERENCE_TYPES; +import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.VALID_PARTY_FUNCTION; +import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.VALID_PARTY_FUNCTION_HBL; import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.VALID_REQUESTED_CARRIER_CLAUSES; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -295,4 +297,36 @@ void testValidConsignmentItemsReferenceTypes() { Set invalidErrors = VALID_CONSIGMENT_ITEMS_REFERENCE_TYPES.validate(rootNode); assertEquals(1, invalidErrors.size()); } + + @Test + void testValidPartyFunction() { + ObjectNode documentParties = rootNode.putObject("documentParties"); + ArrayNode otherParties = documentParties.putArray("other"); + ObjectNode otherParty = otherParties.addObject(); + otherParty.put("partyFunction", "SCO"); + + Set errors = VALID_PARTY_FUNCTION.validate(rootNode); + assertEquals(0, errors.size()); + + otherParty.put("partyFunction", "SSS"); + errors = VALID_PARTY_FUNCTION.validate(rootNode); + assertEquals(1, errors.size()); + } + + @Test + void testValidPartyFunctionHBL() { + ArrayNode houseBillOfLadings = rootNode.putArray("houseBillOfLadings"); + ObjectNode hbl = houseBillOfLadings.addObject(); + ObjectNode documentParties = hbl.putObject("documentParties"); + ArrayNode otherParties = documentParties.putArray("other"); + ObjectNode otherParty = otherParties.addObject(); + otherParty.put("partyFunction", "CS"); + + Set errors = VALID_PARTY_FUNCTION_HBL.validate(rootNode); + assertEquals(0, errors.size()); + + otherParty.put("partyFunction", "SSS"); + errors = VALID_PARTY_FUNCTION_HBL.validate(rootNode); + assertEquals(1, errors.size()); + } }