Merge pull request #8266 from 4Science/CST-5587

Orcid queue and synchronization
This commit is contained in:
Tim Donohue
2022-06-22 08:33:39 -05:00
committed by GitHub
147 changed files with 13757 additions and 405 deletions

View File

@@ -1,42 +0,0 @@
/**
* 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.client;
import org.dspace.app.orcid.exception.OrcidClientException;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.orcid.jaxb.model.v3.release.record.Person;
/**
* Interface for classes that allow to contact ORCID.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidClient {
/**
* Exchange the authorization code for an ORCID iD and 3-legged access token.
* The authorization code expires upon use.
*
* @param code the authorization code
* @return the ORCID token
* @throws OrcidClientException if some error occurs during the exchange
*/
OrcidTokenResponseDTO getAccessToken(String code);
/**
* Retrieves a summary of the ORCID person related to the given orcid.
*
* @param accessToken the access token
* @param orcid the orcid id of the record to retrieve
* @return the Person
* @throws OrcidClientException if some error occurs during the search
*/
Person getPerson(String accessToken, String orcid);
}

View File

@@ -1,167 +0,0 @@
/**
* 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.client;
import static org.apache.http.client.methods.RequestBuilder.get;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.dspace.app.orcid.exception.OrcidClientException;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.dspace.util.ThrowingSupplier;
import org.orcid.jaxb.model.v3.release.record.Person;
/**
* Implementation of {@link OrcidClient}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidClientImpl implements OrcidClient {
private final OrcidConfiguration orcidConfiguration;
private final ObjectMapper objectMapper;
public OrcidClientImpl(OrcidConfiguration orcidConfiguration) {
this.orcidConfiguration = orcidConfiguration;
this.objectMapper = new ObjectMapper();
}
@Override
public OrcidTokenResponseDTO getAccessToken(String code) {
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("code", code));
params.add(new BasicNameValuePair("grant_type", "authorization_code"));
params.add(new BasicNameValuePair("client_id", orcidConfiguration.getClientId()));
params.add(new BasicNameValuePair("client_secret", orcidConfiguration.getClientSecret()));
HttpUriRequest httpUriRequest = RequestBuilder.post(orcidConfiguration.getTokenEndpointUrl())
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Accept", "application/json")
.setEntity(new UrlEncodedFormEntity(params, Charset.defaultCharset()))
.build();
return executeAndParseJson(httpUriRequest, OrcidTokenResponseDTO.class);
}
@Override
public Person getPerson(String accessToken, String orcid) {
HttpUriRequest httpUriRequest = buildGetUriRequest(accessToken, "/" + orcid + "/person");
return executeAndUnmarshall(httpUriRequest, false, Person.class);
}
private HttpUriRequest buildGetUriRequest(String accessToken, String relativePath) {
return get(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Authorization", "Bearer " + accessToken)
.build();
}
private <T> T executeAndParseJson(HttpUriRequest httpUriRequest, Class<T> clazz) {
HttpClient client = HttpClientBuilder.create().build();
return executeAndReturns(() -> {
HttpResponse response = client.execute(httpUriRequest);
if (isNotSuccessfull(response)) {
throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response));
}
return objectMapper.readValue(response.getEntity().getContent(), clazz);
});
}
private <T> T executeAndUnmarshall(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull, Class<T> clazz) {
HttpClient client = HttpClientBuilder.create().build();
return executeAndReturns(() -> {
HttpResponse response = client.execute(httpUriRequest);
if (handleNotFoundAsNull && isNotFound(response)) {
return null;
}
if (isNotSuccessfull(response)) {
throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response));
}
return unmarshall(response.getEntity(), clazz);
});
}
private <T> T executeAndReturns(ThrowingSupplier<T, Exception> supplier) {
try {
return supplier.get();
} catch (OrcidClientException ex) {
throw ex;
} catch (Exception ex) {
throw new OrcidClientException(ex);
}
}
@SuppressWarnings("unchecked")
private <T> T unmarshall(HttpEntity entity, Class<T> clazz) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(entity.getContent());
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return (T) unmarshaller.unmarshal(xmlStreamReader);
}
private String formatErrorMessage(HttpResponse response) {
try {
return IOUtils.toString(response.getEntity().getContent(), Charset.defaultCharset());
} catch (UnsupportedOperationException | IOException e) {
return "Generic error";
}
}
private boolean isNotSuccessfull(HttpResponse response) {
int statusCode = getStatusCode(response);
return statusCode < 200 || statusCode > 299;
}
private boolean isNotFound(HttpResponse response) {
return getStatusCode(response) == HttpStatus.SC_NOT_FOUND;
}
private int getStatusCode(HttpResponse response) {
return response.getStatusLine().getStatusCode();
}
}

View File

@@ -23,19 +23,19 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.app.orcid.client.OrcidClient;
import org.dspace.app.orcid.client.OrcidConfiguration;
import org.dspace.app.orcid.model.OrcidTokenResponseDTO;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidConfiguration;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.ResearcherProfile;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.orcid.jaxb.model.v3.release.record.Email; import org.orcid.jaxb.model.v3.release.record.Email;
import org.orcid.jaxb.model.v3.release.record.Person; import org.orcid.jaxb.model.v3.release.record.Person;

View File

@@ -17,6 +17,7 @@ import java.util.Date;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -26,8 +27,6 @@ import java.util.stream.Stream;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.app.util.AuthorizeUtil; import org.dspace.app.util.AuthorizeUtil;
import org.dspace.authorize.AuthorizeConfiguration; import org.dspace.authorize.AuthorizeConfiguration;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
@@ -59,6 +58,15 @@ import org.dspace.harvest.HarvestedItem;
import org.dspace.harvest.service.HarvestedItemService; import org.dspace.harvest.service.HarvestedItemService;
import org.dspace.identifier.IdentifierException; import org.dspace.identifier.IdentifierException;
import org.dspace.identifier.service.IdentifierService; import org.dspace.identifier.service.IdentifierService;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.dspace.versioning.service.VersioningService; import org.dspace.versioning.service.VersioningService;
import org.dspace.workflow.WorkflowItemService; import org.dspace.workflow.WorkflowItemService;
@@ -129,6 +137,18 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
@Autowired @Autowired
private OrcidTokenService orcidTokenService; private OrcidTokenService orcidTokenService;
@Autowired(required = true)
private OrcidHistoryService orcidHistoryService;
@Autowired(required = true)
private OrcidQueueService orcidQueueService;
@Autowired(required = true)
private OrcidSynchronizationService orcidSynchronizationService;
@Autowired(required = true)
private ResearcherProfileService researcherProfileService;
protected ItemServiceImpl() { protected ItemServiceImpl() {
super(); super();
} }
@@ -750,6 +770,8 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
// remove version attached to the item // remove version attached to the item
removeVersion(context, item); removeVersion(context, item);
removeOrcidSynchronizationStuff(context, item);
// Also delete the item if it appears in a harvested collection. // Also delete the item if it appears in a harvested collection.
HarvestedItem hi = harvestedItemService.find(context, item); HarvestedItem hi = harvestedItemService.find(context, item);
@@ -1630,4 +1652,67 @@ prevent the generation of resource policy entry values with null dspace_object a
return entityTypeService.findByEntityType(context, entityTypeString); return entityTypeService.findByEntityType(context, entityTypeString);
} }
private void removeOrcidSynchronizationStuff(Context context, Item item) throws SQLException, AuthorizeException {
if (isNotProfileOrOrcidEntity(item)) {
return;
}
context.turnOffAuthorisationSystem();
try {
createOrcidQueueRecordsToDeleteOnOrcid(context, item);
deleteOrcidHistoryRecords(context, item);
deleteOrcidQueueRecords(context, item);
} finally {
context.restoreAuthSystemState();
}
}
private boolean isNotProfileOrOrcidEntity(Item item) {
String entityType = getEntityTypeLabel(item);
return !OrcidEntityType.isValidEntityType(entityType)
&& !researcherProfileService.getProfileType().equals(entityType);
}
private void createOrcidQueueRecordsToDeleteOnOrcid(Context context, Item entity) throws SQLException {
String entityType = getEntityTypeLabel(entity);
if (entityType == null || researcherProfileService.getProfileType().equals(entityType)) {
return;
}
Map<Item, String> profileAndPutCodeMap = orcidHistoryService.findLastPutCodes(context, entity);
for (Item profile : profileAndPutCodeMap.keySet()) {
if (orcidSynchronizationService.isSynchronizationAllowed(profile, entity)) {
String putCode = profileAndPutCodeMap.get(profile);
String title = getMetadataFirstValue(entity, "dc", "title", null, Item.ANY);
orcidQueueService.createEntityDeletionRecord(context, profile, title, entityType, putCode);
}
}
}
private void deleteOrcidHistoryRecords(Context context, Item item) throws SQLException {
List<OrcidHistory> historyRecords = orcidHistoryService.findByProfileItemOrEntity(context, item);
for (OrcidHistory historyRecord : historyRecords) {
if (historyRecord.getProfileItem().equals(item)) {
orcidHistoryService.delete(context, historyRecord);
} else {
historyRecord.setEntity(null);
orcidHistoryService.update(context, historyRecord);
}
}
}
private void deleteOrcidQueueRecords(Context context, Item item) throws SQLException {
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findByProfileItemOrEntity(context, item);
for (OrcidQueue orcidQueueRecord : orcidQueueRecords) {
orcidQueueService.delete(context, orcidQueueRecord);
}
}
} }

View File

@@ -1567,4 +1567,5 @@ public class SolrServiceImpl implements SearchService, IndexingService {
} }
return null; return null;
} }
} }

View File

@@ -25,7 +25,6 @@ import org.apache.commons.codec.DecoderException;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.factory.AuthorizeServiceFactory;
import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.AuthorizeService;
@@ -47,6 +46,7 @@ import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.SubscribeService; import org.dspace.eperson.service.SubscribeService;
import org.dspace.event.Event; import org.dspace.event.Event;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.util.UUIDUtils; import org.dspace.util.UUIDUtils;
import org.dspace.versioning.Version; import org.dspace.versioning.Version;
import org.dspace.versioning.VersionHistory; import org.dspace.versioning.VersionHistory;

View File

@@ -0,0 +1,211 @@
/**
* 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;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.dspace.content.Item;
import org.dspace.core.ReloadableEntity;
import org.hibernate.annotations.Type;
/**
* The ORCID history entity that it contains information relating to an attempt
* to synchronize the DSpace items and information on ORCID. While the entity
* {@link OrcidQueue} contains the data to be synchronized with ORCID, this
* entity instead contains the data synchronized with ORCID, with the result of
* the synchronization. Each record in this table is associated with a profile
* item and the entity synchronized (which can be the profile itself, a
* publication or a project/funding). If the entity is the profile itself then
* the metadata field contains the signature of the information synchronized.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Entity
@Table(name = "orcid_history")
public class OrcidHistory implements ReloadableEntity<Integer> {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orcid_history_id_seq")
@SequenceGenerator(name = "orcid_history_id_seq", sequenceName = "orcid_history_id_seq", allocationSize = 1)
private Integer id;
/**
* The profile item.
*/
@ManyToOne
@JoinColumn(name = "owner_id")
protected Item profileItem;
/**
* The synchronized item.
*/
@ManyToOne
@JoinColumn(name = "entity_id")
private Item entity;
/**
* The identifier of the synchronized resource on ORCID side. For more details
* see https://info.orcid.org/faq/what-is-a-put-code/
*/
@Column(name = "put_code")
private String putCode;
/**
* The record type. Could be publication, funding or a profile's section.
*/
@Column(name = "record_type")
private String recordType;
/**
* A description of the synchronized resource.
*/
@Column(name = "description")
private String description;
/**
* The signature of the synchronized metadata. This is used when the entity is
* the owner itself.
*/
@Lob
@Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType")
@Column(name = "metadata")
private String metadata;
/**
* The operation performed on ORCID.
*/
@Enumerated(EnumType.STRING)
@Column(name = "operation")
private OrcidOperation operation;
/**
* The response message incoming from ORCID.
*/
@Lob
@Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType")
@Column(name = "response_message")
private String responseMessage;
/**
* The timestamp of the synchronization attempt.
*/
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "timestamp_last_attempt")
private Date timestamp = new Date();
/**
* The HTTP status incoming from ORCID.
*/
@Column(name = "status")
private Integer status;
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public Integer getID() {
return id;
}
public Item getProfileItem() {
return profileItem;
}
public void setProfileItem(Item profileItem) {
this.profileItem = profileItem;
}
public Item getEntity() {
return entity;
}
public void setEntity(Item entity) {
this.entity = entity;
}
public String getPutCode() {
return putCode;
}
public void setPutCode(String putCode) {
this.putCode = putCode;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
public String getRecordType() {
return recordType;
}
public void setRecordType(String recordType) {
this.recordType = recordType;
}
public String getMetadata() {
return metadata;
}
public void setMetadata(String metadata) {
this.metadata = metadata;
}
public OrcidOperation getOperation() {
return operation;
}
public void setOperation(OrcidOperation operation) {
this.operation = operation;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
}

View File

@@ -5,24 +5,16 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.model; package org.dspace.orcid;
/** /**
* The types of activities defined on ORCID that can be synchronized. * Enum that models an ORCID synchronization operation.
* *
* @author Luca Giamminonni (luca.giamminonni at 4science.it) * @author Luca Giamminonni (luca.giamminonni at 4science.it)
* *
*/ */
public enum OrcidEntityType { public enum OrcidOperation {
INSERT,
/** UPDATE,
* The publication/work activity. DELETE;
*/
PUBLICATION,
/**
* The funding activity.
*/
FUNDING;
} }

View File

@@ -0,0 +1,219 @@
/**
* 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;
import static org.apache.commons.lang3.StringUtils.isEmpty;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;
import org.dspace.content.Item;
import org.dspace.core.ReloadableEntity;
import org.hibernate.annotations.Type;
/**
* Entity that model a record on the ORCID synchronization queue. Each record in
* this table is associated with an profile item and the entity to be
* synchronized (which can be the profile itself, a publication or a
* project/funding). If the entity is the profile itself then the metadata field
* contains the signature of the information to be synchronized.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@Entity
@Table(name = "orcid_queue")
public class OrcidQueue implements ReloadableEntity<Integer> {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "orcid_queue_id_seq")
@SequenceGenerator(name = "orcid_queue_id_seq", sequenceName = "orcid_queue_id_seq", allocationSize = 1)
private Integer id;
/**
* The profile item.
*/
@ManyToOne
@JoinColumn(name = "owner_id")
protected Item profileItem;
/**
* The entity to be synchronized.
*/
@ManyToOne
@JoinColumn(name = "entity_id")
private Item entity;
/**
* A description of the resource to be synchronized.
*/
@Column(name = "description")
private String description;
/**
* The identifier of the resource to be synchronized on ORCID side (in case of
* update or deletion). For more details see
* https://info.orcid.org/faq/what-is-a-put-code/
*/
@Column(name = "put_code")
private String putCode;
/**
* The record type. Could be publication, funding or a profile's section.
*/
@Column(name = "record_type")
private String recordType;
/**
* The signature of the metadata to be synchronized. This is used when the
* entity is the owner itself.
*/
@Lob
@Column(name = "metadata")
@Type(type = "org.dspace.storage.rdbms.hibernate.DatabaseAwareLobType")
private String metadata;
/**
* The operation to be performed on ORCID.
*/
@Enumerated(EnumType.STRING)
@Column(name = "operation")
private OrcidOperation operation;
/**
* Synchronization attempts already made for a particular record.
*/
@Column(name = "attempts")
private Integer attempts = 0;
public boolean isInsertAction() {
return entity != null && isEmpty(putCode);
}
public boolean isUpdateAction() {
return entity != null && isNotEmpty(putCode);
}
public boolean isDeleteAction() {
return entity == null && isNotEmpty(putCode);
}
public void setID(Integer id) {
this.id = id;
}
@Override
public Integer getID() {
return this.id;
}
public Item getProfileItem() {
return profileItem;
}
public void setProfileItem(Item profileItem) {
this.profileItem = profileItem;
}
public Item getEntity() {
return entity;
}
public void setEntity(Item entity) {
this.entity = entity;
}
public String getPutCode() {
return putCode;
}
public void setPutCode(String putCode) {
this.putCode = putCode;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
OrcidQueue other = (OrcidQueue) obj;
return Objects.equals(id, other.id);
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getRecordType() {
return recordType;
}
public void setRecordType(String recordType) {
this.recordType = recordType;
}
public String getMetadata() {
return metadata;
}
public void setMetadata(String metadata) {
this.metadata = metadata;
}
public OrcidOperation getOperation() {
return operation;
}
public void setOperation(OrcidOperation operation) {
this.operation = operation;
}
public Integer getAttempts() {
return attempts;
}
public void setAttempts(Integer attempts) {
this.attempts = attempts;
}
@Override
public String toString() {
return "OrcidQueue [id=" + id + ", profileItem=" + profileItem + ", entity=" + entity + ", description="
+ description
+ ", putCode=" + putCode + ", recordType=" + recordType + ", metadata=" + metadata + ", operation="
+ operation + "]";
}
}

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid; package org.dspace.orcid;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.Entity; import javax.persistence.Entity;

View File

@@ -0,0 +1,81 @@
/**
* 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.client;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.orcid.jaxb.model.v3.release.record.Person;
/**
* Interface for classes that allow to contact ORCID.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidClient {
/**
* Exchange the authorization code for an ORCID iD and 3-legged access token.
* The authorization code expires upon use.
*
* @param code the authorization code
* @return the ORCID token
* @throws OrcidClientException if some error occurs during the exchange
*/
OrcidTokenResponseDTO getAccessToken(String code);
/**
* Retrieves a summary of the ORCID person related to the given orcid.
*
* @param accessToken the access token
* @param orcid the orcid id of the record to retrieve
* @return the Person
* @throws OrcidClientException if some error occurs during the search
*/
Person getPerson(String accessToken, String orcid);
/**
* Push the given object to ORCID.
*
* @param accessToken the access token
* @param orcid the orcid id
* @param object the orcid object to push
* @return the orcid response if no error occurs
* @throws OrcidClientException if some error occurs during the push
* @throws IllegalArgumentException if the given object is not an valid ORCID
* object
*/
OrcidResponse push(String accessToken, String orcid, Object object);
/**
* Update the object with the given putCode.
*
* @param accessToken the access token
* @param orcid the orcid id
* @param object the orcid object to push
* @param putCode the put code of the resource to delete
* @return the orcid response if no error occurs
* @throws OrcidClientException if some error occurs during the push
* @throws IllegalArgumentException if the given object is not an valid ORCID
* object
*/
OrcidResponse update(String accessToken, String orcid, Object object, String putCode);
/**
* Delete the ORCID object with the given putCode on the given path.
*
* @param accessToken the access token
* @param orcid the orcid id
* @param putCode the put code of the resource to delete
* @param path the path of the resource to delete
* @return the orcid response if no error occurs
* @throws OrcidClientException if some error occurs during the search
*/
OrcidResponse deleteByPutCode(String accessToken, String orcid, String putCode, String path);
}

View File

@@ -0,0 +1,320 @@
/**
* 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.client;
import static org.apache.http.client.methods.RequestBuilder.delete;
import static org.apache.http.client.methods.RequestBuilder.get;
import static org.apache.http.client.methods.RequestBuilder.post;
import static org.apache.http.client.methods.RequestBuilder.put;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicNameValuePair;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.util.ThrowingSupplier;
import org.orcid.jaxb.model.v3.release.record.Address;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.Keyword;
import org.orcid.jaxb.model.v3.release.record.OtherName;
import org.orcid.jaxb.model.v3.release.record.Person;
import org.orcid.jaxb.model.v3.release.record.PersonExternalIdentifier;
import org.orcid.jaxb.model.v3.release.record.ResearcherUrl;
import org.orcid.jaxb.model.v3.release.record.Work;
/**
* Implementation of {@link OrcidClient}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidClientImpl implements OrcidClient {
/**
* Mapping between ORCID JAXB models and the sub-paths on ORCID API.
*/
private static final Map<Class<?>, String> PATHS_MAP = initializePathsMap();
private final OrcidConfiguration orcidConfiguration;
private final ObjectMapper objectMapper;
public OrcidClientImpl(OrcidConfiguration orcidConfiguration) {
this.orcidConfiguration = orcidConfiguration;
this.objectMapper = new ObjectMapper();
}
private static Map<Class<?>, String> initializePathsMap() {
Map<Class<?>, String> map = new HashMap<Class<?>, String>();
map.put(Work.class, OrcidEntityType.PUBLICATION.getPath());
map.put(Funding.class, OrcidEntityType.FUNDING.getPath());
map.put(Address.class, OrcidProfileSectionType.COUNTRY.getPath());
map.put(OtherName.class, OrcidProfileSectionType.OTHER_NAMES.getPath());
map.put(ResearcherUrl.class, OrcidProfileSectionType.RESEARCHER_URLS.getPath());
map.put(PersonExternalIdentifier.class, OrcidProfileSectionType.EXTERNAL_IDS.getPath());
map.put(Keyword.class, OrcidProfileSectionType.KEYWORDS.getPath());
return map;
}
@Override
public OrcidTokenResponseDTO getAccessToken(String code) {
List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("code", code));
params.add(new BasicNameValuePair("grant_type", "authorization_code"));
params.add(new BasicNameValuePair("client_id", orcidConfiguration.getClientId()));
params.add(new BasicNameValuePair("client_secret", orcidConfiguration.getClientSecret()));
HttpUriRequest httpUriRequest = RequestBuilder.post(orcidConfiguration.getTokenEndpointUrl())
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Accept", "application/json")
.setEntity(new UrlEncodedFormEntity(params, Charset.defaultCharset()))
.build();
return executeAndParseJson(httpUriRequest, OrcidTokenResponseDTO.class);
}
@Override
public Person getPerson(String accessToken, String orcid) {
HttpUriRequest httpUriRequest = buildGetUriRequest(accessToken, "/" + orcid + "/person");
return executeAndUnmarshall(httpUriRequest, false, Person.class);
}
@Override
public OrcidResponse push(String accessToken, String orcid, Object object) {
String path = getOrcidPathFromOrcidObjectType(object.getClass());
return execute(buildPostUriRequest(accessToken, "/" + orcid + path, object), false);
}
@Override
public OrcidResponse update(String accessToken, String orcid, Object object, String putCode) {
String path = getOrcidPathFromOrcidObjectType(object.getClass());
return execute(buildPutUriRequest(accessToken, "/" + orcid + path + "/" + putCode, object), false);
}
@Override
public OrcidResponse deleteByPutCode(String accessToken, String orcid, String putCode, String path) {
return execute(buildDeleteUriRequest(accessToken, "/" + orcid + path + "/" + putCode), true);
}
private HttpUriRequest buildGetUriRequest(String accessToken, String relativePath) {
return get(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/x-www-form-urlencoded")
.addHeader("Authorization", "Bearer " + accessToken)
.build();
}
private HttpUriRequest buildPostUriRequest(String accessToken, String relativePath, Object object) {
return post(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/vnd.orcid+xml")
.addHeader("Authorization", "Bearer " + accessToken)
.setEntity(convertToEntity(object))
.build();
}
private HttpUriRequest buildPutUriRequest(String accessToken, String relativePath, Object object) {
return put(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Content-Type", "application/vnd.orcid+xml")
.addHeader("Authorization", "Bearer " + accessToken)
.setEntity(convertToEntity(object))
.build();
}
private HttpUriRequest buildDeleteUriRequest(String accessToken, String relativePath) {
return delete(orcidConfiguration.getApiUrl() + relativePath.trim())
.addHeader("Authorization", "Bearer " + accessToken)
.build();
}
private <T> T executeAndParseJson(HttpUriRequest httpUriRequest, Class<T> clazz) {
HttpClient client = HttpClientBuilder.create().build();
return executeAndReturns(() -> {
HttpResponse response = client.execute(httpUriRequest);
if (isNotSuccessfull(response)) {
throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response));
}
return objectMapper.readValue(response.getEntity().getContent(), clazz);
});
}
/**
* Execute the given httpUriRequest, unmarshalling the content with the given
* class.
* @param httpUriRequest the http request to be executed
* @param handleNotFoundAsNull if true this method returns null if the response
* status is 404, if false throws an
* OrcidClientException
* @param clazz the class to be used for the content unmarshall
* @return the response body
* @throws OrcidClientException if the incoming response is not successfull
*/
private <T> T executeAndUnmarshall(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull, Class<T> clazz) {
HttpClient client = HttpClientBuilder.create().build();
return executeAndReturns(() -> {
HttpResponse response = client.execute(httpUriRequest);
if (handleNotFoundAsNull && isNotFound(response)) {
return null;
}
if (isNotSuccessfull(response)) {
throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response));
}
return unmarshall(response.getEntity(), clazz);
});
}
private OrcidResponse execute(HttpUriRequest httpUriRequest, boolean handleNotFoundAsNull) {
HttpClient client = HttpClientBuilder.create().build();
return executeAndReturns(() -> {
HttpResponse response = client.execute(httpUriRequest);
if (handleNotFoundAsNull && isNotFound(response)) {
return new OrcidResponse(getStatusCode(response), null, getContent(response));
}
if (isNotSuccessfull(response)) {
throw new OrcidClientException(getStatusCode(response), formatErrorMessage(response));
}
return new OrcidResponse(getStatusCode(response), getPutCode(response), getContent(response));
});
}
private <T> T executeAndReturns(ThrowingSupplier<T, Exception> supplier) {
try {
return supplier.get();
} catch (OrcidClientException ex) {
throw ex;
} catch (Exception ex) {
throw new OrcidClientException(ex);
}
}
private String marshall(Object object) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(object.getClass());
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
StringWriter stringWriter = new StringWriter();
marshaller.marshal(object, stringWriter);
return stringWriter.toString();
}
@SuppressWarnings("unchecked")
private <T> T unmarshall(HttpEntity entity, Class<T> clazz) throws Exception {
JAXBContext jaxbContext = JAXBContext.newInstance(clazz);
XMLInputFactory xmlInputFactory = XMLInputFactory.newFactory();
xmlInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(entity.getContent());
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
return (T) unmarshaller.unmarshal(xmlStreamReader);
}
private HttpEntity convertToEntity(Object object) {
try {
return new StringEntity(marshall(object), StandardCharsets.UTF_8);
} catch (JAXBException ex) {
throw new IllegalArgumentException("The given object cannot be sent to ORCID", ex);
}
}
private String formatErrorMessage(HttpResponse response) {
try {
return IOUtils.toString(response.getEntity().getContent(), Charset.defaultCharset());
} catch (UnsupportedOperationException | IOException e) {
return "Generic error";
}
}
private boolean isNotSuccessfull(HttpResponse response) {
int statusCode = getStatusCode(response);
return statusCode < 200 || statusCode > 299;
}
private boolean isNotFound(HttpResponse response) {
return getStatusCode(response) == HttpStatus.SC_NOT_FOUND;
}
private int getStatusCode(HttpResponse response) {
return response.getStatusLine().getStatusCode();
}
private String getOrcidPathFromOrcidObjectType(Class<?> clazz) {
String path = PATHS_MAP.get(clazz);
if (path == null) {
throw new IllegalArgumentException("The given class is not an ORCID object's class: " + clazz);
}
return path;
}
private String getContent(HttpResponse response) throws UnsupportedOperationException, IOException {
HttpEntity entity = response.getEntity();
return entity != null ? IOUtils.toString(entity.getContent(), StandardCharsets.UTF_8.name()) : null;
}
/**
* Returns the put code present in the given http response, if any. For more
* details about the put code see For more details see
* https://info.orcid.org/faq/what-is-a-put-code/
* @param response the http response coming from ORCID
* @return the put code, if any
*/
private String getPutCode(HttpResponse response) {
Header[] headers = response.getHeaders("Location");
if (headers.length == 0) {
return null;
}
String value = headers[0].getValue();
return value.substring(value.lastIndexOf("/") + 1);
}
}

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.client; package org.dspace.orcid.client;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -19,6 +19,10 @@ public final class OrcidConfiguration {
private String apiUrl; private String apiUrl;
private String publicUrl;
private String domainUrl;
private String redirectUrl; private String redirectUrl;
private String clientId; private String clientId;
@@ -39,6 +43,14 @@ public final class OrcidConfiguration {
this.apiUrl = apiUrl; this.apiUrl = apiUrl;
} }
public String getDomainUrl() {
return domainUrl;
}
public void setDomainUrl(String domainUrl) {
this.domainUrl = domainUrl;
}
public String getRedirectUrl() { public String getRedirectUrl() {
return redirectUrl; return redirectUrl;
} }
@@ -87,4 +99,12 @@ public final class OrcidConfiguration {
return StringUtils.isNotBlank(scopes) ? StringUtils.split(scopes, ",") : new String[] {}; return StringUtils.isNotBlank(scopes) ? StringUtils.split(scopes, ",") : new String[] {};
} }
public String getPublicUrl() {
return publicUrl;
}
public void setPublicUrl(String publicUrl) {
this.publicUrl = publicUrl;
}
} }

View File

@@ -0,0 +1,56 @@
/**
* 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.client;
import org.apache.http.HttpStatus;
/**
* Model a successfully response incoming from ORCID using {@link OrcidClient}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public final class OrcidResponse {
private final int status;
private final String putCode;
private final String content;
/**
* Create an ORCID response instance with the specified HTTP status, putCode and
* content.
*
* @param status the HTTP status incoming from ORCID
* @param putCode the identifier of the resource ORCID side
* @param content the response body content
*/
public OrcidResponse(int status, String putCode, String content) {
this.status = status;
this.putCode = putCode;
this.content = content;
}
public int getStatus() {
return status;
}
public String getPutCode() {
return putCode;
}
public String getContent() {
return content;
}
public boolean isNotFoundStatus() {
return status == HttpStatus.SC_NOT_FOUND;
}
}

View File

