[CST-5588] ORCID settings (REST) .

This commit is contained in:
eskander
2022-04-13 13:30:07 +02:00
parent 035940018f
commit 1c5e89d4a4
12 changed files with 974 additions and 13 deletions

View File

@@ -0,0 +1,40 @@
/**
* 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.app.orcid.model;
import org.apache.commons.lang3.EnumUtils;
/**
* The entity types of the ORCID objects that can be synchronized.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidEntityType {
PUBLICATION("/work"),
FUNDING("/funding");
private final String path;
private OrcidEntityType(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public static boolean isValid(String entityType) {
return entityType != null ? EnumUtils.isValidEnum(OrcidEntityType.class, entityType.toUpperCase()) : false;
}
public static OrcidEntityType fromString(String entityType) {
return isValid(entityType) ? OrcidEntityType.valueOf(entityType.toUpperCase()) : null;
}
}

View File

@@ -8,9 +8,15 @@
package org.dspace.app.orcid.service;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import org.dspace.app.orcid.model.OrcidEntityType;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.dspace.app.profile.OrcidEntitySyncPreference;
import org.dspace.app.profile.OrcidProfileDisconnectionMode;
import org.dspace.app.profile.OrcidProfileSyncPreference;
import org.dspace.app.profile.OrcidSynchronizationMode;
import org.dspace.content.Item;
import org.dspace.core.Context;
@@ -22,6 +28,14 @@ import org.dspace.core.Context;
*/
public interface OrcidSynchronizationService {
/**
* Check if the given item is linked to an ORCID profile.
*
* @param item the item to check
* @return true if the given item is linked to ORCID
*/
boolean isLinkedToOrcid(Item item);
/**
* Configure the given profile with the data present in the given ORCID token.
* This action is required to synchronize profile and related entities with
@@ -43,6 +57,83 @@ public interface OrcidSynchronizationService {
*/
public void unlinkProfile(Context context, Item profile) throws SQLException;
/**
* Set the synchronization preference for the given profile related to the given
* ORCID entity type.
*
* @param context the relevant DSpace Context.
* @param profile the researcher profile to update
* @param entityType the orcid entity type
* @param value the new synchronization preference value
* @return true if the value has actually been updated,
* false if the value to be set is the same as
* the one already configured
* @throws SQLException if a SQL error occurs during the profile
* update
* @throws IllegalArgumentException if the given researcher profile is no linked
* with an ORCID account
*/
public boolean setEntityPreference(Context context, Item profile, OrcidEntityType entityType,
OrcidEntitySyncPreference value) throws SQLException;
/**
* Update the profile's synchronization preference for the given profile.
*
* @param context the relevant DSpace Context.
* @param profile the researcher profile to update
* @param value the new synchronization preference value
* @return true if the value has actually been updated,
* false if the value to be set is the same as
* the one already configured
* @throws SQLException if a SQL error occurs during the profile
* update
* @throws IllegalArgumentException if the given researcher profile is no linked
* with an ORCID account
*/
public boolean setProfilePreference(Context context, Item profile,
List<OrcidProfileSyncPreference> values) throws SQLException;
/**
* Set the ORCID synchronization mode for the given profile.
*
* @param context the relevant DSpace Context.
* @param profile the researcher profile to update
* @param value the new synchronization mode value
* @return true if the value has actually been updated, false if
* the value to be set is the same as the one already
* configured
* @throws SQLException if a SQL error occurs during the profile update
*/
public boolean setSynchronizationMode(Context context, Item profile, OrcidSynchronizationMode value)
throws SQLException;
/**
* Returns the ORCID synchronization mode configured for the given profile item.
*
* @param profile the researcher profile item
* @return the synchronization mode
*/
Optional<OrcidSynchronizationMode> getSynchronizationMode(Item profile);
/**
* Returns the ORCID synchronization preference related to the given entity type
* configured for the given profile item.
*
* @param profile the researcher profile item
* @param entityType the orcid entity type
* @return the configured preference
*/
Optional<OrcidEntitySyncPreference> getEntityPreference(Item profile, OrcidEntityType entityType);
/**
* Returns the ORCID synchronization preferences related to the profile itself
* configured for the given profile item.
*
* @param profile the researcher profile item
* @return the synchronization mode
*/
List<OrcidProfileSyncPreference> getProfilePreferences(Item profile);
/**
* Returns the configuration ORCID profile's disconnection mode. If that mode is
* not configured or the configuration is wrong, the value DISABLED is returned.

View File

@@ -9,14 +9,26 @@ package org.dspace.app.orcid.service.impl;
import static java.time.LocalDateTime.now;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import static java.util.List.of;
import static org.apache.commons.lang3.EnumUtils.isValidEnum;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.dspace.content.Item.ANY;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.codec.binary.StringUtils;
import org.dspace.app.orcid.model.OrcidEntityType;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.profile.OrcidEntitySyncPreference;
import org.dspace.app.profile.OrcidProfileDisconnectionMode;
import org.dspace.app.profile.OrcidProfileSyncPreference;
import org.dspace.app.profile.OrcidSynchronizationMode;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
@@ -88,6 +100,77 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
}
@Override
public boolean setEntityPreference(Context context, Item profile, OrcidEntityType type,
OrcidEntitySyncPreference value) throws SQLException {
String metadataQualifier = "sync-" + type.name().toLowerCase() + "s";
return updatePreferenceForSynchronizingWithOrcid(context, profile, metadataQualifier, of(value.name()));
}
@Override
public boolean setProfilePreference(Context context, Item profile, List<OrcidProfileSyncPreference> values)
throws SQLException {
List<String> valuesAsString = values.stream()
.map(OrcidProfileSyncPreference::name)
.collect(Collectors.toList());
return updatePreferenceForSynchronizingWithOrcid(context, profile, "sync-profile", valuesAsString);
}
@Override
public boolean setSynchronizationMode(Context context, Item profile, OrcidSynchronizationMode value)
throws SQLException {
if (!isLinkedToOrcid(profile)) {
throw new IllegalArgumentException("The given profile cannot be configured for the ORCID "
+ "synchronization because it is not linked to any ORCID account: "
+ profile.getID());
}
String newValue = value.name();
String oldValue = itemService.getMetadataFirstValue(profile, "dspace", "orcid", "sync-mode", Item.ANY);
if (StringUtils.equals(oldValue, newValue)) {
return false;
} else {
itemService.setMetadataSingleValue(context, profile, "dspace", "orcid", "sync-mode", null, value.name());
return true;
}
}
@Override
public Optional<OrcidSynchronizationMode> getSynchronizationMode(Item item) {
return getMetadataValue(item, "dspace.orcid.sync-mode")
.map(metadataValue -> metadataValue.getValue())
.filter(value -> isValidEnum(OrcidSynchronizationMode.class, value))
.map(value -> OrcidSynchronizationMode.valueOf(value));
}
@Override
public Optional<OrcidEntitySyncPreference> getEntityPreference(Item item, OrcidEntityType entityType) {
return getMetadataValue(item, "dspace.orcid.sync-" + entityType.name().toLowerCase() + "s")
.map(metadataValue -> metadataValue.getValue())
.filter(value -> isValidEnum(OrcidEntitySyncPreference.class, value))
.map(value -> OrcidEntitySyncPreference.valueOf(value));
}
@Override
public List<OrcidProfileSyncPreference> getProfilePreferences(Item item) {
return getMetadataValues(item, "dspace.orcid.sync-profile")
.map(MetadataValue::getValue)
.filter(value -> isValidEnum(OrcidProfileSyncPreference.class, value))
.map(value -> OrcidProfileSyncPreference.valueOf(value))
.collect(Collectors.toList());
}
@Override
public boolean isLinkedToOrcid(Item item) {
return getOrcidAccessToken(item).isPresent() && getOrcid(item).isPresent();
}
@Override
public OrcidProfileDisconnectionMode getDisconnectionMode() {
String value = configurationService.getProperty("orcid.disconnection.allowed-users");
@@ -97,9 +180,58 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
return OrcidProfileDisconnectionMode.fromString(value);
}
private boolean updatePreferenceForSynchronizingWithOrcid(Context context, Item profile,
String metadataQualifier,
List<String> values) throws SQLException {
if (!isLinkedToOrcid(profile)) {
throw new IllegalArgumentException("The given profile cannot be configured for the ORCID "
+ "synchronization because it is not linked to any ORCID account: "
+ profile.getID());
}
List<String> oldValues = itemService.getMetadata(profile, "dspace", "orcid", metadataQualifier, ANY).stream()
.map(metadataValue -> metadataValue.getValue())
.collect(Collectors.toList());
if (containsSameValues(oldValues, values)) {
return false;
}
itemService.clearMetadata(context, profile, "dspace", "orcid", metadataQualifier, ANY);
for (String value : values) {
itemService.addMetadata(context, profile, "dspace", "orcid", metadataQualifier, null, value);
}
return true;
}
private boolean containsSameValues(List<String> firstList, List<String> secondList) {
return new HashSet<>(firstList).equals(new HashSet<>(secondList));
}
private Optional<String> getOrcidAccessToken(Item item) {
return getMetadataValue(item, "dspace.orcid.access-token")
.map(metadataValue -> metadataValue.getValue());
}
public Optional<String> getOrcid(Item item) {
return getMetadataValue(item, "person.identifier.orcid")
.map(metadataValue -> metadataValue.getValue());
}
private Optional<MetadataValue> getMetadataValue(Item item, String metadataField) {
return getMetadataValues(item, metadataField).findFirst();
}
private Stream<MetadataValue> getMetadataValues(Item item, String metadataField) {
return item.getMetadata().stream()
.filter(metadata -> metadataField.equals(metadata.getMetadataField().toString('.')));
.filter(metadata -> metadataField.equals(metadata.getMetadataField().toString('.')));
}
private String getProfileType() {
return configurationService.getProperty("researcher-profile.type", "Person");
}
private void updateItem(Context context, Item item) throws SQLException {

View File

@@ -0,0 +1,21 @@
/**
* 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.app.profile;
/**
* Enum that model the allowed values to configure the ORCID synchronization
* preferences.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidEntitySyncPreference {
DISABLED,
ALL
}

View File

@@ -0,0 +1,21 @@
/**
* 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.app.profile;
/**
* Enum that model the allowed values to configure the ORCID synchronization
* preferences for the user's profile.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidProfileSyncPreference {
BIOGRAPHICAL,
IDENTIFIERS;
}

View File

@@ -0,0 +1,21 @@
/**
* 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.app.profile;
/**
* Enum that model the allowed values to configure the ORCID synchronization
* mode.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidSynchronizationMode {
MANUAL,
BATCH;
}

View File

@@ -62,6 +62,11 @@ public class ResearcherProfile {
return item;
}
public Optional<String> getOrcid() {
return getMetadataValue(item, "person.identifier.orcid")
.map(metadataValue -> metadataValue.getValue());
}
private MetadataValue getDspaceObjectOwnerMetadata(Item item) {
return getMetadataValue(item, "dspace.object.owner")
.filter(metadata -> UUIDUtils.fromString(metadata.getAuthority()) != null)

View File

@@ -7,10 +7,22 @@
*/
package org.dspace.app.rest.converter;
import static org.dspace.app.orcid.model.OrcidEntityType.FUNDING;
import static org.dspace.app.orcid.model.OrcidEntityType.PUBLICATION;
import java.util.List;
import java.util.stream.Collectors;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.profile.OrcidEntitySyncPreference;
import org.dspace.app.profile.OrcidProfileSyncPreference;
import org.dspace.app.profile.OrcidSynchronizationMode;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.rest.model.ResearcherProfileRest;
import org.dspace.app.rest.model.ResearcherProfileRest.OrcidSynchronizationRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
@@ -23,6 +35,9 @@ import org.springframework.stereotype.Component;
@Component
public class ResearcherProfileConverter implements DSpaceConverter<ResearcherProfile, ResearcherProfileRest> {
@Autowired
private OrcidSynchronizationService orcidSynchronizationService;
@Override
public ResearcherProfileRest convert(ResearcherProfile profile, Projection projection) {
ResearcherProfileRest researcherProfileRest = new ResearcherProfileRest();
@@ -33,12 +48,47 @@ public class ResearcherProfileConverter implements DSpaceConverter<ResearcherPro
Item item = profile.getItem();
if (orcidSynchronizationService.isLinkedToOrcid(item)) {
profile.getOrcid().ifPresent(researcherProfileRest::setOrcid);
OrcidSynchronizationRest orcidSynchronization = new OrcidSynchronizationRest();
orcidSynchronization.setMode(getMode(item));
orcidSynchronization.setProfilePreferences(getProfilePreferences(item));
orcidSynchronization.setFundingsPreference(getFundingsPreference(item));
orcidSynchronization.setPublicationsPreference(getPublicationsPreference(item));
researcherProfileRest.setOrcidSynchronization(orcidSynchronization);
}
return researcherProfileRest;
}
private String getPublicationsPreference(Item item) {
return orcidSynchronizationService.getEntityPreference(item, PUBLICATION)
.map(OrcidEntitySyncPreference::name)
.orElse(OrcidEntitySyncPreference.DISABLED.name());
}
private String getFundingsPreference(Item item) {
return orcidSynchronizationService.getEntityPreference(item, FUNDING)
.map(OrcidEntitySyncPreference::name)
.orElse(OrcidEntitySyncPreference.DISABLED.name());
}
private List<String> getProfilePreferences(Item item) {
return orcidSynchronizationService.getProfilePreferences(item).stream()
.map(OrcidProfileSyncPreference::name)
.collect(Collectors.toList());
}
private String getMode(Item item) {
return orcidSynchronizationService.getSynchronizationMode(item)
.map(OrcidSynchronizationMode::name)
.orElse(OrcidSynchronizationMode.MANUAL.name());
}
@Override
public Class<ResearcherProfile> getModelClass() {
return ResearcherProfile.class;
}
}
}

