|
| 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 | +} |
0 commit comments