Skip to content

Commit 7d40f95

Browse files
authored
Merge pull request #11983 from GlobalDataverseCommunityConsortium/GDCC/8914-COAR-compliant_messaging2
GDCC/8914 COAR Messaging Improvement
2 parents 683efb6 + 636e7cc commit 7d40f95

File tree

9 files changed

+1415
-16
lines changed

9 files changed

+1415
-16
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Improved COAR Notify Relationship Announcement Support
2+
3+
Dataverse no longer sends duplicate [COAR Notify Relationship Announcement Workflow](https://coar-notify.net/catalogue/workflows/repository-relationship-repository/) messages when new dataset versions are published (and the relationship metadata has not been changed).

src/main/java/edu/harvard/iq/dataverse/Dataset.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,26 @@ public DatasetVersion getReleasedVersion() {
495495
}
496496
return null;
497497
}
498+
499+
/**
500+
* Returns the second-most-recent released version of this dataset.
501+
* Assumes versions are ordered from most recent to oldest.
502+
*
503+
* @return The prior released version, or null if there is only one or no released versions
504+
*/
505+
public DatasetVersion getPriorReleasedVersion() {
506+
boolean foundReleasedVersion = false;
507+
for (DatasetVersion version : this.getVersions()) {
508+
if (version.isReleased()) {
509+
if(foundReleasedVersion) {
510+
return version;
511+
} else {
512+
foundReleasedVersion = true;
513+
}
514+
}
515+
}
516+
return null;
517+
}
498518

499519
public DatasetVersion getVersionFromId(Long datasetVersionId) {
500520
for (DatasetVersion version : this.getVersions()) {

src/main/java/edu/harvard/iq/dataverse/DatasetFieldCompoundValue.java

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import jakarta.persistence.Transient;
2828

2929
import org.apache.commons.lang3.StringUtils;
30+
import org.apache.commons.lang3.Strings;
3031
import org.apache.commons.lang3.tuple.ImmutablePair;
3132
import org.apache.commons.lang3.tuple.Pair;
3233

@@ -254,4 +255,83 @@ private Map<DatasetField, String> removeLastComma(Map<DatasetField, String> mapI
254255

255256
return mapIn;
256257
}
258+
259+
260+
/**
261+
* Compares this DatasetFieldCompoundValue with another for equality based on
262+
* their child fields. Two compound values are considered equal if they have the
263+
* same child fields with the same values in the same order.
264+
*
265+
* @param other The DatasetFieldCompoundValue to compare with
266+
* @return true if both compound values have equal child fields, false otherwise
267+
*/
268+
public boolean valuesEqual(DatasetFieldCompoundValue other) {
269+
if (this == other) {
270+
return true;
271+
}
272+
if (other == null) {
273+
return false;
274+
}
275+
276+
List<DatasetField> children1 = this.getChildDatasetFields();
277+
List<DatasetField> children2 = other.getChildDatasetFields();
278+
279+
if (children1.size() != children2.size()) {
280+
return false;
281+
}
282+
283+
// Compare each child field
284+
for (DatasetField child1 : children1) {
285+
286+
DatasetField child2 = children2.stream()
287+
.filter(c -> c.getDatasetFieldType().equals(child1.getDatasetFieldType())).findFirst().orElse(null);
288+
289+
if (child2 == null) {
290+
return false;
291+
}
292+
293+
// Compare values based on field type
294+
if (child1.getDatasetFieldType().isControlledVocabulary()) {
295+
296+
List<ControlledVocabularyValue> cvs1 = child1.getControlledVocabularyValues();
297+
List<ControlledVocabularyValue> cvs2 = child2.getControlledVocabularyValues();
298+
299+
if (cvs1.size() != cvs2.size()) {
300+
301+
return false;
302+
}
303+
304+
for (ControlledVocabularyValue cv1Val : cvs1) {
305+
boolean found = cvs2.stream().anyMatch(cv2Val -> cv1Val.getStrValue().equals(cv2Val.getStrValue()));
306+
if (!found) {
307+
return false;
308+
}
309+
}
310+
} else {
311+
// Handle regular field values (including multiple values)
312+
List<DatasetFieldValue> dfvs1 = child1.getDatasetFieldValues();
313+
List<DatasetFieldValue> dfvs2 = child2.getDatasetFieldValues();
314+
315+
if (dfvs1.size() != dfvs2.size()) {
316+
return false;
317+
}
318+
319+
for (DatasetFieldValue dfv1 : dfvs1) {
320+
String value1 = dfv1.getValue();
321+
boolean found = dfvs2.stream()
322+
.anyMatch(dfv2 -> {
323+
String value2 = dfv2.getValue();
324+
if (value1 == null && value2 == null) {
325+
return true;
326+
}
327+
return value1 != null && value1.equals(value2);
328+
});
329+
if (!found) {
330+
return false;
331+
}
332+
}
333+
}
334+
}
335+
return true;
336+
}
257337
}

src/main/java/edu/harvard/iq/dataverse/DatasetFieldValue.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import jakarta.persistence.Table;
2323
import jakarta.persistence.Transient;
2424
import org.apache.commons.lang3.StringUtils;
25+
import org.apache.commons.lang3.Strings;
2526

2627
/**
2728
*
@@ -193,6 +194,22 @@ public DatasetFieldValue copy(DatasetField dsf) {
193194
dsfv.setValue(value);
194195

195196
return dsfv;
196-
}
197+
}
197198

199+
/**
200+
* Compares this DatasetFieldValue with another for equality based on their values.
201+
*
202+
* @param other The DatasetFieldValue to compare with
203+
* @return true if both values are equal (case-sensitive), false otherwise
204+
*/
205+
public boolean valuesEqual(DatasetFieldValue other) {
206+
if (this == other) {
207+
return true;
208+
}
209+
if (other == null) {
210+
return false;
211+
}
212+
return Strings.CS.equals(this.getValue(), other.getValue());
213+
}
214+
198215
}

