diff --git a/docker-compose.yml b/docker-compose.yml index 38007908c6e..14f47ebdb67 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -121,6 +121,8 @@ services: cp -r /opt/solr/server/solr/configsets/search/* search precreate-core statistics /opt/solr/server/solr/configsets/statistics cp -r /opt/solr/server/solr/configsets/statistics/* statistics + precreate-core qaevent /opt/solr/server/solr/configsets/qaevent + cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent exec solr -f volumes: assetstore: diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index 547be787e4c..849dbd1356a 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -819,6 +819,12 @@ + + + eu.openaire + broker-client + 1.1.2 + org.mock-server diff --git a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java index 84ca1692ccf..be763713ec1 100644 --- a/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/CollectionServiceImpl.java @@ -1021,6 +1021,61 @@ private DiscoverResult retrieveCollectionsWithSubmit(Context context, DiscoverQu 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 communities, + String entityType) { + + for (Community community : communities) { + Collection collection = retrieveCollectionWithSubmitByCommunityAndEntityType(context, community, + entityType); + if (collection != null) { + return collection; + } + } + + for (Community community : communities) { + List 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 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 findCollectionsWithSubmit(String q, Context context, Community community, String entityType, int offset, int limit) throws SQLException, SearchServiceException { diff --git a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java index ebea2aa5b82..f6144961c6f 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -77,6 +77,7 @@ 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 implements It @Autowired(required = true) protected SubscribeService subscribeService; + @Autowired + private QAEventsDAO qaEventsDao; + protected ItemServiceImpl() { super(); } @@ -819,6 +823,11 @@ protected void rawDelete(Context context, Item item) throws AuthorizeException, orcidToken.setProfileItem(null); } + List 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); diff --git a/dspace-api/src/main/java/org/dspace/content/QAEvent.java b/dspace-api/src/main/java/org/dspace/content/QAEvent.java new file mode 100644 index 00000000000..9e90f81be32 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/QAEvent.java @@ -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 getMessageDtoClass() { + switch (getSource()) { + case OPENAIRE_SOURCE: + return OpenaireMessageDTO.class; + default: + throw new IllegalArgumentException("Unknown event's source: " + getSource()); + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java b/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java new file mode 100644 index 00000000000..3631a2ff68c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/QAEventProcessed.java @@ -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; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java index 90db5c73140..0a56105ead4 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/CollectionService.java @@ -417,6 +417,34 @@ public List findCollectionsWithSubmit(String q, Context context, Com public List 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) diff --git a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java index 66fe6562ea2..b9fde450c2e 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/eperson/EPersonServiceImpl.java @@ -33,6 +33,7 @@ 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.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 impleme protected ConfigurationService configurationService; @Autowired protected OrcidTokenService orcidTokenService; + @Autowired + protected QAEventsDAO qaEventsDao; protected EPersonServiceImpl() { super(); @@ -487,6 +491,11 @@ public void delete(Context context, EPerson ePerson, boolean cascade) // Remove any subscriptions subscribeService.deleteByEPerson(context, ePerson); + List qaEvents = qaEventsDao.findByEPerson(context, ePerson); + for (QAEventProcessed qaEvent : qaEvents) { + qaEventsDao.delete(context, qaEvent); + } + // Remove ourself ePersonDAO.delete(context, ePerson); diff --git a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java similarity index 92% rename from dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java rename to dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java index b0aa4aba13a..c96fad1de01 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestConnector.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenaireRestConnector.java @@ -40,20 +40,20 @@ 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 OpenAIRERestConnector(String url) { * * @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 OpenAIRERestToken grabNewAccessToken() throws IOException { 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 InputStream get(String file, String accessToken) { } /** - * 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 Response searchProjectByKeywords(int page, int size, String... keywords) } /** - * 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 Response searchProjectByIDAndFunder(String projectID, String projectFunde } /** - * 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 }; diff --git a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestToken.java b/dspace-api/src/main/java/org/dspace/external/OpenaireRestToken.java similarity index 89% rename from dspace-api/src/main/java/org/dspace/external/OpenAIRERestToken.java rename to dspace-api/src/main/java/org/dspace/external/OpenaireRestToken.java index 203f09b3c65..f5dc2b27f8a 100644 --- a/dspace-api/src/main/java/org/dspace/external/OpenAIRERestToken.java +++ b/dspace-api/src/main/java/org/dspace/external/OpenaireRestToken.java @@ -8,13 +8,13 @@ package org.dspace.external; /** - * OpenAIRE rest API token to be used when grabbing an accessToken.
+ * Openaire rest API token to be used when grabbing an accessToken.
* 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); } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java similarity index 94% rename from dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java rename to dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java index 8ca5b7c0ea5..62cef508c55 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenAIREFundingDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OpenaireFundingDataProvider.java @@ -31,7 +31,7 @@ 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 @@ /** * 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 metadataFields; @@ -93,7 +93,7 @@ public Optional getExternalDataObject(String id) { // 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 Optional getExternalDataObject(String id) { 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 List searchExternalDataObjects(String query, int star 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 List searchExternalDataObjects(String query, int star 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 int getNumberOfResults(String query) { * 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 @@ private static boolean isValidProjectURI(String projectURI) { } /** - * 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 void setMetadataFields(Map metadataFields) { } /** - * OpenAIRE Funding External Data Builder Class + * Openaire Funding External Data Builder Class * * @author pgraca */ diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QAEventsDeleteCascadeConsumer.java b/dspace-api/src/main/java/org/dspace/qaevent/QAEventsDeleteCascadeConsumer.java new file mode 100644 index 00000000000..6460c360ecb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/QAEventsDeleteCascadeConsumer.java @@ -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 { + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QASource.java b/dspace-api/src/main/java/org/dspace/qaevent/QASource.java new file mode 100644 index 00000000000..e22f7d32a77 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/QASource.java @@ -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; + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java b/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java new file mode 100644 index 00000000000..63e523b9cb5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java @@ -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; + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QualityAssuranceAction.java b/dspace-api/src/main/java/org/dspace/qaevent/QualityAssuranceAction.java new file mode 100644 index 00000000000..f2aebba799b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/QualityAssuranceAction.java @@ -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); +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QAEntityOpenaireMetadataAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QAEntityOpenaireMetadataAction.java new file mode 100644 index 00000000000..f244418dd06 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAEntityOpenaireMetadataAction.java @@ -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 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 getEntityMetadata() { + return entityMetadata; + } + + public void setEntityMetadata(Map 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; + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireMetadataMapAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireMetadataMapAction.java new file mode 100644 index 00000000000..e1fa23002fc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireMetadataMapAction.java @@ -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 types; + @Autowired + private ItemService itemService; + + public void setItemService(ItemService itemService) { + this.itemService = itemService; + } + + public Map getTypes() { + return types; + } + + public void setTypes(Map 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; + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireSimpleMetadataAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireSimpleMetadataAction.java new file mode 100644 index 00000000000..2509b768aef --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireSimpleMetadataAction.java @@ -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); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDAO.java b/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDAO.java new file mode 100644 index 00000000000..98c38ca3f5a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/dao/QAEventsDAO.java @@ -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 { + + /** + * Returns all the stored QAEventProcessed entities. + * + * @param context the DSpace context + * @return the found entities + * @throws SQLException if an SQL error occurs + */ + public List 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 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 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 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); + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDAOImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDAOImpl.java new file mode 100644 index 00000000000..ac9b96045e4 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/dao/impl/QAEventsDAOImpl.java @@ -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 implements QAEventsDAO { + + @Override + public List 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 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 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 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); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImport.java b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImport.java new file mode 100644 index 00000000000..9087606aa6e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImport.java @@ -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 see + * + *
+ * {
+ * "originalId": "oai:www.openstarts.units.it:10077/21838",
+ * "title": "Egypt, crossroad of translations and literary interweavings",
+ * "topic": "ENRICH/MORE/PROJECT",
+ * "trust": 1.0,
+ * "message": {
+ * "projects[0].acronym": "PAThs",
+ * "projects[0].code": "687567",
+ * "projects[0].funder": "EC",
+ * "projects[0].fundingProgram": "H2020",
+ * "projects[0].jurisdiction": "EU",
+ * "projects[0].openaireId": "40|corda__h2020::6e32f5eb912688f2424c68b851483ea4",
+ * "projects[0].title": "Tracking Papyrus and Parchment Paths"
+ * }
+ * } + *
+ * + * @author Alessandro Martelli (alessandro.martelli at 4science.it) + * @author Luca Giamminonni (luca.giamminonni at 4Science.it) + * + */ +public class OpenaireEventsImport + extends DSpaceRunnable> { + + 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 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 subscriptionIds = listEmailSubscriptions(); + + handler.logInfo("Found " + subscriptionIds.size() + " subscriptions related to the given email"); + + for (String subscriptionId : subscriptionIds) { + + List 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 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 readOpenaireQAEventsFromJson(InputStream inputStream) throws Exception { + return jsonMapper.readValue(inputStream, new TypeReference>() { + }); + } + + /** + * Store the given QAEvents. + * + * @param events the event to be stored + */ + private void storeOpenaireQAEvents(List 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 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); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportCli.java b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportCli.java new file mode 100644 index 00000000000..d98b578cdd3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportCli.java @@ -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); + } + + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportCliScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportCliScriptConfiguration.java new file mode 100644 index 00000000000..5be0453a17f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportCliScriptConfiguration.java @@ -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 + extends OpenaireEventsImportScriptConfiguration { + + @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; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportScriptConfiguration.java new file mode 100644 index 00000000000..60001e73507 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/script/OpenaireEventsImportScriptConfiguration.java @@ -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 extends ScriptConfiguration { + + /* + private AuthorizeService authorizeService; + */ + private Class dspaceRunnableClass; + + @Override + public Class getDspaceRunnableClass() { + return dspaceRunnableClass; + } + + /** + * Generic setter for the dspaceRunnableClass + * @param dspaceRunnableClass The dspaceRunnableClass to be set on this OpenaireEventsImportScriptConfiguration + */ + @Override + public void setDspaceRunnableClass(Class 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; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/OpenaireClientFactory.java b/dspace-api/src/main/java/org/dspace/qaevent/service/OpenaireClientFactory.java new file mode 100644 index 00000000000..e7a7be33c1b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/OpenaireClientFactory.java @@ -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); + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventActionService.java b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventActionService.java new file mode 100644 index 00000000000..2e5690f6225 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventActionService.java @@ -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); +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java new file mode 100644 index 00000000000..2332a55caf5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java @@ -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 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 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 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 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 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); + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/dto/OpenaireMessageDTO.java b/dspace-api/src/main/java/org/dspace/qaevent/service/dto/OpenaireMessageDTO.java new file mode 100644 index 00000000000..59b4acf9db2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/dto/OpenaireMessageDTO.java @@ -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 see + * @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; + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/dto/QAMessageDTO.java b/dspace-api/src/main/java/org/dspace/qaevent/service/dto/QAMessageDTO.java new file mode 100644 index 00000000000..2a63f42e615 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/dto/QAMessageDTO.java @@ -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 { + + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/OpenaireClientFactoryImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/OpenaireClientFactoryImpl.java new file mode 100644 index 00000000000..5839f5e8776 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/OpenaireClientFactoryImpl.java @@ -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; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java new file mode 100644 index 00000000000..cca70ecd043 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventActionServiceImpl.java @@ -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 topicsToActions; + + public void setTopicsToActions(Map topicsToActions) { + this.topicsToActions = topicsToActions; + } + + public Map 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); + } + } + } + } + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java new file mode 100644 index 00000000000..1dfcc1b6d96 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventServiceImpl.java @@ -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 findAllTopics(long offset, long count, String orderField, boolean ascending) { + return findAllTopicsBySource(null, offset, count, orderField, ascending); + } + + @Override + public List 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 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 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 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 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 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 }); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/util/RawJsonDeserializer.java b/dspace-api/src/main/java/org/dspace/util/RawJsonDeserializer.java new file mode 100644 index 00000000000..baadf0d2834 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/RawJsonDeserializer.java @@ -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 { + + @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); + } +} \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2023.08.07__qaevent_processed.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2023.08.07__qaevent_processed.sql new file mode 100644 index 00000000000..467de85f850 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2023.08.07__qaevent_processed.sql @@ -0,0 +1,16 @@ +-- +-- 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 TABLE qaevent_processed ( + qaevent_id VARCHAR(255) NOT NULL, + qaevent_timestamp TIMESTAMP NULL, + eperson_uuid UUID NULL REFERENCES eperson(uuid), + item_uuid uuid NOT NULL REFERENCES item(uuid) +); + +CREATE INDEX item_uuid_idx ON qaevent_processed(item_uuid); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2023.08.07__qaevent_processed.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2023.08.07__qaevent_processed.sql new file mode 100644 index 00000000000..5c3f0fac73c --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2023.08.07__qaevent_processed.sql @@ -0,0 +1,19 @@ +-- +-- 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 TABLE qaevent_processed ( + qaevent_id VARCHAR(255) NOT NULL, + qaevent_timestamp TIMESTAMP NULL, + eperson_uuid UUID NULL, + item_uuid UUID NULL, + CONSTRAINT qaevent_pk PRIMARY KEY (qaevent_id), + CONSTRAINT eperson_uuid_fkey FOREIGN KEY (eperson_uuid) REFERENCES eperson (uuid), + CONSTRAINT item_uuid_fkey FOREIGN KEY (item_uuid) REFERENCES item (uuid) +); + +CREATE INDEX item_uuid_idx ON qaevent_processed(item_uuid); diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 35ed8a235b7..05a4cc5add0 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/local.cfg +++ b/dspace-api/src/test/data/dspaceFolder/config/local.cfg @@ -95,14 +95,14 @@ loglevel.dspace = INFO # IIIF TEST SETTINGS # ######################## iiif.enabled = true -event.dispatcher.default.consumers = versioning, discovery, eperson, orcidqueue, iiif +event.dispatcher.default.consumers = versioning, discovery, eperson, orcidqueue, iiif, qaeventsdelete ########################################### # CUSTOM UNIT / INTEGRATION TEST SETTINGS # ########################################### # custom dispatcher to be used by dspace-api IT that doesn't need SOLR event.dispatcher.exclude-discovery.class = org.dspace.event.BasicDispatcher -event.dispatcher.exclude-discovery.consumers = versioning, eperson +event.dispatcher.exclude-discovery.consumers = versioning, eperson, qaeventsdelete # Configure authority control for Unit Testing (in DSpaceControlledVocabularyTest) # (This overrides default, commented out settings in dspace.cfg) diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml index f1e6c30d139..8a5277ab2da 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml @@ -7,7 +7,7 @@ http://www.springframework.org/schema/util/spring-util.xsd" default-lazy-init="true"> - + @@ -15,10 +15,10 @@ - - + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml index 836d4f08967..8bae32eaefd 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/item-filters.xml @@ -286,7 +286,7 @@ - @@ -329,7 +329,7 @@ - diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml index 808d22a5bf2..a197b2910bd 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/scripts.xml @@ -64,6 +64,11 @@ + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml index 32ab90b2cc6..29703e3ee07 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/solr-services.xml @@ -48,6 +48,11 @@ class="org.dspace.statistics.MockSolrStatisticsCore" autowire-candidate="true"/> + + + + diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index e27fb19a68e..6884b949a66 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -29,6 +29,8 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; import org.dspace.kernel.ServiceManager; +import org.dspace.qaevent.MockQAEventService; +import org.dspace.qaevent.service.QAEventService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.statistics.MockSolrLoggerServiceImpl; import org.dspace.statistics.MockSolrStatisticsCore; @@ -196,6 +198,10 @@ public void destroy() throws Exception { .getServiceByName(AuthoritySearchService.class.getName(), MockAuthoritySolrServiceImpl.class); authorityService.reset(); + MockQAEventService qaEventService = serviceManager + .getServiceByName(QAEventService.class.getName(), MockQAEventService.class); + qaEventService.reset(); + // Reload our ConfigurationService (to reset configs to defaults again) DSpaceServicesFactory.getInstance().getConfigurationService().reloadConfig(); diff --git a/dspace-api/src/test/java/org/dspace/app/scripts/handler/impl/TestDSpaceRunnableHandler.java b/dspace-api/src/test/java/org/dspace/app/scripts/handler/impl/TestDSpaceRunnableHandler.java index aced81cbdfd..7cc1e8cb45d 100644 --- a/dspace-api/src/test/java/org/dspace/app/scripts/handler/impl/TestDSpaceRunnableHandler.java +++ b/dspace-api/src/test/java/org/dspace/app/scripts/handler/impl/TestDSpaceRunnableHandler.java @@ -61,6 +61,12 @@ public void logError(String message) { errorMessages.add(message); } + @Override + public void logError(String message, Throwable throwable) { + super.logError(message, throwable); + errorMessages.add(message); + } + public List getInfoMessages() { return infoMessages; } diff --git a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java index f84d17fc7af..775dfaabe20 100644 --- a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java @@ -49,6 +49,7 @@ import org.dspace.orcid.service.OrcidHistoryService; import org.dspace.orcid.service.OrcidQueueService; import org.dspace.orcid.service.OrcidTokenService; +import org.dspace.qaevent.service.QAEventService; import org.dspace.scripts.factory.ScriptServiceFactory; import org.dspace.scripts.service.ProcessService; import org.dspace.services.factory.DSpaceServicesFactory; @@ -56,6 +57,7 @@ import org.dspace.submit.service.SubmissionConfigService; import org.dspace.supervision.factory.SupervisionOrderServiceFactory; import org.dspace.supervision.service.SupervisionOrderService; +import org.dspace.utils.DSpace; import org.dspace.versioning.factory.VersionServiceFactory; import org.dspace.versioning.service.VersionHistoryService; import org.dspace.versioning.service.VersioningService; @@ -113,7 +115,7 @@ public abstract class AbstractBuilder { static SubmissionConfigService submissionConfigService; static SubscribeService subscribeService; static SupervisionOrderService supervisionOrderService; - + static QAEventService qaEventService; protected Context context; @@ -182,6 +184,7 @@ public static void init() { } subscribeService = ContentServiceFactory.getInstance().getSubscribeService(); supervisionOrderService = SupervisionOrderServiceFactory.getInstance().getSupervisionOrderService(); + qaEventService = new DSpace().getSingletonService(QAEventService.class); } @@ -219,6 +222,7 @@ public static void destroy() { submissionConfigService = null; subscribeService = null; supervisionOrderService = null; + qaEventService = null; } diff --git a/dspace-api/src/test/java/org/dspace/builder/QAEventBuilder.java b/dspace-api/src/test/java/org/dspace/builder/QAEventBuilder.java new file mode 100644 index 00000000000..823080516df --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/builder/QAEventBuilder.java @@ -0,0 +1,141 @@ +/** + * 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.util.Date; + +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.QAEvent; +import org.dspace.core.Context; +import org.dspace.qaevent.service.QAEventService; + +/** + * Builder to construct Quality Assurance Broker Event objects + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +public class QAEventBuilder extends AbstractBuilder { + + private Item item; + private QAEvent target; + private String source = QAEvent.OPENAIRE_SOURCE; + /** + * the title of the DSpace object + * */ + private String title; + /** + * the name of the Quality Assurance Event Topic + * */ + private String topic; + /** + * thr original QA Event imported + * */ + private String message; + /** + * uuid of the targeted DSpace object + * */ + private String relatedItem; + private double trust = 0.5; + private Date lastUpdate = new Date(); + + protected QAEventBuilder(Context context) { + super(context); + } + + public static QAEventBuilder createTarget(final Context context, final Collection col, final String name) { + QAEventBuilder builder = new QAEventBuilder(context); + return builder.create(context, col, name); + } + + public static QAEventBuilder createTarget(final Context context, final Item item) { + QAEventBuilder builder = new QAEventBuilder(context); + return builder.create(context, item); + } + + private QAEventBuilder create(final Context context, final Collection col, final String name) { + this.context = context; + + try { + ItemBuilder itemBuilder = ItemBuilder.createItem(context, col).withTitle(name); + item = itemBuilder.build(); + this.title = name; + context.dispatchEvents(); + indexingService.commit(); + } catch (Exception e) { + return handleException(e); + } + return this; + } + + private QAEventBuilder create(final Context context, final Item item) { + this.context = context; + this.item = item; + return this; + } + + public QAEventBuilder withTopic(final String topic) { + this.topic = topic; + return this; + } + public QAEventBuilder withSource(final String source) { + this.source = source; + return this; + } + public QAEventBuilder withTitle(final String title) { + this.title = title; + return this; + } + public QAEventBuilder withMessage(final String message) { + this.message = message; + return this; + } + public QAEventBuilder withTrust(final double trust) { + this.trust = trust; + return this; + } + public QAEventBuilder withLastUpdate(final Date lastUpdate) { + this.lastUpdate = lastUpdate; + return this; + } + + public QAEventBuilder withRelatedItem(String relatedItem) { + this.relatedItem = relatedItem; + return this; + } + + @Override + public QAEvent build() { + target = new QAEvent(source, "oai:www.dspace.org:" + item.getHandle(), item.getID().toString(), title, topic, + trust, message, lastUpdate); + target.setRelated(relatedItem); + try { + qaEventService.store(context, target); + } catch (Exception e) { + e.printStackTrace(); + } + return target; + } + + @Override + public void cleanup() throws Exception { + qaEventService.deleteEventByEventId(target.getEventId()); + } + + @Override + protected QAEventService getService() { + return qaEventService; + } + + @Override + public void delete(Context c, QAEvent dso) throws Exception { + qaEventService.deleteEventByEventId(target.getEventId()); + +// qaEventService.deleteTarget(dso); + } +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java index 13d037abf82..a177571ffa4 100644 --- a/dspace-api/src/test/java/org/dspace/content/CollectionTest.java +++ b/dspace-api/src/test/java/org/dspace/content/CollectionTest.java @@ -1200,4 +1200,71 @@ public void testGetParentObject() throws SQLException { equalTo(owningCommunity)); } + /** + * Test of retrieveCollectionWithSubmitByEntityType method getting the closest + * collection of non-item type starting from an item + */ + @Test + public void testRetrieveCollectionWithSubmitByEntityType() throws SQLException, AuthorizeException { + context.setDispatcher("default"); + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Group submitters = groupService.create(context); + Collection collection = collectionService.create(context, com); + collectionService.addMetadata(context, collection, "dspace", "entity", "type", + null, "Publication"); + com.addCollection(collection); + WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, false); + Item item = installItemService.installItem(context, workspaceItem); + EPerson epersonA = ePersonService.create(context); + Collection collectionPerson = collectionService.create(context, com); + collectionService.addMetadata(context, collectionPerson, "dspace", "entity", "type", + null, "Person"); + collectionPerson.setSubmitters(submitters); + groupService.addMember(context, submitters, epersonA); + context.setCurrentUser(epersonA); + context.commit(); + context.restoreAuthSystemState(); + Collection resultCollection = collectionService.retrieveCollectionWithSubmitByEntityType + (context, item, "Person"); + + assertThat("testRetrieveCollectionWithSubmitByEntityType 0", resultCollection, notNullValue()); + assertThat("testRetrieveCollectionWithSubmitByEntityType 1", resultCollection, equalTo(collectionPerson)); + + context.setDispatcher("exclude-discovery"); + } + + /** + * Test of rretrieveCollectionWithSubmitByCommunityAndEntityType method getting the closest + * collection of non-community type starting from an community + */ + @Test + public void testRetrieveCollectionWithSubmitByCommunityAndEntityType() throws SQLException, AuthorizeException { + context.setDispatcher("default"); + context.turnOffAuthorisationSystem(); + Community com = communityService.create(null, context); + Group submitters = groupService.create(context); + Collection collection = collectionService.create(context, com); + collectionService.addMetadata(context, collection, "dspace", "entity", "type", + null, "Publication"); + com.addCollection(collection); + WorkspaceItem workspaceItem = workspaceItemService.create(context, collection, false); + Item item = installItemService.installItem(context, workspaceItem); + EPerson epersonA = ePersonService.create(context); + Collection collectionPerson = collectionService.create(context, com); + collectionService.addMetadata(context, collectionPerson, "dspace", "entity", "type", + null, "Person"); + collectionPerson.setSubmitters(submitters); + groupService.addMember(context, submitters, epersonA); + context.setCurrentUser(epersonA); + context.commit(); + context.restoreAuthSystemState(); + Collection resultCollection = collectionService.retrieveCollectionWithSubmitByCommunityAndEntityType + (context, com, "Person"); + + assertThat("testRetrieveCollectionWithSubmitByEntityType 0", resultCollection, notNullValue()); + assertThat("testRetrieveCollectionWithSubmitByEntityType 1", resultCollection, equalTo(collectionPerson)); + + context.setDispatcher("exclude-discovery"); + } } diff --git a/dspace-api/src/test/java/org/dspace/external/MockOpenAIRERestConnector.java b/dspace-api/src/test/java/org/dspace/external/MockOpenaireRestConnector.java similarity index 90% rename from dspace-api/src/test/java/org/dspace/external/MockOpenAIRERestConnector.java rename to dspace-api/src/test/java/org/dspace/external/MockOpenaireRestConnector.java index bac80e196cf..c67402dfdcc 100644 --- a/dspace-api/src/test/java/org/dspace/external/MockOpenAIRERestConnector.java +++ b/dspace-api/src/test/java/org/dspace/external/MockOpenaireRestConnector.java @@ -14,15 +14,15 @@ import eu.openaire.jaxb.model.Response; /** - * Mock the OpenAIRE rest connector for unit testing
+ * Mock the Openaire rest connector for unit testing
* will be resolved against static test xml files * * @author pgraca * */ -public class MockOpenAIRERestConnector extends OpenAIRERestConnector { +public class MockOpenaireRestConnector extends OpenaireRestConnector { - public MockOpenAIRERestConnector(String url) { + public MockOpenaireRestConnector(String url) { super(url); } diff --git a/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenAIREFundingDataProviderTest.java b/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java similarity index 59% rename from dspace-api/src/test/java/org/dspace/external/provider/impl/OpenAIREFundingDataProviderTest.java rename to dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java index 5e96f06ac8a..d14dc990353 100644 --- a/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenAIREFundingDataProviderTest.java +++ b/dspace-api/src/test/java/org/dspace/external/provider/impl/OpenaireFundingDataProviderTest.java @@ -23,15 +23,15 @@ import org.junit.Test; /** - * Unit tests for OpenAIREFundingDataProvider + * Unit tests for OpenaireFundingDataProvider * * @author pgraca * */ -public class OpenAIREFundingDataProviderTest extends AbstractDSpaceTest { +public class OpenaireFundingDataProviderTest extends AbstractDSpaceTest { ExternalDataService externalDataService; - ExternalDataProvider openAIREFundingDataProvider; + ExternalDataProvider openaireFundingDataProvider; /** * This method will be run before every test as per @Before. It will initialize @@ -44,38 +44,38 @@ public class OpenAIREFundingDataProviderTest extends AbstractDSpaceTest { public void init() { // Set up External Service Factory and set data providers externalDataService = ExternalServiceFactory.getInstance().getExternalDataService(); - openAIREFundingDataProvider = externalDataService.getExternalDataProvider("openAIREFunding"); + openaireFundingDataProvider = externalDataService.getExternalDataProvider("openaireFunding"); } @Test public void testNumberOfResultsWSingleKeyword() { - assertNotNull("openAIREFundingDataProvider is not null", openAIREFundingDataProvider); - assertEquals("openAIREFunding.numberOfResults.query:mock", 77, - openAIREFundingDataProvider.getNumberOfResults("mock")); + assertNotNull("openaireFundingDataProvider is not null", openaireFundingDataProvider); + assertEquals("openaireFunding.numberOfResults.query:mock", 77, + openaireFundingDataProvider.getNumberOfResults("mock")); } @Test public void testNumberOfResultsWKeywords() { - assertNotNull("openAIREFundingDataProvider is not null", openAIREFundingDataProvider); - assertEquals("openAIREFunding.numberOfResults.query:mock+test", 77, - openAIREFundingDataProvider.getNumberOfResults("mock+test")); + assertNotNull("openaireFundingDataProvider is not null", openaireFundingDataProvider); + assertEquals("openaireFunding.numberOfResults.query:mock+test", 77, + openaireFundingDataProvider.getNumberOfResults("mock+test")); } @Test public void testQueryResultsWSingleKeyword() { - assertNotNull("openAIREFundingDataProvider is not null", openAIREFundingDataProvider); - List results = openAIREFundingDataProvider.searchExternalDataObjects("mock", 0, 10); - assertEquals("openAIREFunding.searchExternalDataObjects.size", 10, results.size()); + assertNotNull("openaireFundingDataProvider is not null", openaireFundingDataProvider); + List results = openaireFundingDataProvider.searchExternalDataObjects("mock", 0, 10); + assertEquals("openaireFunding.searchExternalDataObjects.size", 10, results.size()); } @Test public void testQueryResultsWKeywords() { String value = "Mushroom Robo-Pic - Development of an autonomous robotic mushroom picking system"; - assertNotNull("openAIREFundingDataProvider is not null", openAIREFundingDataProvider); - List results = openAIREFundingDataProvider.searchExternalDataObjects("mock+test", 0, 10); - assertEquals("openAIREFunding.searchExternalDataObjects.size", 10, results.size()); - assertTrue("openAIREFunding.searchExternalDataObjects.first.value", value.equals(results.get(0).getValue())); + assertNotNull("openaireFundingDataProvider is not null", openaireFundingDataProvider); + List results = openaireFundingDataProvider.searchExternalDataObjects("mock+test", 0, 10); + assertEquals("openaireFunding.searchExternalDataObjects.size", 10, results.size()); + assertTrue("openaireFunding.searchExternalDataObjects.first.value", value.equals(results.get(0).getValue())); } @Test @@ -84,22 +84,22 @@ public void testGetDataObject() { String value = "Portuguese Wild Mushrooms: Chemical characterization and functional study" + " of antiproliferative and proapoptotic properties in cancer cell lines"; - assertNotNull("openAIREFundingDataProvider is not null", openAIREFundingDataProvider); + assertNotNull("openaireFundingDataProvider is not null", openaireFundingDataProvider); - Optional result = openAIREFundingDataProvider.getExternalDataObject(id); + Optional result = openaireFundingDataProvider.getExternalDataObject(id); - assertTrue("openAIREFunding.getExternalDataObject.exists", result.isPresent()); - assertTrue("openAIREFunding.getExternalDataObject.value", value.equals(result.get().getValue())); + assertTrue("openaireFunding.getExternalDataObject.exists", result.isPresent()); + assertTrue("openaireFunding.getExternalDataObject.value", value.equals(result.get().getValue())); } @Test public void testGetDataObjectWInvalidId() { String id = "WRONGID"; - assertNotNull("openAIREFundingDataProvider is not null", openAIREFundingDataProvider); + assertNotNull("openaireFundingDataProvider is not null", openaireFundingDataProvider); - Optional result = openAIREFundingDataProvider.getExternalDataObject(id); + Optional result = openaireFundingDataProvider.getExternalDataObject(id); - assertTrue("openAIREFunding.getExternalDataObject.notExists:WRONGID", result.isEmpty()); + assertTrue("openaireFunding.getExternalDataObject.notExists:WRONGID", result.isEmpty()); } } diff --git a/dspace-api/src/test/java/org/dspace/matcher/QAEventMatcher.java b/dspace-api/src/test/java/org/dspace/matcher/QAEventMatcher.java new file mode 100644 index 00000000000..52f3704a74b --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/matcher/QAEventMatcher.java @@ -0,0 +1,117 @@ +/** + * 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.matcher; + +import static org.dspace.content.QAEvent.OPENAIRE_SOURCE; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + +import org.dspace.content.Item; +import org.dspace.content.QAEvent; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Implementation of {@link org.hamcrest.Matcher} to match a QAEvent by all its + * attributes. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class QAEventMatcher extends TypeSafeMatcher { + + private Matcher eventIdMatcher; + + private Matcher originalIdMatcher; + + private Matcher relatedMatcher; + + private Matcher sourceMatcher; + + private Matcher statusMatcher; + + private Matcher targetMatcher; + + private Matcher titleMatcher; + + private Matcher messageMatcher; + + private Matcher topicMatcher; + + private Matcher trustMatcher; + + private QAEventMatcher(Matcher eventIdMatcher, Matcher originalIdMatcher, + Matcher relatedMatcher, Matcher sourceMatcher, Matcher statusMatcher, + Matcher targetMatcher, Matcher titleMatcher, Matcher messageMatcher, + Matcher topicMatcher, Matcher trustMatcher) { + this.eventIdMatcher = eventIdMatcher; + this.originalIdMatcher = originalIdMatcher; + this.relatedMatcher = relatedMatcher; + this.sourceMatcher = sourceMatcher; + this.statusMatcher = statusMatcher; + this.targetMatcher = targetMatcher; + this.titleMatcher = titleMatcher; + this.messageMatcher = messageMatcher; + this.topicMatcher = topicMatcher; + this.trustMatcher = trustMatcher; + } + + /** + * Creates an instance of {@link QAEventMatcher} that matches an OPENAIRE + * QAEvent with PENDING status, with an event id, without a related item and + * with the given attributes. + * + * @param originalId the original id to match + * @param target the target to match + * @param title the title to match + * @param message the message to match + * @param topic the topic to match + * @param trust the trust to match + * @return the matcher istance + */ + public static QAEventMatcher pendingOpenaireEventWith(String originalId, Item target, + String title, String message, String topic, Double trust) { + + return new QAEventMatcher(notNullValue(String.class), is(originalId), nullValue(String.class), + is(OPENAIRE_SOURCE), is("PENDING"), is(target.getID().toString()), is(title), is(message), is(topic), + is(trust)); + + } + + @Override + public boolean matchesSafely(QAEvent event) { + return eventIdMatcher.matches(event.getEventId()) + && originalIdMatcher.matches(event.getOriginalId()) + && relatedMatcher.matches(event.getRelated()) + && sourceMatcher.matches(event.getSource()) + && statusMatcher.matches(event.getStatus()) + && targetMatcher.matches(event.getTarget()) + && titleMatcher.matches(event.getTitle()) + && messageMatcher.matches(event.getMessage()) + && topicMatcher.matches(event.getTopic()) + && trustMatcher.matches(event.getTrust()); + } + + @Override + public void describeTo(Description description) { + description.appendText("a QA event with the following attributes:") + .appendText(" event id ").appendDescriptionOf(eventIdMatcher) + .appendText(", original id ").appendDescriptionOf(originalIdMatcher) + .appendText(", related ").appendDescriptionOf(relatedMatcher) + .appendText(", source ").appendDescriptionOf(sourceMatcher) + .appendText(", status ").appendDescriptionOf(statusMatcher) + .appendText(", target ").appendDescriptionOf(targetMatcher) + .appendText(", title ").appendDescriptionOf(titleMatcher) + .appendText(", message ").appendDescriptionOf(messageMatcher) + .appendText(", topic ").appendDescriptionOf(topicMatcher) + .appendText(" and trust ").appendDescriptionOf(trustMatcher); + } + +} diff --git a/dspace-api/src/test/java/org/dspace/matcher/QASourceMatcher.java b/dspace-api/src/test/java/org/dspace/matcher/QASourceMatcher.java new file mode 100644 index 00000000000..fe3b7130b54 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/matcher/QASourceMatcher.java @@ -0,0 +1,58 @@ +/** + * 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.matcher; + +import static org.hamcrest.Matchers.is; + +import org.dspace.qaevent.QASource; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Implementation of {@link org.hamcrest.Matcher} to match a QASource by all its + * attributes. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class QASourceMatcher extends TypeSafeMatcher { + + private Matcher nameMatcher; + + private Matcher totalEventsMatcher; + + private QASourceMatcher(Matcher nameMatcher, Matcher totalEventsMatcher) { + this.nameMatcher = nameMatcher; + this.totalEventsMatcher = totalEventsMatcher; + } + + /** + * Creates an instance of {@link QASourceMatcher} that matches a QATopic with + * the given name and total events count. + * @param name the name to match + * @param totalEvents the total events count to match + * @return the matcher instance + */ + public static QASourceMatcher with(String name, long totalEvents) { + return new QASourceMatcher(is(name), is(totalEvents)); + } + + @Override + public boolean matchesSafely(QASource event) { + return nameMatcher.matches(event.getName()) && totalEventsMatcher.matches(event.getTotalEvents()); + } + + @Override + public void describeTo(Description description) { + description.appendText("a QA source with the following attributes:") + .appendText(" name ").appendDescriptionOf(nameMatcher) + .appendText(" and total events ").appendDescriptionOf(totalEventsMatcher); + } + +} diff --git a/dspace-api/src/test/java/org/dspace/matcher/QATopicMatcher.java b/dspace-api/src/test/java/org/dspace/matcher/QATopicMatcher.java new file mode 100644 index 00000000000..dd93972814a --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/matcher/QATopicMatcher.java @@ -0,0 +1,58 @@ +/** + * 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.matcher; + +import static org.hamcrest.Matchers.is; + +import org.dspace.qaevent.QATopic; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Implementation of {@link org.hamcrest.Matcher} to match a QATopic by all its + * attributes. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class QATopicMatcher extends TypeSafeMatcher { + + private Matcher keyMatcher; + + private Matcher totalEventsMatcher; + + private QATopicMatcher(Matcher keyMatcher, Matcher totalEventsMatcher) { + this.keyMatcher = keyMatcher; + this.totalEventsMatcher = totalEventsMatcher; + } + + /** + * Creates an instance of {@link QATopicMatcher} that matches a QATopic with the + * given key and total events count. + * @param key the key to match + * @param totalEvents the total events count to match + * @return the matcher instance + */ + public static QATopicMatcher with(String key, long totalEvents) { + return new QATopicMatcher(is(key), is(totalEvents)); + } + + @Override + public boolean matchesSafely(QATopic event) { + return keyMatcher.matches(event.getKey()) && totalEventsMatcher.matches(event.getTotalEvents()); + } + + @Override + public void describeTo(Description description) { + description.appendText("a QA topic with the following attributes:") + .appendText(" key ").appendDescriptionOf(keyMatcher) + .appendText(" and total events ").appendDescriptionOf(totalEventsMatcher); + } + +} diff --git a/dspace-api/src/test/java/org/dspace/qaevent/MockQAEventService.java b/dspace-api/src/test/java/org/dspace/qaevent/MockQAEventService.java new file mode 100644 index 00000000000..3d460015f7e --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/qaevent/MockQAEventService.java @@ -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.io.IOException; + +import org.apache.solr.client.solrj.SolrServerException; +import org.dspace.qaevent.service.impl.QAEventServiceImpl; +import org.dspace.solr.MockSolrServer; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.stereotype.Service; + +/** + * Mock SOLR service for the qaevents Core. + */ +@Service +public class MockQAEventService extends QAEventServiceImpl implements InitializingBean, DisposableBean { + private MockSolrServer mockSolrServer; + + @Override + public void afterPropertiesSet() throws Exception { + mockSolrServer = new MockSolrServer("qaevent"); + solr = mockSolrServer.getSolrServer(); + } + + /** Clear all records from the search core. */ + public void reset() { + mockSolrServer.reset(); + try { + mockSolrServer.getSolrServer().commit(); + } catch (SolrServerException | IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void destroy() throws Exception { + mockSolrServer.destroy(); + } +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/qaevent/script/OpenaireEventsImportIT.java b/dspace-api/src/test/java/org/dspace/qaevent/script/OpenaireEventsImportIT.java new file mode 100644 index 00000000000..6bb979f48be --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/qaevent/script/OpenaireEventsImportIT.java @@ -0,0 +1,488 @@ +/** + * 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 java.util.List.of; +import static org.dspace.content.QAEvent.OPENAIRE_SOURCE; +import static org.dspace.matcher.QAEventMatcher.pendingOpenaireEventWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsString; +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.io.File; +import java.io.FileInputStream; +import java.io.OutputStream; +import java.net.URL; + +import eu.dnetlib.broker.BrokerClient; +import org.apache.commons.io.IOUtils; +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.ItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.matcher.QASourceMatcher; +import org.dspace.matcher.QATopicMatcher; +import org.dspace.qaevent.service.OpenaireClientFactory; +import org.dspace.qaevent.service.QAEventService; +import org.dspace.qaevent.service.impl.OpenaireClientFactoryImpl; +import org.dspace.utils.DSpace; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration tests for {@link OpenaireEventsImport}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class OpenaireEventsImportIT extends AbstractIntegrationTestWithDatabase { + + private static final String BASE_JSON_DIR_PATH = "org/dspace/app/openaire-events/"; + + private static final String ORDER_FIELD = "topic"; + + private QAEventService qaEventService = new DSpace().getSingletonService(QAEventService.class); + + private Collection collection; + + private BrokerClient brokerClient = OpenaireClientFactory.getInstance().getBrokerClient(); + + private BrokerClient mockBrokerClient = mock(BrokerClient.class); + + @Before + public void setup() { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + context.restoreAuthSystemState(); + + ((OpenaireClientFactoryImpl) OpenaireClientFactory.getInstance()).setBrokerClient(mockBrokerClient); + } + + @After + public void after() { + ((OpenaireClientFactoryImpl) OpenaireClientFactory.getInstance()).setBrokerClient(brokerClient); + } + + @Test + public void testWithoutParameters() throws Exception { + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getWarningMessages(), empty()); + assertThat(handler.getInfoMessages(), empty()); + + Exception exception = handler.getException(); + assertThat(exception, instanceOf(IllegalArgumentException.class)); + assertThat(exception.getMessage(), is("One parameter between the location of the file and the email " + + "must be entered to proceed with the import.")); + + verifyNoInteractions(mockBrokerClient); + } + + @Test + public void testWithBothFileAndEmailParameters() throws Exception { + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-f", getFileLocation("events.json"), + "-e", "test@user.com" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getWarningMessages(), empty()); + assertThat(handler.getInfoMessages(), empty()); + + Exception exception = handler.getException(); + assertThat(exception, instanceOf(IllegalArgumentException.class)); + assertThat(exception.getMessage(), is("Only one parameter between the location of the file and the email " + + "must be entered to proceed with the import.")); + + verifyNoInteractions(mockBrokerClient); + } + + @Test + @SuppressWarnings("unchecked") + public void testManyEventsImportFromFile() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item firstItem = createItem("Test item", "123456789/99998"); + Item secondItem = createItem("Test item 2", "123456789/99999"); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-f", getFileLocation("events.json") }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getWarningMessages(), empty()); + assertThat(handler.getInfoMessages(), contains( + "Trying to read the QA events from the provided file", + "Found 5 events in the given file")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 5L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), containsInAnyOrder( + QATopicMatcher.with("ENRICH/MORE/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MORE/PID", 1L), + QATopicMatcher.with("ENRICH/MISSING/PID", 1L), + QATopicMatcher.with("ENRICH/MISSING/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MISSING/ABSTRACT", 1L))); + + String projectMessage = "{\"projects[0].acronym\":\"PAThs\",\"projects[0].code\":\"687567\"," + + "\"projects[0].funder\":\"EC\",\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths\"}"; + + assertThat(qaEventService.findEventsByTopic("ENRICH/MORE/PROJECT"), contains( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99998", firstItem, + "Egypt, crossroad of translations and literary interweavings", projectMessage, + "ENRICH/MORE/PROJECT", 1.00d))); + + String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; + + assertThat(qaEventService.findEventsByTopic("ENRICH/MISSING/ABSTRACT"), contains( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", secondItem, "Test Publication", + abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + + verifyNoInteractions(mockBrokerClient); + + } + + @Test + public void testManyEventsImportFromFileWithUnknownHandle() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item item = createItem("Test item", "123456789/99999"); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-f", getFileLocation("events.json") }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getWarningMessages(), + contains("An error occurs storing the event with id b4e09c71312cd7c397969f56c900823f: " + + "Skipped event b4e09c71312cd7c397969f56c900823f related to the oai record " + + "oai:www.openstarts.units.it:123456789/99998 as the record was not found", + "An error occurs storing the event with id d050d2c4399c6c6ccf27d52d479d26e4: " + + "Skipped event d050d2c4399c6c6ccf27d52d479d26e4 related to the oai record " + + "oai:www.openstarts.units.it:123456789/99998 as the record was not found")); + assertThat(handler.getInfoMessages(), contains( + "Trying to read the QA events from the provided file", + "Found 5 events in the given file")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 3L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), containsInAnyOrder( + QATopicMatcher.with("ENRICH/MISSING/ABSTRACT", 1L), + QATopicMatcher.with("ENRICH/MISSING/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MORE/PID", 1L) + )); + + String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; + + assertThat(qaEventService.findEventsByTopic("ENRICH/MISSING/ABSTRACT"), contains( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", item, "Test Publication", + abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + + verifyNoInteractions(mockBrokerClient); + + } + + @Test + public void testManyEventsImportFromFileWithUnknownTopic() throws Exception { + + context.turnOffAuthorisationSystem(); + + createItem("Test item", "123456789/99999"); + Item secondItem = createItem("Test item 2", "123456789/999991"); + + context.restoreAuthSystemState(); + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-f", getFileLocation("unknown-topic-events.json") }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getWarningMessages(), + contains("Event for topic ENRICH/MORE/UNKNOWN is not allowed in the qaevents.cfg")); + assertThat(handler.getInfoMessages(), contains( + "Trying to read the QA events from the provided file", + "Found 2 events in the given file")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 1L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), + contains(QATopicMatcher.with("ENRICH/MISSING/ABSTRACT", 1L))); + + String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; + + assertThat(qaEventService.findEventsByTopic("ENRICH/MISSING/ABSTRACT"), contains( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/999991", secondItem, "Test Publication 2", + abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + + verifyNoInteractions(mockBrokerClient); + + } + + @Test + public void testImportFromFileWithoutEvents() throws Exception { + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-f", getFileLocation("empty-file.json") }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), + contains(containsString("A not recoverable error occurs during OPENAIRE events import"))); + assertThat(handler.getWarningMessages(),empty()); + assertThat(handler.getInfoMessages(), contains("Trying to read the QA events from the provided file")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 0L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), empty()); + + verifyNoInteractions(mockBrokerClient); + } + + @Test + @SuppressWarnings("unchecked") + public void testImportFromOpenaireBroker() throws Exception { + + context.turnOffAuthorisationSystem(); + + Item firstItem = createItem("Test item", "123456789/99998"); + Item secondItem = createItem("Test item 2", "123456789/99999"); + Item thirdItem = createItem("Test item 3", "123456789/999991"); + + context.restoreAuthSystemState(); + + URL openaireURL = new URL("http://api.openaire.eu/broker"); + + when(mockBrokerClient.listSubscriptions(openaireURL, "user@test.com")).thenReturn(of("sub1", "sub2", "sub3")); + + doAnswer(i -> writeToOutputStream(i.getArgument(2, OutputStream.class), "events.json")) + .when(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub1"), any()); + + doAnswer(i -> writeToOutputStream(i.getArgument(2, OutputStream.class), "empty-events-list.json")) + .when(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub2"), any()); + + doAnswer(i -> writeToOutputStream(i.getArgument(2, OutputStream.class), "unknown-topic-events.json")) + .when(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub3"), any()); + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-e", "user@test.com" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), empty()); + assertThat(handler.getWarningMessages(), + contains("Event for topic ENRICH/MORE/UNKNOWN is not allowed in the qaevents.cfg")); + assertThat(handler.getInfoMessages(), contains( + "Trying to read the QA events from the OPENAIRE broker", + "Found 3 subscriptions related to the given email", + "Found 5 events from the subscription sub1", + "Found 0 events from the subscription sub2", + "Found 2 events from the subscription sub3")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 6L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), containsInAnyOrder( + QATopicMatcher.with("ENRICH/MORE/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MORE/PID", 1L), + QATopicMatcher.with("ENRICH/MISSING/PID", 1L), + QATopicMatcher.with("ENRICH/MISSING/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MISSING/ABSTRACT", 2L))); + + String projectMessage = "{\"projects[0].acronym\":\"PAThs\",\"projects[0].code\":\"687567\"," + + "\"projects[0].funder\":\"EC\",\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths\"}"; + + assertThat(qaEventService.findEventsByTopic("ENRICH/MORE/PROJECT"), contains( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99998", firstItem, + "Egypt, crossroad of translations and literary interweavings", projectMessage, + "ENRICH/MORE/PROJECT", 1.00d))); + + String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; + + assertThat(qaEventService.findEventsByTopic("ENRICH/MISSING/ABSTRACT"), containsInAnyOrder( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", secondItem, "Test Publication", + abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d), + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/999991", thirdItem, "Test Publication 2", + abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + + verify(mockBrokerClient).listSubscriptions(openaireURL, "user@test.com"); + verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub1"), any()); + verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub2"), any()); + verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub3"), any()); + + verifyNoMoreInteractions(mockBrokerClient); + } + + @Test + public void testImportFromOpenaireBrokerWithErrorDuringListSubscription() throws Exception { + + URL openaireURL = new URL("http://api.openaire.eu/broker"); + + when(mockBrokerClient.listSubscriptions(openaireURL, "user@test.com")) + .thenThrow(new RuntimeException("Connection refused")); + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-e", "user@test.com" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), + contains("A not recoverable error occurs during OPENAIRE events import: Connection refused")); + assertThat(handler.getWarningMessages(), empty()); + assertThat(handler.getInfoMessages(), contains("Trying to read the QA events from the OPENAIRE broker")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 0L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), empty()); + + verify(mockBrokerClient).listSubscriptions(openaireURL, "user@test.com"); + + verifyNoMoreInteractions(mockBrokerClient); + + } + + @Test + @SuppressWarnings("unchecked") + public void testImportFromOpenaireBrokerWithErrorDuringEventsDownload() throws Exception { + + context.turnOffAuthorisationSystem(); + + createItem("Test item", "123456789/99998"); + createItem("Test item 2", "123456789/99999"); + createItem("Test item 3", "123456789/999991"); + + context.restoreAuthSystemState(); + + URL openaireURL = new URL("http://api.openaire.eu/broker"); + + when(mockBrokerClient.listSubscriptions(openaireURL, "user@test.com")).thenReturn(of("sub1", "sub2", "sub3")); + + doAnswer(i -> writeToOutputStream(i.getArgument(2, OutputStream.class), "events.json")) + .when(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub1"), any()); + + doThrow(new RuntimeException("Invalid subscription id")) + .when(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub2"), any()); + + doAnswer(i -> writeToOutputStream(i.getArgument(2, OutputStream.class), "unknown-topic-events.json")) + .when(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub3"), any()); + + TestDSpaceRunnableHandler handler = new TestDSpaceRunnableHandler(); + + String[] args = new String[] { "import-openaire-events", "-e", "user@test.com" }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(handler.getErrorMessages(), contains("An error occurs downloading the events " + + "related to the subscription sub2: Invalid subscription id")); + assertThat(handler.getWarningMessages(), + contains("Event for topic ENRICH/MORE/UNKNOWN is not allowed in the qaevents.cfg")); + assertThat(handler.getInfoMessages(), contains( + "Trying to read the QA events from the OPENAIRE broker", + "Found 3 subscriptions related to the given email", + "Found 5 events from the subscription sub1", + "Found 0 events from the subscription sub2", + "Found 2 events from the subscription sub3")); + + assertThat(qaEventService.findAllSources(0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 6L))); + + assertThat(qaEventService.findAllTopics(0, 20, ORDER_FIELD, false), containsInAnyOrder( + QATopicMatcher.with("ENRICH/MORE/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MISSING/PID", 1L), + QATopicMatcher.with("ENRICH/MORE/PID", 1L), + QATopicMatcher.with("ENRICH/MISSING/PROJECT", 1L), + QATopicMatcher.with("ENRICH/MISSING/ABSTRACT", 2L))); + + assertThat(qaEventService.findEventsByTopic("ENRICH/MORE/PROJECT"), hasSize(1)); + assertThat(qaEventService.findEventsByTopic("ENRICH/MISSING/ABSTRACT"), hasSize(2)); + + verify(mockBrokerClient).listSubscriptions(openaireURL, "user@test.com"); + verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub1"), any()); + verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub2"), any()); + verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub3"), any()); + + verifyNoMoreInteractions(mockBrokerClient); + + } + + private Item createItem(String title, String handle) { + return ItemBuilder.createItem(context, collection) + .withTitle(title) + .withHandle(handle) + .build(); + } + + private Void writeToOutputStream(OutputStream outputStream, String fileName) { + try { + byte[] fileContent = getFileContent(fileName); + IOUtils.write(fileContent, outputStream); + return null; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private byte[] getFileContent(String fileName) throws Exception { + String fileLocation = getFileLocation(fileName); + try (FileInputStream fis = new FileInputStream(new File(fileLocation))) { + return IOUtils.toByteArray(fis); + } + } + + private String getFileLocation(String fileName) throws Exception { + URL resource = getClass().getClassLoader().getResource(BASE_JSON_DIR_PATH + fileName); + if (resource == null) { + throw new IllegalStateException("No resource found named " + BASE_JSON_DIR_PATH + fileName); + } + return new File(resource.getFile()).getAbsolutePath(); + } +} diff --git a/dspace-api/src/test/java/org/dspace/util/RawJsonDeserializerTest.java b/dspace-api/src/test/java/org/dspace/util/RawJsonDeserializerTest.java new file mode 100644 index 00000000000..e1e6e246b99 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/util/RawJsonDeserializerTest.java @@ -0,0 +1,58 @@ +/** + * 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.is; +import static org.hamcrest.Matchers.notNullValue; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.junit.Test; + +/** + * Unit tests for {@link RawJsonDeserializer}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class RawJsonDeserializerTest { + + private String json = "" + + "{" + + " \"attribute\": {" + + " \"firstField\":\"value\"," + + " \"secondField\": 1" + + " }" + + "}"; + + @Test + public void testDeserialization() throws JsonMappingException, JsonProcessingException { + + ObjectMapper mapper = new ObjectMapper(); + + DeserializationTestClass object = mapper.readValue(json, DeserializationTestClass.class); + assertThat(object, notNullValue()); + assertThat(object.getAttribute(), is("{\"firstField\":\"value\",\"secondField\":1}")); + + } + + private static class DeserializationTestClass { + + @JsonDeserialize(using = RawJsonDeserializer.class) + private String attribute; + + public String getAttribute() { + return attribute; + } + + } + +} diff --git a/dspace-api/src/test/resources/org/dspace/app/openaire-events/empty-events-list.json b/dspace-api/src/test/resources/org/dspace/app/openaire-events/empty-events-list.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/app/openaire-events/empty-events-list.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/dspace-api/src/test/resources/org/dspace/app/openaire-events/empty-file.json b/dspace-api/src/test/resources/org/dspace/app/openaire-events/empty-file.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dspace-api/src/test/resources/org/dspace/app/openaire-events/events.json b/dspace-api/src/test/resources/org/dspace/app/openaire-events/events.json new file mode 100644 index 00000000000..9bb8daae36c --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/app/openaire-events/events.json @@ -0,0 +1,62 @@ +[ + + { + "originalId": "oai:www.openstarts.units.it:123456789/99998", + "title": "Egypt, crossroad of translations and literary interweavings", + "topic": "ENRICH/MORE/PROJECT", + "trust": 1.0, + "message": { + "projects[0].acronym": "PAThs", + "projects[0].code": "687567", + "projects[0].funder": "EC", + "projects[0].fundingProgram": "H2020", + "projects[0].jurisdiction": "EU", + "projects[0].openaireId": "40|corda__h2020::6e32f5eb912688f2424c68b851483ea4", + "projects[0].title": "Tracking Papyrus and Parchment Paths" + } + }, + + { + "originalId": "oai:www.openstarts.units.it:123456789/99999", + "title": "Test Publication", + "topic": "ENRICH/MISSING/ABSTRACT", + "trust": 1.0, + "message": { + "abstracts[0]": "Missing Abstract" + } + }, + { + "originalId": "oai:www.openstarts.units.it:123456789/99998", + "title": "Egypt, crossroad of translations and literary interweavings", + "topic": "ENRICH/MISSING/PID", + "trust": 1.0, + "message": { + "pids[0].type": "doi", + "pids[0].value": "10.13137/2282-572x/987" + } + }, + { + "originalId": "oai:www.openstarts.units.it:123456789/99999", + "title": "Test Publication", + "topic": "ENRICH/MORE/PID", + "trust": 0.375, + "message": { + "pids[0].type": "doi", + "pids[0].value": "987654" + } + }, + { + "originalId": "oai:www.openstarts.units.it:123456789/99999", + "title": "Test Publication", + "topic": "ENRICH/MISSING/PROJECT", + "trust": 1.0, + "message": { + "projects[0].acronym": "02.SNES missing project acronym", + "projects[0].code": "prjcode_snes", + "projects[0].funder": "02.SNES missing project funder", + "projects[0].fundingProgram": "02.SNES missing project fundingProgram", + "projects[0].jurisdiction": "02.SNES missing project jurisdiction", + "projects[0].title": "Project01" + } + } +] \ No newline at end of file diff --git a/dspace-api/src/test/resources/org/dspace/app/openaire-events/unknown-topic-events.json b/dspace-api/src/test/resources/org/dspace/app/openaire-events/unknown-topic-events.json new file mode 100644 index 00000000000..3caa72cf35b --- /dev/null +++ b/dspace-api/src/test/resources/org/dspace/app/openaire-events/unknown-topic-events.json @@ -0,0 +1,20 @@ +[ + + { + "originalId": "oai:www.openstarts.units.it:123456789/99998", + "title": "Egypt, crossroad of translations and literary interweavings (3rd-6th centuries). A reconsideration of earlier Coptic literature", + "topic": "ENRICH/MORE/UNKNOWN", + "trust": 1.0 + }, + + { + "originalId": "oai:www.openstarts.units.it:123456789/999991", + "title": "Test Publication 2", + "topic": "ENRICH/MISSING/ABSTRACT", + "trust": 1.0, + "message": { + "abstracts[0]": "Missing Abstract" + } + } + +] \ No newline at end of file diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRelatedRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRelatedRestController.java new file mode 100644 index 00000000000..538d99b3ceb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/QAEventRelatedRestController.java @@ -0,0 +1,143 @@ +/** + * 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; + +import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.UUID; + +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.model.hateoas.ItemResource; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.AuthorizeException; +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.service.QAEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.ControllerUtils; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This RestController will take care to manipulate the related item eventually + * associated with a qa event + * "/api/integration/qualityassuranceevents/{qaeventid}/related" + */ +@RestController +@RequestMapping("/api/" + QAEventRest.CATEGORY + "/qualityassuranceevents" + + REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG + "/related") +public class QAEventRelatedRestController { + + @Autowired + protected Utils utils; + + @Autowired + private ConverterService converterService; + + @Autowired + private ItemService itemService; + + @Autowired + private QAEventService qaEventService; + + /** + * This method associate an item to a qa event + * + * @param qaeventId The qa event id + * @param relatedItemUUID The uuid of the related item to associate with the qa event + * @return The related item + * @throws SQLException If something goes wrong + * @throws AuthorizeException If something goes wrong + */ + @PostMapping + @PreAuthorize("hasAuthority('ADMIN')") + public ResponseEntity> addRelatedItem(@PathVariable(name = "id") String qaeventId, + @RequestParam(name = "item") UUID relatedItemUUID) throws SQLException, AuthorizeException { + + Context context = ContextUtil.obtainCurrentRequestContext(); + + QAEvent qaevent = qaEventService.findEventByEventId(qaeventId); + if (qaevent == null) { + throw new ResourceNotFoundException("No such qa event: " + qaeventId); + } + + if (!qaEventService.isRelatedItemSupported(qaevent)) { + throw new UnprocessableEntityException("The given event does not supports a related item"); + } + + if (qaevent.getRelated() != null) { + throw new UnprocessableEntityException("The given event already has a related item"); + } + + Item relatedItem = itemService.find(context, relatedItemUUID); + if (relatedItem == null) { + throw new UnprocessableEntityException("The proposed related item was not found"); + } + + qaevent.setRelated(relatedItemUUID.toString()); + qaEventService.store(context, qaevent); + + ItemRest relatedItemRest = converterService.toRest(relatedItem, utils.obtainProjection()); + ItemResource itemResource = converterService.toResource(relatedItemRest); + + context.complete(); + + return ControllerUtils.toResponseEntity(HttpStatus.CREATED, new HttpHeaders(), itemResource); + } + + /** + * This method remove the association to a related item from a qa event + * + * @param qaeventId The qa event id + * @return The related item + * @throws SQLException If something goes wrong + * @throws AuthorizeException If something goes wrong + */ + @DeleteMapping + @PreAuthorize("hasAuthority('ADMIN')") + public ResponseEntity> removeRelatedItem(@PathVariable(name = "id") String qaeventId) + throws SQLException, AuthorizeException, IOException { + + Context context = ContextUtil.obtainCurrentRequestContext(); + QAEvent qaevent = qaEventService.findEventByEventId(qaeventId); + + if (qaevent == null) { + throw new ResourceNotFoundException("No such qa event: " + qaeventId); + } + + if (!qaEventService.isRelatedItemSupported(qaevent)) { + throw new UnprocessableEntityException("The given event does not supports a related item"); + } + + if (qaevent.getRelated() != null) { + qaevent.setRelated(null); + qaEventService.store(context, qaevent); + context.complete(); + } + + return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java index b82b4830753..03bea35597b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/RestResourceController.java @@ -572,6 +572,37 @@ public ResponseEntity> upload(H return uploadInternal(request, apiCategory, model, uuid, uploadfile); } + /** + * Called in POST, multipart, upload to a specific rest resource the file passed as "file" request parameter + * + * Note that the regular expression in the request mapping accept a String as identifier; + * + * @param request + * the http request + * @param apiCategory + * the api category + * @param model + * the rest model that identify the REST resource collection + * @param id + * the id of the specific rest resource + * @param uploadfile + * the file to upload + * @return the created resource + * @throws HttpRequestMethodNotSupportedException + */ + @RequestMapping(method = RequestMethod.POST, + value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG, + headers = "content-type=multipart/form-data") + public ResponseEntity> upload(HttpServletRequest request, + @PathVariable String apiCategory, + @PathVariable String model, + @PathVariable String id, + @RequestParam("file") MultipartFile + uploadfile) + throws HttpRequestMethodNotSupportedException { + return uploadInternal(request, apiCategory, model, id, uploadfile); + } + /** * Internal upload method. * @@ -684,6 +715,28 @@ public ResponseEntity> patch(HttpServletRequest request, return patchInternal(request, apiCategory, model, id, jsonNode); } + /** + * PATCH method, using operation on the resources following (JSON) Patch notation (https://tools.ietf + * .org/html/rfc6902) + * + * Note that the regular expression in the request mapping accept a UUID as identifier; + * + * @param request + * @param apiCategory + * @param model + * @param id + * @param jsonNode + * @return + * @throws HttpRequestMethodNotSupportedException + */ + @RequestMapping(method = RequestMethod.PATCH, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG) + public ResponseEntity> patch(HttpServletRequest request, @PathVariable String apiCategory, + @PathVariable String model, + @PathVariable String id, + @RequestBody(required = true) JsonNode jsonNode) { + return patchInternal(request, apiCategory, model, id, jsonNode); + } + /** * Internal patch method * @@ -711,9 +764,13 @@ public ResponseEntity> patchInt log.error(e.getMessage(), e); throw e; } - DSpaceResource result = converter.toResource(modelObject); - //TODO manage HTTPHeader - return ControllerUtils.toResponseEntity(HttpStatus.OK, new HttpHeaders(), result); + if (modelObject != null) { + DSpaceResource result = converter.toResource(modelObject); + //TODO manage HTTPHeader + return ControllerUtils.toResponseEntity(HttpStatus.OK, new HttpHeaders(), result); + } else { + return ControllerUtils.toEmptyResponse(HttpStatus.NO_CONTENT); + } } @@ -1036,6 +1093,17 @@ private String getEncodedParameterStringFromRequestParams( return uriComponentsBuilder.encode().build().toString(); } + /** + * Method to delete an entity by ID + * Note that the regular expression in the request mapping accept a number as identifier; + * + * @param request + * @param apiCategory + * @param model + * @param id + * @return + * @throws HttpRequestMethodNotSupportedException + */ @RequestMapping(method = RequestMethod.DELETE, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_DIGIT) public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, @PathVariable String model, @PathVariable Integer id) @@ -1050,6 +1118,13 @@ public ResponseEntity> delete(HttpServletRequest request, return deleteInternal(apiCategory, model, uuid); } + @RequestMapping(method = RequestMethod.DELETE, value = REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG) + public ResponseEntity> delete(HttpServletRequest request, @PathVariable String apiCategory, + @PathVariable String model, @PathVariable String id) + throws HttpRequestMethodNotSupportedException { + return deleteInternal(apiCategory, model, id); + } + /** * Internal method to delete resource. * diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/QAAuthorizationFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/QAAuthorizationFeature.java new file mode 100644 index 00000000000..332c7a5989f --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/QAAuthorizationFeature.java @@ -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.app.rest.authorization.impl; + +import java.sql.SQLException; + +import org.dspace.app.rest.authorization.AuthorizationFeature; +import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; +import org.dspace.app.rest.model.BaseObjectRest; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * The QA Event feature. It can be used to verify if Quality Assurance can be seen. + * + * Authorization is granted if the current user has READ permissions on the given bitstream. + */ +@Component +@AuthorizationFeatureDocumentation(name = QAAuthorizationFeature.NAME, + description = "It can be used to verify if the user can manage Quality Assurance events") +public class QAAuthorizationFeature implements AuthorizationFeature { + public final static String NAME = "canSeeQA"; + + @Autowired + private ConfigurationService configurationService; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException { + return configurationService.getBooleanProperty("qaevents.enabled", false); + } + + @Override + public String[] getSupportedTypes() { + return new String[]{ + SiteRest.CATEGORY + "." + SiteRest.NAME + }; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QAEventConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QAEventConverter.java new file mode 100644 index 00000000000..a32c0ddc99a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QAEventConverter.java @@ -0,0 +1,115 @@ +/** + * 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 java.text.DecimalFormat; +import javax.annotation.PostConstruct; + +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 org.dspace.app.rest.model.OpenaireQAEventMessageRest; +import org.dspace.app.rest.model.QAEventMessageRest; +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.QAEvent; +import org.dspace.qaevent.service.dto.OpenaireMessageDTO; +import org.dspace.qaevent.service.dto.QAMessageDTO; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation of {@link DSpaceConverter} that converts {@link QAEvent} to + * {@link QAEventRest}. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component +public class QAEventConverter implements DSpaceConverter { + + private static final String OPENAIRE_PID_HREF_PREFIX_PROPERTY = "qaevents.openaire.pid-href-prefix."; + + @Autowired + private ConfigurationService configurationService; + + private ObjectMapper jsonMapper; + + @PostConstruct + public void setup() { + jsonMapper = new JsonMapper(); + jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Override + public QAEventRest convert(QAEvent modelObject, Projection projection) { + QAEventRest rest = new QAEventRest(); + rest.setId(modelObject.getEventId()); + try { + rest.setMessage(convertMessage(jsonMapper.readValue(modelObject.getMessage(), + modelObject.getMessageDtoClass()))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + rest.setOriginalId(modelObject.getOriginalId()); + rest.setProjection(projection); + rest.setTitle(modelObject.getTitle()); + rest.setTopic(modelObject.getTopic()); + rest.setEventDate(modelObject.getLastUpdate()); + rest.setTrust(new DecimalFormat("0.000").format(modelObject.getTrust())); + // right now only the pending status can be found in persisted qa events + rest.setStatus(modelObject.getStatus()); + return rest; + } + + private QAEventMessageRest convertMessage(QAMessageDTO dto) { + if (dto instanceof OpenaireMessageDTO) { + return convertOpenaireMessage(dto); + } + throw new IllegalArgumentException("Unknown message type: " + dto.getClass()); + } + + private QAEventMessageRest convertOpenaireMessage(QAMessageDTO dto) { + OpenaireMessageDTO openaireDto = (OpenaireMessageDTO) dto; + OpenaireQAEventMessageRest message = new OpenaireQAEventMessageRest(); + message.setAbstractValue(openaireDto.getAbstracts()); + message.setOpenaireId(openaireDto.getOpenaireId()); + message.setAcronym(openaireDto.getAcronym()); + message.setCode(openaireDto.getCode()); + message.setFunder(openaireDto.getFunder()); + message.setFundingProgram(openaireDto.getFundingProgram()); + message.setJurisdiction(openaireDto.getJurisdiction()); + message.setTitle(openaireDto.getTitle()); + message.setType(openaireDto.getType()); + message.setValue(openaireDto.getValue()); + message.setPidHref(calculateOpenairePidHref(openaireDto.getType(), openaireDto.getValue())); + return message; + } + + private String calculateOpenairePidHref(String type, String value) { + if (type == null) { + return null; + } + + String hrefType = type.toLowerCase(); + if (!configurationService.hasProperty(OPENAIRE_PID_HREF_PREFIX_PROPERTY + hrefType)) { + return null; + } + + String hrefPrefix = configurationService.getProperty(OPENAIRE_PID_HREF_PREFIX_PROPERTY + hrefType, ""); + return hrefPrefix + value; + } + + @Override + public Class getModelClass() { + return QAEvent.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QASourceConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QASourceConverter.java new file mode 100644 index 00000000000..6c1bc0d66cb --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QASourceConverter.java @@ -0,0 +1,40 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.converter; + +import org.dspace.app.rest.model.QASourceRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.qaevent.QASource; +import org.springframework.stereotype.Component; + +/** + * Implementation of {@link DSpaceConverter} that converts {@link QASource} to + * {@link QASourceRest}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component +public class QASourceConverter implements DSpaceConverter { + + @Override + public Class getModelClass() { + return QASource.class; + } + + @Override + public QASourceRest convert(QASource modelObject, Projection projection) { + QASourceRest rest = new QASourceRest(); + rest.setProjection(projection); + rest.setId(modelObject.getName()); + rest.setLastEvent(modelObject.getLastEvent()); + rest.setTotalEvents(modelObject.getTotalEvents()); + return rest; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QATopicConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QATopicConverter.java new file mode 100644 index 00000000000..efa32baba22 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/QATopicConverter.java @@ -0,0 +1,41 @@ +/** + * 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.QATopicRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.qaevent.QATopic; +import org.springframework.stereotype.Component; + +/** + * Implementation of {@link DSpaceConverter} that converts {@link QATopic} to + * {@link QATopicRest}. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component +public class QATopicConverter implements DSpaceConverter { + + @Override + public Class getModelClass() { + return QATopic.class; + } + + @Override + public QATopicRest convert(QATopic modelObject, Projection projection) { + QATopicRest rest = new QATopicRest(); + rest.setProjection(projection); + rest.setId(modelObject.getKey().replace("/", "!")); + rest.setName(modelObject.getKey()); + rest.setLastEvent(modelObject.getLastEvent()); + rest.setTotalEvents(modelObject.getTotalEvents()); + return rest; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OpenaireQAEventMessageRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OpenaireQAEventMessageRest.java new file mode 100644 index 00000000000..53976b49b64 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/OpenaireQAEventMessageRest.java @@ -0,0 +1,115 @@ +/** + * 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 com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Implementation of {@link QAEventMessageRest} related to OPENAIRE events. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class OpenaireQAEventMessageRest implements QAEventMessageRest { + + // pids + private String type; + + private String value; + + private String pidHref; + + // abstract + @JsonProperty(value = "abstract") + private String abstractValue; + + // project + private String openaireId; + + private String acronym; + + private String code; + + private String funder; + + private String fundingProgram; + + private String jurisdiction; + + private String title; + + public String getType() { + return type; + } + public void setType(String type) { + this.type = type; + } + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } + public String getAbstractValue() { + return abstractValue; + } + public void setAbstractValue(String abstractValue) { + this.abstractValue = abstractValue; + } + public String getOpenaireId() { + return openaireId; + } + public void setOpenaireId(String openaireId) { + this.openaireId = openaireId; + } + 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 getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } + + public String getPidHref() { + return pidHref; + } + + public void setPidHref(String pidHref) { + this.pidHref = pidHref; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QAEventMessageRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QAEventMessageRest.java new file mode 100644 index 00000000000..cc460f604c6 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QAEventMessageRest.java @@ -0,0 +1,18 @@ +/** + * 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; + +/** + * Interface for classes that model a message with the details of a QA event. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public interface QAEventMessageRest { + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QAEventRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QAEventRest.java new file mode 100644 index 00000000000..7a12ade61d8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QAEventRest.java @@ -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.rest.model; + +import java.util.Date; + +import org.dspace.app.rest.RestResourceController; + +/** + * QA event Rest object. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@LinksRest( + links = { + @LinkRest(name = "topic", method = "getTopic"), + @LinkRest(name = "target", method = "getTarget"), + @LinkRest(name = "related", method = "getRelated") + }) +public class QAEventRest extends BaseObjectRest { + + private static final long serialVersionUID = -5001130073350654793L; + public static final String NAME = "qualityassuranceevent"; + public static final String CATEGORY = RestAddressableModel.INTEGRATION; + + public static final String TOPIC = "topic"; + public static final String TARGET = "target"; + public static final String RELATED = "related"; + private String source; + private String originalId; + private String title; + private String topic; + private String trust; + private Date eventDate; + private QAEventMessageRest message; + private String status; + + @Override + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + 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 String getTrust() { + return trust; + } + + public void setTrust(String trust) { + this.trust = trust; + } + + public Date getEventDate() { + return eventDate; + } + + public void setEventDate(Date eventDate) { + this.eventDate = eventDate; + } + + public QAEventMessageRest getMessage() { + return message; + } + + public void setMessage(QAEventMessageRest message) { + this.message = message; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + /** + * @return the source + */ + public String getSource() { + return source; + } + + /** + * @param source the source to set + */ + public void setSource(String source) { + this.source = source; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QASourceRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QASourceRest.java new file mode 100644 index 00000000000..a1480f409cd --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QASourceRest.java @@ -0,0 +1,69 @@ +/** + * 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 org.dspace.app.rest.RestResourceController; + +/** + * REST Representation of a quality assurance broker source + * + * @author Luca Giamminonni (luca.giamminonni at 4Science) + * + */ +public class QASourceRest extends BaseObjectRest { + + private static final long serialVersionUID = -7455358581579629244L; + + public static final String NAME = "qualityassurancesource"; + public static final String CATEGORY = RestAddressableModel.INTEGRATION; + + private String id; + private Date lastEvent; + private long totalEvents; + + @Override + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public Date getLastEvent() { + return lastEvent; + } + + public void setLastEvent(Date lastEvent) { + this.lastEvent = lastEvent; + } + + public long getTotalEvents() { + return totalEvents; + } + + public void setTotalEvents(long totalEvents) { + this.totalEvents = totalEvents; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QATopicRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QATopicRest.java new file mode 100644 index 00000000000..05820b91948 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/QATopicRest.java @@ -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.app.rest.model; + +import java.util.Date; + +import org.dspace.app.rest.RestResourceController; + +/** + * REST Representation of a quality assurance broker topic + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +public class QATopicRest extends BaseObjectRest { + + private static final long serialVersionUID = -7455358581579629244L; + + public static final String NAME = "qualityassurancetopic"; + public static final String CATEGORY = RestAddressableModel.INTEGRATION; + + private String id; + private String name; + private Date lastEvent; + private long totalEvents; + + @Override + public String getType() { + return NAME; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return RestResourceController.class; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getLastEvent() { + return lastEvent; + } + + public void setLastEvent(Date lastEvent) { + this.lastEvent = lastEvent; + } + + public long getTotalEvents() { + return totalEvents; + } + + public void setTotalEvents(long totalEvents) { + this.totalEvents = totalEvents; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QAEventResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QAEventResource.java new file mode 100644 index 00000000000..43e1584a254 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QAEventResource.java @@ -0,0 +1,27 @@ +/** + * 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.hateoas; + +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * QA event Rest resource. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@RelNameDSpaceResource(QAEventRest.NAME) +public class QAEventResource extends DSpaceResource { + + public QAEventResource(QAEventRest data, Utils utils) { + super(data, utils); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QASourceResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QASourceResource.java new file mode 100644 index 00000000000..860e16cee3c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QASourceResource.java @@ -0,0 +1,27 @@ +/** + * 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.hateoas; + +import org.dspace.app.rest.model.QASourceRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * QA source Rest resource. + * + * @author Luca Giamminonni (luca.giamminonni at 4Science) + * + */ +@RelNameDSpaceResource(QASourceRest.NAME) +public class QASourceResource extends DSpaceResource { + + public QASourceResource(QASourceRest data, Utils utils) { + super(data, utils); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QATopicResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QATopicResource.java new file mode 100644 index 00000000000..139d1e59ebc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/QATopicResource.java @@ -0,0 +1,27 @@ +/** + * 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.hateoas; + +import org.dspace.app.rest.model.QATopicRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * QA topic Rest resource. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@RelNameDSpaceResource(QATopicRest.NAME) +public class QATopicResource extends DSpaceResource { + + public QATopicResource(QATopicRest data, Utils utils) { + super(data, utils); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRelatedLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRelatedLinkRepository.java new file mode 100644 index 00000000000..c711d2ec376 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRelatedLinkRepository.java @@ -0,0 +1,77 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository; + +import java.sql.SQLException; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.projection.Projection; +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.service.QAEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository for "related" subresource of a qa event. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component(QAEventRest.CATEGORY + "." + QAEventRest.NAME + "." + QAEventRest.RELATED) +public class QAEventRelatedLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private QAEventService qaEventService; + + @Autowired + private ItemService itemService; + + /** + * Returns the item related to the qa event with the given id. This is another + * item that should be linked to the target item as part of the correction + * + * @param request the http servlet request + * @param id the qa event id + * @param pageable the optional pageable + * @param projection the projection object + * @return the item rest representation of the secondary item related to qa event + */ + @PreAuthorize("hasAuthority('ADMIN')") + public ItemRest getRelated(@Nullable HttpServletRequest request, String id, @Nullable Pageable pageable, + Projection projection) { + Context context = obtainContext(); + QAEvent qaEvent = qaEventService.findEventByEventId(id); + if (qaEvent == null) { + throw new ResourceNotFoundException("No qa event with ID: " + id); + } + if (qaEvent.getRelated() == null) { + return null; + } + UUID itemUuid = UUID.fromString(qaEvent.getRelated()); + Item item; + try { + item = itemService.find(context, itemUuid); + if (item == null) { + throw new ResourceNotFoundException("No related item found with id : " + id); + } + return converter.toRest(item, projection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java new file mode 100644 index 00000000000..dc0654ee494 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventRestRepository.java @@ -0,0 +1,134 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository; + +import java.sql.SQLException; +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.repository.patch.ResourcePatch; +import org.dspace.authorize.AuthorizeException; +import org.dspace.content.Item; +import org.dspace.content.QAEvent; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.dspace.qaevent.dao.QAEventsDAO; +import org.dspace.qaevent.service.QAEventService; +import org.dspace.util.UUIDUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Rest repository that handle QA events. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component(QAEventRest.CATEGORY + "." + QAEventRest.NAME) +public class QAEventRestRepository extends DSpaceRestRepository { + + final static String ORDER_FIELD = "trust"; + + @Autowired + private QAEventService qaEventService; + + @Autowired + private QAEventsDAO qaEventDao; + + @Autowired + private ItemService itemService; + + @Autowired + private ResourcePatch resourcePatch; + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public QAEventRest findOne(Context context, String id) { + QAEvent qaEvent = qaEventService.findEventByEventId(id); + if (qaEvent == null) { + // check if this request is part of a patch flow + qaEvent = (QAEvent) requestService.getCurrentRequest().getAttribute("patchedNotificationEvent"); + if (qaEvent != null && qaEvent.getEventId().contentEquals(id)) { + return converter.toRest(qaEvent, utils.obtainProjection()); + } else { + return null; + } + } + return converter.toRest(qaEvent, utils.obtainProjection()); + } + + @SearchRestMethod(name = "findByTopic") + @PreAuthorize("hasAuthority('ADMIN')") + public Page findByTopic(Context context, @Parameter(value = "topic", required = true) String topic, + Pageable pageable) { + List qaEvents = null; + long count = 0L; + boolean ascending = false; + if (pageable.getSort() != null && pageable.getSort().getOrderFor(ORDER_FIELD) != null) { + ascending = pageable.getSort().getOrderFor(ORDER_FIELD).getDirection() == Direction.ASC; + } + qaEvents = qaEventService.findEventsByTopicAndPage(topic, + pageable.getOffset(), pageable.getPageSize(), ORDER_FIELD, ascending); + count = qaEventService.countEventsByTopic(topic); + if (qaEvents == null) { + return null; + } + return converter.toRestPage(qaEvents, pageable, count, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected void delete(Context context, String eventId) throws AuthorizeException { + Item item = findTargetItem(context, eventId); + EPerson eperson = context.getCurrentUser(); + qaEventService.deleteEventByEventId(eventId); + qaEventDao.storeEvent(context, eventId, eperson, item); + } + + @Override + public Page findAll(Context context, Pageable pageable) { + throw new RepositoryMethodNotImplementedException(QAEventRest.NAME, "findAll"); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, + String id, Patch patch) throws SQLException, AuthorizeException { + QAEvent qaEvent = qaEventService.findEventByEventId(id); + resourcePatch.patch(context, qaEvent, patch.getOperations()); + } + + private Item findTargetItem(Context context, String eventId) { + QAEvent qaEvent = qaEventService.findEventByEventId(eventId); + if (qaEvent == null) { + return null; + } + + try { + return itemService.find(context, UUIDUtils.fromString(qaEvent.getTarget())); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + @Override + public Class getDomainClass() { + return QAEventRest.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventTargetLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventTargetLinkRepository.java new file mode 100644 index 00000000000..2df3836b9bd --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventTargetLinkRepository.java @@ -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.app.rest.repository; + +import java.sql.SQLException; +import java.util.UUID; +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.ItemRest; +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.projection.Projection; +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.service.QAEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository for "target" subresource of a qa event. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component(QAEventRest.CATEGORY + "." + QAEventRest.NAME + "." + QAEventRest.TARGET) +public class QAEventTargetLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private QAEventService qaEventService; + + @Autowired + private ItemService itemService; + + /** + * Returns the item target of the qa event with the given id. + * + * @param request the http servlet request + * @param id the qa event id + * @param pageable the optional pageable + * @param projection the projection object + * @return the item rest representation of the qa event target + */ + @PreAuthorize("hasAuthority('ADMIN')") + public ItemRest getTarget(@Nullable HttpServletRequest request, String id, @Nullable Pageable pageable, + Projection projection) { + Context context = obtainContext(); + QAEvent qaEvent = qaEventService.findEventByEventId(id); + if (qaEvent == null) { + throw new ResourceNotFoundException("No qa event with ID: " + id); + } + UUID itemUuid = UUID.fromString(qaEvent.getTarget()); + Item item; + try { + item = itemService.find(context, itemUuid); + if (item == null) { + throw new ResourceNotFoundException("No target item found with id : " + id); + } + return converter.toRest(item, projection); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventTopicLinkRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventTopicLinkRepository.java new file mode 100644 index 00000000000..f9ed4844287 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QAEventTopicLinkRepository.java @@ -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.app.rest.repository; + +import javax.annotation.Nullable; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.QAEventRest; +import org.dspace.app.rest.model.QATopicRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.QAEvent; +import org.dspace.core.Context; +import org.dspace.qaevent.QATopic; +import org.dspace.qaevent.service.QAEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Link repository for "topic" subresource of a qa event. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component(QAEventRest.CATEGORY + "." + QAEventRest.NAME + "." + QAEventRest.TOPIC) +public class QAEventTopicLinkRepository extends AbstractDSpaceRestRepository implements LinkRestRepository { + + @Autowired + private QAEventService qaEventService; + + /** + * Returns the topic of the qa event with the given id. + * + * @param request the http servlet request + * @param id the qa event id + * @param pageable the optional pageable + * @param projection the projection object + * @return the qa topic rest representation + */ + @PreAuthorize("hasAuthority('ADMIN')") + public QATopicRest getTopic(@Nullable HttpServletRequest request, String id, @Nullable Pageable pageable, + Projection projection) { + Context context = obtainContext(); + QAEvent qaEvent = qaEventService.findEventByEventId(id); + if (qaEvent == null) { + throw new ResourceNotFoundException("No qa event with ID: " + id); + } + QATopic topic = qaEventService.findTopicByTopicId(qaEvent.getTopic().replace("/", "!")); + if (topic == null) { + throw new ResourceNotFoundException("No topic found with id : " + id); + } + return converter.toRest(topic, projection); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QASourceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QASourceRestRepository.java new file mode 100644 index 00000000000..dad2310a77c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QASourceRestRepository.java @@ -0,0 +1,58 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository; + +import java.util.List; + +import org.dspace.app.rest.model.QASourceRest; +import org.dspace.core.Context; +import org.dspace.qaevent.QASource; +import org.dspace.qaevent.service.QAEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Rest repository that handle QA sources. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +@Component(QASourceRest.CATEGORY + "." + QASourceRest.NAME) +public class QASourceRestRepository extends DSpaceRestRepository { + + @Autowired + private QAEventService qaEventService; + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public QASourceRest findOne(Context context, String id) { + QASource qaSource = qaEventService.findSource(id); + if (qaSource == null) { + return null; + } + return converter.toRest(qaSource, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public Page findAll(Context context, Pageable pageable) { + List qaSources = qaEventService.findAllSources(pageable.getOffset(), pageable.getPageSize()); + long count = qaEventService.countSources(); + return converter.toRestPage(qaSources, pageable, count, utils.obtainProjection()); + } + + + @Override + public Class getDomainClass() { + return QASourceRest.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QATopicRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QATopicRestRepository.java new file mode 100644 index 00000000000..97a0ee97812 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/QATopicRestRepository.java @@ -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.repository; + +import java.util.List; + +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.model.QATopicRest; +import org.dspace.core.Context; +import org.dspace.qaevent.QATopic; +import org.dspace.qaevent.service.QAEventService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Component; + +/** + * Rest repository that handle QA topics. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component(QATopicRest.CATEGORY + "." + QATopicRest.NAME) +public class QATopicRestRepository extends DSpaceRestRepository { + + final static String ORDER_FIELD = "topic"; + + @Autowired + private QAEventService qaEventService; + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public QATopicRest findOne(Context context, String id) { + QATopic topic = qaEventService.findTopicByTopicId(id); + if (topic == null) { + return null; + } + return converter.toRest(topic, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public Page findAll(Context context, Pageable pageable) { + boolean ascending = false; + if (pageable.getSort() != null && pageable.getSort().getOrderFor(ORDER_FIELD) != null) { + ascending = pageable.getSort() + .getOrderFor(ORDER_FIELD).getDirection() == Direction.ASC; + } + List topics = qaEventService.findAllTopics(pageable.getOffset(), pageable.getPageSize(), + ORDER_FIELD, ascending); + long count = qaEventService.countTopics(); + if (topics == null) { + return null; + } + return converter.toRestPage(topics, pageable, count, utils.obtainProjection()); + } + + @SearchRestMethod(name = "bySource") + @PreAuthorize("hasAuthority('ADMIN')") + public Page findBySource(Context context, + @Parameter(value = "source", required = true) String source, Pageable pageable) { + boolean ascending = false; + if (pageable.getSort() != null && pageable.getSort().getOrderFor(ORDER_FIELD) != null) { + ascending = pageable.getSort().getOrderFor(ORDER_FIELD).getDirection() == Direction.ASC; + } + List topics = qaEventService.findAllTopicsBySource(source, + pageable.getOffset(), pageable.getPageSize(), ORDER_FIELD, ascending); + long count = qaEventService.countTopicsBySource(source); + if (topics == null) { + return null; + } + return converter.toRestPage(topics, pageable, count, utils.obtainProjection()); + } + + @Override + public Class getDomainClass() { + return QATopicRest.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/QAEventStatusReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/QAEventStatusReplaceOperation.java new file mode 100644 index 00000000000..5f6ff02accd --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/QAEventStatusReplaceOperation.java @@ -0,0 +1,60 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation; + +import java.sql.SQLException; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.content.QAEvent; +import org.dspace.core.Context; +import org.dspace.qaevent.service.QAEventActionService; +import org.dspace.services.RequestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Replace operation related to the {@link QAEvent} status. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +@Component +public class QAEventStatusReplaceOperation extends PatchOperation { + @Autowired + private RequestService requestService; + + @Autowired + private QAEventActionService qaEventActionService; + + @Override + public QAEvent perform(Context context, QAEvent qaevent, Operation operation) throws SQLException { + String value = (String) operation.getValue(); + if (StringUtils.equalsIgnoreCase(value, QAEvent.ACCEPTED)) { + qaEventActionService.accept(context, qaevent); + } else if (StringUtils.equalsIgnoreCase(value, QAEvent.REJECTED)) { + qaEventActionService.reject(context, qaevent); + } else if (StringUtils.equalsIgnoreCase(value, QAEvent.DISCARDED)) { + qaEventActionService.discard(context, qaevent); + } else { + throw new IllegalArgumentException( + "The received operation is not valid: " + operation.getPath() + " - " + value); + } + qaevent.setStatus(value.toUpperCase()); + // HACK, we need to store the temporary object in the request so that a subsequent find would get it + requestService.getCurrentRequest().setAttribute("patchedNotificationEvent", qaevent); + return qaevent; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return StringUtils.equals(operation.getOp(), "replace") && objectToMatch instanceof QAEvent && StringUtils + .containsAny(operation.getValue().toString().toLowerCase(), QAEvent.ACCEPTED, QAEvent.DISCARDED, + QAEvent.REJECTED); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java index 8739f6b8d57..8db5a74eefb 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/RegexUtils.java @@ -33,7 +33,7 @@ private RegexUtils(){} */ public static final String REGEX_REQUESTMAPPING_IDENTIFIER_AS_STRING_VERSION_STRONG = "/{id:^(?!^\\d+$)" + "(?!^[0-9a-fxA-FX]{8}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{4}-[0-9a-fxA-FX]{12}$)" - + "[\\w+\\-\\.:]+$+}"; + + "[\\w+\\-\\.:!]+$+}"; /** * Regular expression in the request mapping to accept number as identifier diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml index eca9acf79fd..e42b7386abe 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/item-submission.xml @@ -135,28 +135,28 @@ fake.workflow.readonly org.dspace.submit.step.SampleStep sample workflow --> - - + + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form @@ -259,13 +259,13 @@ - - + + - - + + @@ -274,17 +274,17 @@ - + - + - + - + - + - + diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml index 9db02440e4c..b045865fe07 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/external-openaire.xml @@ -4,8 +4,8 @@ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-lazy-init="true"> - + - - + + Project diff --git a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml index 4a91ef051e8..ea654ac55cd 100644 --- a/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml +++ b/dspace-server-webapp/src/test/data/dspaceFolder/config/spring/api/test-discovery.xml @@ -86,8 +86,8 @@ - - + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryVersioningIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryVersioningIT.java index 083b27d0e54..7b9cad70e91 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryVersioningIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/DiscoveryVersioningIT.java @@ -1057,8 +1057,8 @@ public void test_discoveryXml_personOrOrgunit_expectLatestVersionsOnly() throws } @Test - public void test_discoveryXml_openAIREFundingAgency_expectLatestVersionsOnly() throws Exception { - final String configuration = "openAIREFundingAgency"; + public void test_discoveryXml_openaireFundingAgency_expectLatestVersionsOnly() throws Exception { + final String configuration = "openaireFundingAgency"; Collection collection = createCollection("OrgUnit"); diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java index 65492cbbe86..2b03b50c559 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ExternalSourcesRestControllerIT.java @@ -51,7 +51,7 @@ public void findAllExternalSources() throws Exception { ExternalSourceMatcher.matchExternalSource( "pubmed", "pubmed", false), ExternalSourceMatcher.matchExternalSource( - "openAIREFunding", "openAIREFunding", false) + "openaireFunding", "openaireFunding", false) ))) .andExpect(jsonPath("$.page.totalElements", Matchers.is(10))); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OpenAIREFundingExternalSourcesIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OpenaireFundingExternalSourcesIT.java similarity index 83% rename from dspace-server-webapp/src/test/java/org/dspace/app/rest/OpenAIREFundingExternalSourcesIT.java rename to dspace-server-webapp/src/test/java/org/dspace/app/rest/OpenaireFundingExternalSourcesIT.java index b8f886b2e42..8c76c7adea0 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/OpenAIREFundingExternalSourcesIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/OpenaireFundingExternalSourcesIT.java @@ -19,7 +19,7 @@ import org.hamcrest.Matchers; import org.junit.Test; -public class OpenAIREFundingExternalSourcesIT extends AbstractControllerIntegrationTest { +public class OpenaireFundingExternalSourcesIT extends AbstractControllerIntegrationTest { /** * Test openaire funding external source @@ -27,10 +27,10 @@ public class OpenAIREFundingExternalSourcesIT extends AbstractControllerIntegrat * @throws Exception */ @Test - public void findOneOpenAIREFundingExternalSourceTest() throws Exception { + public void findOneOpenaireFundingExternalSourceTest() throws Exception { getClient().perform(get("/api/integration/externalsources")).andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.externalsources", Matchers.hasItem( - ExternalSourceMatcher.matchExternalSource("openAIREFunding", "openAIREFunding", false)))); + ExternalSourceMatcher.matchExternalSource("openaireFunding", "openaireFunding", false)))); } /** @@ -39,9 +39,9 @@ public void findOneOpenAIREFundingExternalSourceTest() throws Exception { * @throws Exception */ @Test - public void findOneOpenAIREFundingExternalSourceEntriesEmptyWithQueryTest() throws Exception { + public void findOneOpenaireFundingExternalSourceEntriesEmptyWithQueryTest() throws Exception { - getClient().perform(get("/api/integration/externalsources/openAIREFunding/entries").param("query", "empty")) + getClient().perform(get("/api/integration/externalsources/openaireFunding/entries").param("query", "empty")) .andExpect(status().isOk()).andExpect(jsonPath("$.page.number", is(0))); } @@ -52,11 +52,11 @@ public void findOneOpenAIREFundingExternalSourceEntriesEmptyWithQueryTest() thro * @throws Exception */ @Test - public void findOneOpenAIREFundingExternalSourceEntriesWithQueryMultipleKeywordsTest() throws Exception { + public void findOneOpenaireFundingExternalSourceEntriesWithQueryMultipleKeywordsTest() throws Exception { getClient() .perform( - get("/api/integration/externalsources/openAIREFunding/entries").param("query", "empty+results")) + get("/api/integration/externalsources/openaireFunding/entries").param("query", "empty+results")) .andExpect(status().isOk()).andExpect(jsonPath("$.page.number", is(0))); } @@ -66,14 +66,14 @@ public void findOneOpenAIREFundingExternalSourceEntriesWithQueryMultipleKeywords * @throws Exception */ @Test - public void findOneOpenAIREFundingExternalSourceEntriesWithQueryTest() throws Exception { - getClient().perform(get("/api/integration/externalsources/openAIREFunding/entries").param("query", "mushroom")) + public void findOneOpenaireFundingExternalSourceEntriesWithQueryTest() throws Exception { + getClient().perform(get("/api/integration/externalsources/openaireFunding/entries").param("query", "mushroom")) .andExpect(status().isOk()) .andExpect(jsonPath("$._embedded.externalSourceEntries", Matchers.hasItem(ExternalSourceEntryMatcher.matchExternalSourceEntry( "aW5mbzpldS1yZXBvL2dyYW50QWdyZWVtZW50L05XTy8rLzIzMDAxNDc3MjgvTkw=", "Master switches of initiation of mushroom formation", - "Master switches of initiation of mushroom formation", "openAIREFunding")))); + "Master switches of initiation of mushroom formation", "openaireFunding")))); } @@ -83,19 +83,19 @@ public void findOneOpenAIREFundingExternalSourceEntriesWithQueryTest() throws Ex * @throws Exception */ @Test - public void findOneOpenAIREFundingExternalSourceEntryValueTest() throws Exception { + public void findOneOpenaireFundingExternalSourceEntryValueTest() throws Exception { // "info:eu-repo/grantAgreement/mock/mock/mock/mock" base64 encoded String projectID = "aW5mbzpldS1yZXBvL2dyYW50QWdyZWVtZW50L0ZDVC81ODc2LVBQQ0RUSS8xMTAwNjIvUFQ="; String projectName = "Portuguese Wild Mushrooms: Chemical characterization and functional study" + " of antiproliferative and proapoptotic properties in cancer cell lines"; - getClient().perform(get("/api/integration/externalsources/openAIREFunding/entryValues/" + projectID)) + getClient().perform(get("/api/integration/externalsources/openaireFunding/entryValues/" + projectID)) .andExpect(status().isOk()) .andExpect(jsonPath("$", Matchers.allOf(hasJsonPath("$.id", is(projectID)), hasJsonPath("$.display", is(projectName)), hasJsonPath("$.value", is(projectName)), - hasJsonPath("$.externalSource", is("openAIREFunding")), + hasJsonPath("$.externalSource", is("openaireFunding")), hasJsonPath("$.type", is("externalSourceEntry"))))); } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java new file mode 100644 index 00000000000..592b6594576 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QAEventRestRepositoryIT.java @@ -0,0 +1,801 @@ +/** + * 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; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasNoJsonPath; +import static org.dspace.app.rest.matcher.QAEventMatcher.matchQAEventEntry; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.ArrayList; +import java.util.List; +import javax.ws.rs.core.MediaType; + +import org.dspace.app.rest.matcher.ItemMatcher; +import org.dspace.app.rest.matcher.QAEventMatcher; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EntityTypeBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.QAEventBuilder; +import org.dspace.builder.RelationshipTypeBuilder; +import org.dspace.content.Collection; +import org.dspace.content.EntityType; +import org.dspace.content.Item; +import org.dspace.content.QAEvent; +import org.dspace.content.QAEventProcessed; +import org.dspace.qaevent.dao.QAEventsDAO; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link QAEventRestRepository}. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +public class QAEventRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private QAEventsDAO qaEventsDao; + + @Test + public void findAllNotImplementedTest() throws Exception { + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/qualityassuranceevents")) + .andExpect(status().isMethodNotAllowed()); + String epersonToken = getAuthToken(admin.getEmail(), password); + getClient(epersonToken).perform(get("/api/integration/qualityassuranceevents")) + .andExpect(status().isMethodNotAllowed()); + getClient().perform(get("/api/integration/qualityassuranceevents")).andExpect(status().isMethodNotAllowed()); + } + + @Test + public void findOneTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event4 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event1.getEventId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event1))); + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event4.getEventId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event4))); + } + + @Test + public void findOneWithProjectionTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event5 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withTopic("ENRICH/MISSING/PROJECT") + .withMessage( + "{\"projects[0].acronym\":\"PAThs\"," + + "\"projects[0].code\":\"687567\"," + + "\"projects[0].funder\":\"EC\"," + + "\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths: " + + "An Archaeological Atlas of Coptic Literature." + + "\\nLiterary Texts in their Geographical Context: Production, Copying, Usage, " + + "Dissemination and Storage\"}") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event1.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event1))); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event5.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event5))); + } + + @Test + public void findOneUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + context.restoreAuthSystemState(); + getClient().perform(get("/api/integration/qualityassuranceevents/" + event1.getEventId())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event1.getEventId())) + .andExpect(status().isForbidden()); + } + + @Test + public void findByTopicTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEvent event4 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(2))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder(QAEventMatcher.matchQAEventEntry(event1), + QAEventMatcher.matchQAEventEntry(event2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", + "ENRICH!MISSING!ABSTRACT")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder(QAEventMatcher.matchQAEventEntry(event4)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "not-existing")) + .andExpect(status().isOk()).andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findByTopicPaginatedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEvent event4 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"pmc\",\"pids[0].value\":\"2144303\"}").build(); + QAEvent event5 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"2144304\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID") + .param("size", "2")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(2))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder( + QAEventMatcher.matchQAEventEntry(event1), + QAEventMatcher.matchQAEventEntry(event2)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=1"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=2"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=0"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.prev.href").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(3))) + .andExpect(jsonPath("$.page.totalElements", is(5))); + + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID") + .param("size", "2").param("page", "1")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(2))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder( + QAEventMatcher.matchQAEventEntry(event3), + QAEventMatcher.matchQAEventEntry(event4)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=1"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=2"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=2"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=0"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=0"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(3))) + .andExpect(jsonPath("$.page.totalElements", is(5))); + + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID") + .param("size", "2").param("page", "2")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder( + QAEventMatcher.matchQAEventEntry(event5)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=2"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.next.href").doesNotExist()) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=2"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=0"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=ENRICH!MISSING!PID"), Matchers.containsString("page=1"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(3))) + .andExpect(jsonPath("$.page.totalElements", is(5))); + + } + + @Test + public void findByTopicUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEvent event4 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); + context.restoreAuthSystemState(); + getClient() + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findByTopicForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEvent event4 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); + context.restoreAuthSystemState(); + String epersonToken = getAuthToken(eperson.getEmail(), password); + getClient(epersonToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID")) + .andExpect(status().isForbidden()); + } + + @Test + public void findByTopicBadRequestTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEvent event4 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); + getClient(adminToken).perform(get("/api/integration/qualityassuranceevents/search/findByTopic")) + .andExpect(status().isBadRequest()); + } + + @Test + public void recordDecisionTest() throws Exception { + context.turnOffAuthorisationSystem(); + EntityType publication = EntityTypeBuilder.createEntityTypeBuilder(context, "Publication").build(); + EntityType project = EntityTypeBuilder.createEntityTypeBuilder(context, "Project").build(); + RelationshipTypeBuilder.createRelationshipTypeBuilder(context, publication, project, "isProjectOfPublication", + "isPublicationOfProject", 0, null, 0, + null).withCopyToRight(true).build(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withEntityType("Publication") + .withName("Collection 1").build(); + Collection colFunding = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection Fundings") + .withEntityType("Project").build(); + Item funding = ItemBuilder.createItem(context, colFunding).withTitle("Tracking Papyrus and Parchment Paths") + .build(); + QAEvent eventProjectBound = QAEventBuilder.createTarget(context, col1, "Science and Freedom with project") + .withTopic("ENRICH/MISSING/PROJECT") + .withMessage( + "{\"projects[0].acronym\":\"PAThs\"," + + "\"projects[0].code\":\"687567\"," + + "\"projects[0].funder\":\"EC\"," + + "\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths: " + + "An Archaeological Atlas of Coptic Literature." + + "\\nLiterary Texts in their Geographical Context: Production, Copying, Usage, " + + "Dissemination and Storage\"}") + .withRelatedItem(funding.getID().toString()) + .build(); + QAEvent eventProjectNoBound = QAEventBuilder + .createTarget(context, col1, "Science and Freedom with unrelated project") + .withTopic("ENRICH/MISSING/PROJECT") + .withMessage( + "{\"projects[0].acronym\":\"NEW\"," + + "\"projects[0].code\":\"123456\"," + + "\"projects[0].funder\":\"EC\"," + + "\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"newProjectID\"," + + "\"projects[0].title\":\"A new project\"}") + .build(); + QAEvent eventMissingPID1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent eventMissingPID2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent eventMissingUnknownPID = QAEventBuilder.createTarget(context, col1, "Science and Freedom URN PID") + .withTopic("ENRICH/MISSING/PID") + .withMessage( + "{\"pids[0].type\":\"urn\",\"pids[0].value\":\"http://thesis2.sba.units.it/store/handle/item/12937\"}") + .build(); + QAEvent eventMorePID = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"2144302\"}").build(); + QAEvent eventAbstract = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"An abstract to add...\"}").build(); + QAEvent eventAbstractToDiscard = QAEventBuilder.createTarget(context, col1, "Science and Freedom 7") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage("{\"abstracts[0]\": \"Abstract to discard...\"}").build(); + context.restoreAuthSystemState(); + // prepare the different patches for our decisions + List acceptOp = new ArrayList(); + acceptOp.add(new ReplaceOperation("/status", QAEvent.ACCEPTED)); + List acceptOpUppercase = new ArrayList(); + acceptOpUppercase.add(new ReplaceOperation("/status", QAEvent.ACCEPTED)); + List discardOp = new ArrayList(); + discardOp.add(new ReplaceOperation("/status", QAEvent.DISCARDED)); + List rejectOp = new ArrayList(); + rejectOp.add(new ReplaceOperation("/status", QAEvent.REJECTED)); + String patchAccept = getPatchContent(acceptOp); + String patchAcceptUppercase = getPatchContent(acceptOpUppercase); + String patchDiscard = getPatchContent(discardOp); + String patchReject = getPatchContent(rejectOp); + + String authToken = getAuthToken(admin.getEmail(), password); + // accept pid1, unknownPID, morePID, the two projects and abstract + eventMissingPID1.setStatus(QAEvent.ACCEPTED); + eventMorePID.setStatus(QAEvent.ACCEPTED); + eventMissingUnknownPID.setStatus(QAEvent.ACCEPTED); + eventMissingUnknownPID.setStatus(QAEvent.ACCEPTED); + eventProjectBound.setStatus(QAEvent.ACCEPTED); + eventProjectNoBound.setStatus(QAEvent.ACCEPTED); + eventAbstract.setStatus(QAEvent.ACCEPTED); + + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + eventMissingPID1.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventMissingPID1))); + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + eventMorePID.getEventId()) + .content(patchAcceptUppercase) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventMorePID))); + getClient(authToken) + .perform(patch("/api/integration/qualityassuranceevents/" + eventMissingUnknownPID.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventMissingUnknownPID))); + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + eventProjectBound.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventProjectBound))); + getClient(authToken) + .perform(patch("/api/integration/qualityassuranceevents/" + eventProjectNoBound.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventProjectNoBound))); + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + eventAbstract.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventAbstract))); + // check if the item has been updated + getClient(authToken).perform(get("/api/core/items/" + eventMissingPID1.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect( + jsonPath("$", + hasJsonPath("$.metadata['dc.identifier.other'][0].value", is("10.2307/2144300")))); + getClient(authToken).perform(get("/api/core/items/" + eventMorePID.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasJsonPath("$.metadata['dc.identifier.other'][0].value", is("2144302")))); + getClient(authToken).perform(get("/api/core/items/" + eventMissingUnknownPID.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasJsonPath("$.metadata['dc.identifier.other'][0].value", + is("http://thesis2.sba.units.it/store/handle/item/12937")))); + getClient(authToken).perform(get("/api/core/items/" + eventProjectBound.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasJsonPath("$.metadata['relation.isProjectOfPublication'][0].value", + is(funding.getID().toString())))); + getClient(authToken).perform(get("/api/core/items/" + eventProjectNoBound.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasJsonPath("$.metadata['relation.isProjectOfPublication'][0].value", + is(not(empty()))))); + getClient(authToken).perform(get("/api/core/items/" + eventAbstract.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasJsonPath("$.metadata['dc.description.abstract'][0].value", is("An abstract to add...")))); + // reject pid2 + eventMissingPID2.setStatus(QAEvent.REJECTED); + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + eventMissingPID2.getEventId()) + .content(patchReject) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventMissingPID2))); + getClient(authToken).perform(get("/api/core/items/" + eventMissingPID2.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasNoJsonPath("$.metadata['dc.identifier.other']"))); + // discard abstractToDiscard + eventAbstractToDiscard.setStatus(QAEvent.DISCARDED); + getClient(authToken) + .perform(patch("/api/integration/qualityassuranceevents/" + eventAbstractToDiscard.getEventId()) + .content(patchDiscard) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(eventAbstractToDiscard))); + getClient(authToken).perform(get("/api/core/items/" + eventMissingPID2.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasNoJsonPath("$.metadata['dc.description.abstract']"))); + // no pending qa events should be longer available + getClient(authToken).perform(get("/api/integration/qualityassurancetopics")).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(0))); + // we should have stored the decision into the database as well + } + + @Test + public void setRelatedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Collection colFunding = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection Fundings").build(); + QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withTopic("ENRICH/MISSING/PROJECT") + .withMessage( + "{\"projects[0].acronym\":\"PAThs\"," + + "\"projects[0].code\":\"687567\"," + + "\"projects[0].funder\":\"EC\"," + + "\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths: " + + "An Archaeological Atlas of Coptic Literature." + + "\\nLiterary Texts in their Geographical Context: Production, Copying, Usage, " + + "Dissemination and Storage\"}") + .build(); + Item funding = ItemBuilder.createItem(context, colFunding).withTitle("Tracking Papyrus and Parchment Paths") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + + getClient(authToken) + .perform(post("/api/integration/qualityassuranceevents/" + event.getEventId() + "/related").param("item", + funding.getID().toString())) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", ItemMatcher.matchItemProperties(funding))); + // update our local event copy to reflect the association with the related item + event.setRelated(funding.getID().toString()); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId() + "/related")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", ItemMatcher.matchItemProperties(funding))); + } + + @Test + public void unsetRelatedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Collection colFunding = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection Fundings").build(); + Item funding = ItemBuilder.createItem(context, colFunding).withTitle("Tracking Papyrus and Parchment Paths") + .build(); + QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withTopic("ENRICH/MISSING/PROJECT") + .withMessage( + "{\"projects[0].acronym\":\"PAThs\"," + + "\"projects[0].code\":\"687567\"," + + "\"projects[0].funder\":\"EC\"," + + "\"projects[0].fundingProgram\":\"H2020\"," + + "\"projects[0].jurisdiction\":\"EU\"," + + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths: " + + "An Archaeological Atlas of Coptic Literature." + + "\\nLiterary Texts in their Geographical Context: Production, Copying, Usage, " + + "Dissemination and Storage\"}") + .withRelatedItem(funding.getID().toString()) + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + getClient(authToken) + .perform(delete("/api/integration/qualityassuranceevents/" + event.getEventId() + "/related")) + .andExpect(status().isNoContent()); + + // update our local event copy to reflect the association with the related item + event.setRelated(null); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId() + "/related")) + .andExpect(status().isNoContent()); + } + + @Test + public void setInvalidRelatedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Collection colFunding = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection Fundings").build(); + QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + Item funding = ItemBuilder.createItem(context, colFunding).withTitle("Tracking Papyrus and Parchment Paths") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + + getClient(authToken) + .perform(post("/api/integration/qualityassuranceevents/" + event.getEventId() + "/related").param("item", + funding.getID().toString())) + .andExpect(status().isUnprocessableEntity()); + // check that no related item has been added to our event + getClient(authToken) + .perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()).param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + } + + @Test + public void deleteItemWithEventTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(2))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder(QAEventMatcher.matchQAEventEntry(event1), + QAEventMatcher.matchQAEventEntry(event2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(authToken).perform(delete("/api/core/items/" + event1.getTarget())) + .andExpect(status().is(204)); + + getClient(authToken).perform(get("/api/core/items/" + event1.getTarget())) + .andExpect(status().is(404)); + + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic").param("topic", "ENRICH!MISSING!PID")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder( + QAEventMatcher.matchQAEventEntry(event2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void testEventDeletion() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}") + .build(); + + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchQAEventEntry(event))); + + List processedEvents = qaEventsDao.findAll(context); + assertThat(processedEvents, empty()); + + getClient(authToken).perform(delete("/api/integration/qualityassuranceevents/" + event.getEventId())) + .andExpect(status().isNoContent()); + + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId())) + .andExpect(status().isNotFound()); + + processedEvents = qaEventsDao.findAll(context); + assertThat(processedEvents, hasSize(1)); + + QAEventProcessed processedEvent = processedEvents.get(0); + assertThat(processedEvent.getEventId(), is(event.getEventId())); + assertThat(processedEvent.getItem(), notNullValue()); + assertThat(processedEvent.getItem().getID().toString(), is(event.getTarget())); + assertThat(processedEvent.getEventTimestamp(), notNullValue()); + assertThat(processedEvent.getEperson().getID(), is(admin.getID())); + + getClient(authToken).perform(delete("/api/integration/qualityassuranceevents/" + event.getEventId())) + .andExpect(status().isInternalServerError()); + + authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(delete("/api/integration/qualityassuranceevents/" + event2.getEventId())) + .andExpect(status().isForbidden()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/QASourceRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QASourceRestRepositoryIT.java new file mode 100644 index 00000000000..ac0ccc4ccec --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QASourceRestRepositoryIT.java @@ -0,0 +1,200 @@ +/** + * 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; + +import static org.dspace.app.rest.matcher.QASourceMatcher.matchQASourceEntry; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.builder.QAEventBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.QAEvent; +import org.dspace.services.ConfigurationService; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link QASourceRestRepository}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class QASourceRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + private Item target; + + @Before + public void setup() { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withTitle("Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection") + .build(); + + target = ItemBuilder.createItem(context, collection) + .withTitle("Item") + .build(); + + context.restoreAuthSystemState(); + + configurationService.setProperty("qaevent.sources", + new String[] { "openaire", "test-source", "test-source-2" }); + + } + + @Test + public void testFindAll() throws Exception { + + context.turnOffAuthorisationSystem(); + + createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 2"); + createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 3"); + + createEvent("test-source", "TOPIC/TEST/1", "Title 4"); + createEvent("test-source", "TOPIC/TEST/1", "Title 5"); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancesources")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancesources", contains( + matchQASourceEntry("openaire", 3), + matchQASourceEntry("test-source", 2), + matchQASourceEntry("test-source-2", 0)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(3))); + + } + + @Test + public void testFindAllForbidden() throws Exception { + + context.turnOffAuthorisationSystem(); + + createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent("test-source", "TOPIC/TEST/1", "Title 4"); + + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/integration/qualityassurancesources")) + .andExpect(status().isForbidden()); + + } + + @Test + public void testFindAllUnauthorized() throws Exception { + + context.turnOffAuthorisationSystem(); + + createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent("test-source", "TOPIC/TEST/1", "Title 4"); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/integration/qualityassurancesources")) + .andExpect(status().isUnauthorized()); + + } + + @Test + public void testFindOne() throws Exception { + + context.turnOffAuthorisationSystem(); + + createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 2"); + createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 3"); + + createEvent("test-source", "TOPIC/TEST/1", "Title 4"); + createEvent("test-source", "TOPIC/TEST/1", "Title 5"); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancesources/openaire")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchQASourceEntry("openaire", 3))); + + getClient(authToken).perform(get("/api/integration/qualityassurancesources/test-source")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchQASourceEntry("test-source", 2))); + + getClient(authToken).perform(get("/api/integration/qualityassurancesources/test-source-2")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchQASourceEntry("test-source-2", 0))); + + getClient(authToken).perform(get("/api/integration/qualityassurancesources/unknown-test-source")) + .andExpect(status().isNotFound()); + + } + + @Test + public void testFindOneForbidden() throws Exception { + + context.turnOffAuthorisationSystem(); + + createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent("test-source", "TOPIC/TEST/1", "Title 4"); + + context.restoreAuthSystemState(); + + String token = getAuthToken(eperson.getEmail(), password); + getClient(token).perform(get("/api/integration/qualityassurancesources/openaire")) + .andExpect(status().isForbidden()); + + } + + @Test + public void testFindOneUnauthorized() throws Exception { + + context.turnOffAuthorisationSystem(); + + createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent("test-source", "TOPIC/TEST/1", "Title 4"); + + context.restoreAuthSystemState(); + + getClient().perform(get("/api/integration/qualityassurancesources/openaire")) + .andExpect(status().isUnauthorized()); + + } + + private QAEvent createEvent(String source, String topic, String title) { + return QAEventBuilder.createTarget(context, target) + .withSource(source) + .withTopic(topic) + .withTitle(title) + .build(); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/QATopicRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QATopicRestRepositoryIT.java new file mode 100644 index 00000000000..55bfd0fecaf --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/QATopicRestRepositoryIT.java @@ -0,0 +1,277 @@ +/** + * 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; + +import static org.hamcrest.Matchers.is; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.matcher.QATopicMatcher; +import org.dspace.app.rest.repository.QATopicRestRepository; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.QAEventBuilder; +import org.dspace.content.Collection; +import org.dspace.services.ConfigurationService; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for {@link QATopicRestRepository}. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +public class QATopicRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + @Test + public void findAllTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage( + "{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics")).andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", + Matchers.containsInAnyOrder(QATopicMatcher.matchQATopicEntry("ENRICH/MISSING/PID", 2), + QATopicMatcher.matchQATopicEntry("ENRICH/MISSING/ABSTRACT", 1), + QATopicMatcher.matchQATopicEntry("ENRICH/MORE/PID", 1)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(3))); + + } + + @Test + public void findAllUnauthorizedTest() throws Exception { + getClient().perform(get("/api/integration/qualityassurancetopics")).andExpect(status().isUnauthorized()); + } + + @Test + public void findAllForbiddenTest() throws Exception { + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics")).andExpect(status().isForbidden()); + } + + @Test + public void findAllPaginationTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + //create collection + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage( + "{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics").param("size", "2")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", Matchers.hasSize(2))) + .andExpect(jsonPath("$.page.size", is(2))).andExpect(jsonPath("$.page.totalElements", is(3))); + getClient(authToken) + .perform(get("/api/integration/qualityassurancetopics") + .param("size", "2") + .param("page", "1")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", Matchers.hasSize(1))) + .andExpect(jsonPath("$.page.size", is(2))).andExpect(jsonPath("$.page.totalElements", is(3))); + } + + @Test + public void findOneTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage( + "{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!PID")) + .andExpect(jsonPath("$", QATopicMatcher.matchQATopicEntry("ENRICH/MISSING/PID", 2))); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!ABSTRACT")) + .andExpect(jsonPath("$", QATopicMatcher.matchQATopicEntry("ENRICH/MISSING/ABSTRACT", 1))); + } + + @Test + public void findOneUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID").build(); + context.restoreAuthSystemState(); + getClient().perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!PID")) + .andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!ABSTRACT")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!PID")) + .andExpect(status().isForbidden()); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!ABSTRACT")) + .andExpect(status().isForbidden()); + } + + @Test + public void findBySourceTest() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("qaevent.sources", + new String[] { "openaire", "test-source", "test-source-2" }); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic("ENRICH/MISSING/PID") + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic("ENRICH/MORE/PID") + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic("ENRICH/MISSING/ABSTRACT") + .withMessage( + "{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}") + .build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withTopic("TEST/TOPIC") + .withSource("test-source") + .build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 6") + .withTopic("TEST/TOPIC") + .withSource("test-source") + .build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 7") + .withTopic("TEST/TOPIC/2") + .withSource("test-source") + .build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", "openaire")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", + Matchers.containsInAnyOrder(QATopicMatcher.matchQATopicEntry("ENRICH/MISSING/PID", 2), + QATopicMatcher.matchQATopicEntry("ENRICH/MISSING/ABSTRACT", 1), + QATopicMatcher.matchQATopicEntry("ENRICH/MORE/PID", 1)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(3))); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", "test-source")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", + Matchers.containsInAnyOrder(QATopicMatcher.matchQATopicEntry("TEST/TOPIC/2", 1), + QATopicMatcher.matchQATopicEntry("TEST/TOPIC", 2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", "test-source-2")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findBySourceUnauthorizedTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID").build(); + context.restoreAuthSystemState(); + getClient().perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", "openaire")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findBySourceForbiddenTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withTopic("ENRICH/MISSING/PID").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", "openaire")) + .andExpect(status().isForbidden()); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/QAAuthorizationFeatureIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/QAAuthorizationFeatureIT.java new file mode 100644 index 00000000000..f3e508485a2 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/authorization/QAAuthorizationFeatureIT.java @@ -0,0 +1,84 @@ +/** + * 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.authorization; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.authorization.impl.QAAuthorizationFeature; +import org.dspace.app.rest.converter.SiteConverter; +import org.dspace.app.rest.matcher.AuthorizationMatcher; +import org.dspace.app.rest.model.SiteRest; +import org.dspace.app.rest.projection.DefaultProjection; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.content.Site; +import org.dspace.content.service.SiteService; +import org.dspace.services.ConfigurationService; +import org.hamcrest.Matchers; +import org.junit.Before; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Test suite for the Quality Assurance Authorization feature + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class QAAuthorizationFeatureIT extends AbstractControllerIntegrationTest { + + @Autowired + private AuthorizationFeatureService authorizationFeatureService; + + @Autowired + private SiteService siteService; + + @Autowired + private SiteConverter siteConverter; + + @Autowired + private ConfigurationService configurationService; + + + private AuthorizationFeature qaAuthorizationFeature; + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + qaAuthorizationFeature = authorizationFeatureService.find(QAAuthorizationFeature.NAME); + context.restoreAuthSystemState(); + } + + @Test + public void testQAAuthorizationSuccess() throws Exception { + configurationService.setProperty("qaevents.enabled", true); + Site site = siteService.findSite(context); + SiteRest siteRest = siteConverter.convert(site, DefaultProjection.DEFAULT); + String tokenAdmin = getAuthToken(admin.getEmail(), password); + Authorization authAdminSite = new Authorization(admin, qaAuthorizationFeature, siteRest); + + getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + authAdminSite.getID())) + .andExpect(jsonPath("$", Matchers.is( + AuthorizationMatcher.matchAuthorization(authAdminSite)))); + } + + @Test + public void testQAAuthorizationFail() throws Exception { + configurationService.setProperty("qaevents.enabled", false); + Site site = siteService.findSite(context); + SiteRest siteRest = siteConverter.convert(site, DefaultProjection.DEFAULT); + String tokenAdmin = getAuthToken(admin.getEmail(), password); + Authorization authAdminSite = new Authorization(admin, qaAuthorizationFeature, siteRest); + + getClient(tokenAdmin).perform(get("/api/authz/authorizations/" + authAdminSite.getID())) + .andExpect(status().isNotFound()); + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QAEventMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QAEventMatcher.java new file mode 100644 index 00000000000..b85746c64c6 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QAEventMatcher.java @@ -0,0 +1,126 @@ +/** + * 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.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.emptyOrNullString; +import static org.hamcrest.Matchers.is; + +import java.text.DecimalFormat; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.json.JsonMapper; +import org.apache.commons.lang3.StringUtils; +import org.dspace.content.QAEvent; +import org.dspace.qaevent.service.dto.OpenaireMessageDTO; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.hamcrest.core.IsAnything; + +/** + * Matcher related to {@link QAEventResource}. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +public class QAEventMatcher { + + private QAEventMatcher() { + } + + public static Matcher matchQAEventFullEntry(QAEvent event) { + return allOf( + matchQAEventEntry(event), + hasJsonPath("$._embedded.topic.name", is(event.getTopic())), + hasJsonPath("$._embedded.target.id", is(event.getTarget())), + event.getRelated() != null ? + hasJsonPath("$._embedded.related.id", is(event.getRelated())) : + hasJsonPath("$._embedded.related", is(emptyOrNullString())) + ); + } + + public static Matcher matchQAEventEntry(QAEvent event) { + try { + ObjectMapper jsonMapper = new JsonMapper(); + return allOf(hasJsonPath("$.id", is(event.getEventId())), + hasJsonPath("$.originalId", is(event.getOriginalId())), + hasJsonPath("$.title", is(event.getTitle())), + hasJsonPath("$.trust", is(new DecimalFormat("0.000").format(event.getTrust()))), + hasJsonPath("$.status", Matchers.equalToIgnoringCase(event.getStatus())), + hasJsonPath("$.message", + matchMessage(event.getTopic(), jsonMapper.readValue(event.getMessage(), + OpenaireMessageDTO.class))), + hasJsonPath("$._links.target.href", Matchers.endsWith(event.getEventId() + "/target")), + hasJsonPath("$._links.related.href", Matchers.endsWith(event.getEventId() + "/related")), + hasJsonPath("$._links.topic.href", Matchers.endsWith(event.getEventId() + "/topic")), + hasJsonPath("$.type", is("qualityassuranceevent"))); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private static Matcher matchMessage(String topic, OpenaireMessageDTO message) { + if (StringUtils.endsWith(topic, "/ABSTRACT")) { + return allOf(hasJsonPath("$.abstract", is(message.getAbstracts()))); + } else if (StringUtils.endsWith(topic, "/PID")) { + return allOf( + hasJsonPath("$.value", is(message.getValue())), + hasJsonPath("$.type", is(message.getType())), + hasJsonPath("$.pidHref", is(calculateOpenairePidHref(message.getType(), message.getValue())))); + } else if (StringUtils.endsWith(topic, "/PROJECT")) { + return allOf( + hasJsonPath("$.openaireId", is(message.getOpenaireId())), + hasJsonPath("$.acronym", is(message.getAcronym())), + hasJsonPath("$.code", is(message.getCode())), + hasJsonPath("$.funder", is(message.getFunder())), + hasJsonPath("$.fundingProgram", is(message.getFundingProgram())), + hasJsonPath("$.jurisdiction", is(message.getJurisdiction())), + hasJsonPath("$.title", is(message.getTitle()))); + } + return IsAnything.anything(); + } + + private static String calculateOpenairePidHref(String type, String value) { + if (type == null) { + return null; + } + + String hrefPrefix = null; + + switch (type) { + case "arxiv": + hrefPrefix = "https://arxiv.org/abs/"; + break; + case "handle": + hrefPrefix = "https://hdl.handle.net/"; + break; + case "urn": + hrefPrefix = ""; + break; + case "doi": + hrefPrefix = "https://doi.org/"; + break; + case "pmc": + hrefPrefix = "https://www.ncbi.nlm.nih.gov/pmc/articles/"; + break; + case "pmid": + hrefPrefix = "https://pubmed.ncbi.nlm.nih.gov/"; + break; + case "ncid": + hrefPrefix = "https://ci.nii.ac.jp/ncid/"; + break; + default: + break; + } + + return hrefPrefix != null ? hrefPrefix + value : null; + + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QASourceMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QASourceMatcher.java new file mode 100644 index 00000000000..c0466ee4083 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QASourceMatcher.java @@ -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.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; + +import org.dspace.app.rest.model.hateoas.QASourceResource; +import org.hamcrest.Matcher; + +/** + * Matcher related to {@link QASourceResource}. + * + * @author Luca Giamminonni (luca.giamminonni at 4science.it) + * + */ +public class QASourceMatcher { + + private QASourceMatcher() { } + + public static Matcher matchQASourceEntry(String key, int totalEvents) { + return allOf( + hasJsonPath("$.type", is("qualityassurancesource")), + hasJsonPath("$.id", is(key)), + hasJsonPath("$.totalEvents", is(totalEvents)) + ); + } + + + public static Matcher matchQASourceEntry(String key) { + return allOf( + hasJsonPath("$.type", is("qualityassurancesource")), + hasJsonPath("$.id", is(key)) + ); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QATopicMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QATopicMatcher.java new file mode 100644 index 00000000000..6428a971257 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/QATopicMatcher.java @@ -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.app.rest.matcher; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; + +import org.dspace.app.rest.model.hateoas.QATopicResource; +import org.hamcrest.Matcher; + +/** + * Matcher related to {@link QATopicResource}. + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + * + */ +public class QATopicMatcher { + + private QATopicMatcher() { } + + public static Matcher matchQATopicEntry(String key, int totalEvents) { + return allOf( + hasJsonPath("$.type", is("qualityassurancetopic")), + hasJsonPath("$.name", is(key)), + hasJsonPath("$.id", is(key.replace("/", "!"))), + hasJsonPath("$.totalEvents", is(totalEvents)) + ); + } + + + public static Matcher matchQATopicEntry(String key) { + return allOf( + hasJsonPath("$.type", is("qualityassurancetopic")), + hasJsonPath("$.name", is(key)), + hasJsonPath("$.id", is(key.replace("/", "/"))) + ); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockOpenAIREFundingDataProvider.java b/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockOpenaireFundingDataProvider.java similarity index 91% rename from dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockOpenAIREFundingDataProvider.java rename to dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockOpenaireFundingDataProvider.java index baf1041ab5c..34f1ae16c74 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockOpenAIREFundingDataProvider.java +++ b/dspace-server-webapp/src/test/java/org/dspace/external/provider/impl/MockOpenaireFundingDataProvider.java @@ -14,7 +14,7 @@ import eu.openaire.jaxb.helper.OpenAIREHandler; import eu.openaire.jaxb.model.Response; -import org.dspace.external.OpenAIRERestConnector; +import org.dspace.external.OpenaireRestConnector; import org.mockito.AdditionalMatchers; import org.mockito.ArgumentMatchers; import org.mockito.Mockito; @@ -22,14 +22,14 @@ import org.mockito.stubbing.Answer; /** - * Mock the OpenAIRE external source using a mock rest connector so that query + * Mock the Openaire external source using a mock rest connector so that query * will be resolved against static test files * */ -public class MockOpenAIREFundingDataProvider extends OpenAIREFundingDataProvider { +public class MockOpenaireFundingDataProvider extends OpenaireFundingDataProvider { @Override public void init() throws IOException { - OpenAIRERestConnector restConnector = Mockito.mock(OpenAIRERestConnector.class); + OpenaireRestConnector restConnector = Mockito.mock(OpenaireRestConnector.class); when(restConnector.searchProjectByKeywords(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt(), ArgumentMatchers.startsWith("mushroom"))).thenAnswer(new Answer() { diff --git a/dspace/config/crosswalks/oai/transformers/openaire4.xsl b/dspace/config/crosswalks/oai/transformers/openaire4.xsl index cece890450b..8ac703609d6 100644 --- a/dspace/config/crosswalks/oai/transformers/openaire4.xsl +++ b/dspace/config/crosswalks/oai/transformers/openaire4.xsl @@ -1,5 +1,5 @@ - + @@ -12,7 +12,7 @@ - + - + @@ -66,12 +66,12 @@ - This contexts complies with OpenAIRE Guidelines for Literature Repositories v3.0. + This contexts complies with Openaire Guidelines for Literature Repositories v3.0. - + - - + + - This contexts complies with OpenAIRE Guidelines for Literature Repositories v4.0. + This contexts complies with Openaire Guidelines for Literature Repositories v4.0. @@ -176,7 +176,7 @@ http://irdb.nii.ac.jp/oai http://irdb.nii.ac.jp/oai/junii2-3-1.xsd - @@ -250,13 +250,13 @@ - @@ -319,7 +319,7 @@ - - + @@ -457,7 +457,7 @@ + OR "openAccess", which is required by Openaire. --> org.dspace.xoai.filter.DSpaceAtLeastOneMetadataFilter @@ -483,7 +483,7 @@ + which specifies the openaire project ID. --> org.dspace.xoai.filter.DSpaceAtLeastOneMetadataFilter diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d38ffd64339..2cd2b3948c0 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -780,7 +780,7 @@ event.dispatcher.default.class = org.dspace.event.BasicDispatcher # Add rdf here, if you are using dspace-rdf to export your repository content as RDF. # Add iiif here, if you are using dspace-iiif. # Add orcidqueue here, if the integration with ORCID is configured and wish to enable the synchronization queue functionality -event.dispatcher.default.consumers = versioning, discovery, eperson, submissionconfig +event.dispatcher.default.consumers = versioning, discovery, eperson, submissionconfig, qaeventsdelete # The noindex dispatcher will not create search or browse indexes (useful for batch item imports) event.dispatcher.noindex.class = org.dspace.event.BasicDispatcher @@ -806,6 +806,10 @@ event.consumer.rdf.filters = Community|Collection|Item|Bundle|Bitstream|Site+Add #event.consumer.test.class = org.dspace.event.TestConsumer #event.consumer.test.filters = All+All +# qaevents consumer to delete events related to deleted items +event.consumer.qaeventsdelete.class = org.dspace.qaevent.QAEventsDeleteCascadeConsumer +event.consumer.qaeventsdelete.filters = Item+Delete + # consumer to maintain versions event.consumer.versioning.class = org.dspace.versioning.VersioningConsumer event.consumer.versioning.filters = Item+Install @@ -1646,6 +1650,7 @@ include = ${module_dir}/irus-statistics.cfg include = ${module_dir}/oai.cfg include = ${module_dir}/openaire-client.cfg include = ${module_dir}/orcid.cfg +include = ${module_dir}/qaevents.cfg include = ${module_dir}/rdf.cfg include = ${module_dir}/rest.cfg include = ${module_dir}/iiif.cfg diff --git a/dspace/config/hibernate.cfg.xml b/dspace/config/hibernate.cfg.xml index 3cc0623c349..763a760c0ec 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -62,6 +62,8 @@ + + diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index 1060a330311..e5175c3c4e2 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -185,28 +185,28 @@ extract - - + + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form - + submit.progressbar.describe.stepone org.dspace.app.rest.submit.step.DescribeStep submission-form @@ -361,13 +361,13 @@ - - + + - - + + @@ -376,17 +376,17 @@ - + - + - + - + - + - + diff --git a/dspace/config/modules/openaire-client.cfg b/dspace/config/modules/openaire-client.cfg index ded99d0120e..b43ef781ee4 100644 --- a/dspace/config/modules/openaire-client.cfg +++ b/dspace/config/modules/openaire-client.cfg @@ -16,20 +16,20 @@ # The accessToken it only has a validity of one hour # For more details about the token, please check: https://develop.openaire.eu/personalToken.html # -# the current OpenAIRE Rest client implementation uses basic authentication +# the current Openaire Rest client implementation uses basic authentication # Described here: https://develop.openaire.eu/basic.html # # ---- Token usage required definitions ---- # you can override this settings in your local.cfg file - can be true/false openaire.token.enabled = false -# URL of the OpenAIRE authentication and authorization service +# URL of the Openaire authentication and authorization service openaire.token.url = https://aai.openaire.eu/oidc/token -# you will be required to register at OpenAIRE (https://services.openaire.eu/uoa-user-management/registeredServices) +# you will be required to register at Openaire (https://services.openaire.eu/uoa-user-management/registeredServices) # and create your service in order to get the following data: openaire.token.clientId = CLIENT_ID_HERE openaire.token.clientSecret = CLIENT_SECRET_HERE -# URL of OpenAIRE Rest API +# URL of Openaire Rest API openaire.api.url = https://api.openaire.eu \ No newline at end of file diff --git a/dspace/config/modules/qaevents.cfg b/dspace/config/modules/qaevents.cfg new file mode 100644 index 00000000000..6cbaa120840 --- /dev/null +++ b/dspace/config/modules/qaevents.cfg @@ -0,0 +1,34 @@ +#---------------------------------------------------------------# +#------- Quality Assurance Broker Events CONFIGURATIONS --------# +#---------------------------------------------------------------# +# Configuration properties used by data correction service # +#---------------------------------------------------------------# +# Quality Assurance enable property, false by default +qaevents.enabled = false +qaevents.solr.server = ${solr.server}/${solr.multicorePrefix}qaevent +# A POST to these url(s) will be done to notify oaire of decision taken for each qaevents +# qaevents.openaire.acknowledge-url = https://beta.api-broker.openaire.eu/feedback/events + +# The list of the supported events incoming from openaire (see also dspace/config/spring/api/qaevents.xml) +# add missing abstract suggestion +qaevents.openaire.import.topic = ENRICH/MISSING/ABSTRACT +# add missing publication id suggestion +qaevents.openaire.import.topic = ENRICH/MISSING/PID +# add more publication id suggestion +qaevents.openaire.import.topic = ENRICH/MORE/PID +# add missing project suggestion +qaevents.openaire.import.topic = ENRICH/MISSING/PROJECT +# add more project suggestion +qaevents.openaire.import.topic = ENRICH/MORE/PROJECT + +# The list of the supported pid href for the OPENAIRE events +qaevents.openaire.pid-href-prefix.arxiv = https://arxiv.org/abs/ +qaevents.openaire.pid-href-prefix.handle = https://hdl.handle.net/ +qaevents.openaire.pid-href-prefix.urn = +qaevents.openaire.pid-href-prefix.doi = https://doi.org/ +qaevents.openaire.pid-href-prefix.pmc = https://www.ncbi.nlm.nih.gov/pmc/articles/ +qaevents.openaire.pid-href-prefix.pmid = https://pubmed.ncbi.nlm.nih.gov/ +qaevents.openaire.pid-href-prefix.ncid = https://ci.nii.ac.jp/ncid/ + +# The URI used by the OPENAIRE broker client to import QA events +qaevents.openaire.broker-url = http://api.openaire.eu/broker \ No newline at end of file diff --git a/dspace/config/registries/openaire4-types.xml b/dspace/config/registries/openaire4-types.xml index b3290ac1203..b46802a46fa 100644 --- a/dspace/config/registries/openaire4-types.xml +++ b/dspace/config/registries/openaire4-types.xml @@ -2,13 +2,13 @@ - OpenAIRE4 fields definition + Openaire4 fields definition @@ -108,7 +108,7 @@ datacite @@ -116,7 +116,7 @@ Spatial region or named place where the data was gathered or about which the data is focused. - + datacite subject diff --git a/dspace/config/registries/relationship-formats.xml b/dspace/config/registries/relationship-formats.xml index f2f50182fac..53d15d4d43e 100644 --- a/dspace/config/registries/relationship-formats.xml +++ b/dspace/config/registries/relationship-formats.xml @@ -237,7 +237,7 @@ Contains all uuids of PUBLICATIONS which link to the current ISSUE via a "latest" relationship. In other words, this stores all relationships pointing to the current ISSUE from any PUBLICATION, implying that the ISSUE is marked as "latest". Internally used by DSpace to support versioning. Do not manually add, remove or edit values. - + relation diff --git a/dspace/config/registries/schema-organization-types.xml b/dspace/config/registries/schema-organization-types.xml index 91ee203ae9b..0b31851078f 100644 --- a/dspace/config/registries/schema-organization-types.xml +++ b/dspace/config/registries/schema-organization-types.xml @@ -42,10 +42,10 @@ - + - + organization diff --git a/dspace/config/registries/schema-person-types.xml b/dspace/config/registries/schema-person-types.xml index 3a8f79732d4..0a40060e510 100644 --- a/dspace/config/registries/schema-person-types.xml +++ b/dspace/config/registries/schema-person-types.xml @@ -17,7 +17,7 @@ --> @@ -29,7 +29,7 @@ @@ -74,7 +74,7 @@ @@ -84,10 +84,10 @@ The organizational or institutional affiliation of the creator - + - + person identifier diff --git a/dspace/config/registries/schema-project-types.xml b/dspace/config/registries/schema-project-types.xml index c2f844d2b26..4da07b6a794 100644 --- a/dspace/config/registries/schema-project-types.xml +++ b/dspace/config/registries/schema-project-types.xml @@ -18,7 +18,7 @@ --> @@ -29,7 +29,7 @@ diff --git a/dspace/config/spring/api/discovery.xml b/dspace/config/spring/api/discovery.xml index fb25f11598f..83896feb455 100644 --- a/dspace/config/spring/api/discovery.xml +++ b/dspace/config/spring/api/discovery.xml @@ -110,8 +110,8 @@ - - + +
@@ -2027,7 +2027,7 @@
- diff --git a/dspace/config/spring/api/external-openaire.xml b/dspace/config/spring/api/external-openaire.xml index 25a23a17390..9800e42bd11 100644 --- a/dspace/config/spring/api/external-openaire.xml +++ b/dspace/config/spring/api/external-openaire.xml @@ -7,7 +7,7 @@ http://www.springframework.org/schema/util/spring-util.xsd" default-lazy-init="true"> - + @@ -15,9 +15,9 @@ - - - + + + diff --git a/dspace/config/spring/api/item-filters.xml b/dspace/config/spring/api/item-filters.xml index 24c463fb531..1460c19fe42 100644 --- a/dspace/config/spring/api/item-filters.xml +++ b/dspace/config/spring/api/item-filters.xml @@ -286,7 +286,7 @@ - @@ -329,7 +329,7 @@ - diff --git a/dspace/config/spring/api/qaevents.xml b/dspace/config/spring/api/qaevents.xml new file mode 100644 index 00000000000..8c52b8b1f2d --- /dev/null +++ b/dspace/config/spring/api/qaevents.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dspace/config/spring/api/scripts.xml b/dspace/config/spring/api/scripts.xml index 56eacbfff29..fdb22bad48c 100644 --- a/dspace/config/spring/api/scripts.xml +++ b/dspace/config/spring/api/scripts.xml @@ -3,6 +3,12 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> + + + + + + diff --git a/dspace/config/spring/api/solr-services.xml b/dspace/config/spring/api/solr-services.xml index 80e9449d4c6..a6ba559cda6 100644 --- a/dspace/config/spring/api/solr-services.xml +++ b/dspace/config/spring/api/solr-services.xml @@ -32,6 +32,10 @@ + + + + diff --git a/dspace/config/spring/rest/scripts.xml b/dspace/config/spring/rest/scripts.xml index eda8c579a89..e6a667697d9 100644 --- a/dspace/config/spring/rest/scripts.xml +++ b/dspace/config/spring/rest/scripts.xml @@ -8,6 +8,11 @@ + + + + + diff --git a/dspace/config/submission-forms.xml b/dspace/config/submission-forms.xml index 39a4778356c..d888e0990e6 100644 --- a/dspace/config/submission-forms.xml +++ b/dspace/config/submission-forms.xml @@ -819,9 +819,9 @@ - + -
+ dc @@ -1071,7 +1071,7 @@
-
+ dc @@ -1129,7 +1129,7 @@ relation onebox - openAIREFunding + openaireFunding @@ -1182,7 +1182,7 @@
-
+ person @@ -1246,7 +1246,7 @@
-
+ dc @@ -1269,7 +1269,7 @@ isFundingAgencyOfProject - openAIREFundingAgency + openaireFundingAgency false false @@ -1302,7 +1302,7 @@ -
+ organization @@ -1537,7 +1537,7 @@ - + @@ -1579,7 +1579,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + event_id + + diff --git a/dspace/solr/qaevent/conf/solrconfig.xml b/dspace/solr/qaevent/conf/solrconfig.xml new file mode 100644 index 00000000000..2a5f1ef4e9f --- /dev/null +++ b/dspace/solr/qaevent/conf/solrconfig.xml @@ -0,0 +1,127 @@ + + + + + + 8.8.1 + + + + + + ${solr.data.dir:} + + + + + + + + + + 32 + 1000 + ${solr.lock.type:native} + + false + + + + + + + 10000 + ${solr.autoCommit.maxTime:10000} + true + + + + ${solr.autoSoftCommit.maxTime:-1} + + + + + + ${solr.max.booleanClauses:1024} + + + + + + + + + + + true + 20 + 200 + false + 2 + + + + + + + + + + + + + explicit + 10 + event_id + + + + + + + + + application/json + + + + diff --git a/dspace/solr/qaevent/conf/stopwords.txt b/dspace/solr/qaevent/conf/stopwords.txt new file mode 100644 index 00000000000..8433c832d2c --- /dev/null +++ b/dspace/solr/qaevent/conf/stopwords.txt @@ -0,0 +1,57 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +# a couple of test stopwords to test that the words are really being +# configured from this file: +stopworda +stopwordb + +#Standard english stop words taken from Lucene's StopAnalyzer +an +and +are +as +at +be +but +by +for +if +in +into +is +it +no +not +of +on +or +s +such +t +that +the +their +then +there +these +they +this +to +was +will +with + diff --git a/dspace/solr/qaevent/conf/synonyms.txt b/dspace/solr/qaevent/conf/synonyms.txt new file mode 100644 index 00000000000..b0e31cb7ec8 --- /dev/null +++ b/dspace/solr/qaevent/conf/synonyms.txt @@ -0,0 +1,31 @@ +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +#----------------------------------------------------------------------- +#some test synonym mappings unlikely to appear in real input text +aaa => aaaa +bbb => bbbb1 bbbb2 +ccc => cccc1,cccc2 +a\=>a => b\=>b +a\,a => b\,b +fooaaa,baraaa,bazaaa + +# Some synonym groups specific to this example +GB,gib,gigabyte,gigabytes +MB,mib,megabyte,megabytes +Television, Televisions, TV, TVs +#notice we use "gib" instead of "GiB" so any WordDelimiterFilter coming +#after us won't split it into two words. + +# Synonym mappings can be used for spelling correction too +pixima => pixma + diff --git a/dspace/solr/qaevent/core.properties b/dspace/solr/qaevent/core.properties new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dspace/src/main/docker/dspace-solr/Dockerfile b/dspace/src/main/docker/dspace-solr/Dockerfile index 9fe9adf9440..5d02d0db811 100644 --- a/dspace/src/main/docker/dspace-solr/Dockerfile +++ b/dspace/src/main/docker/dspace-solr/Dockerfile @@ -17,19 +17,22 @@ FROM solr:${SOLR_VERSION}-slim ENV AUTHORITY_CONFIGSET_PATH=/opt/solr/server/solr/configsets/authority/conf \ OAI_CONFIGSET_PATH=/opt/solr/server/solr/configsets/oai/conf \ SEARCH_CONFIGSET_PATH=/opt/solr/server/solr/configsets/search/conf \ - STATISTICS_CONFIGSET_PATH=/opt/solr/server/solr/configsets/statistics/conf - + STATISTICS_CONFIGSET_PATH=/opt/solr/server/solr/configsets/statistics/conf \ + QAEVENT_CONFIGSET_PATH=/opt/solr/server/solr/configsets/qaevent/conf + USER root RUN mkdir -p $AUTHORITY_CONFIGSET_PATH && \ mkdir -p $OAI_CONFIGSET_PATH && \ mkdir -p $SEARCH_CONFIGSET_PATH && \ - mkdir -p $STATISTICS_CONFIGSET_PATH + mkdir -p $STATISTICS_CONFIGSET_PATH && \ + mkdir -p $QAEVENT_CONFIGSET_PATH COPY dspace/solr/authority/conf/* $AUTHORITY_CONFIGSET_PATH/ COPY dspace/solr/oai/conf/* $OAI_CONFIGSET_PATH/ COPY dspace/solr/search/conf/* $SEARCH_CONFIGSET_PATH/ COPY dspace/solr/statistics/conf/* $STATISTICS_CONFIGSET_PATH/ +COPY dspace/solr/qaevent/conf/* $QAEVENT_CONFIGSET_PATH/ RUN chown -R solr:solr /opt/solr/server/solr/configsets