From dbc3de7674307a3fede5906776a9b09fe71a53ea Mon Sep 17 00:00:00 2001 From: TsaghikKhachatryan <58312204+TsaghikKhachatryan@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:07:31 +0400 Subject: [PATCH] refactor(reorder fields): keep order of MARC fields while Creating/Deriving/Editing MARC records. (#858) * refactor(reorder fields): reorder content fields add unit test for reorderMarcRecordFields method return target content instead of null value create constant FIELDS Closes: MODQM-407 --- .../afterprocessing/AdditionalFieldsUtil.java | 69 +++- .../src/test/java/org/folio/TestUtil.java | 34 ++ .../AdditionalFieldsUtilTest.java | 36 ++ .../reorderedParsedRecord.json | 335 ++++++++++++++++++ 4 files changed, 472 insertions(+), 2 deletions(-) create mode 100644 mod-source-record-manager-server/src/test/resources/org/folio/services/afterprocessing/reorderedParsedRecord.json diff --git a/mod-source-record-manager-server/src/main/java/org/folio/services/afterprocessing/AdditionalFieldsUtil.java b/mod-source-record-manager-server/src/main/java/org/folio/services/afterprocessing/AdditionalFieldsUtil.java index e5e9b2c62..b039a45e2 100644 --- a/mod-source-record-manager-server/src/main/java/org/folio/services/afterprocessing/AdditionalFieldsUtil.java +++ b/mod-source-record-manager-server/src/main/java/org/folio/services/afterprocessing/AdditionalFieldsUtil.java @@ -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; @@ -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; @@ -50,6 +59,8 @@ public final class AdditionalFieldsUtil { private final static CacheLoader parsedRecordContentCacheLoader; private final static LoadingCache parsedRecordContentCache; + private static final ObjectMapper objectMapper = new ObjectMapper(); + public static final String FIELDS = "fields"; static { // this function is executed when creating a new item to be saved in the cache. @@ -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; } } @@ -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> jsonNodesByTag = groupNodesByTag(fieldsArrayNode); + + List sourceFields = getSourceFields(sourceContent); + + var rearrangedArray = objectMapper.createArrayNode(); + for (String tag : sourceFields) { + Queue 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 getSourceFields(String source) { + List 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> groupNodesByTag(ArrayNode fieldsArrayNode) { + Map> 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 * diff --git a/mod-source-record-manager-server/src/test/java/org/folio/TestUtil.java b/mod-source-record-manager-server/src/test/java/org/folio/TestUtil.java index 3bffb2a9d..c7d1b325a 100644 --- a/mod-source-record-manager-server/src/test/java/org/folio/TestUtil.java +++ b/mod-source-record-manager-server/src/test/java/org/folio/TestUtil.java @@ -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 @@ -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; + } } diff --git a/mod-source-record-manager-server/src/test/java/org/folio/services/afterprocessing/AdditionalFieldsUtilTest.java b/mod-source-record-manager-server/src/test/java/org/folio/services/afterprocessing/AdditionalFieldsUtilTest.java index 96ab21fe3..dbb0dd72d 100644 --- a/mod-source-record-manager-server/src/test/java/org/folio/services/afterprocessing/AdditionalFieldsUtilTest.java +++ b/mod-source-record-manager-server/src/test/java/org/folio/services/afterprocessing/AdditionalFieldsUtilTest.java @@ -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; @@ -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() { @@ -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 diff --git a/mod-source-record-manager-server/src/test/resources/org/folio/services/afterprocessing/reorderedParsedRecord.json b/mod-source-record-manager-server/src/test/resources/org/folio/services/afterprocessing/reorderedParsedRecord.json new file mode 100644 index 000000000..53a711df0 --- /dev/null +++ b/mod-source-record-manager-server/src/test/resources/org/folio/services/afterprocessing/reorderedParsedRecord.json @@ -0,0 +1,335 @@ +{ + "leader":"01314nam 22003851a 4500", + "fields":[ + { + "040":{ + "subfields":[ + { + "a":"NhCcYBP" + }, + { + "c":"NhCcYBP" + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "001":"ybp7406411" + }, + { + "003":"NhCcYBP" + }, + { + "005":"20120404100627.6" + }, + { + "006":"m||||||||d|||||||" + }, + { + "007":"cr||n|||||||||" + }, + { + "008":"120329s2011 sz a ob 001 0 eng d" + }, + { + "020":{ + "subfields":[ + { + "a":"2940447241 (electronic bk.)" + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "020":{ + "subfields":[ + { + "a":"9782940447244 (electronic bk.)" + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "050":{ + "subfields":[ + { + "a":"Z246" + }, + { + "b":".A43 2011" + } + ], + "ind1":" ", + "ind2":"4" + } + }, + { + "082":{ + "subfields":[ + { + "a":"686.22" + }, + { + "2":"22" + } + ], + "ind1":"0", + "ind2":"4" + } + }, + { + "100":{ + "subfields":[ + { + "a":"Ambrose, Gavin." + } + ], + "ind1":"1", + "ind2":" " + } + }, + { + "245":{ + "subfields":[ + { + "a":"The fundamentals of typography" + }, + { + "h":"[electronic resource] /" + }, + { + "c":"Gavin Ambrose, Paul Harris." + } + ], + "ind1":"1", + "ind2":"4" + } + }, + { + "250":{ + "subfields":[ + { + "a":"2nd ed." + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "260":{ + "subfields":[ + { + "a":"Lausanne ;" + }, + { + "a":"Worthing :" + }, + { + "b":"AVA Academia," + }, + { + "c":"2011." + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "300":{ + "subfields":[ + { + "a":"1 online resource." + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "490":{ + "subfields":[ + { + "a":"AVA Academia series" + } + ], + "ind1":"1", + "ind2":" " + } + }, + { + "500":{ + "subfields":[ + { + "a":"Previous ed.: 2006." + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "504":{ + "subfields":[ + { + "a":"Includes bibliographical references (p. [200]) and index." + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "505":{ + "subfields":[ + { + "a":"Type and language -- A few basics -- Letterforms -- Words and paragraphs -- Using type." + } + ], + "ind1":"0", + "ind2":" " + } + }, + { + "650":{ + "subfields":[ + { + "a":"Graphic design (Typography)" + } + ], + "ind1":" ", + "ind2":"0" + } + }, + { + "650":{ + "subfields":[ + { + "a":"Printing." + } + ], + "ind1":" ", + "ind2":"0" + } + }, + { + "700":{ + "subfields":[ + { + "a":"Harris, Paul," + }, + { + "d":"1971-" + } + ], + "ind1":"1", + "ind2":" " + } + }, + { + "710":{ + "subfields":[ + { + "a":"EBSCOhost" + } + ], + "ind1":"2", + "ind2":" " + } + }, + { + "776":{ + "subfields":[ + { + "c":"Original" + }, + { + "z":"9782940411764" + }, + { + "z":"294041176X" + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "830":{ + "subfields":[ + { + "a":"AVA academia." + } + ], + "ind1":" ", + "ind2":"0" + } + }, + { + "856":{ + "subfields":[ + { + "u":"http://search.ebscohost.com/login.aspx?direct=true&scope=site&db=nlebk&db=nlabk&AN=430135" + } + ], + "ind1":"4", + "ind2":"0" + } + }, + { + "935":{ + "subfields":[ + { + "a":".o13465259" + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "980":{ + "subfields":[ + { + "a":"130307" + }, + { + "b":"7107" + }, + { + "e":"7107" + }, + { + "f":"243965" + }, + { + "g":"1" + } + ], + "ind1":" ", + "ind2":" " + } + }, + { + "981":{ + "subfields":[ + { + "b":"OM" + }, + { + "c":"nlnet" + } + ], + "ind1":" ", + "ind2":" " + } + } + ] +}