[TLC-1097] ORCID external identifier sync fix

Handle SELF and PART_OF identifiers properly based on
configuration, work type, and identifier type
This commit is contained in:
Kim Shepherd
2025-05-28 17:26:13 +02:00
parent cac7eec084
commit ad82b31c74
7 changed files with 201 additions and 53 deletions

View File

@@ -39,6 +39,7 @@ public class OrcidWorkFieldMapping {
* The metadata fields related to the work external identifiers.
*/
private Map<String, String> externalIdentifierFields = new HashMap<>();
private Map<String, List<String>> externalIdentifierPartOfMap = new HashMap<>();
/**
* The metadata field related to the work publication date.
@@ -129,6 +130,15 @@ public class OrcidWorkFieldMapping {
this.externalIdentifierFields = parseConfigurations(externalIdentifierFields);
}
public Map<String, List<String>> getExternalIdentifierPartOfMap() {
return this.externalIdentifierPartOfMap;
}
public void setExternalIdentifierPartOfMap(
HashMap<String, List<String>> externalIdentifierPartOfMap) {
this.externalIdentifierPartOfMap = externalIdentifierPartOfMap;
}
public String getPublicationDateField() {
return publicationDateField;
}

View File

@@ -9,6 +9,7 @@ package org.dspace.orcid.model.factory.impl;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.orcid.jaxb.model.common.Relationship.PART_OF;
import static org.orcid.jaxb.model.common.Relationship.SELF;
import java.util.ArrayList;
@@ -73,12 +74,12 @@ public class OrcidWorkFactory implements OrcidEntityFactory {
@Override
public Activity createOrcidObject(Context context, Item item) {
Work work = new Work();
work.setWorkType(getWorkType(context, item));
work.setJournalTitle(getJournalTitle(context, item));
work.setWorkContributors(getWorkContributors(context, item));
work.setWorkTitle(getWorkTitle(context, item));
work.setPublicationDate(getPublicationDate(context, item));
work.setWorkExternalIdentifiers(getWorkExternalIds(context, item));
work.setWorkType(getWorkType(context, item));
work.setWorkExternalIdentifiers(getWorkExternalIds(context, item, work));
work.setShortDescription(getShortDescription(context, item));
work.setLanguageCode(getLanguageCode(context, item));
work.setUrl(getUrl(context, item));
@@ -148,58 +149,62 @@ public class OrcidWorkFactory implements OrcidEntityFactory {
.orElse(null);
}
/**
* Creates an instance of ExternalIDs from the metadata values of the given
* item, using the orcid.mapping.funding.external-ids configuration.
*/
private ExternalIDs getWorkExternalIds(Context context, Item item) {
ExternalIDs externalIdentifiers = new ExternalIDs();
externalIdentifiers.getExternalIdentifier().addAll(getWorkSelfExternalIds(context, item));
return externalIdentifiers;
private ExternalIDs getWorkExternalIds(Context context, Item item, Work work) {
ExternalIDs externalIDs = new ExternalIDs();
externalIDs.getExternalIdentifier().addAll(getWorkExternalIdList(context, item, work));
return externalIDs;
}
/**
* Creates a list of ExternalID, one for orcid.mapping.funding.external-ids
* value, taking the values from the given item.
*/
private List<ExternalID> getWorkSelfExternalIds(Context context, Item item) {
private List<ExternalID> getWorkExternalIdList(Context context, Item item, Work work) {
List<ExternalID> selfExternalIds = new ArrayList<>();
List<ExternalID> externalIds = new ArrayList<>();
Map<String, String> externalIdentifierFields = fieldMapping.getExternalIdentifierFields();
if (externalIdentifierFields.containsKey(SIMPLE_HANDLE_PLACEHOLDER)) {
String handleType = externalIdentifierFields.get(SIMPLE_HANDLE_PLACEHOLDER);
selfExternalIds.add(getExternalId(handleType, item.getHandle(), SELF));
ExternalID handle = new ExternalID();
handle.setType(handleType);
handle.setValue(item.getHandle());
handle.setRelationship(SELF);
externalIds.add(handle);
}
// Resolve work type, used to determine identifier relationship type
// For version / funding relationships, we might want to use more complex
// business rules than just "work and id type"
final String workType = (work != null && work.getWorkType() != null) ?
work.getWorkType().value() : WorkType.OTHER.value();
getMetadataValues(context, item, externalIdentifierFields.keySet()).stream()
.map(this::getSelfExternalId)
.forEach(selfExternalIds::add);
.map(metadataValue -> this.getExternalId(metadataValue, workType))
.forEach(externalIds::add);
return selfExternalIds;
}
/**
* Creates an instance of ExternalID taking the value from the given
* metadataValue. The type of the ExternalID is calculated using the
* orcid.mapping.funding.external-ids configuration. The relationship of the
* ExternalID is SELF.
*/
private ExternalID getSelfExternalId(MetadataValue metadataValue) {
Map<String, String> externalIdentifierFields = fieldMapping.getExternalIdentifierFields();
String metadataField = metadataValue.getMetadataField().toString('.');
return getExternalId(externalIdentifierFields.get(metadataField), metadataValue.getValue(), SELF);
return externalIds;
}
/**
* Creates an instance of ExternalID with the given type, value and
* relationship.
*/
private ExternalID getExternalId(String type, String value, Relationship relationship) {
private ExternalID getExternalId(MetadataValue metadataValue, String workType) {
Map<String, String> externalIdentifierFields = fieldMapping.getExternalIdentifierFields();
Map<String, List<String>> externalIdentifierPartOfMap = fieldMapping.getExternalIdentifierPartOfMap();
String metadataField = metadataValue.getMetadataField().toString('.');
String identifierType = externalIdentifierFields.get(metadataField);
// Default relationship type is SELF, configuration can
// override to PART_OF based on identifier and work type
Relationship relationship = SELF;
if (externalIdentifierPartOfMap.containsKey(identifierType)
&& externalIdentifierPartOfMap.get(identifierType).contains(workType)) {
relationship = PART_OF;
}
ExternalID externalID = new ExternalID();
externalID.setType(type);
externalID.setValue(value);
externalID.setType(identifierType);
externalID.setValue(metadataValue.getValue());
externalID.setRelationship(relationship);
return externalID;
}

View File

@@ -113,6 +113,14 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, "dc", "identifier", "scopus", scopus);
}
public ItemBuilder withISSN(String issn) {
return addMetadataValue(item, "dc", "identifier", "issn", issn);
}
public ItemBuilder withISBN(String isbn) {
return addMetadataValue(item, "dc", "identifier", "isbn", isbn);
}
public ItemBuilder withRelationFunding(String funding) {
return addMetadataValue(item, "dc", "relation", "funding", funding);
}

View File

@@ -73,6 +73,9 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData
private Collection projects;
private static final String isbn = "978-0-439-02348-1";
private static final String issn = "1234-1234X";
@Before
public void setup() {
@@ -117,6 +120,7 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData
.withLanguage("en_US")
.withType("Book")
.withIsPartOf("Journal")
.withISBN(isbn)
.withDoiIdentifier("doi-id")
.withScopusIdentifier("scopus-id")
.build();
@@ -149,13 +153,102 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData
assertThat(work.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = work.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(3));
assertThat(externalIds, hasSize(4));
assertThat(externalIds, has(selfExternalId("doi", "doi-id")));
assertThat(externalIds, has(selfExternalId("eid", "scopus-id")));
assertThat(externalIds, has(selfExternalId("handle", publication.getHandle())));
// Book type should have SELF rel for ISBN
assertThat(externalIds, has(selfExternalId("isbn", isbn)));
}
@Test
public void testJournalArticleAndISSN() {
context.turnOffAuthorisationSystem();
Item publication = ItemBuilder.createItem(context, publications)
.withTitle("Test publication")
.withAuthor("Walter White")
.withAuthor("Jesse Pinkman")
.withEditor("Editor")
.withIssueDate("2021-04-30")
.withDescriptionAbstract("Publication description")
.withLanguage("en_US")
.withType("Article")
.withIsPartOf("Journal")
.withISSN(issn)
.withDoiIdentifier("doi-id")
.withScopusIdentifier("scopus-id")
.build();
context.restoreAuthSystemState();
Activity activity = entityFactoryService.createOrcidObject(context, publication);
assertThat(activity, instanceOf(Work.class));
Work work = (Work) activity;
assertThat(work.getJournalTitle(), notNullValue());
assertThat(work.getJournalTitle().getContent(), is("Journal"));
assertThat(work.getLanguageCode(), is("en"));
assertThat(work.getPublicationDate(), matches(date("2021", "04", "30")));
assertThat(work.getShortDescription(), is("Publication description"));
assertThat(work.getPutCode(), nullValue());
assertThat(work.getWorkType(), is(WorkType.JOURNAL_ARTICLE));
assertThat(work.getWorkTitle(), notNullValue());
assertThat(work.getWorkTitle().getTitle(), notNullValue());
assertThat(work.getWorkTitle().getTitle().getContent(), is("Test publication"));
assertThat(work.getWorkContributors(), notNullValue());
assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle())));
List<Contributor> contributors = work.getWorkContributors().getContributor();
assertThat(contributors, hasSize(3));
assertThat(contributors, has(contributor("Walter White", AUTHOR, FIRST)));
assertThat(contributors, has(contributor("Editor", EDITOR, FIRST)));
assertThat(contributors, has(contributor("Jesse Pinkman", AUTHOR, ADDITIONAL)));
assertThat(work.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = work.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(4));
assertThat(externalIds, has(selfExternalId("doi", "doi-id")));
assertThat(externalIds, has(selfExternalId("eid", "scopus-id")));
assertThat(externalIds, has(selfExternalId("handle", publication.getHandle())));
// journal-article should have PART_OF rel for ISSN
assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF)));
}
@Test
public void testJournalWithISSN() {
context.turnOffAuthorisationSystem();
Item publication = ItemBuilder.createItem(context, publications)
.withTitle("Test journal")
.withEditor("Editor")
.withType("Journal")
.withISSN(issn)
.build();
context.restoreAuthSystemState();
Activity activity = entityFactoryService.createOrcidObject(context, publication);
assertThat(activity, instanceOf(Work.class));
Work work = (Work) activity;
assertThat(work.getWorkType(), is(WorkType.JOURNAL_ISSUE));
assertThat(work.getWorkTitle(), notNullValue());
assertThat(work.getWorkTitle().getTitle(), notNullValue());
assertThat(work.getWorkTitle().getTitle().getContent(), is("Test journal"));
assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle())));
assertThat(work.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = work.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(2));
// journal-issue should have SELF rel for ISSN
assertThat(externalIds, has(selfExternalId("issn", issn)));
assertThat(externalIds, has(selfExternalId("handle", publication.getHandle())));
}
@Test
public void testEmptyWorkWithUnknownTypeCreation() {
@@ -163,6 +256,7 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData
Item publication = ItemBuilder.createItem(context, publications)
.withType("TYPE")
.withISSN(issn)
.build();
context.restoreAuthSystemState();
@@ -183,8 +277,9 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData
assertThat(work.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = work.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(1));
assertThat(externalIds, hasSize(2));
assertThat(externalIds, has(selfExternalId("handle", publication.getHandle())));
assertThat(externalIds, has(externalId("issn", issn, Relationship.PART_OF)));
}
@Test

