Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(reorder fields): keep order of MARC fields while Creating/Deriving/Editing MARC records. #858

Merged
merged 15 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package org.folio.services.afterprocessing;

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 com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
Expand Down Expand Up @@ -28,9 +32,14 @@
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
import java.util.concurrent.ForkJoinPool;
import java.util.function.Consumer;

Expand All @@ -50,6 +59,8 @@ public final class AdditionalFieldsUtil {

private final static CacheLoader<Object, org.marc4j.marc.Record> parsedRecordContentCacheLoader;
private final static LoadingCache<Object, org.marc4j.marc.Record> parsedRecordContentCache;
private static final ObjectMapper objectMapper = new ObjectMapper();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the future, consider reusing existing ObjectMappers.

public static final String FIELDS = "fields";

static {
// this function is executed when creating a new item to be saved in the cache.
Expand Down Expand Up @@ -149,8 +160,9 @@ public static boolean addFieldToMarcRecord(Record record, String field, char sub
String parsedContentString = new JsonObject(os.toString()).encode();
parsedRecordContentCache.invalidate(record.getParsedRecord().getContent());
// save parsed content string to cache then set it on the record
parsedRecordContentCache.put(parsedContentString, marcRecord);
record.setParsedRecord(record.getParsedRecord().withContent(parsedContentString));
var content = reorderMarcRecordFields(record.getParsedRecord().getContent().toString(), parsedContentString);
parsedRecordContentCache.put(content, marcRecord);
record.setParsedRecord(record.getParsedRecord().withContent(content));
result = true;
}
}
Expand All @@ -160,6 +172,59 @@ public static boolean addFieldToMarcRecord(Record record, String field, char sub
return result;
}

private static String reorderMarcRecordFields(String sourceContent, String targetContent) {
try {
var parsedContent = objectMapper.readTree(targetContent);
var fieldsArrayNode = (ArrayNode) parsedContent.path(FIELDS);

Map<String, Queue<JsonNode>> jsonNodesByTag = groupNodesByTag(fieldsArrayNode);

List<String> sourceFields = getSourceFields(sourceContent);

var rearrangedArray = objectMapper.createArrayNode();
for (String tag : sourceFields) {
Queue<JsonNode> nodes = jsonNodesByTag.get(tag);
if (nodes != null && !nodes.isEmpty()) {
rearrangedArray.addAll(nodes);
jsonNodesByTag.remove(tag);
}
}

jsonNodesByTag.values().forEach(rearrangedArray::addAll);

((ObjectNode)parsedContent).set(FIELDS, rearrangedArray);

return parsedContent.toString();
} catch (Exception e) {
LOGGER.error("An error occurred while reordering Marc record fields: {}", e.getMessage(), e);
return targetContent;
}
}

private static List<String> getSourceFields(String source) {
List<String> sourceFields = new ArrayList<>();
try {
var sourceJson = objectMapper.readTree(source);
var fieldsNode = sourceJson.get(FIELDS);
for (JsonNode fieldNode : fieldsNode) {
String tag = fieldNode.fieldNames().next();
sourceFields.add(tag);
}
} catch (Exception e) {
LOGGER.error("An error occurred while parsing source JSON: {}", e.getMessage(), e);
}
return sourceFields;
}

private static Map<String, Queue<JsonNode>> groupNodesByTag(ArrayNode fieldsArrayNode) {
Map<String, Queue<JsonNode>> jsonNodesByTag = new HashMap<>();
fieldsArrayNode.forEach(node -> {
String tag = node.fieldNames().next();
jsonNodesByTag.computeIfAbsent(tag, k -> new LinkedList<>()).add(node);
});
return jsonNodesByTag;
}

/**
* Adds new controlled field to marc record
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package org.folio;

import io.vertx.core.json.JsonObject;
import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.util.Objects;

import static org.folio.services.afterprocessing.AdditionalFieldsUtil.TAG_999;

/**
* Util class contains helper methods for unit testing needs
Expand All @@ -13,4 +17,34 @@ public final class TestUtil {
public static String readFileFromPath(String path) throws IOException {
return new String(FileUtils.readFileToByteArray(new File(path)));
}

public static boolean recordsHaveSameOrder(String baseContent, String newContent) {
var baseJson = new JsonObject(baseContent);
var newJson = new JsonObject(newContent);

var baseFields = baseJson.getJsonArray("fields");
var newFields = newJson.getJsonArray("fields");

for (Object newFieldObject : newFields) {
var newField = (JsonObject) newFieldObject;
if (newField.containsKey(TAG_999)) {
continue;
}
boolean foundMatchingField = false;
for (Object baseFieldObject : baseFields) {
var baseField = (JsonObject) baseFieldObject;
if (baseField.containsKey(TAG_999)) {
continue;
}
if (Objects.equals(baseField, newField)) {
foundMatchingField = true;
break;
}
}
if (!foundMatchingField) {
return false;
}
}
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.util.Map;
import java.util.UUID;

import static org.folio.TestUtil.recordsHaveSameOrder;
import static org.folio.services.afterprocessing.AdditionalFieldsUtil.INDICATOR;
import static org.folio.services.afterprocessing.AdditionalFieldsUtil.SUBFIELD_I;
import static org.folio.services.afterprocessing.AdditionalFieldsUtil.SUBFIELD_S;
Expand All @@ -38,11 +39,13 @@
import static org.folio.services.afterprocessing.AdditionalFieldsUtil.getCacheStats;
import static org.folio.services.afterprocessing.AdditionalFieldsUtil.getValue;
import static org.hamcrest.Matchers.lessThanOrEqualTo;
import static org.junit.Assert.assertTrue;

@RunWith(BlockJUnit4ClassRunner.class)
public class AdditionalFieldsUtilTest {

private static final String PARSED_RECORD_PATH = "src/test/resources/org/folio/services/afterprocessing/parsedRecord.json";
private static final String REORDERED_PARSED_RECORD_PATH = "src/test/resources/org/folio/services/afterprocessing/reorderedParsedRecord.json";

@BeforeClass
public static void beforeClass() {
Expand Down Expand Up @@ -106,6 +109,39 @@ public void shouldAddInstanceIdSubfield() throws IOException {
Assert.assertEquals(2, totalFieldsCount);
}

@Test
public void shouldReorderParsedRecordContentAfterAddingField() throws IOException {
// given
String instanceId = UUID.randomUUID().toString();
String parsedRecordContent = TestUtil.readFileFromPath(REORDERED_PARSED_RECORD_PATH);
ParsedRecord parsedRecord = new ParsedRecord();
String leader = new JsonObject(parsedRecordContent).getString("leader");
parsedRecord.setContent(parsedRecordContent);
Record record = new Record().withId(UUID.randomUUID().toString()).withParsedRecord(parsedRecord);
var baseRecord = record.getParsedRecord().getContent().toString();
// when
boolean added = addFieldToMarcRecord(record, TAG_999, SUBFIELD_I, instanceId);
// then
assertTrue(recordsHaveSameOrder(baseRecord, record.getParsedRecord().getContent().toString()));
assertTrue(added);
JsonObject content = new JsonObject(record.getParsedRecord().getContent().toString());
JsonArray fields = content.getJsonArray("fields");
String newLeader = content.getString("leader");
Assert.assertNotEquals(leader, newLeader);
Assert.assertFalse(fields.isEmpty());
boolean existsNewField = false;
for (int i = 0; i < fields.size(); i++) {
JsonObject targetField = fields.getJsonObject(i);
if (targetField.containsKey(TAG_999)) {
existsNewField = true;
String currentTag = fields.getJsonObject(i-1).stream().map(Map.Entry::getKey).findFirst().get();
String nextTag = fields.getJsonObject(i).stream().map(Map.Entry::getKey).findFirst().get();
Assert.assertThat(currentTag, lessThanOrEqualTo(nextTag));
}
}
assertTrue(existsNewField);
}

@Test
public void shouldNotAddInstanceIdSubfieldIfNoParsedRecordContent() {
// given
Expand Down
Loading
Loading