Merge pull request #8184 from 4Science/CST-5249

Add a correction service to dspace to enhance the data quality (was Enrich local data via the OpenAIRE Graph)
This commit is contained in:
Tim Donohue
2023-12-18 13:51:24 -06:00
committed by GitHub
120 changed files with 7701 additions and 196 deletions

View File

@@ -1021,6 +1021,61 @@ public class CollectionServiceImpl extends DSpaceObjectServiceImpl<Collection> i
return resp;
}
@Override
public Collection retrieveCollectionWithSubmitByEntityType(Context context, Item item,
String entityType) throws SQLException {
Collection ownCollection = item.getOwningCollection();
return retrieveWithSubmitCollectionByEntityType(context, ownCollection.getCommunities(), entityType);
}
private Collection retrieveWithSubmitCollectionByEntityType(Context context, List<Community> communities,
String entityType) {
for (Community community : communities) {
Collection collection = retrieveCollectionWithSubmitByCommunityAndEntityType(context, community,
entityType);
if (collection != null) {
return collection;
}
}
for (Community community : communities) {
List<Community> parentCommunities = community.getParentCommunities();
Collection collection = retrieveWithSubmitCollectionByEntityType(context, parentCommunities, entityType);
if (collection != null) {
return collection;
}
}
return retrieveCollectionWithSubmitByCommunityAndEntityType(context, null, entityType);
}
@Override
public Collection retrieveCollectionWithSubmitByCommunityAndEntityType(Context context, Community community,
String entityType) {
context.turnOffAuthorisationSystem();
List<Collection> collections;
try {
collections = findCollectionsWithSubmit(null, context, community, entityType, 0, 1);
} catch (SQLException | SearchServiceException e) {
throw new RuntimeException(e);
}
context.restoreAuthSystemState();
if (collections != null && collections.size() > 0) {
return collections.get(0);
}
if (community != null) {
for (Community subCommunity : community.getSubcommunities()) {
Collection collection = retrieveCollectionWithSubmitByCommunityAndEntityType(context,
subCommunity, entityType);
if (collection != null) {
return collection;
}
}
}
return null;
}
@Override
public List<Collection> findCollectionsWithSubmit(String q, Context context, Community community, String entityType,
int offset, int limit) throws SQLException, SearchServiceException {

View File

@@ -77,6 +77,7 @@ 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.qaevent.dao.QAEventsDAO;
import org.dspace.services.ConfigurationService;
import org.dspace.versioning.service.VersioningService;
import org.dspace.workflow.WorkflowItemService;
@@ -170,6 +171,9 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
@Autowired(required = true)
protected SubscribeService subscribeService;
@Autowired
private QAEventsDAO qaEventsDao;
protected ItemServiceImpl() {
super();
}
@@ -819,6 +823,11 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl<Item> implements It
orcidToken.setProfileItem(null);
}
List<QAEventProcessed> qaEvents = qaEventsDao.findByItem(context, item);
for (QAEventProcessed qaEvent : qaEvents) {
qaEventsDao.delete(context, qaEvent);
}
//Only clear collections after we have removed everything else from the item
item.clearCollections();
item.setOwningCollection(null);

View File

