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

MODAUD-225 Handle Repeatable Fields for MARC_BIB and MARC_AUTHORITY Audit Events #205

Merged
merged 25 commits into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a94b40b
MODAUD-214 Marc Bib API Core Impl
dmytrokrutii Feb 12, 2025
c53666f
Merge remote-tracking branch 'origin/master' into MODAUD-214
dmytrokrutii Feb 12, 2025
92baec6
MODAUD-214 Marc Bib Audit API
dmytrokrutii Feb 13, 2025
660909e
Merge branch 'master' into MODAUD-214
dmytrokrutii Feb 13, 2025
2ef6855
MODAUD-214 Remove MARC_RECORD_PAGE_SIZE setting
dmytrokrutii Feb 14, 2025
b42ac97
Merge branch 'master' into MODAUD-214
dmytrokrutii Feb 14, 2025
2f03766
MODAUD-214 Uncomment test suite
dmytrokrutii Feb 14, 2025
fd1350a
MODAUD-214 Fix sonar issues
dmytrokrutii Feb 14, 2025
e635725
MODAUD-214 Update ModuleDescriptor-template.json
dmytrokrutii Feb 14, 2025
377cab5
MODAUD-214 Remove Marc SettingGroup
dmytrokrutii Feb 14, 2025
7bda1ad
MODAUD-225 Marc Authority audit API
dmytrokrutii Feb 14, 2025
1302f0d
MODAUD-225 Rename test
dmytrokrutii Feb 14, 2025
d42b8df
Merge branch 'MODAUD-214' into MODAUD-225
dmytrokrutii Feb 14, 2025
92b8286
Merge branch 'master' into MODAUD-214
dmytrokrutii Feb 18, 2025
66fd2e1
MODAUD-214 Fix git conflict
dmytrokrutii Feb 18, 2025
d7bd0a9
Merge branch 'MODAUD-214' into MODAUD-211
dmytrokrutii Feb 18, 2025
cd086c9
MODAUD-211 Fix compilation error
dmytrokrutii Feb 18, 2025
0b09c55
Merge branch 'master' into MODAUD-214
dmytrokrutii Feb 19, 2025
f8c7a18
MODAUD-225 Marc audit handling repeatable fields
dmytrokrutii Feb 21, 2025
7c41ada
Merge branch 'master' into MODAUD-214
dmytrokrutii Feb 21, 2025
33d3dd4
Merge branch 'MODAUD-214' into MODAUD-211
dmytrokrutii Feb 21, 2025
aa3ffb8
Merge branch 'MODAUD-211' into MODAUD-225
dmytrokrutii Feb 21, 2025
65d5961
Merge branch 'master' into MODAUD-225
dmytrokrutii Feb 21, 2025
b4118ab
MODAUD-225 Fix merge conflict
dmytrokrutii Feb 21, 2025
c964a5a
Merge branch 'master' into MODAUD-225
dmytrokrutii Feb 25, 2025
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
161 changes: 125 additions & 36 deletions mod-audit-server/src/main/java/org/folio/util/marc/MarcUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.Objects;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Utility class for mapping and transforming parsed MARC records into entities suitable
* for audit and logging purposes. This class provides methods to process and compare parsed
* MARC records and extract relevant data for creating audit entities.
* <p>
* The {@code MarcUtil} handles operations such as:
* The {@code ParsedRecordUtil} handles operations such as:
* - Mapping a source record domain event to a MarcAuditEntity.
* - Extracting user IDs from metadata.
* - Flattening complex parsed record structures into more manageable formats.
Expand Down Expand Up @@ -64,22 +66,19 @@ public static MarcAuditEntity mapToEntity(SourceRecordDomainEvent event) {
Map<String, Object> difference;
var oldRecord = event.getEventPayload().getOld();
var newRecord = event.getEventPayload().getNewRecord();
String action;

switch (event.getEventType()) {
case SOURCE_RECORD_CREATED -> {
data = extractRecordData(newRecord, CREATED_BY);
data = extractRecordData(newRecord, CREATED_BY, CREATED);
difference = getDifference(newRecord.getParsedRecord(), event.getEventType());
action = CREATED;
}
case SOURCE_RECORD_UPDATED -> {
data = extractRecordData(newRecord, UPDATED_BY);
data = extractRecordData(newRecord, UPDATED_BY, UPDATED);
difference = calculateDifferences(oldRecord.getParsedRecord(), newRecord.getParsedRecord());
action = UPDATED;
}
case SOURCE_RECORD_DELETED -> {
data = extractRecordData(oldRecord, UPDATED_BY);
data = extractRecordData(oldRecord, UPDATED_BY, DELETED);
difference = getDifference(oldRecord.getParsedRecord(), event.getEventType());
action = DELETED;
}
default -> throw new ValidationException("Unsupported event type: " + event.getEventType());
}
Expand All @@ -89,7 +88,7 @@ public static MarcAuditEntity mapToEntity(SourceRecordDomainEvent event) {
event.getEventMetadata().getEventDate(),
data.recordId,
event.getEventMetadata().getPublishedBy(),
action,
data.action,
data.userId,
difference
);
Expand Down Expand Up @@ -127,11 +126,11 @@ private static MarcAuditItem mapEntityToItem(MarcAuditEntity entity) {
return item;
}


private static RecordData extractRecordData(Record value, String userKey) {
private static RecordData extractRecordData(Record value, String userKey, String action) {
return new RecordData(
value.getMatchedId(),
getUserIdFromMetadata(value.getMetadata(), userKey)
getUserIdFromMetadata(value.getMetadata(), userKey),
action
);
}

Expand Down Expand Up @@ -214,49 +213,139 @@ private static String formatField(Object value) {
return value.toString();
}


/**
* Compares two maps representing parsed records and identifies added, modified,
* and removed entries. It determines the differences by comparing the keys
* and values of the given maps.
* Compares two maps of parsed records and identifies the differences between them.
* The method determines the added, modified, and removed entries by analyzing
* the keys and values in the input maps. The results are aggregated into categorized lists
* and returned as a single map.
*
* @param oldMap the original map representing the state of a parsed record before changes
* @param newMap the updated map representing the state of a parsed record after changes
* @return a map containing three keys ("added", "removed", "modified"), each mapped
* to a list of maps. The "added" key contains entries present only in the new map,
* the "removed" key contains entries present only in the old map,
* and the "modified" key contains entries with mismatched values between the maps.
* @param oldMap a map representing the original state of parsed records
* @param newMap a map representing the updated state of parsed records
* @return a map containing three lists:
* - "added" for entries present in the new map but not in the old map
* - "removed" for entries present in the old map but not in the new map
* - "modified" for
*/
private static Map<String, Object> compareParsedRecords(Map<String, Object> oldMap, Map<String, Object> newMap) {
List<Map<String, Object>> added = new ArrayList<>();
List<Map<String, Object>> modified = new ArrayList<>();
List<Map<String, Object>> removed = new ArrayList<>();

newMap.forEach((key, newValue) -> {
if (!oldMap.containsKey(key)) {
added.add(Map.of(FIELD_KEY, key, VALUE_KEY, newValue));
}
});
populateEntries(newMap, oldMap, added);
populateEntries(oldMap, newMap, removed);
processModifiedEntries(newMap, oldMap, added, removed, modified);
return toMap(added, removed, modified);
}


oldMap.forEach((key, oldValue) -> {
if (!newMap.containsKey(key)) {
removed.add(Map.of(FIELD_KEY, key, VALUE_KEY, oldValue));
/**
* Populates a list with entries from the source map that are absent in the target map.
* For each key-value pair in the source map, if the key is not found in the target map,
* an entry is added to the result list containing the key and value.
*
* @param sourceMap the map containing the source key-value pairs to compare
* @param targetMap the map used as reference to check for existing keys
* @param resultList the list to store entries that are present in the source map
* but not in the target map
*/
private static void populateEntries(Map<String, Object> sourceMap, Map<String, Object> targetMap,
List<Map<String, Object>> resultList) {
sourceMap.forEach((key, value) -> {
if (!targetMap.containsKey(key)) {
resultList.add(Map.of(FIELD_KEY, key, VALUE_KEY, value));
}
});
}

/**
* Processes the differences between two maps representing the new and old states of a dataset.
* The method identifies added, removed, and modified entries by comparing the key-value pairs
* in the input maps and categorizes them into the respective provided lists.
*
* @param newMap the map containing the new state of the dataset
* @param oldMap the map containing the old state of the dataset
* @param added a list to store entries that are present in the new map but not in the old map
* @param removed a list to store entries that are present in the old map but not in the new map
* @param modified a list to store entries that exist in both maps but have different values
*/
private static void processModifiedEntries(Map<String, Object> newMap, Map<String, Object> oldMap,
List<Map<String, Object>> added, List<Map<String, Object>> removed,
List<Map<String, Object>> modified) {
newMap.forEach((key, newValue) -> {
if (!FIELD_005.equals(key) && oldMap.containsKey(key)) {
var oldValue = oldMap.get(key);
if (!Objects.equals(oldValue, newValue)) {
modified.add(Map.of(
FIELD_KEY, key,
OLD_VALUE_KEY, oldValue,
NEW_VALUE_KEY, newValue
));
var modifiedField = getModifiedFieldMap(key, oldValue, newValue);
if (!modifiedField.containsKey(OLD_VALUE_KEY)) {
added.add(modifiedField);
} else if (!modifiedField.containsKey(NEW_VALUE_KEY)) {
removed.add(modifiedField);
} else {
modified.add(modifiedField);
}
}
}
});
}

return toMap(added, removed, modified);
/**
* Generates a map representing the differences between an old and a new value for a specified field key.
* The method computes the added, removed, and modified elements between the old and new values,
* returning a map containing the field key and any relevant changes.
*
* @param key the name of the field being evaluated for changes
* @param oldValue the previous value of the field, which can be a single object or a list of objects
* @param newValue the updated value of the field, which can be a single object or a list of objects
* @return a map containing the field key and detected differences between the old and new values.
* The map may include:
* - "fieldKey" with the provided key
* - "oldValue" with the previous data if applicable
* - "newValue" with the updated data if applicable
*/
private static Map<String, Object> getModifiedFieldMap(String key, Object oldValue, Object newValue) {
var oldList = convertToList(oldValue);
var newList = convertToList(newValue);

var oldSet = new HashSet<>(oldList);
var newSet = new HashSet<>(newList);

var removedSet = new HashSet<>(oldSet);
var addedSet = new HashSet<>(newSet);

removedSet.removeAll(newSet);
addedSet.removeAll(oldSet);

var oldResult = formatSingleOrList(removedSet);
var newResult = formatSingleOrList(addedSet);

var map = new HashMap<String, Object>();
map.put(FIELD_KEY, key);

if (oldList.size() == newList.size()) {
if (!Objects.equals(oldValue, newValue)) {
map.put(OLD_VALUE_KEY, oldValue);
map.put(NEW_VALUE_KEY, newValue);
}
} else {
if (removedSet.isEmpty()) {
map.put(NEW_VALUE_KEY, newResult);
} else if (addedSet.isEmpty()) {
map.put(OLD_VALUE_KEY, oldResult);
} else {
map.put(OLD_VALUE_KEY, oldResult);
map.put(NEW_VALUE_KEY, newResult);
}
}
return map;
}

private static Object formatSingleOrList(Set<Object> set) {
return set.size() == 1 ? set.iterator().next() : new ArrayList<>(set);
}

private static List<Object> convertToList(Object value) {
return value instanceof List<?> ? new ArrayList<>((List<?>) value) : List.of(value);
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -287,7 +376,7 @@ private static void addIfNotEmpty(Map<String, Object> map, String key, Collectio
}
}

private record RecordData(String recordId, String userId) {
private record RecordData(String recordId, String userId, String action) {
}

}
Loading