From fd4a6ad38bbf98ba4b38fc81d68ffc5e3ae6d36e Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Wed, 20 May 2026 16:47:27 +0200 Subject: [PATCH 1/3] Added orcid authority process --- .../orcid/script/OrcidAuthorityAssign.java | 209 +++++++++ ...cidAuthorityAssignScriptConfiguration.java | 43 ++ .../config/spring/api/scripts.xml | 5 + .../orcid/script/OrcidAuthorityAssignIT.java | 423 ++++++++++++++++++ dspace/config/modules/rest.cfg | 1 + dspace/config/spring/api/scripts.xml | 5 + dspace/config/spring/rest/scripts.xml | 5 + 7 files changed, 691 insertions(+) create mode 100644 dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssign.java create mode 100644 dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssignScriptConfiguration.java create mode 100644 dspace-api/src/test/java/org/dspace/orcid/script/OrcidAuthorityAssignIT.java diff --git a/dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssign.java b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssign.java new file mode 100644 index 000000000000..b645ce251ee6 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssign.java @@ -0,0 +1,209 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.orcid.script; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.cli.ParseException; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; +import org.dspace.content.authority.Choices; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.content.service.MetadataValueService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.eperson.factory.EPersonServiceFactory; +import org.dspace.scripts.DSpaceRunnable; +import org.dspace.utils.DSpace; + +/** + * Script that assigns ORCID-based authority values to dc.contributor.author metadata + * by matching author names found in dc.identifier.orcid metadata entries. + * The script always overwrites existing authority values to keep data up-to-date. + * + * @author Matus Kasak (dspace at dataquest.sk) + */ +public class OrcidAuthorityAssign + extends DSpaceRunnable> { + + private static final Logger LOGGER = LogManager.getLogger(); + + private static final Pattern ORCID_PATTERN = + Pattern.compile("(\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX])"); + + private MetadataFieldService metadataFieldService; + private MetadataValueService metadataValueService; + + private Context context; + + @Override + public void setup() throws ParseException { + this.metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService(); + this.metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); + } + + @Override + public void internalRun() throws Exception { + context = new Context(); + assignCurrentUserInContext(); + + try { + context.turnOffAuthorisationSystem(); + performAuthorityAssignment(); + context.complete(); + } catch (Exception e) { + handler.handleException(e); + context.abort(); + } finally { + context.restoreAuthSystemState(); + } + } + + /** + * Set the authority of dc.contributor.author metadata + * based on matching author names in dc.identifier.orcid. + */ + private void performAuthorityAssignment() throws SQLException, IOException, AuthorizeException { + // Build the author-name-to-ORCID map from dc.identifier.orcid + MetadataField orcidField = metadataFieldService.findByElement(context, "dc", "identifier", "orcid"); + if (orcidField == null) { + handler.logError("Metadata field dc.identifier.orcid not found in the registry. Aborting."); + return; + } + + List orcidValues = metadataValueService.findByField(context, orcidField); + handler.logInfo("Found " + orcidValues.size() + " dc.identifier.orcid metadata entries."); + + // Map: normalized author name -> ORCID ID + Map authorNameToOrcid = new HashMap<>(); + + for (MetadataValue orcidMv : orcidValues) { + String rawValue = orcidMv.getValue(); + if (StringUtils.isBlank(rawValue)) { + continue; + } + + // Extract the ORCID ID from the value + Matcher matcher = ORCID_PATTERN.matcher(rawValue); + if (!matcher.find()) { + handler.logWarning("Could not extract ORCID ID from value: " + rawValue); + continue; + } + String orcidId = matcher.group(1); + + // The author name is everything before the ORCID ID, trimmed + String authorName = rawValue.substring(0, matcher.start()).trim(); + if (StringUtils.isBlank(authorName)) { + handler.logWarning("Could not extract author name from value: " + rawValue); + continue; + } + + String normalizedName = normalizeAuthorName(authorName); + // If there's a duplicate author name with different ORCID + if (authorNameToOrcid.containsKey(normalizedName) + && !authorNameToOrcid.get(normalizedName).equals(orcidId)) { + handler.logWarning("Duplicate author name '" + authorName + + "' with different ORCIDs: " + authorNameToOrcid.get(normalizedName) + + " vs " + orcidId + ". Using the latest."); + } + authorNameToOrcid.put(normalizedName, orcidId); + } + + handler.logInfo("Built lookup map with " + authorNameToOrcid.size() + " unique author-ORCID mappings."); + + if (authorNameToOrcid.isEmpty()) { + handler.logInfo("No author-ORCID mappings found. Nothing to do."); + return; + } + + // Load all dc.contributor.author values + MetadataField authorField = metadataFieldService.findByElement(context, "dc", "contributor", "author"); + if (authorField == null) { + handler.logError("Metadata field dc.contributor.author not found in the registry. Aborting."); + return; + } + + List authorValues = metadataValueService.findByField(context, authorField); + handler.logInfo("Found " + authorValues.size() + " dc.contributor.author metadata entries to check."); + + // Match and update + int updated = 0; + int batchSize = 50; + + for (MetadataValue authorMv : authorValues) { + String authorValue = authorMv.getValue(); + if (StringUtils.isBlank(authorValue)) { + continue; + } + + String normalizedAuthor = normalizeAuthorName(authorValue); + String orcidId = authorNameToOrcid.get(normalizedAuthor); + + if (orcidId != null) { + authorMv.setAuthority(orcidId); + authorMv.setConfidence(Choices.CF_ACCEPTED); + metadataValueService.update(context, authorMv, true); + updated++; + + // Evict processed entities from the Hibernate session in batches + // to keep memory bounded. + if (updated % batchSize == 0) { + context.uncacheEntity(authorMv); + handler.logInfo("Progress: " + updated + " authors updated so far..."); + } + } + } + + context.commit(); + + handler.logInfo("Authority assignment complete. Updated: " + updated + + ", Total author entries checked: " + authorValues.size()); + LOGGER.info("OrcidAuthorityAssign updated {} dc.contributor.author entries.", updated); + } + + /** + * Normalize an author name for matching purposes. + */ + private String normalizeAuthorName(String name) { + if (name == null) { + return ""; + } + return name.trim().toLowerCase(Locale.ROOT).replace(",", "").replaceAll("\\s+", " "); + } + + /** + * Assigns the current user to the context. + */ + private void assignCurrentUserInContext() throws SQLException { + UUID uuid = getEpersonIdentifier(); + if (uuid != null) { + EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid); + context.setCurrentUser(ePerson); + } + } + + @Override + @SuppressWarnings("unchecked") + public OrcidAuthorityAssignScriptConfiguration getScriptConfiguration() { + return new DSpace().getServiceManager().getServiceByName("orcid-authority-assign", + OrcidAuthorityAssignScriptConfiguration.class); + } +} diff --git a/dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssignScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssignScriptConfiguration.java new file mode 100644 index 000000000000..55bec59c61aa --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidAuthorityAssignScriptConfiguration.java @@ -0,0 +1,43 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.orcid.script; + +import org.apache.commons.cli.Options; +import org.dspace.scripts.configuration.ScriptConfiguration; + +/** + * Script configuration for {@link OrcidAuthorityAssign}. + * + * This script assigns ORCID-based authority values to dc.contributor.author metadata + * by matching author names found in dc.identifier.orcid metadata entries. + * + * @param the OrcidAuthorityAssign type + */ +public class OrcidAuthorityAssignScriptConfiguration + extends ScriptConfiguration { + + private Class dspaceRunnableClass; + + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + @Override + public void setDspaceRunnableClass(Class dspaceRunnableClass) { + this.dspaceRunnableClass = dspaceRunnableClass; + } + + @Override + public Options getOptions() { + if (options == null) { + super.options = new Options(); + } + return options; + } +} diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml index e388065b68fd..e9767fe4a443 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml @@ -65,6 +65,11 @@ + + + + + diff --git a/dspace-api/src/test/java/org/dspace/orcid/script/OrcidAuthorityAssignIT.java b/dspace-api/src/test/java/org/dspace/orcid/script/OrcidAuthorityAssignIT.java new file mode 100644 index 000000000000..d024b9a4339e --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/orcid/script/OrcidAuthorityAssignIT.java @@ -0,0 +1,423 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.orcid.script; + +import static org.dspace.app.launcher.ScriptLauncher.handleScript; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import java.util.List; + +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.launcher.ScriptLauncher; +import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; +import org.dspace.content.MetadataValue; +import org.dspace.content.authority.Choices; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.content.service.MetadataSchemaService; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for {@link OrcidAuthorityAssign}. + */ +public class OrcidAuthorityAssignIT extends AbstractIntegrationTestWithDatabase { + + private Collection publicationCollection; + + private ItemService itemService; + private MetadataFieldService metadataFieldService; + private MetadataSchemaService metadataSchemaService; + + @Before + public void setup() throws Exception { + itemService = ContentServiceFactory.getInstance().getItemService(); + metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService(); + metadataSchemaService = ContentServiceFactory.getInstance().getMetadataSchemaService(); + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent community") + .build(); + + publicationCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Publications") + .build(); + + // Ensure dc.identifier.orcid metadata field exists + ensureOrcidIdentifierFieldExists(); + + context.restoreAuthSystemState(); + } + + /** + * Test basic scenario: one item with one author having an ORCID. + * The script should assign authority to that author. + */ + @Test + public void testBasicAuthorityAssignment() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Test publication") + .withAuthor("Smith, Donald") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + // Reload item to check updated metadata + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertThat(authors, hasSize(1)); + assertEquals("1234-5678-9012-3456", authors.get(0).getAuthority()); + assertEquals(Choices.CF_ACCEPTED, authors.get(0).getConfidence()); + } + + /** + * Test scenario with multiple authors on one item, only one has an ORCID. + */ + @Test + public void testMultipleAuthorsOnlyOneWithOrcid() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Multi-author publication") + .withAuthor("Smith, Donald") + .withAuthor("Doe, Jane") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertThat(authors, hasSize(2)); + + // Find each author by value and check authority + for (MetadataValue mv : authors) { + if ("Smith, Donald".equals(mv.getValue())) { + assertEquals("1234-5678-9012-3456", mv.getAuthority()); + assertEquals(Choices.CF_ACCEPTED, mv.getConfidence()); + } else if ("Doe, Jane".equals(mv.getValue())) { + assertNull(mv.getAuthority()); + } + } + } + + /** + * Test that the same author appearing across multiple items gets authority assigned on all. + */ + @Test + public void testSameAuthorAcrossMultipleItems() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item1 = ItemBuilder.createItem(context, publicationCollection) + .withTitle("First publication") + .withAuthor("Smith, Donald") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + Item item2 = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Second publication") + .withAuthor("Smith, Donald") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + // Both items should have authority on the same author + context.turnOffAuthorisationSystem(); + item1 = context.reloadEntity(item1); + item2 = context.reloadEntity(item2); + + List authors1 = itemService.getMetadata(item1, "dc", "contributor", "author", Item.ANY); + List authors2 = itemService.getMetadata(item2, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertEquals("1234-5678-9012-3456", authors1.get(0).getAuthority()); + assertEquals("1234-5678-9012-3456", authors2.get(0).getAuthority()); + } + + /** + * Test that existing authority is overwritten (always keep data up-to-date). + */ + @Test + public void testOverwritesExistingAuthority() throws Exception { + context.turnOffAuthorisationSystem(); + + // Create item with an author and ORCID entry + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Publication with existing authority") + .withAuthor("Smith, Donald") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + item = context.reloadEntity(item); + List authorsBefore = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + assertThat(authorsBefore, hasSize(1)); + MetadataValue authorMv = authorsBefore.get(0); + authorMv.setAuthority("old-authority-value"); + authorMv.setConfidence(Choices.CF_UNCERTAIN); + itemService.update(context, item); + context.commit(); + + // Verify original authority was persisted + item = context.reloadEntity(item); + authorsBefore = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + assertEquals("old-authority-value", authorsBefore.get(0).getAuthority()); + assertEquals(Choices.CF_UNCERTAIN, authorsBefore.get(0).getConfidence()); + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + // Authority should be overwritten with the ORCID + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authorsAfter = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertEquals("1234-5678-9012-3456", authorsAfter.get(0).getAuthority()); + assertEquals(Choices.CF_ACCEPTED, authorsAfter.get(0).getConfidence()); + } + + /** + * Test that running the script with no dc.identifier.orcid entries does nothing. + */ + @Test + public void testNoOrcidEntriesDoesNothing() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("No ORCID publication") + .withAuthor("Doe, Jane") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getInfoMessages(), hasItem("No author-ORCID mappings found. Nothing to do.")); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertThat(authors.get(0).getAuthority(), is(nullValue())); + } + + /** + * Test that an ORCID ID with X checksum digit is handled correctly. + */ + @Test + public void testOrcidWithXChecksumDigit() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Checksum X publication") + .withAuthor("White, Walter") + .withMetadata("dc", "identifier", "orcid", "White, Walter 1234-5678-9012-345X") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertEquals("1234-5678-9012-345X", authors.get(0).getAuthority()); + assertEquals(Choices.CF_ACCEPTED, authors.get(0).getConfidence()); + } + + /** + * Test case-insensitive matching of author names. + */ + @Test + public void testCaseInsensitiveMatching() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Case test publication") + .withAuthor("smith, donald") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertEquals("1234-5678-9012-3456", authors.get(0).getAuthority()); + } + + /** + * Test that an author not matching any ORCID entry keeps no authority. + */ + @Test + public void testNonMatchingAuthorKeepsNoAuthority() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Non-matching publication") + .withAuthor("Pinkman, Jesse") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertThat(authors, hasSize(1)); + assertNull(authors.get(0).getAuthority()); + } + + /** + * Test with multiple ORCID entries for different authors. + */ + @Test + public void testMultipleOrcidEntries() throws Exception { + context.turnOffAuthorisationSystem(); + + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Multi-ORCID publication") + .withAuthor("Smith, Donald") + .withAuthor("Doe, Jane") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .withMetadata("dc", "identifier", "orcid", "Doe, Jane 1234-5678-9012-7890") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertThat(authors, hasSize(2)); + + for (MetadataValue mv : authors) { + if ("Smith, Donald".equals(mv.getValue())) { + assertEquals("1234-5678-9012-3456", mv.getAuthority()); + assertEquals(Choices.CF_ACCEPTED, mv.getConfidence()); + } else if ("Doe, Jane".equals(mv.getValue())) { + assertEquals("1234-5678-9012-7890", mv.getAuthority()); + assertEquals(Choices.CF_ACCEPTED, mv.getConfidence()); + } + } + } + + /** + * Test that author matching works even when dc.contributor.author has no comma + * but dc.identifier.orcid uses "Lastname, Firstname ORCID" format (and vice versa). + */ + @Test + public void testCommaFormatMismatch() throws Exception { + context.turnOffAuthorisationSystem(); + + // Author without comma, ORCID entry with comma + Item item = ItemBuilder.createItem(context, publicationCollection) + .withTitle("Comma mismatch publication") + .withAuthor("Smith Donald") + .withMetadata("dc", "identifier", "orcid", "Smith, Donald 1234-5678-9012-3456") + .build(); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = runScript(); + + assertThat(handler.getErrorMessages(), empty()); + + context.turnOffAuthorisationSystem(); + item = context.reloadEntity(item); + List authors = itemService.getMetadata(item, "dc", "contributor", "author", Item.ANY); + context.restoreAuthSystemState(); + + assertThat(authors, hasSize(1)); + assertEquals("1234-5678-9012-3456", authors.get(0).getAuthority()); + assertEquals(Choices.CF_ACCEPTED, authors.get(0).getConfidence()); + } + + /** + * Ensure the dc.identifier.orcid metadata field exists in the database. + * This field is Mendelu-specific and may not exist in the default test registry. + */ + private void ensureOrcidIdentifierFieldExists() throws Exception { + MetadataSchema dcSchema = metadataSchemaService.find(context, "dc"); + MetadataField field = metadataFieldService.findByElement(context, "dc", "identifier", "orcid"); + if (field == null) { + metadataFieldService.create(context, dcSchema, "identifier", "orcid", + "ORCID identifier of an author in format: AuthorName ORCID-ID"); + } + } + + private TestDSpaceRunnableHandler runScript() throws Exception { + String[] args = new String[] { "orcid-authority-assign" }; + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + return handler; + } +} diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 2035077baffe..5dadb55db9fa 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -42,6 +42,7 @@ rest.properties.exposed = orcid.application-client-id rest.properties.exposed = orcid.authorize-url rest.properties.exposed = orcid.scope rest.properties.exposed = orcid.disconnection.allowed-users +rest.properties.exposed = orcid.domain-url rest.properties.exposed = registration.verification.enabled rest.properties.exposed = websvc.opensearch.enable rest.properties.exposed = websvc.opensearch.svccontext diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index d913a30b668e..42c3e7675d45 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -66,6 +66,11 @@ + + + + + diff --git a/dspace/config/spring/rest/scripts.xml b/dspace/config/spring/rest/scripts.xml index 5fe487ddcc5d..7fa20b08acd2 100644 --- a/dspace/config/spring/rest/scripts.xml +++ b/dspace/config/spring/rest/scripts.xml @@ -49,6 +49,11 @@ + + + + + From 341f18717ca25d1dad3613c7093d0735c1c59f27 Mon Sep 17 00:00:00 2001 From: Matus Kasak Date: Mon, 25 May 2026 09:41:44 +0200 Subject: [PATCH 2/3] Used sandbox for testing --- dspace-api/src/test/data/dspaceFolder/config/local.cfg | 3 +++ dspace/config/modules/orcid.cfg | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 7cd47bf4e170..fb05ea8f0e61 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -171,6 +171,9 @@ management.health.solrOai.enabled = false researcher-profile.entity-type = Person orcid.synchronization-enabled = true +# Use ORCID sandbox for tests +orcid.domain-url = https://sandbox.orcid.org + # Configuration settings required for Researcher Profiles # These settings ensure "dspace.object.owner" field are indexed by Authority Control choices.plugin.dspace.object.owner = EPersonAuthority diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg index cde819677447..3d045ea070ad 100644 --- a/dspace/config/modules/orcid.cfg +++ b/dspace/config/modules/orcid.cfg @@ -12,7 +12,7 @@ orcid.disconnection.allowed-users = admin_and_owner #------------------------------------------------------------------# # ORCID API (https://github.com/ORCID/ORCID-Source/tree/master/orcid-api-web#endpoints) -orcid.domain-url= https://sandbox.orcid.org +orcid.domain-url= https://orcid.org orcid.authorize-url = ${orcid.domain-url}/oauth/authorize orcid.token-url = ${orcid.domain-url}/oauth/token orcid.api-url = https://api.sandbox.orcid.org/v3.0 From a8d01428e0889340e89e1d5a8981bf04b48f5676 Mon Sep 17 00:00:00 2001 From: Kasinhou <129340513+Kasinhou@users.noreply.github.com> Date: Tue, 26 May 2026 11:44:37 +0200 Subject: [PATCH 3/3] Fix spacing Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- dspace/config/modules/orcid.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg index 3d045ea070ad..b47ed3d6720a 100644 --- a/dspace/config/modules/orcid.cfg +++ b/dspace/config/modules/orcid.cfg @@ -12,7 +12,7 @@ orcid.disconnection.allowed-users = admin_and_owner #------------------------------------------------------------------# # ORCID API (https://github.com/ORCID/ORCID-Source/tree/master/orcid-api-web#endpoints) -orcid.domain-url= https://orcid.org +orcid.domain-url = https://orcid.org orcid.authorize-url = ${orcid.domain-url}/oauth/authorize orcid.token-url = ${orcid.domain-url}/oauth/token orcid.api-url = https://api.sandbox.orcid.org/v3.0