Skip to content

Commit 64d2757

Browse files
Issue 1361 fix: DOI Organizer creates duplicate dc.identifier.doi metadata (ufal#1368)
* Issue 1361 fix: DOI Organizer creates duplicate dc.identifier.doi metadata * copilot comments * Issue 1361: make DOI metadata save additive, report duplicates via QA saveDOIToObject now adds dc.identifier.doi only when that exact value is not already present, and no longer deletes metadata. The previous fix cleared existing values when more than one was found or when a different DOI was present; since this method runs after the DOI has already been registered with the external agency, silently dropping a (possibly legacy/citable) identifier is lossy and irreversible. The operation stays idempotent, so re-registration no longer creates duplicate values. Items that legitimately end up with more than one dc.identifier.doi value are now surfaced for manual review by adding dc.identifier.doi to the ItemMetadataQAChecker noDuplicate list (metadataqa curation task) rather than being cleaned up silently in the write path. Tests: flip the replace test to assert a pre-existing different DOI is preserved alongside the new one, keep the idempotency test, and add a QA checker IT asserting an item with two DOIs fails curation. * local field renaming --------- Co-authored-by: Ondrej Kosarko <kosarko@ufal.mff.cuni.cz> (cherry picked from commit 3b7db4c)
1 parent 867e43d commit 64d2757

4 files changed

Lines changed: 118 additions & 6 deletions

File tree

dspace-api/src/main/java/org/dspace/ctask/general/ItemMetadataQAChecker.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public void init(Curator curator, String taskId) throws IOException {
8787
"dc.rights.label",
8888
"dc.date.available",
8989
"dc.source.uri",
90+
"dc.identifier.doi",
9091
"metashare.ResourceInfo#DistributionInfo#LicenseInfo.license"
9192
});
9293
strangeMetadata = configurationService.getArrayProperty("lr.curation.metadata.strange", new String[]{

dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,13 +1067,26 @@ protected void saveDOIToObject(Context context, DSpaceObject dso, String doi)
10671067
}
10681068
Item item = (Item) dso;
10691069

1070-
itemService.addMetadata(context, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null,
1071-
doiService.DOIToExternalForm(doi));
1072-
try {
1073-
itemService.update(context, item);
1074-
} catch (SQLException | AuthorizeException ex) {
1075-
throw ex;
1070+
String doiURL = doiService.DOIToExternalForm(doi);
1071+
1072+
// Add the DOI to the metadata only if this exact value is not present yet. This keeps the operation
1073+
// idempotent (re-registration does not create duplicate values) without ever deleting metadata: a
1074+
// pre-existing, different DOI is left untouched. This method is called after the DOI has already been
1075+
// registered with the external agency, so destroying metadata here would be lossy and irreversible.
1076+
// Items that end up with more than one dc.identifier.doi value are surfaced by the ItemMetadataQAChecker
1077+
// curation task for manual review.
1078+
List<MetadataValue> existing = itemService.getMetadata(item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, Item.ANY);
1079+
boolean alreadyPresent = existing.stream()
1080+
.anyMatch(metadataValue -> doiURL.equals(metadataValue.getValue()));
1081+
1082+
if (alreadyPresent) {
1083+
log.debug("The DOI {} is already part of the metadata of Item {}. Not adding it again.",
1084+
doi, item.getID());
1085+
return;
10761086
}
1087+
1088+
itemService.addMetadata(context, item, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, doiURL);
1089+
itemService.update(context, item);
10771090
}
10781091

10791092
/**

dspace-api/src/test/java/org/dspace/curate/ItemMetadataQACheckerIT.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public class ItemMetadataQACheckerIT extends AbstractIntegrationTestWithDatabase
6363
Item itemWithIncorrectLanguageName;
6464
Item itemWithTwoAvailableDates;
6565
Item itemWithTwoAvailableDatesAndLang;
66+
Item itemWithTwoDois;
6667
Item itemVersion1;
6768
Item itemVersion2;
6869
Item itemVersion3;
@@ -146,6 +147,13 @@ public void setUp() throws Exception {
146147
itemService.addMetadata(context, itemWithTwoAvailableDatesAndLang,"dc", "date",
147148
"available", "en_US", "2021-01-01");
148149

150+
itemWithTwoDois = ItemBuilder.createItem(context, collection)
151+
.withTitle("Item With Two DOIs")
152+
.withMetadata("dc", "type", null, "corpus")
153+
.withMetadata("dc", "identifier", "doi", "https://doi.org/10.5072/test-1")
154+
.withMetadata("dc", "identifier", "doi", "https://doi.org/10.5072/test-2")
155+
.build();
156+
149157
itemVersion1 = ItemBuilder.createItem(context, collection)
150158
.withTitle("Item Version 1")
151159
.withMetadata("dc", "type", null, "corpus")
@@ -233,6 +241,20 @@ public void testItemWithTwoAvailableDatesAndLang() throws IOException {
233241
assertTrue("Result should mention multiple dc.date.available", result.contains("dc.date.available"));
234242
}
235243

244+
@Test
245+
public void testItemWithTwoDois() throws IOException {
246+
Curator curator = new Curator();
247+
curator.addTask(TASK_NAME);
248+
context.setCurrentUser(admin);
249+
250+
// Run curator task for item with two dc.identifier.doi - should fail
251+
curator.curate(context, itemWithTwoDois.getHandle());
252+
int status = curator.getStatus(TASK_NAME);
253+
assertEquals("Curation should fail for item with two dc.identifier.doi", Curator.CURATE_FAIL, status);
254+
String result = curator.getResult(TASK_NAME);
255+
assertTrue("Result should mention multiple dc.identifier.doi", result.contains("dc.identifier.doi"));
256+
}
257+
236258
@Test
237259
public void testValidItem() throws IOException {
238260
Curator curator = new Curator();

dspace-api/src/test/java/org/dspace/identifier/DOIIdentifierProviderTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Date;
2424
import java.util.List;
2525
import java.util.Random;
26+
import java.util.stream.Collectors;
2627

2728
import org.apache.commons.collections4.CollectionUtils;
2829
import org.apache.commons.lang3.ObjectUtils;
@@ -332,6 +333,51 @@ public void testStore_DOI_as_item_metadata()
332333
assertTrue("Cannot store DOI as item metadata value.", result);
333334
}
334335

336+
@Test
337+
public void testStore_DOI_keeps_existing_different_doi_metadata() throws SQLException, AuthorizeException,
338+
IOException, IdentifierException, IllegalAccessException, WorkflowException {
339+
Item item = newItem();
340+
341+
// this checks that the method does not fail if there is already a *different* DOI in the metadata,
342+
// here we verify that the existing DOI is preserved (not deleted) and the new one is added alongside it.
343+
// Items with more than one DOI are reported by the ItemMetadataQAChecker curation task, not silently
344+
// cleaned up here.
345+
String oldDoi = DOI.SCHEME + PREFIX + "/" + NAMESPACE_SEPARATOR + "1234";
346+
String newDoi = DOI.SCHEME + PREFIX + "/" + NAMESPACE_SEPARATOR + Long.toHexString(new Date().getTime());
347+
348+
context.turnOffAuthorisationSystem();
349+
itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA,
350+
DOIIdentifierProvider.DOI_ELEMENT,
351+
DOIIdentifierProvider.DOI_QUALIFIER,
352+
null,
353+
doiService.DOIToExternalForm(oldDoi));
354+
provider.saveDOIToObject(context, item, newDoi);
355+
context.restoreAuthSystemState();
356+
357+
checkDoiMetadata(item, oldDoi, newDoi);
358+
}
359+
360+
@Test
361+
public void testStore_DOI_check_single_doi_metadata() throws SQLException, AuthorizeException, IOException,
362+
IdentifierException, IllegalAccessException, WorkflowException {
363+
Item item = newItem();
364+
365+
// this checks that the method does not fail if there is already a DOI in the metadata,
366+
// here we check if DOI metadata are not duplicated
367+
String doi = DOI.SCHEME + PREFIX + "/" + NAMESPACE_SEPARATOR + Long.toHexString(new Date().getTime());
368+
369+
context.turnOffAuthorisationSystem();
370+
itemService.addMetadata(context, item, DOIIdentifierProvider.MD_SCHEMA,
371+
DOIIdentifierProvider.DOI_ELEMENT,
372+
DOIIdentifierProvider.DOI_QUALIFIER,
373+
null,
374+
doiService.DOIToExternalForm(doi));
375+
provider.saveDOIToObject(context, item, doi);
376+
context.restoreAuthSystemState();
377+
378+
checkSingleDoiMetadata(item, doi);
379+
}
380+
335381
@Test
336382
public void testGet_DOI_out_of_item_metadata()
337383
throws SQLException, AuthorizeException, IOException, IdentifierException, IllegalAccessException,
@@ -868,4 +914,34 @@ public void testLoadOrCreateDOIReturnsMintedStatus()
868914
// registerOnline
869915
// reserveOnline
870916

917+
private void checkSingleDoiMetadata(Item item, String doi) throws IdentifierException {
918+
List<MetadataValue> metadata = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA,
919+
DOIIdentifierProvider.DOI_ELEMENT,
920+
DOIIdentifierProvider.DOI_QUALIFIER,
921+
Item.ANY);
922+
boolean result = false;
923+
if (metadata.size() == 1 && metadata.get(0).getValue().equals(doiService.DOIToExternalForm(doi))) {
924+
result = true;
925+
}
926+
assertTrue("Invalid or duplicate 'dc.identifier.doi' metadata value(s).", result);
927+
}
928+
929+
private void checkDoiMetadata(Item item, String... dois) throws IdentifierException {
930+
List<String> values = itemService.getMetadata(item, DOIIdentifierProvider.MD_SCHEMA,
931+
DOIIdentifierProvider.DOI_ELEMENT,
932+
DOIIdentifierProvider.DOI_QUALIFIER,
933+
Item.ANY)
934+
.stream()
935+
.map(MetadataValue::getValue)
936+
.collect(Collectors.toList());
937+
938+
List<String> expected = new ArrayList<>();
939+
for (String doi : dois) {
940+
expected.add(doiService.DOIToExternalForm(doi));
941+
}
942+
943+
assertEquals("Unexpected number of 'dc.identifier.doi' metadata values.", expected.size(), values.size());
944+
assertTrue("Expected 'dc.identifier.doi' metadata values are missing.", values.containsAll(expected));
945+
}
946+
871947
}

0 commit comments

Comments
 (0)