Skip to content

Commit cf33783

Browse files
KasinhouMatus Kasak
andauthored
ZCU-PUB/Added orcid authority process (#1316)
Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
1 parent 105b14f commit cf33783

7 files changed

Lines changed: 691 additions & 0 deletions

File tree

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
package org.dspace.orcid.script;
9+
10+
import java.io.IOException;
11+
import java.sql.SQLException;
12+
import java.util.HashMap;
13+
import java.util.List;
14+
import java.util.Locale;
15+
import java.util.Map;
16+
import java.util.UUID;
17+
import java.util.regex.Matcher;
18+
import java.util.regex.Pattern;
19+
20+
import org.apache.commons.cli.ParseException;
21+
import org.apache.commons.lang3.StringUtils;
22+
import org.apache.logging.log4j.LogManager;
23+
import org.apache.logging.log4j.Logger;
24+
import org.dspace.authorize.AuthorizeException;
25+
import org.dspace.content.MetadataField;
26+
import org.dspace.content.MetadataValue;
27+
import org.dspace.content.authority.Choices;
28+
import org.dspace.content.factory.ContentServiceFactory;
29+
import org.dspace.content.service.MetadataFieldService;
30+
import org.dspace.content.service.MetadataValueService;
31+
import org.dspace.core.Context;
32+
import org.dspace.eperson.EPerson;
33+
import org.dspace.eperson.factory.EPersonServiceFactory;
34+
import org.dspace.scripts.DSpaceRunnable;
35+
import org.dspace.utils.DSpace;
36+
37+
/**
38+
* Script that assigns ORCID-based authority values to dc.contributor.author metadata
39+
* by matching author names found in dc.identifier.orcid metadata entries.
40+
* The script always overwrites existing authority values to keep data up-to-date.
41+
*
42+
* @author Matus Kasak (dspace at dataquest.sk)
43+
*/
44+
public class OrcidAuthorityAssign
45+
extends DSpaceRunnable<OrcidAuthorityAssignScriptConfiguration<OrcidAuthorityAssign>> {
46+
47+
private static final Logger LOGGER = LogManager.getLogger();
48+
49+
private static final Pattern ORCID_PATTERN =
50+
Pattern.compile("(\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX])");
51+
52+
private MetadataFieldService metadataFieldService;
53+
private MetadataValueService metadataValueService;
54+
55+
private Context context;
56+
57+
@Override
58+
public void setup() throws ParseException {
59+
this.metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService();
60+
this.metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService();
61+
}
62+
63+
@Override
64+
public void internalRun() throws Exception {
65+
context = new Context();
66+
assignCurrentUserInContext();
67+
68+
try {
69+
context.turnOffAuthorisationSystem();
70+
performAuthorityAssignment();
71+
context.complete();
72+
} catch (Exception e) {
73+
handler.handleException(e);
74+
context.abort();
75+
} finally {
76+
context.restoreAuthSystemState();
77+
}
78+
}
79+
80+
/**
81+
* Set the authority of dc.contributor.author metadata
82+
* based on matching author names in dc.identifier.orcid.
83+
*/
84+
private void performAuthorityAssignment() throws SQLException, IOException, AuthorizeException {
85+
// Build the author-name-to-ORCID map from dc.identifier.orcid
86+
MetadataField orcidField = metadataFieldService.findByElement(context, "dc", "identifier", "orcid");
87+
if (orcidField == null) {
88+
handler.logError("Metadata field dc.identifier.orcid not found in the registry. Aborting.");
89+
return;
90+
}
91+
92+
List<MetadataValue> orcidValues = metadataValueService.findByField(context, orcidField);
93+
handler.logInfo("Found " + orcidValues.size() + " dc.identifier.orcid metadata entries.");
94+
95+
// Map: normalized author name -> ORCID ID
96+
Map<String, String> authorNameToOrcid = new HashMap<>();
97+
98+
for (MetadataValue orcidMv : orcidValues) {
99+
String rawValue = orcidMv.getValue();
100+
if (StringUtils.isBlank(rawValue)) {
101+
continue;
102+
}
103+
104+
// Extract the ORCID ID from the value
105+
Matcher matcher = ORCID_PATTERN.matcher(rawValue);
106+
if (!matcher.find()) {
107+
handler.logWarning("Could not extract ORCID ID from value: " + rawValue);
108+
continue;
109+
}
110+
String orcidId = matcher.group(1);
111+
112+
// The author name is everything before the ORCID ID, trimmed
113+
String authorName = rawValue.substring(0, matcher.start()).trim();
114+
if (StringUtils.isBlank(authorName)) {
115+
handler.logWarning("Could not extract author name from value: " + rawValue);
116+
continue;
117+
}
118+
119+
String normalizedName = normalizeAuthorName(authorName);
120+
// If there's a duplicate author name with different ORCID
121+
if (authorNameToOrcid.containsKey(normalizedName)
122+
&& !authorNameToOrcid.get(normalizedName).equals(orcidId)) {
123+
handler.logWarning("Duplicate author name '" + authorName
124+
+ "' with different ORCIDs: " + authorNameToOrcid.get(normalizedName)
125+
+ " vs " + orcidId + ". Using the latest.");
126+
}
127+
authorNameToOrcid.put(normalizedName, orcidId);
128+
}
129+
130+
handler.logInfo("Built lookup map with " + authorNameToOrcid.size() + " unique author-ORCID mappings.");
131+
132+
if (authorNameToOrcid.isEmpty()) {
133+
handler.logInfo("No author-ORCID mappings found. Nothing to do.");
134+
return;
135+
}
136+
137+
// Load all dc.contributor.author values
138+
MetadataField authorField = metadataFieldService.findByElement(context, "dc", "contributor", "author");
139+
if (authorField == null) {
140+
handler.logError("Metadata field dc.contributor.author not found in the registry. Aborting.");
141+
return;
142+
}
143+
144+
List<MetadataValue> authorValues = metadataValueService.findByField(context, authorField);
145+
handler.logInfo("Found " + authorValues.size() + " dc.contributor.author metadata entries to check.");
146+
147+
// Match and update
148+
int updated = 0;
149+
int batchSize = 50;
150+
151+
for (MetadataValue authorMv : authorValues) {
152+
String authorValue = authorMv.getValue();
153+
if (StringUtils.isBlank(authorValue)) {
154+
continue;
155+
}
156+
157+
String normalizedAuthor = normalizeAuthorName(authorValue);
158+
String orcidId = authorNameToOrcid.get(normalizedAuthor);
159+
160+
if (orcidId != null) {
161+
authorMv.setAuthority(orcidId);
162+
authorMv.setConfidence(Choices.CF_ACCEPTED);
163+
metadataValueService.update(context, authorMv, true);
164+
updated++;
165+
166+
// Evict processed entities from the Hibernate session in batches
167+
// to keep memory bounded.
168+
if (updated % batchSize == 0) {
169+
context.uncacheEntity(authorMv);
170+
handler.logInfo("Progress: " + updated + " authors updated so far...");
171+
}
172+
}
173+
}
174+
175+
context.commit();
176+
177+
handler.logInfo("Authority assignment complete. Updated: " + updated
178+
+ ", Total author entries checked: " + authorValues.size());
179+
LOGGER.info("OrcidAuthorityAssign updated {} dc.contributor.author entries.", updated);
180+
}
181+
182+
/**
183+
* Normalize an author name for matching purposes.
184+
*/
185+
private String normalizeAuthorName(String name) {
186+
if (name == null) {
187+
return "";
188+
}
189+
return name.trim().toLowerCase(Locale.ROOT).replace(",", "").replaceAll("\\s+", " ");
190+
}
191+
192+
/**
193+
* Assigns the current user to the context.
194+
*/
195+
private void assignCurrentUserInContext() throws SQLException {
196+
UUID uuid = getEpersonIdentifier();
197+
if (uuid != null) {
198+
EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid);
199+
context.setCurrentUser(ePerson);
200+
}
201+
}
202+
203+
@Override
204+
@SuppressWarnings("unchecked")
205+
public OrcidAuthorityAssignScriptConfiguration<OrcidAuthorityAssign> getScriptConfiguration() {
206+
return new DSpace().getServiceManager().getServiceByName("orcid-authority-assign",
207+
OrcidAuthorityAssignScriptConfiguration.class);
208+
}
209+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/**
2+
* The contents of this file are subject to the license and copyright
3+
* detailed in the LICENSE and NOTICE files at the root of the source
4+
* tree and available online at
5+
*
6+
* http://www.dspace.org/license/
7+
*/
8+
package org.dspace.orcid.script;
9+
10+
import org.apache.commons.cli.Options;
11+
import org.dspace.scripts.configuration.ScriptConfiguration;
12+
13+
/**
14+
* Script configuration for {@link OrcidAuthorityAssign}.
15+
*
16+
* This script assigns ORCID-based authority values to dc.contributor.author metadata
17+
* by matching author names found in dc.identifier.orcid metadata entries.
18+
*
19+
* @param <T> the OrcidAuthorityAssign type
20+
*/
21+
public class OrcidAuthorityAssignScriptConfiguration<T extends OrcidAuthorityAssign>
22+
extends ScriptConfiguration<T> {
23+
24+
private Class<T> dspaceRunnableClass;
25+
26+
@Override
27+
public Class<T> getDspaceRunnableClass() {
28+
return dspaceRunnableClass;
29+
}
30+
31+
@Override
32+
public void setDspaceRunnableClass(Class<T> dspaceRunnableClass) {
33+
this.dspaceRunnableClass = dspaceRunnableClass;
34+
}
35+
36+
@Override
37+
public Options getOptions() {
38+
if (options == null) {
39+
super.options = new Options();
40+
}
41+
return options;
42+
}
43+
}

dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
<property name="dspaceRunnableClass" value="org.dspace.orcid.script.OrcidBulkPush"/>
6666
</bean>
6767

68+
<bean id="orcid-authority-assign" class="org.dspace.orcid.script.OrcidAuthorityAssignScriptConfiguration">
69+
<property name="description" value="Assign ORCID-based authority to dc.contributor.author metadata by matching author names in dc.identifier.orcid"/>
70+
<property name="dspaceRunnableClass" value="org.dspace.orcid.script.OrcidAuthorityAssign"/>
71+
</bean>
72+
6873
<bean id="process-cleaner" class="org.dspace.administer.ProcessCleanerCliConfiguration">
6974
<property name="description" value="Cleanup all the old processes in the specified state"/>
7075
<property name="dspaceRunnableClass" value="org.dspace.administer.ProcessCleanerCli"/>

0 commit comments

Comments
 (0)