diff --git a/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java b/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java index 63bc1052..1a496759 100644 --- a/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java +++ b/core/src/main/java/org/dcsa/conformance/core/check/JsonAttributeBasedCheck.java @@ -13,6 +13,7 @@ import org.dcsa.conformance.core.traffic.ConformanceExchange; import org.dcsa.conformance.core.traffic.HttpMessageType; + class JsonAttributeBasedCheck extends ActionCheck { private final String standardsVersion; diff --git a/ovs/pom.xml b/ovs/pom.xml index 34701936..3af3f6e4 100644 --- a/ovs/pom.xml +++ b/ovs/pom.xml @@ -23,5 +23,11 @@ org.projectlombok lombok + + + org.junit.jupiter + junit-jupiter + test + 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 819ad016..4a973c5b 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 @@ -35,38 +35,38 @@ public static LinkedHashMap createModuleScenario .thenEither( scenarioWithParameters(Map.of(CARRIER_SERVICE_NAME, "Great Lion Service")), scenarioWithParameters( - Map.of(CARRIER_SERVICE_NAME, "Blue Whale Service", LIMIT, "1")), + Map.of(CARRIER_SERVICE_NAME, "Blue Whale Service", LIMIT, "5")), scenarioWithParameters( Map.of( CARRIER_SERVICE_NAME, "Red Falcon Service", START_DATE, - "2026-01-01")), + "2024-01-01")), scenarioWithParameters( Map.of( CARRIER_SERVICE_NAME, "Great Lion Service", END_DATE, - "2021-01-01")), + "2025-01-01")), scenarioWithParameters(Map.of(CARRIER_SERVICE_CODE, "BW1")), - scenarioWithParameters(Map.of(CARRIER_SERVICE_CODE, "BW1", LIMIT, "1")), + scenarioWithParameters(Map.of(CARRIER_SERVICE_CODE, "BW1", LIMIT, "5")), scenarioWithParameters(Map.of(UNIVERSAL_SERVICE_REFERENCE, "SR12345A")), scenarioWithParameters( - Map.of(UNIVERSAL_SERVICE_REFERENCE, "SR67890B", LIMIT, "1")))), + Map.of(UNIVERSAL_SERVICE_REFERENCE, "SR67890B", LIMIT, "5")))), Map.entry( "Vessel schedules", noAction() .thenEither( scenarioWithParameters(Map.of(VESSEL_IMO_NUMBER, "9456789")), - scenarioWithParameters(Map.of(VESSEL_IMO_NUMBER, "9876543", LIMIT, "1")))), + scenarioWithParameters(Map.of(VESSEL_IMO_NUMBER, "9876543", LIMIT, "5")))), Map.entry( "Location schedules", noAction() .thenEither( scenarioWithParameters(Map.of(UN_LOCATION_CODE, "NLAMS")), - scenarioWithParameters(Map.of(UN_LOCATION_CODE, "USNYC", LIMIT, "1")), + scenarioWithParameters(Map.of(UN_LOCATION_CODE, "USNYC", LIMIT, "5")), scenarioWithParameters(Map.of(FACILITY_SMDG_CODE, "APM")), - scenarioWithParameters(Map.of(FACILITY_SMDG_CODE, "APM", LIMIT, "1")))), + scenarioWithParameters(Map.of(FACILITY_SMDG_CODE, "APM", LIMIT, "5")))), Map.entry( "Voyage schedules", noAction() @@ -79,7 +79,7 @@ public static LinkedHashMap createModuleScenario Map.of( CARRIER_VOYAGE_NUMBER, "2104S", CARRIER_SERVICE_CODE, "BW1", - LIMIT, "1")), + LIMIT, "5")), scenarioWithParameters( Map.of( CARRIER_VOYAGE_NUMBER, "2103N", @@ -88,7 +88,7 @@ public static LinkedHashMap createModuleScenario Map.of( CARRIER_VOYAGE_NUMBER, "2103S", UNIVERSAL_SERVICE_REFERENCE, "SR12345A", - LIMIT, "1")), + LIMIT, "5")), scenarioWithParameters( Map.of( UNIVERSAL_VOYAGE_REFERENCE, "2103N", @@ -97,7 +97,7 @@ public static LinkedHashMap createModuleScenario Map.of( UNIVERSAL_VOYAGE_REFERENCE, "2103S", CARRIER_SERVICE_CODE, "FE1", - LIMIT, "1")), + LIMIT, "5")), scenarioWithParameters( Map.of( UNIVERSAL_VOYAGE_REFERENCE, "2105N", @@ -106,7 +106,7 @@ public static LinkedHashMap createModuleScenario Map.of( UNIVERSAL_VOYAGE_REFERENCE, "2105S", UNIVERSAL_SERVICE_REFERENCE, "SR54321C", - LIMIT, "1"))))) + LIMIT, "5"))))) .collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); 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 6896ae0d..699ef07e 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 @@ -4,10 +4,17 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import lombok.Getter; +import org.dcsa.conformance.core.UserFacingException; import org.dcsa.conformance.standards.ovs.party.OvsFilterParameter; import org.dcsa.conformance.standards.ovs.party.SuppliedScenarioParameters; @@ -90,6 +97,48 @@ public boolean isInputRequired() { @Override public void handlePartyInput(JsonNode partyInput) { super.handlePartyInput(partyInput); - suppliedScenarioParameters = SuppliedScenarioParameters.fromJson(partyInput.get("input")); + JsonNode inputNode = partyInput.get("input"); + Set inputKeys = + StreamSupport.stream( + ((Iterable) inputNode::fieldNames) + .spliterator(), + false) + .collect(Collectors.toSet()); + + Set missingKeys = + StreamSupport.stream( + ((Iterable) () -> getJsonForHumanReadablePrompt().fieldNames()) + .spliterator(), + false) + .collect(Collectors.toSet()); + missingKeys.removeAll(inputKeys); + if (!missingKeys.isEmpty()) { + throw new UserFacingException( + "The input must contain: %s".formatted(String.join(", ", missingKeys))); + } + + Arrays.stream(OvsFilterParameter.values()) + .map(OvsFilterParameter::getQueryParamName) + .filter( + queryParamName -> + queryParamName.startsWith( + OvsFilterParameter.START_DATE.getQueryParamName()) + || queryParamName.startsWith(OvsFilterParameter.END_DATE.getQueryParamName())) + .filter(inputNode::hasNonNull) + .forEach( + queryParamName -> { + String dateValue = inputNode.path(queryParamName).asText(); + try { + LocalDate.parse(dateValue, DateTimeFormatter.ISO_DATE); + } catch (DateTimeParseException e) { + throw new UserFacingException( + "Invalid date-time format '%s' for input parameter '%s'" + .formatted(dateValue, queryParamName), + e); + } + }); + + suppliedScenarioParameters = SuppliedScenarioParameters.fromJson(inputNode); + } } diff --git a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java index 5cff8bd2..b02fbb4d 100644 --- a/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java +++ b/ovs/src/main/java/org/dcsa/conformance/standards/ovs/checks/OvsChecks.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; import org.dcsa.conformance.core.check.ActionCheck; import org.dcsa.conformance.core.check.JsonAttribute; import org.dcsa.conformance.core.check.JsonContentCheck; @@ -13,6 +14,7 @@ import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.*; import java.util.function.BiPredicate; import java.util.function.Supplier; @@ -20,227 +22,176 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; +@Slf4j @UtilityClass public class OvsChecks { - public static ActionCheck responseContentChecks( - UUID matched, String standardVersion, Supplier sspSupplier) { - + public List buildResponseContentChecks(Map filterParametersMap) { var checks = new ArrayList(); - - checks.add( - JsonAttribute.customValidator( - "Validate carrierServiceCode exists and " - + "matches in JSON response if request parameter has carrierServiceCode", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.CARRIER_SERVICE_CODE, - "*/carrierServiceCode"))); - - checks.add( - JsonAttribute.customValidator( - "Validate universalServiceReference exists and " - + "matches in JSON response if request parameter has universalServiceReference", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE, - "*/universalServiceReference"))); - - checks.add( - JsonAttribute.customValidator( - "Validate vesselIMONumber exists and" - + " matches in JSON response if request parameter has vesselIMONumber", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.VESSEL_IMO_NUMBER, - "*/vesselSchedules/*/vesselIMONumber"))); - - checks.add( - JsonAttribute.customValidator( - "Validate vesselName exists and" - + " matches in JSON response if request parameter has vesselName", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.VESSEL_NAME, - "*/vesselSchedules/*/vesselName"))); - - checks.add( - JsonAttribute.customValidator( - "Validate carrierVoyageNumber exists and " - + "matches in JSON response if request parameter has carrierVoyageNumber", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.CARRIER_VOYAGE_NUMBER, - "*/vesselSchedules/*/transportCalls/*/carrierExportVoyageNumber", - "*/vesselSchedules/*/transportCalls/*/carrierImportVoyageNumber"))); - checks.add( - JsonAttribute.customValidator( - "Validate universalVoyageReference exists and" - + " matches in JSON response if request parameter has universalVoyageReference", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.UNIVERSAL_VOYAGE_REFERENCE, - "*/vesselSchedules/*/transportCalls/*/universalImportVoyageReference", - "*/vesselSchedules/*/transportCalls/*/universalExportVoyageReference"))); + JsonAttribute.customValidator( + "Every response received during a conformance test must contain schedules", + body -> { + Set validationErrors = new LinkedHashSet<>(); + checkServiceSchedulesExist(body) + .forEach( + validationError -> + validationErrors.add( + "CheckServiceSchedules failed: %s".formatted(validationError))); + return validationErrors; + })); checks.add( - JsonAttribute.customValidator( - "Validate UNLocationCode exists and " - + "matches in JSON response if request parameter has UNLocationCode", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.UN_LOCATION_CODE, - "*/vesselSchedules/*/transportCalls/*/location/UNLocationCode"))); + JsonAttribute.customValidator( + "If present, at least one schedule attribute must match the corresponding query parameters", + body -> { + Set validationErrors = new LinkedHashSet<>(); + checkThatScheduleValuesMatchParamValues(body, filterParametersMap) + .forEach( + validationError -> + validationErrors.add( + "Schedule Param Value Validation failed: %s" + .formatted(validationError))); + return validationErrors; + })); checks.add( - JsonAttribute.customValidator( - "Validate facilitySMDGCode exists and " - + "matches in JSON response if request parameter has facilitySMDGCode", - body -> - validateParameter( - body, - sspSupplier, - OvsFilterParameter.FACILITY_SMDG_CODE, - "*/vesselSchedules/*/transportCalls/*/location/facilitySMDGCode"))); + JsonAttribute.customValidator( + "Check eventDateTime is greater than startDate filter parameter if present", + body -> { + Set validationErrors = new LinkedHashSet<>(); + validateDate( + body, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore) + .forEach( + validationError -> + validationErrors.add( + "Start Date EventDateTime validation failed: %s" + .formatted(validationError))); + return validationErrors; + })); checks.add( - JsonAttribute.customValidator( - "Validate startDate exists and " - + "matches in JSON response if request parameter has startDate", - body -> - validateDate( - body, - sspSupplier, - OvsFilterParameter.START_DATE, - (eventDate, expectedStartDate) -> - !eventDate.isAfter( - expectedStartDate)))); // Check if eventDate is before or equal to - // startDate + JsonAttribute.customValidator( + "Check eventDateTime is less than endDate filter parameter if present", + body -> { + Set validationErrors = new LinkedHashSet<>(); + validateDate( + body, filterParametersMap, OvsFilterParameter.END_DATE, LocalDate::isAfter) + .forEach( + validationError -> + validationErrors.add( + "EndDate EventDateTime validation failed: %s" + .formatted(validationError))); + return validationErrors; + })); checks.add( - JsonAttribute.customValidator( - "Validate endDate exists and " - + "matches in JSON response if request parameter has endDate", - body -> - validateDate( - body, - sspSupplier, - OvsFilterParameter.END_DATE, - (eventDate, expectedEndDate) -> - !eventDate.isBefore( - expectedEndDate)))); // Check if eventDate is after or equal to endDate + JsonAttribute.customValidator( + "Check transportCallReference is unique across each service schedules", + OvsChecks::validateUniqueTransportCallReference)); checks.add( - JsonAttribute.customValidator( - "Validate transportCallReference is unique across each array node", - OvsChecks::validateUniqueTransportCallReference)); - - checks.add( - JsonAttribute.customValidator( - "Validate limit exists and the number of schedules does not exceed the limit", - body -> { - Optional> limitParam = - sspSupplier.get().getMap().entrySet().stream() - .filter(e -> e.getKey().equals(OvsFilterParameter.LIMIT)) - .findFirst(); - - if (limitParam.isPresent()) { - int expectedLimit = Integer.parseInt(limitParam.get().getValue().trim()); - if (body.size() > expectedLimit) { - return Set.of( - "The number of schedules exceeds the limit parameter: " + expectedLimit); - } - } - - return Set.of(); - })); + JsonAttribute.customValidator( + "Validate limit exists and the number of schedules does not exceed the limit", + body -> { + Optional> limitParam = + filterParametersMap.entrySet().stream() + .filter(e -> e.getKey().equals(OvsFilterParameter.LIMIT)) + .findFirst(); + + if (limitParam.isPresent()) { + int expectedLimit = Integer.parseInt(limitParam.get().getValue().trim()); + if (body.size() > expectedLimit) { + return Set.of( + "The number of service schedules exceeds the limit parameter: " + + expectedLimit); + } + } + return Set.of(); + })); + return checks; + } + public static ActionCheck responseContentChecks( + UUID matched, String standardVersion, Supplier sspSupplier) { + Map filterParametersMap = sspSupplier.get() != null + ? sspSupplier.get().getMap() + : Map.of(); + var checks = buildResponseContentChecks(filterParametersMap); return JsonAttribute.contentChecks( OvsRole::isPublisher, matched, HttpMessageType.RESPONSE, standardVersion, checks); } - private Set validateParameter( - JsonNode body, - Supplier sspSupplier, - OvsFilterParameter parameter, - String... jsonPaths) { - Optional> param = - sspSupplier.get().getMap().entrySet().stream() - .filter(e -> e.getKey().equals(parameter)) - .findFirst(); - - if (param.isPresent()) { - Set expectedValues = - Arrays.stream(param.get().getValue().split(",")) - .map(String::trim) - .collect(Collectors.toSet()); - - // Check if ANY of the jsonPaths match the expectedValues - if (jsonPaths.length > 1) { - boolean anyMatch = - Stream.of(jsonPaths) - .anyMatch( - jsonPath -> - findMatchingNodes(body, jsonPath) - .anyMatch( - valueNode -> - !(valueNode.isMissingNode() || valueNode.isNull()) - && expectedValues.contains(valueNode.asText()))); - - if (!anyMatch) { - return Set.of( - "Missing or mismatched values for parameter: " - + parameter.getQueryParamName() - + " in any of the paths: " - + Arrays.toString(jsonPaths)); - } - } else { - Set errors = - Stream.of(jsonPaths) - .flatMap( - jsonPath -> - findMatchingNodes(body, jsonPath) - .filter( - valueNode -> - !(valueNode.isMissingNode() || valueNode.isNull()) - && !expectedValues.contains(valueNode.asText())) - .map( - valueNode -> - "Missing or mismatched " - + jsonPath - + ": " - + valueNode.asText())) - .collect(Collectors.toSet()); + public Set checkThatScheduleValuesMatchParamValues( + JsonNode schedulesNode, Map filterParametersMap) { + Set validationErrors = new LinkedHashSet<>(); + Arrays.stream(OvsFilterParameter.values()) + .filter(param -> !param.getJsonPaths().isEmpty()) + .filter(param -> !param.isSeparateCheckRequired()) + .filter(filterParametersMap::containsKey) + .forEach( + filterParameter -> { + Set parameterValues = + Arrays.stream(filterParametersMap.get(filterParameter).split(",")) + .collect(Collectors.toSet()); + Set> attributeValues = new HashSet<>(); + Set jsonPaths = filterParameter.getJsonPaths(); + jsonPaths.forEach( + jsonPathExpression -> { + findMatchingNodes(schedulesNode, jsonPathExpression) + .forEach( + result -> { + if (!result.getValue().isMissingNode() + && !result.getValue().isNull()) { + attributeValues.add(result); + } + }); + }); + if (!attributeValues.isEmpty() + && parameterValues.stream() + .noneMatch( + parameterValue -> + attributeValues.stream() + .anyMatch( + entry -> + entry + .getValue() + .asText() + .trim() + .equals( + parameterValue.trim())))) { // Trim and compare + + String errorMessage = + String.format( + "Value%s '%s' at path%s '%s' do%s not match value%s '%s' of query parameter '%s'", + attributeValues.size() > 1 ? "s" : "", + attributeValues.stream() + .map(e -> e.getValue().asText()) + .collect(Collectors.joining(", ")), + attributeValues.size() > 1 ? "s" : "", + attributeValues.stream() + .map(Map.Entry::getKey) + .collect(Collectors.joining(", ")), // Get keys here + attributeValues.size() > 1 ? "" : "es", + parameterValues.size() > 1 ? "s" : "", + String.join(", ", parameterValues), + filterParameter.getQueryParamName()); + + validationErrors.add(errorMessage); + } + }); - return errors.isEmpty() ? Set.of() : errors; - } - } - return Set.of(); + return validationErrors; } - private Set validateDate( + public Set validateDate( JsonNode body, - Supplier sspSupplier, + Map filterParametersMap, OvsFilterParameter dateParameter, BiPredicate dateComparison) { Optional> dateParam = - sspSupplier.get().getMap().entrySet().stream() + filterParametersMap.entrySet().stream() .filter(e -> e.getKey().equals(dateParameter)) .findFirst(); @@ -255,73 +206,116 @@ private Set validateDate( findMatchingNodes(body, "*/vesselSchedules/*/transportCalls/*/timestamps") .flatMap( timestampsNode -> - StreamSupport.stream(timestampsNode.spliterator(), false) + StreamSupport.stream(timestampsNode.getValue().spliterator(), false) + .filter( + eventDateTimeNode -> + !eventDateTimeNode.isMissingNode() && !eventDateTimeNode.isNull()) .filter( timestampNode -> { - OffsetDateTime eventDateTime = - OffsetDateTime.parse( - timestampNode.path("eventDateTime").asText(), - DateTimeFormatter.ISO_DATE_TIME); - return dateComparison.test(eventDateTime.toLocalDate(), expectedDate); + LocalDate eventDateTime = + stringToISODateTime(timestampNode.path("eventDateTime").asText()); + if (eventDateTime != null) { + return dateComparison.test(eventDateTime, expectedDate); + } + return false; })) .map( timestampNode -> "Event DateTime " + timestampNode.path("eventDateTime").asText() + (dateParameter == OvsFilterParameter.START_DATE - ? " is before or equal to the startDate: " - : " is after or equal to the endDate: ") + ? " is before the startDate: " + : " is after the endDate: ") + expectedDate) .collect(Collectors.toSet()); return errors.isEmpty() ? Set.of() : errors; } - private Set validateUniqueTransportCallReference(JsonNode body) { - Set transportCallReferences = new HashSet<>(); - Set errors = new HashSet<>(); + private static LocalDate stringToISODateTime(String dateTimeString) { + try { + return OffsetDateTime.parse(dateTimeString, DateTimeFormatter.ISO_DATE_TIME).toLocalDate(); + } catch (DateTimeParseException e) { + log.error("Failed to parse date time string: {}", dateTimeString, e); + return null; + } + } - // Iterate over each array node in the response body + public Set validateUniqueTransportCallReference(JsonNode body) { + Set errors = new HashSet<>(); + // Iterate over each service schedule in the response body for (JsonNode node : body) { - // Assuming the path to transportCallReference is consistent across array nodes - findMatchingNodes(node, "*/vesselSchedules/*/transportCalls/*/transportCallReference") + Set transportCallReferences = new HashSet<>(); + findMatchingNodes(node, "vesselSchedules/*/transportCalls/*/transportCallReference") + .filter(tcrNode -> !tcrNode.getValue().isMissingNode() && !tcrNode.getValue().isNull()) .forEach( transportCallReferenceNode -> { - String transportCallReference = transportCallReferenceNode.asText(); + String transportCallReference = transportCallReferenceNode.getValue().asText(); if (!transportCallReferences.add(transportCallReference)) { - errors.add("Duplicate transportCallReference found: " + transportCallReference); + errors.add( + ("Duplicate transportCallReference %s " + "found at %s") + .formatted(transportCallReference, transportCallReferenceNode.getKey())); } }); } - return errors; } - private Stream findMatchingNodes(JsonNode node, String jsonPath) { + public Stream> findMatchingNodes(JsonNode node, String jsonPath) { + return findMatchingNodes(node, jsonPath, ""); + } + + private Stream> findMatchingNodes( + JsonNode node, String jsonPath, String currentPath) { if (jsonPath.isEmpty() || jsonPath.equals("/")) { - return Stream.of(node); + return Stream.of(Map.entry(currentPath, node)); } - String[] pathSegments = jsonPath.split("/"); - if (pathSegments[0].equals("*")) { + String[] pathSegments = jsonPath.split("/", 2); + String segment = pathSegments[0]; + String remainingPath = pathSegments.length > 1 ? pathSegments[1] : ""; + + if (segment.equals("*")) { if (node.isArray()) { - // If the node is an array, iterate over its elements - return StreamSupport.stream(node.spliterator(), false) - .flatMap( - childNode -> - findMatchingNodes( - childNode, - String.join( - "/", Arrays.copyOfRange(pathSegments, 1, pathSegments.length)))); + List> results = new ArrayList<>(); + for (int i = 0; i < node.size(); i++) { + JsonNode childNode = node.get(i); + String newPath = currentPath.isEmpty() ? String.valueOf(i) : currentPath + "/" + i; + results.addAll(findMatchingNodes(childNode, remainingPath, newPath).toList()); + } + return results.stream(); + } else if (node.isObject()) { + return findMatchingNodes(node, remainingPath, currentPath); } else { - // If not an array, treat it as a single node - return findMatchingNodes( - node, String.join("/", Arrays.copyOfRange(pathSegments, 1, pathSegments.length))); + return Stream.of(); } } else { - return findMatchingNodes( - node.path(pathSegments[0]), - String.join("/", Arrays.copyOfRange(pathSegments, 1, pathSegments.length))); + JsonNode childNode = node.path(segment); + if (!childNode.isMissingNode()) { + String newPath = currentPath.isEmpty() ? segment : currentPath + "/" + segment; + return findMatchingNodes(childNode, remainingPath, newPath); + } else { + return Stream.of(); + } } } + + public Set checkServiceSchedulesExist(JsonNode body) { + + if (body == null || body.isMissingNode() || body.isNull()) { + return Set.of("Response body is missing or null."); + } else { + boolean hasVesselSchedules = + findMatchingNodes(body, "*/vesselSchedules") + .anyMatch( + node -> + !node.getValue().isMissingNode() + && node.getValue().isArray() + && !node.getValue().isEmpty()); + if (!hasVesselSchedules) { + return Set.of("Response doesn't have schedules."); + } + } + return Set.of(); + } } 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 index fdb2abe2..b761b177 100644 --- 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 @@ -4,22 +4,34 @@ import java.util.Arrays; import java.util.Map; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; +@Getter 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"), + VESSEL_IMO_NUMBER("vesselIMONumber", false, "*/vesselSchedules/*/vesselIMONumber"), + VESSEL_NAME("vesselName", false, "*/vesselSchedules/*/vesselName"), + CARRIER_VOYAGE_NUMBER( + "carrierVoyageNumber", + false, + "*/vesselSchedules/*/transportCalls/*/carrierExportVoyageNumber", + "*/vesselSchedules/*/transportCalls/*/carrierImportVoyageNumber"), + UNIVERSAL_VOYAGE_REFERENCE( + "universalVoyageReference", + false, + "*/vesselSchedules/*/transportCalls/*/universalImportVoyageReference", + "*/vesselSchedules/*/transportCalls/*/universalExportVoyageReference"), + UN_LOCATION_CODE( + "UNLocationCode", false, "*/vesselSchedules/*/transportCalls/*/location/UNLocationCode"), + FACILITY_SMDG_CODE( + "facilitySMDGCode", false, "*/vesselSchedules/*/transportCalls/*/location/facilitySMDGCode"), + START_DATE("startDate", true, "*/vesselSchedules/*/transportCalls/*/timestamps/*/eventDateTime"), + END_DATE("endDate", true, "*/vesselSchedules/*/transportCalls/*/timestamps/*/eventDateTime"), + LIMIT("limit", true), ; public static final Map byQueryParamName = @@ -28,9 +40,23 @@ public enum OvsFilterParameter { Collectors.toUnmodifiableMap( OvsFilterParameter::getQueryParamName, Function.identity())); - @Getter private final String queryParamName; + private final String queryParamName; - OvsFilterParameter(String queryParamName) { + private final Set jsonPaths; + + private final boolean isSeparateCheckRequired; + + OvsFilterParameter(String queryParamName, boolean isSeparateCheckRequired, String... jsonPaths) { this.queryParamName = queryParamName; + this.jsonPaths = Set.of(jsonPaths); + this.isSeparateCheckRequired = isSeparateCheckRequired; + } + + OvsFilterParameter(String queryParamName) { + this(queryParamName, false, "*/" + queryParamName); + } + + OvsFilterParameter(String queryParamName, boolean isSeparateCheckRequired) { + this(queryParamName, isSeparateCheckRequired, "*/" + 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 31ae5a7b..a3db8b72 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 @@ -120,6 +120,7 @@ public ConformanceResponse handleRequest(ConformanceRequest request) { request.queryParams().containsKey("limit") ? request.queryParams().get("limit").iterator().next() : "100"); + if (filteredArray.size() > limit) { ArrayNode limitedArray = OBJECT_MAPPER.createArrayNode(); for (int i = 0; i < limit; i++) { @@ -129,7 +130,8 @@ public ConformanceResponse handleRequest(ConformanceRequest request) { } Map> headers = - new HashMap<>(Map.of(API_VERSION, List.of(apiVersion))); + new HashMap<>(Map.of(API_VERSION, List.of(apiVersion))); + return request.createResponse(200, headers, new ConformanceMessageBody(filteredArray)); } diff --git a/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json b/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json index 2f261703..447c6198 100644 --- a/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json +++ b/ovs/src/main/resources/standards/ovs/messages/ovs-300-response.json @@ -13,7 +13,7 @@ "transportCalls": [ { "portVisitReference": "NLAMS1234589", - "transportCallReference": "SR11111X-9321483-2107W-NLAMS-ACT-1-1", + "transportCallReference": "SR12345X-9321483-2107W-NLAMS-ACT-1-1", "carrierImportVoyageNumber": "2103N", "carrierExportVoyageNumber": "2103S", "universalImportVoyageReference": "2103N", @@ -28,7 +28,7 @@ { "eventTypeCode": "ARRI", "eventClassifierCode": "PLN", - "eventDateTime": "2025-01-14T09:21:00+01:00", + "eventDateTime": "2024-01-14T09:21:00+01:00", "delayReasonCode": "WEA", "changeRemark": "Bad weather" } @@ -115,5 +115,45 @@ ] } ] + }, + { + "carrierServiceName": "Red Eagle Service", + "carrierServiceCode": "RF1", + "universalServiceReference": "SR54821C", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MAE", + "vesselIMONumber": "9876543", + "vesselName": "Eagle of the Seas", + "vesselCallSign": "EGLS", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "USNYC1234567", + "transportCallReference": "SR12345Z-9876543-2109W-USNYC-ACT-3-3", + "carrierImportVoyageNumber": "2105N", + "carrierExportVoyageNumber": "2105S", + "universalImportVoyageReference": "2105N", + "universalExportVoyageReference": "2105S", + "location": { + "locationType": "FACS", + "UNLocationCode": "USNYC", + "facilitySMDGCode": "APM" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "ACT", + "eventDateTime": "2025-03-15T10:00:00-05:00", + "delayReasonCode": "TRF", + "changeRemark": "Heavy traffic" + } + ] + } + ] + } + ] } + ] diff --git a/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml b/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml index d82950fd..eefd2af6 100644 --- a/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml +++ b/ovs/src/main/resources/standards/ovs/schemas/OVS_v3.0.0.yaml @@ -482,6 +482,7 @@ components: maxLength: 75 UNLocationLocation: title: UNLocation Location + additionalProperties: false x-stoplight: id: x4suin19xkq6q type: object diff --git a/ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java b/ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java new file mode 100644 index 00000000..c5d3e6dd --- /dev/null +++ b/ovs/src/test/java/org/dcsa/conformance/standards/ovs/checks/OvsChecksTest.java @@ -0,0 +1,465 @@ +package org.dcsa.conformance.standards.ovs.checks; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.dcsa.conformance.core.toolkit.JsonToolkit; +import org.dcsa.conformance.standards.ovs.party.OvsFilterParameter; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Stream; + +import static org.dcsa.conformance.core.toolkit.JsonToolkit.OBJECT_MAPPER; +import static org.junit.jupiter.api.Assertions.*; + +class OvsChecksTest { + + private JsonNode serviceNodes; + + @BeforeEach + void setUp() { + JsonNode vesselSchedules = createServiceVesselSchedules("1234567", "Great Vessel"); + serviceNodes = createServiceNodes("Great Lion Service", "GLS", "SR12345A", vesselSchedules); + } + + @Test + void testResponseContentChecks_validResponse() { + Set issues = + executeResponseChecks(Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, "GLS"), serviceNodes); + assertTrue(issues.isEmpty()); + } + + @Test + void testResponseContentChecks_withWrongAttributesValuesResponse() { + JsonNode jsonBody = JsonToolkit.templateFileToJsonNode( + "/messages/ovs-300-response-wrong-attribute-values.json", + Map.ofEntries()); + + Set issues = + executeResponseChecks(Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, "GLS"), jsonBody); + assertFalse(issues.isEmpty()); + } + + @Test + void testResponseContentChecks_withWrongDateTimesResponse() { + JsonNode jsonBody = JsonToolkit.templateFileToJsonNode( + "/messages/ovs-300-response-wrong-date-times.json", + Map.ofEntries()); + + Set issues = + executeResponseChecks(Map.of(OvsFilterParameter.START_DATE, "2027-07-19"), jsonBody); + assertTrue(issues.isEmpty()); + } + + @Test + void testResponseContentChecks_withWrongStructureResponse() { + JsonNode jsonBody = JsonToolkit.templateFileToJsonNode( + "/messages/ovs-300-response-wrong-structure.json", + Map.ofEntries()); + + Set issues = + executeResponseChecks(Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, ""), jsonBody); + assertFalse(issues.isEmpty()); + } + + private Set executeResponseChecks( + Map ovsFilterParameterStringMap, JsonNode serviceNodes) { + Set issues = new HashSet<>(); + + OvsChecks.buildResponseContentChecks(ovsFilterParameterStringMap) + .forEach( + validator -> { + issues.addAll(validator.validate(serviceNodes)); + }); + return issues; + } + + @Test + void testCheckThatScheduleValuesMatchParamValues_match() { + Map filterParametersMap = + Map.of( + OvsFilterParameter.CARRIER_SERVICE_CODE, + "GLS", + OvsFilterParameter.CARRIER_SERVICE_NAME, + "Great Lion Service", + OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE, + "SR12345A"); + + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckThatScheduleValuesMatchParamValues_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.CARRIER_SERVICE_CODE, "BW1"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckCarrierServiceName_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.CARRIER_SERVICE_NAME, "Great Lion Service"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckCarrierServiceName_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.CARRIER_SERVICE_NAME, "Great Tiger Service"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckUniversalServiceReference_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE, "SR12345A"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckUniversalServiceReference_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.UNIVERSAL_SERVICE_REFERENCE, "SRA"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckVesselIMONumber_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.VESSEL_IMO_NUMBER, "1234567"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckVesselIMONumber_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.VESSEL_IMO_NUMBER, "1234"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckVesselName_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.VESSEL_NAME, "Great Vessel"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckVesselName_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.VESSEL_NAME, "Great Bowl"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckCarrierVoyageNumber_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.CARRIER_VOYAGE_NUMBER, "2104N,2104S"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckCarrierVoyageNumber_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.CARRIER_VOYAGE_NUMBER, "2104P,2104Q"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckUniversalVoyageReference_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.UNIVERSAL_VOYAGE_REFERENCE, "SR12345A,SR45678A"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckUniversalVoyageReference_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.UNIVERSAL_VOYAGE_REFERENCE, "SR1245A,SR458A"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckUNLocationCode_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.UN_LOCATION_CODE, "NLAMS"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckUNLocationCode_noMatch() { + Map filterParametersMap = + Map.of(OvsFilterParameter.UN_LOCATION_CODE, "USNYC"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckFacilitySMDGCode_match() { + Map filterParametersMap = + Map.of(OvsFilterParameter.FACILITY_SMDG_CODE, "APM"); + JsonNode transportCall = + serviceNodes.get(0).get("vesselSchedules").get(0).get("transportCalls").get(0); + ((ObjectNode) transportCall.get("location")).put("facilitySMDGCode", "APM"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testCheckFacilitySMDGCode_noMatch() { + JsonNode transportCall = + serviceNodes.get(0).get("vesselSchedules").get(0).get("transportCalls").get(0); + ((ObjectNode) transportCall.get("location")).put("facilitySMDGCode", "APM"); + Map filterParametersMap = + Map.of(OvsFilterParameter.FACILITY_SMDG_CODE, "APP"); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertFalse(result.isEmpty()); + } + + @Test + void testCheckThatScheduleValuesMatchParamValues_emptyParams() { + Map filterParametersMap = Collections.emptyMap(); + Set result = + OvsChecks.checkThatScheduleValuesMatchParamValues(serviceNodes, filterParametersMap); + assertTrue(result.isEmpty()); + } + + @Test + void testValidateDate_dateWithinRange() { + Map filterParametersMap = + Map.of(OvsFilterParameter.START_DATE, "2024-07-19"); + Set result = + OvsChecks.validateDate( + serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore); + assertTrue(result.isEmpty()); + } + + @Test + void testValidateDate_dateOutsideRange() { + Map filterParametersMap = + Map.of(OvsFilterParameter.START_DATE, "2024-07-30"); + Set result = + OvsChecks.validateDate( + serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore); + assertFalse(result.isEmpty()); + } + + @Test + void testValidateDate_noStartDate() { + Map filterParametersMap = Collections.emptyMap(); + Set result = + OvsChecks.validateDate( + serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore); + assertTrue(result.isEmpty()); + } + + @Test + void testValidateUniqueTransportCallReference_unique() { + JsonNode vesselSchedules = createServiceVesselSchedules("1234567", "Great Vessel"); + JsonNode serviceNodes = + createServiceNodes("Great Lion Service", "GLS", "SR12345A", vesselSchedules); + JsonNode transportCalls = vesselSchedules.get(0).get("transportCalls"); + + ObjectNode newTransportCall = + new ObjectMapper().createObjectNode().put("transportCallReference", "TCREF2"); + ((ArrayNode) transportCalls).add(newTransportCall); + Set result = OvsChecks.validateUniqueTransportCallReference(serviceNodes); + assertTrue(result.isEmpty()); + } + + @Test + void testValidateUniqueTransportCallReference_duplicate() { + JsonNode vesselSchedules = createServiceVesselSchedules("1234567", "Great Vessel"); + + JsonNode serviceNodes = + createServiceNodes("Great Lion Service", "GLS", "SR12345A", vesselSchedules); + JsonNode transportCalls = vesselSchedules.get(0).get("transportCalls"); + + ObjectNode newTransportCall = + OBJECT_MAPPER.createObjectNode().put("transportCallReference", "TCREF1"); + ((ArrayNode) transportCalls).add(newTransportCall); + + Set result = OvsChecks.validateUniqueTransportCallReference(serviceNodes); + assertFalse(result.isEmpty()); + assertEquals(1, result.size()); + } + + @Test + void testValidateUniqueTransportCallReference_noVesselSchedules() { + ((ObjectNode) serviceNodes.get(0)).remove("vesselSchedules"); + Set result = OvsChecks.validateUniqueTransportCallReference(serviceNodes); + assertTrue(result.isEmpty()); + } + + @Test + void testFindMatchingNodes_rootMatch() { + JsonNode root = OBJECT_MAPPER.createObjectNode().put("value", "test"); + Stream result = OvsChecks.findMatchingNodes(root, "/").map(Map.Entry::getValue); + assertEquals(1, result.count()); + } + + @Test + void testFindMatchingNodes_arrayMatch() throws IOException { + JsonNode root = OBJECT_MAPPER.readTree("[{\"a\": 1}, {\"b\": 2}]"); + Stream result = OvsChecks.findMatchingNodes(root, "*").map(Map.Entry::getValue); + ; + assertEquals(2, result.count()); + } + + @Test + void testFindMatchingNodes_emptyArrayMatch() throws IOException { + JsonNode root = OBJECT_MAPPER.readTree("[]"); + Stream result = OvsChecks.findMatchingNodes(root, "*").map(Map.Entry::getValue); + ; + assertEquals(0, result.count()); + } + + @Test + void testCheckServiceSchedulesExist_emptyServiceSchedules() { + JsonNode body = OBJECT_MAPPER.createArrayNode(); + Set result = OvsChecks.checkServiceSchedulesExist(body); + assertEquals(1, result.size()); + } + + @Test + void testCheckServiceSchedulesExist_nullServiceNode() { + Set result = OvsChecks.checkServiceSchedulesExist(null); + assertEquals(1, result.size()); + } + + @Test + void testValidateDate_invalidDateFormat() { + Map filterParametersMap = + Map.of(OvsFilterParameter.START_DATE, "2024-07-19"); + JsonNode timeStamps = + serviceNodes + .get(0) + .get("vesselSchedules") + .get(0) + .get("transportCalls") + .get(0) + .get("timestamps"); + + ObjectNode invalidTimeStamp = + OBJECT_MAPPER.createObjectNode().put("eventDateTime", "TCREF1"); + ((ArrayNode) timeStamps).add(invalidTimeStamp); + + Set result = + OvsChecks.validateDate( + serviceNodes, filterParametersMap, OvsFilterParameter.START_DATE, LocalDate::isBefore); + assertTrue(result.isEmpty()); + } + + // Helper method to create a sample JsonNode for vessel schedules + private JsonNode createServiceNodes( + String carrierServiceName, + String carrierServiceCode, + String universalServiceReference, + JsonNode vesselSchedules) { + + // Create the root ArrayNode + ArrayNode rootArrayNode = OBJECT_MAPPER.createArrayNode(); + + // Create the first ObjectNode + ObjectNode firstObjectNode = OBJECT_MAPPER.createObjectNode(); + firstObjectNode.put("carrierServiceName", carrierServiceName); + firstObjectNode.put("carrierServiceCode", carrierServiceCode); + firstObjectNode.put("universalServiceReference", universalServiceReference); + firstObjectNode.set("vesselSchedules", vesselSchedules); + + rootArrayNode.add(firstObjectNode); + return rootArrayNode; + } + + private JsonNode createServiceVesselSchedules(String vesselIMONumber, String vesselName) { + ArrayNode vesselSchedulesArrayNode = OBJECT_MAPPER.createArrayNode(); + ObjectNode vesselSchedule = OBJECT_MAPPER.createObjectNode(); + vesselSchedule.put("vesselIMONumber", vesselIMONumber); + vesselSchedule.put("vesselName", vesselName); + vesselSchedule.set( + "transportCalls", + createTransportCalls("TCREF1", "2104N", "2104S", "SR12345A", "SR45678A", "NLAMS")); + vesselSchedulesArrayNode.add(vesselSchedule); + return vesselSchedulesArrayNode; + } + + private JsonNode createTransportCalls( + String transportCallReference, + String carrierImportVoyageNumber, + String carrierExportVoyageNumber, + String universalImportVoyageReference, + String universalExportVoyageReference, + String UNLocationCode) { + // Create the transportCalls ArrayNode for the vesselSchedule + ArrayNode transportCallsArrayNode = OBJECT_MAPPER.createArrayNode(); + ObjectNode transportCall = OBJECT_MAPPER.createObjectNode(); + transportCall.put("transportCallReference", transportCallReference); + transportCall.put("carrierImportVoyageNumber", carrierImportVoyageNumber); + transportCall.put("carrierExportVoyageNumber", carrierExportVoyageNumber); + transportCall.put("universalImportVoyageReference", universalImportVoyageReference); + transportCall.put("universalExportVoyageReference", universalExportVoyageReference); + + // Create the location ObjectNode for the first transportCall + ObjectNode location = OBJECT_MAPPER.createObjectNode(); + location.put("UNLocationCode", UNLocationCode); + transportCall.set("location", location); + transportCall.set("timestamps", createTimestamps()); + transportCallsArrayNode.add(transportCall); + return transportCallsArrayNode; + } + + private JsonNode createEventDateTime(String eventDateTime) { + // Create a timestamp for timestamps ArrayNode + ObjectNode timestamp = OBJECT_MAPPER.createObjectNode(); + timestamp.put("eventTypeCode", "ARRI"); + timestamp.put("eventClassifierCode", "PLN"); + timestamp.put("eventDateTime", eventDateTime); + return timestamp; + } + + private JsonNode createTimestamps() { + // Create the timestamps ArrayNode + ArrayNode timestampsArrayNode = OBJECT_MAPPER.createArrayNode(); + timestampsArrayNode.add(createEventDateTime("2024-07-21T10:00:00Z")); + timestampsArrayNode.add(createEventDateTime("2024-07-22T10:00:00Z")); + timestampsArrayNode.add(createEventDateTime("2024-07-23T10:00:00Z")); + return timestampsArrayNode; + } +} diff --git a/ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json b/ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json new file mode 100644 index 00000000..4b92b204 --- /dev/null +++ b/ovs/src/test/resources/messages/ovs-300-response-wrong-attribute-values.json @@ -0,0 +1,119 @@ +[ + { + "carrierServiceName": "Great Lioness Service ", + "carrierServiceCode": "AA1", + "universalServiceReference": "SRSSS12345A", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "HLC", + "vesselIMONumber": "9321483", + "vesselName": "King of the Roads", + "vesselCallSign": "NCVV", + "isDummyVessel": true, + "transportCalls": [ + { + "portVisitReference": "NLAMS1234589", + "transportCallReference": "SR11X-9321483-2107W-NLAMS-ACT-1-1", + "carrierImportVoyageNumber": "2755N", + "carrierExportVoyageNumber": "2654S", + "universalImportVoyageReference": "2544N", + "universalExportVoyageReference": "2765S", + "location": { + "locationName": "Port of Amsterdam", + "locationType": "UNLO", + "UNLocationCode": "NLERRAMS" + }, + "statusCode": "OMIT", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "PLN", + "eventDateTime": "2025-01-14T09:21:00+01:00", + "delayReasonCode": "WEA", + "changeRemark": "Bad weather" + } + ] + } + ] + } + ] + }, + { + "carrierServiceName": "Great Tiger Service", + "carrierServiceCode": "FK1", + "universalServiceReference": "SR6789SSS0B", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MSC", + "vesselIMONumber": "9456789", + "vesselName": "Eyes of the Tiger", + "vesselCallSign": "QOCE", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "SGSIN1234567", + "transportCallReference": "SR22XXXY-9456789-2108E-SGSIN-ACT-2-2", + "carrierImportVoyageNumber": "2348N", + "carrierExportVoyageNumber": "2876S", + "universalImportVoyageReference": "2887N", + "universalExportVoyageReference": "2343S", + "location": { + "locationName": "Port of Singapore", + "locationType": "UNLO", + "UNLocationCode": "SGSIN" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "DEPA", + "eventClassifierCode": "ACT", + "eventDateTime": "2025-02-20T15:30:00+08:00", + "delayReasonCode": "TRF", + "changeRemark": "Traffic congestion" + } + ] + } + ] + } + ] + }, + { + "carrierServiceName": "Red Eagle Service", + "carrierServiceCode": "RF1", + "universalServiceReference": "SR54321C", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MAE", + "vesselIMONumber": "9876543", + "vesselName": "Eagle of the Rats", + "vesselCallSign": "EGLS", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "USNYC1234567", + "transportCallReference": "SR33333Z-9876543-2109W-USNYC-ACT-3-3", + "carrierImportVoyageNumber": "2765N", + "carrierExportVoyageNumber": "2889S", + "universalImportVoyageReference": "2444N", + "universalExportVoyageReference": "2555S", + "location": { + "locationName": "Port of New York", + "locationType": "UNLO", + "UNLocationCode": "USSSYC" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "ACT", + "eventDateTime": "2025-03-15T10:00:00-05:00", + "delayReasonCode": "TRF", + "changeRemark": "Heavy traffic" + } + ] + } + ] + } + ] + } +] diff --git a/ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json b/ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json new file mode 100644 index 00000000..257bc336 --- /dev/null +++ b/ovs/src/test/resources/messages/ovs-300-response-wrong-date-times.json @@ -0,0 +1,118 @@ +[ + { + "carrierServiceName": "Great Lion Servicedd", + "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": "2103S", + "location": { + "locationName": "Port of Amsterdam", + "locationType": "UNLO", + "UNLocationCode": "NLAMS" + }, + "statusCode": "OMIT", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "PLN", + "delayReasonCode": "WEA", + "changeRemark": "Bad weather" + } + ] + } + ] + } + ] + }, + { + "carrierServiceName": "Great Lion Servicedd", + "carrierServiceCode": "BW1", + "universalServiceReference": "SR67890B", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MSC", + "vesselIMONumber": "9456789", + "vesselName": "Queen of the Oceans", + "vesselCallSign": "QOCE", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "SGSIN1234567", + "transportCallReference": "SR22222Y-9456789-2108E-SGSIN-ACT-2-2", + "carrierImportVoyageNumber": "2104N", + "carrierExportVoyageNumber": "2104S", + "universalImportVoyageReference": "2104N", + "universalExportVoyageReference": "2104S", + "location": { + "locationName": "Port of Singapore", + "locationType": "UNLO", + "UNLocationCode": "SGSIN" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "DEPA", + "eventClassifierCode": "ACT", + "eventDateTime": "2025/02/20T15:30:00+08:00", + "delayReasonCode": "TRF", + "changeRemark": "Traffic congestion" + } + ] + } + ] + } + ] + }, + { + "carrierServiceName": "Red Falcon Service", + "carrierServiceCode": "RF1", + "universalServiceReference": "SR54321C", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MAE", + "vesselIMONumber": "9876543", + "vesselName": "Eagle of the Seas", + "vesselCallSign": "EGLS", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "USNYC1234567", + "transportCallReference": "SR33333Z-9876543-2109W-USNYC-ACT-3-3", + "carrierImportVoyageNumber": "2105N", + "carrierExportVoyageNumber": "2105S", + "universalImportVoyageReference": "2105N", + "universalExportVoyageReference": "2105S", + "location": { + "locationName": "Port of New York", + "locationType": "UNLO", + "UNLocationCode": "USNYC" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "ACT", + "eventDateTime": "2025.03.15T10:00:00-05:00", + "delayReasonCode": "TRF", + "changeRemark": "Heavy traffic" + } + ] + } + ] + } + ] + } +] diff --git a/ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json b/ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json new file mode 100644 index 00000000..a94748c6 --- /dev/null +++ b/ovs/src/test/resources/messages/ovs-300-response-wrong-structure.json @@ -0,0 +1,119 @@ +[ + { + "carrierServiceName": "Great Lion Servicedd", + "carrierServiceCode": "FE1", + "universalServiceReference": "SR12345A", + "vessels": [ + { + "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": "2103S", + "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" + } + ] + } + ] + } + ] + }, + { + "carrierServiceName": "Great Lion Servicedd", + "carrierServiceCode": "BW1", + "universalServiceReference": "SR67890B", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MSC", + "vesselIMONumber": "9456789", + "vesselName": "Queen of the Oceans", + "vesselCallSign": "QOCE", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "SGSIN1234567", + "transportCallReference": "SR22222Y-9456789-2108E-SGSIN-ACT-2-2", + "carrierImportVoyageNumber": "2104N", + "carrierExportVoyageNumber": "2104S", + "universalImportVoyageReference": "2104N", + "universalExportVoyageReference": "2104S", + "location": { + "locationName": "Port of Singapore", + "locationType": "UNLO", + "UNLocationCode": "SGSIN" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "DEPA", + "eventClassifierCode": "ACT", + "eventDateTime": "2025-02-20T15:30:00+08:00", + "delayReasonCode": "TRF", + "changeRemark": "Traffic congestion" + } + ] + } + ] + } + ] + }, + { + "carrierServiceName": "Red Falcon Service", + "carrierServiceCode": "RF1", + "universalServiceReference": "SR54321C", + "vesselSchedules": [ + { + "vesselOperatorSMDGLinerCode": "MAE", + "vesselIMONumber": "9876543", + "vesselName": "Eagle of the Seas", + "vesselCallSign": "EGLS", + "isDummyVessel": false, + "transportCalls": [ + { + "portVisitReference": "USNYC1234567", + "transportCallReference": "SR33333Z-9876543-2109W-USNYC-ACT-3-3", + "carrierImportVoyageNumber": "2105N", + "carrierExportVoyageNumber": "2105S", + "universalImportVoyageReference": "2105N", + "universalExportVoyageReference": "2105S", + "location": { + "locationName": "Port of New York", + "locationType": "UNLO", + "UNLocationCode": "USNYC" + }, + "statusCode": "ARRI", + "timestamps": [ + { + "eventTypeCode": "ARRI", + "eventClassifierCode": "ACT", + "eventDateTime": "2025-03-15T10:00:00-05:00", + "delayReasonCode": "TRF", + "changeRemark": "Heavy traffic" + } + ] + } + ] + } + ] + } +]