diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java
new file mode 100644
index 0000000000..4fdf15a8a3
--- /dev/null
+++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java
@@ -0,0 +1,547 @@
+/**
+ * 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.external.provider.impl;
+
+import static java.util.Collections.emptyList;
+import static java.util.Comparator.comparing;
+import static java.util.Comparator.reverseOrder;
+import static java.util.Optional.ofNullable;
+import static org.apache.commons.collections4.ListUtils.partition;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+import static org.orcid.jaxb.model.common.CitationType.FORMATTED_UNSPECIFIED;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataFieldName;
+import org.dspace.content.dto.MetadataValueDTO;
+import org.dspace.core.Context;
+import org.dspace.external.model.ExternalDataObject;
+import org.dspace.external.provider.AbstractExternalDataProvider;
+import org.dspace.external.provider.ExternalDataProvider;
+import org.dspace.importer.external.datamodel.ImportRecord;
+import org.dspace.importer.external.metadatamapping.MetadatumDTO;
+import org.dspace.importer.external.service.ImportService;
+import org.dspace.orcid.OrcidToken;
+import org.dspace.orcid.client.OrcidClient;
+import org.dspace.orcid.client.OrcidConfiguration;
+import org.dspace.orcid.model.OrcidTokenResponseDTO;
+import org.dspace.orcid.model.OrcidWorkFieldMapping;
+import org.dspace.orcid.service.OrcidSynchronizationService;
+import org.dspace.orcid.service.OrcidTokenService;
+import org.dspace.web.ContextUtil;
+import org.orcid.jaxb.model.common.ContributorRole;
+import org.orcid.jaxb.model.common.WorkType;
+import org.orcid.jaxb.model.v3.release.common.Contributor;
+import org.orcid.jaxb.model.v3.release.common.ContributorAttributes;
+import org.orcid.jaxb.model.v3.release.common.PublicationDate;
+import org.orcid.jaxb.model.v3.release.common.Subtitle;
+import org.orcid.jaxb.model.v3.release.common.Title;
+import org.orcid.jaxb.model.v3.release.record.Citation;
+import org.orcid.jaxb.model.v3.release.record.ExternalIDs;
+import org.orcid.jaxb.model.v3.release.record.SourceAware;
+import org.orcid.jaxb.model.v3.release.record.Work;
+import org.orcid.jaxb.model.v3.release.record.WorkBulk;
+import org.orcid.jaxb.model.v3.release.record.WorkContributors;
+import org.orcid.jaxb.model.v3.release.record.WorkTitle;
+import org.orcid.jaxb.model.v3.release.record.summary.WorkGroup;
+import org.orcid.jaxb.model.v3.release.record.summary.WorkSummary;
+import org.orcid.jaxb.model.v3.release.record.summary.Works;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * Implementation of {@link ExternalDataProvider} that search for all the works
+ * of the profile with the given orcid id that hava a source other than DSpace.
+ * The id of the external data objects returned by the methods of this class is
+ * the concatenation of the orcid id and the put code associated with the
+ * publication, separated by :: (example 0000-0000-0123-4567::123456)
+ *
+ * @author Luca Giamminonni (luca.giamminonni at 4science.it)
+ *
+ */
+public class OrcidPublicationDataProvider extends AbstractExternalDataProvider {
+
+ private final static Logger LOGGER = LoggerFactory.getLogger(OrcidPublicationDataProvider.class);
+
+ /**
+ * Examples of valid ORCID IDs:
+ *
+ * - 0000-0002-1825-0097
+ * - 0000-0001-5109-3700
+ * - 0000-0002-1694-233X
+ *
+ */
+ private final static Pattern ORCID_ID_PATTERN = Pattern.compile("(\\d{4}-){3}\\d{3}(\\d|X)");
+
+ private final static int MAX_PUT_CODES_SIZE = 100;
+
+ @Autowired
+ private OrcidClient orcidClient;
+
+ @Autowired
+ private OrcidConfiguration orcidConfiguration;
+
+ @Autowired
+ private OrcidSynchronizationService orcidSynchronizationService;
+
+ @Autowired
+ private ImportService importService;
+
+ @Autowired
+ private OrcidTokenService orcidTokenService;
+
+ private OrcidWorkFieldMapping fieldMapping;
+
+ private String sourceIdentifier;
+
+ private String readPublicAccessToken;
+
+ @Override
+ public Optional getExternalDataObject(String id) {
+
+ if (isInvalidIdentifier(id)) {
+ throw new IllegalArgumentException("Invalid identifier '" + id + "', expected ::");
+ }
+
+ String[] idSections = id.split("::");
+ String orcid = idSections[0];
+ String putCode = idSections[1];
+
+ validateOrcidId(orcid);
+
+ return getWork(orcid, putCode)
+ .filter(work -> hasDifferentSourceClientId(work))
+ .filter(work -> work.getPutCode() != null)
+ .map(work -> convertToExternalDataObject(orcid, work));
+ }
+
+ @Override
+ public List searchExternalDataObjects(String orcid, int start, int limit) {
+
+ validateOrcidId(orcid);
+
+ return findWorks(orcid, start, limit).stream()
+ .map(work -> convertToExternalDataObject(orcid, work))
+ .collect(Collectors.toList());
+ }
+
+ private boolean isInvalidIdentifier(String id) {
+ return StringUtils.isBlank(id) || id.split("::").length != 2;
+ }
+
+ private void validateOrcidId(String orcid) {
+ if (!ORCID_ID_PATTERN.matcher(orcid).matches()) {
+ throw new IllegalArgumentException("The given ORCID ID is not valid: " + orcid);
+ }
+ }
+
+ /**
+ * Returns all the works related to the given ORCID in the range from start and
+ * limit.
+ *
+ * @param orcid the ORCID ID of the author to search for works
+ * @param start the start index
+ * @param limit the limit index
+ * @return the list of the works
+ */
+ private List findWorks(String orcid, int start, int limit) {
+ List workSummaries = findWorkSummaries(orcid, start, limit);
+ return findWorks(orcid, workSummaries);
+ }
+
+ /**
+ * Returns all the works summaries related to the given ORCID in the range from
+ * start and limit.
+ *
+ * @param orcid the ORCID ID of the author to search for works summaries
+ * @param start the start index
+ * @param limit the limit index
+ * @return the list of the works summaries
+ */
+ private List findWorkSummaries(String orcid, int start, int limit) {
+ return getWorks(orcid).getWorkGroup().stream()
+ .filter(workGroup -> allWorkSummariesHaveDifferentSourceClientId(workGroup))
+ .map(workGroup -> getPreferredWorkSummary(workGroup))
+ .flatMap(Optional::stream)
+ .skip(start)
+ .limit(limit > 0 ? limit : Long.MAX_VALUE)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Returns all the works related to the given ORCID ID and work summaries (a
+ * work has more details than a work summary).
+ *
+ * @param orcid the ORCID id of the author to search for works
+ * @param workSummaries the work summaries used to search the related works
+ * @return the list of the works
+ */
+ private List findWorks(String orcid, List workSummaries) {
+
+ List workPutCodes = getPutCodes(workSummaries);
+
+ if (CollectionUtils.isEmpty(workPutCodes)) {
+ return emptyList();
+ }
+
+ if (workPutCodes.size() == 1) {
+ return getWork(orcid, workPutCodes.get(0)).stream().collect(Collectors.toList());
+ }
+
+ return partition(workPutCodes, MAX_PUT_CODES_SIZE).stream()
+ .map(putCodes -> getWorkBulk(orcid, putCodes))
+ .flatMap(workBulk -> getWorks(workBulk).stream())
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Search a work by ORCID id and putcode, using API or PUBLIC urls based on
+ * whether the ORCID API keys are configured or not.
+ *
+ * @param orcid the ORCID ID
+ * @param putCode the work's identifier on ORCID
+ * @return the work, if any
+ */
+ private Optional getWork(String orcid, String putCode) {
+ if (orcidConfiguration.isApiConfigured()) {
+ String accessToken = getAccessToken(orcid);
+ return orcidClient.getObject(accessToken, orcid, putCode, Work.class);
+ } else {
+ return orcidClient.getObject(orcid, putCode, Work.class);
+ }
+ }
+
+ /**
+ * Returns all the works related to the given ORCID.
+ *
+ * @param orcid the ORCID ID of the author to search for works
+ * @return the list of the works
+ */
+ private Works getWorks(String orcid) {
+ if (orcidConfiguration.isApiConfigured()) {
+ String accessToken = getAccessToken(orcid);
+ return orcidClient.getWorks(accessToken, orcid);
+ } else {
+ return orcidClient.getWorks(orcid);
+ }
+ }
+
+ /**
+ * Returns all the works related to the given ORCID by the given putCodes.
+ *
+ * @param orcid the ORCID ID of the author to search for works
+ * @param putCodes the work's put codes to search
+ * @return the list of the works
+ */
+ private WorkBulk getWorkBulk(String orcid, List putCodes) {
+ if (orcidConfiguration.isApiConfigured()) {
+ String accessToken = getAccessToken(orcid);
+ return orcidClient.getWorkBulk(accessToken, orcid, putCodes);
+ } else {
+ return orcidClient.getWorkBulk(orcid, putCodes);
+ }
+ }
+
+ private String getAccessToken(String orcid) {
+ List- items = orcidSynchronizationService.findProfilesByOrcid(new Context(), orcid);
+ return Optional.ofNullable(items.isEmpty() ? null : items.get(0))
+ .flatMap(item -> getAccessToken(item))
+ .orElseGet(() -> getReadPublicAccessToken());
+ }
+
+ private Optional getAccessToken(Item item) {
+ return ofNullable(orcidTokenService.findByProfileItem(getContext(), item))
+ .map(OrcidToken::getAccessToken);
+ }
+
+ private String getReadPublicAccessToken() {
+ if (readPublicAccessToken != null) {
+ return readPublicAccessToken;
+ }
+
+ OrcidTokenResponseDTO accessTokenResponse = orcidClient.getReadPublicAccessToken();
+ readPublicAccessToken = accessTokenResponse.getAccessToken();
+
+ return readPublicAccessToken;
+ }
+
+ private List getWorks(WorkBulk workBulk) {
+ return workBulk.getBulk().stream()
+ .filter(bulkElement -> (bulkElement instanceof Work))
+ .map(bulkElement -> ((Work) bulkElement))
+ .collect(Collectors.toList());
+
+ }
+
+ private List getPutCodes(List workSummaries) {
+ return workSummaries.stream()
+ .map(WorkSummary::getPutCode)
+ .map(String::valueOf)
+ .collect(Collectors.toList());
+ }
+
+ private Optional getPreferredWorkSummary(WorkGroup workGroup) {
+ return workGroup.getWorkSummary().stream()
+ .filter(work -> work.getPutCode() != null)
+ .filter(work -> NumberUtils.isCreatable(work.getDisplayIndex()))
+ .sorted(comparing(work -> Integer.valueOf(work.getDisplayIndex()), reverseOrder()))
+ .findFirst();
+ }
+
+ private ExternalDataObject convertToExternalDataObject(String orcid, Work work) {
+ ExternalDataObject externalDataObject = new ExternalDataObject(sourceIdentifier);
+ externalDataObject.setId(orcid + "::" + work.getPutCode().toString());
+
+ String title = getWorkTitle(work);
+ externalDataObject.setDisplayValue(title);
+ externalDataObject.setValue(title);
+
+ addMetadataValue(externalDataObject, fieldMapping.getTitleField(), () -> title);
+ addMetadataValue(externalDataObject, fieldMapping.getTypeField(), () -> getWorkType(work));
+ addMetadataValue(externalDataObject, fieldMapping.getPublicationDateField(), () -> getPublicationDate(work));
+ addMetadataValue(externalDataObject, fieldMapping.getJournalTitleField(), () -> getJournalTitle(work));
+ addMetadataValue(externalDataObject, fieldMapping.getSubTitleField(), () -> getSubTitleField(work));
+ addMetadataValue(externalDataObject, fieldMapping.getShortDescriptionField(), () -> getDescription(work));
+ addMetadataValue(externalDataObject, fieldMapping.getLanguageField(), () -> getLanguage(work));
+
+ for (String contributorField : fieldMapping.getContributorFields().keySet()) {
+ ContributorRole role = fieldMapping.getContributorFields().get(contributorField);
+ addMetadataValues(externalDataObject, contributorField, () -> getContributors(work, role));
+ }
+
+ for (String externalIdField : fieldMapping.getExternalIdentifierFields().keySet()) {
+ String type = fieldMapping.getExternalIdentifierFields().get(externalIdField);
+ addMetadataValues(externalDataObject, externalIdField, () -> getExternalIds(work, type));
+ }
+
+ try {
+ addMetadataValuesFromCitation(externalDataObject, work.getWorkCitation());
+ } catch (Exception e) {
+ LOGGER.error("An error occurs reading the following citation: " + work.getWorkCitation().getCitation(), e);
+ }
+
+ return externalDataObject;
+ }
+
+ private boolean allWorkSummariesHaveDifferentSourceClientId(WorkGroup workGroup) {
+ return workGroup.getWorkSummary().stream().allMatch(this::hasDifferentSourceClientId);
+ }
+
+ @SuppressWarnings("deprecation")
+ private boolean hasDifferentSourceClientId(SourceAware sourceAware) {
+ return Optional.ofNullable(sourceAware.getSource())
+ .map(source -> source.getSourceClientId())
+ .map(sourceClientId -> sourceClientId.getPath())
+ .map(clientId -> !StringUtils.equals(orcidConfiguration.getClientId(), clientId))
+ .orElse(true);
+ }
+
+ private void addMetadataValues(ExternalDataObject externalData, String metadata, Supplier
> values) {
+
+ if (StringUtils.isBlank(metadata)) {
+ return;
+ }
+
+ MetadataFieldName field = new MetadataFieldName(metadata);
+ for (String value : values.get()) {
+ externalData.addMetadata(new MetadataValueDTO(field.schema, field.element, field.qualifier, null, value));
+ }
+ }
+
+ private void addMetadataValue(ExternalDataObject externalData, String metadata, Supplier valueSupplier) {
+ addMetadataValues(externalData, metadata, () -> {
+ String value = valueSupplier.get();
+ return isNotBlank(value) ? List.of(value) : emptyList();
+ });
+ }
+
+ private String getWorkTitle(Work work) {
+ WorkTitle workTitle = work.getWorkTitle();
+ if (workTitle == null) {
+ return null;
+ }
+ Title title = workTitle.getTitle();
+ return title != null ? title.getContent() : null;
+ }
+
+ private String getWorkType(Work work) {
+ WorkType workType = work.getWorkType();
+ return workType != null ? fieldMapping.convertType(workType.value()) : null;
+ }
+
+ private String getPublicationDate(Work work) {
+ PublicationDate publicationDate = work.getPublicationDate();
+ if (publicationDate == null) {
+ return null;
+ }
+
+ StringBuilder builder = new StringBuilder(publicationDate.getYear().getValue());
+ if (publicationDate.getMonth() != null) {
+ builder.append("-");
+ builder.append(publicationDate.getMonth().getValue());
+ }
+
+ if (publicationDate.getDay() != null) {
+ builder.append("-");
+ builder.append(publicationDate.getDay().getValue());
+ }
+
+ return builder.toString();
+ }
+
+ private String getJournalTitle(Work work) {
+ Title journalTitle = work.getJournalTitle();
+ return journalTitle != null ? journalTitle.getContent() : null;
+ }
+
+ private String getSubTitleField(Work work) {
+ WorkTitle workTitle = work.getWorkTitle();
+ if (workTitle == null) {
+ return null;
+ }
+ Subtitle subTitle = workTitle.getSubtitle();
+ return subTitle != null ? subTitle.getContent() : null;
+ }
+
+ private String getDescription(Work work) {
+ return work.getShortDescription();
+ }
+
+ private String getLanguage(Work work) {
+ return work.getLanguageCode() != null ? fieldMapping.convertLanguage(work.getLanguageCode()) : null;
+ }
+
+ private List getContributors(Work work, ContributorRole role) {
+ WorkContributors workContributors = work.getWorkContributors();
+ if (workContributors == null) {
+ return emptyList();
+ }
+
+ return workContributors.getContributor().stream()
+ .filter(contributor -> hasRole(contributor, role))
+ .map(contributor -> getContributorName(contributor))
+ .flatMap(Optional::stream)
+ .collect(Collectors.toList());
+ }
+
+ private void addMetadataValuesFromCitation(ExternalDataObject externalDataObject, Citation citation)
+ throws Exception {
+
+ if (citation == null || citation.getWorkCitationType() == FORMATTED_UNSPECIFIED) {
+ return;
+ }
+
+ getImportRecord(citation).ifPresent(importRecord -> enrichExternalDataObject(externalDataObject, importRecord));
+
+ }
+
+ private Optional getImportRecord(Citation citation) throws Exception {
+ File citationFile = File.createTempFile("temp", "." + citation.getWorkCitationType().value());
+ try (FileOutputStream outputStream = new FileOutputStream(citationFile)) {
+ IOUtils.write(citation.getCitation(), new FileOutputStream(citationFile), Charset.defaultCharset());
+ return Optional.ofNullable(importService.getRecord(citationFile, citationFile.getName()));
+ } finally {
+ citationFile.delete();
+ }
+ }
+
+ private void enrichExternalDataObject(ExternalDataObject externalDataObject, ImportRecord importRecord) {
+ importRecord.getValueList().stream()
+ .filter(metadata -> doesNotContains(externalDataObject, metadata))
+ .forEach(metadata -> addMetadata(externalDataObject, metadata));
+ }
+
+ private void addMetadata(ExternalDataObject externalDataObject, MetadatumDTO metadata) {
+ externalDataObject.addMetadata(new MetadataValueDTO(metadata.getSchema(), metadata.getElement(),
+ metadata.getQualifier(), null, metadata.getValue()));
+ }
+
+ private boolean doesNotContains(ExternalDataObject externalDataObject, MetadatumDTO metadata) {
+ return externalDataObject.getMetadata().stream()
+ .filter(metadataValue -> StringUtils.equals(metadataValue.getSchema(), metadata.getSchema()))
+ .filter(metadataValue -> StringUtils.equals(metadataValue.getElement(), metadata.getElement()))
+ .filter(metadataValue -> StringUtils.equals(metadataValue.getQualifier(), metadata.getQualifier()))
+ .findAny().isEmpty();
+ }
+
+ private boolean hasRole(Contributor contributor, ContributorRole role) {
+ ContributorAttributes attributes = contributor.getContributorAttributes();
+ return attributes != null ? role.equals(attributes.getContributorRole()) : false;
+ }
+
+ private Optional getContributorName(Contributor contributor) {
+ return Optional.ofNullable(contributor.getCreditName())
+ .map(creditName -> creditName.getContent());
+ }
+
+ private List getExternalIds(Work work, String type) {
+ ExternalIDs externalIdentifiers = work.getExternalIdentifiers();
+ if (externalIdentifiers == null) {
+ return emptyList();
+ }
+
+ return externalIdentifiers.getExternalIdentifier().stream()
+ .filter(externalId -> type.equals(externalId.getType()))
+ .map(externalId -> externalId.getValue())
+ .collect(Collectors.toList());
+ }
+
+ private Context getContext() {
+ Context context = ContextUtil.obtainCurrentRequestContext();
+ return context != null ? context : new Context();
+ }
+
+ @Override
+ public boolean supports(String source) {
+ return StringUtils.equals(sourceIdentifier, source);
+ }
+
+ @Override
+ public int getNumberOfResults(String orcid) {
+ return findWorkSummaries(orcid, 0, -1).size();
+ }
+
+ public void setSourceIdentifier(String sourceIdentifier) {
+ this.sourceIdentifier = sourceIdentifier;
+ }
+
+ @Override
+ public String getSourceIdentifier() {
+ return sourceIdentifier;
+ }
+
+ public void setFieldMapping(OrcidWorkFieldMapping fieldMapping) {
+ this.fieldMapping = fieldMapping;
+ }
+
+ public void setReadPublicAccessToken(String readPublicAccessToken) {
+ this.readPublicAccessToken = readPublicAccessToken;
+ }
+
+ public OrcidClient getOrcidClient() {
+ return orcidClient;
+ }
+
+ public void setOrcidClient(OrcidClient orcidClient) {
+ this.orcidClient = orcidClient;
+ }
+
+}
diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java
index e8b0f74186..99d1920aa5 100644
--- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java
+++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClient.java
@@ -7,9 +7,14 @@
*/
package org.dspace.orcid.client;
+import java.util.List;
+import java.util.Optional;
+
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.orcid.jaxb.model.v3.release.record.Person;
+import org.orcid.jaxb.model.v3.release.record.WorkBulk;
+import org.orcid.jaxb.model.v3.release.record.summary.Works;
/**
* Interface for classes that allow to contact ORCID.
@@ -19,6 +24,15 @@ import org.orcid.jaxb.model.v3.release.record.Person;
*/
public interface OrcidClient {
+ /**
+ * Retrieves an /read-public access token using a client-credentials OAuth flow,
+ * or 2-step OAuth.
+ *
+ * @return the ORCID token
+ * @throws OrcidClientException if some error occurs during the exchange
+ */
+ OrcidTokenResponseDTO getReadPublicAccessToken();
+
/**
* Exchange the authorization code for an ORCID iD and 3-legged access token.
* The authorization code expires upon use.
@@ -39,6 +53,75 @@ public interface OrcidClient {
*/
Person getPerson(String accessToken, String orcid);
+ /**
+ * Retrieves all the works related to the given orcid.
+ *
+ * @param accessToken the access token
+ * @param orcid the orcid id related to the works
+ * @return the Works
+ * @throws OrcidClientException if some error occurs during the search
+ */
+ Works getWorks(String accessToken, String orcid);
+
+ /**
+ * Retrieves all the works related to the given orcid.
+ *
+ * @param orcid the orcid id related to the works
+ * @return the Works
+ * @throws OrcidClientException if some error occurs during the search
+ */
+ Works getWorks(String orcid);
+
+ /**
+ * Retrieves all the works with the given putCodes related to the given orcid
+ *
+ * @param accessToken the access token
+ * @param orcid the orcid id
+ * @param putCodes the putCodes of the works to retrieve
+ * @return the Works
+ * @throws OrcidClientException if some error occurs during the search
+ */
+ WorkBulk getWorkBulk(String accessToken, String orcid, List putCodes);
+
+ /**
+ * Retrieves all the works with the given putCodes related to the given orcid
+ *
+ * @param orcid the orcid id
+ * @param putCodes the putCodes of the works to retrieve
+ * @return the Works
+ * @throws OrcidClientException if some error occurs during the search
+ */
+ WorkBulk getWorkBulk(String orcid, List putCodes);
+
+ /**
+ * Retrieves an object from ORCID with the given putCode related to the given
+ * orcid.
+ *
+ * @param accessToken the access token
+ * @param orcid the orcid id
+ * @param putCode the object's put code
+ * @param clazz the object's class
+ * @return the Object, if any
+ * @throws OrcidClientException if some error occurs during the search
+ * @throws IllegalArgumentException if the given object class is not an valid
+ * ORCID object
+ */
+ Optional getObject(String accessToken, String orcid, String putCode, Class clazz);
+
+ /**
+ * Retrieves an object from ORCID with the given putCode related to the given
+ * orcid using the public API.
+ *
+ * @param orcid the orcid id
+ * @param putCode the object's put code
+ * @param clazz the object's class
+ * @return the Object, if any
+ * @throws OrcidClientException if some error occurs during the search
+ * @throws IllegalArgumentException if the given object class is not an valid
+ * ORCID object
+ */
+ Optional getObject(String orcid, String putCode, Class clazz);
+
/**
* Push the given object to ORCID.
*
diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java
index 45387708a9..3e7ca7b210 100644
--- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java
+++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidClientImpl.java
@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Optional;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
@@ -54,6 +55,8 @@ import org.orcid.jaxb.model.v3.release.record.Person;
import org.orcid.jaxb.model.v3.release.record.PersonExternalIdentifier;
import org.orcid.jaxb.model.v3.release.record.ResearcherUrl;
import org.orcid.jaxb.model.v3.release.record.Work;
+import org.orcid.jaxb.model.v3.release.record.WorkBulk;
+import org.orcid.jaxb.model.v3.release.record.summary.Works;
/**
* Implementation of {@link OrcidClient}.
@@ -114,6 +117,50 @@ public class OrcidClientImpl implements OrcidClient {
return executeAndUnmarshall(httpUriRequest, false, Person.class);
}
+ @Override
+ public Works getWorks(String accessToken, String orcid) {
+ HttpUriRequest httpUriRequest = buildGetUriRequest(accessToken, "/" + orcid + "/works");
+ Works works = executeAndUnmarshall(httpUriRequest, true, Works.class);
+ return works != null ? works : new Works();
+ }
+
+ @Override
+ public Works getWorks(String orcid) {
+ HttpUriRequest httpUriRequest = buildGetUriRequestToPublicEndpoint("/" + orcid + "/works");
+ Works works = executeAndUnmarshall(httpUriRequest, true, Works.class);
+ return works != null ? works : new Works();
+ }
+
+ @Override
+ public WorkBulk getWorkBulk(String accessToken, String orcid, List putCodes) {
+ String putCode = String.join(",", putCodes);
+ HttpUriRequest httpUriRequest = buildGetUriRequest(accessToken, "/" + orcid + "/works/" + putCode);
+ WorkBulk workBulk = executeAndUnmarshall(httpUriRequest, true, WorkBulk.class);
+ return workBulk != null ? workBulk : new WorkBulk();
+ }
+
+ @Override
+ public WorkBulk getWorkBulk(String orcid, List putCodes) {
+ String putCode = String.join(",", putCodes);
+ HttpUriRequest httpUriRequest = buildGetUriRequestToPublicEndpoint("/" + orcid + "/works/" + putCode);
+ WorkBulk workBulk = executeAndUnmarshall(httpUriRequest, true, WorkBulk.class);
+ return workBulk != null ? workBulk : new WorkBulk();
+ }
+
+ @Override
+ public Optional getObject(String accessToken, String orcid, String putCode, Class clazz) {
+ String path = getOrcidPathFromOrcidObjectType(clazz);
+ HttpUriRequest httpUriRequest = buildGetUriRequest(accessToken, "/" + orcid + path + "/" + putCode);
+ return Optional.ofNullable(executeAndUnmarshall(httpUriRequest, true, clazz));
+ }
+
+ @Override
+ public Optional getObject(String orcid, String putCode, Class clazz) {
+ String path = getOrcidPathFromOrcidObjectType(clazz);
+ HttpUriRequest httpUriRequest = buildGetUriRequestToPublicEndpoint("/" + orcid + path + "/" + putCode);
+ return Optional.ofNullable(executeAndUnmarshall(httpUriRequest, true, clazz));
+ }
+
@Override
public OrcidResponse push(String accessToken, String orcid, Object object) {
String path = getOrcidPathFromOrcidObjectType(object.getClass());
@@ -131,6 +178,27 @@ public class OrcidClientImpl implements OrcidClient {
return execute(buildDeleteUriRequest(accessToken, "/" + orcid + path + "/" + putCode), true);
}
+ @Override
+ public OrcidTokenResponseDTO getReadPublicAccessToken() {
+ return getClientCredentialsAccessToken("/read-public");
+ }
+
+ private OrcidTokenResponseDTO getClientCredentialsAccessToken(String scope) {
+ List params = new ArrayList();
+ params.add(new BasicNameValuePair("scope", scope));
+ params.add(new BasicNameValuePair("grant_type", "client_credentials"));
+ params.add(new BasicNameValuePair("client_id", orcidConfiguration.getClientId()));
+ params.add(new BasicNameValuePair("client_secret", orcidConfiguration.getClientSecret()));
+
+ HttpUriRequest httpUriRequest = RequestBuilder.post(orcidConfiguration.getTokenEndpointUrl())
+ .addHeader("Content-Type", "application/x-www-form-urlencoded")
+ .addHeader("Accept", "application/json")
+ .setEntity(new UrlEncodedFormEntity(params, Charset.defaultCharset()))
+ .build();
+
+ return executeAndParseJson(httpUriRequest, OrcidTokenResponseDTO.class);
+ }
+
private HttpUriRequest buildGetUriRequest(String accessToken, String relativePath) {
return get(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/x-www-form-urlencoded")
@@ -138,6 +206,12 @@ public class OrcidClientImpl implements OrcidClient {
.build();
}
+ private HttpUriRequest buildGetUriRequestToPublicEndpoint(String relativePath) {
+ return get(orcidConfiguration.getPublicUrl() + relativePath.trim())
+ .addHeader("Content-Type", "application/x-www-form-urlencoded")
+ .build();
+ }
+
private HttpUriRequest buildPostUriRequest(String accessToken, String relativePath, Object object) {
return post(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/vnd.orcid+xml")
diff --git a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java
index f45680e148..550b0215c4 100644
--- a/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java
+++ b/dspace-api/src/main/java/org/dspace/orcid/client/OrcidConfiguration.java
@@ -107,4 +107,8 @@ public final class OrcidConfiguration {
this.publicUrl = publicUrl;
}
+ public boolean isApiConfigured() {
+ return !StringUtils.isAnyBlank(clientId, clientSecret);
+ }
+
}
diff --git a/dspace-api/src/main/java/org/dspace/orcid/service/OrcidSynchronizationService.java b/dspace-api/src/main/java/org/dspace/orcid/service/OrcidSynchronizationService.java
index 66c0bf11b2..575ce6811b 100644
--- a/dspace-api/src/main/java/org/dspace/orcid/service/OrcidSynchronizationService.java
+++ b/dspace-api/src/main/java/org/dspace/orcid/service/OrcidSynchronizationService.java
@@ -155,4 +155,13 @@ public interface OrcidSynchronizationService {
* @return the disconnection mode
*/
OrcidProfileDisconnectionMode getDisconnectionMode();
+
+ /**
+ * Returns all the profiles with the given orcid id.
+ *
+ * @param context the relevant DSpace Context.
+ * @param orcid the orcid id to search for
+ * @return the found profile items
+ */
+ List- findProfilesByOrcid(Context context, String orcid);
}
diff --git a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java
index 7ce423d742..97d832d3de 100644
--- a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java
+++ b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidSynchronizationServiceImpl.java
@@ -30,6 +30,10 @@ import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
+import org.dspace.discovery.DiscoverQuery;
+import org.dspace.discovery.SearchService;
+import org.dspace.discovery.SearchServiceException;
+import org.dspace.discovery.indexobject.IndexableItem;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService;
import org.dspace.orcid.OrcidToken;
@@ -62,6 +66,9 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
@Autowired
private EPersonService ePersonService;
+ @Autowired
+ private SearchService searchService;
+
@Autowired
private OrcidTokenService orcidTokenService;
@@ -306,4 +313,19 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
throw new RuntimeException(e);
}
}
+
+ @Override
+ public List
- findProfilesByOrcid(Context context, String orcid) {
+ DiscoverQuery discoverQuery = new DiscoverQuery();
+ discoverQuery.setDSpaceObjectFilter(IndexableItem.TYPE);
+ discoverQuery.addFilterQueries("search.entitytype:" + researcherProfileService.getProfileType());
+ discoverQuery.addFilterQueries("person.identifier.orcid:" + orcid);
+ try {
+ return searchService.search(context, discoverQuery).getIndexableObjects().stream()
+ .map(object -> ((IndexableItem) object).getIndexedObject())
+ .collect(Collectors.toList());
+ } catch (SearchServiceException ex) {
+ throw new RuntimeException(ex);
+ }
+ }
}
diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml
index 99d4513868..bd6da8ad8a 100644
--- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml
+++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-services.xml
@@ -57,6 +57,16 @@
+
+
+
+
+
+
+ Publication
+
+
+
diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidPublicationDataProviderIT.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidPublicationDataProviderIT.java
new file mode 100644
index 0000000000..dae14115b8
--- /dev/null
+++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OrcidPublicationDataProviderIT.java
@@ -0,0 +1,434 @@
+/**
+ * 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.external.provider.impl;
+
+import static java.util.Optional.of;
+import static org.dspace.app.matcher.LambdaMatcher.has;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.Predicate;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Unmarshaller;
+
+import org.apache.commons.codec.binary.StringUtils;
+import org.dspace.AbstractIntegrationTestWithDatabase;
+import org.dspace.builder.CollectionBuilder;
+import org.dspace.builder.CommunityBuilder;
+import org.dspace.builder.ItemBuilder;
+import org.dspace.builder.OrcidTokenBuilder;
+import org.dspace.content.Collection;
+import org.dspace.content.Item;
+import org.dspace.content.MetadataFieldName;
+import org.dspace.content.dto.MetadataValueDTO;
+import org.dspace.external.model.ExternalDataObject;
+import org.dspace.orcid.client.OrcidClient;
+import org.dspace.orcid.client.OrcidConfiguration;
+import org.dspace.orcid.model.OrcidTokenResponseDTO;
+import org.dspace.utils.DSpace;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.orcid.jaxb.model.v3.release.record.Work;
+import org.orcid.jaxb.model.v3.release.record.WorkBulk;
+import org.orcid.jaxb.model.v3.release.record.summary.Works;
+
+/**
+ * Integration tests for {@link OrcidPublicationDataProvider}.
+ *
+ * @author Luca Giamminonni (luca.giamminonni at 4science.it)
+ *
+ */
+public class OrcidPublicationDataProviderIT extends AbstractIntegrationTestWithDatabase {
+
+ private static final String BASE_XML_DIR_PATH = "org/dspace/app/orcid-works/";
+
+ private static final String ACCESS_TOKEN = "32c83ccb-c6d5-4981-b6ea-6a34a36de8ab";
+
+ private static final String ORCID = "0000-1111-2222-3333";
+
+ private OrcidPublicationDataProvider dataProvider;
+
+ private OrcidConfiguration orcidConfiguration;
+
+ private OrcidClient orcidClient;
+
+ private OrcidClient orcidClientMock;
+
+ private String originalClientId;
+
+ private Collection persons;
+
+ @Before
+ public void setup() throws Exception {
+ context.turnOffAuthorisationSystem();
+
+ parentCommunity = CommunityBuilder.createCommunity(context)
+ .withName("Parent Community")
+ .build();
+
+ persons = CollectionBuilder.createCollection(context, parentCommunity)
+ .withEntityType("Person")
+ .withName("Profiles")
+ .build();
+
+ context.restoreAuthSystemState();
+
+ dataProvider = new DSpace().getServiceManager()
+ .getServiceByName("orcidPublicationDataProvider", OrcidPublicationDataProvider.class);
+
+ orcidConfiguration = new DSpace().getServiceManager()
+ .getServiceByName("org.dspace.orcid.client.OrcidConfiguration", OrcidConfiguration.class);
+
+ orcidClientMock = mock(OrcidClient.class);
+ orcidClient = dataProvider.getOrcidClient();
+
+ dataProvider.setReadPublicAccessToken(null);
+ dataProvider.setOrcidClient(orcidClientMock);
+
+ originalClientId = orcidConfiguration.getClientId();
+ orcidConfiguration.setClientId("DSPACE-CLIENT-ID");
+ orcidConfiguration.setClientSecret("DSPACE-CLIENT-SECRET");
+
+ when(orcidClientMock.getReadPublicAccessToken()).thenReturn(buildTokenResponse(ACCESS_TOKEN));
+
+ when(orcidClientMock.getWorks(any(), eq(ORCID))).thenReturn(unmarshall("works.xml", Works.class));
+ when(orcidClientMock.getWorks(eq(ORCID))).thenReturn(unmarshall("works.xml", Works.class));
+
+ when(orcidClientMock.getObject(any(), eq(ORCID), any(), eq(Work.class)))
+ .then((invocation) -> of(unmarshall("work-" + invocation.getArgument(2) + ".xml", Work.class)));
+ when(orcidClientMock.getObject(eq(ORCID), any(), eq(Work.class)))
+ .then((invocation) -> of(unmarshall("work-" + invocation.getArgument(1) + ".xml", Work.class)));
+
+ when(orcidClientMock.getWorkBulk(any(), eq(ORCID), any()))
+ .then((invocation) -> unmarshallWorkBulk(invocation.getArgument(2)));
+ when(orcidClientMock.getWorkBulk(eq(ORCID), any()))
+ .then((invocation) -> unmarshallWorkBulk(invocation.getArgument(1)));
+
+ }
+
+ @After
+ public void after() {
+ dataProvider.setOrcidClient(orcidClient);
+ orcidConfiguration.setClientId(originalClientId);
+ }
+
+ @Test
+ public void testSearchWithoutPagination() throws Exception {
+
+ List externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+
+ ExternalDataObject firstObject = externalObjects.get(0);
+ assertThat(firstObject.getDisplayValue(), is("The elements of style and the survey of ophthalmology."));
+ assertThat(firstObject.getValue(), is("The elements of style and the survey of ophthalmology."));
+ assertThat(firstObject.getId(), is(ORCID + "::277904"));
+ assertThat(firstObject.getSource(), is("orcidWorks"));
+
+ List metadata = firstObject.getMetadata();
+ assertThat(metadata, hasSize(7));
+ assertThat(metadata, has(metadata("dc.date.issued", "2011")));
+ assertThat(metadata, has(metadata("dc.source", "Test Journal")));
+ assertThat(metadata, has(metadata("dc.language.iso", "it")));
+ assertThat(metadata, has(metadata("dc.type", "Other")));
+ assertThat(metadata, has(metadata("dc.identifier.doi", "10.11234.12")));
+ assertThat(metadata, has(metadata("dc.contributor.author", "Walter White")));
+ assertThat(metadata, has(metadata("dc.title", "The elements of style and the survey of ophthalmology.")));
+
+ ExternalDataObject secondObject = externalObjects.get(1);
+ assertThat(secondObject.getDisplayValue(), is("Another cautionary tale."));
+ assertThat(secondObject.getValue(), is("Another cautionary tale."));
+ assertThat(secondObject.getId(), is(ORCID + "::277902"));
+ assertThat(secondObject.getSource(), is("orcidWorks"));
+
+ metadata = secondObject.getMetadata();
+ assertThat(metadata, hasSize(8));
+ assertThat(metadata, has(metadata("dc.date.issued", "2011-05-01")));
+ assertThat(metadata, has(metadata("dc.description.abstract", "Short description")));
+ assertThat(metadata, has(metadata("dc.relation.ispartof", "Journal title")));
+ assertThat(metadata, has(metadata("dc.contributor.author", "Walter White")));
+ assertThat(metadata, has(metadata("dc.contributor.author", "John White")));
+ assertThat(metadata, has(metadata("dc.contributor.editor", "Jesse Pinkman")));
+ assertThat(metadata, has(metadata("dc.title", "Another cautionary tale.")));
+ assertThat(metadata, has(metadata("dc.type", "Article")));
+
+ ExternalDataObject thirdObject = externalObjects.get(2);
+ assertThat(thirdObject.getDisplayValue(), is("Branch artery occlusion in a young woman."));
+ assertThat(thirdObject.getValue(), is("Branch artery occlusion in a young woman."));
+ assertThat(thirdObject.getId(), is(ORCID + "::277871"));
+ assertThat(thirdObject.getSource(), is("orcidWorks"));
+
+ metadata = thirdObject.getMetadata();
+ assertThat(metadata, hasSize(3));
+ assertThat(metadata, has(metadata("dc.date.issued", "1985-07-01")));
+ assertThat(metadata, has(metadata("dc.title", "Branch artery occlusion in a young woman.")));
+ assertThat(metadata, has(metadata("dc.type", "Article")));
+
+ verify(orcidClientMock).getReadPublicAccessToken();
+ verify(orcidClientMock).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock).getWorkBulk(ACCESS_TOKEN, ORCID, List.of("277904", "277902", "277871"));
+ verifyNoMoreInteractions(orcidClientMock);
+
+ }
+
+ @Test
+ public void testSearchWithInvalidOrcidId() {
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> dataProvider.searchExternalDataObjects("0000-1111-2222", 0, -1));
+
+ assertThat(exception.getMessage(), is("The given ORCID ID is not valid: 0000-1111-2222"));
+
+ }
+
+ @Test
+ public void testSearchWithStoredAccessToken() throws Exception {
+
+ context.turnOffAuthorisationSystem();
+
+ String accessToken = "95cb5ed9-c208-4bbc-bc99-aa0bd76e4452";
+
+ Item profile = ItemBuilder.createItem(context, persons)
+ .withTitle("Profile")
+ .withOrcidIdentifier(ORCID)
+ .withDspaceObjectOwner(eperson.getEmail(), eperson.getID().toString())
+ .build();
+
+ OrcidTokenBuilder.create(context, eperson, accessToken)
+ .withProfileItem(profile)
+ .build();
+
+ context.restoreAuthSystemState();
+
+ List externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+
+ verify(orcidClientMock).getWorks(accessToken, ORCID);
+ verify(orcidClientMock).getWorkBulk(accessToken, ORCID, List.of("277904", "277902", "277871"));
+ verifyNoMoreInteractions(orcidClientMock);
+ }
+
+ @Test
+ public void testSearchWithProfileWithoutAccessToken() throws Exception {
+
+ context.turnOffAuthorisationSystem();
+
+ ItemBuilder.createItem(context, persons)
+ .withTitle("Profile")
+ .withOrcidIdentifier(ORCID)
+ .build();
+
+ context.restoreAuthSystemState();
+
+ List externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+ verify(orcidClientMock).getReadPublicAccessToken();
+ verify(orcidClientMock).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock).getWorkBulk(ACCESS_TOKEN, ORCID, List.of("277904", "277902", "277871"));
+ verifyNoMoreInteractions(orcidClientMock);
+ }
+
+ @Test
+ public void testSearchWithoutResults() throws Exception {
+
+ String unknownOrcid = "1111-2222-3333-4444";
+ when(orcidClientMock.getWorks(ACCESS_TOKEN, unknownOrcid)).thenReturn(new Works());
+
+ List externalObjects = dataProvider.searchExternalDataObjects(unknownOrcid, 0, -1);
+ assertThat(externalObjects, empty());
+
+ verify(orcidClientMock).getReadPublicAccessToken();
+ verify(orcidClientMock).getWorks(ACCESS_TOKEN, unknownOrcid);
+ verifyNoMoreInteractions(orcidClientMock);
+ }
+
+ @Test
+ public void testClientCredentialsTokenCache() throws Exception {
+
+ List externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+
+ verify(orcidClientMock).getReadPublicAccessToken();
+
+ externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+
+ verify(orcidClientMock, times(1)).getReadPublicAccessToken();
+
+ dataProvider.setReadPublicAccessToken(null);
+
+ externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+
+ verify(orcidClientMock, times(2)).getReadPublicAccessToken();
+
+ }
+
+ @Test
+ public void testSearchPagination() throws Exception {
+
+ List externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277904"))));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277902"))));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277871"))));
+
+ verify(orcidClientMock).getReadPublicAccessToken();
+ verify(orcidClientMock).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock).getWorkBulk(ACCESS_TOKEN, ORCID, List.of("277904", "277902", "277871"));
+
+ externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, 5);
+ assertThat(externalObjects, hasSize(3));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277904"))));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277902"))));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277871"))));
+
+ verify(orcidClientMock, times(2)).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock, times(2)).getWorkBulk(ACCESS_TOKEN, ORCID, List.of("277904", "277902", "277871"));
+
+ externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, 2);
+ assertThat(externalObjects, hasSize(2));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277904"))));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277902"))));
+
+ verify(orcidClientMock, times(3)).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock).getWorkBulk(ACCESS_TOKEN, ORCID, List.of("277904", "277902"));
+
+ externalObjects = dataProvider.searchExternalDataObjects(ORCID, 1, 1);
+ assertThat(externalObjects, hasSize(1));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277902"))));
+
+ verify(orcidClientMock, times(4)).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock).getObject(ACCESS_TOKEN, ORCID, "277902", Work.class);
+
+ externalObjects = dataProvider.searchExternalDataObjects(ORCID, 2, 1);
+ assertThat(externalObjects, hasSize(1));
+ assertThat(externalObjects, has((externalObject -> externalObject.getId().equals(ORCID + "::277871"))));
+
+ verify(orcidClientMock, times(5)).getWorks(ACCESS_TOKEN, ORCID);
+ verify(orcidClientMock).getObject(ACCESS_TOKEN, ORCID, "277871", Work.class);
+
+ verifyNoMoreInteractions(orcidClientMock);
+
+ }
+
+ @Test
+ public void testGetExternalDataObject() {
+ Optional optional = dataProvider.getExternalDataObject(ORCID + "::277902");
+ assertThat(optional.isPresent(), is(true));
+
+ ExternalDataObject externalDataObject = optional.get();
+ assertThat(externalDataObject.getDisplayValue(), is("Another cautionary tale."));
+ assertThat(externalDataObject.getValue(), is("Another cautionary tale."));
+ assertThat(externalDataObject.getId(), is(ORCID + "::277902"));
+ assertThat(externalDataObject.getSource(), is("orcidWorks"));
+
+ List metadata = externalDataObject.getMetadata();
+ assertThat(metadata, hasSize(8));
+ assertThat(metadata, has(metadata("dc.date.issued", "2011-05-01")));
+ assertThat(metadata, has(metadata("dc.description.abstract", "Short description")));
+ assertThat(metadata, has(metadata("dc.relation.ispartof", "Journal title")));
+ assertThat(metadata, has(metadata("dc.contributor.author", "Walter White")));
+ assertThat(metadata, has(metadata("dc.contributor.author", "John White")));
+ assertThat(metadata, has(metadata("dc.contributor.editor", "Jesse Pinkman")));
+ assertThat(metadata, has(metadata("dc.title", "Another cautionary tale.")));
+ assertThat(metadata, has(metadata("dc.type", "Article")));
+
+ verify(orcidClientMock).getReadPublicAccessToken();
+ verify(orcidClientMock).getObject(ACCESS_TOKEN, ORCID, "277902", Work.class);
+ verifyNoMoreInteractions(orcidClientMock);
+ }
+
+ @Test
+ public void testGetExternalDataObjectWithInvalidOrcidId() {
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> dataProvider.getExternalDataObject("invalid::277902"));
+
+ assertThat(exception.getMessage(), is("The given ORCID ID is not valid: invalid" ));
+ }
+
+ @Test
+ public void testGetExternalDataObjectWithInvalidId() {
+
+ IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
+ () -> dataProvider.getExternalDataObject("id"));
+
+ assertThat(exception.getMessage(), is("Invalid identifier 'id', expected ::"));
+ }
+
+ @Test
+ public void testSearchWithoutApiKeysConfigured() throws Exception {
+
+ context.turnOffAuthorisationSystem();
+
+ orcidConfiguration.setClientSecret(null);
+
+ ItemBuilder.createItem(context, persons)
+ .withTitle("Profile")
+ .withOrcidIdentifier(ORCID)
+ .build();
+
+ context.restoreAuthSystemState();
+
+ List externalObjects = dataProvider.searchExternalDataObjects(ORCID, 0, -1);
+ assertThat(externalObjects, hasSize(3));
+
+ verify(orcidClientMock).getWorks(ORCID);
+ verify(orcidClientMock).getWorkBulk(ORCID, List.of("277904", "277902", "277871"));
+ verifyNoMoreInteractions(orcidClientMock);
+ }
+
+ private Predicate metadata(String metadataField, String value) {
+ MetadataFieldName metadataFieldName = new MetadataFieldName(metadataField);
+ return metadata(metadataFieldName.schema, metadataFieldName.element, metadataFieldName.qualifier, value);
+ }
+
+ private Predicate metadata(String schema, String element, String qualifier, String value) {
+ return dto -> StringUtils.equals(schema, dto.getSchema())
+ && StringUtils.equals(element, dto.getElement())
+ && StringUtils.equals(qualifier, dto.getQualifier())
+ && StringUtils.equals(value, dto.getValue());
+ }
+
+ private OrcidTokenResponseDTO buildTokenResponse(String accessToken) {
+ OrcidTokenResponseDTO response = new OrcidTokenResponseDTO();
+ response.setAccessToken(accessToken);
+ return response;
+ }
+
+ private WorkBulk unmarshallWorkBulk(List putCodes) throws Exception {
+ return unmarshall("workBulk-" + String.join("-", putCodes) + ".xml", WorkBulk.class);
+ }
+
+ @SuppressWarnings("unchecked")
+ private T unmarshall(String fileName, Class clazz) throws Exception {
+ JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
+ Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
+ URL resource = getClass().getClassLoader().getResource(BASE_XML_DIR_PATH + fileName);
+ if (resource == null) {
+ throw new IllegalStateException("No resource found named " + BASE_XML_DIR_PATH + fileName);
+ }
+ return (T) unmarshaller.unmarshal(new File(resource.getFile()));
+ }
+
+}
diff --git a/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277871.xml b/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277871.xml
new file mode 100644
index 0000000000..f5fd30fa13
--- /dev/null
+++ b/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277871.xml
@@ -0,0 +1,31 @@
+
+
+ 2014-01-22T19:11:57.151Z
+ 2015-06-19T19:14:25.924Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Branch artery occlusion in a young woman.
+
+
+ formatted-unspecified
+ Gittinger JW, Miller NR, Keltner JL, Burde RM. Branch artery occlusion in a young woman. Surv Ophthalmol. 1985 Jul-Aug; 30(1):52-8.
+
+ journal-article
+
+ 1985
+ 07
+ 01
+
+
\ No newline at end of file
diff --git a/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277902.xml b/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277902.xml
new file mode 100644
index 0000000000..aeab728543
--- /dev/null
+++ b/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277902.xml
@@ -0,0 +1,54 @@
+
+
+ 2014-01-22T19:11:57.159Z
+ 2015-06-19T19:14:26.327Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Another cautionary tale.
+
+ Journal title
+ Short description
+ journal-article
+
+ 2011
+ 05
+ 01
+
+
+
+ Walter White
+ walter@test.com
+
+ first
+ author
+
+
+
+ John White
+ john@test.com
+
+ additional
+ author
+
+
+
+ Jesse Pinkman
+
+ first
+ editor
+
+
+
+
\ No newline at end of file
diff --git a/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277904.xml b/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277904.xml
new file mode 100644
index 0000000000..980daa490e
--- /dev/null
+++ b/dspace-api/src/test/resources/org/dspace/app/orcid-works/work-277904.xml
@@ -0,0 +1,62 @@
+
+
+ 2014-01-22T19:11:57.160Z
+ 2015-06-19T19:14:26.350Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ The elements of style and the survey of ophthalmology.
+
+
+ bibtex
+ @article{Test,
+ doi = {10.11234.12},
+ year = 2011,
+ month = {nov},
+ publisher = {Elsevier {BV}},
+ volume = {110},
+ pages = {71--83},
+ author = {Walter White},
+ title = {Title from Bibtex: The elements of style and the survey of ophthalmology.},
+ journal = {Test Journal}
+ }
+
+
+ invention
+
+
+ agr
+ work:external-identifier-id
+ http://orcid.org
+ version-of
+
+
+ doi
+ 10.11234.12
+ http://orcid.org
+ self
+
+
+
+
+ Walter White
+ walter@test.com
+
+ first
+ author
+
+
+
+ it
+
\ No newline at end of file
diff --git a/dspace-api/src/test/resources/org/dspace/app/orcid-works/workBulk-277904-277902-277871.xml b/dspace-api/src/test/resources/org/dspace/app/orcid-works/workBulk-277904-277902-277871.xml
new file mode 100644
index 0000000000..97d39dcf41
--- /dev/null
+++ b/dspace-api/src/test/resources/org/dspace/app/orcid-works/workBulk-277904-277902-277871.xml
@@ -0,0 +1,147 @@
+
+
+
+ 2014-01-22T19:11:57.160Z
+ 2015-06-19T19:14:26.350Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ The elements of style and the survey of ophthalmology.
+
+
+ bibtex
+ @article{Test,
+ doi = {10.11234.12},
+ year = 2011,
+ month = {nov},
+ publisher = {Elsevier {BV}},
+ volume = {110},
+ pages = {71--83},
+ author = {Walter White},
+ title = {Title from Bibtex: The elements of style and the survey of ophthalmology.},
+ journal = {Test Journal}
+ }
+
+
+ invention
+
+
+ agr
+ work:external-identifier-id
+ http://orcid.org
+ version-of
+
+
+ doi
+ 10.11234.12
+ http://orcid.org
+ self
+
+
+
+
+ Walter White
+ walter@test.com
+
+ first
+ author
+
+
+
+ it
+
+
+ 2014-01-22T19:11:57.159Z
+ 2015-06-19T19:14:26.327Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Another cautionary tale.
+
+ Journal title
+ Short description
+ journal-article
+
+ 2011
+ 05
+ 01
+
+
+
+ Walter White
+ walter@test.com
+
+ first
+ author
+
+
+
+ John White
+ john@test.com
+
+ additional
+ author
+
+
+
+ Jesse Pinkman
+
+ first
+ editor
+
+
+
+
+
+ 2014-01-22T19:11:57.151Z
+ 2015-06-19T19:14:25.924Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Branch artery occlusion in a young woman.
+
+
+ formatted-unspecified
+ Gittinger JW, Miller NR, Keltner JL, Burde RM. Branch artery occlusion in a young woman. Surv Ophthalmol. 1985 Jul-Aug; 30(1):52-8.
+
+ journal-article
+
+ 1985
+ 07
+ 01
+
+
+
\ No newline at end of file
diff --git a/dspace-api/src/test/resources/org/dspace/app/orcid-works/workBulk-277904-277902.xml b/dspace-api/src/test/resources/org/dspace/app/orcid-works/workBulk-277904-277902.xml
new file mode 100644
index 0000000000..6c9d0d7db6
--- /dev/null
+++ b/dspace-api/src/test/resources/org/dspace/app/orcid-works/workBulk-277904-277902.xml
@@ -0,0 +1,117 @@
+
+
+
+ 2014-01-22T19:11:57.160Z
+ 2015-06-19T19:14:26.350Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ The elements of style and the survey of ophthalmology.
+
+
+ bibtex
+ @article{Test,
+ doi = {10.11234.12},
+ year = 2011,
+ month = {nov},
+ publisher = {Elsevier {BV}},
+ volume = {110},
+ pages = {71--83},
+ author = {Walter White},
+ title = {Title from Bibtex: The elements of style and the survey of ophthalmology.},
+ journal = {Test Journal}
+ }
+
+
+ invention
+
+
+ agr
+ work:external-identifier-id
+ http://orcid.org
+ version-of
+
+
+ doi
+ 10.11234.12
+ http://orcid.org
+ self
+
+
+
+
+ Walter White
+ walter@test.com
+
+ first
+ author
+
+
+
+ it
+
+
+ 2014-01-22T19:11:57.159Z
+ 2015-06-19T19:14:26.327Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Another cautionary tale.
+
+ Journal title
+ Short description
+ journal-article
+
+ 2011
+ 05
+ 01
+
+
+
+ Walter White
+ walter@test.com
+
+ first
+ author
+
+
+
+ John White
+ john@test.com
+
+ additional
+ author
+
+
+
+ Jesse Pinkman
+
+ first
+ editor
+
+
+
+
+
\ No newline at end of file
diff --git a/dspace-api/src/test/resources/org/dspace/app/orcid-works/works.xml b/dspace-api/src/test/resources/org/dspace/app/orcid-works/works.xml
new file mode 100644
index 0000000000..411160ef8e
--- /dev/null
+++ b/dspace-api/src/test/resources/org/dspace/app/orcid-works/works.xml
@@ -0,0 +1,196 @@
+
+
+ 2015-06-19T19:14:26.350Z
+
+ 2015-06-19T19:14:26.350Z
+
+
+ 2014-01-22T19:11:57.160Z
+ 2015-06-19T19:14:26.350Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ The elements of style and the survey of ophthalmology.
+
+ invention
+
+ 2012
+ 11
+ 01
+
+
+
+
+ 2015-06-19T19:14:26.339Z
+
+
+ 2014-01-22T19:11:57.159Z
+ 2015-06-19T19:14:26.339Z
+
+
+ https://sandbox.orcid.org/client/DSPACE-CLIENT-ID
+ DSPACE-CLIENT-ID
+ sandbox.orcid.org
+
+ DSPACE-CRIS
+
+
+ Introduction.
+
+ journal-article
+
+ 2011
+ 11
+ 01
+
+
+
+
+ 2015-06-19T19:14:26.327Z
+
+
+ 2014-01-22T19:11:57.159Z
+ 2015-06-19T19:14:26.327Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Another cautionary tale.
+
+ journal-article
+
+ 2011
+ 05
+ 01
+
+
+
+ 2014-01-22T19:11:57.159Z
+ 2015-06-19T19:14:26.327Z
+
+
+ https://sandbox.orcid.org/client/4Science
+ 4Science
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Another cautionary tale (4Science).
+
+ journal-article
+
+ 2011
+ 05
+ 01
+
+
+
+
+ 2015-06-19T19:14:26.108Z
+
+
+ 2014-01-22T19:11:57.155Z
+ 2015-06-19T19:14:26.108Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/DSPACE-CLIENT-ID
+ DSPACE-CLIENT-ID
+ sandbox.orcid.org
+
+ DSPACE-CRIS
+
+
+ Functional hemianopsia: a historical perspective.
+
+ journal-article
+
+ 1988
+ 05
+ 01
+
+
+
+ 2014-01-22T19:11:57.151Z
+ 2015-06-19T19:14:25.924Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Branch artery occlusion in a young man.
+
+ journal-article
+
+ 1985
+ 07
+ 01
+
+
+
+
+ 2015-06-19T19:14:26.108Z
+
+
+ 2014-01-22T19:11:57.151Z
+ 2015-06-19T19:14:25.924Z
+
+
+ https://sandbox.orcid.org/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+
+ https://sandbox.orcid.org/client/0000-0002-4105-0763
+ 0000-0002-4105-0763
+ sandbox.orcid.org
+
+ BU Profiles to ORCID Integration Site
+
+
+ Branch artery occlusion in a young woman.
+
+ journal-article
+
+ 1985
+ 07
+ 01
+
+
+
+
\ No newline at end of file
diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java
index 8b39e2004e..cd2c2fe53d 100644
--- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java
+++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java
@@ -53,7 +53,7 @@ public class ExternalSourcesRestControllerIT extends AbstractControllerIntegrati
ExternalSourceMatcher.matchExternalSource(
"openAIREFunding", "openAIREFunding", false)
)))
- .andExpect(jsonPath("$.page.totalElements", Matchers.is(8)));
+ .andExpect(jsonPath("$.page.totalElements", Matchers.is(9)));
}
@Test
diff --git a/dspace/config/crosswalks/orcid/mapConverter-orcid-to-dspace-language-code.properties b/dspace/config/crosswalks/orcid/mapConverter-orcid-to-dspace-language-code.properties
new file mode 100644
index 0000000000..6699d5c195
--- /dev/null
+++ b/dspace/config/crosswalks/orcid/mapConverter-orcid-to-dspace-language-code.properties
@@ -0,0 +1 @@
+# Mapping between languages supported by ORCID and the DSpace common iso languages
\ No newline at end of file
diff --git a/dspace/config/crosswalks/orcid/mapConverter-orcid-to-dspace-publication-type.properties b/dspace/config/crosswalks/orcid/mapConverter-orcid-to-dspace-publication-type.properties
new file mode 100644
index 0000000000..7f41538ac9
--- /dev/null
+++ b/dspace/config/crosswalks/orcid/mapConverter-orcid-to-dspace-publication-type.properties
@@ -0,0 +1,12 @@
+# Mapping between work type supported by ORCID and the DSpace common publication's types
+website = Other
+data-set = Dataset
+book = Book
+book-chapter = Book chapter
+conference-paper = Other
+journal-article = Article
+newspaper-article = Article
+magazine-article = Article
+patent = Other
+report = Other
+book-review = Other
\ No newline at end of file
diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg
index a018358eef..cde8196774 100644
--- a/dspace/config/modules/orcid.cfg
+++ b/dspace/config/modules/orcid.cfg
@@ -134,4 +134,31 @@ orcid.validation.organization.identifier-sources = LEI
#------------------------------------------------------------------#
## Configuration for max attempts during ORCID batch synchronization
-orcid.bulk-synchronization.max-attempts = 5
\ No newline at end of file
+orcid.bulk-synchronization.max-attempts = 5
+
+#------------------------------------------------------------------#
+#--------------------ORCID EXTERNAL DATA MAPPING-------------------#
+#------------------------------------------------------------------#
+
+### Work (Publication) external-data.mapping ###
+orcid.external-data.mapping.publication.title = dc.title
+
+orcid.external-data.mapping.publication.description = dc.description.abstract
+orcid.external-data.mapping.publication.issued-date = dc.date.issued
+orcid.external-data.mapping.publication.language = dc.language.iso
+orcid.external-data.mapping.publication.language.converter = mapConverterOrcidToDSpaceLanguageCode
+orcid.external-data.mapping.publication.is-part-of = dc.relation.ispartof
+orcid.external-data.mapping.publication.type = dc.type
+orcid.external-data.mapping.publication.type.converter = mapConverterOrcidToDSpacePublicationType
+
+##orcid.external-data.mapping.publication.contributors syntax is ::
+orcid.external-data.mapping.publication.contributors = dc.contributor.author::author
+orcid.external-data.mapping.publication.contributors = dc.contributor.editor::editor
+
+##orcid.external-data.mapping.publication.external-ids syntax is :: or $simple-handle::
+##The full list of available external identifiers is available here https://pub.orcid.org/v3.0/identifiers
+orcid.external-data.mapping.publication.external-ids = dc.identifier.doi::doi
+orcid.external-data.mapping.publication.external-ids = dc.identifier.scopus::eid
+orcid.external-data.mapping.publication.external-ids = dc.identifier.pmid::pmid
+orcid.external-data.mapping.publication.external-ids = dc.identifier.isi::wosuid
+orcid.external-data.mapping.publication.external-ids = dc.identifier.issn::issn
\ No newline at end of file
diff --git a/dspace/config/spring/api/external-services.xml b/dspace/config/spring/api/external-services.xml
index fc889e4fc2..2a9f601b52 100644
--- a/dspace/config/spring/api/external-services.xml
+++ b/dspace/config/spring/api/external-services.xml
@@ -93,6 +93,16 @@
+
+
+
+
+
+
+ Publication
+
+
+
diff --git a/dspace/config/spring/api/orcid-services.xml b/dspace/config/spring/api/orcid-services.xml
index 4a2e7e93b8..e5fb002c31 100644
--- a/dspace/config/spring/api/orcid-services.xml
+++ b/dspace/config/spring/api/orcid-services.xml
@@ -146,11 +146,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+