controlFields, ControlField controlField) {
diff --git a/src/main/java/org/folio/processing/matching/loader/query/LoadQueryBuilder.java b/src/main/java/org/folio/processing/matching/loader/query/LoadQueryBuilder.java
index d3663ea4..e347e601 100644
--- a/src/main/java/org/folio/processing/matching/loader/query/LoadQueryBuilder.java
+++ b/src/main/java/org/folio/processing/matching/loader/query/LoadQueryBuilder.java
@@ -1,6 +1,9 @@
package org.folio.processing.matching.loader.query;
+import io.vertx.core.json.Json;
import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.folio.MatchDetail;
import org.folio.processing.value.StringValue;
import org.folio.processing.value.Value;
@@ -8,6 +11,7 @@
import org.folio.rest.jaxrs.model.Field;
import org.folio.rest.jaxrs.model.MatchExpression;
+import java.util.ArrayList;
import java.util.List;
import static org.folio.processing.value.Value.ValueType.DATE;
@@ -23,11 +27,23 @@ public class LoadQueryBuilder {
private LoadQueryBuilder() {
}
+ private static final Logger LOGGER = LogManager.getLogger(LoadQueryBuilder.class);
private static final String JSON_PATH_SEPARATOR = ".";
private static final String IDENTIFIER_TYPE_ID = "identifierTypeId";
private static final String IDENTIFIER_TYPE_VALUE = "instance.identifiers[].value";
- private static final String IDENTIFIER_CQL_QUERY = "identifiers =/@value/@identifierTypeId=\"%s\" %s";
- private static final String WHERE_CLAUSE_CONSTRUCTOR_MATCH_CRITERION = "WHERE_CLAUSE_CONSTRUCTOR";
+ /**
+ * CQL query template to find an instance by a specific identifier.
+ *
+ * This query leverages a relation modifier ({@code @}) to efficiently search within the 'identifiers' JSON array.
+ *
+ * - {@code @identifierTypeId=%s}: Filters array elements to only include those where the 'identifierTypeId'
+ * matches the first placeholder.
+ * - {@code "%s"}: The search term (the identifier's value) is then matched against the 'value' subfield
+ * of the filtered elements.
+ *
+ * This syntax allows PostgreSQL to use the GIN index on the field consistently, improving query performance.
+ */
+ private static final String IDENTIFIER_INDIVIDUAL_CQL_QUERY = "identifiers =/@identifierTypeId=%s \"%s\"";
/**
* Builds LoadQuery,
@@ -39,13 +55,13 @@ private LoadQueryBuilder() {
* @param matchDetail match detail
* @return LoadQuery or null if query cannot be built
*/
- public static LoadQuery build(Value value, MatchDetail matchDetail) {
+ public static LoadQuery build(Value> value, MatchDetail matchDetail) {
if (value != null && (value.getType() == STRING || value.getType() == LIST || value.getType() == DATE)) {
MatchExpression matchExpression = matchDetail.getExistingMatchExpression();
if (matchExpression != null && matchExpression.getDataValueType() == VALUE_FROM_RECORD) {
List fields = matchExpression.getFields();
if (fields != null && !fields.isEmpty()) {
- String fieldPath = fields.get(0).getValue();
+ String fieldPath = fields.getFirst().getValue();
String tableName = StringUtils.substringBefore(fieldPath, JSON_PATH_SEPARATOR);
String fieldName = StringUtils.substringAfter(fieldPath, JSON_PATH_SEPARATOR);
QueryHolder mainQuery = new QueryHolder(value, matchDetail.getMatchCriterion())
@@ -61,13 +77,16 @@ public static LoadQuery build(Value value, MatchDetail matchDetail) {
mainQuery.applyAdditionalCondition(additionalQuery);
// TODO provide all the requirements for MODDATAIMP-592 and refactor code block below
if(checkIfIdentifierTypeExists(matchDetail, fieldPath, additionalField.getLabel())) {
- MatchingCondition matchingCondition =
- MatchingCondition.valueOf(WHERE_CLAUSE_CONSTRUCTOR_MATCH_CRITERION);
- String condition = matchingCondition.constructCqlQuery(value);
- mainQuery.setCqlQuery(String.format(IDENTIFIER_CQL_QUERY, additionalField.getValue(), condition));
+ String cqlQuery = buildIdentifierCqlQuery(value, additionalField.getValue(), matchDetail.getMatchCriterion());
+ mainQuery.setCqlQuery(cqlQuery);
mainQuery.setSqlQuery(StringUtils.EMPTY);
+ } else {
+ LOGGER.debug("LoadQueryBuilder::build - Additional field does not match identifier type criteria: {} fieldPath: {}",
+ additionalField.getLabel(), fieldPath);
}
}
+ LOGGER.debug(() -> String.format("LoadQueryBuilder::build - Built LoadQuery for VALUE: ~| %s |~ MATCHDETAIL: ~| %s |~ CQL: ~| %s |~",
+ Json.encode(value), Json.encode(matchDetail), mainQuery.getCqlQuery()));
return new DefaultJsonLoadQuery(tableName, mainQuery.getSqlQuery(), mainQuery.getCqlQuery());
}
}
@@ -77,8 +96,54 @@ public static LoadQuery build(Value value, MatchDetail matchDetail) {
private static boolean checkIfIdentifierTypeExists(MatchDetail matchDetail, String fieldPath, String additionalFieldPath) {
return matchDetail.getIncomingRecordType() == EntityType.MARC_BIBLIOGRAPHIC && matchDetail.getExistingRecordType() == EntityType.INSTANCE &&
- matchDetail.getMatchCriterion() == MatchDetail.MatchCriterion.EXACTLY_MATCHES && fieldPath.equals(IDENTIFIER_TYPE_VALUE) &&
- additionalFieldPath.equals(IDENTIFIER_TYPE_ID);
+ (matchDetail.getMatchCriterion() == MatchDetail.MatchCriterion.EXACTLY_MATCHES ||
+ matchDetail.getMatchCriterion() == MatchDetail.MatchCriterion.EXISTING_VALUE_CONTAINS_INCOMING_VALUE) &&
+ fieldPath.equals(IDENTIFIER_TYPE_VALUE) && additionalFieldPath.equals(IDENTIFIER_TYPE_ID);
+ }
+
+ /**
+ * Builds CQL query for identifier matching with individual AND conditions for each value
+ *
+ * @param value the value to match against (can be STRING or LIST)
+ * @param identifierTypeId the identifier type ID
+ * @param matchCriterion the match criterion to determine if wildcards should be applied
+ * @return CQL query string with individual AND conditions
+ */
+ private static String buildIdentifierCqlQuery(Value> value, String identifierTypeId, MatchDetail.MatchCriterion matchCriterion) {
+ if (value.getType() == STRING) {
+ String escapedValue = escapeCqlValue(value.getValue().toString());
+ if (matchCriterion == MatchDetail.MatchCriterion.EXISTING_VALUE_CONTAINS_INCOMING_VALUE) {
+ escapedValue = "*" + escapedValue + "*";
+ }
+ return String.format(IDENTIFIER_INDIVIDUAL_CQL_QUERY, identifierTypeId, escapedValue);
+ } else if (value.getType() == LIST) {
+ List conditions = new ArrayList<>();
+ for (Object val : ((org.folio.processing.value.ListValue) value).getValue()) {
+ String escapedValue = escapeCqlValue(val.toString());
+ if (matchCriterion == MatchDetail.MatchCriterion.EXISTING_VALUE_CONTAINS_INCOMING_VALUE) {
+ escapedValue = "*" + escapedValue + "*";
+ }
+ conditions.add(String.format(IDENTIFIER_INDIVIDUAL_CQL_QUERY, identifierTypeId, escapedValue));
+ }
+ return String.join(" OR ", conditions);
+ }
+ return "";
+ }
+
+ /**
+ * Escapes special characters in CQL values to prevent parsing errors
+ *
+ * @param value the value to escape
+ * @return escaped value safe for CQL queries
+ */
+ private static String escapeCqlValue(String value) {
+ // Escape backslashes first, then other special characters
+ return value.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("(", "\\(")
+ .replace(")", "\\)")
+ .replace("*", "\\*")
+ .replace("?", "\\?");
}
}
diff --git a/src/test/java/org/folio/processing/TestUtil.java b/src/test/java/org/folio/processing/TestUtil.java
index 6550dce8..3eedd7bd 100644
--- a/src/test/java/org/folio/processing/TestUtil.java
+++ b/src/test/java/org/folio/processing/TestUtil.java
@@ -1,7 +1,7 @@
package org.folio.processing;
import org.apache.commons.io.FileUtils;
-
+import org.testcontainers.utility.DockerImageName;
import java.io.File;
import java.io.IOException;
@@ -10,6 +10,8 @@
*/
public final class TestUtil {
+ public static final DockerImageName KAFKA_CONTAINER_NAME = DockerImageName.parse("apache/kafka-native:3.8.0");
+
public static String readFileFromPath(String path) throws IOException {
return new String(FileUtils.readFileToByteArray(new File(path)));
}
diff --git a/src/test/java/org/folio/processing/events/AbstractRestTest.java b/src/test/java/org/folio/processing/events/AbstractRestTest.java
index 11992a9b..bcfc195f 100644
--- a/src/test/java/org/folio/processing/events/AbstractRestTest.java
+++ b/src/test/java/org/folio/processing/events/AbstractRestTest.java
@@ -3,6 +3,8 @@
import com.github.tomakehurst.wiremock.common.Slf4jNotifier;
import com.github.tomakehurst.wiremock.core.WireMockConfiguration;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.junit.Rule;
import java.io.IOException;
@@ -10,6 +12,9 @@
import java.util.concurrent.ThreadLocalRandom;
public abstract class AbstractRestTest {
+
+ private static final Logger LOGGER = LogManager.getLogger(AbstractRestTest.class);
+
protected final String TENANT_ID = "diku";
protected final String TOKEN = "token";
private int PORT = nextFreePort();
@@ -22,6 +27,8 @@ public abstract class AbstractRestTest {
.notifier(new Slf4jNotifier(true)));
public static int nextFreePort() {
+ LOGGER.trace("nextFreePort:: creating random port");
+
int maxTries = 10000;
int port = ThreadLocalRandom.current().nextInt(49152 , 65535);
while (true) {
@@ -38,6 +45,7 @@ public static int nextFreePort() {
}
public static boolean isLocalPortFree(int port) {
+ LOGGER.trace("isLocalPortFree:: checking if port {} is free", port);
try {
new ServerSocket(port).close();
return true;
diff --git a/src/test/java/org/folio/processing/events/EventManagerTest.java b/src/test/java/org/folio/processing/events/EventManagerTest.java
index 4dd73aff..a3eae088 100644
--- a/src/test/java/org/folio/processing/events/EventManagerTest.java
+++ b/src/test/java/org/folio/processing/events/EventManagerTest.java
@@ -4,35 +4,33 @@
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.RunTestOnContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
-import net.mguenther.kafka.junit.EmbeddedKafkaCluster;
import org.folio.kafka.KafkaConfig;
+import org.folio.processing.TestUtil;
import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-
-import static net.mguenther.kafka.junit.EmbeddedKafkaCluster.provisionWith;
-import static net.mguenther.kafka.junit.EmbeddedKafkaClusterConfig.defaultClusterConfig;
+import org.testcontainers.kafka.KafkaContainer;
@RunWith(VertxUnitRunner.class)
public class EventManagerTest {
private static final String KAFKA_ENV = "folio";
+ @ClassRule
+ public static KafkaContainer kafkaContainer = new KafkaContainer(TestUtil.KAFKA_CONTAINER_NAME);
+ private static KafkaConfig kafkaConfig;
+
@Rule
public RunTestOnContext rule = new RunTestOnContext();
- public static EmbeddedKafkaCluster kafkaCluster;
- private static KafkaConfig kafkaConfig;
@BeforeClass
public static void setUpClass() {
- kafkaCluster = provisionWith(defaultClusterConfig());
- kafkaCluster.start();
- String[] hostAndPort = kafkaCluster.getBrokerList().split(":");
kafkaConfig = KafkaConfig.builder()
- .kafkaHost(hostAndPort[0])
- .kafkaPort(hostAndPort[1])
- .envId(KAFKA_ENV)
- .build();
+ .kafkaHost(kafkaContainer.getHost())
+ .kafkaPort(kafkaContainer.getFirstMappedPort() + "")
+ .envId(KAFKA_ENV)
+ .build();
}
@Test
diff --git a/src/test/java/org/folio/processing/events/EventManagerUnitTest.java b/src/test/java/org/folio/processing/events/EventManagerUnitTest.java
index fe82a9c8..aceb11cb 100644
--- a/src/test/java/org/folio/processing/events/EventManagerUnitTest.java
+++ b/src/test/java/org/folio/processing/events/EventManagerUnitTest.java
@@ -5,6 +5,8 @@
import io.vertx.ext.unit.Async;
import io.vertx.ext.unit.TestContext;
import io.vertx.ext.unit.junit.VertxUnitRunner;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
import org.folio.ActionProfile;
import org.folio.DataImportEventPayload;
import org.folio.JobProfile;
@@ -51,16 +53,19 @@
@RunWith(VertxUnitRunner.class)
public class EventManagerUnitTest extends AbstractRestTest {
+ private static final Logger LOGGER = LogManager.getLogger(EventManagerUnitTest.class);
private final String PUBLISH_SERVICE_URL = "/pubsub/publish";
@Before
public void beforeTest() {
EventManager.clearEventHandlers();
+ EventManager.registerRestEventPublisher();
WireMock.stubFor(WireMock.post(PUBLISH_SERVICE_URL).willReturn(WireMock.noContent()));
}
@Test
public void shouldHandleEvent(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleEvent");
Async async = testContext.async();
// given
EventManager.registerEventHandler(new CreateInstanceEventHandler());
@@ -95,9 +100,10 @@ public void shouldHandleEvent(TestContext testContext) {
.withOkapiUrl(OKAPI_URL)
.withToken(TOKEN)
.withContext(new HashMap<>())
- .withCurrentNode(profileSnapshot.getChildSnapshotWrappers().get(0));
+ .withCurrentNode(profileSnapshot.getChildSnapshotWrappers().getFirst());
// when
- EventManager.handleEvent(eventPayload, profileSnapshot).whenComplete((nextEventContext, throwable) -> {
+ EventManager.handleEvent(eventPayload, profileSnapshot)
+ .whenComplete((nextEventContext, throwable) -> {
// then
testContext.assertNull(throwable);
testContext.assertEquals(1, nextEventContext.getEventsChain().size());
@@ -112,6 +118,7 @@ public void shouldHandleEvent(TestContext testContext) {
@Test
public void shouldHandleLastEvent(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleLastEvent");
Async async = testContext.async();
// given
EventManager.registerEventHandler(new CreateInstanceEventHandler());
@@ -134,7 +141,7 @@ public void shouldHandleLastEvent(TestContext testContext) {
.withOkapiUrl(OKAPI_URL)
.withToken(TOKEN)
.withContext(new HashMap<>())
- .withCurrentNode(jobProfileSnapshot.getChildSnapshotWrappers().get(0));
+ .withCurrentNode(jobProfileSnapshot.getChildSnapshotWrappers().getFirst());
// when
EventManager.handleEvent(eventPayload, jobProfileSnapshot).whenComplete((nextEventContext, throwable) -> {
// then
@@ -151,6 +158,7 @@ public void shouldHandleLastEvent(TestContext testContext) {
@Test
public void shouldIgnoreEventIfNoHandlersDefined(TestContext testContext) {
+ LOGGER.info("test:: shouldIgnoreEventIfNoHandlersDefined");
Async async = testContext.async();
// given
ProfileSnapshotWrapper profileSnapshot = new ProfileSnapshotWrapper()
@@ -167,7 +175,7 @@ public void shouldIgnoreEventIfNoHandlersDefined(TestContext testContext) {
.withOkapiUrl(OKAPI_URL)
.withToken(TOKEN)
.withContext(new HashMap<>())
- .withCurrentNode(profileSnapshot.getChildSnapshotWrappers().get(0));
+ .withCurrentNode(profileSnapshot.getChildSnapshotWrappers().getFirst());
// when
EventManager.handleEvent(eventPayload, profileSnapshot).whenComplete((nextEventContext, throwable) -> {
@@ -181,6 +189,7 @@ public void shouldIgnoreEventIfNoHandlersDefined(TestContext testContext) {
@Test
public void shouldHandleAsErrorEventIfHandlerCompletedExceptionally(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleAsErrorEventIfHandlerCompletedExceptionally");
Async async = testContext.async();
// given
EventManager.registerEventHandler(new FailExceptionallyHandler());
@@ -199,7 +208,7 @@ public void shouldHandleAsErrorEventIfHandlerCompletedExceptionally(TestContext
.withOkapiUrl(OKAPI_URL)
.withToken(TOKEN)
.withContext(new HashMap<>())
- .withCurrentNode(jobProfileSnapshot.getChildSnapshotWrappers().get(0));
+ .withCurrentNode(jobProfileSnapshot.getChildSnapshotWrappers().getFirst());
// when
EventManager.handleEvent(eventPayload, jobProfileSnapshot).whenComplete((nextEventContext, throwable) -> {
// then
@@ -212,6 +221,7 @@ public void shouldHandleAsErrorEventIfHandlerCompletedExceptionally(TestContext
@Test
public void shouldHandleFirstEventInJobProfile(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleFirstEventInJobProfile");
Async async = testContext.async();
// given
String jobProfileId = UUID.randomUUID().toString();
@@ -255,12 +265,13 @@ public void shouldHandleFirstEventInJobProfile(TestContext testContext) {
@Test
public void shouldHandleAndSetToCurrentNodeAction2Wrapper(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleAndSetToCurrentNodeAction2Wrapper");
Async async = testContext.async();
// given
CreateInstanceEventHandler createInstanceHandler = Mockito.spy(new CreateInstanceEventHandler());
Mockito.doAnswer(invocationOnMock -> {
DataImportEventPayload payload = invocationOnMock.getArgument(0);
- payload.setCurrentNode(payload.getCurrentNode().getChildSnapshotWrappers().get(0));
+ payload.setCurrentNode(payload.getCurrentNode().getChildSnapshotWrappers().getFirst());
return invocationOnMock.callRealMethod();
}).when(createInstanceHandler).handle(any(DataImportEventPayload.class));
@@ -330,6 +341,7 @@ public void shouldHandleAndSetToCurrentNodeAction2Wrapper(TestContext testContex
@Test
public void shouldHandleAndSetToCurrentNodeAction1Wrapper(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleAndSetToCurrentNodeAction1Wrapper");
Async async = testContext.async();
// given
EventHandler matchInstanceHandler = Mockito.mock(EventHandler.class);
@@ -387,13 +399,14 @@ public void shouldHandleAndSetToCurrentNodeAction1Wrapper(TestContext testContex
@Test
public void shouldHandleEventInCascadingProfilesAndSwitchNode(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleEventInCascadingProfilesAndSwitchNode");
Async async = testContext.async();
// given
EventHandler updateInstanceHandler = Mockito.mock(EventHandler.class);
Mockito.doAnswer(invocationOnMock -> {
DataImportEventPayload payload = invocationOnMock.getArgument(0);
- payload.setCurrentNode(payload.getCurrentNode().getChildSnapshotWrappers().get(0));
+ payload.setCurrentNode(payload.getCurrentNode().getChildSnapshotWrappers().getFirst());
return CompletableFuture.completedFuture(payload.withEventType(DI_INVENTORY_INSTANCE_UPDATED.value()));
}).when(updateInstanceHandler).handle(any(DataImportEventPayload.class));
@@ -528,12 +541,13 @@ public void shouldHandleEventInCascadingProfilesAndSwitchNode(TestContext testCo
@Test
public void shouldHandleAndSetToCurrentNodeMatchWrapper2(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleAndSetToCurrentNodeMatchWrapper2");
Async async = testContext.async();
// given
EventHandler updateInstanceHandler = Mockito.mock(EventHandler.class);
Mockito.doAnswer(invocationOnMock -> {
DataImportEventPayload payload = invocationOnMock.getArgument(0);
- payload.setCurrentNode(payload.getCurrentNode().getChildSnapshotWrappers().get(0));
+ payload.setCurrentNode(payload.getCurrentNode().getChildSnapshotWrappers().getFirst());
return CompletableFuture.completedFuture(payload.withEventType(DI_INVENTORY_INSTANCE_UPDATED.value()));
}).when(updateInstanceHandler).handle(any(DataImportEventPayload.class));
Mockito.when(updateInstanceHandler.isEligible(any(DataImportEventPayload.class))).thenReturn(true);
@@ -593,6 +607,7 @@ public void shouldHandleAndSetToCurrentNodeMatchWrapper2(TestContext testContext
@Test
public void shouldHandleEventAndPreparePayloadForPostProcessing(TestContext testContext) {
+ LOGGER.info("test:: shouldHandleEventAndPreparePayloadForPostProcessing");
Async async = testContext.async();
// given
String jobProfileId = UUID.randomUUID().toString();
@@ -633,6 +648,7 @@ public void shouldHandleEventAndPreparePayloadForPostProcessing(TestContext test
@Test
public void shouldPerformEventPostProcessingAndPreparePayloadAfterPostProcessing(TestContext testContext) {
+ LOGGER.info("test:: shouldPerformEventPostProcessingAndPreparePayloadAfterPostProcessing");
Async async = testContext.async();
// given
String jobProfileId = UUID.randomUUID().toString();
@@ -678,6 +694,7 @@ public void shouldPerformEventPostProcessingAndPreparePayloadAfterPostProcessing
@Test
public void shouldClearExtraOLKeyFromPayload(TestContext testContext) {
+ LOGGER.info("test:: shouldClearExtraOLKeyFromPayload");
Async async = testContext.async();
// given
EventManager.registerEventHandler(new CreateInstanceEventHandler());
@@ -702,7 +719,7 @@ public void shouldClearExtraOLKeyFromPayload(TestContext testContext) {
.withOkapiUrl(OKAPI_URL)
.withToken(TOKEN)
.withContext(extraOLKey)
- .withCurrentNode(jobProfileSnapshot.getChildSnapshotWrappers().get(0));
+ .withCurrentNode(jobProfileSnapshot.getChildSnapshotWrappers().getFirst());
// when
EventManager.handleEvent(eventPayload, jobProfileSnapshot).whenComplete((nextEventContext, throwable) -> {
// then
diff --git a/src/test/java/org/folio/processing/events/services/publisher/KafkaEventPublisherTest.java b/src/test/java/org/folio/processing/events/services/publisher/KafkaEventPublisherTest.java
index 420ec02c..8828cb18 100644
--- a/src/test/java/org/folio/processing/events/services/publisher/KafkaEventPublisherTest.java
+++ b/src/test/java/org/folio/processing/events/services/publisher/KafkaEventPublisherTest.java
@@ -3,25 +3,26 @@
import io.vertx.core.Vertx;
import io.vertx.core.json.Json;
import io.vertx.ext.unit.junit.VertxUnitRunner;
-import net.mguenther.kafka.junit.EmbeddedKafkaCluster;
-import net.mguenther.kafka.junit.ObserveKeyValues;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.folio.DataImportEventPayload;
import org.folio.kafka.KafkaConfig;
import org.folio.kafka.KafkaTopicNameHelper;
+import org.folio.processing.TestUtil;
import org.folio.rest.jaxrs.model.Event;
import org.junit.BeforeClass;
+import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
-
+import org.testcontainers.kafka.KafkaContainer;
+import java.time.Duration;
import java.util.HashMap;
import java.util.List;
+import java.util.Properties;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import static net.mguenther.kafka.junit.EmbeddedKafkaCluster.provisionWith;
-import static net.mguenther.kafka.junit.EmbeddedKafkaClusterConfig.defaultClusterConfig;
import static org.folio.DataImportEventTypes.DI_COMPLETED;
import static org.folio.kafka.KafkaTopicNameHelper.getDefaultNameSpace;
import static org.junit.Assert.assertEquals;
@@ -35,30 +36,35 @@ public class KafkaEventPublisherTest {
private static final String TENANT_ID = "diku";
private static final String TOKEN = "stub-token";
- public static EmbeddedKafkaCluster kafkaCluster;
-
+ @ClassRule
+ public static KafkaContainer kafkaContainer = new KafkaContainer(TestUtil.KAFKA_CONTAINER_NAME);
private static KafkaConfig kafkaConfig;
+ private static Properties consumerConfig = new Properties();
private Vertx vertx = Vertx.vertx();
@BeforeClass
public static void setUpClass() {
- kafkaCluster = provisionWith(defaultClusterConfig());
- kafkaCluster.start();
- String[] hostAndPort = kafkaCluster.getBrokerList().split(":");
kafkaConfig = KafkaConfig.builder()
- .kafkaHost(hostAndPort[0])
- .kafkaPort(hostAndPort[1])
+ .kafkaHost(kafkaContainer.getHost())
+ .kafkaPort(kafkaContainer.getFirstMappedPort() + "")
.envId(KAFKA_ENV)
.build();
+ kafkaConfig.getConsumerProps().forEach((key, value) -> {
+ if (value != null) {
+ consumerConfig.put(key, value);
+ }
+ });
+ consumerConfig.put(ConsumerConfig.GROUP_ID_CONFIG, "test");
}
@Test
public void shouldPublishPayload() throws Exception {
+ var tenant = "shouldPublishPayload";
try(KafkaEventPublisher eventPublisher = new KafkaEventPublisher(kafkaConfig, vertx, 100)) {
DataImportEventPayload eventPayload = new DataImportEventPayload()
.withEventType(DI_COMPLETED.value())
.withOkapiUrl(OKAPI_URL)
- .withTenant(TENANT_ID)
+ .withTenant(tenant)
.withToken(TOKEN)
.withContext(new HashMap<>() {{
put("recordId", UUID.randomUUID().toString());
@@ -67,13 +73,8 @@ public void shouldPublishPayload() throws Exception {
CompletableFuture future = eventPublisher.publish(eventPayload);
- String topicToObserve = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), TENANT_ID, DI_COMPLETED.value());
- List observedValues = kafkaCluster.observeValues(ObserveKeyValues.on(topicToObserve, 1)
- .observeFor(30, TimeUnit.SECONDS)
- .build());
-
- Event obtainedEvent = Json.decodeValue(observedValues.get(0), Event.class);
- DataImportEventPayload actualPayload = Json.decodeValue(obtainedEvent.getEventPayload(), DataImportEventPayload.class);
+ String topicToObserve = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), tenant, DI_COMPLETED.value());
+ DataImportEventPayload actualPayload = Json.decodeValue(getEventPayload(topicToObserve), DataImportEventPayload.class);
assertEquals(eventPayload, actualPayload);
assertFalse(future.isCompletedExceptionally());
@@ -82,11 +83,12 @@ public void shouldPublishPayload() throws Exception {
@Test
public void shouldPublishPayloadIfTokenIsNull() throws Exception {
+ var tenant = "shouldPublishPayloadIfTokenIsNull";
try(KafkaEventPublisher eventPublisher = new KafkaEventPublisher(kafkaConfig, vertx, 100)) {
DataImportEventPayload eventPayload = new DataImportEventPayload()
.withEventType(DI_COMPLETED.value())
.withOkapiUrl(OKAPI_URL)
- .withTenant(TENANT_ID)
+ .withTenant(tenant)
.withToken(null)
.withContext(new HashMap<>() {{
put("recordId", UUID.randomUUID().toString());
@@ -96,13 +98,8 @@ public void shouldPublishPayloadIfTokenIsNull() throws Exception {
CompletableFuture future = eventPublisher.publish(eventPayload);
- String topicToObserve = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), TENANT_ID, DI_COMPLETED.value());
- List observedValues = kafkaCluster.observeValues(ObserveKeyValues.on(topicToObserve, 1)
- .observeFor(30, TimeUnit.SECONDS)
- .build());
-
- Event obtainedEvent = Json.decodeValue(observedValues.get(observedValues.size() - 1), Event.class);
- DataImportEventPayload actualPayload = Json.decodeValue(obtainedEvent.getEventPayload(), DataImportEventPayload.class);
+ String topicToObserve = KafkaTopicNameHelper.formatTopicName(KAFKA_ENV, getDefaultNameSpace(), tenant, DI_COMPLETED.value());
+ DataImportEventPayload actualPayload = Json.decodeValue(getEventPayload(topicToObserve), DataImportEventPayload.class);
assertEquals(eventPayload, actualPayload);
assertFalse(future.isCompletedExceptionally());
@@ -171,4 +168,16 @@ public void shouldReturnFailedFutureWhenChunkIdIsNull() throws Exception {
future.get();
}
}
+
+ private String getEventPayload(String topicToObserve) {
+ try (var kafkaConsumer = new KafkaConsumer(consumerConfig)) {
+ kafkaConsumer.subscribe(List.of(topicToObserve));
+ var records = kafkaConsumer.poll(Duration.ofSeconds(30));
+ if (records.isEmpty()) {
+ throw new IllegalStateException("Expected Kafka event at " + topicToObserve + " but got none");
+ }
+ Event obtainedEvent = Json.decodeValue(records.iterator().next().value(), Event.class);
+ return obtainedEvent.getEventPayload();
+ }
+ }
}
diff --git a/src/test/java/org/folio/processing/events/utils/PomReaderUtilTest.java b/src/test/java/org/folio/processing/events/utils/PomReaderUtilTest.java
index b2b3c122..bb2ce73f 100644
--- a/src/test/java/org/folio/processing/events/utils/PomReaderUtilTest.java
+++ b/src/test/java/org/folio/processing/events/utils/PomReaderUtilTest.java
@@ -50,7 +50,7 @@ void readFromJar() throws IOException, XmlPullParserException {
pom.readIt(null, "META-INF/maven/io.vertx"); // force reading from Jar
// first dependency in main pom
- assertThat(pom.getModuleName(), is("vertx_ext_parent"));
+ assertThat(pom.getModuleName(), is("vertx_parent"));
}
@Test
diff --git a/src/test/java/org/folio/processing/mapping/InstanceMappingTest.java b/src/test/java/org/folio/processing/mapping/InstanceMappingTest.java
index ebb42904..02ebe8ee 100644
--- a/src/test/java/org/folio/processing/mapping/InstanceMappingTest.java
+++ b/src/test/java/org/folio/processing/mapping/InstanceMappingTest.java
@@ -70,6 +70,7 @@ public class InstanceMappingTest {
private static final String BIB_WITH_REPEATED_600_SUBFIELD_AND_EMPTY_INDICATOR = "src/test/resources/org/folio/processing/mapping/instance/6xx_subjects_without_indicators.mrc";
private static final String BIB_WITH_008_DATE = "src/test/resources/org/folio/processing/mapping/instance/008_date.mrc";
private static final String BIB_WITHOUT_008_DATE = "src/test/resources/org/folio/processing/mapping/instance/008_empty_date.mrc";
+ private static final String BIB_WITH_INVALID_008_FIELD = "src/test/resources/org/folio/processing/mapping/instance/008_invalid_field.mrc";
private static final String BIB_WITH_DELETED_LEADER = "src/test/resources/org/folio/processing/mapping/instance/deleted_leader.mrc";
private static final String BIB_WITH_RESOURCE_TYPE_SUBFIELD_VALUE = "src/test/resources/org/folio/processing/mapping/instance/336_subfields_mapping.mrc";
private static final String BIB_WITH_720_FIELDS = "src/test/resources/org/folio/processing/mapping/instance/720_fields_samples.mrc";
@@ -691,6 +692,38 @@ public void testMarcToInstanceWithEmpty008Date() throws IOException {
assertEquals("77a09c3c-37bd-4ad3-aae4-9d86fc1b33d8", mappedInstances.get(0).getDates().getDateTypeId());
}
+ @Test
+ public void testMarcToInstanceWithEmpty008Field() throws IOException {
+ MarcReader reader = new MarcStreamReader(new ByteArrayInputStream(TestUtil.readFileFromPath(
+ BIB_WITH_INVALID_008_FIELD).getBytes(StandardCharsets.UTF_8)));
+ JsonObject mappingRules = new JsonObject(TestUtil.readFileFromPath(DEFAULT_MAPPING_RULES_PATH));
+ String rawInstanceDateTypes = TestUtil.readFileFromPath(DEFAULT_INSTANCE_DATE_TYPES_PATH);
+ List instanceDateTypes = List.of(new ObjectMapper().readValue(rawInstanceDateTypes, InstanceDateType[].class));
+
+
+ ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
+ List mappedInstances = new ArrayList<>();
+ while (reader.hasNext()) {
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ MarcJsonWriter writer = new MarcJsonWriter(os);
+ Record targetRecord = reader.next();
+ writer.write(targetRecord);
+ JsonObject marc = new JsonObject(os.toString());
+ Instance instance = mapper.mapRecord(marc, new MappingParameters().withInstanceDateTypes(instanceDateTypes), mappingRules);
+ mappedInstances.add(instance);
+ Validator validator = factory.getValidator();
+ Set> violations = validator.validate(instance);
+ assertTrue(violations.isEmpty());
+ }
+ assertFalse(mappedInstances.isEmpty());
+ assertEquals(1, mappedInstances.size());
+
+ Instance mappedInstance = mappedInstances.get(0);
+ assertNotNull(mappedInstance.getId());
+
+ assertNull(mappedInstances.get(0).getDates());
+ }
+
@Test
public void testMarcToInstanceWithRepeatableSubjectsMappedWithTypeButWithoutIndicators() throws IOException {
final String FIRST_SUBJECT_TYPE_ID = "d6488f88-1e74-40ce-81b5-b19a928ff5b1";
diff --git a/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java b/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java
index bcb085c4..28b047a4 100644
--- a/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java
+++ b/src/test/java/org/folio/processing/mapping/mapper/writer/marc/MarcRecordModifierTest.java
@@ -2033,6 +2033,44 @@ public void shouldRetainExistingRepeatableDataFieldAndAddIncomingWhenExistingIsP
testUpdateRecord(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingParameters);
}
+ @Test
+ public void shouldRetainExistingRepeatableDataFieldAndAddIncomingWhenExistingIsProtectedAndSomeIncomingFieldIsSameWithMatchesMultipleSubfieldProtectionSettings() {
+ String incomingParsedContent = "{\"leader\":\"00129nam 22000611a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"655\":{\"subfields\":[{\"a\":\"Catalogs1.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst015\"}],\"ind1\":\" \",\"ind2\":\"7\"}},{\"655\":{\"subfields\":[{\"a\":\"Periodicals.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst01411641\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}";
+ String existingParsedContent = "{\"leader\":\"00129nam 22000611a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"655\":{\"subfields\":[{\"a\":\"Catalogs0.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst014\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}";
+ String expectedParsedContent = "{\"leader\":\"00200nam 22000731a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"655\":{\"subfields\":[{\"a\":\"Catalogs0.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst014\"}],\"ind1\":\" \",\"ind2\":\"7\"}},{\"655\":{\"subfields\":[{\"a\":\"Catalogs1.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst015\"}],\"ind1\":\" \",\"ind2\":\"7\"}},{\"655\":{\"subfields\":[{\"a\":\"Periodicals.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst01411641\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}";
+
+ List protectionSettings = List.of(
+ new MarcFieldProtectionSetting()
+ .withField("655")
+ .withIndicator1("*")
+ .withIndicator2("*")
+ .withSubfield("*") //any subfield
+ .withData("fast"));
+
+ MappingParameters mappingParameters = new MappingParameters()
+ .withMarcFieldProtectionSettings(protectionSettings);
+ testUpdateRecord(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingParameters);
+ }
+
+ @Test
+ public void shouldRetainExistingRepeatableDataFieldAndAddIncomingWhenExistingIsProtectedAndIncomingFieldIsSameWithMatchesSubfieldProtectionSettings() {
+ String incomingParsedContent = "{\"leader\":\"00129nam 22000611a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"655\":{\"subfields\":[{\"a\":\"Catalogs1.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst015\"}],\"ind1\":\" \",\"ind2\":\"7\"}},{\"655\":{\"subfields\":[{\"a\":\"Periodicals.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst01411641\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}";
+ String existingParsedContent = "{\"leader\":\"00129nam 22000611a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"655\":{\"subfields\":[{\"a\":\"Catalogs0.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst014\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}";
+ String expectedParsedContent = "{\"leader\":\"00200nam 22000731a 4500\",\"fields\":[{\"001\":\"ybp7406411\"},{\"655\":{\"subfields\":[{\"a\":\"Catalogs0.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst014\"}],\"ind1\":\" \",\"ind2\":\"7\"}},{\"655\":{\"subfields\":[{\"a\":\"Catalogs1.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst015\"}],\"ind1\":\" \",\"ind2\":\"7\"}},{\"655\":{\"subfields\":[{\"a\":\"Periodicals.\"},{\"2\":\"fast\"},{\"0\":\"(OCoLC)fst01411641\"}],\"ind1\":\" \",\"ind2\":\"7\"}}]}";
+
+ List protectionSettings = List.of(
+ new MarcFieldProtectionSetting()
+ .withField("655")
+ .withIndicator1("*")
+ .withIndicator2("*")
+ .withSubfield("2") //selected subfield
+ .withData("fast"));
+
+ MappingParameters mappingParameters = new MappingParameters()
+ .withMarcFieldProtectionSettings(protectionSettings);
+ testUpdateRecord(incomingParsedContent, existingParsedContent, expectedParsedContent, mappingParameters);
+ }
+
@Test
public void shouldRetainExistingRepeatableFieldWhenExistingIsProtectedAndHasNoIncomingFieldWithSameTag() {
// 950 is repeatable field
diff --git a/src/test/java/org/folio/processing/matching/loader/LoadQueryBuilderTest.java b/src/test/java/org/folio/processing/matching/loader/LoadQueryBuilderTest.java
index 6ddf57aa..b01a2d99 100644
--- a/src/test/java/org/folio/processing/matching/loader/LoadQueryBuilderTest.java
+++ b/src/test/java/org/folio/processing/matching/loader/LoadQueryBuilderTest.java
@@ -70,6 +70,8 @@ public void shouldBuildQueryWhere_ExistingValueExactlyMatches_MultipleIncomingSt
StringValue value = StringValue.of("ybp7406411");
MatchDetail matchDetail = new MatchDetail()
.withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
.withExistingMatchExpression(new MatchExpression()
.withDataValueType(VALUE_FROM_RECORD)
.withFields(Arrays.asList(
@@ -80,11 +82,9 @@ public void shouldBuildQueryWhere_ExistingValueExactlyMatches_MultipleIncomingSt
LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
//then
assertNotNull(result);
- assertNotNull(result.getSql());
- String expectedSQLQuery = format("CROSS JOIN LATERAL jsonb_array_elements(instance.jsonb -> 'identifiers') fields(field) WHERE field ->> 'value' = 'ybp7406411' AND field ->> 'identifierTypeId' = '439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef'", value.getValue());
- assertEquals(expectedSQLQuery, result.getSql());
+ assertEquals(StringUtils.EMPTY, result.getSql());
assertNotNull(result.getCql());
- String expectedCQLQuery = format("identifiers=\"\\\"identifierTypeId\\\":\\\"439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef\\\"\" AND (identifiers=\"\\\"value\\\":\\\"ybp7406411\\\"\")", value.getValue());
+ String expectedCQLQuery = "identifiers =/@identifierTypeId=439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef \"ybp7406411\"";
assertEquals(expectedCQLQuery, result.getCql());
}
@@ -94,6 +94,8 @@ public void shouldBuildQueryWhere_ExistingValueExactlyMatches_MultipleIncomingLi
ListValue value = ListValue.of(Arrays.asList("ybp7406411", "ybp74064123"));
MatchDetail matchDetail = new MatchDetail()
.withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
.withExistingMatchExpression(new MatchExpression()
.withDataValueType(VALUE_FROM_RECORD)
.withFields(Arrays.asList(
@@ -104,11 +106,9 @@ public void shouldBuildQueryWhere_ExistingValueExactlyMatches_MultipleIncomingLi
LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
//then
assertNotNull(result);
- assertNotNull(result.getSql());
- String expectedSQLQuery = format("CROSS JOIN LATERAL jsonb_array_elements(instance.jsonb -> 'identifiers') fields(field) WHERE (field ->> 'value' = 'ybp7406411' OR field ->> 'value' = 'ybp74064123') AND field ->> 'identifierTypeId' = '439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef'", value.getValue());
- assertEquals(expectedSQLQuery, result.getSql());
+ assertEquals(StringUtils.EMPTY, result.getSql());
assertNotNull(result.getCql());
- String expectedCQLQuery = format("identifiers=\"\\\"identifierTypeId\\\":\\\"439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef\\\"\" AND (identifiers=\"\\\"value\\\":\\\"ybp7406411\\\"\" OR identifiers=\"\\\"value\\\":\\\"ybp74064123\\\"\")", value.getValue());
+ String expectedCQLQuery = "identifiers =/@identifierTypeId=439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef \"ybp7406411\" OR identifiers =/@identifierTypeId=439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef \"ybp74064123\"";
assertEquals(expectedCQLQuery, result.getCql());
}
@@ -490,7 +490,7 @@ public void shouldBuildQuery_ExistingValueBeginsWith_IncomingListValue_WithQuali
@Test
public void shouldReturnNullIfPassedNullValue() {
// given
- Value value = null;
+ Value> value = null;
MatchDetail matchDetail = new MatchDetail()
.withMatchCriterion(EXACTLY_MATCHES)
.withExistingMatchExpression(new MatchExpression()
@@ -507,7 +507,7 @@ public void shouldReturnNullIfPassedNullValue() {
@Test
public void shouldReturnNullIfPassedMissingValue() {
// given
- Value value = MissingValue.getInstance();
+ Value> value = MissingValue.getInstance();
MatchDetail matchDetail = new MatchDetail()
.withMatchCriterion(EXACTLY_MATCHES)
.withExistingMatchExpression(new MatchExpression()
@@ -524,7 +524,7 @@ public void shouldReturnNullIfPassedMissingValue() {
@Test
public void shouldReturnNullIfMatchingByExistingStaticValue() {
// given
- Value value = MissingValue.getInstance();
+ Value> value = MissingValue.getInstance();
MatchDetail matchDetail = new MatchDetail()
.withMatchCriterion(EXACTLY_MATCHES)
.withExistingMatchExpression(new MatchExpression()
@@ -691,8 +691,296 @@ public void shouldBuildQueryWhere_ExistingValueExactlyMatches_MultipleIncomingLi
assertNotEquals(expectedSQLQuery, wrongResult.getSql());
assertNotNull(result.getCql());
assertNotNull(wrongResult.getCql());
- String expectedCQLQuery = format("identifiers =/@value/@identifierTypeId=\"%s\" \"%s\"",identifierTypeFieldValue, value.getValue());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"%s\"",identifierTypeFieldValue, value.getValue());
assertEquals(expectedCQLQuery, result.getCql());
assertNotEquals(expectedCQLQuery, wrongResult.getCql());
}
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithParenthesesInValue() {
+ // given
+ StringValue value = StringValue.of("(OCoLC)1024095011");
+ String identifierTypeFieldValue = "7e591197-f335-4afb-bc6d-a6d76ca3bace";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"\\(OCoLC\\)1024095011\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithQuotesInValue() {
+ // given
+ StringValue value = StringValue.of("test\"quote\"value");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"test\\\"quote\\\"value\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithBackslashesInValue() {
+ // given
+ StringValue value = StringValue.of("path\\to\\resource");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"path\\\\to\\\\resource\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithWildcardsInValue() {
+ // given
+ StringValue value = StringValue.of("test*value?");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"test\\*value\\?\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithMultipleSpecialCharacters() {
+ // given
+ StringValue value = StringValue.of("(test*)\\query?");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"\\(test\\*\\)\\\\query\\?\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_ListWithSpecialCharacters() {
+ // given
+ ListValue value = ListValue.of(Arrays.asList("(OCoLC)123", "test*value", "path\\file"));
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"\\(OCoLC\\)123\" OR identifiers =/@identifierTypeId=%s \"test\\*value\" OR identifiers =/@identifierTypeId=%s \"path\\\\file\"", identifierTypeFieldValue, identifierTypeFieldValue, identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithApostropheInValue() {
+ // given
+ StringValue value = StringValue.of("O'Reilly's Book");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ // Apostrophes don't need escaping in CQL
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"O'Reilly's Book\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_EmptyValue() {
+ // given
+ StringValue value = StringValue.of("");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_PreEscapedValue() {
+ // given
+ StringValue value = StringValue.of("already\\\\escaped");
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ // Should double-escape the already escaped backslashes
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"already\\\\\\\\escaped\"", identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_RealWorldExampleFromProblem() {
+ // Test the exact values from problem.md
+ ListValue value = ListValue.of(Arrays.asList(
+ "(CStRLIN)NYCX1604275S",
+ "(NIC)notisABP6388",
+ "366832",
+ "(OCoLC)1604275"
+ ));
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXACTLY_MATCHES)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+ //when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+ //then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"\\(CStRLIN\\)NYCX1604275S\" OR identifiers =/@identifierTypeId=%s \"\\(NIC\\)notisABP6388\" OR identifiers =/@identifierTypeId=%s \"366832\" OR identifiers =/@identifierTypeId=%s \"\\(OCoLC\\)1604275\"", identifierTypeFieldValue, identifierTypeFieldValue, identifierTypeFieldValue, identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
+ @Test
+ public void shouldBuildQueryWhere_IdentifierMatching_WithListValue_ContainsCriterion() {
+ // given
+ ListValue value = ListValue.of(Arrays.asList(
+ "(OCoLC)1349275037",
+ "9924655804502931",
+ "in00022912564"
+ ));
+ String identifierTypeFieldValue = "439bfbae-75bc-4f74-9fc7-b2a2d47ce3ef";
+ MatchDetail matchDetail = new MatchDetail()
+ .withMatchCriterion(EXISTING_VALUE_CONTAINS_INCOMING_VALUE)
+ .withIncomingRecordType(EntityType.MARC_BIBLIOGRAPHIC)
+ .withExistingRecordType(EntityType.INSTANCE)
+ .withIncomingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("035"),
+ new Field().withLabel("indicator1").withValue(""),
+ new Field().withLabel("indicator2").withValue(""),
+ new Field().withLabel("recordSubfield").withValue("a"))
+ ))
+ .withExistingMatchExpression(new MatchExpression()
+ .withDataValueType(VALUE_FROM_RECORD)
+ .withFields(Arrays.asList(
+ new Field().withLabel("field").withValue("instance.identifiers[].value"),
+ new Field().withLabel("identifierTypeId").withValue(identifierTypeFieldValue))
+ ));
+
+ // when
+ LoadQuery result = LoadQueryBuilder.build(value, matchDetail);
+
+ // then
+ assertNotNull(result);
+ assertEquals(StringUtils.EMPTY, result.getSql());
+ // For EXISTING_VALUE_CONTAINS_INCOMING_VALUE with identifiers, the CQL should use wildcard matching
+ String expectedCQLQuery = format("identifiers =/@identifierTypeId=%s \"*\\(OCoLC\\)1349275037*\" OR identifiers =/@identifierTypeId=%s \"*9924655804502931*\" OR identifiers =/@identifierTypeId=%s \"*in00022912564*\"",
+ identifierTypeFieldValue, identifierTypeFieldValue, identifierTypeFieldValue);
+ assertEquals(expectedCQLQuery, result.getCql());
+ }
+
}
diff --git a/src/test/resources/log4j2.properties b/src/test/resources/log4j2.properties
new file mode 100644
index 00000000..2f71b05c
--- /dev/null
+++ b/src/test/resources/log4j2.properties
@@ -0,0 +1,18 @@
+filters = threshold
+
+filter.threshold.type = ThresholdFilter
+filter.threshold.level = INFO
+
+appenders = console
+
+packages = org.folio.okapi.common.logging
+
+appender.console.type = Console
+appender.console.name = STDOUT
+
+appender.console.layout.type = PatternLayout
+appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5p %-20.20C{1} [%reqId] %m%n
+
+rootLogger.level = INFO
+rootLogger.appenderRefs = INFO
+rootLogger.appenderRef.stdout.ref = STDOUT
diff --git a/src/test/resources/org/folio/processing/mapping/instance/008_invalid_field.mrc b/src/test/resources/org/folio/processing/mapping/instance/008_invalid_field.mrc
new file mode 100644
index 00000000..f5c94826
--- /dev/null
+++ b/src/test/resources/org/folio/processing/mapping/instance/008_invalid_field.mrc
@@ -0,0 +1 @@
+01304cam 2200313Ma 4500001001200000003000600012005001700018006001900035007001500054008000500069040009800074050002300172072002500195082001800220100002400238245006500262260003100327300005800358336002600416337002600442338003600468490009600504500004000600504006900640588002600709710002300735776013400758830009800892ocm85820197OCoLC20160514041104.1m o d cr |||||||||||None aDG1bengepncDG1dDG1dOCLCQdE7BdOCLCFdOCLCOdOCLCQdYDXCPdN$TdIDEBKdOCLCQdCOOdOCLCQ 4aQA278.5b.J27 1991 7aMATx0290202bisacsh04a519.5/3542201 aJackson, J. Edward.12aA user's guide to principal components /cJ. Edward Jackson. aNew York :bWiley,c?1991. a1 online resource (xvii, 569 pages) :billustrations. atextbtxt2rdacontent acomputerbc2rdamedia aonline resourcebcr2rdacarrier1 aWiley series in probability and mathematical statistics. Applied probability and statistics a"A Wiley-Interscience publication." aIncludes bibliographical references (pages 497-550) and indexes.0 aPrint version record.2 aJohn Wiley & Sons.08iPrint version:aJackson, J. Edward.tUser's guide to principal components.dNew York : Wiley, ?1991z0471622672w(DLC) 90028108 0aWiley series in probability and mathematical statistics.pApplied probability and statistics.
\ No newline at end of file
diff --git a/src/test/resources/org/folio/processing/mapping/instance/empty_008_field.mrc b/src/test/resources/org/folio/processing/mapping/instance/empty_008_field.mrc
new file mode 100644
index 00000000..41c0815a
--- /dev/null
+++ b/src/test/resources/org/folio/processing/mapping/instance/empty_008_field.mrc
@@ -0,0 +1 @@
+01300cam 2200313Ma 4500001001200000003000600012005001700018006001900035007001500054008000100069040009800070050002300168072002500191082001800216100002400234245006500258260003100323300005800354336002600412337002600438338003600464490009600500500004000596504006900636588002600705710002300731776013400754830009800888ocm85820197OCoLC20160514041104.1m o d cr ||||||||||| aDG1bengepncDG1dDG1dOCLCQdE7BdOCLCFdOCLCOdOCLCQdYDXCPdN$TdIDEBKdOCLCQdCOOdOCLCQ 4aQA278.5b.J27 1991 7aMATx0290202bisacsh04a519.5/3542201 aJackson, J. Edward.12aA user's guide to principal components /cJ. Edward Jackson. aNew York :bWiley,c?1991. a1 online resource (xvii, 569 pages) :billustrations. atextbtxt2rdacontent acomputerbc2rdamedia aonline resourcebcr2rdacarrier1 aWiley series in probability and mathematical statistics. Applied probability and statistics a"A Wiley-Interscience publication." aIncludes bibliographical references (pages 497-550) and indexes.0 aPrint version record.2 aJohn Wiley & Sons.08iPrint version:aJackson, J. Edward.tUser's guide to principal components.dNew York : Wiley, ?1991z0471622672w(DLC) 90028108 0aWiley series in probability and mathematical statistics.pApplied probability and statistics.
\ No newline at end of file