View File

@@ -7,8 +7,10 @@
*/
package org.dspace.app.rest.model;
import java.util.List;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.dspace.app.rest.RestResourceController;
/**
@@ -32,6 +34,12 @@ public class ResearcherProfileRest extends BaseObjectRest<UUID> {
private boolean visible;
@JsonInclude(JsonInclude.Include.NON_NULL)
private String orcid;
@JsonInclude(JsonInclude.Include.NON_NULL)
private OrcidSynchronizationRest orcidSynchronization;
public boolean isVisible() {
return visible;
}
@@ -40,6 +48,22 @@ public class ResearcherProfileRest extends BaseObjectRest<UUID> {
this.visible = visible;
}
public OrcidSynchronizationRest getOrcidSynchronization() {
return orcidSynchronization;
}
public void setOrcidSynchronization(OrcidSynchronizationRest orcidSynchronization) {
this.orcidSynchronization = orcidSynchronization;
}
public String getOrcid() {
return orcid;
}
public void setOrcid(String orcid) {
this.orcid = orcid;
}
@Override
public String getType() {
return NAME;
@@ -54,4 +78,55 @@ public class ResearcherProfileRest extends BaseObjectRest<UUID> {
public Class<?> getController() {
return RestResourceController.class;
}
}
/**
* Inner class to model ORCID synchronization preferences and mode.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public static class OrcidSynchronizationRest {
private String mode;
private String publicationsPreference;
private String fundingsPreference;
private List<String> profilePreferences;
public String getMode() {
return mode;
}
public void setMode(String mode) {
this.mode = mode;
}
public List<String> getProfilePreferences() {
return profilePreferences;
}
public void setProfilePreferences(List<String> profilePreferences) {
this.profilePreferences = profilePreferences;
}
public String getPublicationsPreference() {
return publicationsPreference;
}
public void setPublicationsPreference(String publicationsPreference) {
this.publicationsPreference = publicationsPreference;
}
public String getFundingsPreference() {
return fundingsPreference;
}
public void setFundingsPreference(String fundingsPreference) {
this.fundingsPreference = fundingsPreference;
}
}
}

View File

@@ -0,0 +1,156 @@
/**
* 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.app.rest.repository.patch.operation;
import static org.dspace.app.orcid.model.OrcidEntityType.FUNDING;
import static org.dspace.app.orcid.model.OrcidEntityType.PUBLICATION;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.profile.OrcidEntitySyncPreference;
import org.dspace.app.profile.OrcidProfileSyncPreference;
import org.dspace.app.profile.OrcidSynchronizationMode;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.exception.RESTAuthorizationException;
import org.dspace.app.rest.exception.UnprocessableEntityException;
import org.dspace.app.rest.model.patch.Operation;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Implementation for ResearcherProfile ORCID synchronization preferences
* patches.
*
* Example:
* <code> curl -X PATCH http://${dspace.server.url}/api/cris/profiles/<:id-eperson> -H "
* Content-Type: application/json" -d '[{
* "op": "replace",
* "path": "/orcid/publications",
* "value": "ALL"
* }]'
* </code>
*/
@Component
public class ResearcherProfileReplaceOrcidSyncPreferencesOperation extends PatchOperation<ResearcherProfile> {
private static final String OPERATION_ORCID_SYNCH = "/orcid";
private static final String PUBLICATIONS_PREFERENCES = "/publications";
private static final String FUNDINGS_PREFERENCES = "/fundings";
private static final String PROFILE_PREFERENCES = "/profile";
private static final String MODE_PREFERENCES = "/mode";
@Autowired
private ResearcherProfileService profileService;
@Autowired
private OrcidSynchronizationService synchronizationService;
@Override
public ResearcherProfile perform(Context context, ResearcherProfile profile, Operation operation)
throws SQLException {
String path = StringUtils.removeStart(operation.getPath(), OPERATION_ORCID_SYNCH);
String value = getNewValueFromOperation(operation);
Item profileItem = profile.getItem();
context.turnOffAuthorisationSystem();
try {
updatePreferences(context, path, value, profileItem);
return profileService.findById(context, profile.getId());
} catch (AuthorizeException e) {
throw new RESTAuthorizationException(e);
} finally {
context.restoreAuthSystemState();
}
}
private String getNewValueFromOperation(Operation operation) {
Object valueObject = operation.getValue();
if (valueObject == null | !(valueObject instanceof String)) {
throw new UnprocessableEntityException("The /orcid value must be a string");
}
return (String) valueObject;
}
private boolean updatePreferences(Context context, String path, String value, Item profileItem)
throws SQLException {
switch (path) {
case PUBLICATIONS_PREFERENCES:
OrcidEntitySyncPreference preference = parsePreference(value);
return synchronizationService.setEntityPreference(context, profileItem, PUBLICATION, preference);
case FUNDINGS_PREFERENCES:
OrcidEntitySyncPreference fundingPreference = parsePreference(value);
return synchronizationService.setEntityPreference(context, profileItem, FUNDING, fundingPreference);
case PROFILE_PREFERENCES:
List<OrcidProfileSyncPreference> profilePreferences = parseProfilePreferences(value);
return synchronizationService.setProfilePreference(context, profileItem, profilePreferences);
case MODE_PREFERENCES:
return synchronizationService.setSynchronizationMode(context, profileItem, parseMode(value));
default:
throw new UnprocessableEntityException("Invalid path starting with " + OPERATION_ORCID_SYNCH);
}
}
@Override
public boolean supports(Object objectToMatch, Operation operation) {
return objectToMatch instanceof ResearcherProfile
&& operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE)
&& operation.getPath().trim().toLowerCase().startsWith(OPERATION_ORCID_SYNCH);
}
private List<OrcidProfileSyncPreference> parseProfilePreferences(String value) {
return Arrays.stream(value.split(","))
.map(String::trim)
.filter(StringUtils::isNotEmpty)
.map(this::parseProfilePreference)
.collect(Collectors.toList());
}
private OrcidProfileSyncPreference parseProfilePreference(String value) {
try {
return OrcidProfileSyncPreference.valueOf(value.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new UnprocessableEntityException("Invalid profile's synchronization preference value: " + value, ex);
}
}
private OrcidSynchronizationMode parseMode(String value) {
try {
return OrcidSynchronizationMode.valueOf(value.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new UnprocessableEntityException("Invalid synchronization mode value: " + value, ex);
}
}
private OrcidEntitySyncPreference parsePreference(String value) {
try {
return OrcidEntitySyncPreference.valueOf(value.toUpperCase());
} catch (IllegalArgumentException ex) {
throw new UnprocessableEntityException("Invalid synchronization preference value: " + value, ex);
}
}
}

View File

@@ -11,14 +11,19 @@ import static com.jayway.jsonpath.JsonPath.read;
import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
import static java.util.Arrays.asList;
import static java.util.UUID.fromString;
import static org.dspace.app.matcher.MetadataValueMatcher.with;
import static org.dspace.app.profile.OrcidEntitySyncPreference.ALL;
import static org.dspace.app.rest.matcher.HalMatcher.matchLinks;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadata;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataDoesNotExist;
import static org.dspace.app.rest.matcher.MetadataMatcher.matchMetadataNotEmpty;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertEquals;
import static org.springframework.data.rest.webmvc.RestMediaTypes.TEXT_URI_LIST;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
@@ -52,6 +57,7 @@ import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.dspace.util.UUIDUtils;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
@@ -1030,19 +1036,318 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
}
@Test
public void testCloneFromExternalProfileAlreadyAssociated() throws Exception {
public void testOrcidMetadataOfEpersonAreCopiedOnProfile() throws Exception {
String id = user.getID().toString();
String authToken = getAuthToken(user.getEmail(), password);
context.turnOffAuthorisationSystem();
getClient(authToken).perform(post("/api/eperson/profiles/").contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id)))
.andExpect(jsonPath("$.visible", is(false))).andExpect(jsonPath("$.type", is("profile")));
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.withOrcidAccessToken("af097328-ac1c-4a3e-9eb4-069897874910")
.withOrcidRefreshToken("32aadae0-829e-49c5-824f-ccaf4d1913e4")
.withOrcidScope("/first-scope")
.withOrcidScope("/second-scope")
.build();
getClient(authToken)
.perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST)
.content("http://localhost:8080/server/api/core/items/" + id))
.andExpect(status().isConflict());
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())
.andExpect(jsonPath("$.id", is(ePersonId.toString())))
.andExpect(jsonPath("$.visible", is(false)))
.andExpect(jsonPath("$.type", is("profile")));
getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcid", is("0000-1111-2222-3333")))
.andExpect(jsonPath("$.orcidSynchronization.mode", is("MANUAL")))
.andExpect(jsonPath("$.orcidSynchronization.publicationsPreference", is("DISABLED")))
.andExpect(jsonPath("$.orcidSynchronization.fundingsPreference", is("DISABLED")))
.andExpect(jsonPath("$.orcidSynchronization.profilePreferences", empty()));
String itemId = getItemIdByProfileId(authToken, ePersonId);
Item profileItem = itemService.find(context, UUIDUtils.fromString(itemId));
assertThat(profileItem, notNullValue());
List<MetadataValue> metadata = profileItem.getMetadata();
assertThat(metadata, hasItem(with("person.identifier.orcid", "0000-1111-2222-3333")));
assertThat(metadata, hasItem(with("dspace.orcid.access-token", "af097328-ac1c-4a3e-9eb4-069897874910")));
assertThat(metadata, hasItem(with("dspace.orcid.refresh-token", "32aadae0-829e-49c5-824f-ccaf4d1913e4")));
assertThat(metadata, hasItem(with("dspace.orcid.scope", "/first-scope", 0)));
assertThat(metadata, hasItem(with("dspace.orcid.scope", "/second-scope", 1)));
}
@Test
public void testPatchToSetOrcidSynchronizationPreferenceForPublications() 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")
.withOrcidAccessToken("af097328-ac1c-4a3e-9eb4-069897874910")
.withOrcidRefreshToken("32aadae0-829e-49c5-824f-ccaf4d1913e4")
.withOrcidScope("/first-scope")
.withOrcidScope("/second-scope")
.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<Operation> operations = asList(new ReplaceOperation("/orcid/publications", 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.publicationsPreference", is(ALL.name())));
getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.publicationsPreference", is(ALL.name())));
operations = asList(new ReplaceOperation("/orcid/publications", "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 testPatchToSetOrcidSynchronizationPreferenceForFundings() 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")
.withOrcidAccessToken("af097328-ac1c-4a3e-9eb4-069897874910")
.withOrcidRefreshToken("32aadae0-829e-49c5-824f-ccaf4d1913e4")
.withOrcidScope("/first-scope")
.withOrcidScope("/second-scope")
.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<Operation> operations = asList(new ReplaceOperation("/orcid/fundings", 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.fundingsPreference", is(ALL.name())));
getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.fundingsPreference", is(ALL.name())));
operations = asList(new ReplaceOperation("/orcid/fundings", "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 {
context.turnOffAuthorisationSystem();
EPerson ePerson = EPersonBuilder.createEPerson(context)
.withCanLogin(true)
.withOrcid("0000-1111-2222-3333")
.withEmail("test@email.it")
.withPassword(password)
.withNameInMetadata("Test", "User")
.withOrcidAccessToken("af097328-ac1c-4a3e-9eb4-069897874910")
.withOrcidRefreshToken("32aadae0-829e-49c5-824f-ccaf4d1913e4")
.withOrcidScope("/first-scope")
.withOrcidScope("/second-scope")
.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<Operation> operations = asList(new ReplaceOperation("/orcid/profile", "IDENTIFIERS"));
getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId)
.content(getPatchContent(operations))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.profilePreferences",
containsInAnyOrder("IDENTIFIERS")));
getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.profilePreferences",
containsInAnyOrder("IDENTIFIERS")));
operations = asList(new ReplaceOperation("/orcid/profiles", "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 testPatchToSetOrcidSynchronizationMode() 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")
.withOrcidAccessToken("af097328-ac1c-4a3e-9eb4-069897874910")
.withOrcidRefreshToken("32aadae0-829e-49c5-824f-ccaf4d1913e4")
.withOrcidScope("/first-scope")
.withOrcidScope("/second-scope")
.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<Operation> operations = asList(new ReplaceOperation("/orcid/mode", "BATCH"));
getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId)
.content(getPatchContent(operations))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.mode", is("BATCH")));
getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.mode", is("BATCH")));
operations = asList(new ReplaceOperation("/orcid/mode", "MANUAL"));
getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId)
.content(getPatchContent(operations))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.mode", is("MANUAL")));
getClient(authToken).perform(get("/api/eperson/profiles/{id}", ePersonId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.orcidSynchronization.mode", is("MANUAL")));
operations = asList(new ReplaceOperation("/orcid/mode", "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 testPatchToSetOrcidSynchronizationPreferenceWithWrongPath() 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")
.withOrcidAccessToken("af097328-ac1c-4a3e-9eb4-069897874910")
.withOrcidRefreshToken("32aadae0-829e-49c5-824f-ccaf4d1913e4")
.withOrcidScope("/first-scope")
.withOrcidScope("/second-scope")
.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<Operation> operations = asList(new ReplaceOperation("/orcid/wrong-path", "BATCH"));
getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId)
.content(getPatchContent(operations))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isUnprocessableEntity());
}
@Test
public void testPatchToSetOrcidSynchronizationPreferenceWithProfileNotLinkedToOrcid() 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")
.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<Operation> operations = asList(new ReplaceOperation("/orcid/mode", "BATCH"));
getClient(authToken).perform(patch("/api/eperson/profiles/{id}", ePersonId)
.content(getPatchContent(operations))
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isBadRequest());
}
@Test
@@ -1528,6 +1833,22 @@ public class ResearcherProfileRestRepositoryIT extends AbstractControllerIntegra
assertThat(getMetadataValues(profile, "dspace.orcid.authenticated"), not(empty()));
}
@Test
public void testCloneFromExternalProfileAlreadyAssociated() throws Exception {
String id = user.getID().toString();
String authToken = getAuthToken(user.getEmail(), password);
getClient(authToken).perform(post("/api/eperson/profiles/").contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(status().isCreated()).andExpect(jsonPath("$.id", is(id)))
.andExpect(jsonPath("$.visible", is(false))).andExpect(jsonPath("$.type", is("profile")));
getClient(authToken)
.perform(post("/api/eperson/profiles/").contentType(TEXT_URI_LIST)
.content("http://localhost:8080/server/api/core/items/" + id))
.andExpect(status().isConflict());
}
private Item createProfile(EPerson ePerson) throws Exception {
String authToken = getAuthToken(ePerson.getEmail(), password);

View File

@@ -72,6 +72,34 @@
<scope_note></scope_note>
</dc-type>
<dc-type>
<schema>dspace</schema>
<element>orcid</element>
<qualifier>sync-mode</qualifier>
<scope_note></scope_note>
</dc-type>
<dc-type>
<schema>dspace</schema>
<element>orcid</element>
<qualifier>sync-publications</qualifier>
<scope_note></scope_note>
</dc-type>
<dc-type>
<schema>dspace</schema>
<element>orcid</element>
<qualifier>sync-fundings</qualifier>
<scope_note></scope_note>
</dc-type>
<dc-type>
<schema>dspace</schema>
<element>orcid</element>
<qualifier>sync-profile</qualifier>
<scope_note></scope_note>
</dc-type>
<dc-type>
<schema>dspace</schema>
<element>orcid</element>