@@ -0,0 +1,358 @@
/**
* 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.consumer;
import static java.util.Arrays.asList;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.Relationship;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.core.Context;
import org.dspace.event.Consumer;
import org.dspace.event.Event;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.factory.OrcidProfileSectionFactory;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidProfileSectionFactoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The consumer to fill the ORCID queue. The addition to the queue is made for
* all archived items that meet one of these conditions:
* <ul>
* <li>are profiles already linked to orcid that have some modified sections to
* be synchronized (based on the preferences set by the user)</li>
* <li>are publications/fundings related to profile items linked to orcid (based
* on the preferences set by the user)</li>
*
* </ul>
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidQueueConsumer implements Consumer {
private static final Logger LOGGER = LoggerFactory.getLogger(OrcidQueueConsumer.class);
private OrcidQueueService orcidQueueService;
private OrcidHistoryService orcidHistoryService;
private OrcidTokenService orcidTokenService;
private OrcidSynchronizationService orcidSynchronizationService;
private ItemService itemService;
private OrcidProfileSectionFactoryService profileSectionFactoryService;
private ConfigurationService configurationService;
private RelationshipService relationshipService;
private List<UUID> alreadyConsumedItems = new ArrayList<>();
@Override
public void initialize() throws Exception {
OrcidServiceFactory orcidServiceFactory = OrcidServiceFactory.getInstance();
this.orcidQueueService = orcidServiceFactory.getOrcidQueueService();
this.orcidHistoryService = orcidServiceFactory.getOrcidHistoryService();
this.orcidSynchronizationService = orcidServiceFactory.getOrcidSynchronizationService();
this.orcidTokenService = orcidServiceFactory.getOrcidTokenService();
this.profileSectionFactoryService = orcidServiceFactory.getOrcidProfileSectionFactoryService();
this.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
this.relationshipService = ContentServiceFactory.getInstance().getRelationshipService();
this.itemService = ContentServiceFactory.getInstance().getItemService();
}
@Override
public void consume(Context context, Event event) throws Exception {
if (isOrcidSynchronizationDisabled()) {
return;
}
DSpaceObject dso = event.getSubject(context);
if (!(dso instanceof Item)) {
return;
}
Item item = (Item) dso;
if (!item.isArchived()) {
return;
}
if (alreadyConsumedItems.contains(item.getID())) {
return;
}
context.turnOffAuthorisationSystem();
try {
consumeItem(context, item);
} finally {
context.restoreAuthSystemState();
}
}
/**
* Consume the item if it is a profile or an ORCID entity.
*/
private void consumeItem(Context context, Item item) throws SQLException {
String entityType = itemService.getEntityTypeLabel(item);
if (entityType == null) {
return;
}
if (OrcidEntityType.isValidEntityType(entityType)) {
consumeEntity(context, item);
} else if (entityType.equals(getProfileType())) {
consumeProfile(context, item);
}
alreadyConsumedItems.add(item.getID());
}
/**
* Search for all related items to the given entity and create a new ORCID queue
* record if one of this is a profile linked with ORCID and the entity item must
* be synchronized with ORCID.
*/
private void consumeEntity(Context context, Item entity) throws SQLException {
List<Item> relatedItems = findAllRelatedItems(context, entity);
for (Item relatedItem : relatedItems) {
if (isNotProfileItem(relatedItem) || isNotLinkedToOrcid(context, relatedItem)) {
continue;
}
if (shouldNotBeSynchronized(relatedItem, entity) || isAlreadyQueued(context, relatedItem, entity)) {
continue;
}
orcidQueueService.create(context, relatedItem, entity);
}
}
private List<Item> findAllRelatedItems(Context context, Item entity) throws SQLException {
return relationshipService.findByItem(context, entity).stream()
.map(relationship -> getRelatedItem(entity, relationship))
.collect(Collectors.toList());
}
private Item getRelatedItem(Item item, Relationship relationship) {
return item.equals(relationship.getLeftItem()) ? relationship.getRightItem() : relationship.getLeftItem();
}
/**
* If the given profile item is linked with ORCID recalculate all the ORCID
* queue records of the configured profile sections that can be synchronized.
*/
private void consumeProfile(Context context, Item item) throws SQLException {
if (isNotLinkedToOrcid(context, item)) {
return;
}
for (OrcidProfileSectionFactory factory : getAllProfileSectionFactories(item)) {
String sectionType = factory.getProfileSectionType().name();
orcidQueueService.deleteByEntityAndRecordType(context, item, sectionType);
if (isProfileSectionSynchronizationDisabled(context, item, factory)) {
continue;
}
List<String> signatures = factory.getMetadataSignatures(context, item);
List<OrcidHistory> historyRecords = findSuccessfullyOrcidHistoryRecords(context, item, sectionType);
createInsertionRecordForNewSignatures(context, item, historyRecords, factory, signatures);
createDeletionRecordForNoMorePresentSignatures(context, item, historyRecords, factory, signatures);
}
}
private boolean isProfileSectionSynchronizationDisabled(Context context,
Item item, OrcidProfileSectionFactory factory) {
List<OrcidProfileSyncPreference> preferences = this.orcidSynchronizationService.getProfilePreferences(item);
return !preferences.contains(factory.getSynchronizationPreference());
}
/**
* Add new INSERTION record in the ORCID queue based on the metadata signatures
* calculated from the current item state.
*/
private void createInsertionRecordForNewSignatures(Context context, Item item, List<OrcidHistory> historyRecords,
OrcidProfileSectionFactory factory, List<String> signatures) throws SQLException {
String sectionType = factory.getProfileSectionType().name();
for (String signature : signatures) {
if (isNotAlreadySynchronized(historyRecords, signature)) {
String description = factory.getDescription(context, item, signature);
orcidQueueService.createProfileInsertionRecord(context, item, description, sectionType, signature);
}
}
}
/**
* Add new DELETION records in the ORCID queue for metadata signature presents
* in the ORCID history no more present in the metadata signatures calculated
* from the current item state.
*/
private void createDeletionRecordForNoMorePresentSignatures(Context context, Item profile,
List<OrcidHistory> historyRecords, OrcidProfileSectionFactory factory, List<String> signatures)
throws SQLException {
String sectionType = factory.getProfileSectionType().name();
for (OrcidHistory historyRecord : historyRecords) {
String storedSignature = historyRecord.getMetadata();
String putCode = historyRecord.getPutCode();
String description = historyRecord.getDescription();
if (signatures.contains(storedSignature) || isAlreadyDeleted(historyRecords, historyRecord)) {
continue;
}
if (StringUtils.isBlank(putCode)) {
LOGGER.warn("The orcid history record with id {} should have a not blank put code",
historyRecord.getID());
continue;
}
orcidQueueService.createProfileDeletionRecord(context, profile, description,
sectionType, storedSignature, putCode);
}
}
private List<OrcidHistory> findSuccessfullyOrcidHistoryRecords(Context context, Item item,
String sectionType) throws SQLException {
return orcidHistoryService.findSuccessfullyRecordsByEntityAndType(context, item, sectionType);
}
private boolean isNotAlreadySynchronized(List<OrcidHistory> records, String signature) {
return getLastOperation(records, signature)
.map(operation -> operation == OrcidOperation.DELETE)
.orElse(Boolean.TRUE);
}
private boolean isAlreadyDeleted(List<OrcidHistory> records, OrcidHistory historyRecord) {
if (historyRecord.getOperation() == OrcidOperation.DELETE) {
return true;
}
return findDeletedHistoryRecordsBySignature(records, historyRecord.getMetadata())
.anyMatch(record -> record.getTimestamp().after(historyRecord.getTimestamp()));
}
private Stream<OrcidHistory> findDeletedHistoryRecordsBySignature(List<OrcidHistory> records, String signature) {
return records.stream()
.filter(record -> signature.equals(record.getMetadata()))
.filter(record -> record.getOperation() == OrcidOperation.DELETE);
}
private Optional<OrcidOperation> getLastOperation(List<OrcidHistory> records, String signature) {
return records.stream()
.filter(record -> signature.equals(record.getMetadata()))
.sorted(comparing(OrcidHistory::getTimestamp, nullsFirst(naturalOrder())).reversed())
.map(OrcidHistory::getOperation)
.findFirst();
}
private boolean isAlreadyQueued(Context context, Item profileItem, Item entity) throws SQLException {
return isNotEmpty(orcidQueueService.findByProfileItemAndEntity(context, profileItem, entity));
}
private boolean isNotLinkedToOrcid(Context context, Item profileItemItem) {
return hasNotOrcidAccessToken(context, profileItemItem)
|| getMetadataValue(profileItemItem, "person.identifier.orcid") == null;
}
private boolean hasNotOrcidAccessToken(Context context, Item profileItemItem) {
return orcidTokenService.findByProfileItem(context, profileItemItem) == null;
}
private boolean shouldNotBeSynchronized(Item profileItem, Item entity) {
return !orcidSynchronizationService.isSynchronizationAllowed(profileItem, entity);
}
private boolean isNotProfileItem(Item profileItemItem) {
return !getProfileType().equals(itemService.getEntityTypeLabel(profileItemItem));
}
private String getMetadataValue(Item item, String metadataField) {
return itemService.getMetadataFirstValue(item, new MetadataFieldName(metadataField), Item.ANY);
}
private List<OrcidProfileSectionFactory> getAllProfileSectionFactories(Item item) {
return this.profileSectionFactoryService.findByPreferences(asList(OrcidProfileSyncPreference.values()));
}
private String getProfileType() {
return configurationService.getProperty("researcher-profile.entity-type", "Person");
}
private boolean isOrcidSynchronizationDisabled() {
return !configurationService.getBooleanProperty("orcid.synchronization-enabled", true);
}
@Override
public void end(Context context) throws Exception {
alreadyConsumedItems.clear();
}
@Override
public void finish(Context context) throws Exception {
// nothing to do
}
}

View File

@@ -0,0 +1,76 @@
/**
* 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.dao;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.core.GenericDAO;
import org.dspace.orcid.OrcidHistory;
/**
* Database Access Object interface class for the OrcidHistory object. The
* implementation of this class is responsible for all database calls for the
* OrcidHistory object and is autowired by spring. This class should only be
* accessed from a single service and should never be exposed outside of the API
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidHistoryDAO extends GenericDAO<OrcidHistory> {
/**
* Find all the ORCID history records by the given profileItem and entity uuids.
*
* @param context the DSpace context
* @param profileItemId the profileItem item uuid
* @param entityId the entity item uuid
* @return the records list
* @throws SQLException if an SQL error occurs
*/
List<OrcidHistory> findByProfileItemAndEntity(Context context, UUID profileItemId, UUID entityId)
throws SQLException;
/**
* Get the OrcidHistory records where the given item is the profileItem or the
* entity
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidHistory entities
* @throws SQLException if database error
*/
public List<OrcidHistory> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Find the OrcidHistory records related to the given entity item.
*
* @param context DSpace context object
* @param entity the entity item
* @return the found put codes
* @throws SQLException if database error
*/
List<OrcidHistory> findByEntity(Context context, Item entity) throws SQLException;
/**
* Find all the successfully Orcid history records with the given record type
* related to the given entity. An history record is considered successful if
* the status is between 200 and 300.
*
* @param context DSpace context object
* @param entity the entity item
* @param recordType the record type
* @return the found orcid history records
* @throws SQLException if database error
*/
List<OrcidHistory> findSuccessfullyRecordsByEntityAndType(Context context, Item entity,
String recordType) throws SQLException;
}

View File

@@ -0,0 +1,107 @@
/**
* 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.dao;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.core.GenericDAO;
import org.dspace.orcid.OrcidQueue;
/**
* Database Access Object interface class for the OrcidQueue object. The
* implementation of this class is responsible for all database calls for the
* OrcidQueue object and is autowired by spring. This class should only be
* accessed from a single service and should never be exposed outside of the API
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidQueueDAO extends GenericDAO<OrcidQueue> {
/**
* Get the orcid queue records by the profileItem id.
*
* @param context DSpace context object
* @param profileItemId the profileItem item id
* @param limit limit
* @param offset offset
* @return the orcid queue records
* @throws SQLException if an SQL error occurs
*/
public List<OrcidQueue> findByProfileItemId(Context context, UUID profileItemId, Integer limit, Integer offset)
throws SQLException;
/**
* Count the orcid queue records with the same profileItemId.
*
* @param context DSpace context object
* @param profileItemId the profileItem item id
* @return the count result
* @throws SQLException if an SQL error occurs
*/
long countByProfileItemId(Context context, UUID profileItemId) throws SQLException;
/**
* Returns all the orcid queue records with the given profileItem and entity
* items.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @return the found orcid queue records
* @throws SQLException
*/
public List<OrcidQueue> findByProfileItemAndEntity(Context context, Item profileItem, Item entity)
throws SQLException;
/**
* Get the OrcidQueue records where the given item is the profileItem OR the
* entity
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidHistory entities
* @throws SQLException if database error
*/
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Find all the OrcidQueue records with the given entity and record type.
*
* @param context DSpace context object
* @param entity the entity item
* @param type the record type
* @throws SQLException if database error occurs
*/
public List<OrcidQueue> findByEntityAndRecordType(Context context, Item entity, String type) throws SQLException;
/**
* Find all the OrcidQueue records with the given profileItem and record type.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param type the record type
* @throws SQLException if database error occurs
*/
public List<OrcidQueue> findByProfileItemAndRecordType(Context context, Item profileItem, String type)
throws SQLException;
/**
* Get all the OrcidQueue records with attempts less than the given attempts.
*
* @param context DSpace context object
* @param attempts the maximum value of attempts
* @return the found OrcidQueue records
* @throws SQLException if database error
*/
public List<OrcidQueue> findByAttemptsLessThan(Context context, int attempts) throws SQLException;
}

View File

@@ -5,13 +5,13 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.dao; package org.dspace.orcid.dao;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.core.GenericDAO; import org.dspace.core.GenericDAO;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.orcid.OrcidToken;
/** /**
* Database Access Object interface class for the OrcidToken object. The * Database Access Object interface class for the OrcidToken object. The

View File

@@ -0,0 +1,64 @@
/**
* 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.dao.impl;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import javax.persistence.Query;
import org.dspace.content.Item;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.dao.OrcidHistoryDAO;
/**
* Implementation of {@link OrcidHistoryDAO}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@SuppressWarnings("unchecked")
public class OrcidHistoryDAOImpl extends AbstractHibernateDAO<OrcidHistory> implements OrcidHistoryDAO {
@Override
public List<OrcidHistory> findByProfileItemAndEntity(Context context, UUID profileItemId, UUID entityId)
throws SQLException {
Query query = createQuery(context,
"FROM OrcidHistory WHERE profileItem.id = :profileItemId AND entity.id = :entityId ");
query.setParameter("profileItemId", profileItemId);
query.setParameter("entityId", entityId);
return query.getResultList();
}
@Override
public List<OrcidHistory> findByProfileItemOrEntity(Context context, Item item) throws SQLException {
Query query = createQuery(context, "FROM OrcidHistory WHERE profileItem.id = :itemId OR entity.id = :itemId");
query.setParameter("itemId", item.getID());
return query.getResultList();
}
@Override
public List<OrcidHistory> findByEntity(Context context, Item entity) throws SQLException {
Query query = createQuery(context, "FROM OrcidHistory WHERE entity.id = :entityId ");
query.setParameter("entityId", entity.getID());
return query.getResultList();
}
@Override
public List<OrcidHistory> findSuccessfullyRecordsByEntityAndType(Context context, Item entity,
String recordType) throws SQLException {
Query query = createQuery(context, "FROM OrcidHistory WHERE entity = :entity AND recordType = :type "
+ "AND status BETWEEN 200 AND 300");
query.setParameter("entity", entity);
query.setParameter("type", recordType);
return query.getResultList();
}
}

View File

@@ -0,0 +1,90 @@
/**
* 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.dao.impl;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import javax.persistence.Query;
import org.dspace.content.Item;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.dao.OrcidQueueDAO;
/**
* Implementation of {@link OrcidQueueDAO}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@SuppressWarnings("unchecked")
public class OrcidQueueDAOImpl extends AbstractHibernateDAO<OrcidQueue> implements OrcidQueueDAO {
@Override
public List<OrcidQueue> findByProfileItemId(Context context, UUID profileItemId, Integer limit, Integer offset)
throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE profileItem.id= :profileItemId");
query.setParameter("profileItemId", profileItemId);
if (limit != null && limit.intValue() > 0) {
query.setMaxResults(limit);
}
query.setFirstResult(offset);
return query.getResultList();
}
@Override
public List<OrcidQueue> findByProfileItemAndEntity(Context context, Item profileItem, Item entity)
throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE profileItem = :profileItem AND entity = :entity");
query.setParameter("profileItem", profileItem);
query.setParameter("entity", entity);
return query.getResultList();
}
@Override
public long countByProfileItemId(Context context, UUID profileItemId) throws SQLException {
Query query = createQuery(context,
"SELECT COUNT(queue) FROM OrcidQueue queue WHERE profileItem.id= :profileItemId");
query.setParameter("profileItemId", profileItemId);
return (long) query.getSingleResult();
}
@Override
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE profileItem.id= :itemId OR entity.id = :itemId");
query.setParameter("itemId", item.getID());
return query.getResultList();
}
@Override
public List<OrcidQueue> findByEntityAndRecordType(Context context, Item entity, String type) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE entity = :entity AND recordType = :type");
query.setParameter("entity", entity);
query.setParameter("type", type);
return query.getResultList();
}
@Override
public List<OrcidQueue> findByProfileItemAndRecordType(Context context, Item profileItem, String type)
throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE profileItem = :profileItem AND recordType = :type");
query.setParameter("profileItem", profileItem);
query.setParameter("type", type);
return query.getResultList();
}
@Override
public List<OrcidQueue> findByAttemptsLessThan(Context context, int attempts) throws SQLException {
Query query = createQuery(context, "FROM OrcidQueue WHERE attempts IS NULL OR attempts < :attempts");
query.setParameter("attempts", attempts);
return query.getResultList();
}
}

View File

@@ -5,17 +5,17 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.dao.impl; package org.dspace.orcid.dao.impl;
import java.sql.SQLException; import java.sql.SQLException;
import javax.persistence.Query; import javax.persistence.Query;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.app.orcid.dao.OrcidTokenDAO;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.AbstractHibernateDAO; import org.dspace.core.AbstractHibernateDAO;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.dao.OrcidTokenDAO;
/** /**
* Implementation of {@link OrcidTokenDAO}. * Implementation of {@link OrcidTokenDAO}.

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.exception; package org.dspace.orcid.exception;
/** /**
* Exception throwable from class that implements {@link OrcidClient} in case of * Exception throwable from class that implements {@link OrcidClient} in case of

View File

@@ -0,0 +1,52 @@
/**
* 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.exception;
import java.util.List;
import java.util.stream.Collectors;
import org.dspace.orcid.model.validator.OrcidValidationError;
/**
* A Runtime exception that occurs when an ORCID object that must be send to
* ORCID is not valid.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidValidationException extends RuntimeException {
private static final long serialVersionUID = 3377335341871311369L;
private final List<OrcidValidationError> errors;
public OrcidValidationException(OrcidValidationError error) {
this(List.of(error));
}
public OrcidValidationException(List<OrcidValidationError> errors) {
super("Errors occurs during ORCID object validation");
this.errors = errors;
}
public List<OrcidValidationError> getErrors() {
return errors;
}
@Override
public String getMessage() {
return super.getMessage() + ". Error codes: " + formatErrors();
}
private String formatErrors() {
return errors.stream()
.map(error -> error.getCode())
.collect(Collectors.joining(","));
}
}

View File

@@ -0,0 +1,54 @@
/**
* 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.factory;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidConfiguration;
import org.dspace.orcid.service.MetadataSignatureGenerator;
import org.dspace.orcid.service.OrcidEntityFactoryService;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidProfileSectionFactoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.services.factory.DSpaceServicesFactory;
/**
* Abstract factory to get services for the orcid package, use
* OrcidHistoryServiceFactory.getInstance() to retrieve an implementation.
*
* @author Luca Giamminonni (luca.giamminonni at 4Science.it)
*
*/
public abstract class OrcidServiceFactory {
public abstract OrcidHistoryService getOrcidHistoryService();
public abstract OrcidQueueService getOrcidQueueService();
public abstract OrcidSynchronizationService getOrcidSynchronizationService();
public abstract OrcidTokenService getOrcidTokenService();
public abstract OrcidProfileSectionFactoryService getOrcidProfileSectionFactoryService();
public abstract MetadataSignatureGenerator getMetadataSignatureGenerator();
public abstract OrcidEntityFactoryService getOrcidEntityFactoryService();
public abstract OrcidClient getOrcidClient();
public abstract OrcidConfiguration getOrcidConfiguration();
public static OrcidServiceFactory getInstance() {
return DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName(
"orcidServiceFactory", OrcidServiceFactory.class);
}
}

View File

@@ -0,0 +1,105 @@
/**
* 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.factory;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidConfiguration;
import org.dspace.orcid.service.MetadataSignatureGenerator;
import org.dspace.orcid.service.OrcidEntityFactoryService;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidProfileSectionFactoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link OrcidServiceFactory}.
*
* @author Luca Giamminonni (luca.giamminonni at 4Science.it)
*
*/
public class OrcidServiceFactoryImpl extends OrcidServiceFactory {
@Autowired
private OrcidHistoryService orcidHistoryService;
@Autowired
private OrcidSynchronizationService orcidSynchronizationService;
@Autowired
private OrcidQueueService orcidQueueService;
@Autowired
private OrcidProfileSectionFactoryService orcidProfileSectionFactoryService;
@Autowired
private OrcidEntityFactoryService orcidEntityFactoryService;
@Autowired
private MetadataSignatureGenerator metadataSignatureGenerator;
@Autowired
private OrcidClient orcidClient;
@Autowired
private OrcidConfiguration orcidConfiguration;
@Autowired
private OrcidTokenService orcidTokenService;
@Override
public OrcidHistoryService getOrcidHistoryService() {
return orcidHistoryService;
}
@Override
public OrcidQueueService getOrcidQueueService() {
return orcidQueueService;
}
@Override
public OrcidSynchronizationService getOrcidSynchronizationService() {
return orcidSynchronizationService;
}
@Override
public OrcidProfileSectionFactoryService getOrcidProfileSectionFactoryService() {
return orcidProfileSectionFactoryService;
}
@Override
public MetadataSignatureGenerator getMetadataSignatureGenerator() {
return metadataSignatureGenerator;
}
@Override
public OrcidEntityFactoryService getOrcidEntityFactoryService() {
return orcidEntityFactoryService;
}
@Override
public OrcidTokenService getOrcidTokenService() {
return orcidTokenService;
}
@Override
public OrcidClient getOrcidClient() {
return orcidClient;
}
@Override
public OrcidConfiguration getOrcidConfiguration() {
return orcidConfiguration;
}
public void setOrcidClient(OrcidClient orcidClient) {
this.orcidClient = orcidClient;
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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 java.util.Arrays;
/**
* The types of activities defined on ORCID that can be synchronized.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidEntityType {
/**
* The ORCID publication/work activity.
*/
PUBLICATION("Publication", "/work"),
/**
* The ORCID funding activity.
*/
FUNDING("Project", "/funding");
/**
* The DSpace entity type.
*/
private final String entityType;
/**
* The subpath of the activity on ORCID API.
*/
private final String path;
private OrcidEntityType(String entityType, String path) {
this.entityType = entityType;
this.path = path;
}
public String getEntityType() {
return entityType;
}
public String getPath() {
return path;
}
/**
* Check if the given DSpace entity type is valid.
* @param entityType the entity type to check
* @return true if valid, false otherwise
*/
public static boolean isValidEntityType(String entityType) {
return Arrays.stream(OrcidEntityType.values())
.anyMatch(orcidEntityType -> orcidEntityType.getEntityType().equalsIgnoreCase(entityType));
}
/**
* Returns an ORCID entity type from a DSpace entity type.
*
* @param entityType the DSpace entity type to search for
* @return the ORCID entity type, if any
*/
public static OrcidEntityType fromEntityType(String entityType) {
return Arrays.stream(OrcidEntityType.values())
.filter(orcidEntityType -> orcidEntityType.getEntityType().equalsIgnoreCase(entityType))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,209 @@
/**
* 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.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.dspace.orcid.model.factory.OrcidFactoryUtils;
import org.dspace.util.SimpleMapConverter;
import org.orcid.jaxb.model.common.FundingContributorRole;
/**
* Class that contains all the mapping between {@link Funding} and DSpace
* metadata fields.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidFundingFieldMapping {
/**
* The metadata fields related to the funding contributors.
*/
private Map<String, FundingContributorRole> contributorFields;
/**
* The metadata fields related to the funding external identifiers.
*/
private Map<String, String> externalIdentifierFields;
/**
* The metadata field related to the funding title.
*/
private String titleField;
/**
* The metadata field related to the funding type.
*/
private String typeField;
/**
* The funding type converter.
*/
private SimpleMapConverter typeConverter;
/**
* The metadata field related to the funding amount.
*/
private String amountField;
/**
* The metadata field related to the funding amount's currency.
*/
private String amountCurrencyField;
/**
* The funding amount's currency converter.
*/
private SimpleMapConverter amountCurrencyConverter;
/**
* The metadata field related to the funding start date.
*/
private String startDateField;
/**
* The metadata field related to the funding end date.
*/
private String endDateField;
/**
* The metadata field related to the funding description.
*/
private String descriptionField;
/**
* The type of the relationship between the funding and the organization.
*/
private String organizationRelationshipType;
private Map<String, FundingContributorRole> parseContributors(String contributors) {
Map<String, String> contributorsMap = parseConfigurations(contributors);
return contributorsMap.keySet().stream()
.collect(toMap(identity(), field -> parseContributorRole(contributorsMap.get(field))));
}
private FundingContributorRole parseContributorRole(String contributorRole) {
try {
return FundingContributorRole.fromValue(contributorRole);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException("The funding contributor role " + contributorRole +
" is invalid, allowed values are " + getAllowedContributorRoles(), ex);
}
}
private List<String> getAllowedContributorRoles() {
return Arrays.asList(FundingContributorRole.values()).stream()
.map(FundingContributorRole::value)
.collect(Collectors.toList());
}
public Map<String, String> getExternalIdentifierFields() {
return externalIdentifierFields;
}
public void setExternalIdentifierFields(String externalIdentifierFields) {
this.externalIdentifierFields = OrcidFactoryUtils.parseConfigurations(externalIdentifierFields);
}
public Map<String, FundingContributorRole> getContributorFields() {
return contributorFields;
}
public void setContributorFields(String contributorFields) {
this.contributorFields = parseContributors(contributorFields);
}
public String getTitleField() {
return titleField;
}
public void setTitleField(String titleField) {
this.titleField = titleField;
}
public String getStartDateField() {
return startDateField;
}
public void setStartDateField(String startDateField) {
this.startDateField = startDateField;
}
public String getEndDateField() {
return endDateField;
}
public void setEndDateField(String endDateField) {
this.endDateField = endDateField;
}
public String getDescriptionField() {
return descriptionField;
}
public void setDescriptionField(String descriptionField) {
this.descriptionField = descriptionField;
}
public String getOrganizationRelationshipType() {
return organizationRelationshipType;
}
public void setOrganizationRelationshipType(String organizationRelationshipType) {
this.organizationRelationshipType = organizationRelationshipType;
}
public String getTypeField() {
return typeField;
}
public void setTypeField(String typeField) {
this.typeField = typeField;
}
public String getAmountField() {
return amountField;
}
public void setAmountField(String amountField) {
this.amountField = amountField;
}
public String getAmountCurrencyField() {
return amountCurrencyField;
}
public void setAmountCurrencyField(String amountCurrencyField) {
this.amountCurrencyField = amountCurrencyField;
}
public String convertAmountCurrency(String currency) {
return amountCurrencyConverter != null ? amountCurrencyConverter.getValue(currency) : currency;
}
public void setAmountCurrencyConverter(SimpleMapConverter amountCurrencyConverter) {
this.amountCurrencyConverter = amountCurrencyConverter;
}
public String convertType(String type) {
return typeConverter != null ? typeConverter.getValue(type) : type;
}
public void setTypeConverter(SimpleMapConverter typeConverter) {
this.typeConverter = typeConverter;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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 org.apache.commons.lang3.EnumUtils;
/**
* Enum that model all the ORCID profile sections that could be synchronized.
* These fields come from the ORCID PERSON schema, see
* https://info.orcid.org/documentation/integration-guide/orcid-record/#PERSON
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidProfileSectionType {
OTHER_NAMES("/other-names"),
COUNTRY("/address"),
KEYWORDS("/keywords"),
EXTERNAL_IDS("/external-identifiers"),
RESEARCHER_URLS("/researcher-urls");
private final String path;
private OrcidProfileSectionType(String path) {
this.path = path;
}
public String getPath() {
return path;
}
public static boolean isValid(String type) {
return type != null ? EnumUtils.isValidEnum(OrcidProfileSectionType.class, type.toUpperCase()) : false;
}
public static OrcidProfileSectionType fromString(String type) {
return isValid(type) ? OrcidProfileSectionType.valueOf(type.toUpperCase()) : null;
}
}

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.model; package org.dspace.orcid.model;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;

View File

@@ -0,0 +1,197 @@
/**
* 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.dspace.util.SimpleMapConverter;
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.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidWorkFieldMapping {
/**
* The metadata fields related to the work contributors.
*/
private Map<String, ContributorRole> contributorFields = new HashMap<>();
/**
* The metadata fields related to the work external identifiers.
*/
private Map<String, String> 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;
/**
* The work type converter.
*/
private SimpleMapConverter typeConverter;
/**
* The work language converter.
*/
private SimpleMapConverter languageConverter;
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<String, ContributorRole> getContributorFields() {
return contributorFields;
}
public void setContributorFields(String contributorFields) {
this.contributorFields = parseContributors(contributorFields);
}
public Map<String, String> 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;
}
private Map<String, ContributorRole> parseContributors(String contributors) {
Map<String, String> 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<String> getAllowedContributorRoles() {
return Arrays.asList(ContributorRole.values()).stream()
.map(ContributorRole::value)
.collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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;
import java.util.Optional;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.exception.OrcidValidationException;
import org.orcid.jaxb.model.common.ContributorRole;
import org.orcid.jaxb.model.common.FundingContributorRole;
import org.orcid.jaxb.model.v3.release.common.Contributor;
import org.orcid.jaxb.model.v3.release.common.Country;
import org.orcid.jaxb.model.v3.release.common.FuzzyDate;
import org.orcid.jaxb.model.v3.release.common.Organization;
import org.orcid.jaxb.model.v3.release.common.Url;
import org.orcid.jaxb.model.v3.release.record.FundingContributor;
/**
* Interface for factory classes that creates common ORCID objects.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidCommonObjectFactory {
/**
* Creates an instance of {@link FuzzyDate} if the given metadata value
* represent a date with a supported format.
*
* @param metadataValue the metadata value
* @return the FuzzyDate istance, if any
*/
public Optional<FuzzyDate> createFuzzyDate(MetadataValue metadataValue);
/**
* Creates an instance of {@link Organization} from the given orgUnit item.
*
* @param context the DSpace context
* @param orgUnit the orgUnit item
* @return the created Organization's instance, if any
*/
public Optional<Organization> createOrganization(Context context, Item orgUnit);
/**
* Creates an instance of {@link Contributor} from the given metadata value.
*
* @param context the DSpace context
* @param metadataValue the metadata value
* @param role the contributor role
* @return the created Contributor instance, if any
*/
public Optional<Contributor> createContributor(Context context, MetadataValue metadataValue, ContributorRole role);
/**
* Creates an instance of {@link FundingContributor} from the given metadata
* value.
*
* @param context the DSpace context
* @param metadataValue the metadata value
* @param role the contributor role
* @return the created FundingContributor instance, if any
*/
public Optional<FundingContributor> createFundingContributor(Context context, MetadataValue metadataValue,
FundingContributorRole role);
/**
* Creates an instance of {@link Url} from the given item.
* @param context the DSpace context
* @param item the item
* @return the created Url instance, if any
*/
public Optional<Url> createUrl(Context context, Item item);
/**
* Creates an instance of {@link Country} from the given metadata value.
*
* @param context the DSpace context
* @param metadataValue the metadata value
* @return the created Country instance, if any
* @throws OrcidValidationException if the given metadata value is not a valid
* ISO 3611 country
*/
public Optional<Country> createCountry(Context context, MetadataValue metadataValue)
throws OrcidValidationException;
}

View File

@@ -0,0 +1,43 @@
/**
* 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;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidEntityType;
import org.orcid.jaxb.model.v3.release.record.Activity;
/**
* Interface to mark factories of Orcid entities.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidEntityFactory {
/**
* Placeholder used to refer the item handle on fields mapping.
*/
String SIMPLE_HANDLE_PLACEHOLDER = "$simple-handle";
/**
* Returns the entity type created from this factory.
*
* @return the entity type
*/
public OrcidEntityType getEntityType();
/**
* Creates an ORCID activity from the given object.
*
* @param context the DSpace context
* @param item the item
* @return the created activity instance
*/
public Activity createOrcidObject(Context context, Item item);
}

View File

@@ -0,0 +1,68 @@
/**
* 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;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
/**
* Utility class for Orcid factory classes. This is used to parse the
* configuration of ORCID entities defined in orcid.cfg (for example see
* contributors and external ids configuration).
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public final class OrcidFactoryUtils {
private OrcidFactoryUtils() {
}
/**
* Parse the given configurations value and returns a map with metadata fields
* as keys and types/sources as values. The expected configuration syntax is a
* list of values field::type separated by commas.
*
* @param configurations the configurations to parse
* @return the configurations parsing result as map
*/
public static Map<String, String> parseConfigurations(String configurations) {
Map<String, String> configurationMap = new HashMap<String, String>();
if (StringUtils.isBlank(configurations)) {
return configurationMap;
}
for (String configuration : configurations.split(",")) {
String[] configurationSections = parseConfiguration(configuration);
configurationMap.put(configurationSections[0], configurationSections[1]);
}
return configurationMap;
}
/**
* Parse the given configuration value and returns it's section. The expected
* configuration syntax is field::type.
*
* @param configuration the configuration to parse
* @return the configuration sections
* @throws IllegalStateException if the given configuration is not valid
*/
private static String[] parseConfiguration(String configuration) {
String[] configurations = configuration.split("::");
if (configurations.length != 2) {
throw new IllegalStateException(
"The configuration '" + configuration + "' is not valid. Expected field::type");
}
return configurations;
}
}

View File

@@ -0,0 +1,78 @@
/**
* 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;
import java.util.List;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.profile.OrcidProfileSyncPreference;
/**
* Interface for classes that creates ORCID profile section object.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidProfileSectionFactory {
/**
* Creates an instance of an ORCID object starting from the metadata values
*
* @param context the DSpace Context
* @param metadataValues the metadata values
* @return the ORCID object
*/
public Object create(Context context, List<MetadataValue> metadataValues);
/**
* Returns the profile section type related to this factory.
*
* @return the profile section type
*/
public OrcidProfileSectionType getProfileSectionType();
/**
* Returns the profile synchronization preference related to this factory.
*
* @return the synchronization preference
*/
public OrcidProfileSyncPreference getSynchronizationPreference();
/**
* Returns all the metadata fields involved in the profile section
* configuration.
*
* @return the metadataFields
*/
public List<String> getMetadataFields();
/**
* Given the input item's metadata values generate a metadata signature for each
* metadata field groups handled by this factory or for each metadata fields if
* the factory is configured with single metadata fields.
*
* @param context the DSpace context
* @param item the item
* @return the metadata signatures
*/
public List<String> getMetadataSignatures(Context context, Item item);
/**
* Returns a description of the item's metadata values related to the given
* signature.
*
* @param context the DSpace context
* @param item the item
* @param signature the metadata signature
* @return the metadata values description
*/
public String getDescription(Context context, Item item, String signature);
}

View File

@@ -0,0 +1,73 @@
/**
* 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 java.lang.String.format;
import java.util.List;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.orcid.model.factory.OrcidCommonObjectFactory;
import org.dspace.orcid.model.factory.OrcidProfileSectionFactory;
import org.dspace.orcid.service.MetadataSignatureGenerator;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Abstract class for that handle commons behaviors of all the available orcid
* profile section factories.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public abstract class AbstractOrcidProfileSectionFactory implements OrcidProfileSectionFactory {
protected final OrcidProfileSectionType sectionType;
protected final OrcidProfileSyncPreference preference;
@Autowired
protected ItemService itemService;
@Autowired
protected OrcidCommonObjectFactory orcidCommonObjectFactory;
@Autowired
protected MetadataSignatureGenerator metadataSignatureGenerator;
public AbstractOrcidProfileSectionFactory(OrcidProfileSectionType sectionType,
OrcidProfileSyncPreference preference) {
this.sectionType = sectionType;
this.preference = preference;
if (!getSupportedTypes().contains(sectionType)) {
throw new IllegalArgumentException(format("The ORCID configuration does not support "
+ "the section type %s. Supported types are %s", sectionType, getSupportedTypes()));
}
}
protected abstract List<OrcidProfileSectionType> getSupportedTypes();
@Override
public OrcidProfileSectionType getProfileSectionType() {
return sectionType;
}
@Override
public OrcidProfileSyncPreference getSynchronizationPreference() {
return preference;
}
protected List<MetadataValue> getMetadataValues(Item item, String metadataField) {
return itemService.getMetadataByMetadataString(item, metadataField);
}
}

View File

@@ -0,0 +1,308 @@
/**
* 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 java.util.Optional.empty;
import static java.util.Optional.of;
import static java.util.Optional.ofNullable;
import static org.apache.commons.lang3.EnumUtils.isValidEnum;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.dspace.orcid.model.factory.OrcidFactoryUtils.parseConfigurations;
import static org.orcid.jaxb.model.common.SequenceType.ADDITIONAL;
import static org.orcid.jaxb.model.common.SequenceType.FIRST;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.handle.service.HandleService;
import org.dspace.orcid.client.OrcidConfiguration;
import org.dspace.orcid.exception.OrcidValidationException;
import org.dspace.orcid.model.factory.OrcidCommonObjectFactory;
import org.dspace.orcid.model.validator.OrcidValidationError;
import org.dspace.util.MultiFormatDateParser;
import org.dspace.util.SimpleMapConverter;
import org.orcid.jaxb.model.common.ContributorRole;
import org.orcid.jaxb.model.common.FundingContributorRole;
import org.orcid.jaxb.model.common.Iso3166Country;
import org.orcid.jaxb.model.v3.release.common.Contributor;
import org.orcid.jaxb.model.v3.release.common.ContributorAttributes;
import org.orcid.jaxb.model.v3.release.common.Country;
import org.orcid.jaxb.model.v3.release.common.CreditName;
import org.orcid.jaxb.model.v3.release.common.DisambiguatedOrganization;
import org.orcid.jaxb.model.v3.release.common.FuzzyDate;
import org.orcid.jaxb.model.v3.release.common.Organization;
import org.orcid.jaxb.model.v3.release.common.OrganizationAddress;
import org.orcid.jaxb.model.v3.release.common.Url;
import org.orcid.jaxb.model.v3.release.record.FundingContributor;
import org.orcid.jaxb.model.v3.release.record.FundingContributorAttributes;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link OrcidCommonObjectFactory}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidCommonObjectFactoryImpl implements OrcidCommonObjectFactory {
@Autowired
private ItemService itemService;
@Autowired
private OrcidConfiguration orcidConfiguration;
@Autowired
private HandleService handleService;
private SimpleMapConverter countryConverter;
private String organizationTitleField;
private String organizationCityField;
private String organizationCountryField;
private String contributorEmailField;
private String contributorOrcidField;
private Map<String, String> disambiguatedOrganizationIdentifierFields = new HashMap<>();
@Override
public Optional<FuzzyDate> createFuzzyDate(MetadataValue metadataValue) {
if (isUnprocessableValue(metadataValue)) {
return empty();
}
Date date = MultiFormatDateParser.parse(metadataValue.getValue());
if (date == null) {
return empty();
}
LocalDate localDate = convertToLocalDate(date);
return of(FuzzyDate.valueOf(localDate.getYear(), localDate.getMonthValue(), localDate.getDayOfMonth()));
}
@Override
public Optional<Organization> createOrganization(Context context, Item orgUnit) {
if (orgUnit == null) {
return Optional.empty();
}
Organization organization = new Organization();
organization.setName(getMetadataValue(orgUnit, organizationTitleField));
organization.setAddress(createOrganizationAddress(orgUnit));
organization.setDisambiguatedOrganization(createDisambiguatedOrganization(orgUnit));
return of(organization);
}
@Override
public Optional<Contributor> createContributor(Context context, MetadataValue metadataValue, ContributorRole role) {
if (isUnprocessableValue(metadataValue)) {
return empty();
}
Contributor contributor = new Contributor();
contributor.setCreditName(new CreditName(metadataValue.getValue()));
contributor.setContributorAttributes(getContributorAttributes(metadataValue, role));
return of(contributor);
}
@Override
public Optional<FundingContributor> createFundingContributor(Context context, MetadataValue metadataValue,
FundingContributorRole role) {
if (isUnprocessableValue(metadataValue)) {
return empty();
}
FundingContributor contributor = new FundingContributor();
contributor.setCreditName(new CreditName(metadataValue.getValue()));
contributor.setContributorAttributes(getFundingContributorAttributes(metadataValue, role));
return of(contributor);
}
@Override
public Optional<Url> createUrl(Context context, Item item) {
String handle = item.getHandle();
if (StringUtils.isBlank(handle)) {
return empty();
}
return of(new Url(handleService.getCanonicalForm(handle)));
}
@Override
public Optional<Country> createCountry(Context context, MetadataValue metadataValue) {
if (isUnprocessableValue(metadataValue)) {
return empty();
}
Optional<Iso3166Country> country = convertToIso3166Country(metadataValue.getValue());
if (country.isEmpty()) {
throw new OrcidValidationException(OrcidValidationError.INVALID_COUNTRY);
}
return country.map(isoCountry -> new Country(isoCountry));
}
private ContributorAttributes getContributorAttributes(MetadataValue metadataValue, ContributorRole role) {
ContributorAttributes attributes = new ContributorAttributes();
attributes.setContributorRole(role != null ? role : null);
attributes.setContributorSequence(metadataValue.getPlace() == 0 ? FIRST : ADDITIONAL);
return attributes;
}
private OrganizationAddress createOrganizationAddress(Item organizationItem) {
OrganizationAddress address = new OrganizationAddress();
address.setCity(getMetadataValue(organizationItem, organizationCityField));
convertToIso3166Country(getMetadataValue(organizationItem, organizationCountryField))
.ifPresent(address::setCountry);
return address;
}
private FundingContributorAttributes getFundingContributorAttributes(MetadataValue metadataValue,
FundingContributorRole role) {
FundingContributorAttributes attributes = new FundingContributorAttributes();
attributes.setContributorRole(role != null ? role : null);
return attributes;
}
private DisambiguatedOrganization createDisambiguatedOrganization(Item organizationItem) {
for (String identifierField : disambiguatedOrganizationIdentifierFields.keySet()) {
String source = disambiguatedOrganizationIdentifierFields.get(identifierField);
String identifier = getMetadataValue(organizationItem, identifierField);
if (isNotBlank(identifier)) {
DisambiguatedOrganization disambiguatedOrganization = new DisambiguatedOrganization();
disambiguatedOrganization.setDisambiguatedOrganizationIdentifier(identifier);
disambiguatedOrganization.setDisambiguationSource(source);
return disambiguatedOrganization;
}
}
return null;
}
private Optional<Iso3166Country> convertToIso3166Country(String countryValue) {
return ofNullable(countryValue)
.map(value -> countryConverter != null ? countryConverter.getValue(value) : value)
.filter(value -> isValidEnum(Iso3166Country.class, value))
.map(value -> Iso3166Country.fromValue(value));
}
private boolean isUnprocessableValue(MetadataValue value) {
return value == null || isBlank(value.getValue());
}
private String getMetadataValue(Item item, String metadataField) {
if (StringUtils.isNotBlank(metadataField)) {
return itemService.getMetadataFirstValue(item, new MetadataFieldName(metadataField), Item.ANY);
} else {
return null;
}
}
private LocalDate convertToLocalDate(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
public String getOrganizationCityField() {
return organizationCityField;
}
public String getOrganizationCountryField() {
return organizationCountryField;
}
public Map<String, String> getDisambiguatedOrganizationIdentifierFields() {
return disambiguatedOrganizationIdentifierFields;
}
public String getContributorEmailField() {
return contributorEmailField;
}
public String getContributorOrcidField() {
return contributorOrcidField;
}
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
public OrcidConfiguration getOrcidConfiguration() {
return orcidConfiguration;
}
public void setOrcidConfiguration(OrcidConfiguration orcidConfiguration) {
this.orcidConfiguration = orcidConfiguration;
}
public void setOrganizationCityField(String organizationCityField) {
this.organizationCityField = organizationCityField;
}
public void setOrganizationCountryField(String organizationCountryField) {
this.organizationCountryField = organizationCountryField;
}
public void setContributorEmailField(String contributorEmailField) {
this.contributorEmailField = contributorEmailField;
}
public void setContributorOrcidField(String contributorOrcidField) {
this.contributorOrcidField = contributorOrcidField;
}
public void setDisambiguatedOrganizationIdentifierFields(String disambiguatedOrganizationIds) {
this.disambiguatedOrganizationIdentifierFields = parseConfigurations(disambiguatedOrganizationIds);
}
public SimpleMapConverter getCountryConverter() {
return countryConverter;
}
public void setCountryConverter(SimpleMapConverter countryConverter) {
this.countryConverter = countryConverter;
}
public String getOrganizationTitleField() {
return organizationTitleField;
}
public void setOrganizationTitleField(String organizationTitleField) {
this.organizationTitleField = organizationTitleField;
}
}

View File

@@ -0,0 +1,301 @@
/**
* 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 java.sql.SQLException;
import java.util.Collection;
import java.util.Currency;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.Relationship;
import org.dspace.content.RelationshipType;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.RelationshipTypeService;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidFundingFieldMapping;
import org.dspace.orcid.model.factory.OrcidCommonObjectFactory;
import org.dspace.orcid.model.factory.OrcidEntityFactory;
import org.orcid.jaxb.model.common.FundingContributorRole;
import org.orcid.jaxb.model.common.FundingType;
import org.orcid.jaxb.model.v3.release.common.Amount;
import org.orcid.jaxb.model.v3.release.common.FuzzyDate;
import org.orcid.jaxb.model.v3.release.common.Organization;
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.ExternalID;
import org.orcid.jaxb.model.v3.release.record.ExternalIDs;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.FundingContributor;
import org.orcid.jaxb.model.v3.release.record.FundingContributors;
import org.orcid.jaxb.model.v3.release.record.FundingTitle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link OrcidEntityFactory} that creates instances of
* {@link Funding}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidFundingFactory implements OrcidEntityFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(OrcidFundingFactory.class);
@Autowired
private ItemService itemService;
@Autowired
private OrcidCommonObjectFactory orcidCommonObjectFactory;
@Autowired
private RelationshipTypeService relationshipTypeService;
@Autowired
private RelationshipService relationshipService;
private OrcidFundingFieldMapping fieldMapping;
@Override
public OrcidEntityType getEntityType() {
return OrcidEntityType.FUNDING;
}
@Override
public Activity createOrcidObject(Context context, Item item) {
Funding funding = new Funding();
funding.setContributors(getContributors(context, item));
funding.setDescription(getDescription(context, item));
funding.setEndDate(getEndDate(context, item));
funding.setExternalIdentifiers(getExternalIds(context, item));
funding.setOrganization(getOrganization(context, item));
funding.setStartDate(getStartDate(context, item));
funding.setTitle(getTitle(context, item));
funding.setType(getType(context, item));
funding.setUrl(getUrl(context, item));
funding.setAmount(getAmount(context, item));
return funding;
}
private FundingContributors getContributors(Context context, Item item) {
FundingContributors fundingContributors = new FundingContributors();
getMetadataValues(context, item, fieldMapping.getContributorFields().keySet()).stream()
.map(metadataValue -> getFundingContributor(context, metadataValue))
.filter(Optional::isPresent)
.map(Optional::get)
.forEach(fundingContributors.getContributor()::add);
return fundingContributors;
}
private Optional<FundingContributor> getFundingContributor(Context context, MetadataValue metadataValue) {
String metadataField = metadataValue.getMetadataField().toString('.');
FundingContributorRole role = fieldMapping.getContributorFields().get(metadataField);
return orcidCommonObjectFactory.createFundingContributor(context, metadataValue, role);
}
private String getDescription(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getDescriptionField())
.map(MetadataValue::getValue)
.orElse(null);
}
private FuzzyDate getEndDate(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getEndDateField())
.flatMap(metadataValue -> orcidCommonObjectFactory.createFuzzyDate(metadataValue))
.orElse(null);
}
private ExternalIDs getExternalIds(Context context, Item item) {
ExternalIDs externalIdentifiers = new ExternalIDs();
getMetadataValues(context, item, fieldMapping.getExternalIdentifierFields().keySet()).stream()
.map(this::getExternalId)
.forEach(externalIdentifiers.getExternalIdentifier()::add);
return externalIdentifiers;
}
private ExternalID getExternalId(MetadataValue metadataValue) {
String metadataField = metadataValue.getMetadataField().toString('.');
return getExternalId(fieldMapping.getExternalIdentifierFields().get(metadataField), metadataValue.getValue());
}
private ExternalID getExternalId(String type, String value) {
ExternalID externalID = new ExternalID();
externalID.setType(type);
externalID.setValue(value);
externalID.setRelationship(org.orcid.jaxb.model.common.Relationship.SELF);
return externalID;
}
/**
* Returns an Organization ORCID entity related to the given item. The
* relationship type configured with
* orcid.mapping.funding.organization-relationship-type is the relationship used
* to search the Organization of the given project item.
*/
private Organization getOrganization(Context context, Item item) {
try {
return relationshipTypeService.findByLeftwardOrRightwardTypeName(context,
fieldMapping.getOrganizationRelationshipType()).stream()
.flatMap(relationshipType -> getRelationships(context, item, relationshipType))
.map(relationship -> getRelatedItem(item, relationship))
.flatMap(orgUnit -> orcidCommonObjectFactory.createOrganization(context, orgUnit).stream())
.findFirst()
.orElse(null);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Stream<Relationship> getRelationships(Context context, Item item, RelationshipType relationshipType) {
try {
return relationshipService.findByItemAndRelationshipType(context, item, relationshipType).stream();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Item getRelatedItem(Item item, Relationship relationship) {
return item.equals(relationship.getLeftItem()) ? relationship.getRightItem() : relationship.getLeftItem();
}
private FuzzyDate getStartDate(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getStartDateField())
.flatMap(metadataValue -> orcidCommonObjectFactory.createFuzzyDate(metadataValue))
.orElse(null);
}
private FundingTitle getTitle(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getTitleField())
.map(metadataValue -> getFundingTitle(context, metadataValue))
.orElse(null);
}
private FundingTitle getFundingTitle(Context context, MetadataValue metadataValue) {
FundingTitle fundingTitle = new FundingTitle();
fundingTitle.setTitle(new Title(metadataValue.getValue()));
return fundingTitle;
}
/**
* Returns an instance of FundingType taking the type from the given item. The
* metadata field to be used to retrieve the item's type is related to the
* configured typeField (orcid.mapping.funding.type).
*/
private FundingType getType(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getTypeField())
.map(type -> fieldMapping.convertType(type.getValue()))
.flatMap(this::getFundingType)
.orElse(FundingType.CONTRACT);
}
private Optional<FundingType> getFundingType(String type) {
try {
return Optional.ofNullable(FundingType.fromValue(type));
} catch (IllegalArgumentException ex) {
LOGGER.warn("The type {} is not valid for ORCID fundings", type);
return Optional.empty();
}
}
private Url getUrl(Context context, Item item) {
return orcidCommonObjectFactory.createUrl(context, item).orElse(null);
}
/**
* Returns an Amount instance taking the amount and currency value from the
* configured metadata values of the given item, if any.
*/
private Amount getAmount(Context context, Item item) {
Optional<String> amount = getAmountValue(context, item);
Optional<String> currency = getCurrencyValue(context, item);
if (amount.isEmpty() || currency.isEmpty()) {
return null;
}
return getAmount(amount.get(), currency.get());
}
/**
* Returns the amount value of the configured metadata field
* orcid.mapping.funding.amount
*/
private Optional<String> getAmountValue(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getAmountField())
.map(MetadataValue::getValue);
}
/**
* Returns the amount value of the configured metadata field
* orcid.mapping.funding.amount.currency (if configured using the converter
* orcid.mapping.funding.amount.currency.converter).
*/
private Optional<String> getCurrencyValue(Context context, Item item) {
return getMetadataValue(context, item, fieldMapping.getAmountCurrencyField())
.map(currency -> fieldMapping.convertAmountCurrency(currency.getValue()))
.filter(currency -> isValidCurrency(currency));
}
private boolean isValidCurrency(String currency) {
try {
return currency != null && Currency.getInstance(currency) != null;
} catch (IllegalArgumentException ex) {
return false;
}
}
private Amount getAmount(String amount, String currency) {
Amount amountObj = new Amount();
amountObj.setContent(amount);
amountObj.setCurrencyCode(currency);
return amountObj;
}
private List<MetadataValue> getMetadataValues(Context context, Item item, Collection<String> metadataFields) {
return metadataFields.stream()
.flatMap(metadataField -> itemService.getMetadataByMetadataString(item, metadataField).stream())
.collect(Collectors.toList());
}
private Optional<MetadataValue> getMetadataValue(Context context, Item item, String metadataField) {
if (isBlank(metadataField)) {
return Optional.empty();
}
return itemService.getMetadataByMetadataString(item, metadataField).stream().findFirst()
.filter(metadataValue -> isNotBlank(metadataValue.getValue()));
}
public OrcidFundingFieldMapping getFieldMapping() {
return fieldMapping;
}
public void setFieldMapping(OrcidFundingFieldMapping fieldMapping) {
this.fieldMapping = fieldMapping;
}
}