src/main/java/edu/harvard/iq/dataverse/api/ldn/COARNotifyRelationshipAnnouncement.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import jakarta.json.Json;
2121
import jakarta.json.JsonObject;
2222
import jakarta.json.JsonObjectBuilder;
23+
import jakarta.json.JsonValue;
2324
import jakarta.ws.rs.BadRequestException;
2425

2526
import org.apache.http.client.methods.CloseableHttpResponse;
@@ -73,12 +74,20 @@ public COARNotifyRelationshipAnnouncement(
7374
*/
7475
public void processMessage(JsonObject msgObject) {
7576
// Extract subject, object, and relationship from the message
76-
String subjectId = extractField(msgObject, subjectKey);
77-
String objectId = extractField(msgObject, objectKey);
78-
String relationshipId = extractField(msgObject, relationshipKey);
77+
String subjectId;
78+
String objectId;
79+
String relationshipId;
80+
try {
81+
// Extract subject, object, and relationship from the message
82+
subjectId = extractField(msgObject, subjectKey);
83+
objectId = extractField(msgObject, objectKey);
84+
relationshipId = extractField(msgObject, relationshipKey);
7985

80-
if (subjectId == null || objectId == null || relationshipId == null) {
81-
throw new BadRequestException("Can't find the subject, relationship or object in the message - ignoring");
86+
if (subjectId == null || objectId == null || relationshipId == null) {
87+
throw new BadRequestException("Can't find the subject, relationship or object in the message - ignoring");
88+
}
89+
} catch (Exception e) {
90+
throw new BadRequestException("Failed to parse subject, relationship or object from the message - ignoring", e);
8291
}
8392

8493
// Get metadata about the citing resource
@@ -112,7 +121,15 @@ public void processMessage(JsonObject msgObject) {
112121
* Extract a field value from the message object.
113122
*/
114123
private String extractField(JsonObject msgObject, String key) {
115-
return msgObject.containsKey(key) ? msgObject.getString(key) : null;
124+
if (msgObject.containsKey(key)) {
125+
JsonValue value = msgObject.get(key);
126+
if (value.getValueType() == JsonValue.ValueType.OBJECT) {
127+
return ((JsonObject) value).getString("@id", null);
128+
} else if (value.getValueType() == JsonValue.ValueType.STRING) {
129+
return msgObject.getString(key);
130+
}
131+
}
132+
return null;
116133
}
117134

118135
/**

src/main/java/edu/harvard/iq/dataverse/workflow/internalspi/COARNotifyRelationshipAnnouncementStep.java

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package edu.harvard.iq.dataverse.workflow.internalspi;
22

3+
import edu.harvard.iq.dataverse.ControlledVocabularyValue;
34
import edu.harvard.iq.dataverse.Dataset;
45
import edu.harvard.iq.dataverse.DatasetField;
6+
import edu.harvard.iq.dataverse.DatasetFieldCompoundValue;
57
import edu.harvard.iq.dataverse.DatasetFieldType;
8+
import edu.harvard.iq.dataverse.DatasetFieldValue;
69
import edu.harvard.iq.dataverse.DatasetVersion;
710
import edu.harvard.iq.dataverse.GlobalId;
811
import edu.harvard.iq.dataverse.branding.BrandingUtil;
@@ -26,7 +29,7 @@
2629
import java.net.URI;
2730
import java.net.URISyntaxException;
2831
import java.nio.charset.StandardCharsets;
29-
import java.util.Arrays;
32+
import java.util.ArrayList;
3033
import java.util.Collection;
3134
import java.util.HashMap;
3235
import java.util.Iterator;
@@ -88,12 +91,34 @@ public WorkflowStepResult run(WorkflowContext context) {
8891
// First check that we have what is required
8992
Dataset d = context.getDataset();
9093
DatasetVersion dv = d.getReleasedVersion();
94+
DatasetVersion priorVersion = d.getPriorReleasedVersion();
9195
List<DatasetField> dvf = dv.getDatasetFields();
9296
Map<String, DatasetField> fields = new HashMap<String, DatasetField>();
9397
List<String> reqFields = ListSplitUtil.split((String) context.getSettings().getOrDefault(COARNotifyRelationshipAnnouncementTriggerFields.toString(), ""));
98+
99+
Map<String, DatasetField> priorFields = new HashMap<String, DatasetField>();
100+
if (priorVersion != null) {
101+
for (DatasetField pdf : priorVersion.getDatasetFields()) {
102+
if (!pdf.isEmpty() && reqFields.contains(pdf.getDatasetFieldType().getName())) {
103+
priorFields.put(pdf.getDatasetFieldType().getName(), pdf);
104+
}
105+
}
106+
}
107+
94108
for (DatasetField df : dvf) {
95109
if (!df.isEmpty() && reqFields.contains(df.getDatasetFieldType().getName())) {
96-
fields.put(df.getDatasetFieldType().getName(), df);
110+
DatasetField priorField = priorFields.get(df.getDatasetFieldType().getName());
111+
112+
if (priorVersion == null || priorField == null) {
113+
// No prior version, include all values
114+
fields.put(df.getDatasetFieldType().getName(), df);
115+
} else {
116+
// Create a filtered field with only new values
117+
DatasetField filteredField = filterNewValues(df, priorField);
118+
if (!filteredField.isEmpty()) {
119+
fields.put(df.getDatasetFieldType().getName(), filteredField);
120+
}
121+
}
97122
}
98123
}
99124

@@ -214,6 +239,14 @@ JsonArray getObjects(WorkflowContext ctxt, Map<String, DatasetField> fields) {
214239

215240
private JsonObject getRelationshipObject(DatasetFieldType dft, JsonValue jval, Dataset d,
216241
Map<String, String> localContext) {
242+
if (logger.isLoggable(Level.FINE)) {
243+
if (jval.getValueType().equals(jakarta.json.JsonValue.ValueType.OBJECT)) {
244+
logger.fine("Parsing : " + JsonUtil.prettyPrint(jval.asJsonObject()));
245+
}
246+
else if (jval.getValueType().equals(jakarta.json.JsonValue.ValueType.STRING)) {
247+
logger.fine("Parsing : " + jval.toString());
248+
}
249+
}
217250
String[] answers = getBestIdAndType(dft, jval);
218251
String id = answers[0];
219252
String type = answers[1];
@@ -335,7 +368,8 @@ private String[] getBestIdAndType(DatasetFieldType dft, JsonValue jv) {
335368
break;
336369
}
337370
}
338-
} else if (jo.containsKey(publicationURL.getLabel())) {
371+
}
372+
if (id == null && jo.containsKey(publicationURL.getLabel())) {
339373

340374
String value = jo.getString(publicationURL.getLabel());
341375
if (isURI(value)) {
@@ -412,4 +446,90 @@ private boolean isURI(String number) {
412446
return false;
413447
}
414448

449+
/**
450+
* Create a new DatasetField containing only values that are new compared to the
451+
* prior field. This creates a detached copy to avoid modifying the managed
452+
* entity.
453+
*
454+
* @param currentField The field from the current version
455+
* @param priorField The field from the prior version
456+
* @return A new DatasetField with only new values
457+
*/
458+
private DatasetField filterNewValues(DatasetField currentField, DatasetField priorField) {
459+
DatasetField filtered = new DatasetField();
460+
DatasetFieldType fieldType = currentField.getDatasetFieldType();
461+
filtered.setDatasetFieldType(fieldType);
462+
463+
// Handle primitive fields
464+
if (fieldType.isPrimitive()) {
465+
if (fieldType.isControlledVocabulary()) {
466+
// Handle controlled vocabulary fields
467+
List<ControlledVocabularyValue> currentCVs = currentField.getControlledVocabularyValues();
468+
List<ControlledVocabularyValue> priorCVs = priorField != null ? priorField.getControlledVocabularyValues() : new ArrayList<>();
469+
470+
List<ControlledVocabularyValue> newCVs = new ArrayList<>();
471+
for (ControlledVocabularyValue currentCV : currentCVs) {
472+
boolean isNew = true;
473+
for (ControlledVocabularyValue priorCV : priorCVs) {
474+
if (currentCV.getStrValue().equals(priorCV.getStrValue())) {
475+
isNew = false;
476+
break;
477+
}
478+
}
479+
if (isNew) {
480+
newCVs.add(currentCV);
481+
}
482+
}
483+
filtered.setControlledVocabularyValues(newCVs);
484+
} else {
485+
// Handle regular fields
486+
List<DatasetFieldValue> currentDFVs = currentField.getDatasetFieldValues();
487+
List<DatasetFieldValue> priorDFVs = priorField != null ? priorField.getDatasetFieldValues() : new ArrayList<>();
488+
489+
List<DatasetFieldValue> newDFVs = new ArrayList<>();
490+
for (DatasetFieldValue currentDFV : currentDFVs) {
491+
boolean isNew = true;
492+
for (DatasetFieldValue priorDFV : priorDFVs) {
493+
if (currentDFV.valuesEqual(priorDFV)) {
494+
isNew = false;
495+
break;
496+
}
497+
}
498+
if (isNew) {
499+
newDFVs.add(currentDFV);
500+
}
501+
}
502+
filtered.setDatasetFieldValues(newDFVs);
503+
}
504+
} else {
505+
// Handle compound fields
506+
List<DatasetFieldCompoundValue> currentCompounds = currentField.getDatasetFieldCompoundValues();
507+
List<DatasetFieldCompoundValue> priorCompounds = priorField != null ? priorField.getDatasetFieldCompoundValues() : new ArrayList<>();
508+
509+
List<DatasetFieldCompoundValue> newCompounds = new ArrayList<>();
510+
511+
for (DatasetFieldCompoundValue currentCompound : currentCompounds) {
512+
boolean isNew = true;
513+
514+
for (DatasetFieldCompoundValue priorCompound : priorCompounds) {
515+
516+
if (currentCompound.valuesEqual(priorCompound)) {
517+
isNew = false;
518+
break;
519+
}
520+
}
521+
522+
if (isNew) {
523+
// Create a copy of the compound value with all its children
524+
DatasetFieldCompoundValue newCompound = currentCompound.copy(filtered);
525+
newCompound.setParentDatasetField(filtered);
526+
newCompounds.add(newCompound);
527+
}
528+
}
529+
530+
filtered.setDatasetFieldCompoundValues(newCompounds);
531+
}
532+
533+
return filtered;
534+
}
415535
}

src/test/java/edu/harvard/iq/dataverse/api/LDNInboxIT.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
package edu.harvard.iq.dataverse.api;
32

43
import org.junit.jupiter.api.Test;

0 commit comments

Comments
 (0)