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"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]