View File

@@ -0,0 +1,75 @@
/**
* 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.dspace.orcid.model.OrcidProfileSectionType.EXTERNAL_IDS;
import static org.dspace.orcid.model.factory.OrcidFactoryUtils.parseConfigurations;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.orcid.jaxb.model.common.Relationship;
import org.orcid.jaxb.model.v3.release.common.Url;
import org.orcid.jaxb.model.v3.release.record.PersonExternalIdentifier;
/**
* Implementation of {@link OrcidProfileSectionFactory} that model an personal
* external id.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidPersonExternalIdentifierFactory extends OrcidSimpleValueObjectFactory {
private Map<String, String> externalIds = new HashMap<>();
public OrcidPersonExternalIdentifierFactory(OrcidProfileSectionType sectionType,
OrcidProfileSyncPreference preference) {
super(sectionType, preference);
}
@Override
public List<OrcidProfileSectionType> getSupportedTypes() {
return List.of(EXTERNAL_IDS);
}
@Override
protected Object create(Context context, MetadataValue metadataValue) {
String currentMetadataField = metadataValue.getMetadataField().toString('.');
String externalIdType = externalIds.get(currentMetadataField);
if (externalIdType == null) {
throw new IllegalArgumentException("Metadata field not supported: " + currentMetadataField);
}
PersonExternalIdentifier externalId = new PersonExternalIdentifier();
externalId.setValue(metadataValue.getValue());
externalId.setType(externalIdType);
externalId.setRelationship(Relationship.SELF);
externalId.setUrl(new Url(metadataValue.getValue()));
return externalId;
}
public Map<String, String> getExternalIds() {
return externalIds;
}
public void setExternalIds(String externalIds) {
this.externalIds = parseConfigurations(externalIds);
setMetadataFields(this.externalIds.keySet().stream().collect(Collectors.joining(",")));
}
}

View File

@@ -0,0 +1,149 @@
/**
* 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 java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static org.dspace.orcid.model.OrcidProfileSectionType.COUNTRY;
import static org.dspace.orcid.model.OrcidProfileSectionType.KEYWORDS;
import static org.dspace.orcid.model.OrcidProfileSectionType.OTHER_NAMES;
import static org.dspace.orcid.model.OrcidProfileSectionType.RESEARCHER_URLS;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.orcid.jaxb.model.v3.release.common.Country;
import org.orcid.jaxb.model.v3.release.common.Url;
import org.orcid.jaxb.model.v3.release.record.Address;
import org.orcid.jaxb.model.v3.release.record.Keyword;
import org.orcid.jaxb.model.v3.release.record.OtherName;
import org.orcid.jaxb.model.v3.release.record.ResearcherUrl;
/**
* Implementation of {@link OrcidProfileSectionFactory} that creates ORCID
* objects with a single value.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidSimpleValueObjectFactory extends AbstractOrcidProfileSectionFactory {
private List<String> metadataFields = new ArrayList<String>();
public OrcidSimpleValueObjectFactory(OrcidProfileSectionType sectionType, OrcidProfileSyncPreference preference) {
super(sectionType, preference);
}
@Override
public List<OrcidProfileSectionType> getSupportedTypes() {
return List.of(COUNTRY, KEYWORDS, OTHER_NAMES, RESEARCHER_URLS);
}
@Override
public Object create(Context context, List<MetadataValue> metadataValues) {
if (CollectionUtils.isEmpty(metadataValues)) {
throw new IllegalArgumentException("No metadata values provided to create ORCID object with simple value");
}
if (metadataValues.size() > 1) {
throw new IllegalArgumentException("Multiple metadata values not supported: " + metadataValues);
}
MetadataValue metadataValue = metadataValues.get(0);
String currentMetadataField = metadataValue.getMetadataField().toString('.');
if (!metadataFields.contains(currentMetadataField)) {
throw new IllegalArgumentException("Metadata field not supported: " + currentMetadataField);
}
return create(context, metadataValue);
}
@Override
public List<String> getMetadataSignatures(Context context, Item item) {
return metadataFields.stream()
.flatMap(metadataField -> getMetadataValues(item, metadataField).stream())
.map(metadataValue -> metadataSignatureGenerator.generate(context, List.of(metadataValue)))
.collect(Collectors.toList());
}
@Override
public String getDescription(Context context, Item item, String signature) {
List<MetadataValue> metadataValues = metadataSignatureGenerator.findBySignature(context, item, signature);
return CollectionUtils.isNotEmpty(metadataValues) ? metadataValues.get(0).getValue() : null;
}
/**
* Create an instance of ORCID profile section based on the configured profile
* section type, taking the value from the given metadataValue.
*/
protected Object create(Context context, MetadataValue metadataValue) {
switch (getProfileSectionType()) {
case COUNTRY:
return createAddress(context, metadataValue);
case KEYWORDS:
return createKeyword(metadataValue);
case OTHER_NAMES:
return createOtherName(metadataValue);
case RESEARCHER_URLS:
return createResearcherUrl(metadataValue);
default:
throw new IllegalStateException("OrcidSimpleValueObjectFactory does not support type "
+ getProfileSectionType());
}
}
private ResearcherUrl createResearcherUrl(MetadataValue metadataValue) {
ResearcherUrl researcherUrl = new ResearcherUrl();
researcherUrl.setUrl(new Url(metadataValue.getValue()));
return researcherUrl;
}
private OtherName createOtherName(MetadataValue metadataValue) {
OtherName otherName = new OtherName();
otherName.setContent(metadataValue.getValue());
return otherName;
}
private Keyword createKeyword(MetadataValue metadataValue) {
Keyword keyword = new Keyword();
keyword.setContent(metadataValue.getValue());
return keyword;
}
private Address createAddress(Context context, MetadataValue metadataValue) {
return orcidCommonObjectFactory.createCountry(context, metadataValue)
.map(this::createAddress)
.orElseThrow(() -> new IllegalArgumentException("No address creatable "
+ "from value " + metadataValue.getValue()));
}
private Address createAddress(Country country) {
Address address = new Address();
address.setCountry(country);
return address;
}
public void setMetadataFields(String metadataFields) {
this.metadataFields = metadataFields != null ? asList(metadataFields.split(",")) : emptyList();
}
@Override
public List<String> getMetadataFields() {
return metadataFields;
}
}

View File

@@ -0,0 +1,283 @@
/**
* 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.orcid.jaxb.model.common.Relationship.SELF;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.apache.commons.lang3.EnumUtils;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidWorkFieldMapping;
import org.dspace.orcid.model.factory.OrcidCommonObjectFactory;
import org.dspace.orcid.model.factory.OrcidEntityFactory;
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.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}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidWorkFactory implements OrcidEntityFactory {
private static final Logger LOGGER = LoggerFactory.getLogger(OrcidWorkFactory.class);
@Autowired
private ItemService itemService;
@Autowired
private OrcidCommonObjectFactory orcidCommonObjectFactory;
private OrcidWorkFieldMapping fieldMapping;
@Override
public OrcidEntityType getEntityType() {
return OrcidEntityType.PUBLICATION;
}
@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.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<String, ContributorRole> contributorFields = fieldMapping.getContributorFields();
List<Contributor> 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<Contributor> getContributor(Context context, MetadataValue metadataValue) {
Map<String, ContributorRole> 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<String> 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<String> 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<Subtitle> 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));
return externalIdentifiers;
}
/**
* Creates a list of ExternalID, one for orcid.mapping.funding.external-ids
* value, taking the values from the given item.
*/
private List<ExternalID> getWorkSelfExternalIds(Context context, Item item) {
List<ExternalID> selfExternalIds = new ArrayList<ExternalID>();
Map<String, String> externalIdentifierFields = fieldMapping.getExternalIdentifierFields();
if (externalIdentifierFields.containsKey(SIMPLE_HANDLE_PLACEHOLDER)) {
String handleType = externalIdentifierFields.get(SIMPLE_HANDLE_PLACEHOLDER);
selfExternalIds.add(getExternalId(handleType, item.getHandle(), SELF));
}
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<String, String> externalIdentifierFields = fieldMapping.getExternalIdentifierFields();
String metadataField = metadataValue.getMetadataField().toString('.');
return getExternalId(externalIdentifierFields.get(metadataField), metadataValue.getValue(), SELF);
}
/**
* 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<WorkType> 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 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<MetadataValue> getMetadataValues(Context context, Item item, Collection<String> metadataFields) {
return metadataFields.stream()
.flatMap(metadataField -> itemService.getMetadataByMetadataString(item, metadataField).stream())
.collect(Collectors.toList());
}
private Optional<MetadataValue> 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(OrcidWorkFieldMapping fieldMapping) {
this.fieldMapping = fieldMapping;
}
}

View File

@@ -0,0 +1,49 @@
/**
* 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.validator;
/**
* Enum that model all the errors that could occurs during an ORCID object
* validation. These codes are used by the {@link OrcidValidator} to returns the
* validation error related to a specific ORCID entity. The values of this enum
* are returned from the OrcidHistoryRestRepository and can be used to show an
* error message to the users when they tries to synchronize some data with
* ORCID.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public enum OrcidValidationError {
AMOUNT_CURRENCY_REQUIRED("amount-currency.required"),
EXTERNAL_ID_REQUIRED("external-id.required"),
TITLE_REQUIRED("title.required"),
TYPE_REQUIRED("type.required"),
FUNDER_REQUIRED("funder.required"),
INVALID_COUNTRY("country.invalid"),
ORGANIZATION_NAME_REQUIRED("organization.name-required"),
PUBLICATION_DATE_INVALID("publication.date-invalid"),
ORGANIZATION_ADDRESS_REQUIRED("organization.address-required"),
ORGANIZATION_CITY_REQUIRED("organization.city-required"),
ORGANIZATION_COUNTRY_REQUIRED("organization.country-required"),
DISAMBIGUATED_ORGANIZATION_REQUIRED("disambiguated-organization.required"),
DISAMBIGUATED_ORGANIZATION_VALUE_REQUIRED("disambiguated-organization.value-required"),
DISAMBIGUATION_SOURCE_REQUIRED("disambiguation-source.required"),
DISAMBIGUATION_SOURCE_INVALID("disambiguation-source.invalid");
private final String code;
private OrcidValidationError(String code) {
this.code = code;
}
public String getCode() {
return code;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.validator;
import java.util.List;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.Work;
/**
* Interface for classes that validate the ORCID entity objects.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidValidator {
/**
* Validate the given orcid object and returns the validation errors, if any.
*
* @param object the ORCID object to validate
* @return the validation errors, if any
*/
List<OrcidValidationError> validate(Object object);
/**
* Validate the given work and returns the validation errors, if any.
*
* @param work the work to validate
* @return the validation errors, if any
*/
List<OrcidValidationError> validateWork(Work work);
/**
* Validate the given funding and returns the validation errors, if any.
*
* @param funding the funding to validate
* @return the validation errors, if any
*/
List<OrcidValidationError> validateFunding(Funding funding);
}

View File

@@ -0,0 +1,235 @@
/**
* 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.validator.impl;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.lang3.ArrayUtils.contains;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.dspace.orcid.model.validator.OrcidValidationError.AMOUNT_CURRENCY_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATED_ORGANIZATION_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATED_ORGANIZATION_VALUE_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATION_SOURCE_INVALID;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATION_SOURCE_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.EXTERNAL_ID_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.FUNDER_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_ADDRESS_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_CITY_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_COUNTRY_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_NAME_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.PUBLICATION_DATE_INVALID;
import static org.dspace.orcid.model.validator.OrcidValidationError.TITLE_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.TYPE_REQUIRED;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.dspace.orcid.model.validator.OrcidValidationError;
import org.dspace.orcid.model.validator.OrcidValidator;
import org.dspace.services.ConfigurationService;
import org.orcid.jaxb.model.v3.release.common.DisambiguatedOrganization;
import org.orcid.jaxb.model.v3.release.common.Organization;
import org.orcid.jaxb.model.v3.release.common.OrganizationAddress;
import org.orcid.jaxb.model.v3.release.common.PublicationDate;
import org.orcid.jaxb.model.v3.release.common.Year;
import org.orcid.jaxb.model.v3.release.record.ExternalIDs;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.FundingTitle;
import org.orcid.jaxb.model.v3.release.record.Work;
import org.orcid.jaxb.model.v3.release.record.WorkTitle;
/**
* Implementation of {@link OrcidValidator}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidValidatorImpl implements OrcidValidator {
private final ConfigurationService configurationService;
public OrcidValidatorImpl(ConfigurationService configurationService) {
this.configurationService = configurationService;
}
@Override
public List<OrcidValidationError> validate(Object object) {
if (object instanceof Work && isWorkValidationEnabled()) {
return validateWork((Work) object);
}
if (object instanceof Funding && isFundingValidationEnabled()) {
return validateFunding((Funding) object);
}
return Collections.emptyList();
}
/**
* A work is valid if has title, type, a valid publication date and at least one
* external id.
*/
@Override
public List<OrcidValidationError> validateWork(Work work) {
List<OrcidValidationError> errors = new ArrayList<OrcidValidationError>();
WorkTitle title = work.getWorkTitle();
if (title == null || title.getTitle() == null || isBlank(title.getTitle().getContent())) {
errors.add(TITLE_REQUIRED);
}
if (work.getWorkType() == null) {
errors.add(TYPE_REQUIRED);
}
ExternalIDs externalIdentifiers = work.getExternalIdentifiers();
if (externalIdentifiers == null || isEmpty(externalIdentifiers.getExternalIdentifier())) {
errors.add(EXTERNAL_ID_REQUIRED);
}
PublicationDate publicationDate = work.getPublicationDate();
if (publicationDate != null && isYearNotValid(publicationDate)) {
errors.add(PUBLICATION_DATE_INVALID);
}
return errors;
}
/**
* A funding is valid if has title, a valid funder organization and at least one
* external id. If it has an amount, the amount currency is required.
*/
@Override
public List<OrcidValidationError> validateFunding(Funding funding) {
List<OrcidValidationError> errors = new ArrayList<OrcidValidationError>();
FundingTitle title = funding.getTitle();
if (title == null || title.getTitle() == null || isBlank(title.getTitle().getContent())) {
errors.add(TITLE_REQUIRED);
}
ExternalIDs externalIdentifiers = funding.getExternalIdentifiers();
if (externalIdentifiers == null || isEmpty(externalIdentifiers.getExternalIdentifier())) {
errors.add(EXTERNAL_ID_REQUIRED);
}
if (funding.getOrganization() == null) {
errors.add(FUNDER_REQUIRED);
} else {
errors.addAll(validate(funding.getOrganization()));
}
if (funding.getAmount() != null && isBlank(funding.getAmount().getCurrencyCode())) {
errors.add(AMOUNT_CURRENCY_REQUIRED);
}
return errors;
}
/**
* The organization is valid if it has a name, a valid address and a valid
* disambiguated-organization complex type.
*/
private List<OrcidValidationError> validate(Organization organization) {
List<OrcidValidationError> errors = new ArrayList<OrcidValidationError>();
if (isBlank(organization.getName())) {
errors.add(ORGANIZATION_NAME_REQUIRED);
}
errors.addAll(validate(organization.getAddress()));
errors.addAll(validate(organization.getDisambiguatedOrganization()));
return errors;
}
/**
* A disambiguated-organization type is valid if it has an identifier and a
* valid source (the valid values for sources are configured with
* orcid.validation.organization.identifier-sources)
*/
private List<OrcidValidationError> validate(DisambiguatedOrganization disambiguatedOrganization) {
List<OrcidValidationError> errors = new ArrayList<OrcidValidationError>();
if (disambiguatedOrganization == null) {
errors.add(DISAMBIGUATED_ORGANIZATION_REQUIRED);
return errors;
}
if (isBlank(disambiguatedOrganization.getDisambiguatedOrganizationIdentifier())) {
errors.add(DISAMBIGUATED_ORGANIZATION_VALUE_REQUIRED);
}
String disambiguationSource = disambiguatedOrganization.getDisambiguationSource();
if (isBlank(disambiguationSource)) {
errors.add(DISAMBIGUATION_SOURCE_REQUIRED);
} else if (isInvalidDisambiguationSource(disambiguationSource)) {
errors.add(DISAMBIGUATION_SOURCE_INVALID);
}
return errors;
}
/**
* An organization address is valid if it has a city and a country.
*/
private List<OrcidValidationError> validate(OrganizationAddress address) {
List<OrcidValidationError> errors = new ArrayList<OrcidValidationError>();
if (address == null) {
errors.add(ORGANIZATION_ADDRESS_REQUIRED);
return errors;
}
if (isBlank(address.getCity())) {
errors.add(ORGANIZATION_CITY_REQUIRED);
}
if (address.getCountry() == null) {
errors.add(ORGANIZATION_COUNTRY_REQUIRED);
}
return errors;
}
private boolean isYearNotValid(PublicationDate publicationDate) {
Year year = publicationDate.getYear();
if (year == null) {
return true;
}
try {
return Integer.valueOf(year.getValue()) < 1900;
} catch (NumberFormatException ex) {
return true;
}
}
private boolean isInvalidDisambiguationSource(String disambiguationSource) {
return !contains(getDisambiguedOrganizationSources(), disambiguationSource);
}
private String[] getDisambiguedOrganizationSources() {
return configurationService.getArrayProperty("orcid.validation.organization.identifier-sources");
}
private boolean isWorkValidationEnabled() {
return configurationService.getBooleanProperty("orcid.validation.work.enabled", true);
}
private boolean isFundingValidationEnabled() {
return configurationService.getBooleanProperty("orcid.validation.funding.enabled", true);
}
}

View File

@@ -0,0 +1,331 @@
/**
* 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.script;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.dspace.profile.OrcidSynchronizationMode.BATCH;
import static org.dspace.profile.OrcidSynchronizationMode.MANUAL;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.exception.OrcidValidationException;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.profile.OrcidSynchronizationMode;
import org.dspace.scripts.DSpaceRunnable;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.utils.DSpace;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Script that perform the bulk synchronization with ORCID registry of all the
* ORCID queue records that has an profileItem that configure the
* synchronization mode equals to BATCH.
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidBulkPush extends DSpaceRunnable<OrcidBulkPushScriptConfiguration<OrcidBulkPush>> {
private static final Logger LOGGER = LoggerFactory.getLogger(OrcidBulkPush.class);
private OrcidQueueService orcidQueueService;
private OrcidHistoryService orcidHistoryService;
private OrcidSynchronizationService orcidSynchronizationService;
private ConfigurationService configurationService;
private Context context;
/**
* Cache that stores the synchronization mode set for a specific profile item.
*/
private Map<Item, OrcidSynchronizationMode> synchronizationModeByProfileItem = new HashMap<>();
private boolean ignoreMaxAttempts = false;
@Override
public void setup() throws ParseException {
OrcidServiceFactory orcidServiceFactory = OrcidServiceFactory.getInstance();
this.orcidQueueService = orcidServiceFactory.getOrcidQueueService();
this.orcidHistoryService = orcidServiceFactory.getOrcidHistoryService();
this.orcidSynchronizationService = orcidServiceFactory.getOrcidSynchronizationService();
this.configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
if (commandLine.hasOption('f')) {
ignoreMaxAttempts = true;
}
}
@Override
public void internalRun() throws Exception {
if (isOrcidSynchronizationDisabled()) {
handler.logWarning("The ORCID synchronization is disabled. The script cannot proceed");
return;
}
context = new Context();
assignCurrentUserInContext();
try {
context.turnOffAuthorisationSystem();
performBulkSynchronization();
context.complete();
} catch (Exception e) {
handler.handleException(e);
context.abort();
} finally {
context.restoreAuthSystemState();
}
}
/**
* Find all the Orcid Queue records that need to be synchronized and perfom the
* synchronization.
*/
private void performBulkSynchronization() throws SQLException {
List<OrcidQueue> queueRecords = findQueueRecordsToSynchronize();
handler.logInfo("Found " + queueRecords.size() + " queue records to synchronize with ORCID");
for (OrcidQueue queueRecord : queueRecords) {
performSynchronization(queueRecord);
}
}
/**
* Returns all the stored Orcid Queue records (ignoring or not the max attempts)
* related to a profile that has the synchronization mode set to BATCH.
*/
private List<OrcidQueue> findQueueRecordsToSynchronize() throws SQLException {
return findQueueRecords().stream()
.filter(record -> getProfileItemSynchronizationMode(record.getProfileItem()) == BATCH)
.collect(Collectors.toList());
}
/**
* If the current script execution is configued to ignore the max attemps,
* returns all the ORCID Queue records, otherwise returns the ORCID Queue
* records that has an attempts value less than the configured max attempts
* value.
*/
private List<OrcidQueue> findQueueRecords() throws SQLException {
if (ignoreMaxAttempts) {
return orcidQueueService.findAll(context);
} else {
int attempts = configurationService.getIntProperty("orcid.bulk-synchronization.max-attempts");
return orcidQueueService.findByAttemptsLessThan(context, attempts);
}
}
/**
* Try to synchronize the given queue record with ORCID, handling any errors.
*/
private void performSynchronization(OrcidQueue queueRecord) {
try {
queueRecord = reload(queueRecord);
handler.logInfo(getOperationInfoMessage(queueRecord));
OrcidHistory orcidHistory = orcidHistoryService.synchronizeWithOrcid(context, queueRecord, false);
handler.logInfo(getSynchronizationResultMessage(orcidHistory));
commitTransaction();
} catch (OrcidValidationException ex) {
rollbackTransaction();
handler.logError(getValidationErrorMessage(ex));
} catch (Exception ex) {
rollbackTransaction();
String errorMessage = getUnexpectedErrorMessage(ex);
LOGGER.error(errorMessage, ex);
handler.logError(errorMessage);
} finally {
incrementAttempts(queueRecord);
}
}
/**
* Returns the Synchronization mode related to the given profile item.
*/
private OrcidSynchronizationMode getProfileItemSynchronizationMode(Item profileItem) {
OrcidSynchronizationMode synchronizationMode = synchronizationModeByProfileItem.get(profileItem);
if (synchronizationMode == null) {
synchronizationMode = orcidSynchronizationService.getSynchronizationMode(profileItem).orElse(MANUAL);
synchronizationModeByProfileItem.put(profileItem, synchronizationMode);
}
return synchronizationMode;
}
/**
* Returns an info log message with the details of the given record's operation.
* This message is logged before ORCID synchronization.
*/
private String getOperationInfoMessage(OrcidQueue record) {
UUID profileItemId = record.getProfileItem().getID();
String putCode = record.getPutCode();
String type = record.getRecordType();
if (record.getOperation() == null) {
return "Synchronization of " + type + " data for profile with ID: " + profileItemId;
}
switch (record.getOperation()) {
case INSERT:
return "Addition of " + type + " for profile with ID: " + profileItemId;
case UPDATE:
return "Update of " + type + " for profile with ID: " + profileItemId + " by put code " + putCode;
case DELETE:
return "Deletion of " + type + " for profile with ID: " + profileItemId + " by put code " + putCode;
default:
return "Synchronization of " + type + " data for profile with ID: " + profileItemId;
}
}
/**
* Returns an info log message with the details of the synchronization result.
* This message is logged after ORCID synchronization.
*/
private String getSynchronizationResultMessage(OrcidHistory orcidHistory) {
String message = "History record created with status " + orcidHistory.getStatus();
switch (orcidHistory.getStatus()) {
case 201:
case 200:
case 204:
message += ". The operation was completed successfully";
break;
case 400:
message += ". The resource sent to ORCID registry is not valid";
break;
case 404:
message += ". The resource does not exists anymore on the ORCID registry";
break;
case 409:
message += ". The resource is already present on the ORCID registry";
break;
case 500:
message += ". An internal server error on ORCID registry side occurs";
break;
default:
message += ". Details: " + orcidHistory.getResponseMessage();
break;
}
return message;
}
private String getValidationErrorMessage(OrcidValidationException ex) {
return ex.getMessage();
}
private String getUnexpectedErrorMessage(Exception ex) {
return "An unexpected error occurs during the synchronization: " + getRootMessage(ex);
}
private void incrementAttempts(OrcidQueue queueRecord) {
queueRecord = reload(queueRecord);
if (queueRecord == null) {
return;
}
try {
queueRecord.setAttempts(queueRecord.getAttempts() != null ? queueRecord.getAttempts() + 1 : 1);
orcidQueueService.update(context, queueRecord);
commitTransaction();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* This method will assign the currentUser to the {@link Context}. The instance
* of the method in this class will fetch the EPersonIdentifier from this class,
* this identifier was given to this class upon instantiation, it'll then be
* used to find the {@link EPerson} associated with it and this {@link EPerson}
* will be set as the currentUser of the created {@link Context}
*/
private void assignCurrentUserInContext() throws SQLException {
UUID uuid = getEpersonIdentifier();
if (uuid != null) {
EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid);
context.setCurrentUser(ePerson);
}
}
private OrcidQueue reload(OrcidQueue queueRecord) {
try {
return context.reloadEntity(queueRecord);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private void commitTransaction() {
try {
context.commit();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private void rollbackTransaction() {
try {
context.rollback();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String getRootMessage(Exception ex) {
String message = ExceptionUtils.getRootCauseMessage(ex);
return isNotEmpty(message) ? message.substring(message.indexOf(":") + 1).trim() : "Generic error";
}
private boolean isOrcidSynchronizationDisabled() {
return !configurationService.getBooleanProperty("orcid.synchronization-enabled", true);
}
@Override
@SuppressWarnings("unchecked")
public OrcidBulkPushScriptConfiguration<OrcidBulkPush> getScriptConfiguration() {
return new DSpace().getServiceManager().getServiceByName("orcid-bulk-push",
OrcidBulkPushScriptConfiguration.class);
}
}

View File

@@ -0,0 +1,65 @@
/**
* 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.script;
import java.sql.SQLException;
import org.apache.commons.cli.Options;
import org.dspace.authorize.service.AuthorizeService;
import org.dspace.core.Context;
import org.dspace.scripts.configuration.ScriptConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Script configuration for {@link OrcidBulkPush}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
* @param <T> the OrcidBulkPush type
*/
public class OrcidBulkPushScriptConfiguration<T extends OrcidBulkPush> extends ScriptConfiguration<T> {
@Autowired
private AuthorizeService authorizeService;
private Class<T> dspaceRunnableClass;
@Override
public boolean isAllowedToExecute(Context context) {
try {
return authorizeService.isAdmin(context);
} catch (SQLException e) {
throw new RuntimeException("SQLException occurred when checking if the current user is an admin", e);
}
}
@Override
public Class<T> getDspaceRunnableClass() {
return dspaceRunnableClass;
}
@Override
public void setDspaceRunnableClass(Class<T> dspaceRunnableClass) {
this.dspaceRunnableClass = dspaceRunnableClass;
}
@Override
public Options getOptions() {
if (options == null) {
Options options = new Options();
options.addOption("f", "force", false, "force the synchronization ignoring maximum attempts");
options.getOption("f").setType(boolean.class);
options.getOption("f").setRequired(false);
super.options = options;
}
return options;
}
}

View File

@@ -0,0 +1,48 @@
/**
* 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.service;
import java.util.List;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
/**
* Interface that mark classes that can be used to generate a signature for
* metadata values. The signature must be a unique identification of a metadata,
* based on the attributes that compose it (such as field, value and authority).
* It is possible to generate a signature for a single metadata value and also
* for a list of values. Given an item, a signature can for example be used to
* check if the associated metadata is present in the item.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface MetadataSignatureGenerator {
/**
* Generate a signature related to the given metadata values.
*
* @param context the DSpace context
* @param metadataValues the metadata values to sign
* @return the generated signature
*/
public String generate(Context context, List<MetadataValue> metadataValues);
/**
* Returns the metadata values traceable by the given item related with the
* given signature.
*
* @param context the DSpace context
* @param item the item
* @param signature the metadata signature
* @return the founded metadata
*/
public List<MetadataValue> findBySignature(Context context, Item item, String signature);
}

View File

@@ -0,0 +1,32 @@
/**
* 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.service;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.orcid.jaxb.model.v3.release.record.Activity;
/**
* Interface that mark classes that handle the configured instance of
* {@link OrcidEntityFactory}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidEntityFactoryService {
/**
* Builds an ORCID Activity object starting from the given item. The actual type
* of Activity constructed depends on the entity type of the input item.
*
* @param context the DSpace context
* @param item the item
* @return the created object
*/
Activity createOrcidObject(Context context, Item item);
}

View File

@@ -0,0 +1,152 @@
/**
* 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.service;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.exception.OrcidValidationException;
/**
* Interface of service to manage OrcidHistory.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*/
public interface OrcidHistoryService {
/**
* Get an OrcidHistory from the database.
*
* @param context DSpace context object
* @param id ID of the OrcidHistory
* @return the OrcidHistory format, or null if the ID is invalid.
* @throws SQLException if database error
*/
public OrcidHistory find(Context context, int id) throws SQLException;
/**
* Find all the ORCID history records.
*
* @param context DSpace context object
* @return the ORCID history records
* @throws SQLException if an SQL error occurs
*/
public List<OrcidHistory> findAll(Context context) throws SQLException;
/**
* Get the OrcidHistory records where the given item is the profile item OR the
* entity
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidHistory entities
* @throws SQLException if database error
*/
public List<OrcidHistory> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Find the OrcidHistory records related to the given entity item.
*
* @param context DSpace context object
* @param entity the entity item
* @return the found put codes
* @throws SQLException if database error
*/
public List<OrcidHistory> findByEntity(Context context, Item entity) throws SQLException;
/**
* Create a new OrcidHistory records related to the given profileItem and entity
* items.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @return the created orcid history record
* @throws SQLException if database error
*/
public OrcidHistory create(Context context, Item profileItem, Item entity) throws SQLException;
/**
* Delete an OrcidHistory
*
* @param context context
* @param orcidHistory the OrcidHistory entity to delete
* @throws SQLException if database error
*/
public void delete(Context context, OrcidHistory orcidHistory) throws SQLException;
/**
* Update the OrcidHistory
*
* @param context context
* @param orcidHistory the OrcidHistory entity to update
* @throws SQLException if database error
*/
public void update(Context context, OrcidHistory orcidHistory) throws SQLException;
/**
* Find the last put code related to the given profileItem and entity item.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @return the found put code, if any
* @throws SQLException if database error
*/
public Optional<String> findLastPutCode(Context context, Item profileItem, Item entity) throws SQLException;
/**
* Find all the last put code related to the entity item each associated with
* the profileItem to which it refers.
*
* @param context DSpace context object
* @param entity the entity item
* @return a map that relates the profileItems with the identified
* putCode
* @throws SQLException if database error
*/
public Map<Item, String> findLastPutCodes(Context context, Item entity) throws SQLException;
/**
* Find all the successfully Orcid history records with the given record type
* related to the given entity. An history record is considered successful if
* the status is between 200 and 300.
*
* @param context DSpace context object
* @param entity the entity item
* @param recordType the record type
* @return the found orcid history records
* @throws SQLException if database error
*/
List<OrcidHistory> findSuccessfullyRecordsByEntityAndType(Context context, Item entity, String recordType)
throws SQLException;
/**
* Synchronize the entity related to the given orcidQueue record with ORCID.
*
* @param context DSpace context object
* @param orcidQueue the orcid queue record that has the
* references of the data to be synchronized
* @param forceAddition to force the insert on the ORCID registry
* @return the created orcid history record with the
* synchronization result
* @throws SQLException if database error
* @throws OrcidValidationException if the data to synchronize with ORCID is not
* valid
*/
public OrcidHistory synchronizeWithOrcid(Context context, OrcidQueue orcidQueue, boolean forceAddition)
throws SQLException, OrcidValidationException;
}

View File

@@ -0,0 +1,55 @@
/**
* 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.service;
import java.util.List;
import java.util.Optional;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.orcid.model.factory.OrcidProfileSectionFactory;
import org.dspace.profile.OrcidProfileSyncPreference;
/**
* Interface that mark classes that handle the configured instance of
* {@link OrcidProfileSectionFactory}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidProfileSectionFactoryService {
/**
* Returns the profile section factory of the given type.
*
* @param type the type of the section configurations to retrieve
* @return the section configurations of the given type
*/
Optional<OrcidProfileSectionFactory> findBySectionType(OrcidProfileSectionType type);
/**
* Returns all the profile section configurations relative to the given
* preferences.
*
* @param preferences the preferences to search for
* @return the section configurations
*/
List<OrcidProfileSectionFactory> findByPreferences(List<OrcidProfileSyncPreference> preferences);
/**
* Builds an ORCID object starting from the given metadata values compliance to
* the given profile section type.
*
* @param context the DSpace context
* @param metadataValues the metadata values
* @param type the profile section type
* @return the created object
*/
Object createOrcidObject(Context context, List<MetadataValue> metadataValues, OrcidProfileSectionType type);
}

View File

@@ -0,0 +1,260 @@
/**
* 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.service;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.profile.OrcidEntitySyncPreference;
/**
* Service that handles ORCID queue records.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OrcidQueueService {
/**
* Create an OrcidQueue record with the given profileItem and entity. The type
* of operation is calculated based on whether or not the given entity was
* already pushed to the ORCID registry.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @return the stored record
* @throws SQLException if an SQL error occurs
*/
public OrcidQueue create(Context context, Item profileItem, Item entity) throws SQLException;
/**
* Create an OrcidQueue record with the given profileItem and entity to push new
* data to ORCID.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @return the stored record
* @throws SQLException if an SQL error occurs
*/
public OrcidQueue createEntityInsertionRecord(Context context, Item profileItem, Item entity) throws SQLException;
/**
* Create an OrcidQueue record with the given profileItem to update a record on
* ORCID with the given putCode.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @param putCode the putCode related to the given entity item
* @return the stored record
* @throws SQLException if an SQL error occurs
*/
public OrcidQueue createEntityUpdateRecord(Context context, Item profileItem, Item entity, String putCode)
throws SQLException;
/**
* Create an OrcidQueue record with the given profileItem to delete a record on
* ORCID related to the given entity type with the given putCode.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param description the orcid queue record description
* @param type the type of the entity item
* @param putCode the putCode related to the given entity item
* @return the stored record
* @throws SQLException if an SQL error occurs
*/
OrcidQueue createEntityDeletionRecord(Context context, Item profileItem, String description, String type,
String putCode)
throws SQLException;
/**
* Create an OrcidQueue record with the profile to add data to ORCID.
*
* @param context DSpace context object
* @param profile the profile item
* @param description the record description
* @param recordType the record type
* @param metadata the metadata signature
* @return the stored record
* @throws SQLException if an SQL error occurs
*/
OrcidQueue createProfileInsertionRecord(Context context, Item profile, String description, String recordType,
String metadata) throws SQLException;
/**
* Create an OrcidQueue record with the profile to remove data from ORCID.
*
* @param context DSpace context object
* @param profile the profile item
* @param description the record description
* @param recordType the record type
* @param putCode the putCode
* @return the stored record
* @throws SQLException if an SQL error occurs
*/
OrcidQueue createProfileDeletionRecord(Context context, Item profile, String description, String recordType,
String metadata, String putCode) throws SQLException;
/**
* Find all the ORCID queue records.
*
* @param context DSpace context object
* @return the ORCID queue records
* @throws SQLException if an SQL error occurs
*/
public List<OrcidQueue> findAll(Context context) throws SQLException;
/**
* Get the orcid queue records by the profileItem id.
*
* @param context DSpace context object
* @param profileItemId the profileItem item id
* @return the orcid queue records
* @throws SQLException if an SQL error occurs
*/
public List<OrcidQueue> findByProfileItemId(Context context, UUID profileItemId) throws SQLException;
/**
* Get the orcid queue records by the profileItem id.
*
* @param context DSpace context object
* @param profileItemId the profileItem item id
* @param limit limit
* @param offset offset
* @return the orcid queue records
* @throws SQLException if an SQL error occurs
*/
public List<OrcidQueue> findByProfileItemId(Context context, UUID profileItemId, Integer limit, Integer offset)
throws SQLException;
/**
* Get the orcid queue records by the profileItem and entity.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param entity the entity item
* @return the found OrcidQueue records
* @throws SQLException if an SQL error occurs
*/
public List<OrcidQueue> findByProfileItemAndEntity(Context context, Item profileItem, Item entity)
throws SQLException;
/**
* Get the OrcidQueue records where the given item is the profileItem OR the
* entity
*
* @param context DSpace context object
* @param item the item to search for
* @return the found OrcidQueue records
* @throws SQLException if database error
*/
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException;
/**
* Get all the OrcidQueue records with attempts less than the given attempts.
*
* @param context DSpace context object
* @param attempts the maximum value of attempts
* @return the found OrcidQueue records
* @throws SQLException if database error
*/
public List<OrcidQueue> findByAttemptsLessThan(Context context, int attempts) throws SQLException;
/**
* Returns the number of records on the OrcidQueue associated with the given
* profileItemId.
*
* @param context DSpace context object
* @param profileItemId the profileItem item id
* @return the record's count
* @throws SQLException if an SQL error occurs
*/
long countByProfileItemId(Context context, UUID profileItemId) throws SQLException;
/**
* Delete the OrcidQueue record with the given id.
*
* @param context DSpace context object
* @param id the id of the record to be deleted
* @throws SQLException if an SQL error occurs
*/
public void deleteById(Context context, Integer id) throws SQLException;
/**
* Delete an OrcidQueue
*
* @param context DSpace context object
* @param orcidQueue the orcidQueue record to delete
* @throws SQLException if database error
* @throws AuthorizeException if authorization error
*/
public void delete(Context context, OrcidQueue orcidQueue) throws SQLException;
/**
* Delete all the OrcidQueue records with the given entity and record type.
*
* @param context DSpace context object
* @param entity the entity item
* @param recordType the record type
* @throws SQLException if database error occurs
*/
public void deleteByEntityAndRecordType(Context context, Item entity, String recordType) throws SQLException;
/**
* Delete all the OrcidQueue records with the given profileItem and record type.
*
* @param context DSpace context object
* @param profileItem the profileItem item
* @param recordType the record type
* @throws SQLException if database error occurs
*/
public void deleteByProfileItemAndRecordType(Context context, Item profileItem, String recordType)
throws SQLException;
/**
* Get an OrcidQueue from the database.
*
* @param context DSpace context object
* @param id ID of the OrcidQueue
* @return the OrcidQueue format, or null if the ID is invalid.
* @throws SQLException if database error
*/
public OrcidQueue find(Context context, int id) throws SQLException;
/**
* Update the OrcidQueue
*
* @param context context
* @param orcidQueue the OrcidQueue to update
* @throws SQLException if database error
*/
public void update(Context context, OrcidQueue orcidQueue) throws SQLException;
/**
* Recalculates the ORCID queue records linked to the given profileItem as
* regards the entities of the given type. The recalculation is done based on
* the preference indicated.
*
* @param context context
* @param profileItem the profileItem
* @param entityType the entity type related to the records to recalculate
* @param preference the preference value on which to base the recalculation
* @throws SQLException if database error
*/
public void recalculateOrcidQueue(Context context, Item profileItem, OrcidEntityType entityType,
OrcidEntitySyncPreference preference) throws SQLException;
}

View File

@@ -5,20 +5,20 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.service; package org.dspace.orcid.service;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import java.util.Optional; 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.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.profile.OrcidEntitySyncPreference;
import org.dspace.profile.OrcidProfileDisconnectionMode;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.dspace.profile.OrcidSynchronizationMode;
/** /**
* Service that handle the the syncronization between a DSpace profile and the * Service that handle the the syncronization between a DSpace profile and the
@@ -110,6 +110,17 @@ public interface OrcidSynchronizationService {
public boolean setSynchronizationMode(Context context, Item profile, OrcidSynchronizationMode value) public boolean setSynchronizationMode(Context context, Item profile, OrcidSynchronizationMode value)
throws SQLException; throws SQLException;
/**
* Check if the given researcher profile item is configured to synchronize the
* given item with ORCID.
*
* @param profile the researcher profile item
* @param item the entity type to check
* @return true if the given entity type can be synchronize with ORCID,
* false otherwise
*/
public boolean isSynchronizationAllowed(Item profile, Item item);
/** /**
* Returns the ORCID synchronization mode configured for the given profile item. * Returns the ORCID synchronization mode configured for the given profile item.
* *

View File

@@ -5,12 +5,12 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.service; package org.dspace.orcid.service;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.orcid.OrcidToken;
/** /**
* Service that handle {@link OrcidToken} entities. * Service that handle {@link OrcidToken} entities.

View File

@@ -0,0 +1,62 @@
/**
* 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.service.impl;
import static java.util.stream.Collectors.toMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import org.dspace.content.Item;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.factory.OrcidEntityFactory;
import org.dspace.orcid.service.OrcidEntityFactoryService;
import org.orcid.jaxb.model.v3.release.record.Activity;
/**
* Implementation of {@link OrcidEntityFactoryService}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidEntityFactoryServiceImpl implements OrcidEntityFactoryService {
/**
* Message of the exception thrown if the given item is not a valid entity for
* ORCID (defined with the entityFactories map).
*/
private final String INVALID_ENTITY_MSG = "The item with id %s is not a configured Orcid entity";
private final Map<OrcidEntityType, OrcidEntityFactory> entityFactories;
private final ItemService itemService;
private OrcidEntityFactoryServiceImpl(List<OrcidEntityFactory> entityFactories, ItemService itemService) {
this.itemService = itemService;
this.entityFactories = entityFactories.stream()
.collect(toMap(OrcidEntityFactory::getEntityType, Function.identity()));
}
@Override
public Activity createOrcidObject(Context context, Item item) {
OrcidEntityFactory factory = getOrcidEntityType(item)
.map(entityType -> entityFactories.get(entityType))
.orElseThrow(() -> new IllegalArgumentException(String.format(INVALID_ENTITY_MSG, item.getID())));
return factory.createOrcidObject(context, item);
}
private Optional<OrcidEntityType> getOrcidEntityType(Item item) {
return Optional.ofNullable(OrcidEntityType.fromEntityType(itemService.getEntityTypeLabel(item)));
}
}

View File

@@ -0,0 +1,360 @@
/**
* 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.service.impl;
import static java.lang.String.format;
import static java.util.Comparator.comparing;
import static java.util.Comparator.naturalOrder;
import static java.util.Comparator.nullsFirst;
import static java.util.Optional.ofNullable;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.math.NumberUtils.isCreatable;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpStatus;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidResponse;
import org.dspace.orcid.dao.OrcidHistoryDAO;
import org.dspace.orcid.dao.OrcidQueueDAO;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.exception.OrcidValidationException;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.orcid.model.validator.OrcidValidationError;
import org.dspace.orcid.model.validator.OrcidValidator;
import org.dspace.orcid.service.MetadataSignatureGenerator;
import org.dspace.orcid.service.OrcidEntityFactoryService;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidProfileSectionFactoryService;
import org.dspace.orcid.service.OrcidTokenService;
import org.orcid.jaxb.model.v3.release.record.Activity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link OrcidHistoryService}.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidHistoryServiceImpl implements OrcidHistoryService {
private static final Logger LOGGER = LoggerFactory.getLogger(OrcidHistoryServiceImpl.class);
@Autowired
private OrcidHistoryDAO orcidHistoryDAO;
@Autowired
private OrcidQueueDAO orcidQueueDAO;
@Autowired
private ItemService itemService;
@Autowired
private OrcidProfileSectionFactoryService profileFactoryService;
@Autowired
private OrcidEntityFactoryService activityFactoryService;
@Autowired
private MetadataSignatureGenerator metadataSignatureGenerator;
@Autowired
private OrcidClient orcidClient;
@Autowired
private OrcidValidator orcidValidator;
@Autowired
private OrcidTokenService orcidTokenService;
@Override
public OrcidHistory find(Context context, int id) throws SQLException {
return orcidHistoryDAO.findByID(context, OrcidHistory.class, id);
}
@Override
public List<OrcidHistory> findAll(Context context) throws SQLException {
return orcidHistoryDAO.findAll(context, OrcidHistory.class);
}
@Override
public List<OrcidHistory> findByProfileItemOrEntity(Context context, Item profileItem) throws SQLException {
return orcidHistoryDAO.findByProfileItemOrEntity(context, profileItem);
}
@Override
public OrcidHistory create(Context context, Item profileItem, Item entity) throws SQLException {
OrcidHistory orcidHistory = new OrcidHistory();
orcidHistory.setEntity(entity);
orcidHistory.setProfileItem(profileItem);
return orcidHistoryDAO.create(context, orcidHistory);
}
@Override
public void delete(Context context, OrcidHistory orcidHistory) throws SQLException {
orcidHistoryDAO.delete(context, orcidHistory);
}
@Override
public void update(Context context, OrcidHistory orcidHistory) throws SQLException {
if (orcidHistory != null) {
orcidHistoryDAO.save(context, orcidHistory);
}
}
@Override
public Optional<String> findLastPutCode(Context context, Item profileItem, Item entity) throws SQLException {
List<OrcidHistory> records = orcidHistoryDAO.findByProfileItemAndEntity(context, profileItem.getID(),
entity.getID());
return findLastPutCode(records, profileItem);
}
@Override
public Map<Item, String> findLastPutCodes(Context context, Item entity) throws SQLException {
Map<Item, String> profileItemAndPutCodeMap = new HashMap<Item, String>();
List<OrcidHistory> orcidHistoryRecords = findByEntity(context, entity);
for (OrcidHistory orcidHistoryRecord : orcidHistoryRecords) {
Item profileItem = orcidHistoryRecord.getProfileItem();
if (profileItemAndPutCodeMap.containsKey(profileItem)) {
continue;
}
findLastPutCode(orcidHistoryRecords, profileItem)
.ifPresent(putCode -> profileItemAndPutCodeMap.put(profileItem, putCode));
}
return profileItemAndPutCodeMap;
}
@Override
public List<OrcidHistory> findByEntity(Context context, Item entity) throws SQLException {
return orcidHistoryDAO.findByEntity(context, entity);
}
@Override
public List<OrcidHistory> findSuccessfullyRecordsByEntityAndType(Context context,
Item entity, String recordType) throws SQLException {
return orcidHistoryDAO.findSuccessfullyRecordsByEntityAndType(context, entity, recordType);
}
@Override
public OrcidHistory synchronizeWithOrcid(Context context, OrcidQueue orcidQueue, boolean forceAddition)
throws SQLException {
Item profileItem = orcidQueue.getProfileItem();
String orcid = getMetadataValue(profileItem, "person.identifier.orcid")
.orElseThrow(() -> new IllegalArgumentException(
format("The related profileItem item (id = %s) does not have an orcid", profileItem.getID())));
String token = getAccessToken(context, profileItem)
.orElseThrow(() -> new IllegalArgumentException(
format("The related profileItem item (id = %s) does not have an access token", profileItem.getID())));
OrcidOperation operation = calculateOperation(orcidQueue, forceAddition);
try {
OrcidResponse response = synchronizeWithOrcid(context, orcidQueue, orcid, token, operation);
OrcidHistory orcidHistory = createHistoryRecordFromOrcidResponse(context, orcidQueue, operation, response);
orcidQueueDAO.delete(context, orcidQueue);
return orcidHistory;
} catch (OrcidValidationException ex) {
throw ex;
} catch (OrcidClientException ex) {
LOGGER.error("An error occurs during the orcid synchronization of ORCID queue " + orcidQueue, ex);
return createHistoryRecordFromOrcidError(context, orcidQueue, operation, ex);
} catch (RuntimeException ex) {
LOGGER.warn("An unexpected error occurs during the orcid synchronization of ORCID queue " + orcidQueue, ex);
return createHistoryRecordFromGenericError(context, orcidQueue, operation, ex);
}
}
private OrcidResponse synchronizeWithOrcid(Context context, OrcidQueue orcidQueue, String orcid, String token,
OrcidOperation operation) throws SQLException {
if (isProfileSectionType(orcidQueue)) {
return synchronizeProfileDataWithOrcid(context, orcidQueue, orcid, token, operation);
} else if (isEntityType(orcidQueue)) {
return synchronizeEntityWithOrcid(context, orcidQueue, orcid, token, operation);
} else {
throw new IllegalArgumentException("The type of the given queue record could not be determined");
}
}
private OrcidOperation calculateOperation(OrcidQueue orcidQueue, boolean forceAddition) {
OrcidOperation operation = orcidQueue.getOperation();
if (operation == null) {
throw new IllegalArgumentException("The orcid queue record with id " + orcidQueue.getID()
+ " has no operation defined");
}
return operation != OrcidOperation.DELETE && forceAddition ? OrcidOperation.INSERT : operation;
}
private OrcidResponse synchronizeEntityWithOrcid(Context context, OrcidQueue orcidQueue,
String orcid, String token, OrcidOperation operation) throws SQLException {
if (operation == OrcidOperation.DELETE) {
return deleteEntityOnOrcid(context, orcid, token, orcidQueue);
} else {
return sendEntityToOrcid(context, orcid, token, orcidQueue, operation == OrcidOperation.UPDATE);
}
}
private OrcidResponse synchronizeProfileDataWithOrcid(Context context, OrcidQueue orcidQueue,
String orcid, String token, OrcidOperation operation) throws SQLException {
if (operation == OrcidOperation.INSERT) {
return sendProfileDataToOrcid(context, orcid, token, orcidQueue);
} else {
return deleteProfileDataOnOrcid(context, orcid, token, orcidQueue);
}
}
private OrcidResponse sendEntityToOrcid(Context context, String orcid, String token, OrcidQueue orcidQueue,
boolean toUpdate) {
Activity activity = activityFactoryService.createOrcidObject(context, orcidQueue.getEntity());
List<OrcidValidationError> validationErrors = orcidValidator.validate(activity);
if (CollectionUtils.isNotEmpty(validationErrors)) {
throw new OrcidValidationException(validationErrors);
}
if (toUpdate) {
activity.setPutCode(getPutCode(orcidQueue));
return orcidClient.update(token, orcid, activity, orcidQueue.getPutCode());
} else {
return orcidClient.push(token, orcid, activity);
}
}
private OrcidResponse sendProfileDataToOrcid(Context context, String orcid, String token, OrcidQueue orcidQueue) {
OrcidProfileSectionType recordType = OrcidProfileSectionType.fromString(orcidQueue.getRecordType());
String signature = orcidQueue.getMetadata();
Item person = orcidQueue.getEntity();
List<MetadataValue> metadataValues = metadataSignatureGenerator.findBySignature(context, person, signature);
Object orcidObject = profileFactoryService.createOrcidObject(context, metadataValues, recordType);
List<OrcidValidationError> validationErrors = orcidValidator.validate(orcidObject);
if (CollectionUtils.isNotEmpty(validationErrors)) {
throw new OrcidValidationException(validationErrors);
}
return orcidClient.push(token, orcid, orcidObject);
}
private OrcidResponse deleteProfileDataOnOrcid(Context context, String orcid, String token, OrcidQueue orcidQueue) {
OrcidProfileSectionType recordType = OrcidProfileSectionType.fromString(orcidQueue.getRecordType());
return orcidClient.deleteByPutCode(token, orcid, orcidQueue.getPutCode(), recordType.getPath());
}
private OrcidResponse deleteEntityOnOrcid(Context context, String orcid, String token, OrcidQueue orcidQueue) {
OrcidEntityType recordType = OrcidEntityType.fromEntityType(orcidQueue.getRecordType());
return orcidClient.deleteByPutCode(token, orcid, orcidQueue.getPutCode(), recordType.getPath());
}
private OrcidHistory createHistoryRecordFromGenericError(Context context, OrcidQueue orcidQueue,
OrcidOperation operation, RuntimeException ex) throws SQLException {
return create(context, orcidQueue, ex.getMessage(), operation, 500, null);
}
private OrcidHistory createHistoryRecordFromOrcidError(Context context, OrcidQueue orcidQueue,
OrcidOperation operation, OrcidClientException ex) throws SQLException {
return create(context, orcidQueue, ex.getMessage(), operation, ex.getStatus(), null);
}
private OrcidHistory createHistoryRecordFromOrcidResponse(Context context, OrcidQueue orcidQueue,
OrcidOperation operation, OrcidResponse orcidResponse) throws SQLException {
int status = orcidResponse.getStatus();
if (operation == OrcidOperation.DELETE && orcidResponse.isNotFoundStatus()) {
status = HttpStatus.SC_NO_CONTENT;
}
return create(context, orcidQueue, orcidResponse.getContent(), operation, status, orcidResponse.getPutCode());
}
private OrcidHistory create(Context context, OrcidQueue orcidQueue, String responseMessage,
OrcidOperation operation, int status, String putCode) throws SQLException {
OrcidHistory history = new OrcidHistory();
history.setEntity(orcidQueue.getEntity());
history.setProfileItem(orcidQueue.getProfileItem());
history.setResponseMessage(responseMessage);
history.setStatus(status);
history.setPutCode(putCode);
history.setRecordType(orcidQueue.getRecordType());
history.setMetadata(orcidQueue.getMetadata());
history.setOperation(operation);
history.setDescription(orcidQueue.getDescription());
return orcidHistoryDAO.create(context, history);
}
private Optional<String> getMetadataValue(Item item, String metadataField) {
return ofNullable(itemService.getMetadataFirstValue(item, new MetadataFieldName(metadataField), Item.ANY))
.filter(StringUtils::isNotBlank);
}
private Optional<String> getAccessToken(Context context, Item item) {
return ofNullable(orcidTokenService.findByProfileItem(context, item))
.map(orcidToken -> orcidToken.getAccessToken());
}
private boolean isProfileSectionType(OrcidQueue orcidQueue) {
return OrcidProfileSectionType.isValid(orcidQueue.getRecordType());
}
private boolean isEntityType(OrcidQueue orcidQueue) {
return OrcidEntityType.isValidEntityType(orcidQueue.getRecordType());
}
private Optional<String> findLastPutCode(List<OrcidHistory> orcidHistoryRecords, Item profileItem) {
return orcidHistoryRecords.stream()
.filter(orcidHistoryRecord -> profileItem.equals(orcidHistoryRecord.getProfileItem()))
.sorted(comparing(OrcidHistory::getTimestamp, nullsFirst(naturalOrder())).reversed())
.map(history -> history.getPutCode())
.filter(putCode -> isNotBlank(putCode))
.findFirst();
}
private Long getPutCode(OrcidQueue orcidQueue) {
return isCreatable(orcidQueue.getPutCode()) ? Long.valueOf(orcidQueue.getPutCode()) : null;
}
public OrcidClient getOrcidClient() {
return orcidClient;
}
public void setOrcidClient(OrcidClient orcidClient) {
this.orcidClient = orcidClient;
}
}

View File

@@ -0,0 +1,61 @@
/**
* 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.service.impl;
import static java.util.stream.Collectors.toMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.orcid.model.factory.OrcidProfileSectionFactory;
import org.dspace.orcid.service.OrcidProfileSectionFactoryService;
import org.dspace.profile.OrcidProfileSyncPreference;
/**
* Implementation of {@link OrcidProfileSectionFactoryService}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidProfileSectionFactoryServiceImpl implements OrcidProfileSectionFactoryService {
private final Map<OrcidProfileSectionType, OrcidProfileSectionFactory> sectionFactories;
private OrcidProfileSectionFactoryServiceImpl(List<OrcidProfileSectionFactory> sectionFactories) {
this.sectionFactories = sectionFactories.stream()
.collect(toMap(OrcidProfileSectionFactory::getProfileSectionType, Function.identity()));
}
@Override
public Optional<OrcidProfileSectionFactory> findBySectionType(OrcidProfileSectionType type) {
return Optional.ofNullable(this.sectionFactories.get(type));
}
@Override
public List<OrcidProfileSectionFactory> findByPreferences(List<OrcidProfileSyncPreference> preferences) {
return filterBy(configuration -> preferences.contains(configuration.getSynchronizationPreference()));
}
@Override
public Object createOrcidObject(Context context, List<MetadataValue> metadataValues, OrcidProfileSectionType type) {
OrcidProfileSectionFactory profileSectionFactory = findBySectionType(type)
.orElseThrow(() -> new IllegalArgumentException("No ORCID profile section factory configured for " + type));
return profileSectionFactory.create(context, metadataValues);
}
private List<OrcidProfileSectionFactory> filterBy(Predicate<OrcidProfileSectionFactory> predicate) {
return sectionFactories.values().stream().filter(predicate).collect(Collectors.toList());
}
}

View File

@@ -0,0 +1,242 @@
/**
* 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.service.impl;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName;
import org.dspace.content.Relationship;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.dao.OrcidQueueDAO;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.profile.OrcidEntitySyncPreference;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link OrcidQueueService}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidQueueServiceImpl implements OrcidQueueService {
@Autowired
private OrcidQueueDAO orcidQueueDAO;
@Autowired
private OrcidHistoryService orcidHistoryService;
@Autowired
private ItemService itemService;
@Autowired
private RelationshipService relationshipService;
@Override
public List<OrcidQueue> findByProfileItemId(Context context, UUID profileItemId) throws SQLException {
return orcidQueueDAO.findByProfileItemId(context, profileItemId, -1, 0);
}
@Override
public List<OrcidQueue> findByProfileItemId(Context context, UUID profileItemId, Integer limit, Integer offset)
throws SQLException {
return orcidQueueDAO.findByProfileItemId(context, profileItemId, limit, offset);
}
@Override
public List<OrcidQueue> findByProfileItemAndEntity(Context context, Item profileItem, Item entity)
throws SQLException {
return orcidQueueDAO.findByProfileItemAndEntity(context, profileItem, entity);
}
@Override
public List<OrcidQueue> findByProfileItemOrEntity(Context context, Item item) throws SQLException {
return orcidQueueDAO.findByProfileItemOrEntity(context, item);
}
@Override
public long countByProfileItemId(Context context, UUID profileItemId) throws SQLException {
return orcidQueueDAO.countByProfileItemId(context, profileItemId);
}
@Override
public List<OrcidQueue> findAll(Context context) throws SQLException {
return orcidQueueDAO.findAll(context, OrcidQueue.class);
}
@Override
public OrcidQueue create(Context context, Item profileItem, Item entity) throws SQLException {
Optional<String> putCode = orcidHistoryService.findLastPutCode(context, profileItem, entity);
if (putCode.isPresent()) {
return createEntityUpdateRecord(context, profileItem, entity, putCode.get());
} else {
return createEntityInsertionRecord(context, profileItem, entity);
}
}
@Override
public OrcidQueue createEntityInsertionRecord(Context context, Item profileItem, Item entity) throws SQLException {
OrcidQueue orcidQueue = new OrcidQueue();
orcidQueue.setEntity(entity);
orcidQueue.setRecordType(itemService.getEntityTypeLabel(entity));
orcidQueue.setProfileItem(profileItem);
orcidQueue.setDescription(getMetadataValue(entity, "dc.title"));
orcidQueue.setOperation(OrcidOperation.INSERT);
return orcidQueueDAO.create(context, orcidQueue);
}
@Override
public OrcidQueue createEntityUpdateRecord(Context context, Item profileItem, Item entity, String putCode)
throws SQLException {
OrcidQueue orcidQueue = new OrcidQueue();
orcidQueue.setProfileItem(profileItem);
orcidQueue.setEntity(entity);
orcidQueue.setPutCode(putCode);
orcidQueue.setRecordType(itemService.getEntityTypeLabel(entity));
orcidQueue.setDescription(getMetadataValue(entity, "dc.title"));
orcidQueue.setOperation(OrcidOperation.UPDATE);
return orcidQueueDAO.create(context, orcidQueue);
}
@Override
public OrcidQueue createEntityDeletionRecord(Context context, Item profileItem, String description, String type,
String putCode)
throws SQLException {
OrcidQueue orcidQueue = new OrcidQueue();
orcidQueue.setRecordType(type);
orcidQueue.setProfileItem(profileItem);
orcidQueue.setPutCode(putCode);
orcidQueue.setDescription(description);
orcidQueue.setOperation(OrcidOperation.DELETE);
return orcidQueueDAO.create(context, orcidQueue);
}
@Override
public OrcidQueue createProfileInsertionRecord(Context context, Item profile, String description, String recordType,
String metadata) throws SQLException {
OrcidQueue orcidQueue = new OrcidQueue();
orcidQueue.setEntity(profile);
orcidQueue.setRecordType(recordType);
orcidQueue.setProfileItem(profile);
orcidQueue.setDescription(description);
orcidQueue.setMetadata(metadata);
orcidQueue.setOperation(OrcidOperation.INSERT);
return orcidQueueDAO.create(context, orcidQueue);
}
@Override
public OrcidQueue createProfileDeletionRecord(Context context, Item profile, String description, String recordType,
String metadata, String putCode) throws SQLException {
OrcidQueue orcidQueue = new OrcidQueue();
orcidQueue.setEntity(profile);
orcidQueue.setRecordType(recordType);
orcidQueue.setProfileItem(profile);
orcidQueue.setDescription(description);
orcidQueue.setPutCode(putCode);
orcidQueue.setMetadata(metadata);
orcidQueue.setOperation(OrcidOperation.DELETE);
return orcidQueueDAO.create(context, orcidQueue);
}
@Override
public void deleteById(Context context, Integer id) throws SQLException {
OrcidQueue orcidQueue = orcidQueueDAO.findByID(context, OrcidQueue.class, id);
if (orcidQueue != null) {
delete(context, orcidQueue);
}
}
@Override
public List<OrcidQueue> findByAttemptsLessThan(Context context, int attempts) throws SQLException {
return orcidQueueDAO.findByAttemptsLessThan(context, attempts);
}
@Override
public void delete(Context context, OrcidQueue orcidQueue) throws SQLException {
orcidQueueDAO.delete(context, orcidQueue);
}
@Override
public void deleteByEntityAndRecordType(Context context, Item entity, String recordType) throws SQLException {
List<OrcidQueue> records = orcidQueueDAO.findByEntityAndRecordType(context, entity, recordType);
for (OrcidQueue record : records) {
orcidQueueDAO.delete(context, record);
}
}
@Override
public void deleteByProfileItemAndRecordType(Context context, Item profileItem, String recordType)
throws SQLException {
List<OrcidQueue> records = orcidQueueDAO.findByProfileItemAndRecordType(context, profileItem, recordType);
for (OrcidQueue record : records) {
orcidQueueDAO.delete(context, record);
}
}
@Override
public OrcidQueue find(Context context, int id) throws SQLException {
return orcidQueueDAO.findByID(context, OrcidQueue.class, id);
}
@Override
public void update(Context context, OrcidQueue orcidQueue) throws SQLException {
orcidQueueDAO.save(context, orcidQueue);
}
@Override
public void recalculateOrcidQueue(Context context, Item profileItem, OrcidEntityType orcidEntityType,
OrcidEntitySyncPreference preference) throws SQLException {
String entityType = orcidEntityType.getEntityType();
if (preference == OrcidEntitySyncPreference.DISABLED) {
deleteByProfileItemAndRecordType(context, profileItem, entityType);
} else {
List<Item> entities = findAllEntitiesLinkableWith(context, profileItem, entityType);
for (Item entity : entities) {
create(context, profileItem, entity);
}
}
}
private List<Item> findAllEntitiesLinkableWith(Context context, Item profile, String entityType) {
return findRelationshipsByItem(context, profile).stream()
.map(relationship -> getRelatedItem(relationship, profile))
.filter(item -> entityType.equals(itemService.getEntityTypeLabel(item)))
.collect(Collectors.toList());
}
private List<Relationship> findRelationshipsByItem(Context context, Item item) {
try {
return relationshipService.findByItem(context, item);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
private Item getRelatedItem(Relationship relationship, Item item) {
return relationship.getLeftItem().equals(item) ? relationship.getRightItem() : relationship.getLeftItem();
}
private String getMetadataValue(Item item, String metadatafield) {
return itemService.getMetadataFirstValue(item, new MetadataFieldName(metadatafield), Item.ANY);
}
}

View File

@@ -5,15 +5,17 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.service.impl; package org.dspace.orcid.service.impl;
import static java.time.LocalDateTime.now; import static java.time.LocalDateTime.now;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
import static java.util.List.of; import static java.util.List.of;
import static java.util.Optional.ofNullable; import static java.util.Optional.ofNullable;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.lang3.EnumUtils.isValidEnum; import static org.apache.commons.lang3.EnumUtils.isValidEnum;
import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.dspace.content.Item.ANY; import static org.dspace.content.Item.ANY;
import static org.dspace.profile.OrcidEntitySyncPreference.DISABLED;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.HashSet; import java.util.HashSet;
@@ -23,15 +25,6 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.commons.codec.binary.StringUtils; import org.apache.commons.codec.binary.StringUtils;
import org.dspace.app.orcid.OrcidToken;
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.orcid.service.OrcidTokenService;
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.authorize.AuthorizeException;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.MetadataValue; import org.dspace.content.MetadataValue;
@@ -39,6 +32,16 @@ import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.model.OrcidEntityType;
import org.dspace.orcid.model.OrcidTokenResponseDTO;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.OrcidEntitySyncPreference;
import org.dspace.profile.OrcidProfileDisconnectionMode;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.dspace.profile.OrcidSynchronizationMode;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -62,6 +65,9 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
@Autowired @Autowired
private OrcidTokenService orcidTokenService; private OrcidTokenService orcidTokenService;
@Autowired
private ResearcherProfileService researcherProfileService;
@Override @Override
public void linkProfile(Context context, Item profile, OrcidTokenResponseDTO token) throws SQLException { public void linkProfile(Context context, Item profile, OrcidTokenResponseDTO token) throws SQLException {
@@ -152,6 +158,32 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
} }
@Override
public boolean isSynchronizationAllowed(Item profile, Item item) {
if (isOrcidSynchronizationDisabled()) {
return false;
}
String entityType = itemService.getEntityTypeLabel(item);
if (entityType == null) {
return false;
}
if (OrcidEntityType.isValidEntityType(entityType)) {
return getEntityPreference(profile, OrcidEntityType.fromEntityType(entityType))
.filter(pref -> pref != DISABLED)
.isPresent();
}
if (entityType.equals(researcherProfileService.getProfileType())) {
return profile.equals(item) && !isEmpty(getProfilePreferences(profile));
}
return false;
}
@Override @Override
public Optional<OrcidSynchronizationMode> getSynchronizationMode(Item item) { public Optional<OrcidSynchronizationMode> getSynchronizationMode(Item item) {
return getMetadataValue(item, "dspace.orcid.sync-mode") return getMetadataValue(item, "dspace.orcid.sync-mode")
@@ -252,6 +284,10 @@ public class OrcidSynchronizationServiceImpl implements OrcidSynchronizationServ
} }
private boolean isOrcidSynchronizationDisabled() {
return !configurationService.getBooleanProperty("orcid.synchronization-enabled", true);
}
private void updateItem(Context context, Item item) throws SQLException { private void updateItem(Context context, Item item) throws SQLException {
try { try {
context.turnOffAuthorisationSystem(); context.turnOffAuthorisationSystem();

View File

@@ -5,17 +5,17 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.orcid.service.impl; package org.dspace.orcid.service.impl;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.List;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.app.orcid.dao.OrcidTokenDAO;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.dao.OrcidTokenDAO;
import org.dspace.orcid.service.OrcidTokenService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
/** /**

View File

@@ -0,0 +1,94 @@
/**
* 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.service.impl;
import static java.util.Comparator.comparing;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.lang3.StringUtils;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.core.Context;
import org.dspace.orcid.service.MetadataSignatureGenerator;
/**
* Implementation of {@link MetadataSignatureGenerator} that composes a
* signature made up of a section for each metadata value, divided by the
* character SIGNATURE_SECTIONS_SEPARATOR. <br/>
* Each section is composed of the metadata field, the metadata value and, if
* present, the authority, divided by the character METADATA_SECTIONS_SEPARATOR.
* <br/>
* The presence of the metadata field allows to have different signatures for
* metadata with the same values but referring to different fields, while the
* authority allows to distinguish metadata that refer to different entities,
* even if they have the same value. Finally, the various sections of the
* signature are sorted by metadata field so that the order of the input
* metadata values does not affect the signature.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class PlainMetadataSignatureGeneratorImpl implements MetadataSignatureGenerator {
private static final String SIGNATURE_SECTIONS_SEPARATOR = "§§";
private static final String METADATA_SECTIONS_SEPARATOR = "::";
@Override
public String generate(Context context, List<MetadataValue> metadataValues) {
return metadataValues.stream()
.sorted(comparing(metadataValue -> metadataValue.getMetadataField().getID()))
.map(this::composeSignatureSection)
.collect(Collectors.joining(SIGNATURE_SECTIONS_SEPARATOR));
}
@Override
public List<MetadataValue> findBySignature(Context context, Item item, String signature) {
return getSignatureSections(signature)
.map(signatureSection -> findFirstBySignatureSection(context, item, signatureSection))
.flatMap(metadataValue -> metadataValue.stream())
.collect(Collectors.toList());
}
private String composeSignatureSection(MetadataValue metadataValue) {
String fieldId = getField(metadataValue);
String metadataValueSignature = fieldId + METADATA_SECTIONS_SEPARATOR + getValue(metadataValue);
if (StringUtils.isNotBlank(metadataValue.getAuthority())) {
return metadataValueSignature + METADATA_SECTIONS_SEPARATOR + metadataValue.getAuthority();
} else {
return metadataValueSignature;
}
}
private Optional<MetadataValue> findFirstBySignatureSection(Context context, Item item, String signatureSection) {
return item.getMetadata().stream()
.filter(metadataValue -> matchSignature(context, metadataValue, signatureSection))
.findFirst();
}
private boolean matchSignature(Context context, MetadataValue metadataValue, String signatureSection) {
return generate(context, List.of(metadataValue)).equals(signatureSection);
}
private Stream<String> getSignatureSections(String signature) {
return Arrays.stream(StringUtils.split(signature, SIGNATURE_SECTIONS_SEPARATOR));
}
private String getField(MetadataValue metadataValue) {
return metadataValue.getMetadataField().toString('.');
}
private String getValue(MetadataValue metadataValue) {
return metadataValue.getValue() != null ? metadataValue.getValue() : "";
}
}

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
/** /**
* Enum that model the allowed values to configure the ORCID synchronization * Enum that model the allowed values to configure the ORCID synchronization

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
import static java.time.LocalDateTime.now; import static java.time.LocalDateTime.now;
import static java.time.format.DateTimeFormatter.ISO_DATE_TIME; import static java.time.format.DateTimeFormatter.ISO_DATE_TIME;
@@ -17,9 +17,6 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.app.profile.service.AfterResearcherProfileCreationAction;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.content.MetadataFieldName; import org.dspace.content.MetadataFieldName;
import org.dspace.content.MetadataValue; import org.dspace.content.MetadataValue;
@@ -27,6 +24,9 @@ import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.profile.service.AfterResearcherProfileCreationAction;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
import static org.apache.commons.lang3.EnumUtils.isValidEnum; import static org.apache.commons.lang3.EnumUtils.isValidEnum;

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
/** /**
* Enum that model the allowed values to configure the ORCID synchronization * Enum that model the allowed values to configure the ORCID synchronization

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
/** /**
* Enum that model the allowed values to configure the ORCID synchronization * Enum that model the allowed values to configure the ORCID synchronization

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
import static org.dspace.core.Constants.READ; import static org.dspace.core.Constants.READ;
import static org.dspace.eperson.Group.ANONYMOUS; import static org.dspace.eperson.Group.ANONYMOUS;

View File

@@ -5,7 +5,7 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile; package org.dspace.profile;
import static java.util.Optional.empty; import static java.util.Optional.empty;
import static java.util.Optional.of; import static java.util.Optional.of;
@@ -28,9 +28,6 @@ import javax.annotation.PostConstruct;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.dspace.app.exception.ResourceAlreadyExistsException; import org.dspace.app.exception.ResourceAlreadyExistsException;
import org.dspace.app.orcid.service.OrcidSynchronizationService;
import org.dspace.app.profile.service.AfterResearcherProfileCreationAction;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.service.AuthorizeService; import org.dspace.authorize.service.AuthorizeService;
import org.dspace.content.Collection; import org.dspace.content.Collection;
@@ -51,6 +48,9 @@ import org.dspace.discovery.indexobject.IndexableCollection;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.GroupService;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.profile.service.AfterResearcherProfileCreationAction;
import org.dspace.profile.service.ResearcherProfileService;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.dspace.util.UUIDUtils; import org.dspace.util.UUIDUtils;
import org.slf4j.Logger; import org.slf4j.Logger;

View File

@@ -5,13 +5,13 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile.service; package org.dspace.profile.service;
import java.sql.SQLException; import java.sql.SQLException;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.profile.ResearcherProfile;
/** /**
* Interface to mark classes that allow to perform additional logic on created * Interface to mark classes that allow to perform additional logic on created

View File

@@ -5,18 +5,18 @@
* *
* http://www.dspace.org/license/ * http://www.dspace.org/license/
*/ */
package org.dspace.app.profile.service; package org.dspace.profile.service;
import java.net.URI; import java.net.URI;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
import org.dspace.app.profile.ResearcherProfile;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.discovery.SearchServiceException; import org.dspace.discovery.SearchServiceException;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.profile.ResearcherProfile;
/** /**
* Service interface class for the {@link ResearcherProfile} object. The * Service interface class for the {@link ResearcherProfile} object. The

View File

@@ -0,0 +1,107 @@
/**
* 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.util;
import java.io.File;
import java.io.FileInputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.lang3.StringUtils;
import org.dspace.services.ConfigurationService;
import org.springframework.util.Assert;
/**
* Class that parse a properties file present in the crosswalks directory and
* allows to get its values given a key.
*
* @author Andrea Bollini
* @author Kostas Stamatis
* @author Luigi Andrea Pascarelli
* @author Panagiotis Koutsourakis
* @author Luca Giamminonni
*/
public class SimpleMapConverter {
private String converterNameFile; // The properties filename
private ConfigurationService configurationService;
private Map<String, String> mapping;
private String defaultValue = "";
/**
* Parse the configured property file.
*/
public void init() {
Assert.notNull(converterNameFile, "No properties file name provided");
Assert.notNull(configurationService, "No configuration service provided");
String mappingFile = configurationService.getProperty(
"dspace.dir") + File.separator + "config" + File.separator + "crosswalks" + File.separator +
converterNameFile;
try (FileInputStream fis = new FileInputStream(new File(mappingFile))) {
Properties mapConfig = new Properties();
mapConfig.load(fis);
this.mapping = parseProperties(mapConfig);
} catch (Exception e) {
throw new IllegalArgumentException("An error occurs parsing " + mappingFile, e);
}
}
/**
* Returns the value related to the given key. If the given key is not found the
* incoming value is returned.
*
* @param key the key to search for a value
* @return the value
*/
public String getValue(String key) {
String value = mapping.getOrDefault(key, defaultValue);
if (StringUtils.isBlank(value)) {
return key;
}
return value;
}
private Map<String, String> parseProperties(Properties properties) {
Map<String, String> mapping = new HashMap<String, String>();
for (Object key : properties.keySet()) {
String keyString = (String) key;
mapping.put(keyString, properties.getProperty(keyString, ""));
}
return mapping;
}
public void setDefaultValue(String defaultValue) {
this.defaultValue = defaultValue;
}
public void setConverterNameFile(String converterNameFile) {
this.converterNameFile = converterNameFile;
}
public void setConfigurationService(ConfigurationService configurationService) {
this.configurationService = configurationService;
}
}

View File

@@ -0,0 +1,54 @@
--
-- 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/
--
-----------------------------------------------------------------------------------
-- Create tables for ORCID Queue and History
-----------------------------------------------------------------------------------
CREATE SEQUENCE orcid_queue_id_seq;
CREATE TABLE orcid_queue
(
id INTEGER NOT NULL,
owner_id UUID NOT NULL,
entity_id UUID,
put_code VARCHAR(255),
record_type VARCHAR(255),
description VARCHAR(255),
operation VARCHAR(255),
metadata CLOB,
attempts INTEGER,
CONSTRAINT orcid_queue_pkey PRIMARY KEY (id),
CONSTRAINT orcid_queue_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES item (uuid),
CONSTRAINT orcid_queue_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES item (uuid)
);
CREATE INDEX orcid_queue_owner_id_index on orcid_queue(owner_id);
CREATE SEQUENCE orcid_history_id_seq;
CREATE TABLE orcid_history
(
id INTEGER NOT NULL,
owner_id UUID NOT NULL,
entity_id UUID,
put_code VARCHAR(255),
timestamp_last_attempt TIMESTAMP,
response_message CLOB,
status INTEGER,
metadata CLOB,
operation VARCHAR(255),
record_type VARCHAR(255),
description VARCHAR(255),
CONSTRAINT orcid_history_pkey PRIMARY KEY (id),
CONSTRAINT orcid_history_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES item (uuid),
CONSTRAINT orcid_history_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES item (uuid)
);
CREATE INDEX orcid_history_owner_id_index on orcid_history(owner_id);

View File

@@ -0,0 +1,54 @@
--
-- 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/
--
-----------------------------------------------------------------------------------
-- Create tables for ORCID Queue and History
-----------------------------------------------------------------------------------
CREATE SEQUENCE orcid_queue_id_seq;
CREATE TABLE orcid_queue
(
id INTEGER NOT NULL,
owner_id RAW(16) NOT NULL,
entity_id RAW(16),
put_code VARCHAR(255),
record_type VARCHAR(255),
description VARCHAR(255),
operation VARCHAR(255),
metadata CLOB,
attempts INTEGER,
CONSTRAINT orcid_queue_pkey PRIMARY KEY (id),
CONSTRAINT orcid_queue_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES item (uuid),
CONSTRAINT orcid_queue_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES item (uuid)
);
CREATE INDEX orcid_queue_owner_id_index on orcid_queue(owner_id);
CREATE SEQUENCE orcid_history_id_seq;
CREATE TABLE orcid_history
(
id INTEGER NOT NULL,
owner_id RAW(16) NOT NULL,
entity_id RAW(16),
put_code VARCHAR(255),
timestamp_last_attempt TIMESTAMP,
response_message CLOB,
status INTEGER,
metadata CLOB,
operation VARCHAR(255),
record_type VARCHAR(255),
description VARCHAR(255),
CONSTRAINT orcid_history_pkey PRIMARY KEY (id),
CONSTRAINT orcid_history_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES item (uuid),
CONSTRAINT orcid_history_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES item (uuid)
);
CREATE INDEX orcid_history_owner_id_index on orcid_history(owner_id);

View File

@@ -0,0 +1,54 @@
--
-- 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/
--
-----------------------------------------------------------------------------------
-- Create tables for ORCID Queue and History
-----------------------------------------------------------------------------------
CREATE SEQUENCE orcid_queue_id_seq;
CREATE TABLE orcid_queue
(
id INTEGER NOT NULL,
owner_id uuid NOT NULL,
entity_id uuid,
attempts INTEGER,
put_code CHARACTER VARYING(255),
record_type CHARACTER VARYING(255),
description CHARACTER VARYING(255),
operation CHARACTER VARYING(255),
metadata TEXT,
CONSTRAINT orcid_queue_pkey PRIMARY KEY (id),
CONSTRAINT orcid_queue_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES item (uuid),
CONSTRAINT orcid_queue_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES item (uuid)
);
CREATE INDEX orcid_queue_owner_id_index on orcid_queue(owner_id);
CREATE SEQUENCE orcid_history_id_seq;
CREATE TABLE orcid_history
(
id INTEGER NOT NULL,
owner_id uuid NOT NULL,
entity_id uuid,
put_code CHARACTER VARYING(255),
timestamp_last_attempt TIMESTAMP,
response_message text,
status INTEGER,
metadata TEXT,
operation CHARACTER VARYING(255),
record_type CHARACTER VARYING(255),
description CHARACTER VARYING(255),
CONSTRAINT orcid_history_pkey PRIMARY KEY (id),
CONSTRAINT orcid_history_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES item (uuid),
CONSTRAINT orcid_history_entity_id_fkey FOREIGN KEY (entity_id) REFERENCES item (uuid)
);
CREATE INDEX orcid_history_owner_id_index on orcid_history(owner_id);

View File

@@ -84,7 +84,7 @@ loglevel.dspace = INFO
# IIIF TEST SETTINGS # # IIIF TEST SETTINGS #
######################## ########################
iiif.enabled = true iiif.enabled = true
event.dispatcher.default.consumers = versioning, discovery, eperson, iiif event.dispatcher.default.consumers = versioning, discovery, eperson, orcidqueue, iiif
########################################### ###########################################
# CUSTOM UNIT / INTEGRATION TEST SETTINGS # # CUSTOM UNIT / INTEGRATION TEST SETTINGS #
@@ -150,7 +150,9 @@ csvexport.dir = dspace-server-webapp/src/test/data/dspaceFolder/exports
# For the tests we have to disable this health indicator because there isn't a mock server and the calculated status was DOWN # For the tests we have to disable this health indicator because there isn't a mock server and the calculated status was DOWN
management.health.solrOai.enabled = false management.health.solrOai.enabled = false
# Enable researcher profiles and orcid synchronization for tests
researcher-profile.entity-type = Person researcher-profile.entity-type = Person
orcid.synchronization-enabled = true
# Configuration settings required for Researcher Profiles # Configuration settings required for Researcher Profiles
# These settings ensure "dspace.object.owner" field are indexed by Authority Control # These settings ensure "dspace.object.owner" field are indexed by Authority Control

View File

@@ -59,6 +59,12 @@
<property name="description" value="Mocking a script for testing purposes" /> <property name="description" value="Mocking a script for testing purposes" />
<property name="dspaceRunnableClass" value="org.dspace.scripts.impl.MockDSpaceRunnableScript"/> <property name="dspaceRunnableClass" value="org.dspace.scripts.impl.MockDSpaceRunnableScript"/>
</bean> </bean>
<bean id="orcid-bulk-push" class="org.dspace.orcid.script.OrcidBulkPushScriptConfiguration">
<property name="description" value="Perform the bulk synchronization of all the BATCH configured ORCID entities placed in the ORCID queue"/>
<property name="dspaceRunnableClass" value="org.dspace.orcid.script.OrcidBulkPush"/>
</bean>
<!-- Keep as last script; for test ScriptRestRepository#findOneScriptByNameTest --> <!-- Keep as last script; for test ScriptRestRepository#findOneScriptByNameTest -->
<bean id="mock-script" class="org.dspace.scripts.MockDSpaceRunnableScriptConfiguration" scope="prototype"> <bean id="mock-script" class="org.dspace.scripts.MockDSpaceRunnableScriptConfiguration" scope="prototype">
<property name="description" value="Mocking a script for testing purposes" /> <property name="description" value="Mocking a script for testing purposes" />

View File

@@ -0,0 +1,55 @@
/**
* 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.matcher;
import java.util.function.Predicate;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;
/**
* Matcher based on an {@link Predicate}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
* @param <T> the type of the instance to match
*/
public class LambdaMatcher<T> extends BaseMatcher<T> {
private final Predicate<T> matcher;
private final String description;
public static <T> LambdaMatcher<T> matches(Predicate<T> matcher) {
return new LambdaMatcher<T>(matcher, "Matches the given predicate");
}
public static <T> LambdaMatcher<T> matches(Predicate<T> matcher, String description) {
return new LambdaMatcher<T>(matcher, description);
}
public static <T> Matcher<java.lang.Iterable<? super T>> has(Predicate<T> matcher) {
return Matchers.hasItem(matches(matcher));
}
private LambdaMatcher(Predicate<T> matcher, String description) {
this.matcher = matcher;
this.description = description;
}
@Override
@SuppressWarnings("unchecked")
public boolean matches(Object argument) {
return matcher.test((T) argument);
}
@Override
public void describeTo(Description description) {
description.appendText(this.description);
}
}

View File

@@ -0,0 +1,136 @@
/**
* 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.matcher;
import static org.hamcrest.Matchers.is;
import org.dspace.content.Item;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.OrcidQueue;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
/**
* Implementation of {@link org.hamcrest.Matcher} to match a OrcidQueue by all
* its attributes.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidQueueMatcher extends TypeSafeMatcher<OrcidQueue> {
private final Matcher<Item> profileItemMatcher;
private final Matcher<Item> entityMatcher;
private final Matcher<String> recordTypeMatcher;
private final Matcher<String> putCodeMatcher;
private final Matcher<String> descriptionMatcher;
private final Matcher<String> metadataMatcher;
private final Matcher<OrcidOperation> operationMatcher;
private final Matcher<Integer> attemptsMatcher;
private OrcidQueueMatcher(Matcher<Item> profileItemMatcher, Matcher<Item> entityMatcher,
Matcher<String> recordTypeMatcher, Matcher<String> putCodeMatcher, Matcher<String> metadataMatcher,
Matcher<String> descriptionMatcher, Matcher<OrcidOperation> operationMatcher,
Matcher<Integer> attemptsMatcher) {
this.profileItemMatcher = profileItemMatcher;
this.entityMatcher = entityMatcher;
this.recordTypeMatcher = recordTypeMatcher;
this.putCodeMatcher = putCodeMatcher;
this.metadataMatcher = metadataMatcher;
this.descriptionMatcher = descriptionMatcher;
this.operationMatcher = operationMatcher;
this.attemptsMatcher = attemptsMatcher;
}
public static OrcidQueueMatcher matches(Item profileItem, Item entity, String recordType,
OrcidOperation operation) {
return new OrcidQueueMatcher(is(profileItem), is(entity), is(recordType), anything(),
anything(), anything(), is(operation), anything());
}
public static OrcidQueueMatcher matches(Item profileItem, Item entity, String recordType,
OrcidOperation operation, int attempts) {
return new OrcidQueueMatcher(is(profileItem), is(entity), is(recordType), anything(),
anything(), anything(), is(operation), is(attempts));
}
public static OrcidQueueMatcher matches(Item profileItem, Item entity, String recordType,
String putCode, OrcidOperation operation) {
return new OrcidQueueMatcher(is(profileItem), is(entity), is(recordType), is(putCode),
anything(), anything(), is(operation), anything());
}
public static OrcidQueueMatcher matches(Item profileItem, Item entity, String recordType,
String putCode, String metadata, String description, OrcidOperation operation) {
return new OrcidQueueMatcher(is(profileItem), is(entity), is(recordType),
is(putCode), is(metadata), is(description), is(operation), anything());
}
public static OrcidQueueMatcher matches(Item item, String recordType,
String putCode, String metadata, String description, OrcidOperation operation) {
return new OrcidQueueMatcher(is(item), is(item), is(recordType),
is(putCode), is(metadata), is(description), is(operation), anything());
}
public static OrcidQueueMatcher matches(Item profileItem, Item entity, String recordType,
String putCode, Matcher<String> metadata, String description, OrcidOperation operation) {
return new OrcidQueueMatcher(is(profileItem), is(entity), is(recordType),
is(putCode), metadata, is(description), is(operation), anything());
}
@Override
public void describeTo(Description description) {
description.appendText("an orcid queue record that with the following attributes:")
.appendText(" item profileItem ").appendDescriptionOf(profileItemMatcher)
.appendText(", item entity ").appendDescriptionOf(entityMatcher)
.appendText(", record type ").appendDescriptionOf(recordTypeMatcher)
.appendText(", metadata ").appendDescriptionOf(metadataMatcher)
.appendText(", description ").appendDescriptionOf(descriptionMatcher)
.appendText(", operation ").appendDescriptionOf(operationMatcher)
.appendText(", attempts ").appendDescriptionOf(attemptsMatcher)
.appendText(" and put code ").appendDescriptionOf(putCodeMatcher);
}
@Override
protected boolean matchesSafely(OrcidQueue item) {
return profileItemMatcher.matches(item.getProfileItem())
&& entityMatcher.matches(item.getEntity())
&& recordTypeMatcher.matches(item.getRecordType())
&& metadataMatcher.matches(item.getMetadata())
&& putCodeMatcher.matches(item.getPutCode())
&& descriptionMatcher.matches(item.getDescription())
&& operationMatcher.matches(item.getOperation())
&& attemptsMatcher.matches(item.getAttempts());
}
private static <T> Matcher<T> anything() {
return new BaseMatcher<T>() {
@Override
public boolean matches(Object item) {
return true;
}
@Override
public void describeTo(Description description) {
}
};
}
}

View File

@@ -13,7 +13,6 @@ import java.util.List;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.app.requestitem.factory.RequestItemServiceFactory; import org.dspace.app.requestitem.factory.RequestItemServiceFactory;
import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.app.requestitem.service.RequestItemService;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
@@ -43,10 +42,13 @@ import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.eperson.service.GroupService; import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.RegistrationDataService; import org.dspace.eperson.service.RegistrationDataService;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidHistoryService;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.factory.ScriptServiceFactory;
import org.dspace.scripts.service.ProcessService; import org.dspace.scripts.service.ProcessService;
import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.utils.DSpace;
import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.factory.VersionServiceFactory;
import org.dspace.versioning.service.VersionHistoryService; import org.dspace.versioning.service.VersionHistoryService;
import org.dspace.versioning.service.VersioningService; import org.dspace.versioning.service.VersioningService;
@@ -97,6 +99,8 @@ public abstract class AbstractBuilder<T, S> {
static ProcessService processService; static ProcessService processService;
static RequestItemService requestItemService; static RequestItemService requestItemService;
static VersioningService versioningService; static VersioningService versioningService;
static OrcidHistoryService orcidHistoryService;
static OrcidQueueService orcidQueueService;
static OrcidTokenService orcidTokenService; static OrcidTokenService orcidTokenService;
protected Context context; protected Context context;
@@ -154,7 +158,9 @@ public abstract class AbstractBuilder<T, S> {
inProgressUserService = XmlWorkflowServiceFactory.getInstance().getInProgressUserService(); inProgressUserService = XmlWorkflowServiceFactory.getInstance().getInProgressUserService();
poolTaskService = XmlWorkflowServiceFactory.getInstance().getPoolTaskService(); poolTaskService = XmlWorkflowServiceFactory.getInstance().getPoolTaskService();
workflowItemRoleService = XmlWorkflowServiceFactory.getInstance().getWorkflowItemRoleService(); workflowItemRoleService = XmlWorkflowServiceFactory.getInstance().getWorkflowItemRoleService();
orcidTokenService = new DSpace().getSingletonService(OrcidTokenService.class); orcidHistoryService = OrcidServiceFactory.getInstance().getOrcidHistoryService();
orcidQueueService = OrcidServiceFactory.getInstance().getOrcidQueueService();
orcidTokenService = OrcidServiceFactory.getInstance().getOrcidTokenService();
} }

View File

@@ -26,6 +26,9 @@ import org.dspace.content.service.DSpaceObjectService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.Group; import org.dspace.eperson.Group;
import org.dspace.profile.OrcidEntitySyncPreference;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.dspace.profile.OrcidSynchronizationMode;
/** /**
* Builder to construct Item objects * Builder to construct Item objects
@@ -39,6 +42,7 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
private WorkspaceItem workspaceItem; private WorkspaceItem workspaceItem;
private Item item; private Item item;
private Group readerGroup = null; private Group readerGroup = null;
private String handle = null;
protected ItemBuilder(Context context) { protected ItemBuilder(Context context) {
super(context); super(context);
@@ -79,15 +83,47 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", authorName); return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", authorName);
} }
public ItemBuilder withAuthor(final String authorName, final String authority) {
return addMetadataValue(item, DC.getName(), "contributor", "author", null, authorName, authority, 600);
}
public ItemBuilder withAuthor(final String authorName, final String authority, final int confidence) { public ItemBuilder withAuthor(final String authorName, final String authority, final int confidence) {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author", return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "author",
null, authorName, authority, confidence); null, authorName, authority, confidence);
} }
public ItemBuilder withEditor(final String editorName) {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "contributor", "editor", editorName);
}
public ItemBuilder withDescriptionAbstract(String description) {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "description", "abstract", description);
}
public ItemBuilder withLanguage(String language) {
return addMetadataValue(item, "dc", "language", "iso", language);
}
public ItemBuilder withIsPartOf(String isPartOf) {
return addMetadataValue(item, "dc", "relation", "ispartof", isPartOf);
}
public ItemBuilder withDoiIdentifier(String doi) {
return addMetadataValue(item, "dc", "identifier", "doi", doi);
}
public ItemBuilder withScopusIdentifier(String scopus) {
return addMetadataValue(item, "dc", "identifier", "scopus", scopus);
}
public ItemBuilder withRelationFunding(String funding) {
return addMetadataValue(item, "dc", "relation", "funding", funding);
}
public ItemBuilder withRelationFunding(String funding, String authority) {
return addMetadataValue(item, DC.getName(), "relation", "funding", null, funding, authority, 600);
}
public ItemBuilder withRelationGrantno(String grantno) {
return addMetadataValue(item, "dc", "relation", "grantno", grantno);
}
public ItemBuilder withPersonIdentifierFirstName(final String personIdentifierFirstName) { public ItemBuilder withPersonIdentifierFirstName(final String personIdentifierFirstName) {
return addMetadataValue(item, "person", "givenName", null, personIdentifierFirstName); return addMetadataValue(item, "person", "givenName", null, personIdentifierFirstName);
} }
@@ -182,11 +218,68 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return addMetadataValue(item, "dspace", "orcid", "authenticated", authenticated); return addMetadataValue(item, "dspace", "orcid", "authenticated", authenticated);
} }
public ItemBuilder withOrcidSynchronizationPublicationsPreference(OrcidEntitySyncPreference value) {
return withOrcidSynchronizationPublicationsPreference(value.name());
}
public ItemBuilder withOrcidSynchronizationPublicationsPreference(String value) {
return setMetadataSingleValue(item, "dspace", "orcid", "sync-publications", value);
}
public ItemBuilder withOrcidSynchronizationFundingsPreference(OrcidEntitySyncPreference value) {
return withOrcidSynchronizationFundingsPreference(value.name());
}
public ItemBuilder withOrcidSynchronizationFundingsPreference(String value) {
return setMetadataSingleValue(item, "dspace", "orcid", "sync-fundings", value);
}
public ItemBuilder withOrcidSynchronizationProfilePreference(OrcidProfileSyncPreference value) {
return withOrcidSynchronizationProfilePreference(value.name());
}
public ItemBuilder withOrcidSynchronizationProfilePreference(String value) {
return addMetadataValue(item, "dspace", "orcid", "sync-profile", value);
}
public ItemBuilder withOrcidSynchronizationMode(OrcidSynchronizationMode mode) {
return withOrcidSynchronizationMode(mode.name());
}
private ItemBuilder withOrcidSynchronizationMode(String mode) {
return setMetadataSingleValue(item, "dspace", "orcid", "sync-mode", mode);
}
public ItemBuilder withPersonCountry(String country) {
return addMetadataValue(item, "person", "country", null, country);
}
public ItemBuilder withScopusAuthorIdentifier(String id) {
return addMetadataValue(item, "person", "identifier", "scopus-author-id", id);
}
public ItemBuilder withResearcherIdentifier(String rid) {
return addMetadataValue(item, "person", "identifier", "rid", rid);
}
public ItemBuilder withVernacularName(String vernacularName) {
return setMetadataSingleValue(item, "person", "name", "translated", vernacularName);
}
public ItemBuilder withVariantName(String variant) {
return addMetadataValue(item, "person", "name", "variant", variant);
}
public ItemBuilder makeUnDiscoverable() { public ItemBuilder makeUnDiscoverable() {
item.setDiscoverable(false); item.setDiscoverable(false);
return this; return this;
} }
public ItemBuilder withHandle(String handle) {
this.handle = handle;
return this;
}
/** /**
* Withdrawn the item under build. Please note that an user need to be loggedin the context to avoid NPE during the * Withdrawn the item under build. Please note that an user need to be loggedin the context to avoid NPE during the
* creation of the provenance metadata * creation of the provenance metadata
@@ -207,6 +300,58 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
return this; return this;
} }
public ItemBuilder withOrgUnitLegalName(String name) {
return addMetadataValue(item, "organization", "legalName", null, name);
}
public ItemBuilder withOrgUnitCountry(String addressCountry) {
return addMetadataValue(item, "organization", "address", "addressCountry", addressCountry);
}
public ItemBuilder withOrgUnitLocality(String addressLocality) {
return addMetadataValue(item, "organization", "address", "addressLocality", addressLocality);
}
public ItemBuilder withOrgUnitCrossrefIdentifier(String crossrefid) {
return addMetadataValue(item, "organization", "identifier", "crossrefid", crossrefid);
}
public ItemBuilder withProjectStartDate(String startDate) {
return addMetadataValue(item, "project", "startDate", null, startDate);
}
public ItemBuilder withProjectEndDate(String endDate) {
return addMetadataValue(item, "project", "endDate", null, endDate);
}
public ItemBuilder withProjectInvestigator(String investigator) {
return addMetadataValue(item, "project", "investigator", null, investigator);
}
public ItemBuilder withDescription(String description) {
return addMetadataValue(item, MetadataSchemaEnum.DC.getName(), "description", null, description);
}
public ItemBuilder withProjectAmount(String amount) {
return addMetadataValue(item, "project", "amount", null, amount);
}
public ItemBuilder withProjectAmountCurrency(String currency) {
return addMetadataValue(item, "project", "amount", "currency", currency);
}
public ItemBuilder withUriIdentifier(String uri) {
return addMetadataValue(item, "dc", "identifier", "uri", uri);
}
public ItemBuilder withIdentifier(String identifier) {
return addMetadataValue(item, "dc", "identifier", null, identifier);
}
public ItemBuilder withOtherIdentifier(String identifier) {
return addMetadataValue(item, "dc", "identifier", "other", identifier);
}
/** /**
* Create an admin group for the collection with the specified members * Create an admin group for the collection with the specified members
* *
@@ -226,7 +371,7 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
@Override @Override
public Item build() { public Item build() {
try { try {
installItemService.installItem(context, workspaceItem); installItemService.installItem(context, workspaceItem, handle);
itemService.update(context, item); itemService.update(context, item);
//Check if we need to make this item private. This has to be done after item install. //Check if we need to make this item private. This has to be done after item install.
@@ -299,4 +444,5 @@ public class ItemBuilder extends AbstractDSpaceObjectBuilder<Item> {
} }
return this; return this;
} }
} }

View File

@@ -0,0 +1,152 @@
/**
* 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.builder;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Date;
import org.apache.log4j.Logger;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.service.OrcidHistoryService;
/**
* Builder to construct OrcidHistory objects
*
* @author Mykhaylo Boychuk (4science)
*/
public class OrcidHistoryBuilder extends AbstractBuilder<OrcidHistory, OrcidHistoryService> {
private static final Logger log = Logger.getLogger(OrcidHistoryBuilder.class);
private OrcidHistory orcidHistory;
protected OrcidHistoryBuilder(Context context) {
super(context);
}
@Override
protected OrcidHistoryService getService() {
return orcidHistoryService;
}
@Override
public void cleanup() throws Exception {
delete(orcidHistory);
}
public static OrcidHistoryBuilder createOrcidHistory(Context context, Item profileItem, Item entity) {
OrcidHistoryBuilder builder = new OrcidHistoryBuilder(context);
return builder.create(context, profileItem, entity);
}
private OrcidHistoryBuilder create(Context context, Item profileItem, Item entity) {
try {
this.context = context;
this.orcidHistory = getService().create(context, profileItem, entity);
} catch (Exception e) {
log.error("Error in OrcidHistoryBuilder.create(..), error: ", e);
}
return this;
}
@Override
public OrcidHistory build() throws SQLException {
try {
getService().update(context, orcidHistory);
context.dispatchEvents();
indexingService.commit();
} catch (Exception e) {
log.error("Error in OrcidHistoryBuilder.build(), error: ", e);
}
return orcidHistory;
}
@Override
public void delete(Context c, OrcidHistory orcidHistory) throws Exception {
if (orcidHistory != null) {
getService().delete(c, orcidHistory);
}
}
/**
* Delete the Test OrcidHistory referred to by the given ID
*
* @param id Integer of Test OrcidHistory to delete
* @throws SQLException
* @throws IOException
*/
public static void deleteOrcidHistory(Integer id) throws SQLException, IOException {
if (id == null) {
return;
}
try (Context c = new Context()) {
OrcidHistory orcidHistory = orcidHistoryService.find(c, id);
if (orcidHistory != null) {
orcidHistoryService.delete(c, orcidHistory);
}
c.complete();
}
}
public void delete(OrcidHistory orcidHistory) throws Exception {
try (Context c = new Context()) {
c.turnOffAuthorisationSystem();
OrcidHistory attachedTab = c.reloadEntity(orcidHistory);
if (attachedTab != null) {
getService().delete(c, attachedTab);
}
c.complete();
}
indexingService.commit();
}
public OrcidHistoryBuilder withResponseMessage(String responseMessage) throws SQLException {
orcidHistory.setResponseMessage(responseMessage);
return this;
}
public OrcidHistoryBuilder withPutCode(String putCode) throws SQLException {
orcidHistory.setPutCode(putCode);
return this;
}
public OrcidHistoryBuilder withStatus(Integer status) throws SQLException {
orcidHistory.setStatus(status);
return this;
}
public OrcidHistoryBuilder withMetadata(String metadata) throws SQLException {
orcidHistory.setMetadata(metadata);
return this;
}
public OrcidHistoryBuilder withRecordType(String recordType) throws SQLException {
orcidHistory.setRecordType(recordType);
return this;
}
public OrcidHistoryBuilder withOperation(OrcidOperation operation) throws SQLException {
orcidHistory.setOperation(operation);
return this;
}
public OrcidHistoryBuilder withDescription(String description) throws SQLException {
orcidHistory.setDescription(description);
return this;
}
public OrcidHistoryBuilder withTimestamp(Date timestamp) {
orcidHistory.setTimestamp(timestamp);
return this;
}
}

View File

@@ -0,0 +1,146 @@
/**
* 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.builder;
import java.sql.SQLException;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.service.OrcidQueueService;
/**
* Builder to construct OrcidQueue objects
*
* @author Mykhaylo Boychuk (4science)
*/
public class OrcidQueueBuilder extends AbstractBuilder<OrcidQueue, OrcidQueueService> {
private OrcidQueue orcidQueue;
protected OrcidQueueBuilder(Context context) {
super(context);
}
@Override
protected OrcidQueueService getService() {
return orcidQueueService;
}
@Override
public void cleanup() throws Exception {
delete(orcidQueue);
}
public static OrcidQueueBuilder createOrcidQueue(Context context, Item profileItem, Item entity) {
OrcidQueueBuilder builder = new OrcidQueueBuilder(context);
return builder.createEntityInsertionRecord(context, profileItem, entity);
}
public static OrcidQueueBuilder createOrcidQueue(Context context, Item profileItem, Item entity, String putCode) {
OrcidQueueBuilder builder = new OrcidQueueBuilder(context);
return builder.createEntityUpdateRecord(context, profileItem, entity, putCode);
}
public static OrcidQueueBuilder createOrcidQueue(Context context, Item profileItem, String description,
String type, String putCode) {
OrcidQueueBuilder builder = new OrcidQueueBuilder(context);
return builder.createEntityDeletionRecord(context, profileItem, description, type, putCode);
}
private OrcidQueueBuilder createEntityDeletionRecord(Context context, Item profileItem,
String description, String type, String putCode) {
try {
this.context = context;
this.orcidQueue = getService().createEntityDeletionRecord(context, profileItem, description, type, putCode);
} catch (Exception e) {
throw new RuntimeException(e);
}
return this;
}
private OrcidQueueBuilder createEntityUpdateRecord(Context context, Item profileItem, Item entity, String putCode) {
try {
this.context = context;
this.orcidQueue = getService().createEntityUpdateRecord(context, profileItem, entity, putCode);
} catch (Exception e) {
throw new RuntimeException(e);
}
return this;
}
private OrcidQueueBuilder createEntityInsertionRecord(Context context, Item profileItem, Item entity) {
try {
this.context = context;
this.orcidQueue = getService().createEntityInsertionRecord(context, profileItem, entity);
} catch (Exception e) {
throw new RuntimeException(e);
}
return this;
}
@Override
public OrcidQueue build() throws SQLException, AuthorizeException {
try {
getService().update(context, orcidQueue);
context.dispatchEvents();
indexingService.commit();
} catch (Exception e) {
throw new RuntimeException(e);
}
return orcidQueue;
}
public OrcidQueueBuilder withPutCode(String putCode) {
orcidQueue.setPutCode(putCode);
return this;
}
public OrcidQueueBuilder withMetadata(String metadata) throws SQLException {
orcidQueue.setMetadata(metadata);
return this;
}
public OrcidQueueBuilder withRecordType(String recordType) throws SQLException {
orcidQueue.setRecordType(recordType);
return this;
}
public OrcidQueueBuilder withOperation(OrcidOperation operation) throws SQLException {
orcidQueue.setOperation(operation);
return this;
}
public OrcidQueueBuilder withDescription(String description) throws SQLException {
orcidQueue.setDescription(description);
return this;
}
@Override
public void delete(Context c, OrcidQueue orcidQueue) throws Exception {
if (orcidQueue != null) {
getService().delete(c, orcidQueue);
}
}
public void delete(OrcidQueue orcidQueue) throws Exception {
try (Context c = new Context()) {
c.turnOffAuthorisationSystem();
OrcidQueue attachedTab = c.reloadEntity(orcidQueue);
if (attachedTab != null) {
getService().delete(c, attachedTab);
}
c.complete();
}
indexingService.commit();
}
}

View File

@@ -9,12 +9,12 @@ package org.dspace.builder;
import java.sql.SQLException; import java.sql.SQLException;
import org.dspace.app.orcid.OrcidToken;
import org.dspace.app.orcid.service.OrcidTokenService;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.orcid.OrcidToken;
import org.dspace.orcid.service.OrcidTokenService;
/** /**
* Builder for {@link OrcidToken} entities. * Builder for {@link OrcidToken} entities.

View File

@@ -25,6 +25,8 @@ import org.dspace.builder.GroupBuilder;
import org.dspace.builder.ItemBuilder; import org.dspace.builder.ItemBuilder;
import org.dspace.builder.MetadataFieldBuilder; import org.dspace.builder.MetadataFieldBuilder;
import org.dspace.builder.MetadataSchemaBuilder; import org.dspace.builder.MetadataSchemaBuilder;
import org.dspace.builder.OrcidHistoryBuilder;
import org.dspace.builder.OrcidQueueBuilder;
import org.dspace.builder.OrcidTokenBuilder; import org.dspace.builder.OrcidTokenBuilder;
import org.dspace.builder.PoolTaskBuilder; import org.dspace.builder.PoolTaskBuilder;
import org.dspace.builder.ProcessBuilder; import org.dspace.builder.ProcessBuilder;
@@ -57,6 +59,8 @@ public class AbstractBuilderCleanupUtil {
} }
private void initMap() { private void initMap() {
map.put(OrcidQueueBuilder.class.getName(), new ArrayList<>());
map.put(OrcidHistoryBuilder.class.getName(), new ArrayList<>());
map.put(OrcidTokenBuilder.class.getName(), new ArrayList<>()); map.put(OrcidTokenBuilder.class.getName(), new ArrayList<>());
map.put(ResourcePolicyBuilder.class.getName(), new ArrayList<>()); map.put(ResourcePolicyBuilder.class.getName(), new ArrayList<>());
map.put(RelationshipBuilder.class.getName(), new ArrayList<>()); map.put(RelationshipBuilder.class.getName(), new ArrayList<>());

View File

@@ -682,7 +682,7 @@ public class ItemTest extends AbstractDSpaceObjectTest {
String schema = "dc"; String schema = "dc";
String element = "contributor"; String element = "contributor";
String qualifier = "author"; String qualifier = "editor";
String lang = Item.ANY; String lang = Item.ANY;
String values = "value0"; String values = "value0";
String authorities = "auth0"; String authorities = "auth0";

View File

@@ -0,0 +1,784 @@
/**
* 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;
import static org.dspace.app.matcher.OrcidQueueMatcher.matches;
import static org.dspace.builder.OrcidHistoryBuilder.createOrcidHistory;
import static org.dspace.builder.RelationshipTypeBuilder.createRelationshipTypeBuilder;
import static org.dspace.orcid.OrcidOperation.DELETE;
import static org.dspace.orcid.OrcidOperation.INSERT;
import static org.dspace.orcid.OrcidOperation.UPDATE;
import static org.dspace.orcid.model.OrcidProfileSectionType.KEYWORDS;
import static org.dspace.profile.OrcidEntitySyncPreference.ALL;
import static org.dspace.profile.OrcidEntitySyncPreference.DISABLED;
import static org.dspace.profile.OrcidProfileSyncPreference.BIOGRAPHICAL;
import static org.dspace.profile.OrcidProfileSyncPreference.IDENTIFIERS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import java.sql.SQLException;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.authorize.AuthorizeException;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EntityTypeBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.OrcidHistoryBuilder;
import org.dspace.builder.RelationshipBuilder;
import org.dspace.content.Collection;
import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.RelationshipType;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.orcid.consumer.OrcidQueueConsumer;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link OrcidQueueConsumer}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidQueueConsumerIT extends AbstractIntegrationTestWithDatabase {
private OrcidQueueService orcidQueueService = OrcidServiceFactory.getInstance().getOrcidQueueService();
private ItemService itemService = ContentServiceFactory.getInstance().getItemService();
private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
private Collection profileCollection;
@Before
public void setup() {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent community")
.build();
profileCollection = createCollection("Profiles", "Person");
context.restoreAuthSystemState();
}
@After
public void after() throws SQLException, AuthorizeException {
List<OrcidQueue> records = orcidQueueService.findAll(context);
for (OrcidQueue record : records) {
orcidQueueService.delete(context, record);
}
context.setDispatcher(null);
}
@Test
public void testWithNotOrcidSynchronizationEntity() throws Exception {
context.turnOffAuthorisationSystem();
Collection orgUnits = CollectionBuilder.createCollection(context, parentCommunity)
.withName("OrgUnits")
.withEntityType("OrgUnit")
.build();
ItemBuilder.createItem(context, orgUnits)
.withTitle("Test OrgUnit")
.withSubject("test")
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, empty());
}
@Test
public void testWithOrcidSynchronizationDisabled() throws Exception {
configurationService.setProperty("orcid.synchronization-enabled", false);
context.turnOffAuthorisationSystem();
ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withSubject("test")
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.withOrcidSynchronizationProfilePreference(IDENTIFIERS)
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, empty());
}
@Test
public void testOrcidQueueRecordCreationForProfile() 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)
.withSubject("test")
.withHandle("123456789/200")
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.withOrcidSynchronizationProfilePreference(IDENTIFIERS)
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(2));
assertThat(queueRecords, hasItem(matches(profile, profile, "KEYWORDS", null,
"dc.subject::test", "test", INSERT)));
assertThat(queueRecords, hasItem(matches(profile, "RESEARCHER_URLS", null,
"dc.identifier.uri::http://localhost:4000/handle/123456789/200",
"http://localhost:4000/handle/123456789/200", INSERT)));
addMetadata(profile, "person", "name", "variant", "User Test", null);
context.commit();
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(3));
assertThat(queueRecords, hasItem(
matches(profile, profile, "KEYWORDS", null, "dc.subject::test", "test", INSERT)));
assertThat(queueRecords, hasItem(matches(profile, "RESEARCHER_URLS", null,
"dc.identifier.uri::http://localhost:4000/handle/123456789/200",
"http://localhost:4000/handle/123456789/200", INSERT)));
assertThat(queueRecords, hasItem(matches(profile, profile, "OTHER_NAMES",
null, "person.name.variant::User Test", "User Test", INSERT)));
}
@Test
public void testOrcidQueueRecordCreationForProfileWithSameMetadataPreviouslyDeleted() 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)
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.INSERT)
.withTimestamp(Date.from(Instant.ofEpochMilli(100000)))
.withStatus(201)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.DELETE)
.withTimestamp(Date.from(Instant.ofEpochMilli(200000)))
.withStatus(204)
.build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
addMetadata(profile, "person", "country", null, "IT", null);
context.commit();
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords.get(0), matches(profile, "COUNTRY", null, "person.country::IT", "IT", INSERT));
}
@Test
public void testOrcidQueueRecordCreationForProfileWithMetadataPreviouslyDeletedAndThenInsertedAgain()
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)
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.INSERT)
.withTimestamp(Date.from(Instant.ofEpochMilli(100000)))
.withStatus(201)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.DELETE)
.withTimestamp(Date.from(Instant.ofEpochMilli(200000)))
.withStatus(204)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.INSERT)
.withTimestamp(Date.from(Instant.ofEpochMilli(300000)))
.withStatus(201)
.build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
addMetadata(profile, "person", "country", null, "IT", null);
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testOrcidQueueRecordCreationForProfileWithNotSuccessfullyMetadataDeletion()
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)
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.INSERT)
.withTimestamp(Date.from(Instant.ofEpochMilli(100000)))
.withStatus(201)
.build();
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType("COUNTRY")
.withMetadata("person.country::IT")
.withPutCode("123456")
.withOperation(OrcidOperation.DELETE)
.withTimestamp(Date.from(Instant.ofEpochMilli(200000)))
.withStatus(400)
.build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
addMetadata(profile, "person", "country", null, "IT", null);
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testOrcidQueueRecordCreationAndDeletion() throws Exception {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withSubject("Science")
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> records = orcidQueueService.findAll(context);
assertThat(records, hasSize(1));
assertThat(records, hasItem(matches(item, KEYWORDS.name(), null, "dc.subject::Science", "Science", INSERT)));
removeMetadata(item, "dc", "subject", null);
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testOrcidQueueRecordCreationAndDeletionWithOrcidHistoryInsertionInTheMiddle() throws Exception {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withSubject("Science")
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> records = orcidQueueService.findAll(context);
assertThat(records, hasSize(1));
assertThat(records, hasItem(matches(item, KEYWORDS.name(), null, "dc.subject::Science", "Science", INSERT)));
OrcidHistoryBuilder.createOrcidHistory(context, item, item)
.withPutCode("12345")
.withMetadata("dc.subject::Science")
.withDescription("Science")
.withRecordType(KEYWORDS.name())
.withOperation(INSERT)
.withStatus(201)
.build();
removeMetadata(item, "dc", "subject", null);
context.commit();
records = orcidQueueService.findAll(context);
assertThat(records, hasSize(1));
assertThat(records, hasItem(matches(item, KEYWORDS.name(), "12345", "dc.subject::Science", "Science", DELETE)));
}
@Test
public void testOrcidQueueRecordCreationAndDeletionWithFailedOrcidHistoryInsertionInTheMiddle() throws Exception {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withSubject("Science")
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> records = orcidQueueService.findAll(context);
assertThat(records, hasSize(1));
assertThat(records, hasItem(matches(item, KEYWORDS.name(), null, "dc.subject::Science", "Science", INSERT)));
OrcidHistoryBuilder.createOrcidHistory(context, item, item)
.withPutCode("12345")
.withMetadata("dc.subject::Science")
.withDescription("Science")
.withRecordType(KEYWORDS.name())
.withOperation(INSERT)
.withStatus(400)
.build();
removeMetadata(item, "dc", "subject", null);
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testNoOrcidQueueRecordCreationOccursIfProfileSynchronizationIsDisabled() throws SQLException {
context.turnOffAuthorisationSystem();
ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testNoOrcidQueueRecordCreationOccursIfNoComplianceMetadataArePresent() throws SQLException {
context.turnOffAuthorisationSystem();
ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-1111-2222-3333")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testOrcidQueueRecordCreationForPublication() 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)
.withOrcidSynchronizationPublicationsPreference(ALL)
.build();
Collection publicationCollection = createCollection("Publications", "Publication");
Item publication = ItemBuilder.createItem(context, publicationCollection)
.withTitle("Test publication")
.withAuthor("Test User")
.build();
EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType,
"isAuthorOfPublication", "isPublicationOfAuthor", 0, null, 0, null).build();
RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", INSERT));
addMetadata(publication, "dc", "contributor", "editor", "Editor", null);
context.commit();
List<OrcidQueue> newOrcidQueueRecords = orcidQueueService.findAll(context);
assertThat(newOrcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), equalTo(newOrcidQueueRecords.get(0)));
}
@Test
public void testOrcidQueueRecordCreationToUpdatePublication() 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)
.withOrcidSynchronizationPublicationsPreference(ALL)
.build();
Collection publicationCollection = createCollection("Publications", "Publication");
Item publication = ItemBuilder.createItem(context, publicationCollection)
.withTitle("Test publication")
.withAuthor("Test User")
.build();
createOrcidHistory(context, profile, publication)
.withPutCode("123456")
.withOperation(INSERT)
.build();
EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType,
"isAuthorOfPublication", "isPublicationOfAuthor", 0, null, 0, null).build();
RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, publication, "Publication", "123456", UPDATE));
}
@Test
public void testNoOrcidQueueRecordCreationOccursIfPublicationSynchronizationIsDisabled() 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 publicationCollection = createCollection("Publications", "Publication");
Item publication = ItemBuilder.createItem(context, publicationCollection)
.withTitle("Test publication")
.withAuthor("Test User")
.build();
EntityType publicationType = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, personType, publicationType,
"isAuthorOfPublication", "isPublicationOfAuthor", 0, null, 0, null).build();
RelationshipBuilder.createRelationshipBuilder(context, profile, publication, isAuthorOfPublication).build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
addMetadata(profile, "dspace", "orcid", "sync-publications", DISABLED.name(), null);
addMetadata(publication, "dc", "date", "issued", "2021-01-01", null);
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testOrcidQueueRecordCreationToUpdateProject() 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)
.withOrcidSynchronizationFundingsPreference(ALL)
.build();
Collection projectCollection = createCollection("Projects", "Project");
Item project = ItemBuilder.createItem(context, projectCollection)
.withTitle("Test project")
.build();
createOrcidHistory(context, profile, project)
.withPutCode("123456")
.build();
EntityType projectType = EntityTypeBuilder.createEntityTypeBuilder(context, "Project").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isProjectOfPerson = createRelationshipTypeBuilder(context, projectType, personType,
"isProjectOfPerson", "isPersonOfProject", 0, null, 0, null).build();
RelationshipBuilder.createRelationshipBuilder(context, project, profile, isProjectOfPerson).build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> orcidQueueRecords = orcidQueueService.findAll(context);
assertThat(orcidQueueRecords, hasSize(1));
assertThat(orcidQueueRecords.get(0), matches(profile, project, "Project", "123456", UPDATE));
}
@Test
public void testNoOrcidQueueRecordCreationOccursForNotConfiguredEntities() 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 projectCollection = createCollection("Projects", "Project");
Item project = ItemBuilder.createItem(context, projectCollection)
.withTitle("Test project")
.withProjectInvestigator("Test User")
.build();
EntityType projectType = EntityTypeBuilder.createEntityTypeBuilder(context, "Project").build();
EntityType personType = EntityTypeBuilder.createEntityTypeBuilder(context, "Person").build();
RelationshipType isProjectOfPerson = createRelationshipTypeBuilder(context, projectType, personType,
"isProjectOfPerson", "isPersonOfProject", 0, null, 0, null).build();
RelationshipBuilder.createRelationshipBuilder(context, project, profile, isProjectOfPerson).build();
context.restoreAuthSystemState();
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testOrcidQueueRecalculationOnProfilePreferenceUpdate() throws Exception {
context.turnOffAuthorisationSystem();
Item profile = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test User")
.withOrcidIdentifier("0000-0000-0012-2345")
.withOrcidAccessToken("ab4d18a0-8d9a-40f1-b601-a417255c8d20", eperson)
.withSubject("Math")
.withHandle("123456789/200")
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> records = orcidQueueService.findAll(context);
assertThat(records, hasSize(1));
assertThat(records, hasItem(matches(profile, "KEYWORDS", null, "dc.subject::Math", "Math", INSERT)));
addMetadata(profile, "person", "identifier", "rid", "ID", null);
addMetadata(profile, "dspace", "orcid", "sync-profile", IDENTIFIERS.name(), null);
context.commit();
records = orcidQueueService.findAll(context);
assertThat(records, hasSize(3));
assertThat(records, hasItem(matches(profile, "KEYWORDS", null, "dc.subject::Math", "Math", INSERT)));
assertThat(records, hasItem(matches(profile, "EXTERNAL_IDS", null, "person.identifier.rid::ID", "ID", INSERT)));
assertThat(records, hasItem(matches(profile, "RESEARCHER_URLS", null,
"dc.identifier.uri::http://localhost:4000/handle/123456789/200",
"http://localhost:4000/handle/123456789/200", INSERT)));
removeMetadata(profile, "dspace", "orcid", "sync-profile");
context.commit();
assertThat(orcidQueueService.findAll(context), empty());
}
@Test
public void testWithManyInsertionAndDeletionOfSameMetadataValue() 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)
.withOrcidSynchronizationProfilePreference(BIOGRAPHICAL)
.withSubject("Science")
.build();
context.restoreAuthSystemState();
context.commit();
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords.get(0), matches(profile, "KEYWORDS", null,
"dc.subject::Science", "Science", INSERT));
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType(KEYWORDS.name())
.withDescription("Science")
.withMetadata("dc.subject::Science")
.withOperation(OrcidOperation.INSERT)
.withPutCode("12345")
.withStatus(201)
.build();
removeMetadata(profile, "dc", "subject", null);
context.commit();
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords.get(0), matches(profile, "KEYWORDS", "12345",
"dc.subject::Science", "Science", DELETE));
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType(KEYWORDS.name())
.withDescription("Science")
.withMetadata("dc.subject::Science")
.withOperation(OrcidOperation.DELETE)
.withStatus(204)
.build();
addMetadata(profile, "dc", "subject", null, "Science", null);
context.commit();
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords.get(0), matches(profile, "KEYWORDS", null,
"dc.subject::Science", "Science", INSERT));
OrcidHistoryBuilder.createOrcidHistory(context, profile, profile)
.withRecordType(KEYWORDS.name())
.withDescription("Science")
.withMetadata("dc.subject::Science")
.withOperation(OrcidOperation.INSERT)
.withPutCode("12346")
.withStatus(201)
.build();
removeMetadata(profile, "dc", "subject", null);
context.commit();
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords.get(0), matches(profile, "KEYWORDS", "12346",
"dc.subject::Science", "Science", DELETE));
}
private void addMetadata(Item item, String schema, String element, String qualifier, String value,
String authority) throws Exception {
context.turnOffAuthorisationSystem();
item = context.reloadEntity(item);
itemService.addMetadata(context, item, schema, element, qualifier, null, value, authority, 600);
itemService.update(context, item);
context.restoreAuthSystemState();
}
private void removeMetadata(Item item, String schema, String element, String qualifier) throws Exception {
context.turnOffAuthorisationSystem();
item = context.reloadEntity(item);
List<MetadataValue> metadata = itemService.getMetadata(item, schema, element, qualifier, Item.ANY);
itemService.removeMetadataValues(context, item, metadata);
itemService.update(context, item);
context.restoreAuthSystemState();
}
private Collection createCollection(String name, String entityType) {
return CollectionBuilder.createCollection(context, parentCommunity)
.withName(name)
.withEntityType(entityType)
.build();
}
}

View File

@@ -0,0 +1,662 @@
/**
* 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.validator;
import static org.dspace.orcid.model.validator.OrcidValidationError.AMOUNT_CURRENCY_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATED_ORGANIZATION_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATED_ORGANIZATION_VALUE_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATION_SOURCE_INVALID;
import static org.dspace.orcid.model.validator.OrcidValidationError.DISAMBIGUATION_SOURCE_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.EXTERNAL_ID_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.FUNDER_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_ADDRESS_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_CITY_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_COUNTRY_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.ORGANIZATION_NAME_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.PUBLICATION_DATE_INVALID;
import static org.dspace.orcid.model.validator.OrcidValidationError.TITLE_REQUIRED;
import static org.dspace.orcid.model.validator.OrcidValidationError.TYPE_REQUIRED;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.Mockito.when;
import java.util.List;
import org.dspace.orcid.model.validator.impl.OrcidValidatorImpl;
import org.dspace.services.ConfigurationService;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.orcid.jaxb.model.common.Iso3166Country;
import org.orcid.jaxb.model.common.Relationship;
import org.orcid.jaxb.model.common.WorkType;
import org.orcid.jaxb.model.v3.release.common.Amount;
import org.orcid.jaxb.model.v3.release.common.DisambiguatedOrganization;
import org.orcid.jaxb.model.v3.release.common.Organization;
import org.orcid.jaxb.model.v3.release.common.OrganizationAddress;
import org.orcid.jaxb.model.v3.release.common.PublicationDate;
import org.orcid.jaxb.model.v3.release.common.Title;
import org.orcid.jaxb.model.v3.release.common.Year;
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.Funding;
import org.orcid.jaxb.model.v3.release.record.FundingTitle;
import org.orcid.jaxb.model.v3.release.record.Work;
import org.orcid.jaxb.model.v3.release.record.WorkTitle;
/**
* Unit tests for {@link OrcidValidatorImpl}
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@RunWith(MockitoJUnitRunner.class)
public class OrcidValidatorTest {
@Mock(lenient = true)
private ConfigurationService configurationService;
@InjectMocks
private OrcidValidatorImpl validator;
@Before
public void before() {
when(configurationService.getBooleanProperty("orcid.validation.work.enabled", true)).thenReturn(true);
when(configurationService.getBooleanProperty("orcid.validation.funding.enabled", true)).thenReturn(true);
when(configurationService.getArrayProperty("orcid.validation.organization.identifier-sources"))
.thenReturn(new String[] { "RINGGOLD", "GRID", "FUNDREF", "LEI" });
}
@Test
public void testWorkWithoutTitleAndTypeAndExternalIds() {
List<OrcidValidationError> errors = validator.validateWork(new Work());
assertThat(errors, hasSize(3));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED, TYPE_REQUIRED, EXTERNAL_ID_REQUIRED));
}
@Test
public void testWorkWithoutWorkTitle() {
Work work = new Work();
work.setWorkType(WorkType.DATA_SET);
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testWorkWithoutTitle() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.setWorkType(WorkType.DATA_SET);
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testWorkWithNullTitle() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title(null));
work.setWorkType(WorkType.DATA_SET);
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testWorkWithEmptyTitle() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title(""));
work.setWorkType(WorkType.DATA_SET);
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testWorkWithoutType() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TYPE_REQUIRED));
}
@Test
public void testWorkWithoutExternalIds() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkType(WorkType.DATA_SET);
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(EXTERNAL_ID_REQUIRED));
}
@Test
public void testWorkWithEmptyExternalIds() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkType(WorkType.DATA_SET);
work.setWorkExternalIdentifiers(new ExternalIDs());
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(EXTERNAL_ID_REQUIRED));
}
@Test
public void testdWorkWithPublicationDateWithoutYear() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.setWorkType(WorkType.DATA_SET);
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
PublicationDate publicationDate = new PublicationDate();
work.setPublicationDate(publicationDate);
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(PUBLICATION_DATE_INVALID));
}
@Test
public void testdWorkWithPublicationDateWithInvalidYear() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.setWorkType(WorkType.DATA_SET);
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
PublicationDate publicationDate = new PublicationDate();
Year year = new Year();
year.setValue("INVALID");
publicationDate.setYear(year);
work.setPublicationDate(publicationDate);
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(PUBLICATION_DATE_INVALID));
}
@Test
public void testdWorkWithPublicationDateWithYearPriorTo1900() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.setWorkType(WorkType.DATA_SET);
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
PublicationDate publicationDate = new PublicationDate();
publicationDate.setYear(new Year(1850));
work.setPublicationDate(publicationDate);
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(PUBLICATION_DATE_INVALID));
}
@Test
public void testValidWork() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.setWorkType(WorkType.DATA_SET);
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
PublicationDate publicationDate = new PublicationDate();
publicationDate.setYear(new Year(1956));
work.setPublicationDate(publicationDate);
List<OrcidValidationError> errors = validator.validateWork(work);
assertThat(errors, empty());
}
@Test
public void testFundingWithoutTitleAndExternalIdsAndOrganization() {
List<OrcidValidationError> errors = validator.validateFunding(new Funding());
assertThat(errors, hasSize(3));
assertThat(errors, containsInAnyOrder(EXTERNAL_ID_REQUIRED, FUNDER_REQUIRED, TITLE_REQUIRED));
}
@Test
public void testFundingWithoutExternalIdsAndOrganization() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Funding title"));
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(2));
assertThat(errors, containsInAnyOrder(EXTERNAL_ID_REQUIRED, FUNDER_REQUIRED));
}
@Test
public void testFundingWithoutTitleAndOrganization() {
Funding funding = new Funding();
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(2));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED, FUNDER_REQUIRED));
}
@Test
public void testFundingWithoutTitleAndExternalIds() {
Funding funding = new Funding();
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(2));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED, EXTERNAL_ID_REQUIRED));
}
@Test
public void testFundingWithoutTitle() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testFundingWithNullTitle() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title(null));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testFundingWithEmptyTitle() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title(""));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testFundingWithEmptyExternalIds() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(EXTERNAL_ID_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutName() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.setName(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(ORGANIZATION_NAME_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithEmptyName() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.setName("");
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(ORGANIZATION_NAME_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutAddress() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.setAddress(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(ORGANIZATION_ADDRESS_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutCity() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.getAddress().setCity(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(ORGANIZATION_CITY_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutCountry() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.getAddress().setCountry(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(ORGANIZATION_COUNTRY_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutDisambiguatedOrganization() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.setDisambiguatedOrganization(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(DISAMBIGUATED_ORGANIZATION_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutDisambiguatedOrganizationId() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.getDisambiguatedOrganization().setDisambiguatedOrganizationIdentifier(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(DISAMBIGUATED_ORGANIZATION_VALUE_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithoutDisambiguatedOrganizationSource() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.getDisambiguatedOrganization().setDisambiguationSource(null);
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(DISAMBIGUATION_SOURCE_REQUIRED));
}
@Test
public void testFundingWithOrganizationWithInvalidDisambiguationSource() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
Organization organization = buildValidOrganization();
organization.getDisambiguatedOrganization().setDisambiguationSource("INVALID");
funding.setOrganization(organization);
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(DISAMBIGUATION_SOURCE_INVALID));
}
@Test
public void testFundingWithoutAmountCurrency() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
funding.setAmount(new Amount());
funding.getAmount().setContent("20000");
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(AMOUNT_CURRENCY_REQUIRED));
}
@Test
public void testValidFunding() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title("Title"));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validateFunding(funding);
assertThat(errors, empty());
}
@Test
public void testWithWorkValidationEnabled() {
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title("Work title"));
work.setWorkExternalIdentifiers(new ExternalIDs());
work.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
List<OrcidValidationError> errors = validator.validate(work);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TYPE_REQUIRED));
}
@Test
public void testWithWorkValidationDisabled() {
when(configurationService.getBooleanProperty("orcid.validation.work.enabled", true)).thenReturn(false);
Work work = new Work();
work.setWorkTitle(new WorkTitle());
work.getWorkTitle().setTitle(new Title("Work title"));
List<OrcidValidationError> errors = validator.validate(work);
assertThat(errors, empty());
}
@Test
public void testWithFundingValidationEnabled() {
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title(""));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validate(funding);
assertThat(errors, hasSize(1));
assertThat(errors, containsInAnyOrder(TITLE_REQUIRED));
}
@Test
public void testWithFundingValidationDisabled() {
when(configurationService.getBooleanProperty("orcid.validation.funding.enabled", true)).thenReturn(false);
Funding funding = new Funding();
funding.setTitle(new FundingTitle());
funding.getTitle().setTitle(new Title(""));
funding.setExternalIdentifiers(new ExternalIDs());
funding.getExternalIdentifiers().getExternalIdentifier().add(buildValidExternalID());
funding.setOrganization(buildValidOrganization());
List<OrcidValidationError> errors = validator.validate(funding);
assertThat(errors, empty());
}
private ExternalID buildValidExternalID() {
ExternalID externalID = new ExternalID();
externalID.setRelationship(Relationship.SELF);
externalID.setType("TYPE");
externalID.setValue("VALUE");
return externalID;
}
private Organization buildValidOrganization() {
Organization organization = new Organization();
organization.setName("Organization");
OrganizationAddress address = new OrganizationAddress();
address.setCity("City");
address.setCountry(Iso3166Country.BA);
organization.setAddress(address);
DisambiguatedOrganization disambiguatedOrganization = new DisambiguatedOrganization();
disambiguatedOrganization.setDisambiguatedOrganizationIdentifier("ID");
disambiguatedOrganization.setDisambiguationSource("LEI");
organization.setDisambiguatedOrganization(disambiguatedOrganization);
return organization;
}
}

View File

@@ -0,0 +1,500 @@
/**
* 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.script;
import static org.dspace.app.launcher.ScriptLauncher.handleScript;
import static org.dspace.app.matcher.LambdaMatcher.matches;
import static org.dspace.app.matcher.OrcidQueueMatcher.matches;
import static org.dspace.builder.OrcidQueueBuilder.createOrcidQueue;
import static org.dspace.orcid.OrcidOperation.DELETE;
import static org.dspace.orcid.OrcidOperation.INSERT;
import static org.dspace.orcid.OrcidOperation.UPDATE;
import static org.dspace.profile.OrcidSynchronizationMode.BATCH;
import static org.dspace.profile.OrcidSynchronizationMode.MANUAL;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.sql.SQLException;
import java.util.List;
import java.util.function.Predicate;
import org.apache.commons.lang3.ArrayUtils;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.app.launcher.ScriptLauncher;
import org.dspace.app.scripts.handler.impl.TestDSpaceRunnableHandler;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EPersonBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.OrcidTokenBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.eperson.EPerson;
import org.dspace.orcid.OrcidHistory;
import org.dspace.orcid.OrcidOperation;
import org.dspace.orcid.OrcidQueue;
import org.dspace.orcid.client.OrcidClient;
import org.dspace.orcid.client.OrcidResponse;
import org.dspace.orcid.exception.OrcidClientException;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.service.OrcidQueueService;
import org.dspace.orcid.service.impl.OrcidHistoryServiceImpl;
import org.dspace.profile.OrcidSynchronizationMode;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link OrcidBulkPush}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidBulkPushIT extends AbstractIntegrationTestWithDatabase {
private Collection profileCollection;
private Collection publicationCollection;
private OrcidHistoryServiceImpl orcidHistoryService;
private OrcidQueueService orcidQueueService;
private ConfigurationService configurationService;
private OrcidClient orcidClient;
private OrcidClient orcidClientMock;
@Before
public void setup() {
orcidHistoryService = (OrcidHistoryServiceImpl) OrcidServiceFactory.getInstance().getOrcidHistoryService();
orcidQueueService = OrcidServiceFactory.getInstance().getOrcidQueueService();
configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
context.setCurrentUser(admin);
parentCommunity = CommunityBuilder.createCommunity(context)
.withName("Parent community")
.build();
profileCollection = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Profiles")
.withEntityType("Person")
.build();
publicationCollection = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Publications")
.withEntityType("Publication")
.build();
orcidClientMock = mock(OrcidClient.class);
orcidClient = orcidHistoryService.getOrcidClient();
orcidHistoryService.setOrcidClient(orcidClientMock);
}
@After
public void after() throws SQLException {
List<OrcidHistory> records = orcidHistoryService.findAll(context);
for (OrcidHistory record : records) {
orcidHistoryService.delete(context, record);
}
orcidHistoryService.setOrcidClient(orcidClient);
}
@Test
public void testWithoutOrcidQueueRecords() throws Exception {
TestDSpaceRunnableHandler handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasSize(1));
assertThat(handler.getInfoMessages().get(0), is("Found 0 queue records to synchronize with ORCID"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
}
@Test
public void testWithManyOrcidQueueRecords() throws Exception {
context.turnOffAuthorisationSystem();
EPerson owner = EPersonBuilder.createEPerson(context)
.withEmail("owner@test.it")
.build();
context.restoreAuthSystemState();
Item firstProfileItem = createProfileItemItem("0000-1111-2222-3333", eperson, BATCH);
Item secondProfileItem = createProfileItemItem("1111-2222-3333-4444", admin, MANUAL);
Item thirdProfileItem = createProfileItemItem("2222-3333-4444-5555", owner, BATCH);
Item firstEntity = createPublication("First publication");
Item secondEntity = createPublication("Second publication");
Item thirdEntity = createPublication("Third publication");
Item fourthEntity = createPublication("Fourth publication");
Item fifthEntity = createPublication("Fifth publication");
when(orcidClientMock.push(any(), eq("0000-1111-2222-3333"), any()))
.thenReturn(createdResponse("12345"));
when(orcidClientMock.update(any(), eq("0000-1111-2222-3333"), any(), eq("98765")))
.thenReturn(updatedResponse("98765"));
when(orcidClientMock.deleteByPutCode(any(), eq("0000-1111-2222-3333"), eq("22222"), eq("/work")))
.thenReturn(deletedResponse());
when(orcidClientMock.push(any(), eq("2222-3333-4444-5555"), any()))
.thenReturn(createdResponse("11111"));
createOrcidQueue(context, firstProfileItem, firstEntity);
createOrcidQueue(context, firstProfileItem, secondEntity, "98765");
createOrcidQueue(context, firstProfileItem, "Description", "Publication", "22222");
createOrcidQueue(context, secondProfileItem, thirdEntity);
createOrcidQueue(context, secondProfileItem, fourthEntity);
createOrcidQueue(context, thirdProfileItem, fifthEntity);
context.commit();
TestDSpaceRunnableHandler handler = runBulkSynchronization(false);
String firstProfileItemId = firstProfileItem.getID().toString();
String thirdProfileItemId = thirdProfileItem.getID().toString();
assertThat(handler.getInfoMessages(), hasSize(9));
assertThat(handler.getInfoMessages(), containsInAnyOrder(
"Found 4 queue records to synchronize with ORCID",
"Addition of Publication for profile with ID: " + firstProfileItemId,
"History record created with status 201. The operation was completed successfully",
"Update of Publication for profile with ID: " + firstProfileItemId + " by put code 98765",
"History record created with status 200. The operation was completed successfully",
"Deletion of Publication for profile with ID: " + firstProfileItemId + " by put code 22222",
"History record created with status 204. The operation was completed successfully",
"Addition of Publication for profile with ID: " + thirdProfileItemId,
"History record created with status 201. The operation was completed successfully"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
verify(orcidClientMock).push(any(), eq("0000-1111-2222-3333"), any());
verify(orcidClientMock).push(any(), eq("2222-3333-4444-5555"), any());
verify(orcidClientMock).update(any(), eq("0000-1111-2222-3333"), any(), eq("98765"));
verify(orcidClientMock).deleteByPutCode(any(), eq("0000-1111-2222-3333"), eq("22222"), eq("/work"));
verifyNoMoreInteractions(orcidClientMock);
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(2));
assertThat(queueRecords, hasItem(matches(secondProfileItem, thirdEntity, "Publication", INSERT, 0)));
assertThat(queueRecords, hasItem(matches(secondProfileItem, fourthEntity, "Publication", INSERT, 0)));
List<OrcidHistory> historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(4));
assertThat(historyRecords, hasItem(matches(history(firstProfileItem, firstEntity, 201, INSERT))));
assertThat(historyRecords, hasItem(matches(history(firstProfileItem, secondEntity, 200, UPDATE))));
assertThat(historyRecords, hasItem(matches(history(firstProfileItem, 204, DELETE))));
assertThat(historyRecords, hasItem(matches(history(thirdProfileItem, fifthEntity, 201, INSERT))));
}
@Test
public void testWithOneValidationError() throws Exception {
Item firstProfileItem = createProfileItemItem("0000-1111-2222-3333", eperson, BATCH);
Item secondProfileItem = createProfileItemItem("1111-2222-3333-4444", admin, BATCH);
Item firstEntity = createPublication("First publication");
Item secondEntity = createPublication("");
Item thirdEntity = createPublication("Third publication");
when(orcidClientMock.push(any(), eq("0000-1111-2222-3333"), any()))
.thenReturn(createdResponse("12345"));
when(orcidClientMock.push(any(), eq("1111-2222-3333-4444"), any()))
.thenReturn(createdResponse("55555"));
createOrcidQueue(context, firstProfileItem, firstEntity);
createOrcidQueue(context, firstProfileItem, secondEntity, "98765");
createOrcidQueue(context, secondProfileItem, thirdEntity);
context.commit();
TestDSpaceRunnableHandler handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasSize(6));
assertThat(handler.getInfoMessages(), containsInAnyOrder(
"Found 3 queue records to synchronize with ORCID",
"Addition of Publication for profile with ID: " + firstProfileItem.getID().toString(),
"History record created with status 201. The operation was completed successfully",
"Update of Publication for profile with ID: " + firstProfileItem.getID().toString() + " by put code 98765",
"Addition of Publication for profile with ID: " + secondProfileItem.getID().toString(),
"History record created with status 201. The operation was completed successfully"));
assertThat(handler.getErrorMessages(), hasSize(1));
assertThat(handler.getErrorMessages(), containsInAnyOrder(
"Errors occurs during ORCID object validation. Error codes: title.required"));
assertThat(handler.getWarningMessages(), empty());
verify(orcidClientMock).push(any(), eq("0000-1111-2222-3333"), any());
verify(orcidClientMock).push(any(), eq("1111-2222-3333-4444"), any());
verifyNoMoreInteractions(orcidClientMock);
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(firstProfileItem, secondEntity, "Publication", UPDATE, 1)));
List<OrcidHistory> historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(2));
assertThat(historyRecords, hasItem(matches(history(firstProfileItem, firstEntity, 201, INSERT))));
assertThat(historyRecords, hasItem(matches(history(secondProfileItem, thirdEntity, 201, INSERT))));
}
@Test
public void testWithUnexpectedErrorForMissingOrcid() throws Exception {
Item firstProfileItem = createProfileItemItem("0000-1111-2222-3333", eperson, BATCH);
Item secondProfileItem = createProfileItemItem("", admin, BATCH);
Item firstEntity = createPublication("First publication");
Item secondEntity = createPublication("Second publication");
when(orcidClientMock.push(any(), eq("0000-1111-2222-3333"), any()))
.thenReturn(createdResponse("12345"));
createOrcidQueue(context, secondProfileItem, secondEntity);
createOrcidQueue(context, firstProfileItem, firstEntity);
context.commit();
TestDSpaceRunnableHandler handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasSize(4));
assertThat(handler.getInfoMessages(), containsInAnyOrder(
"Found 2 queue records to synchronize with ORCID",
"Addition of Publication for profile with ID: " + secondProfileItem.getID().toString(),
"Addition of Publication for profile with ID: " + firstProfileItem.getID().toString(),
"History record created with status 201. The operation was completed successfully"));
assertThat(handler.getErrorMessages(), hasSize(1));
assertThat(handler.getErrorMessages(), contains("An unexpected error occurs during the synchronization: "
+ "The related profileItem item (id = " + secondProfileItem.getID() + ") does not have an orcid"));
assertThat(handler.getWarningMessages(), empty());
verify(orcidClientMock).push(any(), eq("0000-1111-2222-3333"), any());
verifyNoMoreInteractions(orcidClientMock);
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(secondProfileItem, secondEntity, "Publication", INSERT, 1)));
List<OrcidHistory> historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(1));
assertThat(historyRecords, hasItem(matches(history(firstProfileItem, firstEntity, 201, INSERT))));
}
@Test
public void testWithOrcidClientException() throws Exception {
Item firstProfileItem = createProfileItemItem("0000-1111-2222-3333", eperson, BATCH);
Item secondProfileItem = createProfileItemItem("1111-2222-3333-4444", admin, BATCH);
Item firstEntity = createPublication("First publication");
Item secondEntity = createPublication("Second publication");
when(orcidClientMock.push(any(), eq("0000-1111-2222-3333"), any()))
.thenThrow(new OrcidClientException(400, "Bad request"));
when(orcidClientMock.push(any(), eq("1111-2222-3333-4444"), any()))
.thenReturn(createdResponse("55555"));
createOrcidQueue(context, firstProfileItem, firstEntity);
createOrcidQueue(context, secondProfileItem, secondEntity);
context.commit();
TestDSpaceRunnableHandler handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasSize(5));
assertThat(handler.getInfoMessages(), containsInAnyOrder(
"Found 2 queue records to synchronize with ORCID",
"Addition of Publication for profile with ID: " + firstProfileItem.getID().toString(),
"History record created with status 400. The resource sent to ORCID registry is not valid",
"Addition of Publication for profile with ID: " + secondProfileItem.getID().toString(),
"History record created with status 201. The operation was completed successfully"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
verify(orcidClientMock).push(any(), eq("0000-1111-2222-3333"), any());
verify(orcidClientMock).push(any(), eq("1111-2222-3333-4444"), any());
verifyNoMoreInteractions(orcidClientMock);
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(firstProfileItem, firstEntity, "Publication", INSERT, 1)));
List<OrcidHistory> historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(2));
assertThat(historyRecords, hasItem(matches(history(firstProfileItem, firstEntity, 400, INSERT))));
assertThat(historyRecords, hasItem(matches(history(secondProfileItem, secondEntity, 201, INSERT))));
}
@Test
@SuppressWarnings("unchecked")
public void testWithTooManyAttempts() throws Exception {
configurationService.setProperty("orcid.bulk-synchronization.max-attempts", 2);
Item profileItem = createProfileItemItem("0000-1111-2222-3333", eperson, BATCH);
Item entity = createPublication("First publication");
when(orcidClientMock.push(any(), eq("0000-1111-2222-3333"), any()))
.thenThrow(new OrcidClientException(400, "Bad request"));
createOrcidQueue(context, profileItem, entity);
// First attempt
TestDSpaceRunnableHandler handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasItem("Found 1 queue records to synchronize with ORCID"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
List<OrcidQueue> queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(profileItem, entity, "Publication", INSERT, 1)));
List<OrcidHistory> historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(1));
assertThat(historyRecords, hasItem(matches(history(profileItem, entity, 400, INSERT))));
// Second attempt
handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasItem("Found 1 queue records to synchronize with ORCID"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(profileItem, entity, "Publication", INSERT, 2)));
historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(2));
assertThat(historyRecords, contains(matches(history(profileItem, entity, 400, INSERT)),
matches(history(profileItem, entity, 400, INSERT))));
// Third attempt
handler = runBulkSynchronization(false);
assertThat(handler.getInfoMessages(), hasItem("Found 0 queue records to synchronize with ORCID"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(profileItem, entity, "Publication", INSERT, 2)));
historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(2));
assertThat(historyRecords, contains(matches(history(profileItem, entity, 400, INSERT)),
matches(history(profileItem, entity, 400, INSERT))));
// Fourth attempt forcing synchronization
handler = runBulkSynchronization(true);
assertThat(handler.getInfoMessages(), hasItem("Found 1 queue records to synchronize with ORCID"));
assertThat(handler.getErrorMessages(), empty());
assertThat(handler.getWarningMessages(), empty());
queueRecords = orcidQueueService.findAll(context);
assertThat(queueRecords, hasSize(1));
assertThat(queueRecords, hasItem(matches(profileItem, entity, "Publication", INSERT, 3)));
historyRecords = orcidHistoryService.findAll(context);
assertThat(historyRecords, hasSize(3));
assertThat(historyRecords, contains(matches(history(profileItem, entity, 400, INSERT)),
matches(history(profileItem, entity, 400, INSERT)),
matches(history(profileItem, entity, 400, INSERT))));
}
private Predicate<OrcidHistory> history(Item profileItem, Item entity, int status, OrcidOperation operation) {
return history -> profileItem.equals(history.getProfileItem())
&& entity.equals(history.getEntity())
&& history.getStatus().equals(status)
&& operation == history.getOperation();
}
private Predicate<OrcidHistory> history(Item profileItem, int status, OrcidOperation operation) {
return history -> profileItem.equals(history.getProfileItem())
&& history.getStatus().equals(status)
&& operation == history.getOperation();
}
private TestDSpaceRunnableHandler runBulkSynchronization(boolean forceSynchronization) throws Exception {
String[] args = new String[] { "orcid-bulk-push" };
args = forceSynchronization ? ArrayUtils.add(args, "-f") : args;
TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler();
handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl);
return handler;
}
private Item createProfileItemItem(String orcid, EPerson owner, OrcidSynchronizationMode mode)
throws Exception {
Item item = ItemBuilder.createItem(context, profileCollection)
.withTitle("Test user")
.withOrcidIdentifier(orcid)
.withOrcidSynchronizationMode(mode)
.withDspaceObjectOwner(owner.getFullName(), owner.getID().toString())
.build();
OrcidTokenBuilder.create(context, owner, "9c913f57-961e-48af-9223-cfad6562c925")
.withProfileItem(item)
.build();
return item;
}
private Item createPublication(String title) {
return ItemBuilder.createItem(context, publicationCollection)
.withTitle(title)
.withType("Controlled Vocabulary for Resource Type Genres::dataset")
.build();
}
private OrcidResponse createdResponse(String putCode) {
return new OrcidResponse(201, putCode, null);
}
private OrcidResponse updatedResponse(String putCode) {
return new OrcidResponse(200, putCode, null);
}
private OrcidResponse deletedResponse() {
return new OrcidResponse(204, null, null);
}
}

View File

@@ -0,0 +1,296 @@
/**
* 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.service;
import static org.apache.commons.lang.StringUtils.endsWith;
import static org.dspace.app.matcher.LambdaMatcher.has;
import static org.dspace.app.matcher.LambdaMatcher.matches;
import static org.dspace.builder.RelationshipTypeBuilder.createRelationshipTypeBuilder;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.orcid.jaxb.model.common.ContributorRole.AUTHOR;
import static org.orcid.jaxb.model.common.ContributorRole.EDITOR;
import static org.orcid.jaxb.model.common.FundingContributorRole.LEAD;
import static org.orcid.jaxb.model.common.SequenceType.ADDITIONAL;
import static org.orcid.jaxb.model.common.SequenceType.FIRST;
import java.util.List;
import java.util.function.Predicate;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.EntityTypeBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.builder.RelationshipBuilder;
import org.dspace.content.Collection;
import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.RelationshipType;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.junit.Before;
import org.junit.Test;
import org.orcid.jaxb.model.common.ContributorRole;
import org.orcid.jaxb.model.common.FundingContributorRole;
import org.orcid.jaxb.model.common.Iso3166Country;
import org.orcid.jaxb.model.common.Relationship;
import org.orcid.jaxb.model.common.SequenceType;
import org.orcid.jaxb.model.common.WorkType;
import org.orcid.jaxb.model.v3.release.common.Contributor;
import org.orcid.jaxb.model.v3.release.common.FuzzyDate;
import org.orcid.jaxb.model.v3.release.common.Organization;
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.ExternalID;
import org.orcid.jaxb.model.v3.release.record.Funding;
import org.orcid.jaxb.model.v3.release.record.FundingContributor;
import org.orcid.jaxb.model.v3.release.record.FundingContributors;
import org.orcid.jaxb.model.v3.release.record.Work;
/**
* Integration tests for {@link OrcidEntityFactoryService}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidEntityFactoryServiceIT extends AbstractIntegrationTestWithDatabase {
private OrcidEntityFactoryService entityFactoryService;
private Collection orgUnits;
private Collection publications;
private Collection projects;
@Before
public void setup() {
entityFactoryService = OrcidServiceFactory.getInstance().getOrcidEntityFactoryService();
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withTitle("Parent community")
.build();
orgUnits = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.withEntityType("OrgUnit")
.build();
publications = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.withEntityType("Publication")
.build();
projects = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.withEntityType("Project")
.build();
context.restoreAuthSystemState();
}
@Test
public void testWorkCreation() {
context.turnOffAuthorisationSystem();
Item publication = ItemBuilder.createItem(context, publications)
.withTitle("Test publication")
.withAuthor("Walter White")
.withAuthor("Jesse Pinkman")
.withEditor("Editor")
.withIssueDate("2021-04-30")
.withDescriptionAbstract("Publication description")
.withLanguage("en_US")
.withType("Book")
.withIsPartOf("Journal")
.withDoiIdentifier("doi-id")
.withScopusIdentifier("scopus-id")
.build();
context.restoreAuthSystemState();
Activity activity = entityFactoryService.createOrcidObject(context, publication);
assertThat(activity, instanceOf(Work.class));
Work work = (Work) activity;
assertThat(work.getJournalTitle(), notNullValue());
assertThat(work.getJournalTitle().getContent(), is("Journal"));
assertThat(work.getLanguageCode(), is("en"));
assertThat(work.getPublicationDate(), matches(date("2021", "04", "30")));
assertThat(work.getShortDescription(), is("Publication description"));
assertThat(work.getPutCode(), nullValue());
assertThat(work.getWorkType(), is(WorkType.BOOK));
assertThat(work.getWorkTitle(), notNullValue());
assertThat(work.getWorkTitle().getTitle(), notNullValue());
assertThat(work.getWorkTitle().getTitle().getContent(), is("Test publication"));
assertThat(work.getWorkContributors(), notNullValue());
assertThat(work.getUrl(), matches(urlEndsWith(publication.getHandle())));
List<Contributor> contributors = work.getWorkContributors().getContributor();
assertThat(contributors, hasSize(3));
assertThat(contributors, has(contributor("Walter White", AUTHOR, FIRST)));
assertThat(contributors, has(contributor("Editor", EDITOR, FIRST)));
assertThat(contributors, has(contributor("Jesse Pinkman", AUTHOR, ADDITIONAL)));
assertThat(work.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = work.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(3));
assertThat(externalIds, has(selfExternalId("doi", "doi-id")));
assertThat(externalIds, has(selfExternalId("eid", "scopus-id")));
assertThat(externalIds, has(selfExternalId("handle", publication.getHandle())));
}
@Test
public void testEmptyWorkWithUnknownTypeCreation() {
context.turnOffAuthorisationSystem();
Item publication = ItemBuilder.createItem(context, publications)
.withType("TYPE")
.build();
context.restoreAuthSystemState();
Activity activity = entityFactoryService.createOrcidObject(context, publication);
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.getWorkType(), is(WorkType.OTHER));
assertThat(work.getWorkTitle(), nullValue());
assertThat(work.getWorkContributors(), notNullValue());
assertThat(work.getWorkContributors().getContributor(), empty());
assertThat(work.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = work.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(1));
assertThat(externalIds, has(selfExternalId("handle", publication.getHandle())));
}
@Test
public void testFundingCreation() {
context.turnOffAuthorisationSystem();
Item orgUnit = ItemBuilder.createItem(context, orgUnits)
.withOrgUnitLegalName("4Science")
.withOrgUnitCountry("IT")
.withOrgUnitLocality("Milan")
.withOrgUnitCrossrefIdentifier("12345")
.build();
Item projectItem = ItemBuilder.createItem(context, projects)
.withTitle("Test funding")
.withProjectStartDate("2001-03")
.withProjectEndDate("2010-03-25")
.withProjectInvestigator("Walter White")
.withProjectInvestigator("Jesse Pinkman")
.withProjectAmount("123")
.withProjectAmountCurrency("EUR")
.withOtherIdentifier("888-666-444")
.withIdentifier("000-111-333")
.withDescription("This is a funding to test orcid mapping")
.build();
EntityType projectType = EntityTypeBuilder.createEntityTypeBuilder(context, "Project").build();
EntityType orgUnitType = EntityTypeBuilder.createEntityTypeBuilder(context, "OrgUnit").build();
RelationshipType isAuthorOfPublication = createRelationshipTypeBuilder(context, orgUnitType, projectType,
"isOrgUnitOfProject", "isProjectOfOrgUnit", 0, null, 0, null).build();
RelationshipBuilder.createRelationshipBuilder(context, orgUnit, projectItem, isAuthorOfPublication).build();
context.restoreAuthSystemState();
Activity activity = entityFactoryService.createOrcidObject(context, projectItem);
assertThat(activity, instanceOf(Funding.class));
Funding funding = (Funding) activity;
assertThat(funding.getTitle(), notNullValue());
assertThat(funding.getTitle().getTitle(), notNullValue());
assertThat(funding.getTitle().getTitle().getContent(), is("Test funding"));
assertThat(funding.getStartDate(), matches(date("2001", "03", "01")));
assertThat(funding.getEndDate(), matches(date("2010", "03", "25")));
assertThat(funding.getDescription(), is("This is a funding to test orcid mapping"));
assertThat(funding.getUrl(), matches(urlEndsWith(projectItem.getHandle())));
assertThat(funding.getAmount(), notNullValue());
assertThat(funding.getAmount().getContent(), is("123"));
assertThat(funding.getAmount().getCurrencyCode(), is("EUR"));
Organization organization = funding.getOrganization();
assertThat(organization, notNullValue());
assertThat(organization.getName(), is("4Science"));
assertThat(organization.getAddress(), notNullValue());
assertThat(organization.getAddress().getCountry(), is(Iso3166Country.IT));
assertThat(organization.getAddress().getCity(), is("Milan"));
assertThat(organization.getDisambiguatedOrganization(), notNullValue());
assertThat(organization.getDisambiguatedOrganization().getDisambiguatedOrganizationIdentifier(), is("12345"));
assertThat(organization.getDisambiguatedOrganization().getDisambiguationSource(), is("FUNDREF"));
FundingContributors fundingContributors = funding.getContributors();
assertThat(fundingContributors, notNullValue());
List<FundingContributor> contributors = fundingContributors.getContributor();
assertThat(contributors, hasSize(2));
assertThat(contributors, has(fundingContributor("Walter White", LEAD)));
assertThat(contributors, has(fundingContributor("Jesse Pinkman", LEAD)));
assertThat(funding.getExternalIdentifiers(), notNullValue());
List<ExternalID> externalIds = funding.getExternalIdentifiers().getExternalIdentifier();
assertThat(externalIds, hasSize(2));
assertThat(externalIds, has(selfExternalId("other-id", "888-666-444")));
assertThat(externalIds, has(selfExternalId("grant_number", "000-111-333")));
}
private Predicate<ExternalID> selfExternalId(String type, String value) {
return externalId(type, value, Relationship.SELF);
}
private Predicate<ExternalID> externalId(String type, String value, Relationship relationship) {
return externalId -> externalId.getRelationship() == relationship
&& type.equals(externalId.getType())
&& value.equals(externalId.getValue());
}
private Predicate<Contributor> contributor(String name, ContributorRole role, SequenceType sequence) {
return contributor -> contributor.getCreditName().getContent().equals(name)
&& role.equals(contributor.getContributorAttributes().getContributorRole())
&& contributor.getContributorAttributes().getContributorSequence() == sequence;
}
private Predicate<FundingContributor> fundingContributor(String name, FundingContributorRole role) {
return contributor -> contributor.getCreditName().getContent().equals(name)
&& role.equals(contributor.getContributorAttributes().getContributorRole());
}
private Predicate<? super FuzzyDate> date(String year, String month, String days) {
return date -> date != null
&& year.equals(date.getYear().getValue())
&& month.equals(date.getMonth().getValue())
&& days.equals(date.getDay().getValue());
}
private Predicate<Url> urlEndsWith(String handle) {
return url -> url != null && url.getValue() != null && endsWith(url.getValue(), handle);
}
}

View File

@@ -0,0 +1,244 @@
/**
* 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.service;
import static org.dspace.app.matcher.LambdaMatcher.matches;
import static org.dspace.orcid.model.OrcidProfileSectionType.COUNTRY;
import static org.dspace.orcid.model.OrcidProfileSectionType.EXTERNAL_IDS;
import static org.dspace.orcid.model.OrcidProfileSectionType.KEYWORDS;
import static org.dspace.orcid.model.OrcidProfileSectionType.OTHER_NAMES;
import static org.dspace.orcid.model.OrcidProfileSectionType.RESEARCHER_URLS;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import java.util.List;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.orcid.factory.OrcidServiceFactory;
import org.dspace.orcid.model.OrcidProfileSectionType;
import org.dspace.orcid.model.factory.OrcidProfileSectionFactory;
import org.junit.Before;
import org.junit.Test;
import org.orcid.jaxb.model.common.Iso3166Country;
import org.orcid.jaxb.model.common.Relationship;
import org.orcid.jaxb.model.v3.release.record.Address;
import org.orcid.jaxb.model.v3.release.record.Keyword;
import org.orcid.jaxb.model.v3.release.record.OtherName;
import org.orcid.jaxb.model.v3.release.record.PersonExternalIdentifier;
import org.orcid.jaxb.model.v3.release.record.ResearcherUrl;
/**
* Integration tests for {@link OrcidProfileSectionFactoryService}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OrcidProfileSectionFactoryServiceIT extends AbstractIntegrationTestWithDatabase {
private OrcidProfileSectionFactoryService profileSectionFactoryService;
private ItemService itemService;
private Collection collection;
@Before
public void setup() {
profileSectionFactoryService = OrcidServiceFactory.getInstance().getOrcidProfileSectionFactoryService();
itemService = ContentServiceFactory.getInstance().getItemService();
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withTitle("Parent community")
.build();
collection = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.withEntityType("Person")
.build();
context.restoreAuthSystemState();
}
@Test
public void testAddressCreation() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withPersonCountry("IT")
.build();
context.restoreAuthSystemState();
List<MetadataValue> values = List.of(getMetadata(item, "person.country", 0));
Object orcidObject = profileSectionFactoryService.createOrcidObject(context, values, COUNTRY);
assertThat(orcidObject, instanceOf(Address.class));
Address address = (Address) orcidObject;
assertThat(address.getCountry(), notNullValue());
assertThat(address.getCountry().getValue(), is(Iso3166Country.IT));
}
@Test
public void testAddressMetadataSignatureGeneration() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withPersonCountry("IT")
.build();
context.restoreAuthSystemState();
OrcidProfileSectionFactory countryFactory = getFactory(item, COUNTRY);
List<String> signatures = countryFactory.getMetadataSignatures(context, item);
assertThat(signatures, hasSize(1));
assertThat(countryFactory.getDescription(context, item, signatures.get(0)), is("IT"));
}
@Test
public void testExternalIdentifiersCreation() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withScopusAuthorIdentifier("SCOPUS-123456")
.withResearcherIdentifier("R-ID-01")
.build();
context.restoreAuthSystemState();
List<MetadataValue> values = List.of(getMetadata(item, "person.identifier.scopus-author-id", 0));
Object firstOrcidObject = profileSectionFactoryService.createOrcidObject(context, values, EXTERNAL_IDS);
assertThat(firstOrcidObject, instanceOf(PersonExternalIdentifier.class));
assertThat((PersonExternalIdentifier) firstOrcidObject, matches(hasTypeAndValue("SCOPUS", "SCOPUS-123456")));
values = List.of(getMetadata(item, "person.identifier.rid", 0));
Object secondOrcidObject = profileSectionFactoryService.createOrcidObject(context, values, EXTERNAL_IDS);
assertThat(secondOrcidObject, instanceOf(PersonExternalIdentifier.class));
assertThat((PersonExternalIdentifier) secondOrcidObject, matches(hasTypeAndValue("RID", "R-ID-01")));
}
@Test
public void testExternalIdentifiersGeneration() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withScopusAuthorIdentifier("SCOPUS-123456")
.withResearcherIdentifier("R-ID-01")
.build();
context.restoreAuthSystemState();
OrcidProfileSectionFactory externalIdsFactory = getFactory(item, EXTERNAL_IDS);
List<String> signatures = externalIdsFactory.getMetadataSignatures(context, item);
assertThat(signatures, hasSize(2));
List<String> descriptions = signatures.stream()
.map(signature -> externalIdsFactory.getDescription(context, item, signature))
.collect(Collectors.toList());
assertThat(descriptions, containsInAnyOrder("SCOPUS-123456", "R-ID-01"));
}
@Test
public void testResearcherUrlsCreation() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withUriIdentifier("www.test.com")
.build();
context.restoreAuthSystemState();
List<MetadataValue> values = List.of(getMetadata(item, "dc.identifier.uri", 0));
Object orcidObject = profileSectionFactoryService.createOrcidObject(context, values, RESEARCHER_URLS);
assertThat(orcidObject, instanceOf(ResearcherUrl.class));
assertThat((ResearcherUrl) orcidObject, matches(hasUrl("www.test.com")));
}
@Test
public void testKeywordsCreation() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withSubject("Subject")
.build();
context.restoreAuthSystemState();
List<MetadataValue> values = List.of(getMetadata(item, "dc.subject", 0));
Object orcidObject = profileSectionFactoryService.createOrcidObject(context, values, KEYWORDS);
assertThat(orcidObject, instanceOf(Keyword.class));
assertThat((Keyword) orcidObject, matches(hasContent("Subject")));
}
@Test
public void testOtherNamesCreation() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Test profile")
.withVariantName("Variant name")
.withVernacularName("Vernacular name")
.build();
context.restoreAuthSystemState();
List<MetadataValue> values = List.of(getMetadata(item, "person.name.variant", 0));
Object orcidObject = profileSectionFactoryService.createOrcidObject(context, values, OTHER_NAMES);
assertThat(orcidObject, instanceOf(OtherName.class));
assertThat((OtherName) orcidObject, matches(hasValue("Variant name")));
values = List.of(getMetadata(item, "person.name.translated", 0));
orcidObject = profileSectionFactoryService.createOrcidObject(context, values, OTHER_NAMES);
assertThat(orcidObject, instanceOf(OtherName.class));
assertThat((OtherName) orcidObject, matches(hasValue("Vernacular name")));
}
private MetadataValue getMetadata(Item item, String metadataField, int place) {
List<MetadataValue> values = itemService.getMetadataByMetadataString(item, metadataField);
assertThat(values.size(), greaterThan(place));
return values.get(place);
}
private Predicate<PersonExternalIdentifier> hasTypeAndValue(String type, String value) {
return identifier -> value.equals(identifier.getValue())
&& type.equals(identifier.getType())
&& identifier.getRelationship() == Relationship.SELF
&& identifier.getUrl() != null && value.equals(identifier.getUrl().getValue());
}
private Predicate<ResearcherUrl> hasUrl(String url) {
return researcherUrl -> researcherUrl.getUrl() != null && url.equals(researcherUrl.getUrl().getValue());
}
private Predicate<Keyword> hasContent(String value) {
return keyword -> value.equals(keyword.getContent());
}
private Predicate<OtherName> hasValue(String value) {
return name -> value.equals(name.getContent());
}
private OrcidProfileSectionFactory getFactory(Item item, OrcidProfileSectionType sectionType) {
return profileSectionFactoryService.findBySectionType(sectionType)
.orElseThrow(() -> new IllegalStateException("No profile section factory of type " + sectionType));
}
}

View File

@@ -0,0 +1,166 @@
/**
* 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.service;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import java.util.List;
import org.dspace.AbstractIntegrationTestWithDatabase;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.content.Collection;
import org.dspace.content.Item;
import org.dspace.content.MetadataValue;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
import org.dspace.orcid.service.impl.PlainMetadataSignatureGeneratorImpl;
import org.junit.Before;
import org.junit.Test;
/**
* Integration tests for {@link PlainMetadataSignatureGeneratorImpl}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class PlainMetadataSignatureGeneratorIT extends AbstractIntegrationTestWithDatabase {
private ItemService itemService = ContentServiceFactory.getInstance().getItemService();
private Collection collection;
private MetadataSignatureGenerator generator = new PlainMetadataSignatureGeneratorImpl();
@Before
public void setup() {
context.turnOffAuthorisationSystem();
parentCommunity = CommunityBuilder.createCommunity(context)
.withTitle("Parent community")
.build();
collection = CollectionBuilder.createCollection(context, parentCommunity)
.withName("Collection")
.withEntityType("Person")
.build();
context.restoreAuthSystemState();
}
@Test
public void testSignatureGenerationWithManyMetadataValues() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Item title")
.withIssueDate("2020-01-01")
.withAuthor("Jesse Pinkman")
.withEditor("Editor")
.build();
context.restoreAuthSystemState();
MetadataValue author = getMetadata(item, "dc.contributor.author", 0);
MetadataValue editor = getMetadata(item, "dc.contributor.editor", 0);
String signature = generator.generate(context, List.of(author, editor));
assertThat(signature, notNullValue());
String expectedSignature = "dc.contributor.author::Jesse Pinkman§§"
+ "dc.contributor.editor::Editor";
assertThat(signature, equalTo(expectedSignature));
String anotherSignature = generator.generate(context, List.of(editor, author));
assertThat(anotherSignature, equalTo(signature));
List<MetadataValue> metadataValues = generator.findBySignature(context, item, signature);
assertThat(metadataValues, hasSize(2));
assertThat(metadataValues, containsInAnyOrder(author, editor));
}
@Test
public void testSignatureGenerationWithSingleMetadataValue() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Item title")
.withDescription("Description")
.withAuthor("Jesse Pinkman")
.withUriIdentifier("https://www.4science.it/en")
.build();
context.restoreAuthSystemState();
MetadataValue description = getMetadata(item, "dc.description", 0);
String signature = generator.generate(context, List.of(description));
assertThat(signature, notNullValue());
assertThat(signature, equalTo("dc.description::Description"));
List<MetadataValue> metadataValues = generator.findBySignature(context, item, signature);
assertThat(metadataValues, hasSize(1));
assertThat(metadataValues, containsInAnyOrder(description));
MetadataValue url = getMetadata(item, "dc.identifier.uri", 0);
signature = generator.generate(context, List.of(url));
assertThat(signature, equalTo("dc.identifier.uri::https://www.4science.it/en"));
metadataValues = generator.findBySignature(context, item, signature);
assertThat(metadataValues, hasSize(1));
assertThat(metadataValues, containsInAnyOrder(url));
}
@Test
public void testSignatureGenerationWithManyEqualsMetadataValues() {
context.turnOffAuthorisationSystem();
Item item = ItemBuilder.createItem(context, collection)
.withTitle("Item title")
.withDescription("Description")
.withAuthor("Jesse Pinkman")
.withAuthor("Jesse Pinkman")
.build();
context.restoreAuthSystemState();
MetadataValue firstAuthor = getMetadata(item, "dc.contributor.author", 0);
String firstSignature = generator.generate(context, List.of(firstAuthor));
assertThat(firstSignature, notNullValue());
assertThat(firstSignature, equalTo("dc.contributor.author::Jesse Pinkman"));
MetadataValue secondAuthor = getMetadata(item, "dc.contributor.author", 1);
String secondSignature = generator.generate(context, List.of(secondAuthor));
assertThat(secondSignature, notNullValue());
assertThat(secondSignature, equalTo("dc.contributor.author::Jesse Pinkman"));
List<MetadataValue> metadataValues = generator.findBySignature(context, item, firstSignature);
assertThat(metadataValues, hasSize(1));
assertThat(metadataValues, anyOf(contains(firstAuthor), contains(secondAuthor)));
}
private MetadataValue getMetadata(Item item, String metadataField, int place) {
List<MetadataValue> values = itemService.getMetadataByMetadataString(item, metadataField);
assertThat(values.size(), greaterThan(place));
return values.get(place);
}
}

View File

@@ -0,0 +1,167 @@
/**
* 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.util;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.when;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import org.apache.commons.io.FileUtils;
import org.dspace.services.ConfigurationService;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
/**
* Unit tests for {@link SimpleMapConverter}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
@RunWith(MockitoJUnitRunner.class)
public class SimpleMapConverterTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Mock
private ConfigurationService configurationService;
private File dspaceDir;
private File crosswalksDir;
@Before
public void before() throws IOException {
dspaceDir = folder.getRoot();
crosswalksDir = folder.newFolder("config", "crosswalks");
}
@Test
public void testPropertiesParsing() throws IOException {
when(configurationService.getProperty("dspace.dir")).thenReturn(dspaceDir.getAbsolutePath());
createFileInFolder(crosswalksDir, "test.properties", "key1=value1\nkey2=value2\nkey3=value3");
SimpleMapConverter simpleMapConverter = new SimpleMapConverter();
simpleMapConverter.setConfigurationService(configurationService);
simpleMapConverter.setConverterNameFile("test.properties");
simpleMapConverter.init();
assertThat(simpleMapConverter.getValue("key1"), is("value1"));
assertThat(simpleMapConverter.getValue("key2"), is("value2"));
assertThat(simpleMapConverter.getValue("key3"), is("value3"));
assertThat(simpleMapConverter.getValue(""), is(""));
assertThat(simpleMapConverter.getValue(null), nullValue());
assertThat(simpleMapConverter.getValue("key4"), is("key4"));
}
@Test
public void testPropertiesParsingWithDefaultValue() throws IOException {
when(configurationService.getProperty("dspace.dir")).thenReturn(dspaceDir.getAbsolutePath());
createFileInFolder(crosswalksDir, "test.properties", "key1=value1\nkey2=value2\nkey3=value3");
SimpleMapConverter simpleMapConverter = new SimpleMapConverter();
simpleMapConverter.setConfigurationService(configurationService);
simpleMapConverter.setConverterNameFile("test.properties");
simpleMapConverter.setDefaultValue("default");
simpleMapConverter.init();
assertThat(simpleMapConverter.getValue("key1"), is("value1"));
assertThat(simpleMapConverter.getValue("key2"), is("value2"));
assertThat(simpleMapConverter.getValue("key3"), is("value3"));
assertThat(simpleMapConverter.getValue(""), is("default"));
assertThat(simpleMapConverter.getValue(null), is("default"));
assertThat(simpleMapConverter.getValue("key4"), is("default"));
}
@Test
public void testPropertiesParsingWithAnUnexistingFile() throws IOException {
when(configurationService.getProperty("dspace.dir")).thenReturn(dspaceDir.getAbsolutePath());
SimpleMapConverter simpleMapConverter = new SimpleMapConverter();
simpleMapConverter.setConfigurationService(configurationService);
simpleMapConverter.setConverterNameFile("test.properties");
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> simpleMapConverter.init());
assertThat(exception.getMessage(),
is("An error occurs parsing " + dspaceDir.getAbsolutePath() + "/config/crosswalks/test.properties"));
Throwable cause = exception.getCause();
assertThat(cause, notNullValue());
assertThat(cause, instanceOf(FileNotFoundException.class));
}
@Test
public void testPropertiesParsingWithCorruptedFile() throws IOException {
when(configurationService.getProperty("dspace.dir")).thenReturn(dspaceDir.getAbsolutePath());
createFileInFolder(crosswalksDir, "test.properties", "key1=value1\nkey2\nkey3=value3");
SimpleMapConverter simpleMapConverter = new SimpleMapConverter();
simpleMapConverter.setConfigurationService(configurationService);
simpleMapConverter.setConverterNameFile("test.properties");
simpleMapConverter.init();
assertThat(simpleMapConverter.getValue("key1"), is("value1"));
assertThat(simpleMapConverter.getValue("key2"), is("key2"));
assertThat(simpleMapConverter.getValue("key3"), is("value3"));
assertThat(simpleMapConverter.getValue("key4"), is("key4"));
}
@Test
public void testPropertiesParsingWithEmptyFile() throws IOException {
when(configurationService.getProperty("dspace.dir")).thenReturn(dspaceDir.getAbsolutePath());
createFileInFolder(crosswalksDir, "test.properties", "");
SimpleMapConverter simpleMapConverter = new SimpleMapConverter();
simpleMapConverter.setConfigurationService(configurationService);
simpleMapConverter.setConverterNameFile("test.properties");
simpleMapConverter.init();
assertThat(simpleMapConverter.getValue("key1"), is("key1"));
assertThat(simpleMapConverter.getValue("key2"), is("key2"));
}
private void createFileInFolder(File folder, String name, String content) throws IOException {
File file = new File(folder, name);
FileUtils.write(file, content, StandardCharsets.UTF_8);
}
}

View File

@@ -11,7 +11,6 @@ import java.sql.SQLException;
import java.util.UUID; import java.util.UUID;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.authorization.AuthorizationFeature; import org.dspace.app.rest.authorization.AuthorizationFeature;
import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation;
import org.dspace.app.rest.model.BaseObjectRest; import org.dspace.app.rest.model.BaseObjectRest;
@@ -22,6 +21,7 @@ import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.profile.service.ResearcherProfileService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -23,6 +23,7 @@ import org.dspace.content.MetadataValue;
import org.dspace.content.service.ItemService; import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -42,6 +43,9 @@ public class CanSynchronizeWithORCID implements AuthorizationFeature {
@Autowired @Autowired
private ItemService itemService; private ItemService itemService;
@Autowired
private ConfigurationService configurationService;
/** /**
* This method returns true if the BaseObjectRest object is an instance of * This method returns true if the BaseObjectRest object is an instance of
* {@link ItemRest}, there is a current user in the {@link Context} and it is * {@link ItemRest}, there is a current user in the {@link Context} and it is
@@ -59,7 +63,7 @@ public class CanSynchronizeWithORCID implements AuthorizationFeature {
String id = ((ItemRest) object).getId(); String id = ((ItemRest) object).getId();
Item item = itemService.find(context, UUID.fromString(id)); Item item = itemService.find(context, UUID.fromString(id));
return isDspaceObjectOwner(ePerson, item); return isOrcidSynchronizationEnabled() && isDspaceObjectOwner(ePerson, item);
} }
@Override @Override
@@ -67,6 +71,10 @@ public class CanSynchronizeWithORCID implements AuthorizationFeature {
return new String[] { ItemRest.CATEGORY + "." + ItemRest.NAME }; return new String[] { ItemRest.CATEGORY + "." + ItemRest.NAME };
} }
private boolean isOrcidSynchronizationEnabled() {
return configurationService.getBooleanProperty("orcid.synchronization-enabled", true);
}
/** /**
* This method returns true if the given eperson is not null and if the given * This method returns true if the given eperson is not null and if the given
* item has the metadata field dspace.object.owner with an authority equals to * item has the metadata field dspace.object.owner with an authority equals to

View File

@@ -0,0 +1,43 @@
/**
* 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.converter;
import org.dspace.app.rest.model.OrcidHistoryRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.orcid.OrcidHistory;
import org.springframework.stereotype.Component;
/**
* This is the converter from/to the OrcidHistory in the DSpace API data model and
* the REST data model.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@Component
public class OrcidHistoryRestConverter implements DSpaceConverter<OrcidHistory, OrcidHistoryRest> {
@Override
public OrcidHistoryRest convert(OrcidHistory modelObject, Projection projection) {
OrcidHistoryRest rest = new OrcidHistoryRest();
rest.setId(modelObject.getID());
rest.setProfileItemId(modelObject.getProfileItem().getID());
rest.setEntityId(modelObject.getEntity() != null ? modelObject.getEntity().getID() : null);
rest.setResponseMessage(modelObject.getResponseMessage());
rest.setStatus(modelObject.getStatus());
rest.setTimestamp(modelObject.getTimestamp());
rest.setProjection(projection);
rest.setPutCode(modelObject.getPutCode());
return rest;
}
@Override
public Class<OrcidHistory> getModelClass() {
return OrcidHistory.class;
}
}

View File

@@ -0,0 +1,47 @@
/**
* 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.converter;
import org.dspace.app.rest.model.OrcidQueueRest;
import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Item;
import org.dspace.orcid.OrcidQueue;
import org.springframework.stereotype.Component;
/**
* This is the converter from/to the OrcidQueue in the DSpace API data model and
* the REST data model.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*/
@Component
public class OrcidQueueRestConverter implements DSpaceConverter<OrcidQueue, OrcidQueueRest> {
@Override
public OrcidQueueRest convert(OrcidQueue orcidQueue, Projection projection) {
OrcidQueueRest rest = new OrcidQueueRest();
Item entity = orcidQueue.getEntity();
rest.setEntityId(entity != null ? entity.getID() : null);
rest.setDescription(orcidQueue.getDescription());
rest.setRecordType(orcidQueue.getRecordType());
rest.setId(orcidQueue.getID());
rest.setProfileItemId(orcidQueue.getProfileItem().getID());
rest.setOperation(orcidQueue.getOperation() != null ? orcidQueue.getOperation().name() : null);
rest.setProjection(projection);
return rest;
}
@Override
public Class<OrcidQueue> getModelClass() {
return OrcidQueue.class;
}
}

View File

@@ -7,22 +7,22 @@
*/ */
package org.dspace.app.rest.converter; package org.dspace.app.rest.converter;
import static org.dspace.app.orcid.model.OrcidEntityType.FUNDING; import static org.dspace.orcid.model.OrcidEntityType.FUNDING;
import static org.dspace.app.orcid.model.OrcidEntityType.PUBLICATION; import static org.dspace.orcid.model.OrcidEntityType.PUBLICATION;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; 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;
import org.dspace.app.rest.model.ResearcherProfileRest.OrcidSynchronizationRest; import org.dspace.app.rest.model.ResearcherProfileRest.OrcidSynchronizationRest;
import org.dspace.app.rest.projection.Projection; import org.dspace.app.rest.projection.Projection;
import org.dspace.content.Item; import org.dspace.content.Item;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.orcid.service.OrcidSynchronizationService;
import org.dspace.profile.OrcidEntitySyncPreference;
import org.dspace.profile.OrcidProfileSyncPreference;
import org.dspace.profile.OrcidSynchronizationMode;
import org.dspace.profile.ResearcherProfile;
import org.dspace.web.ContextUtil; import org.dspace.web.ContextUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;

View File

@@ -24,6 +24,7 @@ import org.dspace.app.exception.ResourceAlreadyExistsException;
import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.ContextUtil;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.orcid.exception.OrcidValidationException;
import org.dspace.services.ConfigurationService; import org.dspace.services.ConfigurationService;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
@@ -134,6 +135,18 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
HttpStatus.UNPROCESSABLE_ENTITY.value()); HttpStatus.UNPROCESSABLE_ENTITY.value());
} }
/**
* Handle the {@link OrcidValidationException} returning the exception message
* in the response, that always contains only the validation error codes (usable
* for example to show specific messages to users). No other details are present
* in the exception message.
*/
@ExceptionHandler({ OrcidValidationException.class })
protected void handleOrcidValidationException(HttpServletRequest request, HttpServletResponse response,
OrcidValidationException ex) throws IOException {
sendErrorResponse(request, response, ex, ex.getMessage(), HttpStatus.UNPROCESSABLE_ENTITY.value());
}
/** /**
* Add user-friendly error messages to the response body for selected errors. * Add user-friendly error messages to the response body for selected errors.
* Since the error messages will be exposed to the API user, the * Since the error messages will be exposed to the API user, the

View File

@@ -0,0 +1,64 @@
/**
* 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.link;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import java.util.LinkedList;
import org.atteo.evo.inflector.English;
import org.dspace.app.rest.RestResourceController;
import org.dspace.app.rest.model.ItemRest;
import org.dspace.app.rest.model.OrcidQueueRest;
import org.dspace.app.rest.model.hateoas.OrcidQueueResource;
import org.springframework.data.domain.Pageable;
import org.springframework.hateoas.Link;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
/**
* This class' purpose is to provide a factory to add links to the OrcidQueueResource.
* The addLinks factory will be called from the HalLinkService class addLinks method.
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@Component
public class OrcidQueueHalLinkFactory extends HalLinkFactory<OrcidQueueResource, RestResourceController> {
@Override
protected void addLinks(OrcidQueueResource halResource, Pageable pageable, LinkedList<Link> list)
throws Exception {
OrcidQueueRest orcidQueueRest = halResource.getContent();
if (orcidQueueRest.getProfileItemId() != null) {
UriComponentsBuilder uriComponentsBuilder = linkTo(getMethodOn(ItemRest.CATEGORY, ItemRest.NAME)
.findRel(null, null, ItemRest.CATEGORY, English.plural(ItemRest.NAME),
orcidQueueRest.getProfileItemId(), "", null, null)).toUriComponentsBuilder();
String uribuilder = uriComponentsBuilder.build().toString();
list.add(buildLink("profileItem", uribuilder.substring(0, uribuilder.lastIndexOf("/"))));
}
if (orcidQueueRest.getEntityId() != null) {
UriComponentsBuilder uriComponentsBuilder = linkTo(getMethodOn(ItemRest.CATEGORY, ItemRest.NAME)
.findRel(null, null, ItemRest.CATEGORY, English.plural(ItemRest.NAME),
orcidQueueRest.getEntityId(), "", null, null)).toUriComponentsBuilder();
String uribuilder = uriComponentsBuilder.build().toString();
list.add(buildLink("entity", uribuilder.substring(0, uribuilder.lastIndexOf("/"))));
}
}
@Override
protected Class<RestResourceController> getControllerClass() {
return RestResourceController.class;
}
@Override
protected Class<OrcidQueueResource> getResourceClass() {
return OrcidQueueResource.class;
}
}

View File

@@ -18,7 +18,6 @@ import java.util.stream.Collectors;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.dspace.app.profile.service.ResearcherProfileService;
import org.dspace.app.rest.login.PostLoggedInAction; import org.dspace.app.rest.login.PostLoggedInAction;
import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item; import org.dspace.content.Item;
@@ -27,6 +26,7 @@ import org.dspace.content.service.ItemService;
import org.dspace.core.Context; import org.dspace.core.Context;
import org.dspace.eperson.EPerson; import org.dspace.eperson.EPerson;
import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.EPersonService;
import org.dspace.profile.service.ResearcherProfileService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;

View File

@@ -0,0 +1,107 @@
/**
* 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.model;
import java.util.Date;
import java.util.UUID;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.dspace.app.rest.RestResourceController;
/**
* The OrcidHistory REST Resource
*
* @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it)
*/
@LinkRest
public class OrcidHistoryRest extends BaseObjectRest<Integer> {
private static final long serialVersionUID = 1L;
public static final String CATEGORY = RestModel.EPERSON;
public static final String NAME = "orcidhistory";
private UUID profileItemId;
private UUID entityId;
private Integer status;
private String putCode;
private Date timestamp;
private String responseMessage;
public OrcidHistoryRest(){}
@Override
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public String getType() {
return NAME;
}
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Class<RestResourceController> getController() {
return RestResourceController.class;
}
public UUID getProfileItemId() {
return profileItemId;
}
public void setProfileItemId(UUID profileItemId) {
this.profileItemId = profileItemId;
}
public UUID getEntityId() {
return entityId;
}
public void setEntityId(UUID entityId) {
this.entityId = entityId;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getPutCode() {
return putCode;
}
public void setPutCode(String putCode) {
this.putCode = putCode;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public String getResponseMessage() {
return responseMessage;
}
public void setResponseMessage(String responseMessage) {
this.responseMessage = responseMessage;
}
}

View File

@@ -0,0 +1,88 @@
/**
* 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.model;
import java.util.UUID;
import org.dspace.app.rest.RestResourceController;
@LinkRest
public class OrcidQueueRest extends BaseObjectRest<Integer> {
private static final long serialVersionUID = 1L;
public static final String CATEGORY = RestModel.EPERSON;
public static final String NAME = "orcidqueue";
public static final String PLURAL_NAME = "orcidqueues";
private UUID profileItemId;
private UUID entityId;
private String description;
private String recordType;
private String operation;
@Override
public String getType() {
return NAME;
}
@Override
public String getCategory() {
return CATEGORY;
}
@Override
public Class<RestResourceController> getController() {
return RestResourceController.class;
}
public UUID getProfileItemId() {
return profileItemId;
}
public void setProfileItemId(UUID profileItemId) {
this.profileItemId = profileItemId;
}
public UUID getEntityId() {
return entityId;
}
public void setEntityId(UUID entityId) {
this.entityId = entityId;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getRecordType() {
return recordType;
}
public void setRecordType(String recordType) {
this.recordType = recordType;
}
public String getOperation() {
return operation;
}
public void setOperation(String operation) {
this.operation = operation;
}
}

Some files were not shown because too many files have changed in this diff Show More