@@ -0,0 +1,213 @@
/**
* 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.content;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.dspace.qaevent.service.dto.OpenaireMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
import org.dspace.util.RawJsonDeserializer;
/**
* This class represent the Quality Assurance broker data as loaded in our solr
* qaevent core
*
*/
public class QAEvent {
public static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e',
'f' };
public static final String ACCEPTED = "accepted";
public static final String REJECTED = "rejected";
public static final String DISCARDED = "discarded";
public static final String OPENAIRE_SOURCE = "openaire";
private String source;
private String eventId;
/**
* contains the targeted dspace object,
* ie: oai:www.openstarts.units.it:123456789/1120 contains the handle
* of the DSpace pbject in its final part 123456789/1120
* */
private String originalId;
/**
* evaluated with the targeted dspace object id
*
* */
private String target;
private String related;
private String title;
private String topic;
private double trust;
@JsonDeserialize(using = RawJsonDeserializer.class)
private String message;
private Date lastUpdate;
private String status = "PENDING";
public QAEvent() {
}
public QAEvent(String source, String originalId, String target, String title,
String topic, double trust, String message, Date lastUpdate) {
super();
this.source = source;
this.originalId = originalId;
this.target = target;
this.title = title;
this.topic = topic;
this.trust = trust;
this.message = message;
this.lastUpdate = lastUpdate;
try {
computedEventId();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
public String getOriginalId() {
return originalId;
}
public void setOriginalId(String originalId) {
this.originalId = originalId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getTopic() {
return topic;
}
public void setTopic(String topic) {
this.topic = topic;
}
public double getTrust() {
return trust;
}
public void setTrust(double trust) {
this.trust = trust;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getEventId() {
if (eventId == null) {
try {
computedEventId();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
return eventId;
}
public void setEventId(String eventId) {
this.eventId = eventId;
}
public String getTarget() {
return target;
}
public void setTarget(String target) {
this.target = target;
}
public Date getLastUpdate() {
return lastUpdate;
}
public void setLastUpdate(Date lastUpdate) {
this.lastUpdate = lastUpdate;
}
public void setRelated(String related) {
this.related = related;
}
public String getRelated() {
return related;
}
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
public String getSource() {
return source != null ? source : OPENAIRE_SOURCE;
}
public void setSource(String source) {
this.source = source;
}
/*
* DTO constructed via Jackson use empty constructor. In this case, the eventId
* must be compute on the get method. This method create a signature based on
* the event fields and store it in the eventid attribute.
*/
private void computedEventId() throws NoSuchAlgorithmException, UnsupportedEncodingException {
MessageDigest digester = MessageDigest.getInstance("MD5");
String dataToString = "source=" + source + ",originalId=" + originalId + ", title=" + title + ", topic="
+ topic + ", trust=" + trust + ", message=" + message;
digester.update(dataToString.getBytes("UTF-8"));
byte[] signature = digester.digest();
char[] arr = new char[signature.length << 1];
for (int i = 0; i < signature.length; i++) {
int b = signature[i];
int idx = i << 1;
arr[idx] = HEX_DIGITS[(b >> 4) & 0xf];
arr[idx + 1] = HEX_DIGITS[b & 0xf];
}
eventId = new String(arr);
}
public Class<? extends QAMessageDTO> getMessageDtoClass() {
switch (getSource()) {
case OPENAIRE_SOURCE:
return OpenaireMessageDTO.class;
default:
throw new IllegalArgumentException("Unknown event's source: " + getSource());
}
}
}

View File

@@ -0,0 +1,82 @@
/**
* 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.content;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.dspace.eperson.EPerson;
/**
* This class represent the stored information about processed notification
* broker events
*
*/
@Entity
@Table(name = "qaevent_processed")
public class QAEventProcessed implements Serializable {
private static final long serialVersionUID = 3427340199132007814L;
@Id
@Column(name = "qaevent_id")
private String eventId;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "qaevent_timestamp")
private Date eventTimestamp;
@JoinColumn(name = "eperson_uuid")
@ManyToOne
private EPerson eperson;
@JoinColumn(name = "item_uuid")
@ManyToOne
private Item item;
public String getEventId() {
return eventId;
}
public void setEventId(String eventId) {
this.eventId = eventId;
}
public Date getEventTimestamp() {
return eventTimestamp;
}
public void setEventTimestamp(Date eventTimestamp) {
this.eventTimestamp = eventTimestamp;
}
public EPerson getEperson() {
return eperson;
}
public void setEperson(EPerson eperson) {
this.eperson = eperson;
}
public Item getItem() {
return item;
}
public void setItem(Item item) {
this.item = item;
}
}

View File

@@ -417,6 +417,34 @@ public interface CollectionService
public List<Collection> findCollectionsWithSubmit(String q, Context context, Community community,
int offset, int limit) throws SQLException, SearchServiceException;
/**
* Retrieve the first collection in the community or its descending that support
* the provided entityType
*
* @param context the DSpace context
* @param community the root from where the search start
* @param entityType the requested entity type
* @return the first collection in the community or its descending
* that support the provided entityType
*/
public Collection retrieveCollectionWithSubmitByCommunityAndEntityType(Context context, Community community,
String entityType);
/**
* Retrieve the close collection to the item for which the current user has
* 'submit' privileges that support the provided entityType. Close mean the
* collection that can be reach with the minimum steps starting from the item
* (owningCollection, brothers collections, etc)
*
* @param context the DSpace context
* @param item the item from where the search start
* @param entityType the requested entity type
* @return the first collection in the community or its descending
* that support the provided entityType
*/
public Collection retrieveCollectionWithSubmitByEntityType(Context context, Item item, String entityType)
throws SQLException;
/**
* Counts the number of Collection for which the current user has 'submit' privileges.
* NOTE: for better performance, this method retrieves its results from an index (cache)

View File

@@ -33,6 +33,7 @@ import org.dspace.content.DSpaceObjectServiceImpl;
import org.dspace.content.Item;
import org.dspace.content.MetadataField;
import org.dspace.content.MetadataValue;
import org.dspace.content.QAEventProcessed;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.factory.ContentServiceFactory;
import org.dspace.content.service.ItemService;
@@ -47,6 +48,7 @@ import org.dspace.eperson.service.GroupService;
import org.dspace.eperson.service.SubscribeService;
import org.dspace.event.Event;
import org.dspace.orcid.service.OrcidTokenService;
import org.dspace.qaevent.dao.QAEventsDAO;
import org.dspace.services.ConfigurationService;
import org.dspace.util.UUIDUtils;
import org.dspace.versioning.Version;
@@ -106,6 +108,8 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl<EPerson> impleme
protected ConfigurationService configurationService;
@Autowired
protected OrcidTokenService orcidTokenService;
@Autowired
protected QAEventsDAO qaEventsDao;
protected EPersonServiceImpl() {
super();
@@ -487,6 +491,11 @@ public class EPersonServiceImpl extends DSpaceObjectServiceImpl<EPerson> impleme
// Remove any subscriptions
subscribeService.deleteByEPerson(context, ePerson);
List<QAEventProcessed> qaEvents = qaEventsDao.findByEPerson(context, ePerson);
for (QAEventProcessed qaEvent : qaEvents) {
qaEventsDao.delete(context, qaEvent);
}
// Remove ourself
ePersonDAO.delete(context, ePerson);

View File

@@ -40,20 +40,20 @@ import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
/**
* based on OrcidRestConnector it's a rest connector for OpenAIRE API providing
* based on OrcidRestConnector it's a rest connector for Openaire API providing
* ways to perform searches and token grabbing
*
* @author paulo-graca
*
*/
public class OpenAIRERestConnector {
public class OpenaireRestConnector {
/**
* log4j logger
*/
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(OpenAIRERestConnector.class);
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(OpenaireRestConnector.class);
/**
* OpenAIRE API Url
* Openaire API Url
* and can be configured with: openaire.api.url
*/
private String url = "https://api.openaire.eu";
@@ -65,30 +65,30 @@ public class OpenAIRERestConnector {
boolean tokenEnabled = false;
/**
* OpenAIRE Authorization and Authentication Token Service URL
* Openaire Authorization and Authentication Token Service URL
* and can be configured with: openaire.token.url
*/
private String tokenServiceUrl;
/**
* OpenAIRE clientId
* Openaire clientId
* and can be configured with: openaire.token.clientId
*/
private String clientId;
/**
* OpenAIRERest access token
* OpenaireRest access token
*/
private OpenAIRERestToken accessToken;
private OpenaireRestToken accessToken;
/**
* OpenAIRE clientSecret
* Openaire clientSecret
* and can be configured with: openaire.token.clientSecret
*/
private String clientSecret;
public OpenAIRERestConnector(String url) {
public OpenaireRestConnector(String url) {
this.url = url;
}
@@ -99,7 +99,7 @@ public class OpenAIRERestConnector {
*
* @throws IOException
*/
public OpenAIRERestToken grabNewAccessToken() throws IOException {
public OpenaireRestToken grabNewAccessToken() throws IOException {
if (StringUtils.isBlank(tokenServiceUrl) || StringUtils.isBlank(clientId)
|| StringUtils.isBlank(clientSecret)) {
@@ -145,13 +145,13 @@ public class OpenAIRERestConnector {
throw new IOException("Unable to grab the access token using provided service url, client id and secret");
}
return new OpenAIRERestToken(responseObject.get("access_token").toString(),
return new OpenaireRestToken(responseObject.get("access_token").toString(),
Long.valueOf(responseObject.get("expires_in").toString()));
}
/**
* Perform a GET request to the OpenAIRE API
* Perform a GET request to the Openaire API
*
* @param file
* @param accessToken
@@ -218,12 +218,12 @@ public class OpenAIRERestConnector {
}
/**
* Perform an OpenAIRE Project Search By Keywords
* Perform an Openaire Project Search By Keywords
*
* @param page
* @param size
* @param keywords
* @return OpenAIRE Response
* @return Openaire Response
*/
public Response searchProjectByKeywords(int page, int size, String... keywords) {
String path = "search/projects?keywords=" + String.join("+", keywords);
@@ -231,13 +231,13 @@ public class OpenAIRERestConnector {
}
/**
* Perform an OpenAIRE Project Search By ID and by Funder
* Perform an Openaire Project Search By ID and by Funder
*
* @param projectID
* @param projectFunder
* @param page
* @param size
* @return OpenAIRE Response
* @return Openaire Response
*/
public Response searchProjectByIDAndFunder(String projectID, String projectFunder, int page, int size) {
String path = "search/projects?grantID=" + projectID + "&funder=" + projectFunder;
@@ -245,12 +245,12 @@ public class OpenAIRERestConnector {
}
/**
* Perform an OpenAIRE Search request
* Perform an Openaire Search request
*
* @param path
* @param page
* @param size
* @return OpenAIRE Response
* @return Openaire Response
*/
public Response search(String path, int page, int size) {
String[] queryStringPagination = { "page=" + page, "size=" + size };

View File

@@ -8,13 +8,13 @@
package org.dspace.external;
/**
* OpenAIRE rest API token to be used when grabbing an accessToken.<br/>
* Openaire rest API token to be used when grabbing an accessToken.<br/>
* Based on https://develop.openaire.eu/basic.html
*
* @author paulo-graca
*
*/
public class OpenAIRERestToken {
public class OpenaireRestToken {
/**
* Stored access token
@@ -32,7 +32,7 @@ public class OpenAIRERestToken {
* @param accessToken
* @param expiresIn
*/
public OpenAIRERestToken(String accessToken, Long expiresIn) {
public OpenaireRestToken(String accessToken, Long expiresIn) {
this.accessToken = accessToken;
this.setExpirationDate(expiresIn);
}

View File

@@ -31,7 +31,7 @@ import eu.openaire.oaf.model.base.Project;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.Logger;
import org.dspace.content.dto.MetadataValueDTO;
import org.dspace.external.OpenAIRERestConnector;
import org.dspace.external.OpenaireRestConnector;
import org.dspace.external.model.ExternalDataObject;
import org.dspace.external.provider.AbstractExternalDataProvider;
import org.dspace.importer.external.metadatamapping.MetadataFieldConfig;
@@ -39,13 +39,13 @@ import org.springframework.beans.factory.annotation.Autowired;
/**
* This class is the implementation of the ExternalDataProvider interface that
* will deal with the OpenAIRE External Data lookup
* will deal with the Openaire External Data lookup
*
* @author paulo-graca
*/
public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
public class OpenaireFundingDataProvider extends AbstractExternalDataProvider {
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(OpenAIREFundingDataProvider.class);
private static Logger log = org.apache.logging.log4j.LogManager.getLogger(OpenaireFundingDataProvider.class);
/**
* GrantAgreement prefix
@@ -75,7 +75,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
/**
* Connector to handle token and requests
*/
protected OpenAIRERestConnector connector;
protected OpenaireRestConnector connector;
protected Map<String, MetadataFieldConfig> metadataFields;
@@ -93,7 +93,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
// characters that must be escaped for the <:entry-id>
String decodedId = new String(Base64.getDecoder().decode(id));
if (!isValidProjectURI(decodedId)) {
log.error("Invalid ID for OpenAIREFunding - " + id);
log.error("Invalid ID for OpenaireFunding - " + id);
return Optional.empty();
}
Response response = searchByProjectURI(decodedId);
@@ -101,7 +101,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
try {
if (response.getHeader() != null && Integer.parseInt(response.getHeader().getTotal()) > 0) {
Project project = response.getResults().getResult().get(0).getMetadata().getEntity().getProject();
ExternalDataObject externalDataObject = new OpenAIREFundingDataProvider
ExternalDataObject externalDataObject = new OpenaireFundingDataProvider
.ExternalDataObjectBuilder(project)
.setId(generateProjectURI(project))
.setSource(sourceIdentifier)
@@ -123,7 +123,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
limit = LIMIT_DEFAULT;
}
// OpenAIRE uses pages and first page starts with 1
// Openaire uses pages and first page starts with 1
int page = (start / limit) + 1;
// escaping query
@@ -148,7 +148,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
if (projects.size() > 0) {
return projects.stream()
.map(project -> new OpenAIREFundingDataProvider
.map(project -> new OpenaireFundingDataProvider
.ExternalDataObjectBuilder(project)
.setId(generateProjectURI(project))
.setSource(sourceIdentifier)
@@ -176,24 +176,24 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
* Generic setter for the sourceIdentifier
*
* @param sourceIdentifier The sourceIdentifier to be set on this
* OpenAIREFunderDataProvider
* OpenaireFunderDataProvider
*/
@Autowired(required = true)
public void setSourceIdentifier(String sourceIdentifier) {
this.sourceIdentifier = sourceIdentifier;
}
public OpenAIRERestConnector getConnector() {
public OpenaireRestConnector getConnector() {
return connector;
}
/**
* Generic setter for OpenAIRERestConnector
* Generic setter for OpenaireRestConnector
*
* @param connector
*/
@Autowired(required = true)
public void setConnector(OpenAIRERestConnector connector) {
public void setConnector(OpenaireRestConnector connector) {
this.connector = connector;
}
@@ -219,7 +219,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
}
/**
* This method returns an URI based on OpenAIRE 3.0 guidelines
* This method returns an URI based on Openaire 3.0 guidelines
* https://guidelines.openaire.eu/en/latest/literature/field_projectid.html that
* can be used as an ID if is there any missing part, that part it will be
* replaced by the character '+'
@@ -281,7 +281,7 @@ public class OpenAIREFundingDataProvider extends AbstractExternalDataProvider {
}
/**
* OpenAIRE Funding External Data Builder Class
* Openaire Funding External Data Builder Class
*
* @author pgraca
*/

View File

@@ -0,0 +1,50 @@
/**
* 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.qaevent;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.event.Consumer;
import org.dspace.event.Event;
import org.dspace.qaevent.service.QAEventService;
import org.dspace.utils.DSpace;
/**
* Consumer to delete qaevents from solr due to the target item deletion
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAEventsDeleteCascadeConsumer implements Consumer {
private QAEventService qaEventService;
@Override
public void initialize() throws Exception {
qaEventService = new DSpace().getSingletonService(QAEventService.class);
}
@Override
public void finish(Context context) throws Exception {
}
@Override
public void consume(Context context, Event event) throws Exception {
if (event.getEventType() == Event.DELETE) {
if (event.getSubjectType() == Constants.ITEM && event.getSubjectID() != null) {
qaEventService.deleteEventsByTargetId(event.getSubjectID());
}
}
}
public void end(Context context) throws Exception {
}
}

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.qaevent;
import java.util.Date;
/**
* This model class represent the source/provider of the QA events (as Openaire).
*
* @author Luca Giamminonni (luca.giamminonni at 4Science)
*
*/
public class QASource {
private String name;
private long totalEvents;
private Date lastEvent;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public long getTotalEvents() {
return totalEvents;
}
public void setTotalEvents(long totalEvents) {
this.totalEvents = totalEvents;
}
public Date getLastEvent() {
return lastEvent;
}
public void setLastEvent(Date lastEvent) {
this.lastEvent = lastEvent;
}
}

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.qaevent;
import java.util.Date;
/**
* This model class represent the quality assurance broker topic concept. A
* topic represents a type of event and is therefore used to group events.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QATopic {
private String key;
private long totalEvents;
private Date lastEvent;
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public long getTotalEvents() {
return totalEvents;
}
public void setTotalEvents(long totalEvents) {
this.totalEvents = totalEvents;
}
public Date getLastEvent() {
return lastEvent;
}
public void setLastEvent(Date lastEvent) {
this.lastEvent = lastEvent;
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.qaevent;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.qaevent.service.dto.QAMessageDTO;
/**
* Interface for classes that perform a correction on the given item.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public interface QualityAssuranceAction {
/**
* Perform a correction on the given item.
*
* @param context the DSpace context
* @param item the item to correct
* @param relatedItem the related item, if any
* @param message the message with the correction details
*/
public void applyCorrection(Context context, Item item, Item relatedItem, QAMessageDTO message);
}

View File

@@ -0,0 +1,180 @@
/**
* 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.qaevent.action;
import java.sql.SQLException;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Collection;
import org.dspace.content.EntityType;
import org.dspace.content.Item;
import org.dspace.content.RelationshipType;
import org.dspace.content.WorkspaceItem;
import org.dspace.content.service.CollectionService;
import org.dspace.content.service.EntityTypeService;
import org.dspace.content.service.InstallItemService;
import org.dspace.content.service.ItemService;
import org.dspace.content.service.RelationshipService;
import org.dspace.content.service.RelationshipTypeService;
import org.dspace.content.service.WorkspaceItemService;
import org.dspace.core.Context;
import org.dspace.qaevent.QualityAssuranceAction;
import org.dspace.qaevent.service.dto.OpenaireMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link QualityAssuranceAction} that handle the relationship between the
* item to correct and a related item.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAEntityOpenaireMetadataAction implements QualityAssuranceAction {
private String relation;
private String entityType;
private Map<String, String> entityMetadata;
@Autowired
private InstallItemService installItemService;
@Autowired
private ItemService itemService;
@Autowired
private EntityTypeService entityTypeService;
@Autowired
private RelationshipService relationshipService;
@Autowired
private RelationshipTypeService relationshipTypeService;
@Autowired
private WorkspaceItemService workspaceItemService;
@Autowired
private CollectionService collectionService;
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
public String getRelation() {
return relation;
}
public void setRelation(String relation) {
this.relation = relation;
}
public String[] splitMetadata(String metadata) {
String[] result = new String[3];
String[] split = metadata.split("\\.");
result[0] = split[0];
result[1] = split[1];
if (split.length == 3) {
result[2] = split[2];
}
return result;
}
public String getEntityType() {
return entityType;
}
public void setEntityType(String entityType) {
this.entityType = entityType;
}
public Map<String, String> getEntityMetadata() {
return entityMetadata;
}
public void setEntityMetadata(Map<String, String> entityMetadata) {
this.entityMetadata = entityMetadata;
}
@Override
public void applyCorrection(Context context, Item item, Item relatedItem, QAMessageDTO message) {
try {
if (relatedItem != null) {
link(context, item, relatedItem);
} else {
Collection collection = collectionService.retrieveCollectionWithSubmitByEntityType(context,
item, entityType);
if (collection == null) {
throw new IllegalStateException("No collection found by entity type: " + collection);
}
WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, true);
relatedItem = workspaceItem.getItem();
for (String key : entityMetadata.keySet()) {
String value = getValue(message, key);
if (StringUtils.isNotBlank(value)) {
String[] targetMetadata = splitMetadata(entityMetadata.get(key));
itemService.addMetadata(context, relatedItem, targetMetadata[0], targetMetadata[1],
targetMetadata[2], null, value);
}
}
installItemService.installItem(context, workspaceItem);
itemService.update(context, relatedItem);
link(context, item, relatedItem);
}
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e);
}
}
/**
* Create a new relationship between the two given item, based on the configured
* relation.
*/
private void link(Context context, Item item, Item relatedItem) throws SQLException, AuthorizeException {
EntityType project = entityTypeService.findByEntityType(context, entityType);
RelationshipType relType = relationshipTypeService.findByEntityType(context, project).stream()
.filter(r -> StringUtils.equals(r.getRightwardType(), relation)).findFirst()
.orElseThrow(() -> new IllegalStateException("No relationshipType named " + relation
+ " was found for the entity type " + entityType
+ ". A proper configuration is required to use the QAEntitiyMetadataAction."
+ " If you don't manage funding in your repository please skip this topic in"
+ " the qaevents.cfg"));
// Create the relationship
relationshipService.create(context, item, relatedItem, relType, -1, -1);
}
private String getValue(QAMessageDTO message, String key) {
if (!(message instanceof OpenaireMessageDTO)) {
return null;
}
OpenaireMessageDTO openaireMessage = (OpenaireMessageDTO) message;
if (StringUtils.equals(key, "acronym")) {
return openaireMessage.getAcronym();
} else if (StringUtils.equals(key, "code")) {
return openaireMessage.getCode();
} else if (StringUtils.equals(key, "funder")) {
return openaireMessage.getFunder();
} else if (StringUtils.equals(key, "fundingProgram")) {
return openaireMessage.getFundingProgram();
} else if (StringUtils.equals(key, "jurisdiction")) {
return openaireMessage.getJurisdiction();
} else if (StringUtils.equals(key, "openaireId")) {
return openaireMessage.getOpenaireId();
} else if (StringUtils.equals(key, "title")) {
return openaireMessage.getTitle();
}
return null;
}
}

View File

@@ -0,0 +1,86 @@
/**
* 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.qaevent.action;
import java.sql.SQLException;
import java.util.Map;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.qaevent.QualityAssuranceAction;
import org.dspace.qaevent.service.dto.OpenaireMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link QualityAssuranceAction} that add a specific metadata on the given
* item based on the OPENAIRE message type.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAOpenaireMetadataMapAction implements QualityAssuranceAction {
public static final String DEFAULT = "default";
private Map<String, String> types;
@Autowired
private ItemService itemService;
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
public Map<String, String> getTypes() {
return types;
}
public void setTypes(Map<String, String> types) {
this.types = types;
}
/**
* Apply the correction on one metadata field of the given item based on the
* openaire message type.
*/
@Override
public void applyCorrection(Context context, Item item, Item relatedItem, QAMessageDTO message) {
if (!(message instanceof OpenaireMessageDTO)) {
throw new IllegalArgumentException("Unsupported message type: " + message.getClass());
}
OpenaireMessageDTO openaireMessage = (OpenaireMessageDTO) message;
try {
String targetMetadata = types.get(openaireMessage.getType());
if (targetMetadata == null) {
targetMetadata = types.get(DEFAULT);
}
String[] metadata = splitMetadata(targetMetadata);
itemService.addMetadata(context, item, metadata[0], metadata[1], metadata[2], null,
openaireMessage.getValue());
itemService.update(context, item);
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e);
}
}
public String[] splitMetadata(String metadata) {
String[] result = new String[3];
String[] split = metadata.split("\\.");
result[0] = split[0];
result[1] = split[1];
if (split.length == 3) {
result[2] = split[2];
}
return result;
}
}

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.qaevent.action;
import java.sql.SQLException;
import org.dspace.authorize.AuthorizeException;
import org.dspace.content.Item;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.qaevent.QualityAssuranceAction;
import org.dspace.qaevent.service.dto.OpenaireMessageDTO;
import org.dspace.qaevent.service.dto.QAMessageDTO;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link QualityAssuranceAction} that add a simple metadata to the given
* item.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAOpenaireSimpleMetadataAction implements QualityAssuranceAction {
private String metadata;
private String metadataSchema;
private String metadataElement;
private String metadataQualifier;
@Autowired
private ItemService itemService;
public void setItemService(ItemService itemService) {
this.itemService = itemService;
}
public String getMetadata() {
return metadata;
}
public void setMetadata(String metadata) {
this.metadata = metadata;
String[] split = metadata.split("\\.");
this.metadataSchema = split[0];
this.metadataElement = split[1];
if (split.length == 3) {
this.metadataQualifier = split[2];
}
}
@Override
public void applyCorrection(Context context, Item item, Item relatedItem, QAMessageDTO message) {
try {
itemService.addMetadata(context, item, metadataSchema, metadataElement, metadataQualifier, null,
((OpenaireMessageDTO) message).getAbstracts());
itemService.update(context, item);
} catch (SQLException | AuthorizeException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,92 @@
/**
* 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.qaevent.dao;
import java.sql.SQLException;
import java.util.List;
import org.dspace.content.Item;
import org.dspace.content.QAEventProcessed;
import org.dspace.core.Context;
import org.dspace.core.GenericDAO;
import org.dspace.eperson.EPerson;
/**
* DAO that handle processed QA Events.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public interface QAEventsDAO extends GenericDAO<QAEventProcessed> {
/**
* Returns all the stored QAEventProcessed entities.
*
* @param context the DSpace context
* @return the found entities
* @throws SQLException if an SQL error occurs
*/
public List<QAEventProcessed> findAll(Context context) throws SQLException;
/**
* Returns the stored QAEventProcessed entities by item.
*
* @param context the DSpace context
* @param item the item to search for
* @return the found entities
* @throws SQLException if an SQL error occurs
*/
public List<QAEventProcessed> findByItem(Context context, Item item) throws SQLException;
/**
* Returns the stored QAEventProcessed entities by eperson.
*
* @param context the DSpace context
* @param ePerson the ePerson to search for
* @return the found entities
* @throws SQLException if an SQL error occurs
*/
public List<QAEventProcessed> findByEPerson(Context context, EPerson ePerson) throws SQLException;
/**
* Search a page of quality assurance broker events by notification ID.
*
* @param context the DSpace context
* @param eventId the event id
* @param start the start index
* @param size the size to be applied
* @return the processed events
* @throws SQLException if an SQL error occurs
*/
public List<QAEventProcessed> searchByEventId(Context context, String eventId, Integer start, Integer size)
throws SQLException;
/**
* Check if an event with the given checksum is already stored.
*
* @param context the DSpace context
* @param checksum the checksum to search for
* @return true if the given checksum is related to an already
* stored event, false otherwise
* @throws SQLException if an SQL error occurs
*/
public boolean isEventStored(Context context, String checksum) throws SQLException;
/**
* Store an event related to the given checksum.
*
* @param context the DSpace context
* @param checksum the checksum of the event to be store
* @param eperson the eperson who handle the event
* @param item the item related to the event
* @return true if the creation is completed with success, false
* otherwise
*/
boolean storeEvent(Context context, String checksum, EPerson eperson, Item item);
}

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.qaevent.dao.impl;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import javax.persistence.Query;
import org.dspace.content.Item;
import org.dspace.content.QAEventProcessed;
import org.dspace.core.AbstractHibernateDAO;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.qaevent.dao.QAEventsDAO;
/**
* Implementation of {@link QAEventsDAO} that store processed events using an
* SQL DBMS.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAEventsDAOImpl extends AbstractHibernateDAO<QAEventProcessed> implements QAEventsDAO {
@Override
public List<QAEventProcessed> findAll(Context context) throws SQLException {
return findAll(context, QAEventProcessed.class);
}
@Override
public boolean storeEvent(Context context, String checksum, EPerson eperson, Item item) {
QAEventProcessed qaEvent = new QAEventProcessed();
qaEvent.setEperson(eperson);
qaEvent.setEventId(checksum);
qaEvent.setItem(item);
qaEvent.setEventTimestamp(new Date());
try {
create(context, qaEvent);
return true;
} catch (SQLException e) {
return false;
}
}
@Override
public boolean isEventStored(Context context, String checksum) throws SQLException {
Query query = createQuery(context,
"SELECT count(eventId) FROM QAEventProcessed qaevent WHERE qaevent.eventId = :event_id ");
query.setParameter("event_id", checksum);
return count(query) != 0;
}
@Override
public List<QAEventProcessed> searchByEventId(Context context, String eventId, Integer start, Integer size)
throws SQLException {
Query query = createQuery(context,
"SELECT * FROM QAEventProcessed qaevent WHERE qaevent.qaevent_id = :event_id ");
query.setFirstResult(start);
query.setMaxResults(size);
query.setParameter("event_id", eventId);
return findMany(context, query);
}
@Override
public List<QAEventProcessed> findByItem(Context context, Item item) throws SQLException {
Query query = createQuery(context, ""
+ " SELECT qaevent "
+ " FROM QAEventProcessed qaevent "
+ " WHERE qaevent.item = :item ");
query.setParameter("item", item);
return findMany(context, query);
}
@Override
public List<QAEventProcessed> findByEPerson(Context context, EPerson ePerson) throws SQLException {
Query query = createQuery(context, ""
+ " SELECT qaevent "
+ " FROM QAEventProcessed qaevent "
+ " WHERE qaevent.eperson = :eperson ");
query.setParameter("eperson", ePerson);
return findMany(context, query);
}
}

View File

@@ -0,0 +1,314 @@
/**
* 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.qaevent.script;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.substringAfter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import eu.dnetlib.broker.BrokerClient;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.dspace.content.QAEvent;
import org.dspace.core.Context;
import org.dspace.eperson.EPerson;
import org.dspace.eperson.factory.EPersonServiceFactory;
import org.dspace.qaevent.service.OpenaireClientFactory;
import org.dspace.qaevent.service.QAEventService;
import org.dspace.scripts.DSpaceRunnable;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.dspace.utils.DSpace;
/**
* Implementation of {@link DSpaceRunnable} to perfom a QAEvents import from a
* json file. The JSON file contains an array of JSON Events, where each event
* has the following structure. The message attribute follows the structure
* documented at
* @see <a href="https://graph.openaire.eu/docs/category/entities" target="_blank"> see </a>
*
* <code> <br/>
* { <br/>
* "originalId": "oai:www.openstarts.units.it:10077/21838",<br/>
* "title": "Egypt, crossroad of translations and literary interweavings", <br/>
* "topic": "ENRICH/MORE/PROJECT", <br/>
* "trust": 1.0, <br/>
* "message": { <br/>
* "projects[0].acronym": "PAThs", <br/>
* "projects[0].code": "687567", <br/>
* "projects[0].funder": "EC",<br/>
* "projects[0].fundingProgram": "H2020", <br/>
* "projects[0].jurisdiction": "EU",<br/>
* "projects[0].openaireId": "40|corda__h2020::6e32f5eb912688f2424c68b851483ea4", <br/>
* "projects[0].title": "Tracking Papyrus and Parchment Paths" <br/>
* } <br/>
* }
* </code>
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
* @author Luca Giamminonni (luca.giamminonni at 4Science.it)
*
*/
public class OpenaireEventsImport
extends DSpaceRunnable<OpenaireEventsImportScriptConfiguration<OpenaireEventsImport>> {
private QAEventService qaEventService;
private String[] topicsToImport;
private ConfigurationService configurationService;
private BrokerClient brokerClient;
private ObjectMapper jsonMapper;
private URL openaireBrokerURL;
private String fileLocation;
private String email;
private Context context;
@Override
@SuppressWarnings({ "rawtypes" })
public OpenaireEventsImportScriptConfiguration getScriptConfiguration() {
OpenaireEventsImportScriptConfiguration configuration = new DSpace().getServiceManager()
.getServiceByName("import-openaire-events", OpenaireEventsImportScriptConfiguration.class);
return configuration;
}
@Override
public void setup() throws ParseException {
jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
qaEventService = new DSpace().getSingletonService(QAEventService.class);
configurationService = DSpaceServicesFactory.getInstance().getConfigurationService();
brokerClient = OpenaireClientFactory.getInstance().getBrokerClient();
topicsToImport = configurationService.getArrayProperty("qaevents.openaire.import.topic");
openaireBrokerURL = getOpenaireBrokerUri();
fileLocation = commandLine.getOptionValue("f");
email = commandLine.getOptionValue("e");
}
@Override
public void internalRun() throws Exception {
if (StringUtils.isAllBlank(fileLocation, email)) {
throw new IllegalArgumentException("One parameter between the location of the file and the email "
+ "must be entered to proceed with the import.");
}
if (StringUtils.isNoneBlank(fileLocation, email)) {
throw new IllegalArgumentException("Only one parameter between the location of the file and the email "
+ "must be entered to proceed with the import.");
}
context = new Context();
assignCurrentUserInContext();
try {
importOpenaireEvents();
} catch (Exception ex) {
handler.logError("A not recoverable error occurs during OPENAIRE events import: " + getMessage(ex), ex);
throw ex;
}
}
/**
* Read the OPENAIRE events from the given JSON file or directly from the
* OPENAIRE broker and try to store them.
*/
private void importOpenaireEvents() throws Exception {
if (StringUtils.isNotBlank(fileLocation)) {
handler.logInfo("Trying to read the QA events from the provided file");
importOpenaireEventsFromFile();
} else {
handler.logInfo("Trying to read the QA events from the OPENAIRE broker");
importOpenaireEventsFromBroker();
}
}
/**
* Read the OPENAIRE events from the given file location and try to store them.
*/
private void importOpenaireEventsFromFile() throws Exception {
InputStream eventsFileInputStream = getQAEventsFileInputStream();
List<QAEvent> qaEvents = readOpenaireQAEventsFromJson(eventsFileInputStream);
handler.logInfo("Found " + qaEvents.size() + " events in the given file");
storeOpenaireQAEvents(qaEvents);
}
/**
* Import the OPENAIRE events from the Broker using the subscription related to
* the given email and try to store them.
*/
private void importOpenaireEventsFromBroker() {
List<String> subscriptionIds = listEmailSubscriptions();
handler.logInfo("Found " + subscriptionIds.size() + " subscriptions related to the given email");
for (String subscriptionId : subscriptionIds) {
List<QAEvent> events = readOpenaireQAEventsFromBroker(subscriptionId);
handler.logInfo("Found " + events.size() + " events from the subscription " + subscriptionId);
storeOpenaireQAEvents(events);
}
}
/**
* Obtain an InputStream from the runnable instance.
*/
private InputStream getQAEventsFileInputStream() throws Exception {
return handler.getFileStream(context, fileLocation)
.orElseThrow(() -> new IllegalArgumentException("Error reading file, the file couldn't be "
+ "found for filename: " + fileLocation));
}
/**
* Read all the QAEvent from the OPENAIRE Broker related to the subscription
* with the given id.
*/
private List<QAEvent> readOpenaireQAEventsFromBroker(String subscriptionId) {
try {
InputStream eventsInputStream = getEventsBySubscriptions(subscriptionId);
return readOpenaireQAEventsFromJson(eventsInputStream);
} catch (Exception ex) {
handler.logError("An error occurs downloading the events related to the subscription "
+ subscriptionId + ": " + getMessage(ex), ex);
}
return List.of();
}
/**
* Read all the QAEvent present in the given input stream.
*
* @return the QA events to be imported
*/
private List<QAEvent> readOpenaireQAEventsFromJson(InputStream inputStream) throws Exception {
return jsonMapper.readValue(inputStream, new TypeReference<List<QAEvent>>() {
});
}
/**
* Store the given QAEvents.
*
* @param events the event to be stored
*/
private void storeOpenaireQAEvents(List<QAEvent> events) {
for (QAEvent event : events) {
try {
storeOpenaireQAEvent(event);
} catch (RuntimeException e) {
handler.logWarning("An error occurs storing the event with id "
+ event.getEventId() + ": " + getMessage(e));
}
}
}
/**
* Store the given QAEvent, skipping it if it is not supported.
*
* @param event the event to be stored
*/
private void storeOpenaireQAEvent(QAEvent event) {
if (!StringUtils.equalsAny(event.getTopic(), topicsToImport)) {
handler.logWarning("Event for topic " + event.getTopic() + " is not allowed in the qaevents.cfg");
return;
}
event.setSource(QAEvent.OPENAIRE_SOURCE);
qaEventService.store(context, event);
}
/**
* Download the events related to the given subscription from the OPENAIRE broker.
*
* @param subscriptionId the subscription id
* @return an input stream from which to read the events in json format
*/
private InputStream getEventsBySubscriptions(String subscriptionId) throws Exception {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
brokerClient.downloadEvents(openaireBrokerURL, subscriptionId, outputStream);
return new ByteArrayInputStream(outputStream.toByteArray());
}
/**
* Takes all the subscription related to the given email from the OPENAIRE
* broker.
*/
private List<String> listEmailSubscriptions() {
try {
return brokerClient.listSubscriptions(openaireBrokerURL, email);
} catch (Exception ex) {
throw new IllegalArgumentException("An error occurs retriving the subscriptions "
+ "from the OPENAIRE broker: " + getMessage(ex), ex);
}
}
private URL getOpenaireBrokerUri() {
try {
return new URL(configurationService.getProperty("qaevents.openaire.broker-url", "http://api.openaire.eu/broker"));
} catch (MalformedURLException e) {
throw new IllegalStateException("The configured OPENAIRE broker URL is not valid.", e);
}
}
/**
* Get the root exception message from the given exception.
*/
private String getMessage(Exception ex) {
String message = ExceptionUtils.getRootCauseMessage(ex);
// Remove the Exception name from the message
return isNotBlank(message) ? substringAfter(message, ":").trim() : "";
}
private void assignCurrentUserInContext() throws SQLException {
UUID uuid = getEpersonIdentifier();
if (uuid != null) {
EPerson ePerson = EPersonServiceFactory.getInstance().getEPersonService().find(context, uuid);
context.setCurrentUser(ePerson);
}
}
}

View File

@@ -0,0 +1,42 @@
/**
* 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.qaevent.script;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.ParseException;
import org.dspace.utils.DSpace;
/**
* Extensions of {@link OpenaireEventsImport} to run the script on console.
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*
*/
public class OpenaireEventsImportCli extends OpenaireEventsImport {
@Override
@SuppressWarnings({ "rawtypes" })
public OpenaireEventsImportCliScriptConfiguration getScriptConfiguration() {
return new DSpace().getServiceManager()
.getServiceByName("import-openaire-events", OpenaireEventsImportCliScriptConfiguration.class);
}
@Override
public void setup() throws ParseException {
super.setup();
// in case of CLI we show the help prompt
if (commandLine.hasOption('h')) {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("Import Notification event json file", getScriptConfiguration().getOptions());
System.exit(0);
}
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.qaevent.script;
import org.apache.commons.cli.Options;
/**
* Extension of {@link OpenaireEventsImportScriptConfiguration} to run the script on
* console.
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*
*/
public class OpenaireEventsImportCliScriptConfiguration<T extends OpenaireEventsImport>
extends OpenaireEventsImportScriptConfiguration<T> {
@Override
public Options getOptions() {
Options options = super.getOptions();
options.addOption("h", "help", false, "help");
options.getOption("h").setType(boolean.class);
super.options = options;
return options;
}
}

View File

@@ -0,0 +1,72 @@
/**
* 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.qaevent.script;
import java.io.InputStream;
import org.apache.commons.cli.Options;
import org.dspace.scripts.configuration.ScriptConfiguration;
/**
* Extension of {@link ScriptConfiguration} to perfom a QAEvents import from
* file.
*
* @author Alessandro Martelli (alessandro.martelli at 4science.it)
*
*/
public class OpenaireEventsImportScriptConfiguration<T extends OpenaireEventsImport> extends ScriptConfiguration<T> {
/*
private AuthorizeService authorizeService;
*/
private Class<T> dspaceRunnableClass;
@Override
public Class<T> getDspaceRunnableClass() {
return dspaceRunnableClass;
}
/**
* Generic setter for the dspaceRunnableClass
* @param dspaceRunnableClass The dspaceRunnableClass to be set on this OpenaireEventsImportScriptConfiguration
*/
@Override
public void setDspaceRunnableClass(Class<T> dspaceRunnableClass) {
this.dspaceRunnableClass = 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 Options getOptions() {
if (options == null) {
Options options = new Options();
options.addOption("f", "file", true, "Import data from Openaire quality assurance broker JSON file."
+ " This parameter is mutually exclusive to the email parameter.");
options.getOption("f").setType(InputStream.class);
options.getOption("f").setRequired(false);
options.addOption("e", "email", true, "Email related to the subscriptions to import data from Openaire "
+ "broker. This parameter is mutually exclusive to the file parameter.");
options.getOption("e").setType(String.class);
options.getOption("e").setRequired(false);
super.options = options;
}
return options;
}
}

View File

@@ -0,0 +1,31 @@
/**
* 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.qaevent.service;
import eu.dnetlib.broker.BrokerClient;
import org.dspace.utils.DSpace;
/**
* Factory for the {@link BrokerClient}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface OpenaireClientFactory {
/**
* Returns an instance of the {@link BrokerClient}.
*
* @return the client instance
*/
public BrokerClient getBrokerClient();
public static OpenaireClientFactory getInstance() {
return new DSpace().getServiceManager().getServiceByName("openaireClientFactory", OpenaireClientFactory.class);
}
}

View File

@@ -0,0 +1,45 @@
/**
* 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.qaevent.service;
import org.dspace.content.QAEvent;
import org.dspace.core.Context;
/**
* Service that handle the actions that can be done related to an
* {@link QAEvent}.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public interface QAEventActionService {
/**
* Accept the given event.
*
* @param context the DSpace context
* @param qaevent the event to be accepted
*/
public void accept(Context context, QAEvent qaevent);
/**
* Discard the given event.
*
* @param context the DSpace context
* @param qaevent the event to be discarded
*/
public void discard(Context context, QAEvent qaevent);
/**
* Reject the given event.
*
* @param context the DSpace context
* @param qaevent the event to be rejected
*/
public void reject(Context context, QAEvent qaevent);
}

View File

@@ -0,0 +1,158 @@
/**
* 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.qaevent.service;
import java.util.List;
import java.util.UUID;
import org.dspace.content.QAEvent;
import org.dspace.core.Context;
import org.dspace.qaevent.QASource;
import org.dspace.qaevent.QATopic;
/**
* Service that handles {@link QAEvent}.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public interface QAEventService {
/**
* Find all the event's topics.
*
* @param offset the offset to apply
* @return the topics list
*/
public List<QATopic> findAllTopics(long offset, long count, String orderField, boolean ascending);
/**
* Find all the event's topics related to the given source.
*
* @param source the source to search for
* @param offset the offset to apply
* @param count the page size
* @return the topics list
*/
public List<QATopic> findAllTopicsBySource(String source, long offset, long count,
String orderField, boolean ascending);
/**
* Count all the event's topics.
*
* @return the count result
*/
public long countTopics();
/**
* Count all the event's topics related to the given source.
*
* @param source the source to search for
* @return the count result
*/
public long countTopicsBySource(String source);
/**
* Find all the events by topic.
*
* @param topic the topic to search for
* @param offset the offset to apply
* @param pageSize the page size
* @param orderField the field to order for
* @param ascending true if the order should be ascending, false otherwise
* @return the events
*/
public List<QAEvent> findEventsByTopicAndPage(String topic, long offset, int pageSize,
String orderField, boolean ascending);
/**
* Find all the events by topic.
*
* @param topic the topic to search for
* @return the events
*/
public List<QAEvent> findEventsByTopic(String topic);
/**
* Find all the events by topic.
*
* @param topic the topic to search for
* @return the events count
*/
public long countEventsByTopic(String topic);
/**
* Find an event by the given id.
*
* @param id the id of the event to search for
* @return the event
*/
public QAEvent findEventByEventId(String id);
/**
* Store the given event.
*
* @param context the DSpace context
* @param event the event to store
*/
public void store(Context context, QAEvent event);
/**
* Delete an event by the given id.
*
* @param id the id of the event to delete
*/
public void deleteEventByEventId(String id);
/**
* Delete events by the given target id.
*
* @param targetId the id of the target id
*/
public void deleteEventsByTargetId(UUID targetId);
/**
* Find a specific topid by the given id.
*
* @param topicId the topic id to search for
* @return the topic
*/
public QATopic findTopicByTopicId(String topicId);
/**
* Find a specific source by the given name.
*
* @param source the source name
* @return the source
*/
public QASource findSource(String source);
/**
* Find all the event's sources.
*
* @param offset the offset to apply
* @param pageSize the page size
* @return the sources list
*/
public List<QASource> findAllSources(long offset, int pageSize);
/**
* Count all the event's sources.
*
* @return the count result
*/
public long countSources();
/**
* Check if the given QA event supports a related item.
*
* @param qaevent the event to be verified
* @return true if the event supports a related item, false otherwise.
*/
public boolean isRelatedItemSupported(QAEvent qaevent);
}

View File

@@ -0,0 +1,173 @@
/**
* 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.qaevent.service.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Implementation of {@link QAMessageDTO} that model message coming from OPENAIRE.
* @see <a href="https://graph.openaire.eu/docs/category/entities" target="_blank"> see </a>
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OpenaireMessageDTO implements QAMessageDTO {
@JsonProperty("pids[0].value")
private String value;
@JsonProperty("pids[0].type")
private String type;
@JsonProperty("instances[0].hostedby")
private String instanceHostedBy;
@JsonProperty("instances[0].instancetype")
private String instanceInstanceType;
@JsonProperty("instances[0].license")
private String instanceLicense;
@JsonProperty("instances[0].url")
private String instanceUrl;
@JsonProperty("abstracts[0]")
private String abstracts;
@JsonProperty("projects[0].acronym")
private String acronym;
@JsonProperty("projects[0].code")
private String code;
@JsonProperty("projects[0].funder")
private String funder;
@JsonProperty("projects[0].fundingProgram")
private String fundingProgram;
@JsonProperty("projects[0].jurisdiction")
private String jurisdiction;
@JsonProperty("projects[0].openaireId")
private String openaireId;
@JsonProperty("projects[0].title")
private String title;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getInstanceHostedBy() {
return instanceHostedBy;
}
public void setInstanceHostedBy(String instanceHostedBy) {
this.instanceHostedBy = instanceHostedBy;
}
public String getInstanceInstanceType() {
return instanceInstanceType;
}
public void setInstanceInstanceType(String instanceInstanceType) {
this.instanceInstanceType = instanceInstanceType;
}
public String getInstanceLicense() {
return instanceLicense;
}
public void setInstanceLicense(String instanceLicense) {
this.instanceLicense = instanceLicense;
}
public String getInstanceUrl() {
return instanceUrl;
}
public void setInstanceUrl(String instanceUrl) {
this.instanceUrl = instanceUrl;
}
public String getAbstracts() {
return abstracts;
}
public void setAbstracts(String abstracts) {
this.abstracts = abstracts;
}
public String getAcronym() {
return acronym;
}
public void setAcronym(String acronym) {
this.acronym = acronym;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getFunder() {
return funder;
}
public void setFunder(String funder) {
this.funder = funder;
}
public String getFundingProgram() {
return fundingProgram;
}
public void setFundingProgram(String fundingProgram) {
this.fundingProgram = fundingProgram;
}
public String getJurisdiction() {
return jurisdiction;
}
public void setJurisdiction(String jurisdiction) {
this.jurisdiction = jurisdiction;
}
public String getOpenaireId() {
return openaireId;
}
public void setOpenaireId(String openaireId) {
this.openaireId = openaireId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}

View File

@@ -0,0 +1,21 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.qaevent.service.dto;
import org.dspace.content.QAEvent;
/**
* Interface for classes that contains the details related to a {@link QAEvent}.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public interface QAMessageDTO {
}

View File

@@ -0,0 +1,35 @@
/**
* 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.qaevent.service.impl;
import eu.dnetlib.broker.BrokerClient;
import org.dspace.qaevent.service.OpenaireClientFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link OpenaireClientFactory} that returns the instance of
* {@link BrokerClient} managed by the Spring context.
*
* @author Luca Giamminonni (luca.giamminonni at 4science.it)
*
*/
public class OpenaireClientFactoryImpl implements OpenaireClientFactory {
@Autowired
private BrokerClient brokerClient;
@Override
public BrokerClient getBrokerClient() {
return brokerClient;
}
public void setBrokerClient(BrokerClient brokerClient) {
this.brokerClient = brokerClient;
}
}

View File

@@ -0,0 +1,127 @@
/**
* 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.qaevent.service.impl;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Map;
import java.util.UUID;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.logging.log4j.Logger;
import org.dspace.content.Item;
import org.dspace.content.QAEvent;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.qaevent.QualityAssuranceAction;
import org.dspace.qaevent.service.QAEventActionService;
import org.dspace.qaevent.service.QAEventService;
import org.dspace.services.ConfigurationService;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link QAEventActionService}.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAEventActionServiceImpl implements QAEventActionService {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(QAEventActionServiceImpl.class);
private ObjectMapper jsonMapper;
@Autowired
private QAEventService qaEventService;
@Autowired
private ItemService itemService;
@Autowired
private ConfigurationService configurationService;
private Map<String, QualityAssuranceAction> topicsToActions;
public void setTopicsToActions(Map<String, QualityAssuranceAction> topicsToActions) {
this.topicsToActions = topicsToActions;
}
public Map<String, QualityAssuranceAction> getTopicsToActions() {
return topicsToActions;
}
public QAEventActionServiceImpl() {
jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public void accept(Context context, QAEvent qaevent) {
Item item = null;
Item related = null;
try {
item = itemService.find(context, UUID.fromString(qaevent.getTarget()));
if (qaevent.getRelated() != null) {
related = itemService.find(context, UUID.fromString(qaevent.getRelated()));
}
topicsToActions.get(qaevent.getTopic()).applyCorrection(context, item, related,
jsonMapper.readValue(qaevent.getMessage(), qaevent.getMessageDtoClass()));
qaEventService.deleteEventByEventId(qaevent.getEventId());
makeAcknowledgement(qaevent.getEventId(), qaevent.getSource(), QAEvent.ACCEPTED);
} catch (SQLException | JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public void discard(Context context, QAEvent qaevent) {
qaEventService.deleteEventByEventId(qaevent.getEventId());
makeAcknowledgement(qaevent.getEventId(), qaevent.getSource(), QAEvent.DISCARDED);
}
@Override
public void reject(Context context, QAEvent qaevent) {
qaEventService.deleteEventByEventId(qaevent.getEventId());
makeAcknowledgement(qaevent.getEventId(), qaevent.getSource(), QAEvent.REJECTED);
}
/**
* Make acknowledgement to the configured urls for the event status.
*/
private void makeAcknowledgement(String eventId, String source, String status) {
String[] ackwnoledgeCallbacks = configurationService
.getArrayProperty("qaevents." + source + ".acknowledge-url");
if (ackwnoledgeCallbacks != null) {
for (String ackwnoledgeCallback : ackwnoledgeCallbacks) {
if (StringUtils.isNotBlank(ackwnoledgeCallback)) {
ObjectNode node = jsonMapper.createObjectNode();
node.put("eventId", eventId);
node.put("status", status);
StringEntity requestEntity = new StringEntity(node.toString(), ContentType.APPLICATION_JSON);
CloseableHttpClient httpclient = HttpClients.createDefault();
HttpPost postMethod = new HttpPost(ackwnoledgeCallback);
postMethod.setEntity(requestEntity);
try {
httpclient.execute(postMethod);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
}
}
}
}
}

View File

@@ -0,0 +1,467 @@
/**
* 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.qaevent.service.impl;
import static java.util.Comparator.comparing;
import static org.apache.commons.lang3.StringUtils.endsWith;
import static org.dspace.content.QAEvent.OPENAIRE_SOURCE;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.SolrQuery.ORDER;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.FacetField.Count;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.common.SolrDocument;
import org.apache.solr.common.SolrDocumentList;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.params.FacetParams;
import org.dspace.content.Item;
import org.dspace.content.QAEvent;
import org.dspace.content.service.ItemService;
import org.dspace.core.Context;
import org.dspace.handle.service.HandleService;
import org.dspace.qaevent.QASource;
import org.dspace.qaevent.QATopic;
import org.dspace.qaevent.dao.QAEventsDAO;
import org.dspace.qaevent.dao.impl.QAEventsDAOImpl;
import org.dspace.qaevent.service.QAEventService;
import org.dspace.services.ConfigurationService;
import org.dspace.services.factory.DSpaceServicesFactory;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Implementation of {@link QAEventService} that use Solr to store events. When
* the user performs an action on the event (such as accepting the suggestion or
* rejecting it) then the event is removed from solr and saved in the database
* (see {@link QAEventsDAO}) so that it is no longer proposed.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class QAEventServiceImpl implements QAEventService {
@Autowired(required = true)
protected ConfigurationService configurationService;
@Autowired(required = true)
protected ItemService itemService;
@Autowired
private HandleService handleService;
@Autowired
private QAEventsDAOImpl qaEventsDao;
private ObjectMapper jsonMapper;
public QAEventServiceImpl() {
jsonMapper = new JsonMapper();
jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
/**
* Non-Static CommonsHttpSolrServer for processing indexing events.
*/
protected SolrClient solr = null;
public static final String SOURCE = "source";
public static final String ORIGINAL_ID = "original_id";
public static final String TITLE = "title";
public static final String TOPIC = "topic";
public static final String TRUST = "trust";
public static final String MESSAGE = "message";
public static final String EVENT_ID = "event_id";
public static final String RESOURCE_UUID = "resource_uuid";
public static final String LAST_UPDATE = "last_update";
public static final String RELATED_UUID = "related_uuid";
protected SolrClient getSolr() {
if (solr == null) {
String solrService = DSpaceServicesFactory.getInstance().getConfigurationService()
.getProperty("qaevents.solr.server", "http://localhost:8983/solr/qaevent");
return new HttpSolrClient.Builder(solrService).build();
}
return solr;
}
@Override
public long countTopics() {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery("*:*");
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(TOPIC);
QueryResponse response;
try {
response = getSolr().query(solrQuery);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
return response.getFacetField(TOPIC).getValueCount();
}
@Override
public long countTopicsBySource(String source) {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery("*:*");
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(TOPIC);
solrQuery.addFilterQuery("source:" + source);
QueryResponse response;
try {
response = getSolr().query(solrQuery);
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
return response.getFacetField(TOPIC).getValueCount();
}
@Override
public void deleteEventByEventId(String id) {
try {
getSolr().deleteById(id);
getSolr().commit();
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void deleteEventsByTargetId(UUID targetId) {
try {
getSolr().deleteByQuery(RESOURCE_UUID + ":" + targetId.toString());
getSolr().commit();
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public QATopic findTopicByTopicId(String topicId) {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery(TOPIC + ":" + topicId.replaceAll("!", "/"));
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(TOPIC);
QueryResponse response;
try {
response = getSolr().query(solrQuery);
FacetField facetField = response.getFacetField(TOPIC);
for (Count c : facetField.getValues()) {
if (c.getName().equals(topicId.replace("!", "/"))) {
QATopic topic = new QATopic();
topic.setKey(c.getName());
topic.setTotalEvents(c.getCount());
topic.setLastEvent(new Date());
return topic;
}
}
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
return null;
}
@Override
public List<QATopic> findAllTopics(long offset, long count, String orderField, boolean ascending) {
return findAllTopicsBySource(null, offset, count, orderField, ascending);
}
@Override
public List<QATopic> findAllTopicsBySource(String source, long offset, long count,
String orderField, boolean ascending) {
if (source != null && isNotSupportedSource(source)) {
return null;
}
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setSort(orderField, ascending ? ORDER.asc : ORDER.desc);
solrQuery.setFacetSort(FacetParams.FACET_SORT_INDEX);
solrQuery.setQuery("*:*");
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.setFacetLimit((int) (offset + count));
solrQuery.addFacetField(TOPIC);
if (source != null) {
solrQuery.addFilterQuery(SOURCE + ":" + source);
}
QueryResponse response;
List<QATopic> topics = new ArrayList<>();
try {
response = getSolr().query(solrQuery);
FacetField facetField = response.getFacetField(TOPIC);
topics = new ArrayList<>();
int idx = 0;
for (Count c : facetField.getValues()) {
if (idx < offset) {
idx++;
continue;
}
QATopic topic = new QATopic();
topic.setKey(c.getName());
topic.setTotalEvents(c.getCount());
topic.setLastEvent(new Date());
topics.add(topic);
idx++;
}
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
return topics;
}
@Override
public void store(Context context, QAEvent dto) {
if (isNotSupportedSource(dto.getSource())) {
throw new IllegalArgumentException("The source of the given event is not supported: " + dto.getSource());
}
if (StringUtils.isBlank(dto.getTopic())) {
throw new IllegalArgumentException("A topic is mandatory for an event");
}
String checksum = dto.getEventId();
try {
if (!qaEventsDao.isEventStored(context, checksum)) {
SolrInputDocument doc = createSolrDocument(context, dto, checksum);
UpdateRequest updateRequest = new UpdateRequest();
updateRequest.add(doc);
updateRequest.process(getSolr());
getSolr().commit();
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public QAEvent findEventByEventId(String eventId) {
SolrQuery param = new SolrQuery(EVENT_ID + ":" + eventId);
QueryResponse response;
try {
response = getSolr().query(param);
if (response != null) {
SolrDocumentList list = response.getResults();
if (list != null && list.size() == 1) {
SolrDocument doc = list.get(0);
return getQAEventFromSOLR(doc);
}
}
} catch (SolrServerException | IOException e) {
throw new RuntimeException("Exception querying Solr", e);
}
return null;
}
@Override
public List<QAEvent> findEventsByTopicAndPage(String topic, long offset,
int pageSize, String orderField, boolean ascending) {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setStart(((Long) offset).intValue());
if (pageSize != -1) {
solrQuery.setRows(pageSize);
}
solrQuery.setSort(orderField, ascending ? ORDER.asc : ORDER.desc);
solrQuery.setQuery(TOPIC + ":" + topic.replaceAll("!", "/"));
QueryResponse response;
try {
response = getSolr().query(solrQuery);
if (response != null) {
SolrDocumentList list = response.getResults();
List<QAEvent> responseItem = new ArrayList<>();
for (SolrDocument doc : list) {
QAEvent item = getQAEventFromSOLR(doc);
responseItem.add(item);
}
return responseItem;
}
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
return List.of();
}
@Override
public List<QAEvent> findEventsByTopic(String topic) {
return findEventsByTopicAndPage(topic, 0, -1, TRUST, false);
}
@Override
public long countEventsByTopic(String topic) {
SolrQuery solrQuery = new SolrQuery();
solrQuery.setRows(0);
solrQuery.setQuery(TOPIC + ":" + topic.replace("!", "/"));
QueryResponse response = null;
try {
response = getSolr().query(solrQuery);
return response.getResults().getNumFound();
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
}
@Override
public QASource findSource(String sourceName) {
if (isNotSupportedSource(sourceName)) {
return null;
}
SolrQuery solrQuery = new SolrQuery("*:*");
solrQuery.setRows(0);
solrQuery.addFilterQuery(SOURCE + ":" + sourceName);
solrQuery.setFacet(true);
solrQuery.setFacetMinCount(1);
solrQuery.addFacetField(SOURCE);
QueryResponse response;
try {
response = getSolr().query(solrQuery);
FacetField facetField = response.getFacetField(SOURCE);
for (Count c : facetField.getValues()) {
if (c.getName().equalsIgnoreCase(sourceName)) {
QASource source = new QASource();
source.setName(c.getName());
source.setTotalEvents(c.getCount());
source.setLastEvent(new Date());
return source;
}
}
} catch (SolrServerException | IOException e) {
throw new RuntimeException(e);
}
QASource source = new QASource();
source.setName(sourceName);
source.setTotalEvents(0L);
return source;
}
@Override
public List<QASource> findAllSources(long offset, int pageSize) {
return Arrays.stream(getSupportedSources())
.map((sourceName) -> findSource(sourceName))
.sorted(comparing(QASource::getTotalEvents).reversed())
.skip(offset)
.limit(pageSize)
.collect(Collectors.toList());
}
@Override
public long countSources() {
return getSupportedSources().length;
}
@Override
public boolean isRelatedItemSupported(QAEvent qaevent) {
// Currently only PROJECT topics related to OPENAIRE supports related items
return qaevent.getSource().equals(OPENAIRE_SOURCE) && endsWith(qaevent.getTopic(), "/PROJECT");
}
private SolrInputDocument createSolrDocument(Context context, QAEvent dto, String checksum) throws Exception {
SolrInputDocument doc = new SolrInputDocument();
doc.addField(SOURCE, dto.getSource());
doc.addField(EVENT_ID, checksum);
doc.addField(ORIGINAL_ID, dto.getOriginalId());
doc.addField(TITLE, dto.getTitle());
doc.addField(TOPIC, dto.getTopic());
doc.addField(TRUST, dto.getTrust());
doc.addField(MESSAGE, dto.getMessage());
doc.addField(LAST_UPDATE, new Date());
final String resourceUUID = getResourceUUID(context, dto.getOriginalId());
if (resourceUUID == null) {
throw new IllegalArgumentException("Skipped event " + checksum +
" related to the oai record " + dto.getOriginalId() + " as the record was not found");
}
doc.addField(RESOURCE_UUID, resourceUUID);
doc.addField(RELATED_UUID, dto.getRelated());
return doc;
}
private String getResourceUUID(Context context, String originalId) throws Exception {
String id = getHandleFromOriginalId(originalId);
if (id != null) {
Item item = (Item) handleService.resolveToObject(context, id);
if (item != null) {
final String itemUuid = item.getID().toString();
context.uncacheEntity(item);
return itemUuid;
} else {
return null;
}
} else {
throw new IllegalArgumentException("Malformed originalId " + originalId);
}
}
// oai:www.openstarts.units.it:10077/21486
private String getHandleFromOriginalId(String originalId) {
int startPosition = originalId.lastIndexOf(':');
if (startPosition != -1) {
return originalId.substring(startPosition + 1, originalId.length());
} else {
return null;
}
}
private QAEvent getQAEventFromSOLR(SolrDocument doc) {
QAEvent item = new QAEvent();
item.setSource((String) doc.get(SOURCE));
item.setEventId((String) doc.get(EVENT_ID));
item.setLastUpdate((Date) doc.get(LAST_UPDATE));
item.setMessage((String) doc.get(MESSAGE));
item.setOriginalId((String) doc.get(ORIGINAL_ID));
item.setTarget((String) doc.get(RESOURCE_UUID));
item.setTitle((String) doc.get(TITLE));
item.setTopic((String) doc.get(TOPIC));
item.setTrust((double) doc.get(TRUST));
item.setRelated((String) doc.get(RELATED_UUID));
return item;
}
private boolean isNotSupportedSource(String source) {
return !ArrayUtils.contains(getSupportedSources(), source);
}
private String[] getSupportedSources() {
return configurationService.getArrayProperty("qaevent.sources", new String[] { QAEvent.OPENAIRE_SOURCE });
}
}

View File

@@ -0,0 +1,35 @@
/**
* 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.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Extension of {@link JsonDeserializer} that convert a json to a String.
*
* @author Andrea Bollini (andrea.bollini at 4science.it)
*
*/
public class RawJsonDeserializer extends JsonDeserializer<String> {
@Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
JsonNode node = mapper.readTree(jp);
return mapper.writeValueAsString(node);
}
}