diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidEntityType.java b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidEntityType.java index c777573c1b30..21712cf9783f 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidEntityType.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidEntityType.java @@ -22,6 +22,11 @@ public enum OrcidEntityType { */ PUBLICATION("Publication", "/work"), + /** + * The ORCID product/work activity. + */ + PRODUCT("Product", "/work"), + /** * The ORCID funding activity. */ diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/OrcidProductWorkFieldMapping.java b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidProductWorkFieldMapping.java new file mode 100644 index 000000000000..c3b77a59733b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/orcid/model/OrcidProductWorkFieldMapping.java @@ -0,0 +1,287 @@ +/** + * 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.orcid.model; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; +import static org.dspace.orcid.model.factory.OrcidFactoryUtils.parseConfigurations; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.integration.crosswalks.CSLItemDataCrosswalk; +import org.dspace.util.SimpleMapConverter; +import org.orcid.jaxb.model.common.CitationType; +import org.orcid.jaxb.model.common.ContributorRole; +import org.orcid.jaxb.model.v3.release.record.Work; + +/** + * Class that contains all the mapping between {@link Work} and DSpace metadata + * fields. Adapted from {@link org.dspace.orcid.model.OrcidWorkFieldMapping} for Product entity + */ +public class OrcidProductWorkFieldMapping { + + /** + * The metadata fields related to the work contributors. + */ + private Map contributorFields = new HashMap<>(); + + /** + * The metadata fields related to the work external identifiers. + */ + private Map externalIdentifierFields = new HashMap<>(); + + /** + * The metadata field related to the work publication date. + */ + private String publicationDateField; + + /** + * The metadata field related to the work title. + */ + private String titleField; + + /** + * The metadata field related to the work type. + */ + private String typeField; + + /** + * The metadata field related to the work journal title. + */ + private String journalTitleField; + + /** + * The metadata field related to the work description. + */ + private String shortDescriptionField; + + /** + * The metadata field related to the work language. + */ + private String languageField; + + /** + * The metadata field related to the work sub title. + */ + private String subTitleField; + + private CitationType citationType; + + /** + * The work type converter. + */ + private SimpleMapConverter typeConverter; + + /** + * The work language converter. + */ + private SimpleMapConverter languageConverter; + + private Map citationCrosswalks; + + private String fundingField; + + private String fundingExternalIdType; + + private String fundingExternalId; + + private String fundingEntityExternalId; + + private String fundingUrlField; + + public String convertType(String type) { + return typeConverter != null ? typeConverter.getValue(type) : type; + } + + public String convertLanguage(String language) { + return languageConverter != null ? languageConverter.getValue(language) : language; + } + + public String getTitleField() { + return titleField; + } + + public void setTitleField(String titleField) { + this.titleField = titleField; + } + + public String getTypeField() { + return typeField; + } + + public void setTypeField(String typeField) { + this.typeField = typeField; + } + + public void setTypeConverter(SimpleMapConverter typeConverter) { + this.typeConverter = typeConverter; + } + + public Map getContributorFields() { + return contributorFields; + } + + public void setContributorFields(String contributorFields) { + this.contributorFields = parseContributors(contributorFields); + } + + public Map getExternalIdentifierFields() { + return externalIdentifierFields; + } + + public void setExternalIdentifierFields(String externalIdentifierFields) { + this.externalIdentifierFields = parseConfigurations(externalIdentifierFields); + } + + public String getPublicationDateField() { + return publicationDateField; + } + + public void setPublicationDateField(String publicationDateField) { + this.publicationDateField = publicationDateField; + } + + public String getJournalTitleField() { + return journalTitleField; + } + + public void setJournalTitleField(String journalTitleField) { + this.journalTitleField = journalTitleField; + } + + public String getShortDescriptionField() { + return shortDescriptionField; + } + + public void setShortDescriptionField(String shortDescriptionField) { + this.shortDescriptionField = shortDescriptionField; + } + + public String getLanguageField() { + return languageField; + } + + public void setLanguageField(String languageField) { + this.languageField = languageField; + } + + public void setLanguageConverter(SimpleMapConverter languageConverter) { + this.languageConverter = languageConverter; + } + + public String getSubTitleField() { + return subTitleField; + } + + public void setSubTitleField(String subTitleField) { + this.subTitleField = subTitleField; + } + + public Map getCitationCrosswalks() { + return citationCrosswalks; + } + + public void setCitationCrosswalks(Map citationCrosswalks) { + this.citationCrosswalks = citationCrosswalks; + } + + public String getFundingField() { + return fundingField; + } + + public void setFundingField(String fundingField) { + this.fundingField = fundingField; + } + + public String getFundingExternalIdType() { + return fundingExternalIdType; + } + + public void setFundingExternalIdType(String fundingExternalIdType) { + this.fundingExternalIdType = fundingExternalIdType; + } + + public String getFundingExternalId() { + return fundingExternalId; + } + + public void setFundingExternalId(String fundingExternalId) { + this.fundingExternalId = fundingExternalId; + } + + public String getFundingEntityExternalId() { + return fundingEntityExternalId; + } + + public void setFundingEntityExternalId(String fundingEntityExternalId) { + this.fundingEntityExternalId = fundingEntityExternalId; + } + + public String getFundingUrlField() { + return fundingUrlField; + } + + public void setFundingUrlField(String fundingUrlField) { + this.fundingUrlField = fundingUrlField; + } + + public CitationType getCitationType() { + return citationType; + } + + public void setCitationType(String citationType) { + this.citationType = parseCitationType(citationType); + } + + private CitationType parseCitationType(String citationType) { + + if (StringUtils.isBlank(citationType)) { + return null; + } + + try { + return CitationType.fromValue(citationType); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("The citation type " + citationType + " is invalid, " + + "allowed values are " + getAllowedCitationTypes(), ex); + } + } + + private Map parseContributors(String contributors) { + Map contributorsMap = parseConfigurations(contributors); + return contributorsMap.keySet().stream() + .collect(toMap(identity(), field -> parseContributorRole(contributorsMap.get(field)))); + } + + private ContributorRole parseContributorRole(String contributorRole) { + try { + return ContributorRole.fromValue(contributorRole); + } catch (IllegalArgumentException ex) { + throw new IllegalArgumentException("The contributor role " + contributorRole + + " is invalid, allowed values are " + getAllowedContributorRoles(), ex); + } + } + + private List getAllowedContributorRoles() { + return Arrays.asList(ContributorRole.values()).stream() + .map(ContributorRole::value) + .collect(Collectors.toList()); + } + + private List getAllowedCitationTypes() { + return Arrays.asList(CitationType.values()).stream() + .map(CitationType::value) + .collect(Collectors.toList()); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidProductWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidProductWorkFactory.java new file mode 100644 index 000000000000..2636592cfae4 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidProductWorkFactory.java @@ -0,0 +1,398 @@ +/** + * 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.orcid.model.factory.impl; + +import static org.apache.commons.lang3.StringUtils.isBlank; +import static org.apache.commons.lang3.StringUtils.isNotBlank; +import static org.dspace.core.CrisConstants.PLACEHOLDER_PARENT_METADATA_VALUE; +import static org.orcid.jaxb.model.common.Relationship.FUNDED_BY; +import static org.orcid.jaxb.model.common.Relationship.SELF; + +import java.io.ByteArrayOutputStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.EnumUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.authority.service.AuthorityValueService; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.integration.crosswalks.CSLItemDataCrosswalk; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.orcid.model.OrcidEntityType; +import org.dspace.orcid.model.OrcidProductWorkFieldMapping; +import org.dspace.orcid.model.factory.OrcidCommonObjectFactory; +import org.dspace.orcid.model.factory.OrcidEntityFactory; +import org.dspace.util.UUIDUtils; +import org.orcid.jaxb.model.common.CitationType; +import org.orcid.jaxb.model.common.ContributorRole; +import org.orcid.jaxb.model.common.LanguageCode; +import org.orcid.jaxb.model.common.Relationship; +import org.orcid.jaxb.model.common.WorkType; +import org.orcid.jaxb.model.v3.release.common.Contributor; +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.common.Url; +import org.orcid.jaxb.model.v3.release.record.Activity; +import org.orcid.jaxb.model.v3.release.record.Citation; +import org.orcid.jaxb.model.v3.release.record.ExternalID; +import org.orcid.jaxb.model.v3.release.record.ExternalIDs; +import org.orcid.jaxb.model.v3.release.record.Work; +import org.orcid.jaxb.model.v3.release.record.WorkContributors; +import org.orcid.jaxb.model.v3.release.record.WorkTitle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link OrcidEntityFactory} that creates instances of + * {@link Work}. Copy of {@link OrcidWorkFactory} + * Adapted for Product Entity with own mapping in {@link org.dspace.orcid.model.OrcidProductWorkFieldMapping} + * + */ +public class OrcidProductWorkFactory implements OrcidEntityFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(OrcidProductWorkFactory.class); + + @Autowired + private ItemService itemService; + + @Autowired + private OrcidCommonObjectFactory orcidCommonObjectFactory; + + private OrcidProductWorkFieldMapping fieldMapping; + + @Override + public OrcidEntityType getEntityType() { + return OrcidEntityType.PRODUCT; + } + + @Override + public Activity createOrcidObject(Context context, Item item) { + Work work = new Work(); + work.setJournalTitle(getJournalTitle(context, item)); + work.setWorkContributors(getWorkContributors(context, item)); + work.setWorkTitle(getWorkTitle(context, item)); + work.setPublicationDate(getPublicationDate(context, item)); + work.setWorkExternalIdentifiers(getWorkExternalIds(context, item)); + work.setWorkType(getWorkType(context, item)); + work.setWorkCitation(getWorkCitation(context, item)); + work.setShortDescription(getShortDescription(context, item)); + work.setLanguageCode(getLanguageCode(context, item)); + work.setUrl(getUrl(context, item)); + return work; + } + + private Title getJournalTitle(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getJournalTitleField()) + .map(metadataValue -> new Title(metadataValue.getValue())) + .orElse(null); + } + + private WorkContributors getWorkContributors(Context context, Item item) { + Map contributorFields = fieldMapping.getContributorFields(); + List contributors = getMetadataValues(context, item, contributorFields.keySet()).stream() + .map(metadataValue -> getContributor(context, metadataValue)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + return new WorkContributors(contributors); + } + + private Optional getContributor(Context context, MetadataValue metadataValue) { + Map contributorFields = fieldMapping.getContributorFields(); + ContributorRole role = contributorFields.get(metadataValue.getMetadataField().toString('.')); + return orcidCommonObjectFactory.createContributor(context, metadataValue, role); + } + + /** + * Create an instance of WorkTitle from the given item. + */ + private WorkTitle getWorkTitle(Context context, Item item) { + Optional workTitleValue = getWorkTitleValue(context, item); + if (workTitleValue.isEmpty()) { + return null; + } + + WorkTitle workTitle = new WorkTitle(); + workTitle.setTitle(new Title(workTitleValue.get())); + getSubTitle(context, item).ifPresent(workTitle::setSubtitle); + return workTitle; + } + + /** + * Take the work title from the configured metadata field of the given item + * (orcid.mapping.work.title), if any. + */ + private Optional getWorkTitleValue(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getTitleField()) + .map(MetadataValue::getValue); + } + + /** + * Take the work title from the configured metadata field of the given item + * (orcid.mapping.work.sub-title), if any. + */ + private Optional getSubTitle(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getSubTitleField()) + .map(MetadataValue::getValue) + .map(Subtitle::new); + } + + private PublicationDate getPublicationDate(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getPublicationDateField()) + .flatMap(orcidCommonObjectFactory::createFuzzyDate) + .map(PublicationDate::new) + .orElse(null); + } + + /** + * Creates an instance of ExternalIDs from the metadata values of the given + * item, using the orcid.mapping.funding.external-ids configuration. + */ + private ExternalIDs getWorkExternalIds(Context context, Item item) { + ExternalIDs externalIdentifiers = new ExternalIDs(); + externalIdentifiers.getExternalIdentifier().addAll(getWorkSelfExternalIds(context, item)); + externalIdentifiers.getExternalIdentifier().addAll(getWorkFundedByExternalIds(context, item)); + return externalIdentifiers; + } + + /** + * Creates a list of ExternalID, one for orcid.mapping.funding.external-ids + * value, taking the values from the given item. + */ + private List getWorkSelfExternalIds(Context context, Item item) { + + List selfExternalIds = new ArrayList(); + + Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); + + if (externalIdentifierFields.containsKey(SIMPLE_HANDLE_PLACEHOLDER)) { + String handleType = externalIdentifierFields.get(SIMPLE_HANDLE_PLACEHOLDER); + selfExternalIds.add(getExternalId(handleType, item.getHandle(), SELF)); + } + + getMetadataValues(context, item, externalIdentifierFields.keySet()).stream() + .map(this::getSelfExternalId) + .forEach(selfExternalIds::add); + + return selfExternalIds; + } + + /** + * Creates an instance of ExternalID taking the value from the given + * metadataValue. The type of the ExternalID is calculated using the + * orcid.mapping.funding.external-ids configuration. The relationship of the + * ExternalID is SELF. + */ + private ExternalID getSelfExternalId(MetadataValue metadataValue) { + Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); + String metadataField = metadataValue.getMetadataField().toString('.'); + return getExternalId(externalIdentifierFields.get(metadataField), metadataValue.getValue(), SELF); + } + + private List getWorkFundedByExternalIds(Context context, Item item) { + + if (isBlank(fieldMapping.getFundingExternalIdType())) { + return Collections.emptyList(); + } + + return getMetadataValues(context, item, fieldMapping.getFundingField()).stream() + .map(metadataValue -> getWorkFundedByExternalId(context, item, metadataValue)) + .flatMap(Optional::stream) + .collect(Collectors.toList()); + } + + private Optional getWorkFundedByExternalId(Context context, Item work, MetadataValue fundingMetadata) { + return getFundedByExternalIdFromFunding(context, fundingMetadata) + .or(() -> getFundedByExternalIdFromWork(context, work, fundingMetadata.getPlace())); + } + + private Optional getFundedByExternalIdFromFunding(Context context, MetadataValue fundingMetadata) { + + if (isAuthoritySet(fundingMetadata.getAuthority())) { + return findItemById(context, UUIDUtils.fromString(fundingMetadata.getAuthority())) + .map(funding -> getFundingExternalId(context, funding)); + } + + return Optional.empty(); + } + + private Optional getFundedByExternalIdFromWork(Context context, Item work, int fundingPlace) { + List externalIdValues = getMetadataValues(context, work, fieldMapping.getFundingExternalId()); + + if (externalIdValues.size() > fundingPlace && isNotPlaceholder(externalIdValues.get(fundingPlace))) { + String value = externalIdValues.get(fundingPlace).getValue(); + return Optional.of(getExternalId(fieldMapping.getFundingExternalIdType(), value, FUNDED_BY)); + } + + return Optional.empty(); + } + + private ExternalID getFundingExternalId(Context context, Item funding) { + + String externalIdValue = getMetadataValue(context, funding, fieldMapping.getFundingEntityExternalId()) + .map(MetadataValue::getValue) + .orElse(null); + + if (externalIdValue == null) { + return null; + } + + Optional fundingUrl = getMetadataValue(context, funding, fieldMapping.getFundingUrlField()) + .map(fundingUrlMetadata -> new Url(fundingUrlMetadata.getValue())) + .or(() -> orcidCommonObjectFactory.createUrl(context, funding)); + + ExternalID externalId = getExternalId(fieldMapping.getFundingExternalIdType(), externalIdValue, FUNDED_BY); + fundingUrl.ifPresent(externalId::setUrl); + return externalId; + } + + private boolean isAuthoritySet(String authority) { + return isNotBlank(authority) && !StringUtils.startsWith(authority, AuthorityValueService.REFERENCE); + } + + /** + * Creates an instance of ExternalID with the given type, value and + * relationship. + */ + private ExternalID getExternalId(String type, String value, Relationship relationship) { + ExternalID externalID = new ExternalID(); + externalID.setType(type); + externalID.setValue(value); + externalID.setRelationship(relationship); + return externalID; + } + + /** + * Creates an instance of WorkType from the given item, taking the value fom the + * configured metadata field (orcid.mapping.work.type). + */ + private WorkType getWorkType(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getTypeField()) + .map(MetadataValue::getValue) + .map(type -> fieldMapping.convertType(type)) + .flatMap(this::getWorkType) + .orElse(WorkType.UNDEFINED); + } + + /** + * Creates an instance of WorkType from the given workType value, if valid. + */ + private Optional getWorkType(String workType) { + try { + return Optional.ofNullable(WorkType.fromValue(workType)); + } catch (IllegalArgumentException ex) { + LOGGER.warn("The type {} is not valid for ORCID works", workType); + return Optional.empty(); + } + } + + private Citation getWorkCitation(Context context, Item item) { + + CSLItemDataCrosswalk citationCrosswalk = getCitationCrosswalk(); + + if (citationCrosswalk == null || !citationCrosswalk.canDisseminate(context, item)) { + return null; + } + + try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { + citationCrosswalk.disseminate(context, item, out); + return new Citation(out.toString(), fieldMapping.getCitationType()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private CSLItemDataCrosswalk getCitationCrosswalk() { + CitationType citationType = fieldMapping.getCitationType(); + return citationType != null ? fieldMapping.getCitationCrosswalks().get(citationType.value()) : null; + } + + private String getShortDescription(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getShortDescriptionField()) + .map(MetadataValue::getValue) + .orElse(null); + } + + private String getLanguageCode(Context context, Item item) { + return getMetadataValue(context, item, fieldMapping.getLanguageField()) + .map(MetadataValue::getValue) + .map(language -> fieldMapping.convertLanguage(language)) + .filter(language -> isValidLanguage(language)) + .orElse(null); + } + + private boolean isValidLanguage(String language) { + + if (isBlank(language)) { + return false; + } + + boolean isValid = EnumUtils.isValidEnum(LanguageCode.class, language); + if (!isValid) { + LOGGER.warn("The language {} is not a valid language code for ORCID works", language); + } + return isValid; + } + + private Url getUrl(Context context, Item item) { + return orcidCommonObjectFactory.createUrl(context, item).orElse(null); + } + + private List getMetadataValues(Context context, Item item, String metadataField) { + if (isBlank(metadataField)) { + return Collections.emptyList(); + } + return itemService.getMetadataByMetadataString(item, metadataField); + } + + private boolean isNotPlaceholder(MetadataValue metadata) { + return metadata != null && metadata.getValue() != null + && !metadata.getValue().equals(PLACEHOLDER_PARENT_METADATA_VALUE); + } + + private List getMetadataValues(Context context, Item item, Collection metadataFields) { + return metadataFields.stream() + .flatMap(metadataField -> itemService.getMetadataByMetadataString(item, metadataField).stream()) + .collect(Collectors.toList()); + } + + private Optional findItemById(Context context, UUID id) { + try { + return Optional.ofNullable(itemService.find(context, id)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private Optional getMetadataValue(Context context, Item item, String metadataField) { + + if (isBlank(metadataField)) { + return Optional.empty(); + } + + return itemService.getMetadataByMetadataString(item, metadataField).stream() + .filter(metadataValue -> isNotBlank(metadataValue.getValue())) + .findFirst(); + } + + public void setFieldMapping(OrcidProductWorkFieldMapping fieldMapping) { + this.fieldMapping = fieldMapping; + } + +} diff --git a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java index b9eedd2fcf42..88acf447e928 100644 --- a/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/ItemBuilder.java @@ -593,6 +593,15 @@ public ItemBuilder withOrcidSynchronizationFundingsPreference(String value) { return setMetadataSingleValue(item, "dspace", "orcid", "sync-fundings", value); } + public ItemBuilder withOrcidSynchronizationProductsPreference(OrcidEntitySyncPreference value) { + return withOrcidSynchronizationProductsPreference(value.name()); + } + + public ItemBuilder withOrcidSynchronizationProductsPreference(String value) { + return setMetadataSingleValue(item, "dspace", "orcid", "sync-products", value); + } + + public ItemBuilder withOrcidSynchronizationProfilePreference(OrcidProfileSyncPreference value) { return withOrcidSynchronizationProfilePreference(value.name()); } diff --git a/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java b/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java index a1ebec2197e4..4fb9de54e153 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/OrcidQueueConsumerIT.java @@ -586,6 +586,41 @@ public void testOrcidQueueRecordCreationForFunding() throws Exception { assertThat(orcidQueueRecords.get(0), equalTo(newOrcidQueueRecords.get(0))); } + @Test + public void testOrcidQueueRecordCreationForProduct() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item profile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) + .withOrcidSynchronizationProductsPreference(ALL) + .build(); + + Collection productCollection = createCollection("Products", "Product"); + + Item product = ItemBuilder.createItem(context, productCollection) + .withTitle("Test product") + .withAuthor("Test User", profile.getID().toString()) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + List orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, product, "Product", null, INSERT)); + + addMetadata(product, "dc", "type", null, "http://purl.org/coar/resource_type/scheme/c_12cc", null); + context.commit(); + + List newOrcidQueueRecords = orcidQueueService.findAll(context); + assertThat(newOrcidQueueRecords, hasSize(1)); + + assertThat(orcidQueueRecords.get(0), equalTo(newOrcidQueueRecords.get(0))); + } + @Test public void testOrcidQueueRecordCreationToUpdateFunding() throws Exception { @@ -618,6 +653,38 @@ public void testOrcidQueueRecordCreationToUpdateFunding() throws Exception { assertThat(orcidQueueRecords.get(0), matches(profile, funding, "Funding", "123456", UPDATE)); } + @Test + public void testOrcidQueueRecordCreationToUpdateProduct() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item profile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) + .withOrcidSynchronizationProductsPreference(ALL) + .build(); + + Collection productCollection = createCollection("Products", "Product"); + + Item product = ItemBuilder.createItem(context, productCollection) + .withTitle("Test product") + .build(); + + createOrcidHistory(context, profile, product) + .withPutCode("123456") + .build(); + + addMetadata(product, "dc", "contributor", "author", "Test User", profile.getID().toString()); + + context.restoreAuthSystemState(); + context.commit(); + + List orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords.get(0), matches(profile, product, "Product", "123456", UPDATE)); + } + @Test public void testNoOrcidQueueRecordCreationOccursIfFundingSynchronizationIsDisabled() throws Exception { @@ -648,6 +715,36 @@ public void testNoOrcidQueueRecordCreationOccursIfFundingSynchronizationIsDisabl assertThat(orcidQueueService.findAll(context), empty()); } + @Test + public void testNoOrcidQueueRecordCreationOccursIfProductSynchronizationIsDisabled() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item profile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) + .build(); + + Collection productCollection = createCollection("Products", "Product"); + + Item product = ItemBuilder.createItem(context, productCollection) + .withTitle("Test product") + .withAuthor("Test User", profile.getID().toString()) + .build(); + + context.restoreAuthSystemState(); + context.commit(); + + assertThat(orcidQueueService.findAll(context), empty()); + + addMetadata(profile, "dspace", "orcid", "sync-products", DISABLED.name(), null); + addMetadata(product, "dc", "description", "abstract", "Product Poduct Pduct Puct Pct Pt P", null); + context.commit(); + + assertThat(orcidQueueService.findAll(context), empty()); + } + @Test public void testNoOrcidQueueRecordCreationOccursIfProfileHasNotOrcidIdentifier() throws Exception { @@ -657,6 +754,7 @@ public void testNoOrcidQueueRecordCreationOccursIfProfileHasNotOrcidIdentifier() .withTitle("Test User") .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) .withOrcidSynchronizationFundingsPreference(ALL) + .withOrcidSynchronizationProductsPreference(ALL) .build(); Collection fundingCollection = createCollection("Fundings", "Funding"); @@ -666,6 +764,13 @@ public void testNoOrcidQueueRecordCreationOccursIfProfileHasNotOrcidIdentifier() .withFundingInvestigator("Test User", profile.getID().toString()) .build(); + Collection productCollection = createCollection("Products", "Product"); + + ItemBuilder.createItem(context, productCollection) + .withTitle("Test product") + .withAuthor("Test User", profile.getID().toString()) + .build(); + context.restoreAuthSystemState(); context.commit(); @@ -834,6 +939,7 @@ public void testWithMetadataFieldToIgnore() throws Exception { .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson) .withOrcidSynchronizationFundingsPreference(ALL) .withOrcidSynchronizationPublicationsPreference(ALL) + .withOrcidSynchronizationProductsPreference(ALL) .build(); Collection publicationCollection = createCollection("Publications", "Publication"); @@ -866,6 +972,13 @@ public void testWithMetadataFieldToIgnore() throws Exception { .withFundingCoInvestigator("Test User", profile.getID().toString()) .build(); + Collection productCollection = createCollection("Products", "Product"); + + Item firstProduct = ItemBuilder.createItem(context, productCollection) + .withTitle("Test product") + .withAuthor("Test User", profile.getID().toString()) + .build(); + context.restoreAuthSystemState(); List records = orcidQueueService.findAll(context); @@ -873,7 +986,6 @@ public void testWithMetadataFieldToIgnore() throws Exception { assertThat(records, hasItem(matches(profile, firstPublication, "Publication", null, INSERT))); assertThat(records, hasItem(matches(profile, secondPublication, "Publication", null, INSERT))); assertThat(records, hasItem(matches(profile, firstFunding, "Funding", null, INSERT))); - } @Test diff --git a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java index d582b7c6058b..4d56bcb9c466 100644 --- a/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/orcid/service/OrcidEntityFactoryServiceIT.java @@ -75,6 +75,8 @@ public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithData private Collection publications; + private Collection products; + private Collection fundings; @Before @@ -103,6 +105,11 @@ public void setup() { .withEntityType("Publication") .build(); + products = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .withEntityType("Product") + .build(); + fundings = CollectionBuilder.createCollection(context, parentCommunity) .withName("Collection") .withEntityType("Funding") @@ -174,6 +181,69 @@ public void testWorkCreation() { } + @Test + public void testProductWorkCreation() { + + context.turnOffAuthorisationSystem(); + + Item author = ItemBuilder.createItem(context, persons) + .withTitle("Jesse Pinkman") + .withOrcidIdentifier("0000-1111-2222-3333") + .withPersonEmail("test@test.it") + .build(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test dataset") + .withAuthor("Walter White") + .withAuthor("Jesse Pinkman", author.getID().toString()) + .withEditor("Editor") + .withIssueDate("2021-04-30") + .withDescriptionAbstract("Product description") + .withLanguage("en_US") + .withType("http://purl.org/coar/resource_type/c_ddb1") + .withIsPartOf("Collection of Products") + .withDoiIdentifier("doi-id") + .withScopusIdentifier("scopus-id") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getJournalTitle(), notNullValue()); + assertThat(work.getJournalTitle().getContent(), is("Collection of Products")); + assertThat(work.getLanguageCode(), is("en")); + assertThat(work.getPublicationDate(), matches(date("2021", "04", "30"))); + assertThat(work.getShortDescription(), is("Product description")); + assertThat(work.getPutCode(), nullValue()); + // assertThat(work.getWorkCitation(), notNullValue()); + // assertThat(work.getWorkCitation().getCitation(), containsString("Test product")); + assertThat(work.getWorkType(), is(WorkType.DATA_SET)); + assertThat(work.getWorkTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle(), notNullValue()); + assertThat(work.getWorkTitle().getTitle().getContent(), is("Test dataset")); + assertThat(work.getWorkContributors(), notNullValue()); + assertThat(work.getUrl(), matches(urlEndsWith(product.getHandle()))); + + List contributors = work.getWorkContributors().getContributor(); + assertThat(contributors, hasSize(2)); + assertThat(contributors, has(contributor("Walter White", AUTHOR, FIRST))); + // assertThat(contributors, has(contributor("Editor", EDITOR, FIRST))); + assertThat(contributors, has(contributor("Jesse Pinkman", AUTHOR, ADDITIONAL, + "0000-1111-2222-3333", "test@test.it"))); + + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(3)); + assertThat(externalIds, has(selfExternalId("doi", "doi-id"))); + assertThat(externalIds, has(selfExternalId("eid", "scopus-id"))); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + + } + @Test public void testWorkWithFundingCreation() { context.turnOffAuthorisationSystem(); @@ -200,6 +270,32 @@ public void testWorkWithFundingCreation() { assertThat(externalIds, has(fundedByExternalId("grant_number", "123456"))); } + @Test + public void testProductWorkWithFundingCreation() { + context.turnOffAuthorisationSystem(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test dataset") + .withAuthor("Walter White") + .withIssueDate("2021-04-30") + .withType("http://purl.org/coar/resource_type/H6QP-SC1X") + .withRelationFunding("Test funding") + .withRelationGrantno("123456") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + assertThat(externalIds, has(fundedByExternalId("grant_number", "123456"))); + } + @Test public void testWorkWithFundingWithoutGrantNumberCreation() { context.turnOffAuthorisationSystem(); @@ -224,6 +320,30 @@ public void testWorkWithFundingWithoutGrantNumberCreation() { assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); } + @Test + public void testProductWorkWithFundingWithoutGrantNumberCreation() { + context.turnOffAuthorisationSystem(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test product") + .withAuthor("Walter White") + .withIssueDate("2021-04-30") + .withType("http://purl.org/coar/resource_type/c_e9a0") + .withRelationFunding("Test funding") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(1)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + } + @Test public void testWorkWithFundingWithGrantNumberPlaceholderCreation() { context.turnOffAuthorisationSystem(); @@ -249,12 +369,37 @@ public void testWorkWithFundingWithGrantNumberPlaceholderCreation() { assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); } + @Test + public void testProductWorkWithFundingWithGrantNumberPlaceholderCreation() { + context.turnOffAuthorisationSystem(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test dataset") + .withAuthor("Walter White") + .withIssueDate("2021-04-30") + .withType("http://purl.org/coar/resource_type/c_7ad9") + .withRelationFunding("Test funding") + .withRelationGrantno(CrisConstants.PLACEHOLDER_PARENT_METADATA_VALUE) + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(1)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + } + @Test public void testWorkWithFundingEntityWithoutGrantNumberCreation() { context.turnOffAuthorisationSystem(); - Item funding = ItemBuilder.createItem(context, publications) + Item funding = ItemBuilder.createItem(context, fundings) .withTitle("Test funding") .build(); @@ -280,12 +425,43 @@ public void testWorkWithFundingEntityWithoutGrantNumberCreation() { assertThat(externalIds, has(fundedByExternalId("grant_number", "123456"))); } + @Test + public void testProductWorkWithFundingEntityWithoutGrantNumberCreation() { + + context.turnOffAuthorisationSystem(); + + Item funding = ItemBuilder.createItem(context, fundings) + .withTitle("Test funding") + .build(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test product") + .withAuthor("Walter White") + .withIssueDate("2021-04-30") + .withType("http://purl.org/coar/resource_type/c_7ad9") + .withRelationFunding("Test funding", funding.getID().toString()) + .withRelationGrantno("123456") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + assertThat(externalIds, has(fundedByExternalId("grant_number", "123456"))); + } + @Test public void testWorkWithFundingEntityWithGrantNumberCreation() { context.turnOffAuthorisationSystem(); - Item funding = ItemBuilder.createItem(context, publications) + Item funding = ItemBuilder.createItem(context, fundings) .withHandle("123456789/0001") .withTitle("Test funding") .withFundingIdentifier("987654") @@ -314,12 +490,47 @@ public void testWorkWithFundingEntityWithGrantNumberCreation() { "http://localhost:4000/handle/123456789/0001"))); } + @Test + public void testProductWorkWithFundingEntityWithGrantNumberCreation() { + + context.turnOffAuthorisationSystem(); + + Item funding = ItemBuilder.createItem(context, fundings) + .withHandle("123456789/0001") + .withTitle("Test funding") + .withFundingIdentifier("987654") + .build(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test product") + .withAuthor("Walter White") + .withIssueDate("2021-04-30") + .withType("http://purl.org/coar/resource_type/c_18cc") + .withRelationFunding("Test funding", funding.getID().toString()) + .withRelationGrantno("123456") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + assertThat(externalIds, has(fundedByExternalId("grant_number", "987654", + "http://localhost:4000/handle/123456789/0001"))); + } + + @Test public void testWorkWithFundingEntityWithGrantNumberAndUrlCreation() { context.turnOffAuthorisationSystem(); - Item funding = ItemBuilder.createItem(context, publications) + Item funding = ItemBuilder.createItem(context, fundings) .withHandle("123456789/0001") .withTitle("Test funding") .withFundingIdentifier("987654") @@ -348,6 +559,40 @@ public void testWorkWithFundingEntityWithGrantNumberAndUrlCreation() { assertThat(externalIds, has(fundedByExternalId("grant_number", "987654", "http://test-funding"))); } + @Test + public void testProductWorkWithFundingEntityWithGrantNumberAndUrlCreation() { + + context.turnOffAuthorisationSystem(); + + Item funding = ItemBuilder.createItem(context, fundings) + .withHandle("123456789/0001") + .withTitle("Test funding") + .withFundingIdentifier("987654") + .withFundingAwardUrl("http://test-funding") + .build(); + + Item product = ItemBuilder.createItem(context, products) + .withTitle("Test product") + .withAuthor("Walter White") + .withIssueDate("2021-04-30") + .withType("http://purl.org/coar/resource_type/c_18cc") + .withRelationFunding("Test funding", funding.getID().toString()) + .withRelationGrantno("123456") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(2)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + assertThat(externalIds, has(fundedByExternalId("grant_number", "987654", "http://test-funding"))); + } + @Test public void testEmptyWorkWithUnknownTypeCreation() { @@ -380,6 +625,39 @@ public void testEmptyWorkWithUnknownTypeCreation() { assertThat(externalIds, has(selfExternalId("handle", publication.getHandle()))); } + @Test + public void testEmptyProductWorkWithUnknownTypeCreation() { + + context.turnOffAuthorisationSystem(); + + Item product = ItemBuilder.createItem(context, products) + .withType("http://purl.org/coar/resource_type/") + .build(); + + context.restoreAuthSystemState(); + + Activity activity = entityFactoryService.createOrcidObject(context, product); + assertThat(activity, instanceOf(Work.class)); + + Work work = (Work) activity; + assertThat(work.getJournalTitle(), nullValue()); + assertThat(work.getLanguageCode(), nullValue()); + assertThat(work.getPublicationDate(), nullValue()); + assertThat(work.getShortDescription(), nullValue()); + assertThat(work.getPutCode(), nullValue()); + // assertThat(work.getWorkCitation(), notNullValue()); + assertThat(work.getWorkType(), is(WorkType.DATA_SET)); + assertThat(work.getWorkTitle(), nullValue()); + assertThat(work.getWorkContributors(), notNullValue()); + assertThat(work.getWorkContributors().getContributor(), empty()); + assertThat(work.getExternalIdentifiers(), notNullValue()); + + List externalIds = work.getExternalIdentifiers().getExternalIdentifier(); + assertThat(externalIds, hasSize(1)); + assertThat(externalIds, has(selfExternalId("handle", product.getHandle()))); + } + + @Test public void testFundingCreation() { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java index 54dffa57881e..2704ceeee748 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ResearcherProfileConverter.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.converter; import static org.dspace.orcid.model.OrcidEntityType.FUNDING; +import static org.dspace.orcid.model.OrcidEntityType.PRODUCT; import static org.dspace.orcid.model.OrcidEntityType.PUBLICATION; import java.util.List; @@ -60,6 +61,7 @@ public ResearcherProfileRest convert(ResearcherProfile profile, Projection proje orcidSynchronization.setProfilePreferences(getProfilePreferences(item)); orcidSynchronization.setFundingsPreference(getFundingsPreference(item)); orcidSynchronization.setPublicationsPreference(getPublicationsPreference(item)); + orcidSynchronization.setProductsPreference(getProductsPreference(item)); researcherProfileRest.setOrcidSynchronization(orcidSynchronization); } @@ -72,6 +74,13 @@ private String getPublicationsPreference(Item item) { .orElse(OrcidEntitySyncPreference.DISABLED.name()); } + private String getProductsPreference(Item item) { + return orcidSynchronizationService.getEntityPreference(item, PRODUCT) + .map(OrcidEntitySyncPreference::name) + .orElse(OrcidEntitySyncPreference.DISABLED.name()); + } + + private String getFundingsPreference(Item item) { return orcidSynchronizationService.getEntityPreference(item, FUNDING) .map(OrcidEntitySyncPreference::name) @@ -95,4 +104,4 @@ public Class getModelClass() { return ResearcherProfile.class; } -} \ No newline at end of file +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java index 4224cfeeb924..3c5b74519af3 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ResearcherProfileRest.java @@ -91,6 +91,8 @@ public static class OrcidSynchronizationRest { private String publicationsPreference; + private String productsPreference; + private String fundingsPreference; private List profilePreferences; @@ -119,6 +121,14 @@ public void setPublicationsPreference(String publicationsPreference) { this.publicationsPreference = publicationsPreference; } + public String getProductsPreference() { + return productsPreference; + } + + public void setProductsPreference(String productsPreference) { + this.productsPreference = productsPreference; + } + public String getFundingsPreference() { return fundingsPreference; } @@ -129,4 +139,4 @@ public void setFundingsPreference(String fundingsPreference) { } -} \ No newline at end of file +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileReplaceOrcidSyncPreferencesOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileReplaceOrcidSyncPreferencesOperation.java index 5084931382a5..ca65ca83d420 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileReplaceOrcidSyncPreferencesOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ResearcherProfileReplaceOrcidSyncPreferencesOperation.java @@ -8,6 +8,7 @@ package org.dspace.app.rest.repository.patch.operation; import static org.dspace.orcid.model.OrcidEntityType.FUNDING; +import static org.dspace.orcid.model.OrcidEntityType.PRODUCT; import static org.dspace.orcid.model.OrcidEntityType.PUBLICATION; import java.sql.SQLException; @@ -54,6 +55,8 @@ public class ResearcherProfileReplaceOrcidSyncPreferencesOperation extends Patch private static final String PUBLICATIONS_PREFERENCES = "/publications"; + private static final String PRODUCTS_PREFERENCES = "/products"; + private static final String FUNDINGS_PREFERENCES = "/fundings"; private static final String PROFILE_PREFERENCES = "/profile"; @@ -121,6 +124,9 @@ private boolean updatePreferences(Context context, String path, String value, It case PUBLICATIONS_PREFERENCES: OrcidEntitySyncPreference preference = parsePreference(value); return synchronizationService.setEntityPreference(context, profileItem, PUBLICATION, preference); + case PRODUCTS_PREFERENCES: + OrcidEntitySyncPreference productPreference = parsePreference(value); + return synchronizationService.setEntityPreference(context, profileItem, PRODUCT, productPreference); case FUNDINGS_PREFERENCES: OrcidEntitySyncPreference fundingPreference = parsePreference(value); return synchronizationService.setEntityPreference(context, profileItem, FUNDING, fundingPreference); @@ -137,9 +143,16 @@ private boolean updatePreferences(Context context, String path, String value, It private void reloadOrcidQueue(Context context, String path, String value, Item profileItem) throws SQLException, AuthorizeException { - if (path.equals(PUBLICATIONS_PREFERENCES) || path.equals(FUNDINGS_PREFERENCES)) { + if (path.equals(PUBLICATIONS_PREFERENCES) || path.equals(FUNDINGS_PREFERENCES) + || path.equals(PRODUCTS_PREFERENCES)) { OrcidEntitySyncPreference preference = parsePreference(value); - OrcidEntityType entityType = path.equals(PUBLICATIONS_PREFERENCES) ? PUBLICATION : FUNDING; + OrcidEntityType entityType = FUNDING; + if (path.equals(PUBLICATIONS_PREFERENCES)) { + entityType = PUBLICATION; + } + if (path.equals(PRODUCTS_PREFERENCES)) { + entityType = PRODUCT; + } orcidQueueService.recalculateOrcidQueue(context, profileItem, entityType, preference); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java index f3877445895a..2d6cead4fabb 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemRestRepositoryIT.java @@ -4166,6 +4166,94 @@ public void testDeletionOfFundingToBeSynchronizedWithOrcid() throws Exception { } + @Test + public void testDeletionOfProductToBeSynchronizedWithOrcid() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection profileCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Profiles") + .withEntityType("Person") + .build(); + + Collection productCollection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Products") + .withEntityType("Product") + .build(); + + EPerson firstOwner = EPersonBuilder.createEPerson(context) + .withEmail("owner2@test.com") + .build(); + + EPerson secondOwner = EPersonBuilder.createEPerson(context) + .withEmail("owner3@test.com") + .build(); + + EPerson thirdOwner = EPersonBuilder.createEPerson(context) + .withEmail("owner1@test.com") + .build(); + + Item firstProfile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withDspaceObjectOwner(firstOwner.getFullName(), firstOwner.getID().toString()) + .withOrcidIdentifier("0000-1111-2222-3333") + .withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", firstOwner) + .withOrcidSynchronizationProductsPreference(ALL) + .build(); + + Item secondProfile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withDspaceObjectOwner(secondOwner.getFullName(), secondOwner.getID().toString()) + .withOrcidIdentifier("4444-1111-2222-3333") + .withOrcidAccessToken("bb4d18a0-8d9a-40f1-b601-a417255c8d20", secondOwner) + .build(); + + Item thirdProfile = ItemBuilder.createItem(context, profileCollection) + .withTitle("Test User") + .withDspaceObjectOwner(thirdOwner.getFullName(), thirdOwner.getID().toString()) + .withOrcidIdentifier("5555-1111-2222-3333") + .withOrcidAccessToken("cb4d18a0-8d9a-40f1-b601-a417255c8d20", thirdOwner) + .withOrcidSynchronizationProductsPreference(ALL) + .build(); + + Item product = ItemBuilder.createItem(context, productCollection) + .withTitle("Test product") + + .build(); + + createOrcidQueue(context, firstProfile, product).build(); + createOrcidQueue(context, secondProfile, product).build(); + + List historyRecords = new ArrayList<>(); + historyRecords.add(createOrcidHistory(context, firstProfile, product).build()); + historyRecords.add(createOrcidHistory(context, firstProfile, product).withPutCode("12345").build()); + historyRecords.add(createOrcidHistory(context, secondProfile, product).build()); + historyRecords.add(createOrcidHistory(context, secondProfile, product).withPutCode("67891").build()); + historyRecords.add(createOrcidHistory(context, thirdProfile, product).build()); + + context.restoreAuthSystemState(); + + String token = getAuthToken(admin.getEmail(), password); + + getClient(token).perform(delete("/api/core/items/" + product.getID())) + .andExpect(status().is(204)); + + List orcidQueueRecords = orcidQueueService.findAll(context); + assertThat(orcidQueueRecords, hasSize(1)); + assertThat(orcidQueueRecords, hasItem(matches(firstProfile, null, "Product", "12345", DELETE))); + + for (OrcidHistory historyRecord : historyRecords) { + historyRecord = context.reloadEntity(historyRecord); + assertThat(historyRecord, notNullValue()); + assertThat(historyRecord.getEntity(), nullValue()); + } + + } + private void initPublicationAuthorsRelationships() throws SQLException { context.turnOffAuthorisationSystem(); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java index 092b4e47479a..b0328b3a73dd 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ResearcherProfileRestRepositoryIT.java @@ -1171,6 +1171,7 @@ public void testOrcidMetadataOfEpersonAreCopiedOnProfile() throws Exception { .andExpect(jsonPath("$.orcidSynchronization.mode", is("MANUAL"))) .andExpect(jsonPath("$.orcidSynchronization.publicationsPreference", is("DISABLED"))) .andExpect(jsonPath("$.orcidSynchronization.fundingsPreference", is("DISABLED"))) + .andExpect(jsonPath("$.orcidSynchronization.productsPreference", is("DISABLED"))) .andExpect(jsonPath("$.orcidSynchronization.profilePreferences", empty())); String itemId = getItemIdByProfileId(authToken, ePersonId); @@ -1307,6 +1308,65 @@ public void testPatchToSetOrcidSynchronizationPreferenceForFundings() throws Exc } + @Test + public void testPatchToSetOrcidSynchronizationPreferenceForProduct() throws Exception { + + context.turnOffAuthorisationSystem(); + + EPerson ePerson = EPersonBuilder.createEPerson(context) + .withCanLogin(true) + .withOrcid("0000-1111-2222-3333") + .withEmail("test@email.it") + .withPassword(password) + .withNameInMetadata("Test", "User") + .withOrcidScope("/first-scope") + .withOrcidScope("/second-scope") + .build(); + + OrcidTokenBuilder.create(context, ePerson, "af097328-ac1c-4a3e-9eb4-069897874910").build(); + + context.restoreAuthSystemState(); + + String ePersonId = ePerson.getID().toString(); + String authToken = getAuthToken(ePerson.getEmail(), password); + + getClient(authToken).perform(post("/api/eperson/profiles/") + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + + List operations = asList(new ReplaceOperation("/orcid/products", ALL.name())); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.orcidSynchronization.productsPreference", is(ALL.name()))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.orcidSynchronization.productsPreference", is(ALL.name()))); + + operations = asList(new ReplaceOperation("/orcid/products", MINE.name())); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.orcidSynchronization.productsPreference", is(MINE.name()))); + + getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.orcidSynchronization.productsPreference", is(MINE.name()))); + + operations = asList(new ReplaceOperation("/orcid/products", "INVALID_VALUE")); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId) + .content(getPatchContent(operations)) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isUnprocessableEntity()); + + } + @Test public void testPatchToSetOrcidSynchronizationPreferenceForProfile() throws Exception { @@ -2144,8 +2204,12 @@ public void testOrcidSynchronizationPreferenceUpdateForceOrcidQueueRecalculation Collection publications = createCollection("Publications", "Publication"); + Collection products = createCollection("Products", "Product"); + Item publication = createPublication(publications, "Test publication", profile); + Item product = createProduct(products, "Test product", profile); + Collection fundings = createCollection("Fundings", "Funding"); Item firstFunding = createFundingWithInvestigator(fundings, "First funding", profile); @@ -2178,21 +2242,43 @@ public void testOrcidSynchronizationPreferenceUpdateForceOrcidQueueRecalculation assertThat(queueRecords, has(orcidQueueRecordWithEntity(firstFunding))); assertThat(queueRecords, has(orcidQueueRecordWithEntity(secondFunding))); + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId.toString()) + .content(getPatchContent(asList(new ReplaceOperation("/orcid/products", "ALL")))) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()); + + queueRecords = orcidQueueService.findByProfileItemId(context, profileItemId); + assertThat(queueRecords, hasSize(4)); + assertThat(queueRecords, has(orcidQueueRecordWithEntity(publication))); + assertThat(queueRecords, has(orcidQueueRecordWithEntity(firstFunding))); + assertThat(queueRecords, has(orcidQueueRecordWithEntity(secondFunding))); + assertThat(queueRecords, has(orcidQueueRecordWithEntity(product))); + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId.toString()) .content(getPatchContent(asList(new ReplaceOperation("/orcid/publications", "DISABLED")))) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isOk()); queueRecords = orcidQueueService.findByProfileItemId(context, profileItemId); - assertThat(queueRecords, hasSize(2)); + assertThat(queueRecords, hasSize(3)); assertThat(queueRecords, has(orcidQueueRecordWithEntity(firstFunding))); assertThat(queueRecords, has(orcidQueueRecordWithEntity(secondFunding))); + assertThat(queueRecords, has(orcidQueueRecordWithEntity(product))); getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId.toString()) .content(getPatchContent(asList(new ReplaceOperation("/orcid/fundings", "DISABLED")))) .contentType(MediaType.APPLICATION_JSON_VALUE)) .andExpect(status().isOk()); + queueRecords = orcidQueueService.findByProfileItemId(context, profileItemId); + assertThat(queueRecords, hasSize(1)); + assertThat(queueRecords, has(orcidQueueRecordWithEntity(product))); + + getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId.toString()) + .content(getPatchContent(asList(new ReplaceOperation("/orcid/products", "DISABLED")))) + .contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()); + assertThat(orcidQueueService.findByProfileItemId(context, profileItemId), empty()); configurationService.setProperty("orcid.linkable-metadata-fields.ignore", "crisfund.coinvestigators"); @@ -2615,6 +2701,13 @@ private Item createPublication(Collection collection, String title, Item author) .build(); } + private Item createProduct(Collection collection, String title, Item author) { + return ItemBuilder.createItem(context, collection) + .withTitle(title) + .withAuthor(author.getName(), author.getID().toString()) + .build(); + } + private Item createFundingWithInvestigator(Collection collection, String title, Item investigator) { return ItemBuilder.createItem(context, collection) .withTitle(title) diff --git a/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-product-type.properties b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-product-type.properties new file mode 100644 index 000000000000..697ff74646bc --- /dev/null +++ b/dspace/config/crosswalks/orcid/mapConverter-dspace-to-orcid-product-type.properties @@ -0,0 +1,20 @@ +# Mapping between DSpace common product's types and the type supported by ORCID +http\://purl.org/coar/resource_type/c_ddb1 = data-set +http\://purl.org/coar/resource_type/NHD0-W6SY = data-set +http\://purl.org/coar/resource_type/W2XT-7017 = data-set +http\://purl.org/coar/resource_type/CQMR-7K63 = data-set +http\://purl.org/coar/resource_type/FF4C-28RK = data-set +http\://purl.org/coar/resource_type/DD58-GFSX = data-set +http\://purl.org/coar/resource_type/H41Y-FW7B = data-set +http\://purl.org/coar/resource_type/2H0M-X761 = data-set +http\://purl.org/coar/resource_type/A8F1-NPV9 = data-set +http\://purl.org/coar/resource_type/AM6W-6QAW = data-set +http\://purl.org/coar/resource_type/FXF3-D3G7 = data-set +http\://purl.org/coar/resource_type/c_cb28 = data-set +http\://purl.org/coar/resource_type/ACF7-8YT9 = data-set +http\://purl.org/coar/resource_type/c_5ce6 = software +http\://purl.org/coar/resource_type/c_18cc = lecture-speech +http\://purl.org/coar/resource_type/c_18cd = lecture-speech +http\://purl.org/coar/resource_type/c_7ad9 = website +http\://purl.org/coar/resource_type/c_e9a0 = online-resource +http\://purl.org/coar/resource_type/H6QP-SC1X = trademark diff --git a/dspace/config/modules/orcid.cfg b/dspace/config/modules/orcid.cfg index 4bce1fe38f96..b7790f40fde0 100644 --- a/dspace/config/modules/orcid.cfg +++ b/dspace/config/modules/orcid.cfg @@ -86,6 +86,20 @@ orcid.mapping.work.funding.external-id.value = dc.relation.grantno orcid.mapping.work.funding.external-id.entity-value = oairecerif.funding.identifier orcid.mapping.work.funding.url = crisfund.award.url +### Work (Product) mapping ### +# where is differs from publication mapping above +# see orcid-services.xml for properties being changed here! +# https://info.orcid.org/documentation/integration-and-api-faq/#easy-faq-2682 +# https://info.orcid.org/faq/what-contributor-information-should-i-include-when-adding-works-or-funding-items/ + +# aligned to default submittion form for "product" +orcid.mapping.work.product.contributors = dc.contributor.author::author + +# Additional Mapping to CRediT roles https://credit.niso.org/ possible +# The roles are not part of the current used orcid model and thus it is not possible to configure the NISO-roles + +orcid.mapping.work.product.type.converter = mapConverterDSpaceToOrcidProductType + ### Funding mapping ### orcid.mapping.funding.title = dc.title orcid.mapping.funding.type = dc.type diff --git a/dspace/config/registries/dspace-types.xml b/dspace/config/registries/dspace-types.xml index 861dc67a816a..52665475cf77 100644 --- a/dspace/config/registries/dspace-types.xml +++ b/dspace/config/registries/dspace-types.xml @@ -87,6 +87,13 @@ Stores the publication synchronization with ORCID preference chosen by the user + + dspace + orcid + sync-products + Stores the product synchronization with ORCID preference chosen by the user + + dspace orcid diff --git a/dspace/config/spring/api/orcid-services.xml b/dspace/config/spring/api/orcid-services.xml index eb4e20a459c2..c47d20b96b11 100644 --- a/dspace/config/spring/api/orcid-services.xml +++ b/dspace/config/spring/api/orcid-services.xml @@ -46,6 +46,9 @@ + + + @@ -77,6 +80,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -171,6 +200,12 @@ + + + + + +