View File

@@ -7,6 +7,7 @@ Dataset = data-set
Learning\ Object = other
Image = other
Image,\ 3-D = other
Journal = journal-issue
Map = other
Musical\ Score = other
Plan\ or\ blueprint = other

View File

@@ -1,4 +1,3 @@
#------------------------------------------------------------------#
#--------------------ORCID GENERIC CONFIGURATIONS------------------#
#------------------------------------------------------------------#
@@ -61,12 +60,18 @@ orcid.mapping.work.contributors = dc.contributor.editor::editor
##orcid.mapping.work.external-ids syntax is <metadatafield>::<type> or $simple-handle::<type>
##The full list of available external identifiers is available here https://pub.orcid.org/v3.0/identifiers
# The identifiers need to have a relationship of SELF, PART_OF, VERSION_OF or FUNDED_BY.
# The default for most identifiers is SELF. The default for identifiers more commonly
# associated with 'parent' publciations (ISSN, ISBN) is PART_OF.
# See the map in `orcid-services.xml`
# VERSION_OF and FUNDED_BY are not currently implemented.
orcid.mapping.work.external-ids = dc.identifier.doi::doi
orcid.mapping.work.external-ids = dc.identifier.scopus::eid
orcid.mapping.work.external-ids = dc.identifier.pmid::pmid
orcid.mapping.work.external-ids = $simple-handle::handle
orcid.mapping.work.external-ids = dc.identifier.isi::wosuid
orcid.mapping.work.external-ids = dc.identifier.issn::issn
orcid.mapping.work.external-ids = dc.identifier.isbn::isbn
### Funding mapping ###
orcid.mapping.funding.title = dc.title
@@ -146,6 +151,9 @@ orcid.bulk-synchronization.max-attempts = 5
#--------------------ORCID EXTERNAL DATA MAPPING-------------------#
#------------------------------------------------------------------#
# Note - the below mapping is for ORCID->DSpace imports, not for
# DSpace->ORCID exports (see orcid.mapping.work.*)
### Work (Publication) external-data.mapping ###
orcid.external-data.mapping.publication.title = dc.title

View File

@@ -55,6 +55,27 @@
<bean id="orcidWorkFactoryFieldMapping" class="org.dspace.orcid.model.OrcidWorkFieldMapping" >
<property name="contributorFields" value="${orcid.mapping.work.contributors}" />
<property name="externalIdentifierFields" value="${orcid.mapping.work.external-ids}" />
<property name="externalIdentifierPartOfMap">
<map>
<entry key="issn">
<list>
<value>journal-article</value>
<value>magazine-article</value>
<value>newspaper-article</value>
<value>data-set</value>
<value>learning-object</value>
<value>other</value>
</list>
</entry>
<entry key="isbn">
<list>
<value>book-chapter</value>
<value>book-review</value>
<value>other</value>
</list>
</entry>
</map>
</property>
<property name="publicationDateField" value="${orcid.mapping.work.publication-date}" />
<property name="titleField" value="${orcid.mapping.work.title}" />
<property name="journalTitleField" value="${orcid.mapping.work.journal-title}" />