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: + * + */ + 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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + +