diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonAttribute.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonAttribute.java index 24987c15..50d66306 100644 --- a/core/src/main/java/org/dcsa/conformance/core/check/JsonAttribute.java +++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonAttribute.java @@ -10,6 +10,8 @@ public class JsonAttribute { + private static final BiFunction> EMPTY_VALIDATOR = (ignoredA, ignoredB) -> Set.of(); + public static ActionCheck contentChecks( Predicate isRelevantForRoleName, UUID matchedExchangeUuid, @@ -86,6 +88,45 @@ public static ActionCheck contentChecks( ); } + public static ActionCheck contentChecks( + String title, + Predicate isRelevantForRoleName, + UUID matchedExchangeUuid, + HttpMessageType httpMessageType, + JsonContentCheckRebaser rebaser, + List checks + ) { + return contentChecks( + "", + title, + isRelevantForRoleName, + matchedExchangeUuid, + httpMessageType, + rebaser, + checks + ); + } + + public static ActionCheck contentChecks( + String titlePrefix, + String title, + Predicate isRelevantForRoleName, + UUID matchedExchangeUuid, + HttpMessageType httpMessageType, + JsonContentCheckRebaser rebaser, + List checks + ) { + return new JsonRebaseableAttributeBasedCheck( + titlePrefix, + title, + isRelevantForRoleName, + matchedExchangeUuid, + httpMessageType, + rebaser, + checks + ); + } + public static Predicate isTrue( @NonNull JsonPointer jsonPointer @@ -136,19 +177,18 @@ public static JsonContentMatchedValidation presenceImpliesOtherField( return (JsonNode nodeToValidate, String contextPath) -> { var sourceField = nodeToValidate.path(sourceFieldName); var impliedField = nodeToValidate.path(impliedFieldName); + if (sourceField.isMissingNode() || !impliedField.isMissingNode()) { return Set.of(); } - return Set.of("The field '%s.%s' being present makes '%s.%s' mandatory".formatted( - contextPath, - sourceFieldName, - contextPath, - impliedFieldName + return Set.of("The field '%s' being present makes '%s' mandatory".formatted( + concatContextPath(contextPath, sourceFieldName), + concatContextPath(contextPath, impliedFieldName) )); }; } - public static JsonContentCheck allIndividualMatchesMustBeValid( + public static JsonRebaseableContentCheck allIndividualMatchesMustBeValid( @NonNull String name, @NonNull @@ -156,14 +196,13 @@ public static JsonContentCheck allIndividualMatchesMustBeValid( @NonNull JsonContentMatchedValidation subvalidation ) { - return JsonContentCheckImpl.of( - name, - (body) -> { - var v = new MultiAttributeValidatorImpl(body, subvalidation); - scanner.accept(v); - return v.getValidationIssues(); - } - ); + return new JsonRebaseableCheckImpl( + name, + (body, contextPath) -> { + var v = new MultiAttributeValidatorImpl(contextPath, body, subvalidation); + scanner.accept(v); + return v.getValidationIssues(); + }); } public static JsonContentMatchedValidation unique( @@ -216,7 +255,7 @@ public static JsonContentMatchedValidation unique( public static JsonContentMatchedValidation path(String path, JsonContentMatchedValidation delegate) { return (nodeToValidate, contextPath) -> { - var fullContext = contextPath.isEmpty() ? path : contextPath + "." + path; + var fullContext = concatContextPath(contextPath, path); return delegate.validate(nodeToValidate.path(path), fullContext); }; } @@ -229,46 +268,53 @@ public static JsonContentMatchedValidation at(JsonPointer pointer, JsonContentMa }; } - public static JsonContentCheck mustBeNotNull( + public static String concatContextPath(String contextPath, String nextPathSegment) { + return contextPath.isEmpty() ? nextPathSegment : contextPath + "." + nextPathSegment; + } + + public static JsonRebaseableContentCheck mustBeNotNull( JsonPointer jsonPointer, String reason ) { - return new JsonContentCheckImpl( - jsonCheckName(jsonPointer), - at(jsonPointer, node -> { + return new JsonRebaseableCheckImpl( + jsonCheckName(jsonPointer), + (body, contextPath) -> { + var node = body.at(jsonPointer); if (node.isMissingNode() || node.isNull()) { return Set.of( - "The value of '%s' must present and not null because %s" - .formatted(renderJsonPointer(jsonPointer), reason)); + "The value of '%s' must present and not null because %s" + .formatted(renderJsonPointer(jsonPointer, contextPath), reason)); } return Collections.emptySet(); - } - )); + }); } - public static JsonContentCheck mustEqual( + public static JsonRebaseableContentCheck mustEqual( JsonPointer jsonPointer, String expectedValue) { Objects.requireNonNull( expectedValue, "expectedValue cannot be null; Note: Use `() -> getDspSupplier().get().foo()` (or similar) when testing a value against a dynamic scenario property" ); - return new JsonContentCheckImpl( - "%s: Must equal '%s'".formatted(jsonCheckName(jsonPointer), expectedValue), - at(jsonPointer, node -> { - var actualValue = node.asText(null); - if (!Objects.equals(expectedValue, actualValue)) { - return Set.of( - "The value of '%s' was '%s' instead of '%s'" - .formatted(renderJsonPointer(jsonPointer), renderValue(node), renderValue(expectedValue))); - } - return Collections.emptySet(); - } - )); + return new JsonRebaseableCheckImpl( + "%s: Must equal '%s'".formatted(jsonCheckName(jsonPointer), expectedValue), + (body, contextPath) -> { + var node = body.at(jsonPointer); + var actualValue = node.asText(null); + if (!Objects.equals(expectedValue, actualValue)) { + return Set.of( + "The value of '%s' was '%s' instead of '%s'" + .formatted( + renderJsonPointer(jsonPointer, contextPath), + renderValue(node), + renderValue(expectedValue))); + } + return Collections.emptySet(); + }); } - public static JsonContentCheck mustEqual( + public static JsonRebaseableContentCheck mustEqual( JsonPointer jsonPointer, @NonNull Supplier expectedValueSupplier) { @@ -280,7 +326,7 @@ public static JsonContentCheck mustEqual( } - public static JsonContentCheck mustEqual( + public static JsonRebaseableContentCheck mustEqual( String name, JsonPointer jsonPointer, @NonNull @@ -290,9 +336,10 @@ public static JsonContentCheck mustEqual( if (v != null) { context = ": Must equal '%s'".formatted(v); } - return new JsonContentCheckImpl( + return new JsonRebaseableCheckImpl( name + context, - at(jsonPointer, node -> { + (body, contextPath) -> { + var node = body.at(jsonPointer); var actualValue = node.asText(null); var expectedValue = expectedValueSupplier.get(); if (expectedValue == null) { @@ -303,15 +350,15 @@ public static JsonContentCheck mustEqual( if (!Objects.equals(expectedValue, actualValue)) { return Set.of( "The value of '%s' was '%s' instead of '%s'" - .formatted(renderJsonPointer(jsonPointer), renderValue(node), renderValue(expectedValue))); + .formatted(renderJsonPointer(jsonPointer, contextPath), renderValue(node), renderValue(expectedValue))); } return Collections.emptySet(); } - )); + ); } - public static JsonContentCheck mustEqual( + public static JsonRebaseableContentCheck mustEqual( String name, String path, @NonNull @@ -321,24 +368,26 @@ public static JsonContentCheck mustEqual( if (v != null) { context = ": Must equal '%s'".formatted(v); } - return new JsonContentCheckImpl( + return new JsonRebaseableCheckImpl( name + context, - path(path, node -> { - var actualValue = node.asText(null); - var expectedValue = expectedValueSupplier.get(); - if (expectedValue == null) { - throw new IllegalStateException("The supplier of the expected value for " + path - + " returned `null` and `null` is not supported for equals. Usually this indicates that the dynamic" - + " scenario property was not properly recorded at this stage."); - } - if (!Objects.equals(expectedValue, actualValue)) { - return Set.of( - "The value of '%s' was '%s' instead of '%s'" - .formatted(path, renderValue(node), renderValue(expectedValue))); - } - return Collections.emptySet(); + (body, contextPath) -> { + var node = body.path(path); + var actualValue = node.asText(null); + var nodePath = concatContextPath(contextPath, path); + var expectedValue = expectedValueSupplier.get(); + if (expectedValue == null) { + throw new IllegalStateException("The supplier of the expected value for " + nodePath + + " returned `null` and `null` is not supported for equals. Usually this indicates that the dynamic" + + " scenario property was not properly recorded at this stage."); } - )); + if (!Objects.equals(expectedValue, actualValue)) { + return Set.of( + "The value of '%s' was '%s' instead of '%s'" + .formatted(nodePath, renderValue(node), renderValue(expectedValue))); + } + return Collections.emptySet(); + } + ); } public static JsonContentMatchedValidation matchedMustBePresent() { @@ -370,29 +419,8 @@ public static JsonContentMatchedValidation matchedMustEqual(Supplier exp }; } - public static JsonContentMatchedValidation matchedMustEqualIfPresent(Supplier expectedValueSupplier) { - return (nodeToValidate, contextPath) -> { - if (isJsonNodeAbsent(nodeToValidate)) { - return Set.of(); - } - var actualValue = nodeToValidate.asText(null); - var expectedValue = expectedValueSupplier.get(); - if (expectedValue == null) { - throw new IllegalStateException("The supplier of the expected value for " + contextPath - + " returned `null` and `null` is not supported for equals. Usually this indicates that the dynamic" - + " scenario property was not properly recorded at this stage."); - } - if (!Objects.equals(expectedValue, actualValue)) { - return Set.of( - "The value of '%s' was '%s' instead of '%s'" - .formatted(contextPath, renderValue(nodeToValidate), renderValue(expectedValue))); - } - return Collections.emptySet(); - }; - } - - public static JsonContentCheck mustBePresent(JsonPointer jsonPointer) { - return JsonContentCheckImpl.of(jsonPointer, matchedMustBePresent()); + public static JsonRebaseableContentCheck mustBePresent(JsonPointer jsonPointer) { + return JsonRebaseableCheckImpl.of(jsonPointer, matchedMustBePresent()::validate); } public static JsonContentMatchedValidation matchedMustBeAbsent() { @@ -406,10 +434,10 @@ public static JsonContentMatchedValidation matchedMustBeAbsent() { }; } - public static JsonContentCheck mustBeAbsent( + public static JsonRebaseableContentCheck mustBeAbsent( JsonPointer jsonPointer ) { - return JsonContentCheckImpl.of(jsonPointer, matchedMustBeAbsent()); + return JsonRebaseableCheckImpl.of(jsonPointer, matchedMustBeAbsent()::validate); } public static JsonContentMatchedValidation combine( @@ -442,14 +470,18 @@ public static JsonContentMatchedValidation matchedMustBeDatasetKeywordIfPresent( }; } - public static JsonContentCheck mustBeDatasetKeywordIfPresent( + public static JsonRebaseableContentCheck mustBeDatasetKeywordIfPresent( JsonPointer jsonPointer, KeywordDataset dataset ) { - return JsonContentCheckImpl.of(jsonPointer, matchedMustBeDatasetKeywordIfPresent(dataset)); + return JsonRebaseableCheckImpl.of( + renderJsonPointer(jsonPointer), + jsonPointer, + matchedMustBeDatasetKeywordIfPresent(dataset)::validate + ); } - public static JsonContentCheck atMostOneOf( + public static JsonRebaseableContentCheck atMostOneOf( @NonNull JsonPointer ... ptrs ) { if (ptrs.length < 2) { @@ -460,9 +492,9 @@ public static JsonContentCheck atMostOneOf( .map(JsonAttribute::renderJsonPointer) .collect(Collectors.joining(", ")) ); - return new JsonContentCheckImpl( + return new JsonRebaseableCheckImpl( name, - (body) -> { + (body, contextPath) -> { var present = Arrays.stream(ptrs) .filter(p -> isJsonNodePresent(body.at(p))) .toList(); @@ -472,14 +504,14 @@ public static JsonContentCheck atMostOneOf( return Set.of( "At most one of the following can be present: %s".formatted( present.stream() - .map(JsonAttribute::renderJsonPointer) + .map(ptr -> JsonAttribute.renderJsonPointer(ptr, contextPath)) .collect(Collectors.joining(", " )) )); }); } - public static JsonContentCheck allOrNoneArePresent( + public static JsonRebaseableContentCheck allOrNoneArePresent( @NonNull JsonPointer ... ptrs ) { if (ptrs.length < 2) { @@ -490,30 +522,31 @@ public static JsonContentCheck allOrNoneArePresent( .map(JsonAttribute::renderJsonPointer) .collect(Collectors.joining(", ")) ); - return new JsonContentCheckImpl( - name, - (body) -> { - var firstPtr = ptrs[0]; - var firstNode = body.at(firstPtr); - Predicate check; - if (firstNode.isMissingNode() || firstNode.isNull()) { - check = JsonAttribute::isJsonNodePresent; - } else { - check = JsonAttribute::isJsonNodeAbsent; - } - var conflictingPtr = Arrays.stream(ptrs) - .filter(p -> check.test(body.at(p))) - .findAny() - .orElse(null); - if (conflictingPtr != null) { - return Set.of("'%s' and '%s' must both be present or absent".formatted( - renderJsonPointer(firstPtr), renderJsonPointer(conflictingPtr)) - ); - } - return Set.of(); - }); + return new JsonRebaseableCheckImpl( + name, + (body, contextPath) -> { + var firstPtr = ptrs[0]; + var firstNode = body.at(firstPtr); + Predicate check; + if (firstNode.isMissingNode() || firstNode.isNull()) { + check = JsonAttribute::isJsonNodePresent; + } else { + check = JsonAttribute::isJsonNodeAbsent; + } + var conflictingPtr = + Arrays.stream(ptrs).filter(p -> check.test(body.at(p))).findAny().orElse(null); + if (conflictingPtr != null) { + return Set.of( + "'%s' and '%s' must both be present or absent" + .formatted( + renderJsonPointer(firstPtr, contextPath), + renderJsonPointer(conflictingPtr, contextPath))); + } + return Set.of(); + }); } + @Deprecated public static JsonContentCheck ifThen( @NonNull String name, @@ -532,6 +565,7 @@ public static JsonContentCheck ifThen( }); } + @Deprecated public static JsonContentCheck ifThenElse( @NonNull String name, @@ -552,6 +586,61 @@ public static JsonContentCheck ifThenElse( }); } + public static JsonRebaseableContentCheck ifThen( + @NonNull + String name, + @NonNull + Predicate when, + @NonNull + BiFunction> then + ) { + return ifThenElse(name, when, then, EMPTY_VALIDATOR); + } + + public static JsonRebaseableContentCheck ifThen( + @NonNull + String name, + @NonNull + Predicate when, + @NonNull + JsonRebaseableContentCheck then + ) { + return ifThenElse(name, when, then::validate, EMPTY_VALIDATOR); + } + + public static JsonRebaseableContentCheck ifThenElse( + @NonNull + String name, + @NonNull + Predicate when, + @NonNull + BiFunction> then, + @NonNull + BiFunction> elseCheck + ) { + return new JsonRebaseableCheckImpl( + name, + (body, contextPath) -> { + if (when.test(body)) { + return then.apply(body, contextPath); + } + return elseCheck.apply(body, contextPath); + }); + } + + public static JsonRebaseableContentCheck ifThenElse( + @NonNull + String name, + @NonNull + Predicate when, + @NonNull + JsonRebaseableContentCheck then, + @NonNull + JsonRebaseableContentCheck elseCheck + ) { + return ifThenElse(name, when, (BiFunction>) then::validate, elseCheck::validate); + } + public static JsonContentMatchedValidation ifMatchedThen( @NonNull Predicate when, @@ -589,36 +678,25 @@ public static JsonContentCheck customValidator( return JsonContentCheckImpl.of(description, validator); } - public static JsonContentCheck customValidator( + public static JsonRebaseableContentCheck customValidator( @NonNull String description, @NonNull JsonContentMatchedValidation validator ) { - return JsonContentCheckImpl.of(description, atRoot(validator)); + return new JsonRebaseableCheckImpl(description, validator::validate); } private static Function at(JsonPointer jsonPointer) { return (refNode) -> refNode.at(jsonPointer); } - private static Function path(String path) { - return (refNode) -> refNode.path(path); - } - private static Function> at(JsonPointer jsonPointer, Function> validator) { return at(jsonPointer).andThen(validator); } - private static Function> path(String path, Function> validator) { - return path(path).andThen(validator); - } - private static Function> atMatched(JsonPointer jsonPointer, JsonContentMatchedValidation validator) { return (refNode) -> validator.validate(refNode.at(jsonPointer), renderJsonPointer(jsonPointer)); } - private static Function> atRoot(JsonContentMatchedValidation validator) { - return (refNode) -> validator.validate(refNode, ""); - } static String renderValue(JsonNode node) { if (node == null || node.isMissingNode()) { @@ -638,8 +716,16 @@ static String renderValue(String v) { return v == null ? "(null)" : v; } + static String renderJsonPointer(JsonPointer jsonPointer, String contextPath) { + var pointer = jsonPointer.toString().substring(1).replace("/", "."); + if (contextPath.isEmpty()) { + return pointer; + } + return contextPath + "." + pointer; + } + static String renderJsonPointer(JsonPointer jsonPointer) { - return jsonPointer.toString().substring(1).replace("/", "."); + return renderJsonPointer(jsonPointer, ""); } private static String jsonCheckName(JsonPointer jsonPointer) { @@ -655,6 +741,43 @@ private static boolean isJsonNodeAbsent(JsonNode node) { return !isJsonNodePresent(node); } + static JsonContentCheckRebaser rebaserFor(List paths) { + if (paths.isEmpty()) { + throw new IllegalStateException("No paths"); + } + return jsonContentMatchedValidation -> (node, contextPath) -> { + for (var path : paths) { + node = node.path(path); + contextPath = concatContextPath(contextPath, path); + } + return jsonContentMatchedValidation.validate(node, contextPath); + }; + } + + record JsonRebaseableCheckImpl( + String description, + BiFunction> impl + ) implements JsonRebaseableContentCheck { + @Override + public Set validate(JsonNode body, String contextPath) { + return impl.apply(body, contextPath); + } + + public static JsonRebaseableContentCheck of(JsonPointer jsonPointer, BiFunction> validator) { + return of(jsonCheckName(jsonPointer), jsonPointer, validator); + } + + public static JsonRebaseableContentCheck of(String description, JsonPointer jsonPointer, BiFunction> validator) { + return new JsonRebaseableCheckImpl( + description, + (refNode, context) -> { + var node = refNode.at(jsonPointer); + var path = renderJsonPointer(jsonPointer, context); + return validator.apply(node, path); + } + ); + } + } record JsonContentCheckImpl( @NonNull @@ -671,16 +794,9 @@ private static JsonContentCheck of(String description, Function> impl) { - return new JsonContentCheckImpl(jsonCheckName(pointer), at(pointer, impl)); - } - private static JsonContentCheck of(JsonPointer pointer, JsonContentMatchedValidation impl) { return of(jsonCheckName(pointer), atMatched(pointer, impl)); } - private static JsonContentCheck of(String description, JsonPointer pointer, JsonContentMatchedValidation impl) { - return new JsonContentCheckImpl(description, atMatched(pointer, impl)); - } } } diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonContentCheckRebaser.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonContentCheckRebaser.java new file mode 100644 index 00000000..118ee855 --- /dev/null +++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonContentCheckRebaser.java @@ -0,0 +1,23 @@ +package org.dcsa.conformance.core.check; + +import java.util.List; + +import static org.dcsa.conformance.core.check.JsonAttribute.rebaserFor; + +public interface JsonContentCheckRebaser { + + + default JsonRebaseableContentCheck offset(JsonRebaseableContentCheck jsonRebaseableContentCheck) { + JsonContentMatchedValidation m = offset(jsonRebaseableContentCheck::validate); + return new JsonAttribute.JsonRebaseableCheckImpl( + jsonRebaseableContentCheck.description(), + m::validate + ); + } + + JsonContentMatchedValidation offset(JsonContentMatchedValidation jsonContentMatchedValidation); + + static JsonContentCheckRebaser of(String path) { + return rebaserFor(List.of(path)); + } +} diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonContentMatchedValidation.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonContentMatchedValidation.java index 1c9d5375..527911e4 100644 --- a/core/src/main/java/org/dcsa/conformance/core/check/JsonContentMatchedValidation.java +++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonContentMatchedValidation.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.JsonNode; import java.util.Set; +@FunctionalInterface public interface JsonContentMatchedValidation { /** * @param nodeToValidate The node to validate diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonRebaseableAttributeBasedCheck.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonRebaseableAttributeBasedCheck.java new file mode 100644 index 00000000..0c51096b --- /dev/null +++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonRebaseableAttributeBasedCheck.java @@ -0,0 +1,67 @@ +package org.dcsa.conformance.core.check; + +import com.fasterxml.jackson.databind.JsonNode; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import lombok.NonNull; +import org.dcsa.conformance.core.traffic.ConformanceExchange; +import org.dcsa.conformance.core.traffic.HttpMessageType; + +class JsonRebaseableAttributeBasedCheck extends ActionCheck { + + private final JsonContentCheckRebaser rebaser; + private final List validators; + + JsonRebaseableAttributeBasedCheck( + String titlePrefix, + String title, + Predicate isRelevantForRoleName, + UUID matchedExchangeUuid, + HttpMessageType httpMessageType, + @NonNull JsonContentCheckRebaser rebaser, + @NonNull + List<@NonNull JsonRebaseableContentCheck> validators) { + super(titlePrefix, title, isRelevantForRoleName, matchedExchangeUuid, httpMessageType); + if (validators.isEmpty()) { + throw new IllegalArgumentException("Must have at least one subcheck (validators must be non-empty)"); + } + this.rebaser = rebaser; + this.validators = validators; + } + + @Override + protected final Set checkConformance(Function getExchangeByUuid) { + // All checks are delegated to sub-checks; nothing to do in here. + return Collections.emptySet(); + } + + @Override + protected Stream createSubChecks() { + return this.validators.stream() + .map(validator -> new SingleValidatorCheck(this::isRelevantForRole, matchedExchangeUuid, httpMessageType, rebaser.offset(validator))); + } + + + private static class SingleValidatorCheck extends ActionCheck { + + private final JsonRebaseableContentCheck validator; + + public SingleValidatorCheck(Predicate isRelevantForRoleName, UUID matchedExchangeUuid, HttpMessageType httpMessageType, @NonNull JsonRebaseableContentCheck validator) { + super(validator.description(), isRelevantForRoleName, matchedExchangeUuid, httpMessageType); + this.validator = validator; + } + + @Override + protected Set checkConformance(Function getExchangeByUuid) { + ConformanceExchange exchange = getExchangeByUuid.apply(matchedExchangeUuid); + if (exchange == null) return Collections.emptySet(); + JsonNode jsonBody = exchange.getMessage(httpMessageType).body().getJsonBody(); + return this.validator.validate(jsonBody); + } + } +} diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonRebaseableContentCheck.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonRebaseableContentCheck.java new file mode 100644 index 00000000..166068b0 --- /dev/null +++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonRebaseableContentCheck.java @@ -0,0 +1,20 @@ +package org.dcsa.conformance.core.check; + +import com.fasterxml.jackson.databind.JsonNode; + +import java.util.Set; + +public interface JsonRebaseableContentCheck extends JsonContentCheck { + /** + * @param nodeToValidate The node to validate + * @param contextPath The path to this node, which should be included in any validation errors + * to describe where in the Json tree the error applies. + * @return A set of validation errors (returns the empty set if everything is ok) + */ + Set validate(JsonNode nodeToValidate, String contextPath); + + @Override + default Set validate(JsonNode body) { + return validate(body, ""); + } +} diff --git a/core/src/main/java/org/dcsa/conformance/core/check/MultiAttributeValidatorImpl.java b/core/src/main/java/org/dcsa/conformance/core/check/MultiAttributeValidatorImpl.java index e228be6e..80e18d69 100644 --- a/core/src/main/java/org/dcsa/conformance/core/check/MultiAttributeValidatorImpl.java +++ b/core/src/main/java/org/dcsa/conformance/core/check/MultiAttributeValidatorImpl.java @@ -1,5 +1,6 @@ package org.dcsa.conformance.core.check; +import static org.dcsa.conformance.core.check.JsonAttribute.concatContextPath; import static org.dcsa.conformance.core.check.JsonAttribute.renderJsonPointer; import com.fasterxml.jackson.core.JsonPointer; @@ -16,6 +17,7 @@ @RequiredArgsConstructor class MultiAttributeValidatorImpl implements MultiAttributeValidator { + private final String contextPath; private final JsonNode body; private final JsonContentMatchedValidation validation; @@ -47,7 +49,7 @@ public AttributePathBuilder path(String path) { } private void validateAll(List matches) { - matches.stream().map(m -> validation.validate(m.node, m.render())) + matches.stream().map(m -> validation.validate(m.node, concatContextPath(contextPath, m.render()))) .filter(s -> !s.isEmpty()) .forEach(validationIssues::addAll); } diff --git a/ebl-issuance/pom.xml b/ebl-issuance/pom.xml index 0a21a176..fb06674e 100644 --- a/ebl-issuance/pom.xml +++ b/ebl-issuance/pom.xml @@ -19,7 +19,11 @@ core 0.0.1-SNAPSHOT - + + org.dcsa.conformance + ebl + 0.0.1-SNAPSHOT + org.projectlombok lombok diff --git a/ebl-issuance/src/main/resources/standards/eblissuance/messages/eblissuance-v30-request.json b/ebl-issuance/src/main/resources/standards/eblissuance/messages/eblissuance-v30-request.json index 9d8df87e..c747d6d7 100644 --- a/ebl-issuance/src/main/resources/standards/eblissuance/messages/eblissuance-v30-request.json +++ b/ebl-issuance/src/main/resources/standards/eblissuance/messages/eblissuance-v30-request.json @@ -2,10 +2,10 @@ "document": { "transportDocumentReference": "TRANSPORT_DOCUMENT_REFERENCE_PLACEHOLDER", "shippingInstructionsReference": "SHIPPING_INSTRUCTION_REFERENCE_PLACEHOLDER", - "transportDocumentTypeCode": "SWB", + "transportDocumentTypeCode": "BOL", "freightPaymentTermCode": "PRE", "isElectronic": true, - "isToOrder": true, + "isToOrder": false, "invoicePayableAt": { "locationType": "UNCO", "UNLocationCode": "DKAAR" @@ -29,6 +29,26 @@ }, "partyFunction": "OS", "isToBeNotified": false + }, + { + "party": { + "partyName": "CONSIGNEE_NAME_PLACEHOLDER", + "partyCodes": [ + { + "partyCode": "CONSIGNEE_PARTY_CODE_PLACEHOLDER", + "codeListProvider": "EPUI", + "codeListName": "CONSIGNEE_CODE_LIST_NAME_PLACEHOLDER" + } + ], + "partyContactDetails": [ + { + "name": "DCSA test person", + "email": "no-reply@dcsa.example.org" + } + ] + }, + "partyFunction": "CN", + "isToBeNotified": false } ], "consignmentItems": [ diff --git a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/Shipper_GetTransportDocumentAction.java b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/Shipper_GetTransportDocumentAction.java index d9ce8603..4dc5944f 100644 --- a/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/Shipper_GetTransportDocumentAction.java +++ b/ebl/src/main/java/org/dcsa/conformance/standards/ebl/action/Shipper_GetTransportDocumentAction.java @@ -65,7 +65,7 @@ protected Stream createSubChecks() { getMatchedExchangeUuid(), HttpMessageType.RESPONSE, responseSchemaValidator), - EBLChecks.tdContentChecks(getMatchedExchangeUuid(), expectedTdStatus, getCspSupplier(), getDspSupplier())); + EBLChecks.tdPlusScenarioContentChecks(getMatchedExchangeUuid(), expectedTdStatus, getCspSupplier(), getDspSupplier())); } }; } 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 99bda0f5..d1cea195 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 @@ -1,5 +1,7 @@ package org.dcsa.conformance.standards.ebl.checks; +import static org.dcsa.conformance.core.check.JsonAttribute.concatContextPath; + import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.JsonNode; import java.util.*; @@ -42,7 +44,7 @@ public class EBLChecks { JsonPointer.compile("/transports/onwardInlandRouting/UNLocationCode"), }; - private static final JsonContentCheck ONLY_EBLS_CAN_BE_NEGOTIABLE = JsonAttribute.ifThen( + private static final JsonRebaseableContentCheck ONLY_EBLS_CAN_BE_NEGOTIABLE = JsonAttribute.ifThen( "Validate transportDocumentTypeCode vs. isToOrder", JsonAttribute.isTrue(JsonPointer.compile("/isToOrder")), JsonAttribute.mustEqual(JsonPointer.compile("/transportDocumentTypeCode"), "BOL") @@ -54,7 +56,7 @@ public class EBLChecks { mav.submitAllMatching("consignmentItems.*.references.*.type"); }; - private static final JsonContentCheck VALID_REFERENCE_TYPES = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck VALID_REFERENCE_TYPES = JsonAttribute.allIndividualMatchesMustBeValid( "All reference 'type' fields must be valid", ALL_REFERENCE_TYPES, JsonAttribute.matchedMustBeDatasetKeywordIfPresent(EblDatasets.REFERENCE_TYPE) @@ -79,7 +81,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( }; } - private static final JsonContentCheck TLR_CC_T_COMBINATION_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck TLR_CC_T_COMBINATION_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid( "Validate combination of 'countryCode' and 'type' in 'taxAndLegalReferences'", (mav) -> { mav.submitAllMatching("issuingParty.taxLegalReferences.*"); @@ -88,7 +90,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( combineAndValidateAgainstDataset(EblDatasets.LTR_CC_T_COMBINATIONS, "countryCode", "type") ); - private static final JsonContentCheck TLR_CC_T_COMBINATION_UNIQUE = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck TLR_CC_T_COMBINATION_UNIQUE = JsonAttribute.allIndividualMatchesMustBeValid( "Each document party can be used at most once", (mav) -> { mav.submitAllMatching("issuingParty.taxLegalReferences"); @@ -120,7 +122,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( return !norNode.asBoolean(false); }; - private static final JsonContentCheck ISO_EQUIPMENT_CODE_IMPLIES_REEFER = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck ISO_EQUIPMENT_CODE_IMPLIES_REEFER = JsonAttribute.allIndividualMatchesMustBeValid( "All utilizedTransportEquipments with a reefer ISO Equipment Code must have at least isNonOperatingReefer", ALL_UTE, JsonAttribute.ifMatchedThen( @@ -136,7 +138,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( ) ); - private static final JsonContentCheck NOR_PLUS_ISO_CODE_IMPLIES_ACTIVE_REEFER = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck NOR_PLUS_ISO_CODE_IMPLIES_ACTIVE_REEFER = JsonAttribute.allIndividualMatchesMustBeValid( "All utilizedTransportEquipments where 'isNonOperatingReefer' is 'false' must have 'activeReeferSettings'", ALL_UTE, JsonAttribute.ifMatchedThen( @@ -145,7 +147,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( ) ); - private static final JsonContentCheck NOR_IS_TRUE_IMPLIES_NO_ACTIVE_REEFER = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck NOR_IS_TRUE_IMPLIES_NO_ACTIVE_REEFER = JsonAttribute.allIndividualMatchesMustBeValid( "All utilizedTransportEquipments where 'isNonOperatingReefer' is 'true' cannot have 'activeReeferSettings'", ALL_UTE, JsonAttribute.ifMatchedThen( @@ -155,7 +157,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( ); private static final Consumer ALL_AMF = (mav) -> mav.submitAllMatching("advanceManifestFilings.*"); - private static final JsonContentCheck AMF_SELF_FILER_CODE_CONDITIONALLY_MANDATORY = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck AMF_SELF_FILER_CODE_CONDITIONALLY_MANDATORY = JsonAttribute.allIndividualMatchesMustBeValid( "Validate conditionally mandatory 'selfFilerCode' in 'advanceManifestFilings'", ALL_AMF, (nodeToValidate, contextPath) -> { @@ -174,7 +176,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( return Set.of(); }); - private static final JsonContentCheck AMF_CC_MTC_COMBINATION_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck AMF_CC_MTC_COMBINATION_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid( "Validate combination of 'countryCode' and 'manifestTypeCode' in 'advanceManifestFilings'", ALL_AMF, combineAndValidateAgainstDataset(EblDatasets.AMF_CC_MTC_COMBINATIONS, "countryCode", "manifestTypeCode") @@ -187,13 +189,13 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( mav.submitAllMatching("utilizedTransportEquipments.*.customsReferences.*"); }; - private static final JsonContentCheck CR_CC_T_COMBINATION_KNOWN = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck CR_CC_T_COMBINATION_KNOWN = JsonAttribute.allIndividualMatchesMustBeValid( "The combination of 'countryCode' and 'type' in 'customsReferences' must be valid", ALL_CUSTOMS_REFERENCES, combineAndValidateAgainstDataset(EblDatasets.CUSTOMS_REFERENCE_CC_RTC_COMBINATIONS, "countryCode", "type") ); - private static final JsonContentCheck CR_CC_T_CODES_UNIQUE = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck CR_CC_T_CODES_UNIQUE = JsonAttribute.allIndividualMatchesMustBeValid( "The combination of 'countryCode' and 'type' in '*.customsReferences' must be unique", (mav) -> { mav.submitAllMatching("customsReferences"); @@ -204,7 +206,7 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( JsonAttribute.unique("countryCode", "type") ); - private static final JsonContentCheck COUNTRY_CODE_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck COUNTRY_CODE_VALIDATIONS = JsonAttribute.allIndividualMatchesMustBeValid( "Validate field is a known ISO 3166 alpha 2 code", (mav) -> { mav.submitAllMatching("advancedManifestFilings.*.countryCode"); @@ -218,22 +220,22 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( JsonAttribute.matchedMustBeDatasetKeywordIfPresent(EblDatasets.ISO_3166_ALPHA2_COUNTRY_CODES) ); - private static final JsonContentCheck OUTER_PACKAGING_CODE_IS_VALID = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck OUTER_PACKAGING_CODE_IS_VALID = JsonAttribute.allIndividualMatchesMustBeValid( "Validate that 'packagingCode' is a known code", (mav) -> mav.submitAllMatching("consignmentItems.*.cargoItems.*.outerPackaging.packageCode"), JsonAttribute.matchedMustBeDatasetKeywordIfPresent(EblDatasets.OUTER_PACKAGING_CODE) ); - private static final JsonContentCheck DOCUMENT_PARTY_FUNCTIONS_MUST_BE_UNIQUE = JsonAttribute.customValidator( + private static final JsonRebaseableContentCheck DOCUMENT_PARTY_FUNCTIONS_MUST_BE_UNIQUE = JsonAttribute.customValidator( "Each document party can be used at most once", JsonAttribute.path( "documentParties", JsonAttribute.unique("partyFunction") )); - private static final JsonContentCheck VALIDATE_DOCUMENT_PARTIES = JsonAttribute.customValidator( + private static final JsonRebaseableContentCheck VALIDATE_DOCUMENT_PARTIES = JsonAttribute.customValidator( "Validate documentParties", - body -> { + (body, contextPath) -> { var issues = new LinkedHashSet(); var documentParties = body.path("documentParties"); var isToOrder = body.path("isToOrder").asBoolean(false); @@ -244,28 +246,32 @@ private static JsonContentMatchedValidation combineAndValidateAgainstDataset( .collect(Collectors.toSet()); if (!partyFunctions.contains("OS")) { - issues.add("The 'OS' party is mandatory in the eBL phase (SI/TD)"); + var documentPartiesPath = concatContextPath(contextPath, "documentParties"); + issues.add("The 'OS' party is mandatory in the eBL phase at '%s' (SI/TD)".formatted(documentPartiesPath)); } + var isToOrderPath = concatContextPath(contextPath, "isToOrder"); if (isToOrder) { if (partyFunctions.contains("CN")) { - issues.add("The 'CN' party cannot be used when 'isToOrder' is true (use 'END' instead)"); + issues.add("The 'CN' party cannot be used when '%s' is true (use 'END' instead)".formatted(isToOrderPath)); } } else { if (!partyFunctions.contains("CN")) { - issues.add("The 'CN' party is mandatory when 'isToOrder' is false"); + issues.add("The 'CN' party is mandatory when '%s' is false".formatted(isToOrderPath)); } if (partyFunctions.contains("END")) { - issues.add("The 'END' party cannot be used when 'isToOrder' is false"); + issues.add("The 'END' party cannot be used when '%s' is false".formatted(isToOrderPath)); } } if (!partyFunctions.contains("SCO")) { if (!body.path("serviceContractReference").isTextual()) { - issues.add("The 'SCO' party is mandatory when 'serviceContractReference' is absent"); + var scrPath = concatContextPath(contextPath, "serviceContractReference"); + issues.add("The 'SCO' party is mandatory when '%s' is absent".formatted(scrPath)); } if (!body.path("contractQuotationReference").isTextual()) { - issues.add("The 'SCO' party is mandatory when 'contractQuotationReference' is absent"); + var cqrPath = concatContextPath(contextPath, "contractQuotationReference"); + issues.add("The 'SCO' party is mandatory when '%s' is absent".formatted(cqrPath)); } } return issues; @@ -275,9 +281,9 @@ private static Consumer allDg(Consumer consumer.accept(mav.path("consignmentItems").all().path("cargoItems").all().path("outerPackaging").path("dangerousGoods").all()); } - private static final JsonContentCheck CARGO_ITEM_REFERENCES_KNOWN_EQUIPMENT = JsonAttribute.customValidator( + private static final JsonRebaseableContentCheck CARGO_ITEM_REFERENCES_KNOWN_EQUIPMENT = JsonAttribute.customValidator( "Equipment References in 'cargoItems' must be present in 'utilizedTransportEquipments'", - (body) -> { + (body, contextPath) -> { var knownEquipmentReferences = allEquipmentReferences(body); var missing = new LinkedHashSet(); for (var consignmentItem : body.path("consignmentItems")) { @@ -292,24 +298,26 @@ private static Consumer allDg(Consumer "The equipment reference '%s' was used in a cargoItem but was not present in '%s'".formatted(ref, path)) .collect(Collectors.toSet()); } ); - private static final JsonContentCheck UTE_EQUIPMENT_REFERENCE_UNIQUE = JsonAttribute.customValidator( + private static final JsonRebaseableContentCheck UTE_EQUIPMENT_REFERENCE_UNIQUE = JsonAttribute.customValidator( "Equipment References in 'utilizedTransportEquipments' must be unique", - (body) -> { + (body, contextPath) -> { var duplicates = new LinkedHashSet(); allEquipmentReferences(body, duplicates); + var path = concatContextPath(contextPath, "utilizedTransportEquipments"); return duplicates.stream() - .map("The equipment reference '%s' was used more than once in 'utilizedTransportEquipments'"::formatted) + .map(ref -> "The equipment reference '%s' was used more than once in '%s'".formatted(ref, path)) .collect(Collectors.toSet()); } ); - private static final JsonContentCheck VOLUME_IMPLIES_VOLUME_UNIT = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck VOLUME_IMPLIES_VOLUME_UNIT = JsonAttribute.allIndividualMatchesMustBeValid( "The use of 'volume' implies 'volumeUnit'", (mav) -> { mav.submitAllMatching("consignmentItems.*"); @@ -379,20 +387,20 @@ private static void utilizedTransportEquipmentCargoItemAlignment( : "")); } - private static Function> allUtilizedTransportEquipmentCargoItemAreAligned( + private static JsonContentMatchedValidation allUtilizedTransportEquipmentCargoItemAreAligned( String uteNumberFieldName, String uteNumberUnitFieldName, String cargoItemNumberFieldName, String cargoItemNumberUnitFieldName, Map conversionTable ) { - return (documentRoot) -> { - var consignmentItems = documentRoot.path("consignmentItems"); + return (tdRoot, contextPath) -> { + var consignmentItems = tdRoot.path("consignmentItems"); var issues = new LinkedHashSet(); var index = 0; var key = "utilizedTransportEquipments"; - for (var ute : documentRoot.path(key)) { - var uteContextPath = key + "[" + index + "]"; + for (var ute : tdRoot.path(key)) { + var uteContextPath = concatContextPath(contextPath, key + "[" + index + "]"); ++index; var equipmentReference = ute.path("equipment").path("equipmentReference").asText(null); var uteNumberNode = ute.path(uteNumberFieldName); @@ -476,7 +484,7 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment( "FTQ->MTQ", 35.3146667 ); - private static final JsonContentCheck CONSIGNMENT_ITEM_VS_CARGO_ITEM_WEIGHT_IS_ALIGNED = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck CONSIGNMENT_ITEM_VS_CARGO_ITEM_WEIGHT_IS_ALIGNED = JsonAttribute.allIndividualMatchesMustBeValid( "Validate that 'consignmentItem' weight is aligned with its 'cargoItems'", (mav) -> mav.submitAllMatching("consignmentItems.*"), consignmentItemCargoItemAlignment("weight", @@ -484,7 +492,7 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment( RATIO_WEIGHT )); - private static final JsonContentCheck CONSIGNMENT_ITEM_VS_CARGO_ITEM_VOLUME_IS_ALIGNED = JsonAttribute.allIndividualMatchesMustBeValid( + private static final JsonRebaseableContentCheck CONSIGNMENT_ITEM_VS_CARGO_ITEM_VOLUME_IS_ALIGNED = JsonAttribute.allIndividualMatchesMustBeValid( "Validate that 'consignmentItem' volume is aligned with its 'cargoItems'", (mav) -> mav.submitAllMatching("consignmentItems.*"), consignmentItemCargoItemAlignment("volume", @@ -492,7 +500,7 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment( RATIO_VOLUME )); - private static final JsonContentCheck ADVANCED_MANIFEST_FILING_CODES_UNIQUE = JsonAttribute.customValidator( + private static final JsonRebaseableContentCheck ADVANCED_MANIFEST_FILING_CODES_UNIQUE = JsonAttribute.customValidator( "The combination of 'countryCode' and 'manifestTypeCode' in 'advanceManifestFilings' must be unique", JsonAttribute.unique("countryCode", "manifestTypeCode") ); @@ -532,7 +540,7 @@ private static JsonContentMatchedValidation consignmentItemCargoItemAlignment( VALIDATE_DOCUMENT_PARTIES ); - private static final List STATIC_TD_CHECKS = Arrays.asList( + private static final List STATIC_TD_CHECKS = Arrays.asList( ONLY_EBLS_CAN_BE_NEGOTIABLE, JsonAttribute.ifThenElse( "'isShippedOnBoardType' vs. 'shippedOnBoardDate' or 'receivedForShipmentDate'", @@ -913,10 +921,6 @@ private static JsonContentMatchedValidation checkCSPAllUsedAtLeastOnce( } private static JsonContentMatchedValidation checkEquipmentReference(Supplier cspSupplier) { - Supplier> expectedValueSupplier = () -> { - var csp = cspSupplier.get(); - return setOf(csp.equipmentReference(), csp.equipmentReference2()); - }; BiFunction resolver = (ute, pathBuilder) -> { var equipmentReferenceNode = ute.get("equipmentReference"); if (equipmentReferenceNode != null) { @@ -1132,11 +1136,10 @@ public static ActionCheck tdNotificationContentChecks(UUID matched, TransportDoc ); } - public static ActionCheck tdContentChecks(UUID matched, TransportDocumentStatus transportDocumentStatus, Supplier cspSupplier, Supplier dspSupplier) { - List jsonContentChecks = new ArrayList<>(); + public static void genericTdContentChecks(List jsonContentChecks, Supplier tdrSupplier, TransportDocumentStatus transportDocumentStatus) { jsonContentChecks.add(JsonAttribute.mustEqual( TD_TDR, - () -> dspSupplier.get().transportDocumentReference() + tdrSupplier )); jsonContentChecks.add(JsonAttribute.mustEqual( TD_TRANSPORT_DOCUMENT_STATUS, @@ -1146,6 +1149,25 @@ public static ActionCheck tdContentChecks(UUID matched, TransportDocumentStatus for (var ptr : TD_UN_LOCATION_CODES) { jsonContentChecks.add(JsonAttribute.mustBeDatasetKeywordIfPresent(ptr, EblDatasets.UN_LOCODE_DATASET)); } + } + + public static List genericTDContentChecks(TransportDocumentStatus transportDocumentStatus, Supplier tdrReferenceSupplier) { + List jsonContentChecks = new ArrayList<>(); + genericTdContentChecks( + jsonContentChecks, + tdrReferenceSupplier, + transportDocumentStatus + ); + return jsonContentChecks; + } + + public static ActionCheck tdPlusScenarioContentChecks(UUID matched, TransportDocumentStatus transportDocumentStatus, Supplier cspSupplier, Supplier dspSupplier) { + List jsonContentChecks = new ArrayList<>(); + genericTdContentChecks( + jsonContentChecks, + () -> dspSupplier.get().transportDocumentReference(), + transportDocumentStatus + ); jsonContentChecks.add(JsonAttribute.allIndividualMatchesMustBeValid( "[Scenario] Validate the containers reefer settings", mav-> mav.submitAllMatching("utilizedTransportEquipments.*"), @@ -1179,7 +1201,7 @@ public static ActionCheck tdContentChecks(UUID matched, TransportDocumentStatus )); jsonContentChecks.add(JsonAttribute.allIndividualMatchesMustBeValid( "[Scenario] Whether the cargo should be DG", - mav-> mav.path("consignmentItems").all().path("cargoItems").all().path("outerPackaging").path("dangerousGoods").submitPath(), + mav-> mav.submitAllMatching("consignmentItems.*.cargoItems.*.outerPackaging.*.dangerousGoods"), (nodeToValidate, contextPath) -> { var scenario = dspSupplier.get().scenarioType(); if (scenario == ScenarioType.DG) { diff --git a/pint/pom.xml b/pint/pom.xml index e3a07d74..8c2eca6d 100644 --- a/pint/pom.xml +++ b/pint/pom.xml @@ -20,6 +20,11 @@ 0.0.1-SNAPSHOT + + org.dcsa.conformance + ebl + 0.0.1-SNAPSHOT + org.projectlombok lombok diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateAndCloseTransferAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateAndCloseTransferAction.java index 78b8f249..8320b86a 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateAndCloseTransferAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateAndCloseTransferAction.java @@ -1,8 +1,5 @@ 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.Objects; import java.util.function.Supplier; @@ -17,6 +14,8 @@ import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; import org.dcsa.conformance.standards.eblinterop.party.PintRole; +import static org.dcsa.conformance.standards.eblinterop.checks.PintChecks.*; + @Getter @Slf4j public class PintInitiateAndCloseTransferAction extends PintAction { @@ -143,6 +142,10 @@ protected Stream createSubChecks() { HttpMessageType.REQUEST, requestSchemaValidator ), + tdContentChecks( + getMatchedExchangeUuid(), + () -> getSsp() + ), validateInitiateTransferRequest( getMatchedExchangeUuid(), () -> getSsp(), 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 index 94d42fc6..901988a0 100644 --- 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 @@ -1,7 +1,6 @@ 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.validateUnsignedStartResponse; +import static org.dcsa.conformance.standards.eblinterop.checks.PintChecks.*; import static org.dcsa.conformance.standards.eblinterop.crypto.SignedNodeSupport.parseSignedNodeNoErrors; import com.fasterxml.jackson.databind.JsonNode; @@ -159,6 +158,10 @@ protected Stream createSubChecks() { HttpMessageType.REQUEST, requestSchemaValidator ), + tdContentChecks( + getMatchedExchangeUuid(), + () -> getSsp() + ), validateInitiateTransferRequest( getMatchedExchangeUuid(), () -> getSsp(), diff --git a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java index a2765914..7c8789a5 100644 --- a/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java +++ b/pint/src/main/java/org/dcsa/conformance/standards/eblinterop/action/PintInitiateTransferUnsignedErrorAction.java @@ -1,5 +1,6 @@ package org.dcsa.conformance.standards.eblinterop.action; +import static org.dcsa.conformance.standards.eblinterop.checks.PintChecks.tdContentChecks; import static org.dcsa.conformance.standards.eblinterop.checks.PintChecks.validateInitiateTransferRequest; import static org.dcsa.conformance.standards.eblinterop.crypto.SignedNodeSupport.parseSignedNodeNoErrors; @@ -135,6 +136,10 @@ protected Stream createSubChecks() { HttpMessageType.REQUEST, requestSchemaValidator ), + tdContentChecks( + getMatchedExchangeUuid(), + () -> getSsp() + ), validateInitiateTransferRequest( getMatchedExchangeUuid(), () -> getSsp(), 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 index 05431785..13277c92 100644 --- 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 @@ -1,6 +1,7 @@ package org.dcsa.conformance.standards.eblinterop.checks; import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; +import static org.dcsa.conformance.standards.ebl.checks.EBLChecks.genericTDContentChecks; import com.fasterxml.jackson.core.JsonPointer; import com.fasterxml.jackson.databind.JsonNode; @@ -11,6 +12,7 @@ import java.util.function.*; import org.dcsa.conformance.core.check.*; import org.dcsa.conformance.core.traffic.HttpMessageType; +import org.dcsa.conformance.standards.ebl.party.TransportDocumentStatus; import org.dcsa.conformance.standards.eblinterop.action.PintResponseCode; import org.dcsa.conformance.standards.eblinterop.crypto.SignatureVerifier; import org.dcsa.conformance.standards.eblinterop.models.DynamicScenarioParameters; @@ -132,6 +134,18 @@ public static JsonContentMatchedValidation pathChain(JsonContentMatchedValidatio return combined; } + public static ActionCheck tdContentChecks(UUID matched, Supplier senderScenarioParametersSupplier) { + var checks = genericTDContentChecks(TransportDocumentStatus.TD_ISSUED, delayedValue(senderScenarioParametersSupplier, SenderScenarioParameters::transportDocumentReference)); + return JsonAttribute.contentChecks( + "Complex validations of transport document", + PintRole::isSendingPlatform, + matched, + HttpMessageType.REQUEST, + JsonContentCheckRebaser.of("transportDocument"), + checks + ); + } + public static JsonContentMatchedValidation expectedTDChecksum(Supplier dynamicScenarioParametersSupplier) { return JsonAttribute.matchedMustEqual(delayedValue(dynamicScenarioParametersSupplier, DynamicScenarioParameters::transportDocumentChecksum)); } 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 index d76a5f6c..4ebf9d6f 100644 --- 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 @@ -18,7 +18,6 @@ import java.security.interfaces.RSAPublicKey; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.RSAPublicKeySpec; -import java.security.spec.X509EncodedKeySpec; import java.util.Base64; public class PayloadSignerFactory { 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 index 314ca69b..f217a9fc 100644 --- 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 @@ -8,14 +8,15 @@ "locationType": "UNCO", "UNLocationCode": "DKCPH" }, - "cargoMovementTypeAtOrigin": "CY", - "cargoMovementTypeAtDestination": "CY", + "cargoMovementTypeAtOrigin": "FCL", + "cargoMovementTypeAtDestination": "FCL", "deliveryTypeAtDestination": "CY", "receiptTypeAtOrigin": "CY", "transportDocumentTypeCode": "SWB", "isShippedOnBoardType": true, + "shippedOnBoardDate": "2024-02-02", "freightPaymentTermCode": "PRE", - "transportDocumentStatus": "ISSU", + "transportDocumentStatus": "ISSUED", "utilizedTransportEquipments": [], "transports": { "vesselName": "Emma Maersk", @@ -40,7 +41,45 @@ ], "consignmentItems": [], "documentParties": [ - + { + "party": { + "partyName": "DCSA CTK SHIPPER", + "partyContactDetails": [ + { + "name": "DCSA test person", + "email": "no-reply@dcsa.example.org" + } + ] + }, + "partyFunction": "OS", + "isToBeNotified": false + }, + { + "party": { + "partyName": "DCSA CTK Consignee", + "partyContactDetails": [ + { + "name": "DCSA test person", + "email": "no-reply@dcsa.example.org" + } + ] + }, + "partyFunction": "CN", + "isToBeNotified": false + }, + { + "party": { + "partyName": "DCSA CTK Service Contract Owner", + "partyContactDetails": [ + { + "name": "DCSA test person", + "email": "no-reply@dcsa.example.org" + } + ] + }, + "partyFunction": "SCO", + "isToBeNotified": false + } ], "carrierCode": "ASDF", "carrierCodeListProvider": "NMFTA",