diff --git a/dspace-api/pom.xml b/dspace-api/pom.xml index f6b06f69edb7..0aafa5df73fb 100644 --- a/dspace-api/pom.xml +++ b/dspace-api/pom.xml @@ -374,6 +374,11 @@ + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.13.5 + javax.cache cache-api @@ -837,6 +842,10 @@ org.yaml snakeyaml + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + diff --git a/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java b/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java index 2677cb20501f..80bda610c7dd 100644 --- a/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java +++ b/dspace-api/src/main/java/org/dspace/administer/MetadataImporter.java @@ -21,6 +21,8 @@ import org.apache.commons.cli.DefaultParser; import org.apache.commons.cli.Options; import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.MetadataField; import org.dspace.content.MetadataSchema; @@ -30,8 +32,6 @@ import org.dspace.content.service.MetadataFieldService; import org.dspace.content.service.MetadataSchemaService; import org.dspace.core.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -40,9 +40,9 @@ /** * @author Richard Jones * - * This class takes an xml document as passed in the arguments and + * This class takes an XML document as passed in the arguments and * uses it to create metadata elements in the Metadata Registry if - * they do not already exist + * they do not already exist. * * The format of the XML file is as follows: * @@ -69,7 +69,7 @@ public class MetadataImporter { /** * logging category */ - private static final Logger log = LoggerFactory.getLogger(MetadataImporter.class); + private static final Logger log = LogManager.getLogger(); /** * Default constructor @@ -89,6 +89,7 @@ private MetadataImporter() { } * @throws SAXException if parser error * @throws NonUniqueMetadataException if duplicate metadata * @throws RegistryImportException if import fails + * @throws XPathExpressionException passed through **/ public static void main(String[] args) throws ParseException, SQLException, IOException, TransformerException, @@ -125,6 +126,7 @@ public static void main(String[] args) * @throws SAXException if parser error * @throws NonUniqueMetadataException if duplicate metadata * @throws RegistryImportException if import fails + * @throws XPathExpressionException passed through */ public static void loadRegistry(String file, boolean forceUpdate) throws SQLException, IOException, TransformerException, ParserConfigurationException, AuthorizeException, @@ -203,7 +205,7 @@ private static void loadSchema(Context context, Node node, boolean updateExistin if (s == null) { // Schema does not exist - create - log.info("Registering Schema " + name + " (" + namespace + ")"); + log.info("Registering Schema {}({})", name, namespace); metadataSchemaService.create(context, name, namespace); } else { // Schema exists - if it's the same namespace, allow the type imports to continue @@ -215,7 +217,7 @@ private static void loadSchema(Context context, Node node, boolean updateExistin // It's a different namespace - have we been told to update? if (updateExisting) { // Update the existing schema namespace and continue to type import - log.info("Updating Schema " + name + ": New namespace " + namespace); + log.info("Updating Schema {}: New namespace {}", name, namespace); s.setNamespace(namespace); metadataSchemaService.update(context, s); } else { @@ -274,7 +276,7 @@ private static void loadType(Context context, Node node) if (qualifier == null) { fieldName = schema + "." + element; } - log.info("Registering metadata field " + fieldName); + log.info("Registering metadata field {}", fieldName); MetadataField field = metadataFieldService.create(context, schemaObj, element, qualifier, scopeNote); metadataFieldService.update(context, field); } diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/ItemFilter.java b/dspace-api/src/main/java/org/dspace/app/ldn/ItemFilter.java new file mode 100644 index 000000000000..44dd5389d3de --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/ItemFilter.java @@ -0,0 +1,30 @@ +/** + * 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.ldn; + +/** + * model class for the item filters configured into item-filters.xml + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class ItemFilter { + + private String id; + + public ItemFilter(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java new file mode 100644 index 000000000000..9e03cf73927e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageConsumer.java @@ -0,0 +1,231 @@ +/** + * 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.ldn; + +import static java.lang.String.format; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.ldn.factory.NotifyServiceFactory; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.content.Bitstream; +import org.dspace.content.BitstreamFormat; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.content.MetadataValue; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.logic.LogicalStatement; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.core.I18nUtil; +import org.dspace.core.LDN; +import org.dspace.event.Consumer; +import org.dspace.event.Event; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; +import org.dspace.web.ContextUtil; + +/** + * class for creating a new LDN Messages of installed item + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class LDNMessageConsumer implements Consumer { + + private NotifyPatternToTriggerService notifyPatternToTriggerService; + private NotifyServiceInboundPatternService inboundPatternService; + private LDNMessageService ldnMessageService; + private ConfigurationService configurationService; + private ItemService itemService; + private BitstreamService bitstreamService; + + @Override + public void initialize() throws Exception { + notifyPatternToTriggerService = NotifyServiceFactory.getInstance().getNotifyPatternToTriggerService(); + ldnMessageService = NotifyServiceFactory.getInstance().getLDNMessageService(); + configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + itemService = ContentServiceFactory.getInstance().getItemService(); + bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + inboundPatternService = NotifyServiceFactory.getInstance().getNotifyServiceInboundPatternService(); + } + + @Override + public void consume(Context context, Event event) throws Exception { + + if (event.getSubjectType() != Constants.ITEM || + event.getEventType() != Event.INSTALL) { + return; + } + + Item item = (Item) event.getSubject(context); + createManualLDNMessages(context, item); + createAutomaticLDNMessages(context, item); + } + + private void createManualLDNMessages(Context context, Item item) throws SQLException, JsonProcessingException { + List patternsToTrigger = + notifyPatternToTriggerService.findByItem(context, item); + + for (NotifyPatternToTrigger patternToTrigger : patternsToTrigger) { + createLDNMessage(context,patternToTrigger.getItem(), + patternToTrigger.getNotifyService(), patternToTrigger.getPattern()); + } + } + + private void createAutomaticLDNMessages(Context context, Item item) throws SQLException, JsonProcessingException { + + List inboundPatterns = inboundPatternService.findAutomaticPatterns(context); + + for (NotifyServiceInboundPattern inboundPattern : inboundPatterns) { + if (StringUtils.isEmpty(inboundPattern.getConstraint()) || + evaluateFilter(context, item, inboundPattern.getConstraint())) { + createLDNMessage(context, item, inboundPattern.getNotifyService(), inboundPattern.getPattern()); + } + } + } + + private boolean evaluateFilter(Context context, Item item, String constraint) { + LogicalStatement filter = + new DSpace().getServiceManager().getServiceByName(constraint, LogicalStatement.class); + + return filter != null && filter.getResult(context, item); + } + + private void createLDNMessage(Context context, Item item, NotifyServiceEntity service, String pattern) + throws SQLException, JsonMappingException, JsonProcessingException { + + LDN ldn = getLDNMessage(pattern); + LDNMessageEntity ldnMessage = + ldnMessageService.create(context, format("urn:uuid:%s", UUID.randomUUID())); + + ldnMessage.setObject(item); + ldnMessage.setTarget(service); + ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED); + ldnMessage.setQueueTimeout(new Date()); + + appendGeneratedMessage(ldn, ldnMessage, pattern); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + ldnMessage.setType(StringUtils.joinWith(",", notification.getType())); + + ArrayList notificationTypeArrayList = new ArrayList(notification.getType()); + // sorting the list + Collections.sort(notificationTypeArrayList); + ldnMessage.setActivityStreamType(notificationTypeArrayList.get(0)); + ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1)); + + ldnMessageService.update(context, ldnMessage); + } + + private LDN getLDNMessage(String pattern) { + try { + return LDN.getLDNMessage(I18nUtil.getLDNFilename(Locale.getDefault(), pattern)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void appendGeneratedMessage(LDN ldn, LDNMessageEntity ldnMessage, String pattern) { + Item item = (Item) ldnMessage.getObject(); + ldn.addArgument(getUiUrl()); + ldn.addArgument(configurationService.getProperty("ldn.notify.inbox")); + ldn.addArgument(configurationService.getProperty("dspace.name")); + ldn.addArgument(Objects.requireNonNullElse(ldnMessage.getTarget().getUrl(), "")); + ldn.addArgument(Objects.requireNonNullElse(ldnMessage.getTarget().getLdnUrl(), "")); + ldn.addArgument(getUiUrl() + "/handle/" + ldnMessage.getObject().getHandle()); + ldn.addArgument(getIdentifierUri(item)); + ldn.addArgument(generateBitstreamDownloadUrl(item)); + ldn.addArgument(getBitstreamMimeType(findPrimaryBitstream(item))); + ldn.addArgument(ldnMessage.getID()); + + ldnMessage.setMessage(ldn.generateLDNMessage()); + } + + private String getUiUrl() { + return configurationService.getProperty("dspace.ui.url"); + } + + private String getIdentifierUri(Item item) { + return itemService.getMetadataByMetadataString(item, "dc.identifier.uri") + .stream() + .findFirst() + .map(MetadataValue::getValue) + .orElse(""); + } + + private String generateBitstreamDownloadUrl(Item item) { + String uiUrl = getUiUrl(); + return findPrimaryBitstream(item) + .map(bs -> uiUrl + "/bitstreams/" + bs.getID() + "/download") + .orElse(""); + } + + private Optional findPrimaryBitstream(Item item) { + List bundles = item.getBundles(Constants.CONTENT_BUNDLE_NAME); + return bundles.stream() + .findFirst() + .map(Bundle::getPrimaryBitstream) + .or(() -> bundles.stream() + .findFirst() + .flatMap(bundle -> CollectionUtils.isNotEmpty(bundle.getBitstreams()) + ? Optional.of(bundle.getBitstreams().get(0)) + : Optional.empty())); + } + + private String getBitstreamMimeType(Optional bitstream) { + return bitstream.map(bs -> { + try { + Context context = ContextUtil.obtainCurrentRequestContext(); + BitstreamFormat bitstreamFormat = bs.getFormat(context); + if (bitstreamFormat.getShortDescription().equals("Unknown")) { + return getUserFormatMimeType(bs); + } + return bitstreamFormat.getMIMEType(); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }).orElse(""); + } + + private String getUserFormatMimeType(Bitstream bitstream) { + return bitstreamService.getMetadataFirstValue(bitstream, + "dc", "format", "mimetype", Item.ANY); + } + + @Override + public void end(Context ctx) throws Exception { + + } + + @Override + public void finish(Context ctx) throws Exception { + + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java new file mode 100644 index 000000000000..5d96dd3f956c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageEntity.java @@ -0,0 +1,278 @@ +/** + * 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.ldn; + +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.content.DSpaceObject; +import org.dspace.core.ReloadableEntity; + +/** + * Class representing ldnMessages stored in the DSpace system and, when locally resolvable, + * some information are stored as dedicated attributes. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Entity +@Table(name = "ldn_message") +public class LDNMessageEntity implements ReloadableEntity { + + /** + * LDN messages interact with a fictitious queue. Scheduled tasks manage the queue. + */ + + /** + * Message must not be processed. + */ + public static final Integer QUEUE_STATUS_UNTRUSTED_IP = 0; + + /** + * Message queued, it has to be elaborated. + */ + public static final Integer QUEUE_STATUS_QUEUED = 1; + + /** + * Message has been taken from the queue and it's elaboration is in progress. + */ + public static final Integer QUEUE_STATUS_PROCESSING = 2; + + /** + * Message has been correctly elaborated. + */ + public static final Integer QUEUE_STATUS_PROCESSED = 3; + + /** + * Message has not been correctly elaborated - despite more than "ldn.processor.max.attempts" retryies + */ + public static final Integer QUEUE_STATUS_FAILED = 4; + + /** + * Message must not be processed + */ + public static final Integer QUEUE_STATUS_UNTRUSTED = 5; + + /** + * Message is not processed since action is not mapped + */ + public static final Integer QUEUE_STATUS_UNMAPPED_ACTION = 6; + + @Id + private String id; + + @ManyToOne + @JoinColumn(name = "object", referencedColumnName = "uuid") + private DSpaceObject object; + + @Column(name = "message", nullable = false, columnDefinition = "text") + private String message; + + @Column(name = "type") + private String type; + + @Column(name = "queue_status") + private Integer queueStatus; + + @Column(name = "queue_attempts") + private Integer queueAttempts = 0; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "queue_last_start_time") + private Date queueLastStartTime = null; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "queue_timeout") + private Date queueTimeout = null; + + @ManyToOne + @JoinColumn(name = "origin", referencedColumnName = "id") + private NotifyServiceEntity origin; + + @ManyToOne + @JoinColumn(name = "target", referencedColumnName = "id") + private NotifyServiceEntity target; + + @ManyToOne + @JoinColumn(name = "inReplyTo", referencedColumnName = "id") + private LDNMessageEntity inReplyTo; + + @ManyToOne + @JoinColumn(name = "context", referencedColumnName = "uuid") + private DSpaceObject context; + + @Column(name = "activity_stream_type") + private String activityStreamType; + + @Column(name = "coar_notify_type") + private String coarNotifyType; + + @Column(name = "source_ip") + private String sourceIp; + + protected LDNMessageEntity() { + + } + + public LDNMessageEntity(String id) { + this.id = id; + } + + @Override + public String getID() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * + * @return the DSpace item related to this message + */ + public DSpaceObject getObject() { + return object; + } + + public void setObject(DSpaceObject object) { + this.object = object; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getActivityStreamType() { + return activityStreamType; + } + + public void setActivityStreamType(String activityStreamType) { + this.activityStreamType = activityStreamType; + } + + public String getCoarNotifyType() { + return coarNotifyType; + } + + public void setCoarNotifyType(String coarNotifyType) { + this.coarNotifyType = coarNotifyType; + } + + /** + * + * @return The originator of the activity, typically the service responsible for sending the notification + */ + public NotifyServiceEntity getOrigin() { + return origin; + } + + public void setOrigin(NotifyServiceEntity origin) { + this.origin = origin; + } + + /** + * + * @return The intended destination of the activity, typically the service which consumes the notification + */ + public NotifyServiceEntity getTarget() { + return target; + } + + public void setTarget(NotifyServiceEntity target) { + this.target = target; + } + + /** + * + * @return This property is used when the notification is a direct response to a previous notification; + * contains an {@link org.dspace.app.ldn.LDNMessageEntity#inReplyTo id} + */ + public LDNMessageEntity getInReplyTo() { + return inReplyTo; + } + + public void setInReplyTo(LDNMessageEntity inReplyTo) { + this.inReplyTo = inReplyTo; + } + + /** + * + * @return This identifies another resource which is relevant to understanding the notification + */ + public DSpaceObject getContext() { + return context; + } + + public void setContext(DSpaceObject context) { + this.context = context; + } + + public Integer getQueueStatus() { + return queueStatus; + } + + public void setQueueStatus(Integer queueStatus) { + this.queueStatus = queueStatus; + } + + public Integer getQueueAttempts() { + return queueAttempts; + } + + public void setQueueAttempts(Integer queueAttempts) { + this.queueAttempts = queueAttempts; + } + + public Date getQueueLastStartTime() { + return queueLastStartTime; + } + + public void setQueueLastStartTime(Date queueLastStartTime) { + this.queueLastStartTime = queueLastStartTime; + } + + public Date getQueueTimeout() { + return queueTimeout; + } + + public void setQueueTimeout(Date queueTimeout) { + this.queueTimeout = queueTimeout; + } + + public String getSourceIp() { + return sourceIp; + } + + public void setSourceIp(String sourceIp) { + this.sourceIp = sourceIp; + } + + @Override + public String toString() { + return "LDNMessage id:" + this.getID() + " typed:" + this.getType(); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageQueueStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageQueueStatus.java new file mode 100644 index 000000000000..ad3dd36e69c5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMessageQueueStatus.java @@ -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/ + */ +package org.dspace.app.ldn; + +public enum LDNMessageQueueStatus { + + /** + * Resulting processing status of an LDN Message (aka queue management) + */ + QUEUED, PROCESSING, PROCESSED, FAILED; +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNMetadataFields.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMetadataFields.java new file mode 100644 index 000000000000..67a87c144c83 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNMetadataFields.java @@ -0,0 +1,38 @@ +/** + * 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.ldn; + +/** + * Constants for LDN metadata fields + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) + */ +public final class LDNMetadataFields { + + // schema and element are the same for each metadata of LDN coar-notify + public static final String SCHEMA = "coar"; + public static final String ELEMENT = "notify"; + + // qualifiers + public static final String INITIALIZE = "initialize"; + public static final String REQUEST_REVIEW = "requestreview"; + public static final String REQUEST_ENDORSEMENT = "requestendorsement"; + public static final String EXAMINATION = "examination"; + public static final String REFUSED = "refused"; + public static final String REVIEW = "review"; + public static final String ENDORSMENT = "endorsement"; + public static final String RELEASE = "release"; + + /** + * + */ + private LDNMetadataFields() { + + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueExtractor.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueExtractor.java new file mode 100644 index 000000000000..57a7cdfb07bf --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueExtractor.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn; + +import java.sql.SQLException; + +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.factory.LDNMessageServiceFactory; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.core.Context; + +/** + * LDN Message manager: scheduled task invoking extractAndProcessMessageFromQueue() of {@link LDNMessageService} + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) + */ +public class LDNQueueExtractor { + + private static final LDNMessageService ldnMessageService = LDNMessageServiceFactory.getInstance() + .getLDNMessageService(); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNQueueExtractor.class); + + /** + * Default constructor + */ + private LDNQueueExtractor() { + } + + /** + * invokes + * @see org.dspace.app.ldn.service.impl.LDNMessageServiceImpl#extractAndProcessMessageFromQueue(Context) + * to process the oldest ldn messages from the queue. An LdnMessage is processed when is routed to a + * @see org.dspace.app.ldn.processor.LDNProcessor + * Also a +1 is added to the ldnMessage entity + * @see org.dspace.app.ldn.LDNMessageEntity#getQueueAttempts() + * @return the number of processed ldnMessages. + * @throws SQLException + */ + public static int extractMessageFromQueue() throws SQLException { + Context context = new Context(Context.Mode.READ_WRITE); + int processed_messages = ldnMessageService.extractAndProcessMessageFromQueue(context); + if (processed_messages > 0) { + log.info("Processed Messages x" + processed_messages); + } + context.complete(); + return processed_messages; + } + +}; \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueTimeoutChecker.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueTimeoutChecker.java new file mode 100644 index 000000000000..36a927d672bc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNQueueTimeoutChecker.java @@ -0,0 +1,53 @@ +/** + * 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.ldn; + +import java.sql.SQLException; + +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.factory.LDNMessageServiceFactory; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.core.Context; + +/** + * LDN Message manager: scheduled task invoking checkQueueMessageTimeout() of {@link LDNMessageService} + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) + */ +public class LDNQueueTimeoutChecker { + + private static final LDNMessageService ldnMessageService = LDNMessageServiceFactory.getInstance() + .getLDNMessageService(); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNQueueTimeoutChecker.class); + + /** + * Default constructor + */ + private LDNQueueTimeoutChecker() { + } + + /** + * invokes + * @see org.dspace.app.ldn.service.impl.LDNMessageServiceImpl#checkQueueMessageTimeout(Context) + * to refresh the queue status of timed-out and in progressing status ldn messages: + * according to their attempts put them back in queue or set their status as failed if maxAttempts + * reached. + * @return the number of managed ldnMessages. + * @throws SQLException + */ + public static int checkQueueMessageTimeout() throws SQLException { + Context context = new Context(Context.Mode.READ_WRITE); + int fixed_messages = 0; + fixed_messages = ldnMessageService.checkQueueMessageTimeout(context); + if (fixed_messages > 0) { + log.info("Managed Messages x" + fixed_messages); + } + context.complete(); + return fixed_messages; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNRouter.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNRouter.java new file mode 100644 index 000000000000..14957aa503a3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNRouter.java @@ -0,0 +1,91 @@ +/** + * 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.ldn; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.processor.LDNProcessor; + +/** + * Linked Data Notification router. + */ +public class LDNRouter { + + private Map, LDNProcessor> incomingProcessors = new HashMap<>(); + private Map, LDNProcessor> outcomingProcessors = new HashMap<>(); + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNRouter.class); + + /** + * Route notification to processor + * + * @return LDNProcessor processor to process notification, can be null + */ + public LDNProcessor route(LDNMessageEntity ldnMessage) { + if (ldnMessage == null) { + log.warn("A null LDNMessage was received and could not be routed."); + return null; + } + if (StringUtils.isEmpty(ldnMessage.getType())) { + log.warn("LDNMessage " + ldnMessage + " was received. It has no type, so it couldn't be routed."); + return null; + } + Set ldnMessageTypeSet = new HashSet(); + ldnMessageTypeSet.add(ldnMessage.getActivityStreamType()); + ldnMessageTypeSet.add(ldnMessage.getCoarNotifyType()); + + LDNProcessor processor = null; + if (ldnMessage.getTarget() == null) { + processor = incomingProcessors.get(ldnMessageTypeSet); + } else if (ldnMessage.getOrigin() == null) { + processor = outcomingProcessors.get(ldnMessageTypeSet); + } + + return processor; + } + + /** + * Get all incoming routes. + * + * @return Map, LDNProcessor> + */ + public Map, LDNProcessor> getIncomingProcessors() { + return incomingProcessors; + } + + /** + * Set all incoming routes. + * + * @param incomingProcessors + */ + public void setIncomingProcessors(Map, LDNProcessor> incomingProcessors) { + this.incomingProcessors = incomingProcessors; + } + + /** + * Get all outcoming routes. + * + * @return Map, LDNProcessor> + */ + public Map, LDNProcessor> getOutcomingProcessors() { + return outcomingProcessors; + } + + /** + * Set all outcoming routes. + * + * @param outcomingProcessors + */ + public void setOutcomingProcessors(Map, LDNProcessor> outcomingProcessors) { + this.outcomingProcessors = outcomingProcessors; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyPatternToTrigger.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyPatternToTrigger.java new file mode 100644 index 000000000000..b393d8bedbc5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyPatternToTrigger.java @@ -0,0 +1,83 @@ +/** + * 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.ldn; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.dspace.content.Item; +import org.dspace.core.ReloadableEntity; + +/** + * Database object representing notify patterns to be triggered + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Entity +@Table(name = "notifypatterns_to_trigger") +public class NotifyPatternToTrigger implements ReloadableEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notifypatterns_to_trigger_id_seq") + @SequenceGenerator(name = "notifypatterns_to_trigger_id_seq", + sequenceName = "notifypatterns_to_trigger_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne + @JoinColumn(name = "item_id", referencedColumnName = "uuid") + private Item item; + + @ManyToOne + @JoinColumn(name = "service_id", referencedColumnName = "id") + private NotifyServiceEntity notifyService; + + @Column(name = "pattern") + private String pattern; + + public void setId(Integer id) { + this.id = id; + } + + public Item getItem() { + return item; + } + + public void setItem(Item item) { + this.item = item; + } + + public NotifyServiceEntity getNotifyService() { + return notifyService; + } + + public void setNotifyService(NotifyServiceEntity notifyService) { + this.notifyService = notifyService; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + @Override + public Integer getID() { + return id; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java new file mode 100644 index 000000000000..206ed16fa00e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java @@ -0,0 +1,156 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn; + +import java.math.BigDecimal; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.dspace.core.ReloadableEntity; + +/** + * Database object representing notify services + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Entity +@Table(name = "notifyservice") +public class NotifyServiceEntity implements ReloadableEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notifyservice_id_seq") + @SequenceGenerator(name = "notifyservice_id_seq", sequenceName = "notifyservice_id_seq", + allocationSize = 1) + private Integer id; + + @Column(name = "name", nullable = false) + private String name; + + @Column(name = "description", columnDefinition = "text") + private String description; + + @Column(name = "url") + private String url; + + @Column(name = "ldn_url") + private String ldnUrl; + + @OneToMany(mappedBy = "notifyService") + private List inboundPatterns; + + @Column(name = "enabled") + private boolean enabled = false; + + @Column(name = "score") + private BigDecimal score; + + @Column(name = "lower_ip") + private String lowerIp; + + @Column(name = "upper_ip") + private String upperIp; + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + /** + * @return URL of an informative website + */ + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + /** + * @return URL of the LDN InBox + */ + public String getLdnUrl() { + return ldnUrl; + } + + public void setLdnUrl(String ldnUrl) { + this.ldnUrl = ldnUrl; + } + + /** + * @return The list of the inbound patterns configuration supported by the service + */ + public List getInboundPatterns() { + return inboundPatterns; + } + + public void setInboundPatterns(List inboundPatterns) { + this.inboundPatterns = inboundPatterns; + } + + @Override + public Integer getID() { + return id; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public BigDecimal getScore() { + return score; + } + + public void setScore(BigDecimal score) { + this.score = score; + } + + public String getLowerIp() { + return lowerIp; + } + + public void setLowerIp(String lowerIp) { + this.lowerIp = lowerIp; + } + + public String getUpperIp() { + return upperIp; + } + + public void setUpperIp(String upperIp) { + this.upperIp = upperIp; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceInboundPattern.java b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceInboundPattern.java new file mode 100644 index 000000000000..0c367d505131 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceInboundPattern.java @@ -0,0 +1,104 @@ +/** + * 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.ldn; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.SequenceGenerator; +import javax.persistence.Table; + +import org.dspace.core.ReloadableEntity; + +/** + * Database object representing notify service inbound patterns. Every {@link org.dspace.app.ldn.NotifyServiceEntity} + * may have inbounds and outbounds. Inbounds are to be sent to the external service. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Entity +@Table(name = "notifyservice_inbound_pattern") +public class NotifyServiceInboundPattern implements ReloadableEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "notifyservice_inbound_pattern_id_seq") + @SequenceGenerator(name = "notifyservice_inbound_pattern_id_seq", + sequenceName = "notifyservice_inbound_pattern_id_seq", + allocationSize = 1) + private Integer id; + + @ManyToOne + @JoinColumn(name = "service_id", referencedColumnName = "id") + private NotifyServiceEntity notifyService; + + @Column(name = "pattern") + private String pattern; + + @Column(name = "constraint_name") + private String constraint; + + @Column(name = "automatic") + private boolean automatic; + + @Override + public Integer getID() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public NotifyServiceEntity getNotifyService() { + return notifyService; + } + + public void setNotifyService(NotifyServiceEntity notifyService) { + this.notifyService = notifyService; + } + + /** + * @see coar documentation + * @return pattern of the inbound notification + */ + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + /** + * @return the condition checked for automatic evaluation + */ + public String getConstraint() { + return constraint; + } + + public void setConstraint(String constraint) { + this.constraint = constraint; + } + + /** + * when true - the notification is automatically when constraints are respected. + * @return the automatic flag + */ + public boolean isAutomatic() { + return automatic; + } + + public void setAutomatic(boolean automatic) { + this.automatic = automatic; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNAction.java new file mode 100644 index 000000000000..b0c895de9958 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNAction.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.app.ldn.action; + +import org.dspace.app.ldn.model.Notification; +import org.dspace.content.Item; +import org.dspace.core.Context; + +/** + * An action that is run after a notification has been processed. + */ +public interface LDNAction { + + /** + * Execute action for provided notification and item corresponding to the + * notification context. + * + *@param context the context + * @param notification the processed notification to perform action against + * @param item the item corresponding to the notification context + * @return ActionStatus the resulting status of the action + * @throws Exception general exception that can be thrown while executing action + */ + public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception; + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNActionStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNActionStatus.java new file mode 100644 index 000000000000..86f56ed9baab --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNActionStatus.java @@ -0,0 +1,15 @@ +/** + * 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.ldn.action; + +/** + * Resulting status of an execution of an action. + */ +public enum LDNActionStatus { + CONTINUE, ABORT; +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNCorrectionAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNCorrectionAction.java new file mode 100644 index 000000000000..5ce3804bcee8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNCorrectionAction.java @@ -0,0 +1,108 @@ +/** + * 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.ldn.action; + +import java.math.BigDecimal; +import java.sql.SQLException; +import java.util.Date; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +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.service.QAEventService; +import org.dspace.qaevent.service.dto.NotifyMessageDTO; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Implementation for LDN Correction Action. It creates a QA Event according to the LDN Message received * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class LDNCorrectionAction implements LDNAction { + + private static final Logger log = LogManager.getLogger(LDNEmailAction.class); + + private String qaEventTopic; + + @Autowired + private ConfigurationService configurationService; + @Autowired + protected ItemService itemService; + @Autowired + private QAEventService qaEventService; + @Autowired + private LDNMessageService ldnMessageService; + @Autowired + private HandleService handleService; + + @Override + public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception { + LDNActionStatus result = LDNActionStatus.ABORT; + String itemName = itemService.getName(item); + QAEvent qaEvent = null; + if (notification.getObject() != null) { + String citeAs = notification.getObject().getIetfCiteAs(); + if (citeAs == null || citeAs.isEmpty()) { + citeAs = notification.getObject().getId(); + } + NotifyMessageDTO message = new NotifyMessageDTO(); + message.setHref(citeAs); + message.setRelationship(notification.getObject().getAsRelationship()); + if (notification.getOrigin() != null) { + message.setServiceId(notification.getOrigin().getId()); + message.setServiceName(notification.getOrigin().getInbox()); + } + BigDecimal score = getScore(context, notification); + double doubleScoreValue = score != null ? score.doubleValue() : 0d; + ObjectMapper mapper = new ObjectMapper(); + qaEvent = new QAEvent(QAEvent.COAR_NOTIFY_SOURCE, + handleService.findHandle(context, item), item.getID().toString(), itemName, + this.getQaEventTopic(), doubleScoreValue, + mapper.writeValueAsString(message), + new Date()); + qaEventService.store(context, qaEvent); + result = LDNActionStatus.CONTINUE; + } + + return result; + } + + private BigDecimal getScore(Context context, Notification notification) throws SQLException { + + if (notification.getOrigin() == null) { + return BigDecimal.ZERO; + } + + NotifyServiceEntity service = ldnMessageService.findNotifyService(context, notification.getOrigin()); + + if (service == null) { + return BigDecimal.ZERO; + } + + return service.getScore(); + } + + public String getQaEventTopic() { + return qaEventTopic; + } + + public void setQaEventTopic(String qaEventTopic) { + this.qaEventTopic = qaEventTopic; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java new file mode 100644 index 000000000000..b87001f81500 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNEmailAction.java @@ -0,0 +1,155 @@ +/** + * 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.ldn.action; + +import static java.lang.String.format; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Calendar; +import java.util.LinkedList; +import java.util.List; +import java.util.Locale; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.model.Notification; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.core.Email; +import org.dspace.core.I18nUtil; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Action to send email to receipients provided in actionSendFilter. The email + * body will be result of templating actionSendFilter. + */ +public class LDNEmailAction implements LDNAction { + + private static final Logger log = LogManager.getLogger(LDNEmailAction.class); + + private final static String DATE_PATTERN = "dd-MM-yyyy HH:mm:ss"; + + @Autowired + private ConfigurationService configurationService; + + /* + * Supported for actionSendFilter are: + * - + * - GROUP: + * - SUBMITTER + */ + private String actionSendFilter; + + // The file name for the requested email + private String actionSendEmailTextFile; + + /** + * Execute sending an email. + * + * Template context parameters: + * + * {0} Service Name + * {1} Item Name + * {2} Service URL + * {3} Item URL + * {4} Submitter's Name + * {5} Date of the received LDN notification + * {6} LDN notification + * {7} Item + * + * @param notification + * @param item + * @return ActionStatus + * @throws Exception + */ + @Override + public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception { + try { + Locale supportedLocale = I18nUtil.getEPersonLocale(context.getCurrentUser()); + Email email = Email.getEmail(I18nUtil.getEmailFilename(supportedLocale, actionSendEmailTextFile)); + + // Setting recipients email + for (String recipient : retrieveRecipientsEmail(item)) { + email.addRecipient(recipient); + } + + String date = new SimpleDateFormat(DATE_PATTERN).format(Calendar.getInstance().getTime()); + + email.addArgument(notification.getActor().getName()); + email.addArgument(item.getName()); + email.addArgument(notification.getActor().getId()); + email.addArgument(notification.getContext() != null ? + notification.getContext().getId() : notification.getObject().getId()); + email.addArgument(item.getSubmitter().getFullName()); + email.addArgument(date); + email.addArgument(notification); + email.addArgument(item); + + email.send(); + } catch (Exception e) { + log.error("An Error Occurred while sending a notification email", e); + } + + return LDNActionStatus.CONTINUE; + } + + /** + * @return String + */ + public String getActionSendFilter() { + return actionSendFilter; + } + + /** + * @param actionSendFilter + */ + public void setActionSendFilter(String actionSendFilter) { + this.actionSendFilter = actionSendFilter; + } + + /** + * @return String + */ + public String getActionSendEmailTextFile() { + return actionSendEmailTextFile; + } + + /** + * @param actionSendEmailTextFile + */ + public void setActionSendEmailTextFile(String actionSendEmailTextFile) { + this.actionSendEmailTextFile = actionSendEmailTextFile; + } + + /** + * Parses actionSendFilter for reserved tokens and returns list of email + * recipients. + * + * @param item the item which to get submitter email + * @return List list of email recipients + */ + private List retrieveRecipientsEmail(Item item) { + List recipients = new LinkedList(); + + if (actionSendFilter.startsWith("SUBMITTER")) { + recipients.add(item.getSubmitter().getEmail()); + } else if (actionSendFilter.startsWith("GROUP:")) { + String groupName = actionSendFilter.replace("GROUP:", ""); + String property = format("email.%s.list", groupName); + String[] groupEmails = configurationService.getArrayProperty(property); + recipients = Arrays.asList(groupEmails); + } else { + recipients.add(actionSendFilter); + } + + return recipients; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNRelationCorrectionAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNRelationCorrectionAction.java new file mode 100644 index 000000000000..0c2f5dfc1729 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/LDNRelationCorrectionAction.java @@ -0,0 +1,104 @@ +/** + * 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.ldn.action; + +import java.math.BigDecimal; +import java.sql.SQLException; +import java.util.Date; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +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.service.QAEventService; +import org.dspace.qaevent.service.dto.NotifyMessageDTO; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Implementation for LDN Correction Action. It creates a QA Event according to the LDN Message received * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class LDNRelationCorrectionAction implements LDNAction { + + private static final Logger log = LogManager.getLogger(LDNEmailAction.class); + + private String qaEventTopic; + + @Autowired + private ConfigurationService configurationService; + @Autowired + protected ItemService itemService; + @Autowired + private QAEventService qaEventService; + @Autowired + private LDNMessageService ldnMessageService; + @Autowired + private HandleService handleService; + + @Override + public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception { + LDNActionStatus result = LDNActionStatus.ABORT; + String itemName = itemService.getName(item); + QAEvent qaEvent = null; + if (notification.getObject() != null) { + NotifyMessageDTO message = new NotifyMessageDTO(); + message.setHref(notification.getObject().getAsSubject()); + message.setRelationship(notification.getObject().getAsRelationship()); + if (notification.getOrigin() != null) { + message.setServiceId(notification.getOrigin().getId()); + message.setServiceName(notification.getOrigin().getInbox()); + } + BigDecimal score = getScore(context, notification); + double doubleScoreValue = score != null ? score.doubleValue() : 0d; + ObjectMapper mapper = new ObjectMapper(); + qaEvent = new QAEvent(QAEvent.COAR_NOTIFY_SOURCE, + handleService.findHandle(context, item), item.getID().toString(), itemName, + this.getQaEventTopic(), doubleScoreValue, + mapper.writeValueAsString(message), + new Date()); + qaEventService.store(context, qaEvent); + result = LDNActionStatus.CONTINUE; + } + + return result; + } + + private BigDecimal getScore(Context context, Notification notification) throws SQLException { + + if (notification.getOrigin() == null) { + return BigDecimal.ZERO; + } + + NotifyServiceEntity service = ldnMessageService.findNotifyService(context, notification.getOrigin()); + + if (service == null) { + return BigDecimal.ZERO; + } + + return service.getScore(); + } + + public String getQaEventTopic() { + return qaEventTopic; + } + + public void setQaEventTopic(String qaEventTopic) { + this.qaEventTopic = qaEventTopic; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java b/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java new file mode 100644 index 000000000000..16fb43317e2c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/action/SendLDNMessageAction.java @@ -0,0 +1,118 @@ +/** + * 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.ldn.action; + +import java.net.URI; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.Header; +import org.apache.http.HttpException; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.model.Notification; +import org.dspace.content.Item; +import org.dspace.core.Context; + +/** + * Action to send LDN Message + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class SendLDNMessageAction implements LDNAction { + + private static final Logger log = LogManager.getLogger(SendLDNMessageAction.class); + + private CloseableHttpClient client = null; + + public SendLDNMessageAction() { + HttpClientBuilder builder = HttpClientBuilder.create(); + client = builder + .disableAutomaticRetries() + .setMaxConnTotal(5) + .build(); + } + + public SendLDNMessageAction(CloseableHttpClient client) { + this(); + if (client != null) { + this.client = client; + } + } + + @Override + public LDNActionStatus execute(Context context, Notification notification, Item item) throws Exception { + //TODO authorization with Bearer token should be supported. + + String url = notification.getTarget().getInbox(); + + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader("Content-Type", "application, ld+json"); + ObjectMapper mapper = new ObjectMapper(); + httpPost.setEntity(new StringEntity(mapper.writeValueAsString(notification), "UTF-8")); + + LDNActionStatus result = LDNActionStatus.ABORT; + // NOTE: Github believes there is a "Potential server-side request forgery due to a user-provided value" + // This is a false positive because the LDN Service URL is configured by the user from DSpace. + // See the frontend configuration at [dspace.ui.url]/admin/ldn/services + try ( + CloseableHttpResponse response = client.execute(httpPost); + ) { + if (isSuccessful(response.getStatusLine().getStatusCode())) { + result = LDNActionStatus.CONTINUE; + } else if (isRedirect(response.getStatusLine().getStatusCode())) { + result = handleRedirect(response, httpPost); + } + } catch (Exception e) { + log.error(e); + } + return result; + } + + private boolean isSuccessful(int statusCode) { + return statusCode == HttpStatus.SC_ACCEPTED || + statusCode == HttpStatus.SC_CREATED; + } + + private boolean isRedirect(int statusCode) { + //org.apache.http.HttpStatus has no enum value for 308! + return statusCode == (HttpStatus.SC_TEMPORARY_REDIRECT + 1) || + statusCode == HttpStatus.SC_TEMPORARY_REDIRECT; + } + + private LDNActionStatus handleRedirect(CloseableHttpResponse oldresponse, + HttpPost request) throws HttpException { + Header[] urls = oldresponse.getHeaders(HttpHeaders.LOCATION); + String url = urls.length > 0 && urls[0] != null ? urls[0].getValue() : null; + if (url == null) { + throw new HttpException("Error following redirect, unable to reach" + + " the correct url."); + } + LDNActionStatus result = LDNActionStatus.ABORT; + try { + request.setURI(new URI(url)); + try ( + CloseableHttpResponse response = client.execute(request); + ) { + if (isSuccessful(response.getStatusLine().getStatusCode())) { + return LDNActionStatus.CONTINUE; + } + } + } catch (Exception e) { + log.error("Error following redirect:", e); + } + + return LDNActionStatus.ABORT; + } +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/LDNMessageDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/LDNMessageDao.java new file mode 100644 index 000000000000..c3f4907cce11 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/LDNMessageDao.java @@ -0,0 +1,68 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.dao; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * Database Access Object interface class for the LDNMessage object. + * + * The implementation of this class is responsible for all database calls for + * the LDNMessage object and is autowired by spring + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface LDNMessageDao extends GenericDAO { + + /** + * load the oldest ldn messages considering their {@link org.dspace.app.ldn.LDNMessageEntity#queueLastStartTime} + * @param context + * @param max_attempts consider ldn_message entity with queue_attempts <= max_attempts + * @return ldn message entities to be routed + * @throws SQLException + */ + public List findOldestMessageToProcess(Context context, int max_attempts) throws SQLException; + + /** + * find ldn message entties in processing status and already timed out. + * @param context + * @param max_attempts consider ldn_message entity with queue_attempts <= max_attempts + * @return ldn message entities + * @throws SQLException + */ + public List findProcessingTimedoutMessages(Context context, int max_attempts) throws SQLException; + + /** + * find all ldn messages related to an item + * @param context + * @param item item related to the returned ldn messages + * @param activities involves only this specific group of activities + * @return all ldn messages related to the given item + * @throws SQLException + */ + public List findAllMessagesByItem( + Context context, Item item, String... activities) throws SQLException; + + /** + * find all ldn messages related to an item and to a specific ldn message + * @param context + * @param msg the referring ldn message + * @param item the referring repository item + * @param relatedTypes filter for @see org.dspace.app.ldn.LDNMessageEntity#activityStreamType + * @return all related ldn messages + * @throws SQLException + */ + public List findAllRelatedMessagesByItem( + Context context, LDNMessageEntity msg, Item item, String... relatedTypes) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyPatternToTriggerDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyPatternToTriggerDao.java new file mode 100644 index 000000000000..9ecd1b728a4e --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyPatternToTriggerDao.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.dao; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * This is the Data Access Object for the {@link NotifyPatternToTrigger} object + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface NotifyPatternToTriggerDao extends GenericDAO { + + /** + * find the NotifyPatternToTrigger matched with the provided item + * + * @param context the context + * @param item the item + * @return the NotifyPatternToTrigger matched the provided item + * @throws SQLException if database error + */ + public List findByItem(Context context, Item item) throws SQLException; + + /** + * find the NotifyPatternToTrigger matched with the provided + * item and pattern + * + * @param context the context + * @param item the item + * @param pattern the pattern + * @return the NotifyPatternToTrigger matched the provided + * item and pattern + * @throws SQLException if database error + */ + public List findByItemAndPattern(Context context, Item item, String pattern) + throws SQLException; + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceDao.java new file mode 100644 index 000000000000..9751b3038290 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceDao.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.ldn.dao; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * This is the Data Access Object for the {@link NotifyServiceEntity} object + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface NotifyServiceDao extends GenericDAO { + /** + * find the NotifyServiceEntity matched with the provided ldnUrl + * + * @param context the context + * @param ldnUrl the ldnUrl + * @return the NotifyServiceEntity matched the provided ldnUrl + * @throws SQLException if database error + */ + public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException; + + /** + * find all NotifyServiceEntity matched the provided inbound pattern + * from the related notifyServiceInboundPatterns + * also with 'automatic' equals to false + * + * @param context the context + * @param pattern the ldnUrl + * @return all NotifyServiceEntity matched the provided pattern + * @throws SQLException if database error + */ + public List findManualServicesByInboundPattern(Context context, String pattern) + throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceInboundPatternDao.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceInboundPatternDao.java new file mode 100644 index 000000000000..194d30e79598 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/NotifyServiceInboundPatternDao.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.app.ldn.dao; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.core.Context; +import org.dspace.core.GenericDAO; + +/** + * This is the Data Access Object for the {@link NotifyServiceInboundPattern} object + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface NotifyServiceInboundPatternDao extends GenericDAO { + + /** + * find all notifyServiceInboundPatterns matched with + * the provided notifyServiceEntity and pattern + * + * @param context the context + * @param notifyServiceEntity the notifyServiceEntity + * @param pattern the pattern + * @return all notifyServiceInboundPatterns matched with + * the provided notifyServiceEntity and pattern + * @throws SQLException if database error + */ + public NotifyServiceInboundPattern findByServiceAndPattern(Context context, + NotifyServiceEntity notifyServiceEntity, + String pattern) throws SQLException; + /** + * find all automatic notifyServiceInboundPatterns + * + * @param context the context + * @return all automatic notifyServiceInboundPatterns + * @throws SQLException if database error + */ + List findAutomaticPatterns(Context context) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java new file mode 100644 index 000000000000..0a0fe672fb0a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/LDNMessageDaoImpl.java @@ -0,0 +1,146 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.dao.impl; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.app.ldn.LDNMessageEntity_; +import org.dspace.app.ldn.dao.LDNMessageDao; +import org.dspace.content.Item; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Hibernate implementation of the Database Access Object interface class for + * the LDNMessage object. This class is responsible for all database calls for + * the LDNMessage object and is autowired by spring + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class LDNMessageDaoImpl extends AbstractHibernateDAO implements LDNMessageDao { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNMessageDaoImpl.class); + + @Override + public List findOldestMessageToProcess(Context context, int max_attempts) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class); + Root root = criteriaQuery.from(LDNMessageEntity.class); + criteriaQuery.select(root); + List andPredicates = new ArrayList<>(3); + andPredicates + .add(criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_QUEUED)); + andPredicates.add(criteriaBuilder.lessThan(root.get(LDNMessageEntity_.queueAttempts), max_attempts)); + andPredicates.add(criteriaBuilder.lessThan(root.get(LDNMessageEntity_.queueTimeout), new Date())); + criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {}))); + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts))); + orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime))); + criteriaQuery.orderBy(orderList); + List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1); + if (result == null || result.isEmpty()) { + log.debug("No LDN messages found to be processed"); + } + return result; + } + + @Override + public List findProcessingTimedoutMessages(Context context, int max_attempts) + throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class); + Root root = criteriaQuery.from(LDNMessageEntity.class); + criteriaQuery.select(root); + List andPredicates = new ArrayList<>(3); + andPredicates.add( + criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSING)); + andPredicates.add(criteriaBuilder.lessThanOrEqualTo(root.get(LDNMessageEntity_.queueAttempts), max_attempts)); + andPredicates.add(criteriaBuilder.lessThan(root.get(LDNMessageEntity_.queueTimeout), new Date())); + criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {}))); + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts))); + orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime))); + criteriaQuery.orderBy(orderList); + List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1); + if (result == null || result.isEmpty()) { + log.debug("No LDN messages found to be processed"); + } + return result; + } + + @Override + public List findAllRelatedMessagesByItem( + Context context, LDNMessageEntity msg, Item item, String... relatedTypes) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class); + Root root = criteriaQuery.from(LDNMessageEntity.class); + criteriaQuery.select(root); + List andPredicates = new ArrayList<>(); + Predicate relatedtypePredicate = null; + andPredicates.add( + criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED)); + andPredicates.add( + criteriaBuilder.isNull(root.get(LDNMessageEntity_.target))); + andPredicates.add( + criteriaBuilder.equal(root.get(LDNMessageEntity_.inReplyTo), msg)); + if (relatedTypes != null && relatedTypes.length > 0) { + relatedtypePredicate = root.get(LDNMessageEntity_.activityStreamType).in(relatedTypes); + andPredicates.add(relatedtypePredicate); + } + criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {}))); + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime))); + orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts))); + criteriaQuery.orderBy(orderList); + List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1); + if (result == null || result.isEmpty()) { + log.debug("No LDN messages ACK found to be processed"); + } + return result; + } + + @Override + public List findAllMessagesByItem( + Context context, Item item, String... activities) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, LDNMessageEntity.class); + Root root = criteriaQuery.from(LDNMessageEntity.class); + criteriaQuery.select(root); + List andPredicates = new ArrayList<>(); + Predicate activityPredicate = null; + andPredicates.add( + criteriaBuilder.equal(root.get(LDNMessageEntity_.queueStatus), LDNMessageEntity.QUEUE_STATUS_PROCESSED)); + andPredicates.add( + criteriaBuilder.equal(root.get(LDNMessageEntity_.object), item)); + if (activities != null && activities.length > 0) { + activityPredicate = root.get(LDNMessageEntity_.activityStreamType).in(activities); + andPredicates.add(activityPredicate); + } + criteriaQuery.where(criteriaBuilder.and(andPredicates.toArray(new Predicate[] {}))); + List orderList = new LinkedList<>(); + orderList.add(criteriaBuilder.asc(root.get(LDNMessageEntity_.queueLastStartTime))); + orderList.add(criteriaBuilder.desc(root.get(LDNMessageEntity_.queueAttempts))); + criteriaQuery.orderBy(orderList); + List result = list(context, criteriaQuery, false, LDNMessageEntity.class, -1, -1); + if (result == null || result.isEmpty()) { + log.debug("No LDN messages found"); + } + return result; + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyPatternToTriggerDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyPatternToTriggerDaoImpl.java new file mode 100644 index 000000000000..47c584518b14 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyPatternToTriggerDaoImpl.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.ldn.dao.impl; + +import java.sql.SQLException; +import java.util.List; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.NotifyPatternToTrigger_; +import org.dspace.app.ldn.dao.NotifyPatternToTriggerDao; +import org.dspace.content.Item; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Implementation of {@link NotifyPatternToTriggerDao}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyPatternToTriggerDaoImpl extends AbstractHibernateDAO + implements NotifyPatternToTriggerDao { + + @Override + public List findByItem(Context context, Item item) + throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyPatternToTrigger.class); + Root notifyServiceEntityRoot = criteriaQuery.from(NotifyPatternToTrigger.class); + criteriaQuery.select(notifyServiceEntityRoot); + criteriaQuery.where(criteriaBuilder.equal( + notifyServiceEntityRoot.get(NotifyPatternToTrigger_.item), item)); + return list(context, criteriaQuery, false, NotifyPatternToTrigger.class, -1, -1); + } + @Override + public List findByItemAndPattern(Context context, Item item, String pattern) + throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyPatternToTrigger.class); + Root notifyServiceEntityRoot = criteriaQuery.from(NotifyPatternToTrigger.class); + criteriaQuery.select(notifyServiceEntityRoot); + criteriaQuery.where(criteriaBuilder.and( + criteriaBuilder.equal( + notifyServiceEntityRoot.get(NotifyPatternToTrigger_.item), item), + criteriaBuilder.equal( + notifyServiceEntityRoot.get(NotifyPatternToTrigger_.pattern), pattern) + )); + return list(context, criteriaQuery, false, NotifyPatternToTrigger.class, -1, -1); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceDaoImpl.java new file mode 100644 index 000000000000..cac804ef0c1f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceDaoImpl.java @@ -0,0 +1,62 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.dao.impl; + +import java.sql.SQLException; +import java.util.List; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Root; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceEntity_; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.NotifyServiceInboundPattern_; +import org.dspace.app.ldn.dao.NotifyServiceDao; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Implementation of {@link NotifyServiceDao}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceDaoImpl extends AbstractHibernateDAO implements NotifyServiceDao { + + @Override + public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceEntity.class); + Root notifyServiceEntityRoot = criteriaQuery.from(NotifyServiceEntity.class); + criteriaQuery.select(notifyServiceEntityRoot); + criteriaQuery.where(criteriaBuilder.equal( + notifyServiceEntityRoot.get(NotifyServiceEntity_.ldnUrl), ldnUrl)); + return uniqueResult(context, criteriaQuery, false, NotifyServiceEntity.class); + } + + @Override + public List findManualServicesByInboundPattern(Context context, String pattern) + throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceEntity.class); + Root notifyServiceEntityRoot = criteriaQuery.from(NotifyServiceEntity.class); + + Join notifyServiceInboundPatternJoin = + notifyServiceEntityRoot.join(NotifyServiceEntity_.inboundPatterns); + + criteriaQuery.select(notifyServiceEntityRoot); + criteriaQuery.where(criteriaBuilder.and( + criteriaBuilder.equal( + notifyServiceInboundPatternJoin.get(NotifyServiceInboundPattern_.pattern), pattern), + criteriaBuilder.equal( + notifyServiceInboundPatternJoin.get(NotifyServiceInboundPattern_.automatic), false))); + + return list(context, criteriaQuery, false, NotifyServiceEntity.class, -1, -1); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceInboundPatternDaoImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceInboundPatternDaoImpl.java new file mode 100644 index 000000000000..5168fd0bedf8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/dao/impl/NotifyServiceInboundPatternDaoImpl.java @@ -0,0 +1,59 @@ +/** + * 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.ldn.dao.impl; + +import java.sql.SQLException; +import java.util.List; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.NotifyServiceInboundPattern_; +import org.dspace.app.ldn.dao.NotifyServiceInboundPatternDao; +import org.dspace.core.AbstractHibernateDAO; +import org.dspace.core.Context; + +/** + * Implementation of {@link NotifyServiceInboundPatternDao}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceInboundPatternDaoImpl + extends AbstractHibernateDAO implements NotifyServiceInboundPatternDao { + + @Override + public NotifyServiceInboundPattern findByServiceAndPattern(Context context, NotifyServiceEntity notifyServiceEntity, + String pattern) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceInboundPattern.class); + Root inboundPatternRoot = criteriaQuery.from(NotifyServiceInboundPattern.class); + criteriaQuery.select(inboundPatternRoot); + criteriaQuery.where(criteriaBuilder.and( + criteriaBuilder.equal( + inboundPatternRoot.get(NotifyServiceInboundPattern_.notifyService), notifyServiceEntity), + criteriaBuilder.equal( + inboundPatternRoot.get(NotifyServiceInboundPattern_.pattern), pattern) + )); + return uniqueResult(context, criteriaQuery, false, NotifyServiceInboundPattern.class); + } + + @Override + public List findAutomaticPatterns(Context context) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, NotifyServiceInboundPattern.class); + Root inboundPatternRoot = criteriaQuery.from(NotifyServiceInboundPattern.class); + criteriaQuery.select(inboundPatternRoot); + criteriaQuery.where( + criteriaBuilder.equal( + inboundPatternRoot.get(NotifyServiceInboundPattern_.automatic), true) + ); + return list(context, criteriaQuery, false, NotifyServiceInboundPattern.class, -1, -1); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactory.java new file mode 100644 index 000000000000..bbf521123bca --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactory.java @@ -0,0 +1,29 @@ +/** + * 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.ldn.factory; + +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Abstract factory to get services for the NotifyService package, + * use NotifyServiceFactory.getInstance() to retrieve an implementation + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) + */ +public abstract class LDNMessageServiceFactory { + + public abstract LDNMessageService getLDNMessageService(); + + public static LDNMessageServiceFactory getInstance() { + return DSpaceServicesFactory.getInstance() + .getServiceManager() + .getServiceByName("ldnMessageServiceFactory", + LDNMessageServiceFactory.class); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactoryImpl.java new file mode 100644 index 000000000000..a001ece04069 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNMessageServiceFactoryImpl.java @@ -0,0 +1,29 @@ +/** + * 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.ldn.factory; + +import org.dspace.app.ldn.service.LDNMessageService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory implementation to get services for the notifyservices package, use + * NotifyServiceFactory.getInstance() to retrieve an implementation + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) + */ +public class LDNMessageServiceFactoryImpl extends LDNMessageServiceFactory { + + @Autowired(required = true) + private LDNMessageService ldnMessageService; + + @Override + public LDNMessageService getLDNMessageService() { + return ldnMessageService; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactory.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactory.java new file mode 100644 index 000000000000..4b0f107d2498 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactory.java @@ -0,0 +1,29 @@ +/** + * 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.ldn.factory; + +import org.dspace.app.ldn.LDNRouter; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Abstract factory to get services for the ldn package, use + * LDNRouterFactory.getInstance() to retrieve an implementation + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) + */ +public abstract class LDNRouterFactory { + + public abstract LDNRouter getLDNRouter(); + + public static LDNRouterFactory getInstance() { + return DSpaceServicesFactory.getInstance() + .getServiceManager() + .getServiceByName("ldnRouter", + LDNRouterFactory.class); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactoryImpl.java new file mode 100644 index 000000000000..f411b9d935d0 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/LDNRouterFactoryImpl.java @@ -0,0 +1,28 @@ +/** + * 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.ldn.factory; + +import org.dspace.app.ldn.LDNRouter; +import org.springframework.beans.factory.annotation.Autowired; +/** + * Factory implementation to get services for the ldn package, + * use ldnRouter spring bean instance to retrieve an implementation + * + * @author Francesco Bacchelli (mohamed.eskander at 4science.com) + */ +public class LDNRouterFactoryImpl extends LDNRouterFactory { + + @Autowired(required = true) + private LDNRouter ldnRouter; + + @Override + public LDNRouter getLDNRouter() { + return ldnRouter; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactory.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactory.java new file mode 100644 index 000000000000..ea488ca25031 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactory.java @@ -0,0 +1,36 @@ +/** + * 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.ldn.factory; + +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Abstract factory to get services for the NotifyService package, + * use NotifyServiceFactory.getInstance() to retrieve an implementation + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public abstract class NotifyServiceFactory { + + public abstract NotifyService getNotifyService(); + + public abstract NotifyServiceInboundPatternService getNotifyServiceInboundPatternService(); + + public abstract NotifyPatternToTriggerService getNotifyPatternToTriggerService(); + + public abstract LDNMessageService getLDNMessageService(); + + public static NotifyServiceFactory getInstance() { + return DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( + "notifyServiceFactory", NotifyServiceFactory.class); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactoryImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactoryImpl.java new file mode 100644 index 000000000000..84e15ee261a2 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/factory/NotifyServiceFactoryImpl.java @@ -0,0 +1,56 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.factory; + +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Factory implementation to get services for the notifyservices package, + * use NotifyServiceFactory.getInstance() to retrieve an implementation + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceFactoryImpl extends NotifyServiceFactory { + + @Autowired(required = true) + private NotifyService notifyService; + + @Autowired(required = true) + private NotifyServiceInboundPatternService notifyServiceInboundPatternService; + + @Autowired(required = true) + private NotifyPatternToTriggerService notifyPatternToTriggerService; + + @Autowired(required = true) + private LDNMessageService ldnMessageService; + + @Override + public NotifyService getNotifyService() { + return notifyService; + } + + @Override + public NotifyServiceInboundPatternService getNotifyServiceInboundPatternService() { + return notifyServiceInboundPatternService; + } + + @Override + public NotifyPatternToTriggerService getNotifyPatternToTriggerService() { + return notifyPatternToTriggerService; + } + + @Override + public LDNMessageService getLDNMessageService() { + return ldnMessageService; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Actor.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Actor.java new file mode 100644 index 000000000000..a81cc3f8008a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Actor.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.ldn.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * used to map @see org.dspace.app.ldn.model.Notification + */ +public class Actor extends Base { + + @JsonProperty("name") + private String name; + + /** + * + */ + public Actor() { + super(); + } + + /** + * @return String + */ + public String getName() { + return name; + } + + /** + * @param name + */ + public void setName(String name) { + this.name = name; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Base.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Base.java new file mode 100644 index 000000000000..6ddaae110e1f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Base.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.ldn.model; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * used to map @see org.dspace.app.ldn.model.Notification + */ +@JsonInclude(Include.NON_EMPTY) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Base { + + @JsonProperty("id") + private String id; + + @JsonProperty("type") + private Set type; + + /** + * + */ + public Base() { + type = new HashSet<>(); + } + + /** + * @return String + */ + public String getId() { + return id; + } + + /** + * @param id + */ + public void setId(String id) { + this.id = id; + } + + /** + * @return Set + */ + public Set getType() { + return type; + } + + /** + * @param type + */ + public void setType(java.lang.Object type) { + if (type instanceof String) { + this.type.add((String) type); + } else if (type instanceof Collection) { + this.type.addAll((Collection) type); + } + } + + /** + * @param type + */ + public void addType(String type) { + this.type.add(type); + } + + /** + * @return int + */ + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + /** + * @param obj + * @return boolean + */ + @Override + public boolean equals(java.lang.Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Base other = (Base) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Citation.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Citation.java new file mode 100644 index 000000000000..c6629f5e7b91 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Citation.java @@ -0,0 +1,59 @@ +/** + * 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.ldn.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + + +/** + * used to map @see org.dspace.app.ldn.model.Notification + */ +public class Citation extends Base { + + @JsonProperty("ietf:cite-as") + private String ietfCiteAs; + + @JsonProperty("url") + private Url url; + + /** + * + */ + public Citation() { + super(); + } + + /** + * @return String + */ + public String getIetfCiteAs() { + return ietfCiteAs; + } + + /** + * @param ietfCiteAs + */ + public void setIetfCiteAs(String ietfCiteAs) { + this.ietfCiteAs = ietfCiteAs; + } + + /** + * @return Url + */ + public Url getUrl() { + return url; + } + + /** + * @param url + */ + public void setUrl(Url url) { + this.url = url; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Context.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Context.java new file mode 100644 index 000000000000..78fe37341697 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Context.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.ldn.model; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; + + +/** + * used to map @see org.dspace.app.ldn.model.Notification + */ +public class Context extends Citation { + + @JsonProperty("IsSupplementedBy") + private List isSupplementedBy; + + @JsonProperty("IsSupplementTo") + private List isSupplementTo; + + /** + * + */ + public Context() { + super(); + } + + /** + * @return List + */ + public List getIsSupplementedBy() { + return isSupplementedBy; + } + + /** + * @param isSupplementedBy + */ + public void setIsSupplementedBy(List isSupplementedBy) { + this.isSupplementedBy = isSupplementedBy; + } + + /** + * @return List + */ + public List getIsSupplementTo() { + return isSupplementTo; + } + + /** + * @param isSupplementTo + */ + public void setIsSupplementTo(List isSupplementTo) { + this.isSupplementTo = isSupplementTo; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Notification.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Notification.java new file mode 100644 index 000000000000..52bc9840f42d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Notification.java @@ -0,0 +1,159 @@ +/** + * 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.ldn.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * the json object from witch @see org.dspace.app.ldn.LDNMessageEntity are created. + * see official coar doc + */ +@JsonPropertyOrder(value = { + "@context", + "id", + "type", + "actor", + "context", + "object", + "origin", + "target", + "inReplyTo" +}) +public class Notification extends Base { + + @JsonProperty("@context") + private String[] c = new String[] { + "https://purl.org/coar/notify", + "https://www.w3.org/ns/activitystreams" + }; + + @JsonProperty("actor") + private Actor actor; + + @JsonProperty("context") + private Context context; + + @JsonProperty("object") + private Object object; + + @JsonProperty("origin") + private Service origin; + + @JsonProperty("target") + private Service target; + + @JsonProperty("inReplyTo") + private String inReplyTo; + + /** + * + */ + public Notification() { + super(); + } + + /** + * @return String[] + */ + public String[] getC() { + return c; + } + + /** + * @param c + */ + public void setC(String[] c) { + this.c = c; + } + + /** + * @return Actor + */ + public Actor getActor() { + return actor; + } + + /** + * @param actor + */ + public void setActor(Actor actor) { + this.actor = actor; + } + + /** + * @return Context + */ + public Context getContext() { + return context; + } + + /** + * @param context + */ + public void setContext(Context context) { + this.context = context; + } + + /** + * @return Object + */ + public Object getObject() { + return object; + } + + /** + * @param object + */ + public void setObject(Object object) { + this.object = object; + } + + /** + * @return Service + */ + public Service getOrigin() { + return origin; + } + + /** + * @param origin + */ + public void setOrigin(Service origin) { + this.origin = origin; + } + + /** + * @return Service + */ + public Service getTarget() { + return target; + } + + /** + * @param target + */ + public void setTarget(Service target) { + this.target = target; + } + + /** + * @return String + */ + public String getInReplyTo() { + return inReplyTo; + } + + /** + * @param inReplyTo + */ + public void setInReplyTo(String inReplyTo) { + this.inReplyTo = inReplyTo; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java new file mode 100644 index 000000000000..0302b528aa8d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatus.java @@ -0,0 +1,63 @@ +/** + * 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.ldn.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder(value = { + "itemuuid", + "notifyStatus" +}) + +/** + * item requests of LDN messages of type + * + * "Offer", "coar-notify:EndorsementAction" + * "Offer", "coar-notify:IngestAction" + * "Offer", "coar-notify:ReviewAction" + * + * and their acknownledgements - if any + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) + */ +public class NotifyRequestStatus extends Base { + + private UUID itemUuid; + + private List notifyStatus; + + public NotifyRequestStatus() { + super(); + this.notifyStatus = new ArrayList(); + } + + public UUID getItemUuid() { + return itemUuid; + } + + public void setItemUuid(UUID itemUuid) { + this.itemUuid = itemUuid; + } + + public void addRequestStatus(RequestStatus rs) { + this.notifyStatus.add(rs); + } + + public List getNotifyStatus() { + return notifyStatus; + } + + public void setNotifyStatus(List notifyStatus) { + this.notifyStatus = notifyStatus; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.java new file mode 100644 index 000000000000..437c624f84d8 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/NotifyRequestStatusEnum.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.ldn.model; +/** + * REQUESTED means acknowledgements not received yet + * ACCEPTED means acknowledgements of "Accept" type received + * REJECTED means ack of "TentativeReject" type received + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) + */ +public enum NotifyRequestStatusEnum { + REJECTED, ACCEPTED, REQUESTED +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Object.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Object.java new file mode 100644 index 000000000000..8913af47dae1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Object.java @@ -0,0 +1,74 @@ +/** + * 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.ldn.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * used to map @see org.dspace.app.ldn.model.Notification + */ +public class Object extends Citation { + + @JsonProperty("as:object") + private String asObject; + + @JsonProperty("as:relationship") + private String asRelationship; + + @JsonProperty("as:subject") + private String asSubject; + + @JsonProperty("sorg:name") + private String title; + + /** + * + */ + public Object() { + super(); + } + + /** + * @return String + */ + public String getTitle() { + return title; + } + + /** + * @param title + */ + public void setTitle(String title) { + this.title = title; + } + + public String getAsObject() { + return asObject; + } + + public void setAsObject(String asObject) { + this.asObject = asObject; + } + + public String getAsRelationship() { + return asRelationship; + } + + public void setAsRelationship(String asRelationship) { + this.asRelationship = asRelationship; + } + + public String getAsSubject() { + return asSubject; + } + + public void setAsSubject(String asSubject) { + this.asSubject = asSubject; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.java new file mode 100644 index 000000000000..d19369830787 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/RequestStatus.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.app.ldn.model; + +/** + * Informations about the Offer and Acknowledgements targeting a specified Item + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.com) + */ +public class RequestStatus { + + private String serviceName; + private String serviceUrl; + private String offerType; + private NotifyRequestStatusEnum status; + + public String getServiceName() { + return serviceName; + } + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + public String getServiceUrl() { + return serviceUrl; + } + public void setServiceUrl(String serviceUrl) { + this.serviceUrl = serviceUrl; + } + public NotifyRequestStatusEnum getStatus() { + return status; + } + public void setStatus(NotifyRequestStatusEnum status) { + this.status = status; + } + public String getOfferType() { + return offerType; + } + public void setOfferType(String offerType) { + this.offerType = offerType; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Service.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Service.java new file mode 100644 index 000000000000..cdd3ba5bb536 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Service.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.ldn.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * used to map @see org.dspace.app.ldn.model.Notification + */ +public class Service extends Base { + + @JsonProperty("inbox") + private String inbox; + + /** + * + */ + public Service() { + super(); + } + + /** + * @return String + */ + public String getInbox() { + return inbox; + } + + /** + * @param inbox + */ + public void setInbox(String inbox) { + this.inbox = inbox; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/model/Url.java b/dspace-api/src/main/java/org/dspace/app/ldn/model/Url.java new file mode 100644 index 000000000000..47093e02e44b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/model/Url.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.ldn.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * used to map @see org.dspace.app.ldn.model.Citation + */ +public class Url extends Base { + + @JsonProperty("mediaType") + private String mediaType; + + /** + * + */ + public Url() { + super(); + } + + /** + * @return String + */ + public String getMediaType() { + return mediaType; + } + + /** + * @param mediaType + */ + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNContextRepeater.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNContextRepeater.java new file mode 100644 index 000000000000..a5ef0957ff8f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNContextRepeater.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.app.ldn.processor; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.model.Context; +import org.dspace.app.ldn.model.Notification; + +/** + * Context repeater to iterate over array context properties of a received + * notification. The returned notification iterator is a notification with + * context array elements hoisted onto the root of the notification context. + */ +public class LDNContextRepeater { + + private final static Logger log = LogManager.getLogger(LDNContextRepeater.class); + + private final static String CONTEXT = "context"; + + private String repeatOver; + + /** + * @return String + */ + public String getRepeatOver() { + return repeatOver; + } + + /** + * @param repeatOver + */ + public void setRepeatOver(String repeatOver) { + this.repeatOver = repeatOver; + } + + /** + * @param notification + * @return Iterator + */ + public Iterator iterator(Notification notification) { + return new NotificationIterator(notification, repeatOver); + } + + /** + * Private inner class defining the notification iterator. + */ + private class NotificationIterator implements Iterator { + + private final List notifications; + + /** + * Convert notification to JsonNode in order to clone for each context array + * element. Each element is then hoisted to the root of the cloned notification + * context. + * + * @param notification received notification + * @param repeatOver which context property to repeat over + */ + private NotificationIterator(Notification notification, String repeatOver) { + this.notifications = new ArrayList<>(); + + if (Objects.nonNull(repeatOver)) { + ObjectMapper objectMapper = new ObjectMapper(); + + JsonNode notificationNode = objectMapper.valueToTree(notification); + + log.debug("Notification {}", notificationNode); + + JsonNode topContextNode = notificationNode.get(CONTEXT); + if (topContextNode.isNull()) { + log.warn("Notification is missing context"); + return; + } + + JsonNode contextArrayNode = topContextNode.get(repeatOver); + if (contextArrayNode == null || contextArrayNode.isNull()) { + log.error("Notification context {} is not defined", repeatOver); + return; + } + + if (contextArrayNode.isArray()) { + + for (JsonNode contextNode : ((ArrayNode) contextArrayNode)) { + + try { + Context context = objectMapper.treeToValue(contextNode, Context.class); + + Notification copy = objectMapper.treeToValue(notificationNode, Notification.class); + + copy.setContext(context); + + this.notifications.add(copy); + } catch (JsonProcessingException e) { + log.error("Failed to copy notification"); + } + + } + + } else { + log.error("Notification context {} is not an array", repeatOver); + } + + } else { + this.notifications.add(notification); + } + } + + /** + * @return boolean + */ + @Override + public boolean hasNext() { + return !this.notifications.isEmpty(); + } + + /** + * @return Notification + */ + @Override + public Notification next() { + return this.notifications.remove(0); + } + + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java new file mode 100644 index 000000000000..223c8b7a3422 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNMetadataProcessor.java @@ -0,0 +1,187 @@ +/** + * 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.ldn.processor; + +import static java.lang.String.format; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpResponseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.action.LDNAction; +import org.dspace.app.ldn.action.LDNActionStatus; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.utility.LDNUtils; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.core.Context; +import org.dspace.handle.service.HandleService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Linked Data Notification metadata processor for consuming notifications. The + * storage of notification details are within item metadata. + */ +public class LDNMetadataProcessor implements LDNProcessor { + + private final static Logger log = LogManager.getLogger(LDNMetadataProcessor.class); + + @Autowired + private ItemService itemService; + + @Autowired + private HandleService handleService; + + private LDNContextRepeater repeater = new LDNContextRepeater(); + + private List actions = new ArrayList<>(); + + /** + * Initialize velocity engine for templating. + */ + private LDNMetadataProcessor() { + + } + + /** + * Process notification by repeating over context, processing each context + * notification, and running actions post processing. + * + * @param notification received notification + * @throws Exception something went wrong processing the notification + */ + @Override + public void process(Context context, Notification notification) throws Exception { + Item item = lookupItem(context, notification); + runActions(context, notification, item); + } + + /** + * Run all actions defined for the processor. + * + * @param notification current context notification + * @param item associated item + * + * @return ActionStatus result status of running the action + * + * @throws Exception failed execute the action + */ + private LDNActionStatus runActions(Context context, Notification notification, Item item) throws Exception { + LDNActionStatus operation = LDNActionStatus.CONTINUE; + for (LDNAction action : actions) { + log.info("Running action {} for notification {} {}", + action.getClass().getSimpleName(), + notification.getId(), + notification.getType()); + + operation = action.execute(context, notification, item); + if (operation == LDNActionStatus.ABORT) { + break; + } + } + + return operation; + } + + /** + * @return LDNContextRepeater + */ + public LDNContextRepeater getRepeater() { + return repeater; + } + + /** + * @param repeater + */ + public void setRepeater(LDNContextRepeater repeater) { + this.repeater = repeater; + } + + /** + * @return List + */ + public List getActions() { + return actions; + } + + /** + * @param actions + */ + public void setActions(List actions) { + this.actions = actions; + } + + /** + * Lookup associated item to the notification context. If UUID in URL, lookup bu + * UUID, else lookup by handle. + * + * @param context current context + * @param notification current context notification + * + * @return Item associated item + * + * @throws SQLException failed to lookup item + * @throws HttpResponseException redirect failure + */ + private Item lookupItem(Context context, Notification notification) throws SQLException, HttpResponseException { + Item item = null; + + String url = null; + if (notification.getContext() != null) { + url = notification.getContext().getId(); + } else { + url = notification.getObject().getId(); + } + + log.info("Looking up item {}", url); + + if (LDNUtils.hasUUIDInURL(url)) { + UUID uuid = LDNUtils.getUUIDFromURL(url); + + item = itemService.find(context, uuid); + + if (Objects.isNull(item)) { + throw new HttpResponseException(HttpStatus.SC_NOT_FOUND, + format("Item with uuid %s not found", uuid)); + } + + } else { + String handle = handleService.resolveUrlToHandle(context, url); + + if (Objects.isNull(handle)) { + throw new HttpResponseException(HttpStatus.SC_NOT_FOUND, + format("Handle not found for %s", url)); + } + + DSpaceObject object = handleService.resolveToObject(context, handle); + + if (Objects.isNull(object)) { + throw new HttpResponseException(HttpStatus.SC_NOT_FOUND, + format("Item with handle %s not found", handle)); + } + + if (object.getType() == Constants.ITEM) { + item = (Item) object; + } else { + throw new HttpResponseException(HttpStatus.SC_UNPROCESSABLE_ENTITY, + format("Handle %s does not resolve to an item", handle)); + } + } + + return item; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNProcessor.java b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNProcessor.java new file mode 100644 index 000000000000..279ec5cedc4b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/processor/LDNProcessor.java @@ -0,0 +1,25 @@ +/** + * 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.ldn.processor; + +import org.dspace.app.ldn.model.Notification; +import org.dspace.core.Context; + +/** + * Processor interface to allow for custom implementations of process. + */ +public interface LDNProcessor { + + /** + * Process received notification. + * + * @param notification received notification + * @throws Exception something went wrong processing the notification + */ + public void process(Context context, Notification notification) throws Exception; +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java new file mode 100644 index 000000000000..c7d3f588881f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/LDNMessageService.java @@ -0,0 +1,148 @@ +/** + * 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.ldn.service; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.model.NotifyRequestStatus; +import org.dspace.app.ldn.model.Service; +import org.dspace.content.Item; +import org.dspace.core.Context; + +/** + * Service interface class for the {@link LDNMessageEntity} object. + * + * @author Mohamed Eskander (mohamed.eskander at 4science dot it) + */ +public interface LDNMessageService { + + /** + * find the ldn message by id + * + * @param context the context + * @param id the uri + * @return the ldn message by id + * @throws SQLException If something goes wrong in the database + */ + public LDNMessageEntity find(Context context, String id) throws SQLException; + + /** + * find all ldn messages + * + * @param context the context + * @return all ldn messages by id + * @throws SQLException If something goes wrong in the database + */ + public List findAll(Context context) throws SQLException; + + /** + * Creates a new LDNMessage + * + * @param context The DSpace context + * @param id the uri + * @return the created LDN Message + * @throws SQLException If something goes wrong in the database + */ + public LDNMessageEntity create(Context context, String id) throws SQLException; + + /** + * Creates a new LDNMessage + * + * @param context The DSpace context + * @param notification the requested notification + * @param sourceIp the source ip + * @return the created LDN Message + * @throws SQLException If something goes wrong in the database + */ + public LDNMessageEntity create(Context context, Notification notification, String sourceIp) throws SQLException; + + /** + * Update the provided LDNMessage + * + * @param context The DSpace context + * @param ldnMessage the LDNMessage + * @throws SQLException If something goes wrong in the database + */ + public void update(Context context, LDNMessageEntity ldnMessage) throws SQLException; + + /** + * Find the oldest queued LDNMessages that still can be elaborated + * + * @return list of LDN messages + * @param context The DSpace context + * @throws SQLException If something goes wrong in the database + */ + public List findOldestMessagesToProcess(Context context) throws SQLException; + + /** + * Find all messages in the queue with the Processing status but timed-out + * + * @return all the LDN Messages to be fixed on their queue_ attributes + * @param context The DSpace context + * @throws SQLException If something goes wrong in the database + */ + public List findProcessingTimedoutMessages(Context context) throws SQLException; + + /** + * Find all messages in the queue with the Processing status but timed-out and modify their queue_status + * considering the queue_attempts + * + * @return number of messages fixed + * @param context The DSpace context + */ + public int checkQueueMessageTimeout(Context context); + + /** + * Elaborates the oldest enqueued message + * + * @return number of messages fixed + * @param context The DSpace context + */ + public int extractAndProcessMessageFromQueue(Context context) throws SQLException; + + /** + * find the related notify service entity + * + * @param context the context + * @param service the service + * @return the NotifyServiceEntity + * @throws SQLException if something goes wrong + */ + public NotifyServiceEntity findNotifyService(Context context, Service service) throws SQLException; + + /** + * find the ldn messages of Requests by item uuid + * + * @param context the context + * @param item the item + * @return the item requests object + * @throws SQLException If something goes wrong in the database + */ + public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws SQLException; + + /** + * delete the provided ldn message + * + * @param context the context + * @param ldnMessage the ldn message + * @throws SQLException if something goes wrong + */ + public void delete(Context context, LDNMessageEntity ldnMessage) throws SQLException; + + /** + * check if IP number is included in the configured ip-range on the Notify Service + * + * @param origin the Notify Service entity + * @param sourceIp the ip to evaluate + */ + public boolean isValidIp(NotifyServiceEntity origin, String sourceIp); +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyPatternToTriggerService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyPatternToTriggerService.java new file mode 100644 index 000000000000..c2c3de7e06d5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyPatternToTriggerService.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.ldn.service; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.content.Item; +import org.dspace.core.Context; + +/** + * Service interface class for the {@link NotifyPatternToTrigger} object. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface NotifyPatternToTriggerService { + + /** + * find all notify patterns to be triggered + * + * @param context the context + * @return all notify patterns to be trigger + * @throws SQLException if database error + */ + public List findAll(Context context) throws SQLException; + + /** + * find list of Notify Patterns To be Triggered by item + * + * @param context the context + * @param item the item of NotifyPatternToTrigger + * @return the matched NotifyPatternToTrigger list by item + * @throws SQLException if database error + */ + public List findByItem(Context context, Item item) + throws SQLException; + + /** + * find list of Notify Patterns To be Triggered by item and pattern + * + * @param context the context + * @param item the item of NotifyPatternToTrigger + * @param pattern the pattern of NotifyPatternToTrigger + * + * @return the matched NotifyPatternToTrigger list by item and pattern + * @throws SQLException if database error + */ + public List findByItemAndPattern(Context context, Item item, String pattern) + throws SQLException; + + /** + * create new notifyPatternToTrigger + * + * @param context the context + * @return the created NotifyPatternToTrigger + * @throws SQLException if database error + */ + public NotifyPatternToTrigger create(Context context) throws SQLException; + + /** + * update the provided notifyPatternToTrigger + * + * @param context the context + * @param notifyPatternToTrigger the notifyPatternToTrigger + * @throws SQLException if database error + */ + public void update(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException; + + /** + * delete the provided notifyPatternToTrigger + * + * @param context the context + * @param notifyPatternToTrigger the notifyPatternToTrigger + * @throws SQLException if database error + */ + public void delete(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException; + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyService.java new file mode 100644 index 000000000000..6ff4c34780c5 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyService.java @@ -0,0 +1,91 @@ +/** + * 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.ldn.service; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.core.Context; + +/** + * Service interface class for the {@link NotifyServiceEntity} object. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface NotifyService { + + /** + * find all notify service entities + * + * @param context the context + * @return all notify service entities + * @throws SQLException if database error + */ + public List findAll(Context context) throws SQLException; + + /** + * find one NotifyServiceEntity by id + * + * @param context the context + * @param id the id of NotifyServiceEntity + * @return the matched NotifyServiceEntity by id + * @throws SQLException if database error + */ + public NotifyServiceEntity find(Context context, Integer id) throws SQLException; + + /** + * create new notifyServiceEntity + * + * @param context the context + * @return the created NotifyServiceEntity + * @throws SQLException if database error + */ + public NotifyServiceEntity create(Context context) throws SQLException; + + /** + * update the provided notifyServiceEntity + * + * @param context the context + * @param notifyServiceEntity the notifyServiceEntity + * @throws SQLException if database error + */ + public void update(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException; + + /** + * delete the provided notifyServiceEntity + * + * @param context the context + * @param notifyServiceEntity the notifyServiceEntity + * @throws SQLException if database error + */ + public void delete(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException; + + /** + * find the NotifyServiceEntity matched with the provided ldnUrl + * + * @param context the context + * @param ldnUrl the ldnUrl + * @return the NotifyServiceEntity matched the provided ldnUrl + * @throws SQLException if database error + */ + public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException; + + /** + * find all NotifyServiceEntity matched the provided inbound pattern + * from its related notifyServiceInboundPatterns + * also with 'automatic' equals to false + * + * @param context the context + * @param pattern the ldnUrl + * @return all NotifyServiceEntity matched the provided pattern + * @throws SQLException if database error + */ + public List findManualServicesByInboundPattern(Context context, String pattern) + throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyServiceInboundPatternService.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyServiceInboundPatternService.java new file mode 100644 index 000000000000..8cd92d45dd30 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/NotifyServiceInboundPatternService.java @@ -0,0 +1,76 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.service; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.core.Context; + +/** + * Service interface class for the {@link NotifyServiceInboundPattern} object. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface NotifyServiceInboundPatternService { + + /** + * find all notifyServiceInboundPatterns matched with + * the provided notifyServiceEntity and pattern + * + * @param context the context + * @param notifyServiceEntity the notifyServiceEntity + * @param pattern the pattern + * @return all notifyServiceInboundPatterns matched with + * the provided notifyServiceEntity and pattern + * @throws SQLException if database error + */ + public NotifyServiceInboundPattern findByServiceAndPattern(Context context, + NotifyServiceEntity notifyServiceEntity, + String pattern) throws SQLException; + + /** + * find all automatic notifyServiceInboundPatterns + * + * @param context the context + * @return all automatic notifyServiceInboundPatterns + * @throws SQLException if database error + */ + public List findAutomaticPatterns(Context context) throws SQLException; + + /** + * create new notifyServiceInboundPattern + * + * @param context the context + * @param notifyServiceEntity the notifyServiceEntity + * @return the created notifyServiceInboundPattern + * @throws SQLException if database error + */ + public NotifyServiceInboundPattern create(Context context, NotifyServiceEntity notifyServiceEntity) + throws SQLException; + + /** + * update the provided notifyServiceInboundPattern + * + * @param context the context + * @param inboundPattern the notifyServiceInboundPattern + * @throws SQLException if database error + */ + public void update(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException; + + /** + * delete the provided notifyServiceInboundPattern + * + * @param context the context + * @param inboundPattern the notifyServiceInboundPattern + * @throws SQLException if database error + */ + public void delete(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException; +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java new file mode 100644 index 000000000000..b69ecec6a142 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/LDNMessageServiceImpl.java @@ -0,0 +1,360 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.service.impl; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang.time.DateUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.app.ldn.LDNRouter; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.dao.LDNMessageDao; +import org.dspace.app.ldn.dao.NotifyServiceDao; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.model.NotifyRequestStatus; +import org.dspace.app.ldn.model.NotifyRequestStatusEnum; +import org.dspace.app.ldn.model.RequestStatus; +import org.dspace.app.ldn.model.Service; +import org.dspace.app.ldn.processor.LDNProcessor; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.ldn.utility.LDNUtils; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.handle.service.HandleService; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Implementation of {@link LDNMessageService} + * + * @author Mohamed Eskander (mohamed.eskander at 4science dot it) + */ +public class LDNMessageServiceImpl implements LDNMessageService { + + @Autowired(required = true) + private LDNMessageDao ldnMessageDao; + @Autowired(required = true) + private NotifyServiceDao notifyServiceDao; + @Autowired(required = true) + private ConfigurationService configurationService; + @Autowired(required = true) + private HandleService handleService; + @Autowired(required = true) + private ItemService itemService; + @Autowired(required = true) + private LDNRouter ldnRouter; + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(LDNMessageServiceImpl.class); + + protected LDNMessageServiceImpl() { + + } + + @Override + public LDNMessageEntity find(Context context, String id) throws SQLException { + return ldnMessageDao.findByID(context, LDNMessageEntity.class, id); + } + + @Override + public List findAll(Context context) throws SQLException { + return ldnMessageDao.findAll(context, LDNMessageEntity.class); + } + + @Override + public LDNMessageEntity create(Context context, String id) throws SQLException { + LDNMessageEntity result = ldnMessageDao.findByID(context, LDNMessageEntity.class, id); + if (result != null) { + throw new SQLException("Duplicate LDN Message ID [" + id + "] detected. This message is rejected."); + } + return ldnMessageDao.create(context, new LDNMessageEntity(id)); + } + + @Override + public LDNMessageEntity create(Context context, Notification notification, String sourceIp) throws SQLException { + LDNMessageEntity ldnMessage = create(context, notification.getId()); + ldnMessage.setObject(findDspaceObjectByUrl(context, notification.getObject().getId())); + if (null != notification.getContext()) { + ldnMessage.setContext(findDspaceObjectByUrl(context, notification.getContext().getId())); + } + ldnMessage.setOrigin(findNotifyService(context, notification.getOrigin())); + ldnMessage.setInReplyTo(find(context, notification.getInReplyTo())); + ObjectMapper mapper = new ObjectMapper(); + String message = null; + try { + message = mapper.writeValueAsString(notification); + ldnMessage.setMessage(message); + } catch (JsonProcessingException e) { + log.error("Notification json can't be correctly processed " + + "and stored inside the LDN Message Entity" + ldnMessage); + log.error(e); + } + ldnMessage.setType(StringUtils.joinWith(",", notification.getType())); + Set notificationType = notification.getType(); + if (notificationType == null) { + log.error("Notification has no notificationType attribute! " + notification); + return null; + } + ArrayList notificationTypeArrayList = new ArrayList(notificationType); + // sorting the list + Collections.sort(notificationTypeArrayList); + ldnMessage.setActivityStreamType(notificationTypeArrayList.get(0)); + if (notificationTypeArrayList.size() > 1) { + ldnMessage.setCoarNotifyType(notificationTypeArrayList.get(1)); + } + ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED); + ldnMessage.setSourceIp(sourceIp); + if (ldnMessage.getOrigin() == null && !"Offer".equalsIgnoreCase(ldnMessage.getActivityStreamType())) { + ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED); + } else { + + boolean ipCheckRangeEnabled = configurationService.getBooleanProperty("ldn.ip-range.enabled", true); + if (ipCheckRangeEnabled && !isValidIp(ldnMessage.getOrigin(), sourceIp)) { + ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNTRUSTED_IP); + } + } + ldnMessage.setQueueTimeout(new Date()); + + update(context, ldnMessage); + return ldnMessage; + } + + @Override + public boolean isValidIp(NotifyServiceEntity origin, String sourceIp) { + + String lowerIp = origin.getLowerIp(); + String upperIp = origin.getUpperIp(); + + try { + InetAddress ip = InetAddress.getByName(sourceIp); + InetAddress lowerBoundAddress = InetAddress.getByName(lowerIp); + InetAddress upperBoundAddress = InetAddress.getByName(upperIp); + + long ipLong = ipToLong(ip); + long lowerBoundLong = ipToLong(lowerBoundAddress); + long upperBoundLong = ipToLong(upperBoundAddress); + + return ipLong >= lowerBoundLong && ipLong <= upperBoundLong; + } catch (UnknownHostException e) { + return false; + } + } + + private long ipToLong(InetAddress ip) { + byte[] octets = ip.getAddress(); + long result = 0; + for (byte octet : octets) { + result <<= 8; + result |= octet & 0xff; + } + return result; + } + + @Override + public void update(Context context, LDNMessageEntity ldnMessage) throws SQLException { + if (ldnMessage.getOrigin() != null && + LDNMessageEntity.QUEUE_STATUS_UNTRUSTED.compareTo(ldnMessage.getQueueStatus()) == 0) { + ldnMessage.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED); + } + ldnMessageDao.save(context, ldnMessage); + } + + private DSpaceObject findDspaceObjectByUrl(Context context, String url) throws SQLException { + String dspaceUrl = configurationService.getProperty("dspace.ui.url") + "/handle/"; + + if (url.startsWith(dspaceUrl)) { + return handleService.resolveToObject(context, url.substring(dspaceUrl.length())); + } + + String handleResolver = configurationService.getProperty("handle.canonical.prefix", "https://hdl.handle.net/"); + if (url.startsWith(handleResolver)) { + return handleService.resolveToObject(context, url.substring(handleResolver.length())); + } + + dspaceUrl = configurationService.getProperty("dspace.ui.url") + "/items/"; + if (url.startsWith(dspaceUrl)) { + return itemService.find(context, UUID.fromString(url.substring(dspaceUrl.length()))); + } + + return null; + } + + public NotifyServiceEntity findNotifyService(Context context, Service service) throws SQLException { + return notifyServiceDao.findByLdnUrl(context, service.getInbox()); + } + + @Override + public List findOldestMessagesToProcess(Context context) throws SQLException { + List result = null; + int max_attempts = configurationService.getIntProperty("ldn.processor.max.attempts"); + result = ldnMessageDao.findOldestMessageToProcess(context, max_attempts); + return result; + } + + @Override + public List findProcessingTimedoutMessages(Context context) throws SQLException { + List result = null; + int max_attempts = configurationService.getIntProperty("ldn.processor.max.attempts"); + result = ldnMessageDao.findProcessingTimedoutMessages(context, max_attempts); + return result; + } + + @Override + public int extractAndProcessMessageFromQueue(Context context) throws SQLException { + int result = 0; + int timeoutInMinutes = configurationService.getIntProperty("ldn.processor.queue.msg.timeout"); + if (timeoutInMinutes == 0) { + timeoutInMinutes = 60; + } + List msgs = null; + try { + msgs = findOldestMessagesToProcess(context); + if (msgs != null && msgs.size() > 0) { + LDNMessageEntity msg = null; + LDNProcessor processor = null; + for (int i = 0; processor == null && i < msgs.size() && msgs.get(i) != null; i++) { + processor = ldnRouter.route(msgs.get(i)); + msg = msgs.get(i); + if (processor == null) { + log.info( + "No processor found for LDN message " + msgs.get(i)); + msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_UNMAPPED_ACTION); + msg.setQueueAttempts(msg.getQueueAttempts() + 1); + update(context, msg); + } + } + if (processor != null) { + try { + msg.setQueueLastStartTime(new Date()); + msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_PROCESSING); + msg.setQueueTimeout(DateUtils.addMinutes(new Date(), timeoutInMinutes)); + update(context, msg); + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(msg.getMessage(), Notification.class); + processor.process(context, notification); + msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_PROCESSED); + result = 1; + } catch (Exception e) { + result = -1; + log.error(e); + } finally { + msg.setQueueAttempts(msg.getQueueAttempts() + 1); + update(context, msg); + } + } else { + log.info("Found x" + msgs.size() + " LDN messages but none processor found."); + } + } + } catch (SQLException e) { + result = -1; + log.error(e); + } + return result; + } + + @Override + public int checkQueueMessageTimeout(Context context) { + int result = 0; + int timeoutInMinutes = configurationService.getIntProperty("ldn.processor.queue.msg.timeout"); + if (timeoutInMinutes == 0) { + timeoutInMinutes = 60; + } + int maxAttempts = configurationService.getIntProperty("ldn.processor.max.attempts"); + if (maxAttempts == 0) { + maxAttempts = 5; + } + log.debug("Using parameters: [timeoutInMinutes]=" + timeoutInMinutes + ",[maxAttempts]=" + maxAttempts); + /* + * put failed on processing messages with timed-out timeout and + * attempts >= configured_max_attempts put queue on processing messages with + * timed-out timeout and attempts < configured_max_attempts + */ + List msgsToCheck = null; + try { + msgsToCheck = findProcessingTimedoutMessages(context); + } catch (SQLException e) { + result = -1; + log.error("An error occured on searching for timedout LDN messages!", e); + return result; + } + if (msgsToCheck == null || msgsToCheck.isEmpty()) { + return result; + } + for (int i = 0; i < msgsToCheck.size() && msgsToCheck.get(i) != null; i++) { + LDNMessageEntity msg = msgsToCheck.get(i); + try { + if (msg.getQueueAttempts() >= maxAttempts) { + msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_FAILED); + } else { + msg.setQueueStatus(LDNMessageEntity.QUEUE_STATUS_QUEUED); + } + update(context, msg); + result++; + } catch (SQLException e) { + log.error("Can't update LDN message " + msg); + log.error(e); + } + } + return result; + } + + @Override + public NotifyRequestStatus findRequestsByItem(Context context, Item item) throws SQLException { + NotifyRequestStatus result = new NotifyRequestStatus(); + result.setItemUuid(item.getID()); + List msgs = ldnMessageDao.findAllMessagesByItem( + context, item, "Offer"); + if (msgs != null && !msgs.isEmpty()) { + for (LDNMessageEntity msg : msgs) { + RequestStatus offer = new RequestStatus(); + offer.setServiceName(msg.getOrigin() == null ? "Unknown Service" : msg.getOrigin().getName()); + offer.setServiceUrl(msg.getOrigin() == null ? "" : msg.getOrigin().getUrl()); + offer.setOfferType(LDNUtils.getNotifyType(msg.getCoarNotifyType())); + List acks = ldnMessageDao.findAllRelatedMessagesByItem( + context, msg, item, "Accept", "TentativeReject", "TentativeAccept", "Announce"); + if (acks == null || acks.isEmpty()) { + offer.setStatus(NotifyRequestStatusEnum.REQUESTED); + } else if (acks.stream() + .filter(c -> (c.getActivityStreamType().equalsIgnoreCase("TentativeAccept") || + c.getActivityStreamType().equalsIgnoreCase("Accept"))) + .findAny().isPresent()) { + offer.setStatus(NotifyRequestStatusEnum.ACCEPTED); + } else if (acks.stream() + .filter(c -> c.getActivityStreamType().equalsIgnoreCase("TentativeReject")) + .findAny().isPresent()) { + offer.setStatus(NotifyRequestStatusEnum.REJECTED); + } + if (acks.stream().filter( + c -> c.getActivityStreamType().equalsIgnoreCase("Announce")) + .findAny().isEmpty()) { + result.addRequestStatus(offer); + } + } + } + return result; + } + + public void delete(Context context, LDNMessageEntity ldnMessage) throws SQLException { + ldnMessageDao.delete(context, ldnMessage); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyPatternToTriggerImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyPatternToTriggerImpl.java new file mode 100644 index 000000000000..89ec4abe58d9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyPatternToTriggerImpl.java @@ -0,0 +1,62 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.ldn.service.impl; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.dao.NotifyPatternToTriggerDao; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link NotifyPatternToTriggerService}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyPatternToTriggerImpl implements NotifyPatternToTriggerService { + + @Autowired(required = true) + private NotifyPatternToTriggerDao notifyPatternToTriggerDao; + + @Override + public List findAll(Context context) throws SQLException { + return notifyPatternToTriggerDao.findAll(context, NotifyPatternToTrigger.class); + } + + @Override + public List findByItem(Context context, Item item) throws SQLException { + return notifyPatternToTriggerDao.findByItem(context, item); + } + + @Override + public List findByItemAndPattern(Context context, Item item, String pattern) + throws SQLException { + return notifyPatternToTriggerDao.findByItemAndPattern(context, item, pattern); + } + + @Override + public NotifyPatternToTrigger create(Context context) throws SQLException { + NotifyPatternToTrigger notifyPatternToTrigger = new NotifyPatternToTrigger(); + return notifyPatternToTriggerDao.create(context, notifyPatternToTrigger); + } + + @Override + public void update(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException { + notifyPatternToTriggerDao.save(context, notifyPatternToTrigger); + } + + @Override + public void delete(Context context, NotifyPatternToTrigger notifyPatternToTrigger) throws SQLException { + notifyPatternToTriggerDao.delete(context, notifyPatternToTrigger); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceImpl.java new file mode 100644 index 000000000000..d2289fd77a1c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceImpl.java @@ -0,0 +1,66 @@ +/** + * 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.ldn.service.impl; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.dao.NotifyServiceDao; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link NotifyService}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceImpl implements NotifyService { + + @Autowired(required = true) + private NotifyServiceDao notifyServiceDao; + + @Override + public List findAll(Context context) throws SQLException { + return notifyServiceDao.findAll(context, NotifyServiceEntity.class); + } + + @Override + public NotifyServiceEntity find(Context context, Integer id) throws SQLException { + return notifyServiceDao.findByID(context, NotifyServiceEntity.class, id); + } + + @Override + public NotifyServiceEntity create(Context context) throws SQLException { + NotifyServiceEntity notifyServiceEntity = new NotifyServiceEntity(); + return notifyServiceDao.create(context, notifyServiceEntity); + } + + @Override + public void update(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException { + notifyServiceDao.save(context, notifyServiceEntity); + } + + @Override + public void delete(Context context, NotifyServiceEntity notifyServiceEntity) throws SQLException { + notifyServiceDao.delete(context, notifyServiceEntity); + } + + @Override + public NotifyServiceEntity findByLdnUrl(Context context, String ldnUrl) throws SQLException { + return notifyServiceDao.findByLdnUrl(context, ldnUrl); + } + + @Override + public List findManualServicesByInboundPattern(Context context, String pattern) + throws SQLException { + return notifyServiceDao.findManualServicesByInboundPattern(context, pattern); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceInboundPatternServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceInboundPatternServiceImpl.java new file mode 100644 index 000000000000..c699d9fd0376 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/service/impl/NotifyServiceInboundPatternServiceImpl.java @@ -0,0 +1,59 @@ +/** + * 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.ldn.service.impl; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.dao.NotifyServiceInboundPatternDao; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation Service class for the {@link NotifyServiceInboundPatternService}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceInboundPatternServiceImpl implements NotifyServiceInboundPatternService { + + @Autowired + private NotifyServiceInboundPatternDao inboundPatternDao; + + @Override + public NotifyServiceInboundPattern findByServiceAndPattern(Context context, + NotifyServiceEntity notifyServiceEntity, + String pattern) throws SQLException { + return inboundPatternDao.findByServiceAndPattern(context, notifyServiceEntity, pattern); + } + + @Override + public List findAutomaticPatterns(Context context) throws SQLException { + return inboundPatternDao.findAutomaticPatterns(context); + } + + @Override + public NotifyServiceInboundPattern create(Context context, NotifyServiceEntity notifyServiceEntity) + throws SQLException { + NotifyServiceInboundPattern inboundPattern = new NotifyServiceInboundPattern(); + inboundPattern.setNotifyService(notifyServiceEntity); + return inboundPatternDao.create(context, inboundPattern); + } + + @Override + public void update(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException { + inboundPatternDao.save(context, inboundPattern); + } + + @Override + public void delete(Context context, NotifyServiceInboundPattern inboundPattern) throws SQLException { + inboundPatternDao.delete(context, inboundPattern); + } +} diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/utility/LDNUtils.java b/dspace-api/src/main/java/org/dspace/app/ldn/utility/LDNUtils.java new file mode 100644 index 000000000000..949da655bc70 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/app/ldn/utility/LDNUtils.java @@ -0,0 +1,96 @@ +/** + * 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.ldn.utility; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Some linked data notification utilities. + */ +public class LDNUtils { + + private final static Pattern UUID_REGEX_PATTERN = Pattern.compile( + "\\p{XDigit}{8}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{4}-\\p{XDigit}{12}"); + + private final static String SIMPLE_PROTOCOL_REGEX = "^(http[s]?://www\\.|http[s]?://|www\\.)"; + + /** + * + */ + private LDNUtils() { + + } + + /** + * Whether the URL contains an UUID. Used to determine item id from item URL. + * + * @param url item URL + * @return boolean true if URL has UUID, false otherwise + */ + public static boolean hasUUIDInURL(String url) { + Matcher matcher = UUID_REGEX_PATTERN.matcher(url); + + return matcher.find(); + } + + /** + * Extract UUID from URL. + * + * @param url item URL + * @return UUID item id + */ + public static UUID getUUIDFromURL(String url) { + Matcher matcher = UUID_REGEX_PATTERN.matcher(url); + StringBuilder handle = new StringBuilder(); + if (matcher.find()) { + handle.append(matcher.group(0)); + } + return UUID.fromString(handle.toString()); + } + + /** + * Remove http or https protocol from URL. + * + * @param url URL + * @return String URL without protocol + */ + public static String removedProtocol(String url) { + return url.replaceFirst(SIMPLE_PROTOCOL_REGEX, EMPTY); + } + + /** + * Custom context resolver processing. Currently converting DOI URL to DOI id. + * + * @param value context ietf:cite-as + * @return String ietf:cite-as identifier + */ + public static String processContextResolverId(String value) { + String resolverId = value; + resolverId = resolverId.replace("https://doi.org/", "doi:"); + + return resolverId; + } + + /** + * Clear the coarNotifyType from the source code. + * + * @param coarNotifyType coar Notify Type to sanitize + * @return String just the notify type + */ + public static String getNotifyType(String coarNotifyType) { + String justNotifyType = coarNotifyType; + justNotifyType = justNotifyType.substring(justNotifyType.lastIndexOf(":") + 1); + justNotifyType = justNotifyType.replace("Action", ""); + return justNotifyType; + } + +} \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java index 867e684db86b..7465fa6e1279 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterScriptConfiguration.java @@ -33,7 +33,8 @@ public Options getOptions() { options.addOption("v", "verbose", false, "print all extracted text and other details to STDOUT"); options.addOption("q", "quiet", false, "do not print anything except in the event of errors."); options.addOption("f", "force", false, "force all bitstreams to be processed"); - options.addOption("i", "identifier", true, "ONLY process bitstreams belonging to identifier"); + options.addOption("i", "identifier", true, + "ONLY process bitstreams belonging to the provided handle identifier"); options.addOption("m", "maximum", true, "process no more than maximum items"); options.addOption("h", "help", false, "help"); diff --git a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java index b50fb22355a3..a6ba9fde88d9 100644 --- a/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/mediafilter/MediaFilterServiceImpl.java @@ -132,12 +132,18 @@ public void applyFiltersAllItems(Context context) throws Exception { @Override public void applyFiltersCommunity(Context context, Community community) throws Exception { //only apply filters if community not in skip-list + // ensure that the community is attached to the current hibernate session + // as we are committing after each item (handles, sub-communties and + // collections are lazy attributes) + community = context.reloadEntity(community); if (!inSkipList(community.getHandle())) { List subcommunities = community.getSubcommunities(); for (Community subcommunity : subcommunities) { applyFiltersCommunity(context, subcommunity); } - + // ensure that the community is attached to the current hibernate session + // as we are committing after each item + community = context.reloadEntity(community); List collections = community.getCollections(); for (Collection collection : collections) { applyFiltersCollection(context, collection); @@ -148,6 +154,9 @@ public void applyFiltersCommunity(Context context, Community community) @Override public void applyFiltersCollection(Context context, Collection collection) throws Exception { + // ensure that the collection is attached to the current hibernate session + // as we are committing after each item (handles are lazy attributes) + collection = context.reloadEntity(collection); //only apply filters if collection not in skip-list if (!inSkipList(collection.getHandle())) { Iterator itemIterator = itemService.findAllByCollection(context, collection); @@ -171,6 +180,8 @@ public void applyFiltersItem(Context c, Item item) throws Exception { } // clear item objects from context cache and internal cache c.uncacheEntity(currentItem); + // commit after each item to release DB resources + c.commit(); currentItem = null; } } diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java index 6281b6910701..3c2ad71846a6 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/SolrSuggestionStorageServiceImpl.java @@ -56,6 +56,7 @@ * */ public class SolrSuggestionStorageServiceImpl implements SolrSuggestionStorageService { + private static final Logger log = LogManager.getLogger(SolrSuggestionStorageServiceImpl.class); protected SolrClient solrSuggestionClient; diff --git a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java index a408c448e9f5..76e8213cd7d2 100644 --- a/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java +++ b/dspace-api/src/main/java/org/dspace/app/suggestion/openaire/PublicationLoaderRunnable.java @@ -12,6 +12,8 @@ import java.util.List; import org.apache.commons.cli.ParseException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.discovery.DiscoverQuery; @@ -23,8 +25,6 @@ import org.dspace.scripts.DSpaceRunnable; import org.dspace.sort.SortOption; import org.dspace.utils.DSpace; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Runner responsible to import metadata about authors from OpenAIRE to Solr. @@ -33,13 +33,13 @@ * with this UUID will be used. * Invocation without any parameter results in massive import, processing all * authors registered in DSpace. - * + * * @author Alessandro Martelli (alessandro.martelli at 4science.it) */ public class PublicationLoaderRunnable extends DSpaceRunnable> { - private static final Logger LOGGER = LoggerFactory.getLogger(PublicationLoaderRunnable.class); + private static final Logger LOGGER = LogManager.getLogger(); private PublicationLoader oairePublicationLoader = null; @@ -63,9 +63,9 @@ public void setup() throws ParseException { profile = commandLine.getOptionValue("s"); if (profile == null) { - LOGGER.info("No argument for -s, process all profile"); + LOGGER.info("No argument for -s, process all profiles"); } else { - LOGGER.info("Process eperson item with UUID " + profile); + LOGGER.info("Process eperson item with UUID {}", profile); } } @@ -87,7 +87,7 @@ public void internalRun() throws Exception { * the researcher with this UUID will be chosen. If the uuid doesn't match any * researcher, the method returns an empty array list. If uuid is null, all * research will be return. - * + * * @param profileUUID uuid of the researcher. If null, all researcher will be * returned. * @return the researcher with specified UUID or all researchers @@ -96,10 +96,10 @@ public void internalRun() throws Exception { private Iterator getResearchers(String profileUUID) { SearchService searchService = new DSpace().getSingletonService(SearchService.class); DiscoverQueryBuilder queryBuilder = SearchUtils.getQueryBuilder(); - List filters = new ArrayList(); + List filters = new ArrayList<>(); String query = "*:*"; if (profileUUID != null) { - query = "search.resourceid:" + profileUUID.toString(); + query = "search.resourceid:" + profileUUID; } try { DiscoverQuery discoverQuery = queryBuilder.buildQuery(context, null, diff --git a/dspace-api/src/main/java/org/dspace/app/util/AbstractDSpaceWebapp.java b/dspace-api/src/main/java/org/dspace/app/util/AbstractDSpaceWebapp.java index c0cb5d226c1f..48df3dbc12d5 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/AbstractDSpaceWebapp.java +++ b/dspace-api/src/main/java/org/dspace/app/util/AbstractDSpaceWebapp.java @@ -12,13 +12,13 @@ import java.sql.Timestamp; import java.util.Date; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.util.factory.UtilServiceFactory; import org.dspace.app.util.service.WebAppService; import org.dspace.core.Context; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Represent a DSpace application while it is running. This helps us report @@ -29,11 +29,10 @@ */ abstract public class AbstractDSpaceWebapp implements DSpaceWebappMXBean { - private static final Logger log = LoggerFactory.getLogger(AbstractDSpaceWebapp.class); + private static final Logger log = LogManager.getLogger(); protected final WebAppService webAppService = UtilServiceFactory.getInstance().getWebAppService(); - protected String kind; protected Date started; diff --git a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java index 11f9aadd869b..c5093d3320a8 100644 --- a/dspace-api/src/main/java/org/dspace/app/util/DCInput.java +++ b/dspace-api/src/main/java/org/dspace/app/util/DCInput.java @@ -16,10 +16,10 @@ import javax.annotation.Nullable; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.MetadataSchemaEnum; import org.dspace.core.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Class representing a line in an input form. @@ -28,7 +28,7 @@ */ public class DCInput { - private static final Logger log = LoggerFactory.getLogger(DCInput.class); + private static final Logger log = LogManager.getLogger(); /** * the DC element name @@ -183,7 +183,7 @@ public DCInput(Map fieldMap, Map> listMap) } //check if the input have a language tag - language = Boolean.valueOf(fieldMap.get("language")); + language = Boolean.parseBoolean(fieldMap.get("language")); valueLanguageList = new ArrayList<>(); if (language) { String languageNameTmp = fieldMap.get("value-pairs-name"); @@ -219,7 +219,7 @@ public DCInput(Map fieldMap, Map> listMap) || "yes".equalsIgnoreCase(closedVocabularyStr); // parsing of the element (using the colon as split separator) - typeBind = new ArrayList(); + typeBind = new ArrayList<>(); String typeBindDef = fieldMap.get("type-bind"); if (typeBindDef != null && typeBindDef.trim().length() > 0) { String[] types = typeBindDef.split(","); @@ -523,7 +523,7 @@ public boolean isClosedVocabulary() { * @return true when there is no type restriction or typeName is allowed */ public boolean isAllowedFor(String typeName) { - if (typeBind.size() == 0) { + if (typeBind.isEmpty()) { return true; } diff --git a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java index 1d67da37ecb3..a1e096dc4d64 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/AuthenticationServiceImpl.java @@ -15,6 +15,8 @@ import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authenticate.service.AuthenticationService; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; @@ -22,8 +24,6 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.eperson.service.EPersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -49,15 +49,14 @@ * specified first (in the configuration) thus getting highest priority. * * @author Larry Stone - * @version $Revision$ * @see AuthenticationMethod */ public class AuthenticationServiceImpl implements AuthenticationService { /** - * SLF4J logging category + * Logging category */ - private final Logger log = (Logger) LoggerFactory.getLogger(AuthenticationServiceImpl.class); + private final Logger log = LogManager.getLogger(); @Autowired(required = true) protected EPersonService ePersonService; @@ -121,6 +120,7 @@ protected int authenticateInternal(Context context, return bestRet; } + @Override public void updateLastActiveDate(Context context) { EPerson me = context.getCurrentUser(); if (me != null) { diff --git a/dspace-api/src/main/java/org/dspace/authenticate/OidcAuthenticationBean.java b/dspace-api/src/main/java/org/dspace/authenticate/OidcAuthenticationBean.java index 8a4ac190c816..c628f0dce27d 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/OidcAuthenticationBean.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/OidcAuthenticationBean.java @@ -24,6 +24,8 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authenticate.oidc.OidcClient; import org.dspace.authenticate.oidc.model.OidcTokenResponseDTO; import org.dspace.core.Context; @@ -31,8 +33,6 @@ import org.dspace.eperson.Group; import org.dspace.eperson.service.EPersonService; import org.dspace.services.ConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -51,7 +51,7 @@ public class OidcAuthenticationBean implements AuthenticationMethod { private final static String LOGIN_PAGE_URL_FORMAT = "%s?client_id=%s&response_type=code&scope=%s&redirect_uri=%s"; - private static final Logger LOGGER = LoggerFactory.getLogger(OidcAuthenticationBean.class); + private static final Logger LOGGER = LogManager.getLogger(); private static final String OIDC_AUTHENTICATED = "oidc.authenticated"; @@ -174,7 +174,7 @@ public String loginPageURL(Context context, HttpServletRequest request, HttpServ final Entry entry = iterator.next(); if (isBlank(entry.getValue())) { - LOGGER.error(" * {} is missing", entry.getKey()); + LOGGER.error(" * {} is missing", entry::getKey); } } return ""; @@ -183,7 +183,7 @@ public String loginPageURL(Context context, HttpServletRequest request, HttpServ try { return format(LOGIN_PAGE_URL_FORMAT, authorizeUrl, clientId, scopes, encode(redirectUri, "UTF-8")); } catch (UnsupportedEncodingException e) { - LOGGER.error(e.getMessage(), e); + LOGGER.error(e::getMessage, e); return ""; } diff --git a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java index a11bbfc867b4..6804ad9160dd 100644 --- a/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java +++ b/dspace-api/src/main/java/org/dspace/authenticate/OrcidAuthenticationBean.java @@ -23,6 +23,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -39,8 +41,6 @@ import org.dspace.services.ConfigurationService; import org.orcid.jaxb.model.v3.release.record.Email; import org.orcid.jaxb.model.v3.release.record.Person; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -53,7 +53,7 @@ public class OrcidAuthenticationBean implements AuthenticationMethod { public static final String ORCID_AUTH_ATTRIBUTE = "orcid-authentication"; - private final static Logger LOGGER = LoggerFactory.getLogger(OrcidAuthenticationBean.class); + private final static Logger LOGGER = LogManager.getLogger(); private final static String LOGIN_PAGE_URL_FORMAT = "%s?client_id=%s&response_type=code&scope=%s&redirect_uri=%s"; @@ -282,7 +282,8 @@ private Person getPersonFromOrcid(OrcidTokenResponseDTO token) { try { return orcidClient.getPerson(token.getAccessToken(), token.getOrcid()); } catch (Exception ex) { - LOGGER.error("An error occurs retriving the ORCID record with id " + token.getOrcid(), ex); + LOGGER.error("An error occurs retriving the ORCID record with id {}", + token.getOrcid(), ex); return null; } } diff --git a/dspace-api/src/main/java/org/dspace/coarnotify/NotifyConfigurationService.java b/dspace-api/src/main/java/org/dspace/coarnotify/NotifyConfigurationService.java new file mode 100644 index 000000000000..c9d65186b631 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/coarnotify/NotifyConfigurationService.java @@ -0,0 +1,34 @@ +/** + * 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.coarnotify; + +import java.util.List; +import java.util.Map; + +/** + * Simple bean to manage different COAR Notify configuration + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyConfigurationService { + + /** + * Mapping the submission step process identifier with the configuration + * (see configuration at coar-notify.xml) + */ + private Map> patterns; + + public Map> getPatterns() { + return patterns; + } + + public void setPatterns(Map> patterns) { + this.patterns = patterns; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/coarnotify/NotifyPattern.java b/dspace-api/src/main/java/org/dspace/coarnotify/NotifyPattern.java new file mode 100644 index 000000000000..d678aa052303 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/coarnotify/NotifyPattern.java @@ -0,0 +1,44 @@ +/** + * 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.coarnotify; + +/** + * A collection of configured patterns to be met when adding COAR Notify services. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyPattern { + + private String pattern; + private boolean multipleRequest; + + public NotifyPattern() { + + } + + public NotifyPattern(String pattern, boolean multipleRequest) { + this.pattern = pattern; + this.multipleRequest = multipleRequest; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public boolean isMultipleRequest() { + return multipleRequest; + } + + public void setMultipleRequest(boolean multipleRequest) { + this.multipleRequest = multipleRequest; + } +} diff --git a/dspace-api/src/main/java/org/dspace/coarnotify/NotifySubmissionConfiguration.java b/dspace-api/src/main/java/org/dspace/coarnotify/NotifySubmissionConfiguration.java new file mode 100644 index 000000000000..ee23ee346521 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/coarnotify/NotifySubmissionConfiguration.java @@ -0,0 +1,65 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.coarnotify; + +import java.util.List; + +/** + * this class represents the Configuration of Submission COAR Notify + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifySubmissionConfiguration { + + /** + * the map key of configured bean of COARNotifyConfigurationService + * in coar-notify.xml + */ + private String id; + + /** + * the map values of configured bean of COARNotifyConfigurationService + * in coar-notify.xml + */ + private List patterns; + + public NotifySubmissionConfiguration() { + + } + + public NotifySubmissionConfiguration(String id, List patterns) { + super(); + this.id = id; + this.patterns = patterns; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + /** + * Gets the list of configured COAR Notify Patterns + * + * @return the list of configured COAR Notify Patterns + */ + public List getPatterns() { + return patterns; + } + + /** + * Sets the list of configured COAR Notify Patterns + * @param patterns + */ + public void setPatterns(final List patterns) { + this.patterns = patterns; + } +} diff --git a/dspace-api/src/main/java/org/dspace/coarnotify/SubmissionNotifyServiceImpl.java b/dspace-api/src/main/java/org/dspace/coarnotify/SubmissionNotifyServiceImpl.java new file mode 100644 index 000000000000..afb771529f4a --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/coarnotify/SubmissionNotifyServiceImpl.java @@ -0,0 +1,53 @@ +/** + * 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.coarnotify; + +import java.util.ArrayList; +import java.util.List; + +import org.dspace.coarnotify.service.SubmissionNotifyService; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation of {@link SubmissionNotifyService} + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class SubmissionNotifyServiceImpl implements SubmissionNotifyService { + + @Autowired(required = true) + private NotifyConfigurationService coarNotifyConfigurationService; + + protected SubmissionNotifyServiceImpl() { + + } + + @Override + public NotifySubmissionConfiguration findOne(String id) { + List patterns = + coarNotifyConfigurationService.getPatterns().get(id); + + if (patterns == null) { + return null; + } + + return new NotifySubmissionConfiguration(id, patterns); + } + + @Override + public List findAll() { + List coarNotifies = new ArrayList<>(); + + coarNotifyConfigurationService.getPatterns().forEach((id, patterns) -> + coarNotifies.add(new NotifySubmissionConfiguration(id, patterns) + )); + + return coarNotifies; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/coarnotify/service/SubmissionNotifyService.java b/dspace-api/src/main/java/org/dspace/coarnotify/service/SubmissionNotifyService.java new file mode 100644 index 000000000000..43f3ea17330b --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/coarnotify/service/SubmissionNotifyService.java @@ -0,0 +1,39 @@ +/** + * 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.coarnotify.service; + +import java.util.List; + +import org.dspace.coarnotify.NotifySubmissionConfiguration; + +/** + * Service interface class for the Creative Submission COAR Notify. + * The implementation of this class is responsible for all business logic calls for the Creative Submission COAR Notify + * and is autowired by spring + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface SubmissionNotifyService { + + /** + * Find the COARE Notify corresponding to the provided ID + * found in the configuration + * + * @param id - the ID of the COAR Notify to be found + * @return the corresponding COAR Notify if found or null when not found + */ + public NotifySubmissionConfiguration findOne(String id); + + /** + * Find all configured COAR Notifies + * + * @return all configured COAR Notifies + */ + public List findAll(); + +} diff --git a/dspace-api/src/main/java/org/dspace/content/ItemFilterService.java b/dspace-api/src/main/java/org/dspace/content/ItemFilterService.java new file mode 100644 index 000000000000..8b664a972605 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/ItemFilterService.java @@ -0,0 +1,33 @@ +/** + * 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.util.List; + +import org.dspace.app.ldn.ItemFilter; + +/** + * Service interface class for the Item Filter Object + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface ItemFilterService { + + /** + * @param id the bean name of item filter + * @return one logical item filter by id + * defined in item-filter.xml + */ + public ItemFilter findOne(String id); + + /** + * @return all logical item filters + * defined in item-filter.xml + */ + public List findAll(); +} diff --git a/dspace-api/src/main/java/org/dspace/content/ItemFilterServiceImpl.java b/dspace-api/src/main/java/org/dspace/content/ItemFilterServiceImpl.java new file mode 100644 index 000000000000..bdb23d65666c --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/content/ItemFilterServiceImpl.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.content; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.dspace.app.ldn.ItemFilter; +import org.dspace.content.logic.LogicalStatement; +import org.dspace.kernel.ServiceManager; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Service implementation for {@link ItemFilterService} + * + * @author Mohamd Eskander (mohamed.eskander at 4science.com) + */ +public class ItemFilterServiceImpl implements ItemFilterService { + + @Autowired + private ServiceManager serviceManager; + + @Override + public ItemFilter findOne(String id) { + return findAll() + .stream() + .filter(itemFilter -> + itemFilter.getId().equals(id)) + .findFirst() + .orElse(null); + } + + @Override + public List findAll() { + Map ldnFilters = + serviceManager.getServiceByName("ldnItemFilters", Map.class); + return ldnFilters.keySet() + .stream() + .sorted() + .map(ItemFilter::new) + .collect(Collectors.toList()); + } + + public ServiceManager getServiceManager() { + return serviceManager; + } + + public void setServiceManager(ServiceManager serviceManager) { + this.serviceManager = serviceManager; + } + +} \ No newline at end of file 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 9791f69abbc5..264685be7432 100644 --- a/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/ItemServiceImpl.java @@ -49,6 +49,7 @@ import org.dspace.content.service.RelationshipService; import org.dspace.content.service.WorkspaceItemService; import org.dspace.content.virtual.VirtualMetadataPopulator; +import org.dspace.contentreport.QueryPredicate; import org.dspace.core.Constants; import org.dspace.core.Context; import org.dspace.core.LogHelper; @@ -175,7 +176,6 @@ public class ItemServiceImpl extends DSpaceObjectServiceImpl implements It private QAEventsDAO qaEventsDao; protected ItemServiceImpl() { - super(); } @Override @@ -275,9 +275,8 @@ public Item createTemplateItem(Context context, Collection collection) throws SQ + template.getID())); return template; - } else { - return collection.getTemplateItem(); } + return collection.getTemplateItem(); } @Override @@ -1190,9 +1189,8 @@ public boolean canEdit(Context context, Item item) throws SQLException { if (item.getOwningCollection() == null) { if (!isInProgressSubmission(context, item)) { return true; - } else { - return false; } + return false; } return collectionService.canEditBoolean(context, item.getOwningCollection(), false); @@ -1284,8 +1282,8 @@ protected void addDefaultPoliciesNotInPlace(Context context, DSpaceObject dso, if (!authorizeService .isAnIdenticalPolicyAlreadyInPlace(context, dso, defaultPolicy.getGroup(), Constants.READ, defaultPolicy.getID()) && - (!appendMode && this.isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || - appendMode && this.shouldBeAppended(context, dso, defaultPolicy))) { + (!appendMode && isNotAlreadyACustomRPOfThisTypeOnDSO(context, dso) || + appendMode && shouldBeAppended(context, dso, defaultPolicy))) { ResourcePolicy newPolicy = resourcePolicyService.clone(context, defaultPolicy); newPolicy.setdSpaceObject(dso); newPolicy.setAction(Constants.READ); @@ -1384,9 +1382,8 @@ public Iterator findArchivedByMetadataField(Context context, if (Item.ANY.equals(value)) { return itemDAO.findByMetadataField(context, mdf, null, true); - } else { - return itemDAO.findByMetadataField(context, mdf, value, true); } + return itemDAO.findByMetadataField(context, mdf, value, true); } @Override @@ -1430,9 +1427,24 @@ public Iterator findByMetadataField(Context context, if (Item.ANY.equals(value)) { return itemDAO.findByMetadataField(context, mdf, null, true); - } else { - return itemDAO.findByMetadataField(context, mdf, value, true); } + return itemDAO.findByMetadataField(context, mdf, value, true); + } + + @Override + public List findByMetadataQuery(Context context, List queryPredicates, + List collectionUuids, long offset, int limit) + throws SQLException { + return itemDAO.findByMetadataQuery(context, queryPredicates, collectionUuids, "text_value ~ ?", + offset, limit); + } + + + @Override + public long countForMetadataQuery(Context context, List queryPredicates, + List collectionUuids) + throws SQLException { + return itemDAO.countForMetadataQuery(context, queryPredicates, collectionUuids, "text_value ~ ?"); } @Override @@ -1498,20 +1510,19 @@ public DSpaceObject getParentObject(Context context, Item item) throws SQLExcept Collection ownCollection = item.getOwningCollection(); if (ownCollection != null) { return ownCollection; - } else { - InProgressSubmission inprogress = ContentServiceFactory.getInstance().getWorkspaceItemService() - .findByItem(context, - item); - if (inprogress == null) { - inprogress = WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(context, item); - } + } + InProgressSubmission inprogress = ContentServiceFactory.getInstance().getWorkspaceItemService() + .findByItem(context, + item); + if (inprogress == null) { + inprogress = WorkflowServiceFactory.getInstance().getWorkflowItemService().findByItem(context, item); + } - if (inprogress != null) { - return inprogress.getCollection(); - } - // is a template item? - return item.getTemplateItemOf(); + if (inprogress != null) { + return inprogress.getCollection(); } + // is a template item? + return item.getTemplateItemOf(); } @Override @@ -1611,9 +1622,8 @@ public Item findByIdOrLegacyId(Context context, String id) throws SQLException { try { if (StringUtils.isNumeric(id)) { return findByLegacyId(context, Integer.parseInt(id)); - } else { - return find(context, UUID.fromString(id)); } + return find(context, UUID.fromString(id)); } catch (IllegalArgumentException e) { // Not a valid legacy ID or valid UUID return 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 index d78d140dcfe5..0f2608ef705c 100644 --- a/dspace-api/src/main/java/org/dspace/content/QAEvent.java +++ b/dspace-api/src/main/java/org/dspace/content/QAEvent.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import org.dspace.qaevent.service.dto.CorrectionTypeMessageDTO; +import org.dspace.qaevent.service.dto.NotifyMessageDTO; import org.dspace.qaevent.service.dto.OpenaireMessageDTO; import org.dspace.qaevent.service.dto.QAMessageDTO; import org.dspace.util.RawJsonDeserializer; @@ -22,6 +23,7 @@ * This class represent the Quality Assurance broker data as loaded in our solr * qaevent core * + * @author Andrea Bollini (andrea.bollini at 4science.it) */ public class QAEvent { public static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', @@ -30,8 +32,9 @@ public class QAEvent { public static final String REJECTED = "rejected"; public static final String DISCARDED = "discarded"; - public static final String OPENAIRE_SOURCE = "OpenAIRE"; + public static final String OPENAIRE_SOURCE = "openaire"; public static final String DSPACE_USERS_SOURCE = "DSpaceUsers"; + public static final String COAR_NOTIFY_SOURCE = "coar-notify"; private String source; @@ -205,6 +208,8 @@ public Class getMessageDtoClass() { switch (getSource()) { case OPENAIRE_SOURCE: return OpenaireMessageDTO.class; + case COAR_NOTIFY_SOURCE: + return NotifyMessageDTO.class; case DSPACE_USERS_SOURCE: return CorrectionTypeMessageDTO.class; default: diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java index d4ccebf82e2c..1d07da0e51c7 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTCrosswalk.java @@ -18,12 +18,12 @@ import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamSource; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.SelfNamedPlugin; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.jdom2.Namespace; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Configurable XSLT-driven Crosswalk @@ -88,7 +88,7 @@ public abstract class XSLTCrosswalk extends SelfNamedPlugin { /** * log4j category */ - private static final Logger LOG = LoggerFactory.getLogger(XSLTCrosswalk.class); + private static final Logger LOG = LogManager.getLogger(); /** * DSpace XML Namespace in JDOM form. @@ -168,8 +168,8 @@ protected Transformer getTransformer(String direction) { transformFile.lastModified() > transformLastModified) { try { LOG.debug( - (transformer == null ? "Loading {} XSLT stylesheet from {}" : "Reloading {} XSLT stylesheet from " + - "{}"), + (transformer == null ? "Loading {} XSLT stylesheet from {}" + : "Reloading {} XSLT stylesheet from {}"), getPluginInstanceName(), transformFile.toString()); Source transformSource diff --git a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java index 26371b46aab0..70bb3f1316e0 100644 --- a/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java +++ b/dspace-api/src/main/java/org/dspace/content/crosswalk/XSLTDisseminationCrosswalk.java @@ -23,6 +23,8 @@ import javax.xml.transform.TransformerException; import org.apache.commons.lang3.ArrayUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -51,8 +53,6 @@ import org.jdom2.output.XMLOutputter; import org.jdom2.transform.JDOMResult; import org.jdom2.transform.JDOMSource; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Configurable XSLT-driven dissemination Crosswalk @@ -88,7 +88,7 @@ public class XSLTDisseminationCrosswalk /** * log4j category */ - private static final Logger LOG = LoggerFactory.getLogger(XSLTDisseminationCrosswalk.class); + private static final Logger LOG = LogManager.getLogger(); /** * DSpace context, will be created if XSLTDisseminationCrosswalk had been started by command-line. @@ -140,12 +140,13 @@ private void init() // right format for value of "schemaLocation" attribute. schemaLocation = configurationService.getProperty(prefix + "schemaLocation"); if (schemaLocation == null) { - LOG.warn("No schemaLocation for crosswalk=" + myAlias + ", key=" + prefix + "schemaLocation"); + LOG.warn("No schemaLocation for crosswalk={}, key={}schemaLocation", myAlias, prefix); } else if (schemaLocation.length() > 0 && schemaLocation.indexOf(' ') < 0) { // sanity check: schemaLocation should have space. - LOG.warn("Possible INVALID schemaLocation (no space found) for crosswalk=" + - myAlias + ", key=" + prefix + "schemaLocation" + - "\n\tCorrect format is \"{namespace} {schema-URL}\""); + LOG.warn("Possible INVALID schemaLocation (no space found) for crosswalk={}," + + " key={}schemaLocation" + + "\n\tCorrect format is \"{namespace} {schema-URL}\"", + myAlias, prefix); } // grovel for namespaces of the form: @@ -172,7 +173,7 @@ public Namespace[] getNamespaces() { try { init(); } catch (CrosswalkInternalException e) { - LOG.error(e.toString()); + LOG.error(e::toString); } return (Namespace[]) ArrayUtils.clone(namespaces); } @@ -187,7 +188,7 @@ public String getSchemaLocation() { try { init(); } catch (CrosswalkInternalException e) { - LOG.error(e.toString()); + LOG.error(e::toString); } return schemaLocation; } @@ -220,7 +221,7 @@ public Element disseminateElement(Context context, DSpaceObject dso, } for (Map.Entry parameter : parameters.entrySet()) { - LOG.debug("Setting parameter {} to {}", parameter.getKey(), parameter.getValue()); + LOG.debug("Setting parameter {} to {}", parameter::getKey, parameter::getValue); xform.setParameter(parameter.getKey(), parameter.getValue()); } @@ -232,7 +233,7 @@ public Element disseminateElement(Context context, DSpaceObject dso, root.detach(); return root; } catch (TransformerException e) { - LOG.error("Got error: " + e.toString()); + LOG.error("Got error: ()", e::toString); throw new CrosswalkInternalException("XSL translation failed: " + e.toString(), e); } } @@ -278,13 +279,13 @@ public List disseminateList(Context context, DSpaceObject dso) .map(Element.class::cast).collect(Collectors.toList()); return elementList; } catch (TransformerException e) { - LOG.error("Got error: " + e.toString()); + LOG.error("Got error: {}", e::toString); throw new CrosswalkInternalException("XSL translation failed: " + e.toString(), e); } } /** - * Determine is this crosswalk can dessiminate the given object. + * Determine is this crosswalk can disseminate the given object. * * @see DisseminationCrosswalk */ @@ -304,7 +305,7 @@ public boolean preferList() { try { init(); } catch (CrosswalkInternalException e) { - LOG.error(e.toString()); + LOG.error(e::toString); } return preferList; } @@ -312,7 +313,7 @@ public boolean preferList() { /** * Generate an intermediate representation of a DSpace object. * - * @param dso The dspace object to build a representation of. + * @param dso The DSpace object to build a representation of. * @param dcvs list of metadata * @return element */ @@ -480,9 +481,7 @@ private static String checkedString(String value) { if (reason == null) { return value; } else { - if (LOG.isDebugEnabled()) { - LOG.debug("Filtering out non-XML characters in string, reason=" + reason); - } + LOG.debug("Filtering out non-XML characters in string, reason={}", reason); StringBuilder result = new StringBuilder(value.length()); for (int i = 0; i < value.length(); ++i) { char c = value.charAt(i); @@ -567,11 +566,11 @@ public static void main(String[] argv) throws Exception { System.err.println("=== Stack Trace ==="); e.printStackTrace(System.err); System.err.println("====================="); - LOG.error("Caught: {}.", e.toString()); - LOG.error(e.getMessage()); + LOG.error("Caught: {}.", e::toString); + LOG.error(e::getMessage); CharArrayWriter traceWriter = new CharArrayWriter(2048); e.printStackTrace(new PrintWriter(traceWriter)); - LOG.error(traceWriter.toString()); + LOG.error(traceWriter::toString); System.exit(1); } @@ -588,11 +587,11 @@ public static void main(String[] argv) throws Exception { System.err.println("=== Stack Trace ==="); e.printStackTrace(System.err); System.err.println("====================="); - LOG.error("Caught: {}.", e.toString()); - LOG.error(e.getMessage()); + LOG.error("Caught: {}.", e::toString); + LOG.error(e::getMessage); CharArrayWriter traceWriter = new CharArrayWriter(2048); e.printStackTrace(new PrintWriter(traceWriter)); - LOG.error(traceWriter.toString()); + LOG.error(traceWriter::toString); System.exit(1); } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/ItemDAO.java b/dspace-api/src/main/java/org/dspace/content/dao/ItemDAO.java index 49d3527a358a..92dc0fb237c9 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/ItemDAO.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/ItemDAO.java @@ -11,11 +11,13 @@ import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.UUID; import org.dspace.content.Collection; import org.dspace.content.Community; import org.dspace.content.Item; import org.dspace.content.MetadataField; +import org.dspace.contentreport.QueryPredicate; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -27,12 +29,11 @@ * @author kevinvandevelde at atmire.com */ public interface ItemDAO extends DSpaceObjectLegacySupportDAO { - public Iterator findAll(Context context, boolean archived) throws SQLException; + Iterator findAll(Context context, boolean archived) throws SQLException; - public Iterator findAll(Context context, boolean archived, int limit, int offset) throws SQLException; + Iterator findAll(Context context, boolean archived, int limit, int offset) throws SQLException; - @Deprecated - public Iterator findAll(Context context, boolean archived, boolean withdrawn) throws SQLException; + @Deprecated Iterator findAll(Context context, boolean archived, boolean withdrawn) throws SQLException; /** * Find all items that are: @@ -45,7 +46,7 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO { * @return iterator over all regular items. * @throws SQLException if database error. */ - public Iterator findAllRegularItems(Context context) throws SQLException; + Iterator findAllRegularItems(Context context) throws SQLException; /** * Find all Items modified since a Date. @@ -55,10 +56,10 @@ public interface ItemDAO extends DSpaceObjectLegacySupportDAO { * @return iterator over items * @throws SQLException if database error */ - public Iterator findByLastModifiedSince(Context context, Date since) + Iterator findByLastModifiedSince(Context context, Date since) throws SQLException; - public Iterator findBySubmitter(Context context, EPerson eperson) throws SQLException; + Iterator findBySubmitter(Context context, EPerson eperson) throws SQLException; /** * Find all the items by a given submitter. The order is @@ -70,19 +71,40 @@ public Iterator findByLastModifiedSince(Context context, Date since) * @return an iterator over the items submitted by eperson * @throws SQLException if database error */ - public Iterator findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems) + Iterator findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems) throws SQLException; - public Iterator findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit) + Iterator findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit) throws SQLException; - public Iterator findByMetadataField(Context context, MetadataField metadataField, String value, + Iterator findByMetadataField(Context context, MetadataField metadataField, String value, boolean inArchive) throws SQLException; - public Iterator findByAuthorityValue(Context context, MetadataField metadataField, String authority, + /** + * Returns all the Items that belong to the specified aollections (if any) + * and match the provided predicates. + * @param context The relevant DSpace context + * @param queryPredicates List of predicates that returned items are required to match + * @param collectionUuids UUIDs of the collections to search. + * If none are provided, the entire repository will be searched. + * @param regexClause Syntactic expression used to query the database using a regular expression + * (e.g.: "text_value ~ ?") + * @param offset The offset for the query + * @param limit Maximum number of items to return + * @return A list containing the items that match the provided criteria + * @throws SQLException if something goes wrong + */ + List findByMetadataQuery(Context context, List queryPredicates, + List collectionUuids, String regexClause, + long offset, int limit) throws SQLException; + + long countForMetadataQuery(Context context, List queryPredicates, + List collectionUuids, String regexClause) throws SQLException; + + Iterator findByAuthorityValue(Context context, MetadataField metadataField, String authority, boolean inArchive) throws SQLException; - public Iterator findArchivedByCollection(Context context, Collection collection, Integer limit, + Iterator findArchivedByCollection(Context context, Collection collection, Integer limit, Integer offset) throws SQLException; /** @@ -95,7 +117,7 @@ public Iterator findArchivedByCollection(Context context, Collection colle * @return An iterator containing the items for which the constraints hold true * @throws SQLException If something goes wrong */ - public Iterator findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit, + Iterator findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit, Integer offset) throws SQLException; /** @@ -106,11 +128,11 @@ public Iterator findArchivedByCollectionExcludingOwning(Context context, C * @return The total amount of items that fit the constraints * @throws SQLException If something goes wrong */ - public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException; + int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException; - public Iterator findAllByCollection(Context context, Collection collection) throws SQLException; + Iterator findAllByCollection(Context context, Collection collection) throws SQLException; - public Iterator findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) + Iterator findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) throws SQLException; /** @@ -123,7 +145,7 @@ public Iterator findAllByCollection(Context context, Collection collection * @return item count * @throws SQLException if database error */ - public int countItems(Context context, Collection collection, boolean includeArchived, boolean includeWithdrawn) + int countItems(Context context, Collection collection, boolean includeArchived, boolean includeWithdrawn) throws SQLException; /** @@ -139,7 +161,7 @@ public int countItems(Context context, Collection collection, boolean includeArc * @return item count * @throws SQLException if database error */ - public int countItems(Context context, List collections, boolean includeArchived, + int countItems(Context context, List collections, boolean includeArchived, boolean includeWithdrawn) throws SQLException; /** @@ -153,7 +175,7 @@ public int countItems(Context context, List collections, boolean inc * @return iterator over items * @throws SQLException if database error */ - public Iterator findAll(Context context, boolean archived, + Iterator findAll(Context context, boolean archived, boolean withdrawn, boolean discoverable, Date lastModified) throws SQLException; @@ -187,7 +209,7 @@ public Iterator findAll(Context context, boolean archived, * @return count of items * @throws SQLException if database error */ - public int countItems(Context context, EPerson submitter, boolean includeArchived, boolean includeWithdrawn) + int countItems(Context context, EPerson submitter, boolean includeArchived, boolean includeWithdrawn) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java index 0e051625aaee..276ea2b3430a 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/BitstreamDAOImpl.java @@ -12,6 +12,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.UUID; import javax.persistence.Query; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; @@ -26,6 +27,7 @@ import org.dspace.core.AbstractHibernateDSODAO; import org.dspace.core.Constants; import org.dspace.core.Context; +import org.dspace.core.UUIDIterator; /** * Hibernate implementation of the Database Access Object interface class for the Bitstream object. @@ -76,7 +78,7 @@ public List findBitstreamsWithNoRecentChecksum(Context context) throw @Override public Iterator findByCommunity(Context context, Community community) throws SQLException { - Query query = createQuery(context, "select b from Bitstream b " + + Query query = createQuery(context, "select b.id from Bitstream b " + "join b.bundles bitBundles " + "join bitBundles.items item " + "join item.collections itemColl " + @@ -84,40 +86,45 @@ public Iterator findByCommunity(Context context, Community community) "WHERE :community IN community"); query.setParameter("community", community); - - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Bitstream.class, this); } @Override public Iterator findByCollection(Context context, Collection collection) throws SQLException { - Query query = createQuery(context, "select b from Bitstream b " + + Query query = createQuery(context, "select b.id from Bitstream b " + "join b.bundles bitBundles " + "join bitBundles.items item " + "join item.collections c " + "WHERE :collection IN c"); query.setParameter("collection", collection); - - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Bitstream.class, this); } @Override public Iterator findByItem(Context context, Item item) throws SQLException { - Query query = createQuery(context, "select b from Bitstream b " + + Query query = createQuery(context, "select b.id from Bitstream b " + "join b.bundles bitBundles " + "join bitBundles.items item " + "WHERE :item IN item"); query.setParameter("item", item); - - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Bitstream.class, this); } @Override public Iterator findByStoreNumber(Context context, Integer storeNumber) throws SQLException { - Query query = createQuery(context, "select b from Bitstream b where b.storeNumber = :storeNumber"); + Query query = createQuery(context, "select b.id from Bitstream b where b.storeNumber = :storeNumber"); query.setParameter("storeNumber", storeNumber); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Bitstream.class, this); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java b/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java index 5c840f68e998..0a36fdeb2ab0 100644 --- a/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java +++ b/dspace-api/src/main/java/org/dspace/content/dao/impl/ItemDAOImpl.java @@ -8,25 +8,37 @@ package org.dspace.content.dao.impl; import java.sql.SQLException; +import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.List; +import java.util.UUID; import javax.persistence.Query; import javax.persistence.TemporalType; import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaBuilder.In; import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; import org.apache.logging.log4j.Logger; import org.dspace.content.Collection; +import org.dspace.content.DSpaceObject_; import org.dspace.content.Item; import org.dspace.content.Item_; import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; +import org.dspace.content.MetadataValue_; import org.dspace.content.dao.ItemDAO; +import org.dspace.contentreport.QueryOperator; +import org.dspace.contentreport.QueryPredicate; import org.dspace.core.AbstractHibernateDSODAO; import org.dspace.core.Context; +import org.dspace.core.UUIDIterator; import org.dspace.eperson.EPerson; +import org.dspace.util.JpaCriteriaBuilderKit; /** * Hibernate implementation of the Database Access Object interface class for the Item object. @@ -39,33 +51,38 @@ public class ItemDAOImpl extends AbstractHibernateDSODAO implements ItemDA private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(ItemDAOImpl.class); protected ItemDAOImpl() { - super(); } @Override public Iterator findAll(Context context, boolean archived) throws SQLException { - Query query = createQuery(context, "FROM Item WHERE inArchive=:in_archive ORDER BY id"); + Query query = createQuery(context, "SELECT i.id FROM Item i WHERE inArchive=:in_archive ORDER BY id"); query.setParameter("in_archive", archived); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findAll(Context context, boolean archived, int limit, int offset) throws SQLException { - Query query = createQuery(context, "FROM Item WHERE inArchive=:in_archive ORDER BY id"); + Query query = createQuery(context, "SELECT i.id FROM Item i WHERE inArchive=:in_archive ORDER BY id"); query.setParameter("in_archive", archived); query.setFirstResult(offset); query.setMaxResults(limit); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findAll(Context context, boolean archived, boolean withdrawn) throws SQLException { Query query = createQuery(context, - "FROM Item WHERE inArchive=:in_archive or withdrawn=:withdrawn ORDER BY id"); + "SELECT i.id FROM Item i WHERE inArchive=:in_archive or withdrawn=:withdrawn ORDER BY id"); query.setParameter("in_archive", archived); query.setParameter("withdrawn", withdrawn); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override @@ -74,12 +91,14 @@ public Iterator findAllRegularItems(Context context) throws SQLException { // It does not include workspace, workflow or template items. Query query = createQuery( context, - "SELECT i FROM Item as i " + + "SELECT i.id FROM Item as i " + "LEFT JOIN Version as v ON i = v.item " + "WHERE i.inArchive=true or i.withdrawn=true or (i.inArchive=false and v.id IS NOT NULL) " + "ORDER BY i.id" ); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override @@ -87,7 +106,7 @@ public Iterator findAll(Context context, boolean archived, boolean withdrawn, boolean discoverable, Date lastModified) throws SQLException { StringBuilder queryStr = new StringBuilder(); - queryStr.append("SELECT i FROM Item i"); + queryStr.append("SELECT i.id FROM Item i"); queryStr.append(" WHERE (inArchive = :in_archive OR withdrawn = :withdrawn)"); queryStr.append(" AND discoverable = :discoverable"); @@ -103,16 +122,20 @@ public Iterator findAll(Context context, boolean archived, if (lastModified != null) { query.setParameter("last_modified", lastModified, TemporalType.TIMESTAMP); } - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findBySubmitter(Context context, EPerson eperson) throws SQLException { Query query = createQuery(context, - "FROM Item WHERE inArchive=:in_archive and submitter=:submitter ORDER BY id"); + "SELECT i.id FROM Item i WHERE inArchive=:in_archive and submitter=:submitter ORDER BY id"); query.setParameter("in_archive", true); query.setParameter("submitter", eperson); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override @@ -121,16 +144,18 @@ public Iterator findBySubmitter(Context context, EPerson eperson, boolean if (!retrieveAllItems) { return findBySubmitter(context, eperson); } - Query query = createQuery(context, "FROM Item WHERE submitter=:submitter ORDER BY id"); + Query query = createQuery(context, "SELECT i.id FROM Item i WHERE submitter=:submitter ORDER BY id"); query.setParameter("submitter", eperson); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findBySubmitter(Context context, EPerson eperson, MetadataField metadataField, int limit) throws SQLException { StringBuilder query = new StringBuilder(); - query.append("SELECT item FROM Item as item "); + query.append("SELECT item.id FROM Item as item "); addMetadataLeftJoin(query, Item.class.getSimpleName().toLowerCase(), Collections.singletonList(metadataField)); query.append(" WHERE item.inArchive = :in_archive"); query.append(" AND item.submitter =:submitter"); @@ -142,13 +167,15 @@ public Iterator findBySubmitter(Context context, EPerson eperson, Metadata hibernateQuery.setParameter("in_archive", true); hibernateQuery.setParameter("submitter", eperson); hibernateQuery.setMaxResults(limit); - return iterate(hibernateQuery); + @SuppressWarnings("unchecked") + List uuids = hibernateQuery.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findByMetadataField(Context context, MetadataField metadataField, String value, boolean inArchive) throws SQLException { - String hqlQueryString = "SELECT item FROM Item as item join item.metadata metadatavalue " + + String hqlQueryString = "SELECT item.id FROM Item as item join item.metadata metadatavalue " + "WHERE item.inArchive=:in_archive AND metadatavalue.metadataField = :metadata_field"; if (value != null) { hqlQueryString += " AND STR(metadatavalue.value) = :text_value"; @@ -160,27 +187,130 @@ public Iterator findByMetadataField(Context context, MetadataField metadat if (value != null) { query.setParameter("text_value", value); } - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); + } + + @Override + public List findByMetadataQuery(Context context, List queryPredicates, + List collectionUuids, String regexClause, long offset, int limit) throws SQLException { + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); + Root itemRoot = criteriaQuery.from(Item.class); + criteriaQuery.select(itemRoot); + List predicates = toPredicates(criteriaBuilder, criteriaQuery, itemRoot, + queryPredicates, collectionUuids, regexClause); + criteriaQuery.where(criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new))); + criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(DSpaceObject_.id))); + criteriaQuery.groupBy(itemRoot.get(DSpaceObject_.id)); + try { + return list(context, criteriaQuery, false, Item.class, limit, (int) offset); + } catch (Exception e) { + e.printStackTrace(); + throw e; + } + } + + @Override + public long countForMetadataQuery(Context context, List queryPredicates, + List collectionUuids, String regexClause) throws SQLException { + // Build the query infrastructure + CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); + // Select + Root itemRoot = criteriaQuery.from(Item.class); + // Apply the selected predicates + List predicates = toPredicates(criteriaBuilder, criteriaQuery, itemRoot, + queryPredicates, collectionUuids, regexClause); + criteriaQuery.where(criteriaBuilder.and(predicates.stream().toArray(Predicate[]::new))); + // Execute the query + return countLong(context, criteriaQuery, criteriaBuilder, itemRoot); + } + + private List toPredicates(CriteriaBuilder criteriaBuilder, CriteriaQuery query, + Root root, List queryPredicates, + List collectionUuids, String regexClause) { + List predicates = new ArrayList<>(); + + if (!collectionUuids.isEmpty()) { + Subquery scollQuery = query.subquery(Collection.class); + Root collRoot = scollQuery.from(Collection.class); + In inColls = criteriaBuilder.in(collRoot.get(DSpaceObject_.ID)); + collectionUuids.forEach(inColls::value); + scollQuery.select(collRoot.get(DSpaceObject_.ID)) + .where(criteriaBuilder.and( + criteriaBuilder.equal(collRoot.get(DSpaceObject_.ID), + root.get(Item_.OWNING_COLLECTION).get(DSpaceObject_.ID)), + collRoot.get(DSpaceObject_.ID).in(collectionUuids) + )); + predicates.add(criteriaBuilder.exists(scollQuery)); + } + + for (int i = 0; i < queryPredicates.size(); i++) { + QueryPredicate predicate = queryPredicates.get(i); + QueryOperator op = predicate.getOperator(); + if (op == null) { + log.warn("Skipping Invalid Operator: null"); + continue; + } + + if (op.getUsesRegex()) { + if (regexClause.isEmpty()) { + log.warn("Skipping Unsupported Regex Operator: " + op); + continue; + } + } + + List mvPredicates = new ArrayList<>(); + Subquery mvQuery = query.subquery(MetadataValue.class); + Root mvRoot = mvQuery.from(MetadataValue.class); + mvPredicates.add(criteriaBuilder.equal( + mvRoot.get(MetadataValue_.D_SPACE_OBJECT), root.get(DSpaceObject_.ID))); + + if (!predicate.getFields().isEmpty()) { + In inFields = criteriaBuilder.in(mvRoot.get(MetadataValue_.METADATA_FIELD)); + predicate.getFields().forEach(inFields::value); + mvPredicates.add(inFields); + } + + JpaCriteriaBuilderKit jpaKit = new JpaCriteriaBuilderKit<>(criteriaBuilder, mvQuery, mvRoot); + mvPredicates.add(op.buildJpaPredicate(predicate.getValue(), regexClause, jpaKit)); + + mvQuery.select(mvRoot.get(MetadataValue_.D_SPACE_OBJECT)) + .where(mvPredicates.stream().toArray(Predicate[]::new)); + + if (op.getNegate()) { + predicates.add(criteriaBuilder.not(criteriaBuilder.exists(mvQuery))); + } else { + predicates.add(criteriaBuilder.exists(mvQuery)); + } + } + + log.debug(String.format("Running custom query with %d filters", queryPredicates.size())); + return predicates; } @Override public Iterator findByAuthorityValue(Context context, MetadataField metadataField, String authority, boolean inArchive) throws SQLException { Query query = createQuery(context, - "SELECT item FROM Item as item join item.metadata metadatavalue " + + "SELECT item.id FROM Item as item join item.metadata metadatavalue " + "WHERE item.inArchive=:in_archive AND metadatavalue.metadataField = :metadata_field AND " + "metadatavalue.authority = :authority ORDER BY item.id"); query.setParameter("in_archive", inArchive); query.setParameter("metadata_field", metadataField); query.setParameter("authority", authority); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findArchivedByCollection(Context context, Collection collection, Integer limit, Integer offset) throws SQLException { Query query = createQuery(context, - "select i from Item i join i.collections c " + + "select i.id from Item i join i.collections c " + "WHERE :collection IN c AND i.inArchive=:in_archive ORDER BY i.id"); query.setParameter("collection", collection); query.setParameter("in_archive", true); @@ -190,29 +320,31 @@ public Iterator findArchivedByCollection(Context context, Collection colle if (limit != null) { query.setMaxResults(limit); } - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findArchivedByCollectionExcludingOwning(Context context, Collection collection, Integer limit, Integer offset) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); Root itemRoot = criteriaQuery.from(Item.class); criteriaQuery.select(itemRoot); criteriaQuery.where(criteriaBuilder.and( criteriaBuilder.notEqual(itemRoot.get(Item_.owningCollection), collection), criteriaBuilder.isMember(collection, itemRoot.get(Item_.collections)), criteriaBuilder.isTrue(itemRoot.get(Item_.inArchive)))); - criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(Item_.id))); - criteriaQuery.groupBy(itemRoot.get(Item_.id)); + criteriaQuery.orderBy(criteriaBuilder.asc(itemRoot.get(DSpaceObject_.id))); + criteriaQuery.groupBy(itemRoot.get(DSpaceObject_.id)); return list(context, criteriaQuery, false, Item.class, limit, offset).iterator(); } @Override public int countArchivedByCollectionExcludingOwning(Context context, Collection collection) throws SQLException { CriteriaBuilder criteriaBuilder = getCriteriaBuilder(context); - CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); + CriteriaQuery criteriaQuery = getCriteriaQuery(criteriaBuilder, Item.class); Root itemRoot = criteriaQuery.from(Item.class); criteriaQuery.select(itemRoot); criteriaQuery.where(criteriaBuilder.and( @@ -225,17 +357,18 @@ public int countArchivedByCollectionExcludingOwning(Context context, Collection @Override public Iterator findAllByCollection(Context context, Collection collection) throws SQLException { Query query = createQuery(context, - "select i from Item i join i.collections c WHERE :collection IN c ORDER BY i.id"); + "select i.id from Item i join i.collections c WHERE :collection IN c ORDER BY i.id"); query.setParameter("collection", collection); - - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override public Iterator findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) throws SQLException { Query query = createQuery(context, - "select i from Item i join i.collections c WHERE :collection IN c ORDER BY i.id"); + "select i.id from Item i join i.collections c WHERE :collection IN c ORDER BY i.id"); query.setParameter("collection", collection); if (offset != null) { @@ -244,8 +377,9 @@ public Iterator findAllByCollection(Context context, Collection collection if (limit != null) { query.setMaxResults(limit); } - - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override @@ -281,9 +415,11 @@ public int countItems(Context context, List collections, boolean inc public Iterator findByLastModifiedSince(Context context, Date since) throws SQLException { Query query = createQuery(context, - "SELECT i FROM Item i WHERE last_modified > :last_modified ORDER BY id"); + "SELECT i.id FROM Item i WHERE last_modified > :last_modified ORDER BY id"); query.setParameter("last_modified", since, TemporalType.TIMESTAMP); - return iterate(query); + @SuppressWarnings("unchecked") + List uuids = query.getResultList(); + return new UUIDIterator(context, uuids, Item.class, this); } @Override diff --git a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java index 2ce3f50a3cbc..ca27abe20614 100644 --- a/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java +++ b/dspace-api/src/main/java/org/dspace/content/packager/RoleIngester.java @@ -19,6 +19,8 @@ import javax.xml.parsers.ParserConfigurationException; import org.apache.commons.codec.DecoderException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Collection; import org.dspace.content.Community; @@ -36,8 +38,6 @@ import org.dspace.eperson.factory.EPersonServiceFactory; import org.dspace.eperson.service.EPersonService; import org.dspace.eperson.service.GroupService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; @@ -51,8 +51,7 @@ * @author mwood */ public class RoleIngester implements PackageIngester { - private static final Logger log = LoggerFactory - .getLogger(RoleIngester.class); + private static final Logger log = LogManager.getLogger(); protected CommunityService communityService = ContentServiceFactory.getInstance().getCommunityService(); protected CollectionService collectionService = ContentServiceFactory.getInstance().getCollectionService(); @@ -217,10 +216,10 @@ void ingestDocument(Context context, DSpaceObject parent, // Community or Collection that doesn't currently exist in the // system. So, log a warning & skip it for now. log.warn( - "Skipping group named '" + name + "' as it seems to correspond to a Community or Collection that " + + "Skipping group named '{}' as it seems to correspond to a Community or Collection that " + "does not exist in the system. " + "If you are performing an AIP restore, you can ignore this warning as the " + - "Community/Collection AIP will likely create this group once it is processed."); + "Community/Collection AIP will likely create this group once it is processed.", name); continue; } log.debug("Translated group name: {}", name); @@ -307,7 +306,7 @@ void ingestDocument(Context context, DSpaceObject parent, // Always set the name: parent.createBlop() is guessing groupService.setName(groupObj, name); - log.info("Created Group {}.", groupObj.getName()); + log.info("Created Group {}.", groupObj::getName); } // Add EPeople to newly created Group diff --git a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java index 43a804cde2eb..a1d00e7f1311 100644 --- a/dspace-api/src/main/java/org/dspace/content/service/ItemService.java +++ b/dspace-api/src/main/java/org/dspace/content/service/ItemService.java @@ -26,6 +26,7 @@ import org.dspace.content.MetadataValue; import org.dspace.content.Thumbnail; import org.dspace.content.WorkspaceItem; +import org.dspace.contentreport.QueryPredicate; import org.dspace.core.Context; import org.dspace.discovery.SearchServiceException; import org.dspace.eperson.EPerson; @@ -41,7 +42,7 @@ public interface ItemService extends DSpaceObjectService, DSpaceObjectLegacySupportService { - public Thumbnail getThumbnail(Context context, Item item, boolean requireOriginal) throws SQLException; + Thumbnail getThumbnail(Context context, Item item, boolean requireOriginal) throws SQLException; /** * Create a new item, with a new internal ID. Authorization is done @@ -53,7 +54,7 @@ public interface ItemService * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Item create(Context context, WorkspaceItem workspaceItem) throws SQLException, AuthorizeException; + Item create(Context context, WorkspaceItem workspaceItem) throws SQLException, AuthorizeException; /** * Create a new item, with a provided ID. Authorisation is done @@ -66,7 +67,7 @@ public interface ItemService * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Item create(Context context, WorkspaceItem workspaceItem, UUID uuid) throws SQLException, AuthorizeException; + Item create(Context context, WorkspaceItem workspaceItem, UUID uuid) throws SQLException, AuthorizeException; /** * Create an empty template item for this collection. If one already exists, @@ -80,7 +81,7 @@ public interface ItemService * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Item createTemplateItem(Context context, Collection collection) throws SQLException, AuthorizeException; + Item createTemplateItem(Context context, Collection collection) throws SQLException, AuthorizeException; /** * Get all the items in the archive. Only items with the "in archive" flag @@ -90,7 +91,7 @@ public interface ItemService * @return an iterator over the items in the archive. * @throws SQLException if database error */ - public Iterator findAll(Context context) throws SQLException; + Iterator findAll(Context context) throws SQLException; /** * Get all the items in the archive. Only items with the "in archive" flag @@ -102,7 +103,7 @@ public interface ItemService * @return an iterator over the items in the archive. * @throws SQLException if database error */ - public Iterator findAll(Context context, Integer limit, Integer offset) throws SQLException; + Iterator findAll(Context context, Integer limit, Integer offset) throws SQLException; /** * Get all "final" items in the archive, both archived ("in archive" flag) or @@ -112,8 +113,7 @@ public interface ItemService * @return an iterator over the items in the archive. * @throws SQLException if database error */ - @Deprecated - public Iterator findAllUnfiltered(Context context) throws SQLException; + @Deprecated Iterator findAllUnfiltered(Context context) throws SQLException; /** * Find all items that are: @@ -126,7 +126,7 @@ public interface ItemService * @return iterator over all regular items. * @throws SQLException if database error. */ - public Iterator findAllRegularItems(Context context) throws SQLException; + Iterator findAllRegularItems(Context context) throws SQLException; /** * Find all the items in the archive by a given submitter. The order is @@ -137,7 +137,7 @@ public interface ItemService * @return an iterator over the items submitted by eperson * @throws SQLException if database error */ - public Iterator findBySubmitter(Context context, EPerson eperson) + Iterator findBySubmitter(Context context, EPerson eperson) throws SQLException; /** @@ -152,7 +152,7 @@ public Iterator findBySubmitter(Context context, EPerson eperson) * @return an iterator over the items submitted by eperson * @throws SQLException if database error */ - public Iterator findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems) + Iterator findBySubmitter(Context context, EPerson eperson, boolean retrieveAllItems) throws SQLException; /** @@ -164,7 +164,7 @@ public Iterator findBySubmitter(Context context, EPerson eperson, boolean * @return an iterator over the items submitted by eperson * @throws SQLException if database error */ - public Iterator findBySubmitterDateSorted(Context context, EPerson eperson, Integer limit) + Iterator findBySubmitterDateSorted(Context context, EPerson eperson, Integer limit) throws SQLException; /** @@ -175,7 +175,7 @@ public Iterator findBySubmitterDateSorted(Context context, EPerson eperson * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public Iterator findByCollection(Context context, Collection collection) throws SQLException; + Iterator findByCollection(Context context, Collection collection) throws SQLException; /** * Get all the archived items in this collection. The order is indeterminate. @@ -187,7 +187,7 @@ public Iterator findBySubmitterDateSorted(Context context, EPerson eperson * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public Iterator findByCollection(Context context, Collection collection, Integer limit, Integer offset) + Iterator findByCollection(Context context, Collection collection, Integer limit, Integer offset) throws SQLException; /** @@ -200,7 +200,7 @@ public Iterator findByCollection(Context context, Collection collection, I * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public Iterator findByCollectionMapping(Context context, Collection collection, Integer limit, Integer offset) + Iterator findByCollectionMapping(Context context, Collection collection, Integer limit, Integer offset) throws SQLException; /** @@ -211,7 +211,7 @@ public Iterator findByCollectionMapping(Context context, Collection collec * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public int countByCollectionMapping(Context context, Collection collection) throws SQLException; + int countByCollectionMapping(Context context, Collection collection) throws SQLException; /** * Get all the items (including private and withdrawn) in this collection. The order is indeterminate. @@ -223,7 +223,7 @@ public Iterator findByCollectionMapping(Context context, Collection collec * @param offset offset value * @throws SQLException if database error */ - public Iterator findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) + Iterator findAllByCollection(Context context, Collection collection, Integer limit, Integer offset) throws SQLException; /** @@ -234,7 +234,7 @@ public Iterator findAllByCollection(Context context, Collection collection * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public Iterator findInArchiveOrWithdrawnDiscoverableModifiedSince(Context context, Date since) + Iterator findInArchiveOrWithdrawnDiscoverableModifiedSince(Context context, Date since) throws SQLException; /** @@ -244,7 +244,7 @@ public Iterator findInArchiveOrWithdrawnDiscoverableModifiedSince(Context * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Context context, Date since) + Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Context context, Date since) throws SQLException; /** @@ -255,7 +255,7 @@ public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Conte * @return an iterator over the items in the collection. * @throws SQLException if database error */ - public Iterator findAllByCollection(Context context, Collection collection) throws SQLException; + Iterator findAllByCollection(Context context, Collection collection) throws SQLException; /** * See whether this Item is contained by a given Collection. @@ -265,7 +265,7 @@ public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Conte * @return true if {@code collection} contains this Item. * @throws SQLException if database error */ - public boolean isIn(Item item, Collection collection) throws SQLException; + boolean isIn(Item item, Collection collection) throws SQLException; /** * Get the communities this item is in. Returns an unordered array of the @@ -277,7 +277,7 @@ public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Conte * @return the communities this item is in. * @throws SQLException if database error */ - public List getCommunities(Context context, Item item) throws SQLException; + List getCommunities(Context context, Item item) throws SQLException; /** @@ -288,7 +288,7 @@ public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Conte * @return the bundles in an unordered array * @throws SQLException if database error */ - public List getBundles(Item item, String name) throws SQLException; + List getBundles(Item item, String name) throws SQLException; /** * Add an existing bundle to this item. This has immediate effect. @@ -299,7 +299,7 @@ public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Conte * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public void addBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException; + void addBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException; /** * Remove a bundle. This may result in the bundle being deleted, if the @@ -312,7 +312,7 @@ public Iterator findInArchiveOrWithdrawnNonDiscoverableModifiedSince(Conte * @throws AuthorizeException if authorization error * @throws IOException if IO error */ - public void removeBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException, + void removeBundle(Context context, Item item, Bundle bundle) throws SQLException, AuthorizeException, IOException; /** @@ -325,7 +325,7 @@ public void removeBundle(Context context, Item item, Bundle bundle) throws SQLEx * @throws AuthorizeException if authorization error * @throws IOException if IO error */ - public void removeAllBundles(Context context, Item item) throws AuthorizeException, SQLException, IOException; + void removeAllBundles(Context context, Item item) throws AuthorizeException, SQLException, IOException; /** * Create a single bitstream in a new bundle. Provided as a convenience @@ -340,7 +340,7 @@ public void removeBundle(Context context, Item item, Bundle bundle) throws SQLEx * @throws IOException if IO error * @throws SQLException if database error */ - public Bitstream createSingleBitstream(Context context, InputStream is, Item item, String name) + Bitstream createSingleBitstream(Context context, InputStream is, Item item, String name) throws AuthorizeException, IOException, SQLException; /** @@ -354,7 +354,7 @@ public Bitstream createSingleBitstream(Context context, InputStream is, Item ite * @throws IOException if IO error * @throws SQLException if database error */ - public Bitstream createSingleBitstream(Context context, InputStream is, Item item) + Bitstream createSingleBitstream(Context context, InputStream is, Item item) throws AuthorizeException, IOException, SQLException; /** @@ -367,7 +367,7 @@ public Bitstream createSingleBitstream(Context context, InputStream is, Item ite * @return non-internal bitstreams. * @throws SQLException if database error */ - public List getNonInternalBitstreams(Context context, Item item) throws SQLException; + List getNonInternalBitstreams(Context context, Item item) throws SQLException; /** * Remove just the DSpace license from an item This is useful to update the @@ -382,7 +382,7 @@ public Bitstream createSingleBitstream(Context context, InputStream is, Item ite * @throws AuthorizeException if authorization error * @throws IOException if IO error */ - public void removeDSpaceLicense(Context context, Item item) throws SQLException, AuthorizeException, + void removeDSpaceLicense(Context context, Item item) throws SQLException, AuthorizeException, IOException; /** @@ -394,7 +394,7 @@ public void removeDSpaceLicense(Context context, Item item) throws SQLException, * @throws AuthorizeException if authorization error * @throws IOException if IO error */ - public void removeLicenses(Context context, Item item) throws SQLException, AuthorizeException, IOException; + void removeLicenses(Context context, Item item) throws SQLException, AuthorizeException, IOException; /** * Withdraw the item from the archive. It is kept in place, and the content @@ -405,7 +405,7 @@ public void removeDSpaceLicense(Context context, Item item) throws SQLException, * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public void withdraw(Context context, Item item) throws SQLException, AuthorizeException; + void withdraw(Context context, Item item) throws SQLException, AuthorizeException; /** @@ -416,7 +416,7 @@ public void removeDSpaceLicense(Context context, Item item) throws SQLException, * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public void reinstate(Context context, Item item) throws SQLException, AuthorizeException; + void reinstate(Context context, Item item) throws SQLException, AuthorizeException; /** * Return true if this Collection 'owns' this item @@ -425,7 +425,7 @@ public void removeDSpaceLicense(Context context, Item item) throws SQLException, * @param collection Collection * @return true if this Collection owns this item */ - public boolean isOwningCollection(Item item, Collection collection); + boolean isOwningCollection(Item item, Collection collection); /** * remove all of the policies for item and replace them with a new list of @@ -439,7 +439,7 @@ public void removeDSpaceLicense(Context context, Item item) throws SQLException, * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public void replaceAllItemPolicies(Context context, Item item, List newpolicies) + void replaceAllItemPolicies(Context context, Item item, List newpolicies) throws SQLException, AuthorizeException; @@ -455,7 +455,7 @@ public void replaceAllItemPolicies(Context context, Item item, List newpolicies) + void replaceAllBitstreamPolicies(Context context, Item item, List newpolicies) throws SQLException, AuthorizeException; @@ -469,7 +469,7 @@ public void replaceAllBitstreamPolicies(Context context, Item item, List getCollectionsNotLinked(Context context, Item item) throws SQLException; + List getCollectionsNotLinked(Context context, Item item) throws SQLException; /** * return TRUE if context's user can edit item, false otherwise @@ -686,7 +686,7 @@ public void move(Context context, Item item, Collection from, Collection to, boo * @return boolean true = current user can edit item * @throws SQLException if database error */ - public boolean canEdit(Context context, Item item) throws java.sql.SQLException; + boolean canEdit(Context context, Item item) throws java.sql.SQLException; /** * return TRUE if context's user can create new version of the item, false @@ -697,7 +697,7 @@ public void move(Context context, Item item, Collection from, Collection to, boo * @return boolean true = current user can create new version of the item * @throws SQLException if database error */ - public boolean canCreateNewVersion(Context context, Item item) throws SQLException; + boolean canCreateNewVersion(Context context, Item item) throws SQLException; /** * Returns an iterator of in archive items possessing the passed metadata field, or only @@ -712,7 +712,7 @@ public void move(Context context, Item item, Collection from, Collection to, boo * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Iterator findArchivedByMetadataField(Context context, String schema, + Iterator findArchivedByMetadataField(Context context, String schema, String element, String qualifier, String value) throws SQLException, AuthorizeException; @@ -727,7 +727,7 @@ public Iterator findArchivedByMetadataField(Context context, String schema * @throws SQLException if database error * @throws AuthorizeException if authorization error */ - public Iterator findArchivedByMetadataField(Context context, String metadataField, String value) + Iterator findArchivedByMetadataField(Context context, String metadataField, String value) throws SQLException, AuthorizeException; /** @@ -744,10 +744,42 @@ public Iterator findArchivedByMetadataField(Context context, String metada * @throws AuthorizeException if authorization error * @throws IOException if IO error */ - public Iterator findByMetadataField(Context context, + Iterator findByMetadataField(Context context, String schema, String element, String qualifier, String value) throws SQLException, AuthorizeException, IOException; + /** + * Returns a list of items that match the given predicates, within the + * specified collections, if any. This querying method is used by the + * Filtered Items report functionality. + * @param context DSpace context object + * @param queryPredicates metadata field predicates + * @param collectionUuids UUIDs of the collections to search + * @param offset position in the list to start returning items + * @param limit maximum number of items to return + * @return a list of matching items in the specified collections, + * or in any collection if no collection UUIDs are provided + * @throws SQLException if a database error occurs + */ + List findByMetadataQuery(Context context, List queryPredicates, + List collectionUuids, long offset, int limit) + throws SQLException; + + /** + * Returns the total number of items that match the given predicates, within the + * specified collections, if any. This querying method is used for pagination by the + * Filtered Items report functionality. + * @param context DSpace context object + * @param queryPredicates metadata field predicates + * @param collectionUuids UUIDs of the collections to search + * @return the total number of matching items in the specified collections, + * or in any collection if no collection UUIDs are provided + * @throws SQLException if a database error occurs + */ + long countForMetadataQuery(Context context, List queryPredicates, + List collectionUuids) + throws SQLException; + /** * Find all the items in the archive with a given authority key value * in the indicated metadata field. @@ -762,12 +794,12 @@ public Iterator findByMetadataField(Context context, * @throws AuthorizeException if authorization error * @throws IOException if IO error */ - public Iterator findByAuthorityValue(Context context, + Iterator findByAuthorityValue(Context context, String schema, String element, String qualifier, String value) throws SQLException, AuthorizeException; - public Iterator findByMetadataFieldAuthority(Context context, String mdString, String authority) + Iterator findByMetadataFieldAuthority(Context context, String mdString, String authority) throws SQLException, AuthorizeException; /** @@ -779,7 +811,7 @@ public Iterator findByMetadataFieldAuthority(Context context, String mdStr * @param item item * @return true or false */ - public boolean isItemListedForUser(Context context, Item item); + boolean isItemListedForUser(Context context, Item item); /** * counts items in the given collection @@ -789,7 +821,7 @@ public Iterator findByMetadataFieldAuthority(Context context, String mdStr * @return total items * @throws SQLException if database error */ - public int countItems(Context context, Collection collection) throws SQLException; + int countItems(Context context, Collection collection) throws SQLException; /** * counts all items in the given collection including withdrawn items @@ -799,7 +831,7 @@ public Iterator findByMetadataFieldAuthority(Context context, String mdStr * @return total items * @throws SQLException if database error */ - public int countAllItems(Context context, Collection collection) throws SQLException; + int countAllItems(Context context, Collection collection) throws SQLException; /** * Find all Items modified since a Date. @@ -809,7 +841,7 @@ public Iterator findByMetadataFieldAuthority(Context context, String mdStr * @return iterator over items * @throws SQLException if database error */ - public Iterator findByLastModifiedSince(Context context, Date last) + Iterator findByLastModifiedSince(Context context, Date last) throws SQLException; /** @@ -820,7 +852,7 @@ public Iterator findByLastModifiedSince(Context context, Date last) * @return total items * @throws SQLException if database error */ - public int countItems(Context context, Community community) throws SQLException; + int countItems(Context context, Community community) throws SQLException; /** * counts all items in the given community including withdrawn @@ -830,7 +862,7 @@ public Iterator findByLastModifiedSince(Context context, Date last) * @return total items * @throws SQLException if database error */ - public int countAllItems(Context context, Community community) throws SQLException; + int countAllItems(Context context, Community community) throws SQLException; /** * counts all items @@ -877,7 +909,7 @@ public Iterator findByLastModifiedSince(Context context, Date last) * @throws SQLException * @throws SearchServiceException */ - public List findItemsWithEdit(Context context, int offset, int limit) + List findItemsWithEdit(Context context, int offset, int limit) throws SQLException, SearchServiceException; /** @@ -887,7 +919,7 @@ public List findItemsWithEdit(Context context, int offset, int limit) * @throws SQLException * @throws SearchServiceException */ - public int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; + int countItemsWithEdit(Context context) throws SQLException, SearchServiceException; /** * Check if the supplied item is an inprogress submission @@ -947,7 +979,7 @@ public List findItemsWithEdit(Context context, int offset, int limit) * relationships. * @return metadata fields that match the parameters */ - public List getMetadata(Item item, String schema, String element, String qualifier, + List getMetadata(Item item, String schema, String element, String qualifier, String lang, boolean enableVirtualMetadata); /** @@ -955,7 +987,7 @@ public List getMetadata(Item item, String schema, String element, * @param item the item. * @return the label of the entity type, taken from the item metadata, or null if not found. */ - public String getEntityTypeLabel(Item item); + String getEntityTypeLabel(Item item); /** * Retrieve the entity type of the given item. @@ -963,6 +995,6 @@ public List getMetadata(Item item, String schema, String element, * @param item the item. * @return the entity type of the given item, or null if not found. */ - public EntityType getEntityType(Context context, Item item) throws SQLException; + EntityType getEntityType(Context context, Item item) throws SQLException; } diff --git a/dspace-api/src/main/java/org/dspace/contentreport/ContentReportServiceImpl.java b/dspace-api/src/main/java/org/dspace/contentreport/ContentReportServiceImpl.java new file mode 100644 index 000000000000..e22ac0e96fdc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/ContentReportServiceImpl.java @@ -0,0 +1,190 @@ +/** + * 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.contentreport; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.apache.logging.log4j.Logger; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.service.CollectionService; +import org.dspace.content.service.ItemService; +import org.dspace.content.service.MetadataFieldService; +import org.dspace.contentreport.service.ContentReportService; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; + +public class ContentReportServiceImpl implements ContentReportService { + + private static final Logger log = org.apache.logging.log4j.LogManager + .getLogger(ContentReportServiceImpl.class); + + @Autowired + protected ConfigurationService configurationService; + @Autowired + private CollectionService collectionService; + @Autowired + private ItemService itemService; + @Autowired + private MetadataFieldService metadataFieldService; + + /** + * Returns true< if Content Reports are enabled. + * @return true< if Content Reports are enabled + */ + @Override + public boolean getEnabled() { + return configurationService.getBooleanProperty("contentreport.enable"); + } + + /** + * Retrieves item statistics per collection according to a set of Boolean filters. + * @param context DSpace context + * @param filters Set of filters + * @return a list of collections with the requested statistics for each of them + */ + @Override + public List findFilteredCollections(Context context, java.util.Collection filters) { + List colls = new ArrayList<>(); + try { + List collections = collectionService.findAll(context); + for (Collection collection : collections) { + FilteredCollection coll = new FilteredCollection(); + coll.setHandle(collection.getHandle()); + coll.setLabel(collection.getName()); + Community community = collection.getCommunities().stream() + .findFirst() + .orElse(null); + if (community != null) { + coll.setCommunityLabel(community.getName()); + coll.setCommunityHandle(community.getHandle()); + } + colls.add(coll); + + Iterator items = itemService.findAllByCollection(context, collection); + int nbTotalItems = 0; + while (items.hasNext()) { + Item item = items.next(); + nbTotalItems++; + boolean matchesAllFilters = true; + for (Filter filter : filters) { + if (filter.testItem(context, item)) { + coll.addValue(filter, 1); + } else { + // This ensures the requested filter is present in the collection record + // even when there are no matching items. + coll.addValue(filter, 0); + matchesAllFilters = false; + } + } + if (matchesAllFilters) { + coll.addAllFiltersValue(1); + } + } + coll.setTotalItems(nbTotalItems); + coll.seal(); + } + } catch (SQLException e) { + log.error("SQLException trying to receive filtered collections statistics", e); + } + return colls; + } + + /** + * Retrieves a list of items according to a set of criteria. + * @param context DSpace context + * @param query structured query to find items against + * @return a list of items filtered according to the provided query + */ + @Override + public FilteredItems findFilteredItems(Context context, FilteredItemsQuery query) { + FilteredItems report = new FilteredItems(); + + List predicates = query.getQueryPredicates(); + List collectionUuids = getUuidsFromStrings(query.getCollections()); + Set filters = query.getFilters(); + + try { + List items = itemService.findByMetadataQuery(context, predicates, collectionUuids, + query.getOffset(), query.getPageLimit()); + items.stream() + .filter(item -> filters.stream().allMatch(f -> f.testItem(context, item))) + .forEach(report::addItem); + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + try { + long count = itemService.countForMetadataQuery(context, predicates, collectionUuids); + report.setItemCount(count); + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + + return report; + } + + /** + * Converts a metadata field name to a list of {@link MetadataField} instances + * (one if no wildcards are used, possibly more otherwise). + * @param context DSpace context + * @param metadataField field to search for + * @return a corresponding list of {@link MetadataField} entries + */ + @Override + public List getMetadataFields(org.dspace.core.Context context, String metadataField) + throws SQLException { + List fields = new ArrayList<>(); + if ("*".equals(metadataField)) { + return fields; + } + String schema = ""; + String element = ""; + String qualifier = null; + String[] parts = metadataField.split("\\."); + if (parts.length > 0) { + schema = parts[0]; + } + if (parts.length > 1) { + element = parts[1]; + } + if (parts.length > 2) { + qualifier = parts[2]; + } + + if (Item.ANY.equals(qualifier)) { + fields.addAll(metadataFieldService.findFieldsByElementNameUnqualified(context, schema, element)); + } else { + MetadataField mf = metadataFieldService.findByElement(context, schema, element, qualifier); + if (mf != null) { + fields.add(mf); + } + } + return fields; + } + + private static List getUuidsFromStrings(List collSel) { + List uuids = new ArrayList<>(); + for (String s: collSel) { + try { + uuids.add(UUID.fromString(s)); + } catch (IllegalArgumentException e) { + log.warn("Invalid collection UUID: " + s); + } + } + return uuids; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/Filter.java b/dspace-api/src/main/java/org/dspace/contentreport/Filter.java new file mode 100644 index 000000000000..e5fa588140f3 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/Filter.java @@ -0,0 +1,399 @@ +/** + * 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.contentreport; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.authorize.factory.AuthorizeServiceFactory; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.contentreport.ItemFilterUtil.BundleName; +import org.dspace.core.Context; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Available filters for the Filtered Collections and Filtered Items reports. + * In this enum, each item corresponds to a separate property, not values of + * a single property, hence the @JsonProperty applied to each of them. + * For each item, the annotation value is read through reflection and copied into + * the id property, which eliminates repetitions, hence reducing the risk or errors. + * + * @author Jean-François Morin (Université Laval) + */ +public enum Filter { + + @JsonProperty("is_item") + IS_ITEM(FilterCategory.PROPERTY, (context, item) -> true), + @JsonProperty("is_withdrawn") + IS_WITHDRAWN(FilterCategory.PROPERTY, (context, item) -> item.isWithdrawn()), + @JsonProperty("is_not_withdrawn") + IS_NOT_WITHDRAWN(FilterCategory.PROPERTY, (context, item) -> !item.isWithdrawn()), + @JsonProperty("is_discoverable") + IS_DISCOVERABLE(FilterCategory.PROPERTY, (context, item) -> item.isDiscoverable()), + @JsonProperty("is_not_discoverable") + IS_NOT_DISCOVERABLE(FilterCategory.PROPERTY, (context, item) -> !item.isDiscoverable()), + + /** + * Matches items having multiple original bitstreams. + */ + @JsonProperty("has_multiple_originals") + HAS_MULTIPLE_ORIGINALS(FilterCategory.BITSTREAM, (context, item) -> + ItemFilterUtil.countOriginalBitstream(item) > 1), + /** + * Matches items having no original bitstreams. + */ + @JsonProperty("has_no_originals") + HAS_NO_ORIGINALS(FilterCategory.BITSTREAM, (context, item) -> ItemFilterUtil.countOriginalBitstream(item) == 0), + /** + * Matches items having exactly one original bitstream. + */ + @JsonProperty("has_one_original") + HAS_ONE_ORIGINAL(FilterCategory.BITSTREAM, (context, item) -> ItemFilterUtil.countOriginalBitstream(item) == 1), + + /** + * Matches items having bitstreams with a MIME type that matches one defined in the "rest.report-mime-document" + * configuration property. + */ + @JsonProperty("has_doc_original") + HAS_DOC_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> + ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()) > 0), + /** + * Matches items having bitstreams with a MIME type starting with "image" (e.g., image/jpeg, image/png). + */ + @JsonProperty("has_image_original") + HAS_IMAGE_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> + ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image") > 0), + /** + * Matches items having bitstreams with a MIME type other than document (cf. HAS_DOCUMENT above) or image + * (cf. HAS_IMAGE_ORIGINAL above). + */ + @JsonProperty("has_unsupp_type") + HAS_UNSUPPORTED_TYPE(FilterCategory.BITSTREAM_MIME, (context, item) -> { + int bitCount = ItemFilterUtil.countOriginalBitstream(item); + if (bitCount == 0) { + return false; + } + int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()); + int imgCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image"); + return (bitCount - docCount - imgCount) > 0; + }), + /** + * Matches items having bitstreams of multiple types (document, image, other). + */ + @JsonProperty("has_mixed_original") + HAS_MIXED_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> { + int countBit = ItemFilterUtil.countOriginalBitstream(item); + if (countBit <= 1) { + return false; + } + int countDoc = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()); + if (countDoc > 0) { + return countDoc != countBit; + } + int countImg = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image"); + if (countImg > 0) { + return countImg != countBit; + } + return false; + }), + @JsonProperty("has_pdf_original") + HAS_PDF_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> + ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.MIMES_PDF) > 0), + @JsonProperty("has_jpg_original") + HAS_JPEG_ORIGINAL(FilterCategory.BITSTREAM_MIME, (context, item) -> + ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.MIMES_JPG) > 0), + /** + * Matches items having at least one PDF of size less than 20 kb (configurable in rest.cfg). + */ + @JsonProperty("has_small_pdf") + HAS_SMALL_PDF(FilterCategory.BITSTREAM_MIME, (context, item) -> + ItemFilterUtil.countBitstreamSmallerThanMinSize( + context, BundleName.ORIGINAL, item, ItemFilterUtil.MIMES_PDF, "rest.report-pdf-min-size") > 0), + /** + * Matches items having at least one PDF of size more than 25 Mb (configurable in rest.cfg). + */ + @JsonProperty("has_large_pdf") + HAS_LARGE_PDF(FilterCategory.BITSTREAM_MIME, (context, item) -> + ItemFilterUtil.countBitstreamLargerThanMaxSize( + context, BundleName.ORIGINAL, item, ItemFilterUtil.MIMES_PDF, "rest.report-pdf-max-size") > 0), + /** + * Matches items having at least one non-text bitstream. + */ + @JsonProperty("has_doc_without_text") + HAS_DOC_WITHOUT_TEXT(FilterCategory.BITSTREAM_MIME, (context, item) -> { + int countDoc = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()); + if (countDoc == 0) { + return false; + } + int countText = ItemFilterUtil.countBitstream(BundleName.TEXT, item); + return countDoc > countText; + }), + + /** + * Matches items having at least one image, but all of supported types. + */ + @JsonProperty("has_only_supp_image_type") + HAS_ONLY_SUPPORTED_IMAGE_TYPE(FilterCategory.MIME, (context, item) -> { + int imageCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image/"); + if (imageCount == 0) { + return false; + } + int suppImageCount = ItemFilterUtil.countOriginalBitstreamMime( + context, item, ItemFilterUtil.getSupportedImageMimeTypes()); + return (imageCount == suppImageCount); + }), + /** + * Matches items having at least one image of an unsupported type. + */ + @JsonProperty("has_unsupp_image_type") + HAS_UNSUPPORTED_IMAGE_TYPE(FilterCategory.MIME, (context, item) -> { + int imageCount = ItemFilterUtil.countOriginalBitstreamMimeStartsWith(context, item, "image/"); + if (imageCount == 0) { + return false; + } + int suppImageCount = ItemFilterUtil.countOriginalBitstreamMime( + context, item, ItemFilterUtil.getSupportedImageMimeTypes()); + return (imageCount - suppImageCount) > 0; + }), + /** + * Matches items having at least one document, but all of supported types. + */ + @JsonProperty("has_only_supp_doc_type") + HAS_ONLY_SUPPORTED_DOC_TYPE(FilterCategory.MIME, (context, item) -> { + int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()); + if (docCount == 0) { + return false; + } + int suppDocCount = ItemFilterUtil.countOriginalBitstreamMime( + context, item, ItemFilterUtil.getSupportedDocumentMimeTypes()); + return docCount == suppDocCount; + }), + /** + * Matches items having at least one document of an unsupported type. + */ + @JsonProperty("has_unsupp_doc_type") + HAS_UNSUPPORTED_DOC_TYPE(FilterCategory.MIME, (context, item) -> { + int docCount = ItemFilterUtil.countOriginalBitstreamMime(context, item, ItemFilterUtil.getDocumentMimeTypes()); + if (docCount == 0) { + return false; + } + int suppDocCount = ItemFilterUtil.countOriginalBitstreamMime( + context, item, ItemFilterUtil.getSupportedDocumentMimeTypes()); + return (docCount - suppDocCount) > 0; + }), + + /** + * Matches items having at least one unsupported bundle. + */ + @JsonProperty("has_unsupported_bundle") + HAS_UNSUPPORTED_BUNDLE(FilterCategory.BUNDLE, (context, item) -> { + String[] bundleList = DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("rest.report-supp-bundles"); + return ItemFilterUtil.hasUnsupportedBundle(item, bundleList); + }), + /** + * Matches items having at least one thumbnail of size less than 400 bytes (configurable in rest.cfg). + */ + @JsonProperty("has_small_thumbnail") + HAS_SMALL_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> + ItemFilterUtil.countBitstreamSmallerThanMinSize( + context, BundleName.THUMBNAIL, item, ItemFilterUtil.MIMES_JPG, "rest.report-thumbnail-min-size") > 0), + /** + * Matches items having at least one original without a thumbnail. + */ + @JsonProperty("has_original_without_thumbnail") + HAS_ORIGINAL_WITHOUT_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> { + int countBit = ItemFilterUtil.countOriginalBitstream(item); + if (countBit == 0) { + return false; + } + int countThumb = ItemFilterUtil.countBitstream(BundleName.THUMBNAIL, item); + return countBit > countThumb; + }), + /** + * Matches items having at least one non-JPEG thumbnail. + */ + @JsonProperty("has_invalid_thumbnail_name") + HAS_INVALID_THUMBNAIL_NAME(FilterCategory.BUNDLE, (context, item) -> { + List originalNames = ItemFilterUtil.getBitstreamNames(BundleName.ORIGINAL, item); + List thumbNames = ItemFilterUtil.getBitstreamNames(BundleName.THUMBNAIL, item); + if (thumbNames.size() != originalNames.size()) { + return false; + } + return originalNames.stream() + .anyMatch(name -> !thumbNames.contains(name + ".jpg") && !thumbNames.contains(name + ".jpeg")); + }), + /** + * Matches items having at least one non-generated thumbnail. + */ + @JsonProperty("has_non_generated_thumb") + HAS_NON_GENERATED_THUMBNAIL(FilterCategory.BUNDLE, (context, item) -> { + String[] generatedThumbDesc = DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("rest.report-gen-thumbnail-desc"); + int countThumb = ItemFilterUtil.countBitstream(BundleName.THUMBNAIL, item); + if (countThumb == 0) { + return false; + } + int countGen = ItemFilterUtil.countBitstreamByDesc(BundleName.THUMBNAIL, item, generatedThumbDesc); + return (countThumb > countGen); + }), + /** + * Matches items having no licence-typed bitstreams. + */ + @JsonProperty("no_license") + NO_LICENSE(FilterCategory.BUNDLE, (context, item) -> + ItemFilterUtil.countBitstream(BundleName.LICENSE, item) == 0), + /** + * Matches items having licence documentation (a licence bitstream named other than license.txt). + */ + @JsonProperty("has_license_documentation") + HAS_LICENSE_DOCUMENTATION(FilterCategory.BUNDLE, (context, item) -> { + List names = ItemFilterUtil.getBitstreamNames(BundleName.LICENSE, item); + return names.stream() + .anyMatch(name -> !name.equals("license.txt")); + }), + + /** + * Matches items having at least one original with restricted access. + */ + @JsonProperty("has_restricted_original") + HAS_RESTRICTED_ORIGINAL(FilterCategory.PERMISSION, (context, item) -> { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(BundleName.ORIGINAL.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .anyMatch(bit -> { + try { + if (!getAuthorizeService() + .authorizeActionBoolean(getAnonymousContext(), bit, org.dspace.core.Constants.READ)) { + return true; + } + } catch (SQLException e) { + getLog().warn("SQL Exception testing original bitstream access " + e.getMessage(), e); + } + return false; + }); + }), + /** + * Matches items having at least one thumbnail with restricted access. + */ + @JsonProperty("has_restricted_thumbnail") + HAS_RESTRICTED_THUMBNAIL(FilterCategory.PERMISSION, (context, item) -> { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(BundleName.THUMBNAIL.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .anyMatch(bit -> { + try { + if (!getAuthorizeService() + .authorizeActionBoolean(getAnonymousContext(), bit, org.dspace.core.Constants.READ)) { + return true; + } + } catch (SQLException e) { + getLog().warn("SQL Exception testing thumbnail bitstream access " + e.getMessage(), e); + } + return false; + }); + }), + /** + * Matches items having metadata with restricted access. + */ + @JsonProperty("has_restricted_metadata") + HAS_RESTRICTED_METADATA(FilterCategory.PERMISSION, (context, item) -> { + try { + return !getAuthorizeService() + .authorizeActionBoolean(getAnonymousContext(), item, org.dspace.core.Constants.READ); + } catch (SQLException e) { + getLog().warn("SQL Exception testing item metadata access " + e.getMessage(), e); + return false; + } + }); + + private static final Logger log = LogManager.getLogger(); + private static AuthorizeService authorizeService; + private static Context anonymousContext; + + private String id; + private FilterCategory category; + private BiPredicate itemTester; + + Filter(FilterCategory category, BiPredicate itemTester) { + try { + JsonProperty jp = getClass().getField(name()).getAnnotation(JsonProperty.class); + id = Optional.ofNullable(jp).map(JsonProperty::value).orElse(name()); + } catch (Exception e) { + id = name(); + } + this.category = category; + this.itemTester = itemTester; + } + + public String getId() { + return id; + } + + public FilterCategory getCategory() { + return category; + } + + public boolean testItem(Context context, Item item) { + return itemTester.test(context, item); + } + + private static Logger getLog() { + return log; + } + + private static AuthorizeService getAuthorizeService() { + if (authorizeService == null) { + authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + } + return authorizeService; + } + + private static Context getAnonymousContext() { + if (anonymousContext == null) { + anonymousContext = new Context(); + } + return anonymousContext; + } + + @JsonCreator + public static Filter get(String id) { + return Arrays.stream(values()) + .filter(item -> Objects.equals(item.id, id)) + .findFirst() + .orElse(null); + } + + public static Set getFilters(String filters) { + String[] ids = Optional.ofNullable(filters).orElse("").split("[^a-z_]+"); + Set set = Arrays.stream(ids) + .map(Filter::get) + .filter(f -> f != null) + .collect(Collectors.toCollection(() -> EnumSet.noneOf(Filter.class))); + if (set == null) { + set = EnumSet.noneOf(Filter.class); + } + return set; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/FilterCategory.java b/dspace-api/src/main/java/org/dspace/contentreport/FilterCategory.java new file mode 100644 index 000000000000..8823ff31413f --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/FilterCategory.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.contentreport; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Identifies the category/section of filters defined in the {@link Filter} enum. + * This enum will be used when/if the structured filter definitions are returned to + * the Angular layer through a REST endpoint. + * + * @author Jean-François Morin (Université Laval) + */ +public enum FilterCategory { + + PROPERTY("property"), + BITSTREAM("bitstream"), + BITSTREAM_MIME("bitstream_mime"), + MIME("mime"), + BUNDLE("bundle"), + PERMISSION("permission"); + + private String id; + private List filters; + + FilterCategory(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public List getFilters() { + if (filters == null) { + filters = Arrays.stream(Filter.values()) + .filter(f -> f.getCategory() == this) + .collect(Collectors.toUnmodifiableList()); + } + return filters; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/FilteredCollection.java b/dspace-api/src/main/java/org/dspace/contentreport/FilteredCollection.java new file mode 100644 index 000000000000..21a39778babf --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/FilteredCollection.java @@ -0,0 +1,219 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.contentreport; + +import java.io.Serializable; +import java.util.EnumMap; +import java.util.Map; +import java.util.Optional; + +/** + * This class represents an entry in the Filtered Collections report. + * + * @author Jean-François Morin (Université Laval) + */ +public class FilteredCollection implements Cloneable, Serializable { + + private static final long serialVersionUID = -231735620268582719L; + + /** Name of the collection */ + private String label; + /** Handle of the collection, used to make it clickable from the generated report */ + private String handle; + /** Name of the owning community */ + private String communityLabel; + /** Handle of the owning community, used to make it clickable from the generated report */ + private String communityHandle; + /** Total number of items in the collection */ + private int totalItems; + /** Number of filtered items per requested filter in the collection */ + private Map values = new EnumMap<>(Filter.class); + /** Number of items in the collection that match all requested filters */ + private int allFiltersValue; + /** + * Indicates whether this object is protected against further changes. + * This is used in computing summary data in the parent FilteredCollectionsRest class. + */ + private boolean sealed; + + /** + * Shortcut method that builds a FilteredCollectionRest instance + * from its building blocks. + * @param label Name of the collection + * @param handle Handle of the collection + * @param communityLabel Name of the owning community + * @param communityHandle Handle of the owning community + * @param totalItems Total number of items in the collection + * @param allFiltersValue Number of items in the collection that match all requested filters + * @param values Number of filtered items per requested filter in the collection + * @param doSeal true if the collection must be sealed immediately + * @return a FilteredCollectionRest instance built from the provided parameters + */ + public static FilteredCollection of(String label, String handle, + String communityLabel, String communityHandle, + int totalItems, int allFiltersValue, Map values, boolean doSeal) { + var coll = new FilteredCollection(); + coll.label = label; + coll.handle = handle; + coll.communityLabel = communityLabel; + coll.communityHandle = communityHandle; + coll.totalItems = totalItems; + coll.allFiltersValue = allFiltersValue; + Optional.ofNullable(values).ifPresent(vs -> vs.forEach(coll::addValue)); + if (doSeal) { + coll.seal(); + } + return coll; + } + + /** + * Returns the item counts per filter. + * If this object is sealed, a defensive copy will be returned. + * + * @return the item counts per filter + */ + public Map getValues() { + if (sealed) { + return new EnumMap<>(values); + } + return values; + } + + /** + * Increments a filtered item count for a given filter. + * + * @param filter Filter to add to the requested filters in this collection + * @param delta Number by which the filtered item count must be incremented + * for the requested filter + */ + public void addValue(Filter filter, int delta) { + checkSealed(); + Integer oldValue = values.getOrDefault(filter, Integer.valueOf(0)); + int newValue = oldValue.intValue() + delta; + values.put(filter, Integer.valueOf(newValue)); + } + + /** + * Sets all filtered item counts for this collection. + * The contents are copied into this object's internal Map, which is protected against + * further tampering with the provided Map. + * + * @param values Values that replace the current ones + */ + public void setValues(Map values) { + checkSealed(); + this.values.clear(); + this.values.putAll(values); + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + checkSealed(); + this.label = label; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + checkSealed(); + this.handle = handle; + } + + public String getCommunityLabel() { + return communityLabel; + } + + public void setCommunityLabel(String communityLabel) { + checkSealed(); + this.communityLabel = communityLabel; + } + + public String getCommunityHandle() { + return communityHandle; + } + + public void setCommunityHandle(String communityHandle) { + checkSealed(); + this.communityHandle = communityHandle; + } + + public int getTotalItems() { + return totalItems; + } + + public void setTotalItems(int totalItems) { + checkSealed(); + this.totalItems = totalItems; + } + + public int getAllFiltersValue() { + return allFiltersValue; + } + + /** + * Increments the count of items matching all filters. + * + * @param delta Number by which the count must be incremented + */ + public void addAllFiltersValue(int delta) { + checkSealed(); + allFiltersValue++; + } + + /** + * Replaces the count of items matching all filters. + * + * @param allFiltersValue Number that replaces the current item count + */ + public void setAllFiltersValue(int allFiltersValue) { + checkSealed(); + this.allFiltersValue = allFiltersValue; + } + + public boolean getSealed() { + return sealed; + } + + /** + * Seals this filtered collection object. + * No changes to this object can be made afterwards. Any attempt will throw + * an IllegalStateException. + */ + public void seal() { + sealed = true; + } + + private void checkSealed() { + if (sealed) { + throw new IllegalStateException("This filtered collection record is sealed" + + " and cannot be modified anymore. You can apply changes to a non-sealed clone."); + } + } + + /** + * Returns a non-sealed clone of this filtered collection record. + * + * @return a new non-sealed FilteredCollectionRest instance containing + * all attribute values of this object + */ + @Override + public FilteredCollection clone() { + var clone = new FilteredCollection(); + clone.label = label; + clone.handle = handle; + clone.values.putAll(values); + clone.allFiltersValue = allFiltersValue; + return clone; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/FilteredCollections.java b/dspace-api/src/main/java/org/dspace/contentreport/FilteredCollections.java new file mode 100644 index 000000000000..058d1546d160 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/FilteredCollections.java @@ -0,0 +1,107 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.contentreport; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * This class represents the complete result of a Filtered Collections report query. + * In addition to the list of FilteredCollection entries, it contains the lazily computed + * summary to be included in the completed report. + * + * @author Jean-François Morin (Université Laval) + */ +public class FilteredCollections implements Serializable { + + private static final long serialVersionUID = 3622651208704009095L; + + /** Collections included in the report */ + private List collections = new ArrayList<>(); + /** + * Summary generated by adding up data for each filter included in the report. + * It will be regenerated if any non-sealed collection item is found in + * the {@link #collections} collection attribute. + */ + private FilteredCollection summary; + + /** + * Shortcut method that builds a FilteredCollectionsRest instance + * from its building blocks. + * @param collections a list of FilteredCollectionRest instances + * @return a FilteredCollectionsRest instance built from the provided parameters + */ + public static FilteredCollections of(Collection collections) { + var colls = new FilteredCollections(); + Optional.ofNullable(collections).ifPresent(cs -> cs.stream().forEach(colls::addCollection)); + return colls; + } + + /** + * Returns a defensive copy of the collections included in this report. + * + * @return the collections included in this report + */ + public List getCollections() { + return new ArrayList<>(collections); + } + + /** + * Adds a {@link FilteredCollectionRest} object to this report. + * + * @param coll {@link FilteredCollectionRest} to add to this report + */ + public void addCollection(FilteredCollection coll) { + summary = null; + collections.add(coll); + } + + /** + * Sets all collections for this report. + * The contents are copied into this object's internal list, which is protected against + * further tampering with the provided list. + * + * @param collections Values that replace the current ones + */ + public void setCollections(List collections) { + summary = null; + this.collections.clear(); + this.collections.addAll(collections); + } + + /** + * Returns the report summary. + * If the summary has not been computed yet and/or the report includes non-sealed collections, + * it will be regenerated. + * + * @return the generated report summary + */ + public FilteredCollection getSummary() { + boolean needsRefresh = summary == null || collections.stream().anyMatch(c -> !c.getSealed()); + if (needsRefresh) { + summary = new FilteredCollection(); + for (var coll : collections) { + coll.getValues().forEach(summary::addValue); + } + int total = collections.stream() + .mapToInt(FilteredCollection::getTotalItems) + .sum(); + summary.setTotalItems(total); + int allFilters = collections.stream() + .mapToInt(FilteredCollection::getAllFiltersValue) + .sum(); + summary.setAllFiltersValue(allFilters); + summary.seal(); + } + return summary; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/FilteredItems.java b/dspace-api/src/main/java/org/dspace/contentreport/FilteredItems.java new file mode 100644 index 000000000000..71c4c74a3ecd --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/FilteredItems.java @@ -0,0 +1,70 @@ +/** + * 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.contentreport; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import org.dspace.content.Item; + +/** + * This class represents a list of items for a Filtered Items report query. + * Since the underlying list should correspond to only a page of results, + * the total number of items found through the query is included in this report. + * + * @author Jean-François Morin (Université Laval) + */ +public class FilteredItems implements Serializable { + + private static final long serialVersionUID = 7980375013177658249L; + + /** Items included in the report */ + private List items = new ArrayList<>(); + /** Total item count (for pagination) */ + private long itemCount; + + /** + * Returns a defensive copy of the items included in this report. + * + * @return the items included in this report + */ + public List getItems() { + return new ArrayList<>(items); + } + + /** + * Adds an {@link ItemRest} object to this report. + * + * @param item {@link ItemRest} to add to this report + */ + public void addItem(Item item) { + items.add(item); + } + + /** + * Sets all items for this report. + * The contents are copied into this object's internal list, which is protected + * against further tampering with the provided list. + * + * @param items Values that replace the current ones + */ + public void setItems(List items) { + this.items.clear(); + this.items.addAll(items); + } + + public long getItemCount() { + return itemCount; + } + + public void setItemCount(long itemCount) { + this.itemCount = itemCount; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/FilteredItemsQuery.java b/dspace-api/src/main/java/org/dspace/contentreport/FilteredItemsQuery.java new file mode 100644 index 000000000000..3dc9faed1cfc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/FilteredItemsQuery.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.contentreport; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * Structured query contents for the Filtered Items report + * @author Jean-François Morin (Université Laval) + */ +public class FilteredItemsQuery { + + private List collections = new ArrayList<>(); + private List queryPredicates = new ArrayList<>(); + private long offset; + private int pageLimit; + private Set filters = EnumSet.noneOf(Filter.class); + private List additionalFields = new ArrayList<>(); + + /** + * Shortcut method that builds a FilteredItemsQuery instance + * from its building blocks. + * @param collectionUuids collection UUIDs to add + * @param predicates query predicates used to filter existing items + * @param pageLimit number of items per page + * @param filters filters to apply to existing items + * The filters mapping to true will be applied, others (either missing or + * mapping to false) will not. + * @param additionalFields additional fields to display in the resulting report + * @return a FilteredItemsQuery instance built from the provided parameters + */ + public static FilteredItemsQuery of(Collection collectionUuids, + Collection predicates, long offset, int pageLimit, + Collection filters, Collection additionalFields) { + var query = new FilteredItemsQuery(); + Optional.ofNullable(collectionUuids).ifPresent(query.collections::addAll); + Optional.ofNullable(predicates).ifPresent(query.queryPredicates::addAll); + query.offset = offset; + query.pageLimit = pageLimit; + Optional.ofNullable(filters).ifPresent(query.filters::addAll); + Optional.ofNullable(additionalFields).ifPresent(query.additionalFields::addAll); + return query; + } + + public List getCollections() { + return collections; + } + + public void setCollections(List collections) { + this.collections.clear(); + if (collections != null) { + this.collections.addAll(collections); + } + } + + public List getQueryPredicates() { + return queryPredicates; + } + + public void setQueryPredicates(List queryPredicates) { + this.queryPredicates.clear(); + if (queryPredicates != null) { + this.queryPredicates.addAll(queryPredicates); + } + } + + public long getOffset() { + return offset; + } + + public void setOffset(long offset) { + this.offset = offset; + } + + public int getPageLimit() { + return pageLimit; + } + + public void setPageLimit(int pageLimit) { + this.pageLimit = pageLimit; + } + + public Set getFilters() { + return filters; + } + + public void setFilters(Set filters) { + this.filters.clear(); + if (filters != null) { + this.filters.addAll(filters); + } + } + + public List getAdditionalFields() { + return additionalFields; + } + + public void setAdditionalFields(List additionalFields) { + this.additionalFields.clear(); + if (additionalFields != null) { + this.additionalFields.addAll(additionalFields); + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/ItemFilterUtil.java b/dspace-api/src/main/java/org/dspace/contentreport/ItemFilterUtil.java new file mode 100644 index 000000000000..20c714fcf3b1 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/ItemFilterUtil.java @@ -0,0 +1,353 @@ +/** + * 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.contentreport; + +import static org.dspace.content.Item.ANY; + +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Utility methods for applying some of the filters defined in the {@link Filter} enum. + * + * @author Jean-François Morin (Université Laval) (port to DSpace 7.x) + * @author Terry Brady, Georgetown University (original code in DSpace 6.x) + */ +public class ItemFilterUtil { + + protected static ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + private static final Logger log = LogManager.getLogger(ItemFilterUtil.class); + public static final String[] MIMES_PDF = {"application/pdf"}; + public static final String[] MIMES_JPG = {"image/jpeg"}; + + /** + * Supported bundle types. + * N.B.: Bundle names are used in metadata as they are named here. + * Do NOT change these names, the name() method is invoked at multiple + * locations in this class and enum Filter. + * If these names are to change, the name() invocations shall be changed + * so that they refer to these unchanged names, likely through a String property. + */ + enum BundleName { + ORIGINAL, TEXT, LICENSE, THUMBNAIL; + } + + private ItemFilterUtil() {} + + static String[] getDocumentMimeTypes() { + return DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("rest.report-mime-document"); + } + + static String[] getSupportedDocumentMimeTypes() { + return DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("rest.report-mime-document-supported"); + } + + static String[] getSupportedImageMimeTypes() { + return DSpaceServicesFactory.getInstance().getConfigurationService() + .getArrayProperty("rest.report-mime-document-image"); + } + + /** + * Counts the original bitstreams of a given item. + * @param item Provided item + * @return the number of original bitstreams in the item + */ + static int countOriginalBitstream(Item item) { + return countBitstream(BundleName.ORIGINAL, item); + } + + /** + * Counts the bitstreams of a given item for a specific type. + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @return the number of matching bitstreams in the item + */ + static int countBitstream(BundleName bundleName, Item item) { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .mapToInt(bundle -> bundle.getBitstreams().size()) + .sum(); + } + + /** + * Retrieves the bitstream names of an given item for a specific bundle type. + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @return the names of matching bitstreams in the item + */ + static List getBitstreamNames(BundleName bundleName, Item item) { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .map(Bitstream::getName) + .collect(Collectors.toList()); + } + + /** + * Counts the original bitstreams of a given item matching one of a list of specific MIME types. + * @param context DSpace context + * @param item Provided item + * @param mimeList List of MIME types to filter bitstreams + * @return number of matching original bitstreams + */ + static int countOriginalBitstreamMime(Context context, Item item, String[] mimeList) { + return countBitstreamMime(context, BundleName.ORIGINAL, item, mimeList); + } + + /** + * Counts the bitstreams of a given item for a specific type matching one of a list of specific MIME types. + * @param context DSpace context + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @param mimeList List of MIME types to filter bitstreams + * @return number of matching bitstreams + */ + static int countBitstreamMime(Context context, BundleName bundleName, Item item, String[] mimeList) { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .mapToInt(bit -> { + int count = 0; + for (String mime : mimeList) { + try { + if (bit.getFormat(context).getMIMEType().equals(mime.trim())) { + count++; + } + } catch (SQLException e) { + log.error("Get format error for bitstream " + bit.getName()); + } + } + return count; + }) + .sum(); + } + + /** + * Counts the bitstreams of a given item for a specific type matching one of a list of specific descriptions. + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @param descList List of descriptions to filter bitstreams + * @return number of matching bitstreams + */ + static int countBitstreamByDesc(BundleName bundleName, Item item, String[] descList) { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .filter(bit -> bit.getDescription() != null) + .mapToInt(bit -> { + int count = 0; + for (String desc : descList) { + String bitDesc = bit.getDescription(); + if (bitDesc.equals(desc.trim())) { + count++; + } + } + return count; + }) + .sum(); + } + + /** + * Counts the bitstreams of a given item smaller than a given size for a specific type + * matching one of a list of specific MIME types. + * @param context DSpace context + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @param mimeList List of MIME types to filter bitstreams + * @param prop Configurable property providing the size to filter bitstreams + * @return number of matching bitstreams + */ + static int countBitstreamSmallerThanMinSize( + Context context, BundleName bundleName, Item item, String[] mimeList, String prop) { + long size = DSpaceServicesFactory.getInstance().getConfigurationService().getLongProperty(prop); + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .mapToInt(bit -> { + int count = 0; + for (String mime : mimeList) { + try { + if (bit.getFormat(context).getMIMEType().equals(mime.trim())) { + if (bit.getSizeBytes() < size) { + count++; + } + } + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + } + return count; + }) + .sum(); + } + + /** + * Counts the bitstreams of a given item larger than a given size for a specific type + * matching one of a list of specific MIME types. + * @param context DSpace context + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @param mimeList List of MIME types to filter bitstreams + * @param prop Configurable property providing the size to filter bitstreams + * @return number of matching bitstreams + */ + static int countBitstreamLargerThanMaxSize( + Context context, BundleName bundleName, Item item, String[] mimeList, String prop) { + long size = DSpaceServicesFactory.getInstance().getConfigurationService().getLongProperty(prop); + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .mapToInt(bit -> { + int count = 0; + for (String mime : mimeList) { + try { + if (bit.getFormat(context).getMIMEType().equals(mime.trim())) { + if (bit.getSizeBytes() > size) { + count++; + } + } + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + } + return count; + }) + .sum(); + } + + /** + * Counts the original bitstreams of a given item whose MIME type starts with a specific prefix. + * @param context DSpace context + * @param item Provided item + * @param prefix Prefix to filter bitstreams + * @return number of matching original bitstreams + */ + static int countOriginalBitstreamMimeStartsWith(Context context, Item item, String prefix) { + return countBitstreamMimeStartsWith(context, BundleName.ORIGINAL, item, prefix); + } + + /** + * Counts the bitstreams of a given item for a specific type whose MIME type starts with a specific prefix. + * @param context DSpace context + * @param bundleName Type of bundle to filter bitstreams + * @param item Provided item + * @param prefix Prefix to filter bitstreams + * @return number of matching bitstreams + */ + static int countBitstreamMimeStartsWith(Context context, BundleName bundleName, Item item, String prefix) { + return item.getBundles().stream() + .filter(bundle -> bundle.getName().equals(bundleName.name())) + .map(Bundle::getBitstreams) + .flatMap(List::stream) + .mapToInt(bit -> { + int count = 0; + try { + if (bit.getFormat(context).getMIMEType().startsWith(prefix)) { + count++; + } + } catch (SQLException e) { + log.error(e.getMessage(), e); + } + return count; + }) + .sum(); + } + + /** + * Returns true if a given item has a bundle not matching a specific list of bundles. + * @param item Provided item + * @param bundleList List of bundle names to filter bundles + * @return true if the item has a (non-)matching bundle + */ + static boolean hasUnsupportedBundle(Item item, String[] bundleList) { + if (bundleList == null) { + return false; + } + Set bundles = Arrays.stream(bundleList) + .collect(Collectors.toSet()); + return item.getBundles().stream() + .anyMatch(bundle -> !bundles.contains(bundle.getName())); + } + + static boolean hasOriginalBitstreamMime(Context context, Item item, String[] mimeList) { + return hasBitstreamMime(context, BundleName.ORIGINAL, item, mimeList); + } + + static boolean hasBitstreamMime(Context context, BundleName bundleName, Item item, String[] mimeList) { + return countBitstreamMime(context, bundleName, item, mimeList) > 0; + } + + /** + * Returns true if a given item has at least one field of a specific list whose value + * matches a provided regular expression. + * @param item Provided item + * @param fieldList List of fields to check + * @param regex Regular expression to check field values against + * @return true if there is at least one matching field, false otherwise + */ + static boolean hasMetadataMatch(Item item, String fieldList, Pattern regex) { + if ("*".equals(fieldList)) { + return itemService.getMetadata(item, ANY, ANY, ANY, ANY).stream() + .anyMatch(md -> regex.matcher(md.getValue()).matches()); + } + + return Arrays.stream(fieldList.split(",")) + .map(field -> itemService.getMetadataByMetadataString(item, field.trim())) + .flatMap(List::stream) + .anyMatch(md -> regex.matcher(md.getValue()).matches()); + } + + /** + * Returns true if a given item has at all fields of a specific list whose values + * match a provided regular expression. + * @param item Provided item + * @param fieldList List of fields to check + * @param regex Regular expression to check field values against + * @return true if all specified fields match, false otherwise + */ + static boolean hasOnlyMetadataMatch(Item item, String fieldList, Pattern regex) { + if ("*".equals(fieldList)) { + return itemService.getMetadata(item, ANY, ANY, ANY, ANY).stream() + .allMatch(md -> regex.matcher(md.getValue()).matches()); + } + + return Arrays.stream(fieldList.split(",")) + .map(field -> itemService.getMetadataByMetadataString(item, field.trim())) + .flatMap(List::stream) + .allMatch(md -> regex.matcher(md.getValue()).matches()); + } + + static boolean recentlyModified(Item item, int days) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -days); + return cal.getTime().before(item.getLastModified()); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/QueryOperator.java b/dspace-api/src/main/java/org/dspace/contentreport/QueryOperator.java new file mode 100644 index 000000000000..1fb6e5fc0ffc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/QueryOperator.java @@ -0,0 +1,102 @@ +/** + * 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.contentreport; + +import java.util.Arrays; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.Predicate; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.apache.commons.lang3.function.TriFunction; +import org.dspace.content.MetadataValue; +import org.dspace.content.MetadataValue_; +import org.dspace.util.DSpacePostgreSQLDialect; +import org.dspace.util.JpaCriteriaBuilderKit; + +/** + * Operators available for creating predicates to query the + * Filtered Items report + * @author Jean-François Morin (Université Laval) + */ +public enum QueryOperator { + + EXISTS("exists", true, false, + (val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().isNotNull(jpaKit.root().get(MetadataValue_.VALUE))), + DOES_NOT_EXIST("doesnt_exist", true, true, + (val, regexClause, jpaKit) -> EXISTS.buildJpaPredicate(val, regexClause, jpaKit)), + EQUALS("equals", true, false, + (val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().equal(jpaKit.root().get(MetadataValue_.VALUE), val)), + DOES_NOT_EQUAL("not_equals", true, true, + (val, regexClause, jpaKit) -> EQUALS.buildJpaPredicate(val, regexClause, jpaKit)), + LIKE("like", true, false, + (val, regexClause, jpaKit) -> jpaKit.criteriaBuilder().like(jpaKit.root().get(MetadataValue_.VALUE), val)), + NOT_LIKE("not_like", true, true, + (val, regexClause, jpaKit) -> LIKE.buildJpaPredicate(val, regexClause, jpaKit)), + CONTAINS("contains", true, false, + (val, regexClause, jpaKit) -> LIKE.buildJpaPredicate("%" + val + "%", regexClause, jpaKit)), + DOES_NOT_CONTAIN("doesnt_contain", true, true, + (val, regexClause, jpaKit) -> CONTAINS.buildJpaPredicate(val, regexClause, jpaKit)), + MATCHES("matches", false, false, + (val, regexClause, jpaKit) -> regexPredicate(val, DSpacePostgreSQLDialect.REGEX_MATCHES, jpaKit)), + DOES_NOT_MATCH("doesnt_match", false, false, + (val, regexClause, jpaKit) -> regexPredicate(val, DSpacePostgreSQLDialect.REGEX_NOT_MATCHES, jpaKit)); + + private final String code; + private final TriFunction, Predicate> predicateBuilder; + private final boolean usesRegex; + private final boolean negate; + + QueryOperator(String code, boolean usesRegex, boolean negate, + TriFunction, Predicate> predicateBuilder) { + this.code = code; + this.usesRegex = usesRegex; + this.negate = negate; + this.predicateBuilder = predicateBuilder; + } + + @JsonProperty + public String getCode() { + return code; + } + + public boolean getUsesRegex() { + return usesRegex; + } + + public boolean getNegate() { + return negate; + } + + public Predicate buildJpaPredicate(String val, String regexClause, JpaCriteriaBuilderKit jpaKit) { + return predicateBuilder.apply(val, regexClause, jpaKit); + } + + @JsonCreator + public static QueryOperator get(String code) { + return Arrays.stream(values()) + .filter(item -> item.code.equalsIgnoreCase(code)) + .findFirst() + .orElse(null); + } + + private static Predicate regexPredicate(String val, String regexFunction, + JpaCriteriaBuilderKit jpaKit) { + // Source: https://stackoverflow.com/questions/24995881/use-regular-expressions-in-jpa-criteriabuilder + CriteriaBuilder builder = jpaKit.criteriaBuilder(); + Expression patternExpression = builder.literal(val); + Path path = jpaKit.root().get(MetadataValue_.VALUE); + // "matches" comes from the name of the regex function + // defined in class DSpacePostgreSQLDialect + return builder.equal(builder + .function(regexFunction, Boolean.class, path, patternExpression), Boolean.TRUE); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/QueryPredicate.java b/dspace-api/src/main/java/org/dspace/contentreport/QueryPredicate.java new file mode 100644 index 000000000000..91c9b78255ad --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/QueryPredicate.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.contentreport; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.dspace.content.MetadataField; + +/** + * Data structure representing a query predicate used by the Filtered Items report + * to filter items to retrieve. + * @author Jean-François Morin (Université Laval) + */ +public class QueryPredicate { + + private List fields = new ArrayList<>(); + private QueryOperator operator; + private String value; + + /** + * Shortcut method that builds a QueryPredicate from a single field, an operator, and a value. + * @param field Predicate subject + * @param operator Predicate operator + * @param value Predicate object + * @return a QueryPredicate instance built from the provided parameters + */ + public static QueryPredicate of(MetadataField field, QueryOperator operator, String value) { + var predicate = new QueryPredicate(); + predicate.fields.add(field); + predicate.operator = operator; + predicate.value = value; + return predicate; + } + + /** + * Shortcut method that builds a QueryPredicate from a list of fields, an operator, and a value. + * @param fields Fields that form the predicate subject + * @param operator Predicate operator + * @param value Predicate object + * @return a QueryPredicate instance built from the provided parameters + */ + public static QueryPredicate of(Collection fields, QueryOperator operator, String value) { + var predicate = new QueryPredicate(); + predicate.fields.addAll(fields); + predicate.operator = operator; + predicate.value = value; + return predicate; + } + + public List getFields() { + return fields; + } + + public QueryOperator getOperator() { + return operator; + } + + public String getValue() { + return value; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/contentreport/service/ContentReportService.java b/dspace-api/src/main/java/org/dspace/contentreport/service/ContentReportService.java new file mode 100644 index 000000000000..16c01b8b4808 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/contentreport/service/ContentReportService.java @@ -0,0 +1,55 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.contentreport.service; + +import java.sql.SQLException; +import java.util.Collection; +import java.util.List; + +import org.dspace.content.MetadataField; +import org.dspace.contentreport.Filter; +import org.dspace.contentreport.FilteredCollection; +import org.dspace.contentreport.FilteredItems; +import org.dspace.contentreport.FilteredItemsQuery; +import org.dspace.core.Context; + +public interface ContentReportService { + + /** + * Returns true< if Content Reports are enabled. + * @return true< if Content Reports are enabled + */ + boolean getEnabled(); + + /** + * Retrieves item statistics per collection according to a set of Boolean filters. + * @param context DSpace context + * @param filters Set of filters + * @return a list of collections with the requested statistics for each of them + */ + List findFilteredCollections(Context context, Collection filters); + + /** + * Retrieves a list of items according to a set of criteria. + * @param context DSpace context + * @param query structured query to find items against + * @return a list of items filtered according to the provided query + */ + FilteredItems findFilteredItems(Context context, FilteredItemsQuery query); + + /** + * Converts a metadata field name to a list of {@link MetadataField} instances + * (one if no wildcards are used, possibly more otherwise). + * @param context DSpace context + * @param metadataField field to search for + * @return a corresponding list of {@link MetadataField} entries + */ + List getMetadataFields(org.dspace.core.Context context, String metadataField) + throws SQLException; + +} diff --git a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java index 32ad747d765e..38923658f0dd 100644 --- a/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java +++ b/dspace-api/src/main/java/org/dspace/core/AbstractHibernateDAO.java @@ -102,6 +102,16 @@ public T findByID(Context context, Class clazz, int id) throws SQLException { return result; } + @Override + public T findByID(Context context, Class clazz, String id) throws SQLException { + if (id == null) { + return null; + } + @SuppressWarnings("unchecked") + T result = (T) getHibernateSession(context).get(clazz, id); + return result; + } + @Override public List findMany(Context context, String query) throws SQLException { @SuppressWarnings("unchecked") diff --git a/dspace-api/src/main/java/org/dspace/core/GenericDAO.java b/dspace-api/src/main/java/org/dspace/core/GenericDAO.java index a04a0ccbdcc8..9835e18ad3cf 100644 --- a/dspace-api/src/main/java/org/dspace/core/GenericDAO.java +++ b/dspace-api/src/main/java/org/dspace/core/GenericDAO.java @@ -102,6 +102,17 @@ public interface GenericDAO { */ public T findByID(Context context, Class clazz, UUID id) throws SQLException; + /** + * Fetch the entity identified by its String primary key. + * + * @param context current DSpace context. + * @param clazz class of entity to be found. + * @param id primary key of the database record. + * @return the found entity. + * @throws SQLException + */ + public T findByID(Context context, Class clazz, String id) throws SQLException; + /** * Execute a JPQL query and return a collection of results. * diff --git a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java index 0fc48b908b82..b8798371c19f 100644 --- a/dspace-api/src/main/java/org/dspace/core/I18nUtil.java +++ b/dspace-api/src/main/java/org/dspace/core/I18nUtil.java @@ -377,6 +377,22 @@ public static String getEmailFilename(Locale locale, String name) { return templateName; } + /** + * Get the appropriate localized version of a ldn template according to language settings + * + * @param locale Locale for this request + * @param name String - base name of the ldn template + * @return templateName + * String - localized filename of a ldn template + */ + public static String getLDNFilename(Locale locale, String name) { + String templateFile = + DSpaceServicesFactory.getInstance().getConfigurationService().getProperty("dspace.dir") + + File.separator + "config" + File.separator + "ldn" + File.separator + name; + + return getFilename(locale, templateFile, ""); + } + /** * Creates array of Locales from text list of locale-specifications. * Used to parse lists in DSpace configuration properties. diff --git a/dspace-api/src/main/java/org/dspace/core/LDN.java b/dspace-api/src/main/java/org/dspace/core/LDN.java new file mode 100644 index 000000000000..283850eb1059 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/LDN.java @@ -0,0 +1,212 @@ +/** + * 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.core; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.io.BufferedReader; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import javax.mail.MessagingException; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.Velocity; +import org.apache.velocity.app.VelocityEngine; +import org.apache.velocity.exception.MethodInvocationException; +import org.apache.velocity.exception.ParseErrorException; +import org.apache.velocity.exception.ResourceNotFoundException; +import org.apache.velocity.runtime.resource.loader.StringResourceLoader; +import org.apache.velocity.runtime.resource.util.StringResourceRepository; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; + +/** + * Class representing an LDN message json + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class LDN { + /** + * The content of the ldn message + */ + private String content; + private String contentName; + + /** + * The arguments to fill out + */ + private final List arguments; + + private static final Logger LOG = LogManager.getLogger(); + + /** Velocity template settings. */ + private static final String RESOURCE_REPOSITORY_NAME = "LDN"; + private static final Properties VELOCITY_PROPERTIES = new Properties(); + static { + VELOCITY_PROPERTIES.put(Velocity.RESOURCE_LOADERS, "string"); + VELOCITY_PROPERTIES.put("resource.loader.string.description", + "Velocity StringResource loader"); + VELOCITY_PROPERTIES.put("resource.loader.string.class", + StringResourceLoader.class.getName()); + VELOCITY_PROPERTIES.put("resource.loader.string.repository.name", + RESOURCE_REPOSITORY_NAME); + VELOCITY_PROPERTIES.put("resource.loader.string.repository.static", + "false"); + } + + /** Velocity template for the message*/ + private Template template; + + /** + * Create a new ldn message. + */ + public LDN() { + arguments = new ArrayList<>(20); + template = null; + content = EMPTY; + } + + /** + * Set the content of the message. Setting this also "resets" the message + * formatting - addArgument will start over. Comments and any + * "Subject:" line must be stripped. + * + * @param name a name for this message + * @param cnt the content of the message + */ + public void setContent(String name, String cnt) { + content = cnt; + contentName = name; + arguments.clear(); + } + + /** + * Fill out the next argument in the template + * + * @param arg the value for the next argument + */ + public void addArgument(Object arg) { + arguments.add(arg); + } + + /** + * Generates the ldn message. + * + * @throws MessagingException if there was a problem sending the mail. + * @throws IOException if IO error + */ + public String generateLDNMessage() { + ConfigurationService config + = DSpaceServicesFactory.getInstance().getConfigurationService(); + + VelocityEngine templateEngine = new VelocityEngine(); + templateEngine.init(VELOCITY_PROPERTIES); + + VelocityContext vctx = new VelocityContext(); + vctx.put("config", new LDN.UnmodifiableConfigurationService(config)); + vctx.put("params", Collections.unmodifiableList(arguments)); + + if (null == template) { + if (StringUtils.isBlank(content)) { + LOG.error("template has no content"); + throw new RuntimeException("template has no content"); + } + // No template, so use a String of content. + StringResourceRepository repo = (StringResourceRepository) + templateEngine.getApplicationAttribute(RESOURCE_REPOSITORY_NAME); + repo.putStringResource(contentName, content); + // Turn content into a template. + template = templateEngine.getTemplate(contentName); + } + + StringWriter writer = new StringWriter(); + try { + template.merge(vctx, writer); + } catch (MethodInvocationException | ParseErrorException + | ResourceNotFoundException ex) { + LOG.error("Template not merged: {}", ex.getMessage()); + throw new RuntimeException("Template not merged", ex); + } + return writer.toString(); + } + + /** + * Get the VTL template for a ldn message. The message is suitable + * for inserting values using Apache Velocity. + * + * @param ldnMessageFile + * full name for the ldn template, for example "/dspace/config/ldn/request-review". + * + * @return the ldn object, configured with body. + * + * @throws IOException if IO error, + * if the template couldn't be found, or there was some other + * error reading the template + */ + public static LDN getLDNMessage(String ldnMessageFile) + throws IOException { + StringBuilder contentBuffer = new StringBuilder(); + try ( + InputStream is = new FileInputStream(ldnMessageFile); + InputStreamReader ir = new InputStreamReader(is, "UTF-8"); + BufferedReader reader = new BufferedReader(ir); + ) { + boolean more = true; + while (more) { + String line = reader.readLine(); + if (line == null) { + more = false; + } else { + contentBuffer.append(line); + contentBuffer.append("\n"); + } + } + } + LDN ldn = new LDN(); + ldn.setContent(ldnMessageFile, contentBuffer.toString()); + return ldn; + } + + /** + * Wrap ConfigurationService to prevent templates from modifying + * the configuration. + */ + public static class UnmodifiableConfigurationService { + private final ConfigurationService configurationService; + + /** + * Swallow an instance of ConfigurationService. + * + * @param cs the real instance, to be wrapped. + */ + public UnmodifiableConfigurationService(ConfigurationService cs) { + configurationService = cs; + } + + /** + * Look up a key in the actual ConfigurationService. + * + * @param key to be looked up in the DSpace configuration. + * @return whatever value ConfigurationService associates with {@code key}. + */ + public String get(String key) { + return configurationService.getProperty(key); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java index d895f9a76481..80bc0beac83a 100644 --- a/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/LicenseServiceImpl.java @@ -19,12 +19,12 @@ import java.io.PrintWriter; import javax.servlet.http.HttpServletRequest; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.service.LicenseService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.services.model.Request; import org.dspace.web.ContextUtil; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Encapsulate the deposit license. @@ -32,7 +32,7 @@ * @author mhwood */ public class LicenseServiceImpl implements LicenseService { - private final Logger log = LoggerFactory.getLogger(LicenseServiceImpl.class); + private final Logger log = LogManager.getLogger(); /** * The default license @@ -53,7 +53,7 @@ public void writeLicenseFile(String licenseFile, out.print(newLicense); out.close(); } catch (IOException e) { - log.warn("license_write: " + e.getLocalizedMessage()); + log.warn("license_write: {}", e::getLocalizedMessage); } license = newLicense; } @@ -140,7 +140,7 @@ protected void init() { br.close(); } catch (IOException e) { - log.error("Can't load license: " + licenseFile.toString(), e); + log.error("Can't load license {}: ", licenseFile.toString(), e); // FIXME: Maybe something more graceful here, but with the // configuration we can't do anything diff --git a/dspace-api/src/main/java/org/dspace/core/NewsServiceImpl.java b/dspace-api/src/main/java/org/dspace/core/NewsServiceImpl.java index d2bdf787f818..59b4c43fa866 100644 --- a/dspace-api/src/main/java/org/dspace/core/NewsServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/core/NewsServiceImpl.java @@ -19,11 +19,11 @@ import java.util.ArrayList; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.service.NewsService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -32,7 +32,7 @@ * @author mhwood */ public class NewsServiceImpl implements NewsService { - private final Logger log = LoggerFactory.getLogger(NewsServiceImpl.class); + private final Logger log = LogManager.getLogger(); private List acceptableFilenames; @@ -94,7 +94,7 @@ public String readNewsFile(String newsFile) { ir.close(); fir.close(); } catch (IOException e) { - log.warn("news_read: " + e.getLocalizedMessage()); + log.warn("news_read: {}", e::getLocalizedMessage); } return text.toString(); @@ -117,7 +117,7 @@ public String writeNewsFile(String newsFile, String news) { out.print(news); out.close(); } catch (IOException e) { - log.warn("news_write: " + e.getLocalizedMessage()); + log.warn("news_write: {}", e::getLocalizedMessage); } return news; diff --git a/dspace-api/src/main/java/org/dspace/core/UUIDIterator.java b/dspace-api/src/main/java/org/dspace/core/UUIDIterator.java new file mode 100644 index 000000000000..28c8d7354169 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/core/UUIDIterator.java @@ -0,0 +1,63 @@ +/** + * 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.core; + +import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; +import java.util.UUID; + +import com.google.common.collect.AbstractIterator; +import org.dspace.content.DSpaceObject; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Iterator implementation which allows to iterate over items and commit while + * iterating. Using an iterator over previous retrieved UUIDs the iterator doesn't + * get invalidated after a commit that would instead close the database ResultSet + * + * @author Andrea Bollini (andrea.bollini at 4science.com) + * @param class type + */ +public class UUIDIterator extends AbstractIterator { + private Class clazz; + + private Iterator iterator; + + @Autowired + private AbstractHibernateDSODAO dao; + + private Context ctx; + + public UUIDIterator(Context ctx, List uuids, Class clazz, AbstractHibernateDSODAO dao) + throws SQLException { + this.ctx = ctx; + this.clazz = clazz; + this.dao = dao; + this.iterator = uuids.iterator(); + } + + @Override + protected T computeNext() { + try { + if (iterator.hasNext()) { + T item = dao.findByID(ctx, clazz, iterator.next()); + if (item != null) { + return item; + } else { + return computeNext(); + } + } else { + return endOfData(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java b/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java index d1fa70565dfd..47fa6ee6452d 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java +++ b/dspace-api/src/main/java/org/dspace/ctask/general/ClamScan.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.Bundle; @@ -29,8 +31,6 @@ import org.dspace.curate.AbstractCurationTask; import org.dspace.curate.Curator; import org.dspace.curate.Suspendable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * ClamScan.java @@ -58,7 +58,7 @@ public class ClamScan extends AbstractCurationTask { protected final String SCAN_FAIL_MESSAGE = "Error encountered using virus service - check setup"; protected final String NEW_ITEM_HANDLE = "in workflow"; - private static final Logger log = LoggerFactory.getLogger(ClamScan.class); + private static final Logger log = LogManager.getLogger(); protected String host = null; protected int port = 0; @@ -100,7 +100,7 @@ public int perform(DSpaceObject dso) throws IOException { try { Bundle bundle = itemService.getBundles(item, "ORIGINAL").get(0); - results = new ArrayList(); + results = new ArrayList<>(); for (Bitstream bitstream : bundle.getBitstreams()) { InputStream inputstream = bitstreamService.retrieve(Curator.curationContext(), bitstream); logDebugMessage("Scanning " + bitstream.getName() + " . . . "); @@ -157,7 +157,7 @@ protected void openSession() throws IOException { try { socket.setSoTimeout(timeout); } catch (SocketException e) { - log.error("Could not set socket timeout . . . " + timeout + "ms", e); + log.error("Could not set socket timeout . . . {}ms", timeout, e); throw (new IOException(e)); } try { @@ -293,8 +293,6 @@ protected String getItemHandle(Item item) { protected void logDebugMessage(String message) { - if (log.isDebugEnabled()) { - log.debug(message); - } + log.debug(message); } } diff --git a/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.java b/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.java index 21ffdd026055..7b47a9fd90dc 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.java +++ b/dspace-api/src/main/java/org/dspace/ctask/test/WorkflowReportTest.java @@ -10,11 +10,11 @@ import java.io.IOException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.curate.AbstractCurationTask; import org.dspace.curate.Curator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Curation task which simply reports its invocation without changing anything. @@ -24,13 +24,13 @@ */ public class WorkflowReportTest extends AbstractCurationTask { - private static final Logger LOG = LoggerFactory.getLogger(WorkflowReportTest.class); + private static final Logger LOG = LogManager.getLogger(); @Override public int perform(DSpaceObject dso) throws IOException { LOG.info("Class {} as task {} received 'perform' for object {}", - WorkflowReportTest.class.getSimpleName(), taskId, dso); + WorkflowReportTest.class::getSimpleName, () -> taskId, () -> dso); curator.report(String.format( "Class %s as task %s received 'perform' for object %s%n", WorkflowReportTest.class.getSimpleName(), taskId, dso)); diff --git a/dspace-api/src/main/java/org/dspace/ctask/testing/PropertyParameterTestingTask.java b/dspace-api/src/main/java/org/dspace/ctask/testing/PropertyParameterTestingTask.java index 279204cf575b..14bd42049479 100644 --- a/dspace-api/src/main/java/org/dspace/ctask/testing/PropertyParameterTestingTask.java +++ b/dspace-api/src/main/java/org/dspace/ctask/testing/PropertyParameterTestingTask.java @@ -10,12 +10,12 @@ import java.io.IOException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.core.Context; import org.dspace.curate.AbstractCurationTask; import org.dspace.curate.Curator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Logs what it was asked to do, samples run parameters and task @@ -32,8 +32,7 @@ */ public class PropertyParameterTestingTask extends AbstractCurationTask { - private static final Logger LOG - = LoggerFactory.getLogger(PropertyParameterTestingTask.class); + private static final Logger LOG = LogManager.getLogger(); @Override public void init(Curator curator, String taskId) diff --git a/dspace-api/src/main/java/org/dspace/curate/LogReporter.java b/dspace-api/src/main/java/org/dspace/curate/LogReporter.java index bd3ee3cffb9c..bdf444357668 100644 --- a/dspace-api/src/main/java/org/dspace/curate/LogReporter.java +++ b/dspace-api/src/main/java/org/dspace/curate/LogReporter.java @@ -10,8 +10,8 @@ import java.io.IOException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * Write curation report records through the logging framework. @@ -22,7 +22,7 @@ */ public class LogReporter implements Reporter { - private static final Logger LOG = LoggerFactory.getLogger("curation"); + private static final Logger LOG = LogManager.getLogger("curation"); private final StringBuilder buffer = new StringBuilder(); @Override @@ -31,7 +31,7 @@ public Appendable append(CharSequence cs) for (int pos = 0; pos < cs.length(); pos++) { char c = cs.charAt(pos); if (c == '\n') { - LOG.info(buffer.toString()); + LOG.info(buffer::toString); buffer.delete(0, buffer.length()); // Clear the buffer } else { buffer.append(c); @@ -56,7 +56,7 @@ public Appendable append(char c) public void close() throws Exception { if (buffer.length() > 0) { - LOG.info(buffer.toString()); + LOG.info(buffer::toString); } } } diff --git a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java index 17340c5a58a9..ba34df4dae66 100644 --- a/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java +++ b/dspace-api/src/main/java/org/dspace/eperson/PasswordHash.java @@ -16,10 +16,10 @@ import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * For handling digested secrets (such as passwords). @@ -31,7 +31,7 @@ * @author mwood */ public class PasswordHash { - private static final Logger log = LoggerFactory.getLogger(PasswordHash.class); + private static final Logger log = LogManager.getLogger(); private static final ConfigurationService config = DSpaceServicesFactory.getInstance().getConfigurationService(); private static final Charset UTF_8 = Charset.forName("UTF-8"); // Should always succeed: UTF-8 is required @@ -133,7 +133,7 @@ public PasswordHash(String password) { try { hash = digest(salt, algorithm, password); } catch (NoSuchAlgorithmException e) { - log.error(e.getMessage()); + log.error(e::getMessage); hash = new byte[] {0}; } } @@ -149,7 +149,7 @@ public boolean matches(String secret) { try { candidate = digest(salt, algorithm, secret); } catch (NoSuchAlgorithmException e) { - log.error(e.getMessage()); + log.error(e::getMessage); return false; } return Arrays.equals(candidate, hash); @@ -225,7 +225,7 @@ private synchronized byte[] generateSalt() { if (null == rng) { rng = new SecureRandom(); log.info("Initialized a random number stream using {} provided by {}", - rng.getAlgorithm(), rng.getProvider()); + rng::getAlgorithm, rng::getProvider); rngUses = 0; } diff --git a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java index 4fdf15a8a3ad..9a4f9c9b423c 100644 --- a/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java +++ b/dspace-api/src/main/java/org/dspace/external/provider/impl/OrcidPublicationDataProvider.java @@ -28,6 +28,8 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.MetadataFieldName; import org.dspace.content.dto.MetadataValueDTO; @@ -63,13 +65,11 @@ import org.orcid.jaxb.model.v3.release.record.summary.WorkGroup; import org.orcid.jaxb.model.v3.release.record.summary.WorkSummary; import org.orcid.jaxb.model.v3.release.record.summary.Works; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** * Implementation of {@link ExternalDataProvider} that search for all the works - * of the profile with the given orcid id that hava a source other than DSpace. + * of the profile with the given orcid id that have a source other than DSpace. * The id of the external data objects returned by the methods of this class is * the concatenation of the orcid id and the put code associated with the * publication, separated by :: (example 0000-0000-0123-4567::123456) @@ -79,7 +79,7 @@ */ public class OrcidPublicationDataProvider extends AbstractExternalDataProvider { - private final static Logger LOGGER = LoggerFactory.getLogger(OrcidPublicationDataProvider.class); + private final static Logger LOGGER = LogManager.getLogger(); /** * Examples of valid ORCID IDs: @@ -335,7 +335,8 @@ private ExternalDataObject convertToExternalDataObject(String orcid, Work work) try { addMetadataValuesFromCitation(externalDataObject, work.getWorkCitation()); } catch (Exception e) { - LOGGER.error("An error occurs reading the following citation: " + work.getWorkCitation().getCitation(), e); + LOGGER.error("An error occurs reading the following citation: {}", + work.getWorkCitation().getCitation(), e); } return externalDataObject; diff --git a/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java b/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java index b5ee1806cd56..915bd25b065f 100644 --- a/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java +++ b/dspace-api/src/main/java/org/dspace/google/client/GoogleAnalyticsClientImpl.java @@ -19,9 +19,9 @@ 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.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.google.GoogleAnalyticsEvent; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Implementation of {@link GoogleAnalyticsClient}. @@ -31,7 +31,7 @@ */ public class GoogleAnalyticsClientImpl implements GoogleAnalyticsClient { - private static final Logger LOGGER = LoggerFactory.getLogger(GoogleAnalyticsClientImpl.class); + private static final Logger LOGGER = LogManager.getLogger(); private final String keyPrefix; diff --git a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java index b70eda960d35..ae31e54f7e96 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DOIIdentifierProvider.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Objects; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -30,8 +32,6 @@ import org.dspace.identifier.doi.DOIIdentifierNotApplicableException; import org.dspace.identifier.service.DOIService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -49,7 +49,7 @@ * @author Kim Shepherd */ public class DOIIdentifierProvider extends FilteredIdentifierProvider { - private static final Logger log = LoggerFactory.getLogger(DOIIdentifierProvider.class); + private static final Logger log = LogManager.getLogger(); /** * A DOIConnector connects the DOIIdentifierProvider to the API of the DOI @@ -286,7 +286,7 @@ public void register(Context context, DSpaceObject dso, String identifier, Filte try { doiRow = loadOrCreateDOI(context, dso, doi, filter); } catch (SQLException ex) { - log.error("Error in databse connection: " + ex.getMessage()); + log.error("Error in databse connection: {}", ex::getMessage); throw new RuntimeException("Error in database conncetion.", ex); } @@ -492,7 +492,7 @@ public void updateMetadata(Context context, DSpaceObject dso, String identifier) if (doiService.findDOIByDSpaceObject(context, dso) != null) { // We can skip the filter here since we know the DOI already exists for the item - log.debug("updateMetadata: found DOIByDSpaceObject: " + + log.debug("updateMetadata: found DOIByDSpaceObject: {}", doiService.findDOIByDSpaceObject(context, dso).getDoi()); updateFilter = DSpaceServicesFactory.getInstance().getServiceManager().getServiceByName( "always_true_filter", TrueFilter.class); @@ -501,7 +501,7 @@ public void updateMetadata(Context context, DSpaceObject dso, String identifier) DOI doiRow = loadOrCreateDOI(context, dso, doi, updateFilter); if (PENDING.equals(doiRow.getStatus()) || MINTED.equals(doiRow.getStatus())) { - log.info("Not updating metadata for PENDING or MINTED doi: " + doi); + log.info("Not updating metadata for PENDING or MINTED doi: {}", doi); return; } @@ -611,8 +611,8 @@ public String mint(Context context, DSpaceObject dso, Filter filter) throws Iden try { doi = getDOIByObject(context, dso); } catch (SQLException e) { - log.error("Error while attemping to retrieve information about a DOI for " - + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + " with ID " + dso.getID() + "."); + log.error("Error while attemping to retrieve information about a DOI for {} with ID {}.", + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso), dso.getID()); throw new RuntimeException("Error while attempting to retrieve " + "information about a DOI for " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + " with ID " + dso.getID() + ".", e); @@ -624,7 +624,7 @@ public String mint(Context context, DSpaceObject dso, Filter filter) throws Iden } catch (SQLException e) { log.error("Error while creating new DOI for Object of " + - "ResourceType {} with id {}.", dso.getType(), dso.getID()); + "ResourceType {} with id {}.", dso::getType, dso::getID); throw new RuntimeException("Error while attempting to create a " + "new DOI for " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + " with ID " + dso.getID() + ".", e); @@ -709,9 +709,9 @@ public void delete(Context context, DSpaceObject dso) doi = getDOIByObject(context, dso); } } catch (SQLException ex) { - log.error("Error while attemping to retrieve information about a DOI for " + - contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + - " with ID " + dso.getID() + ".", ex); + log.error("Error while attemping to retrieve information about a DOI for {} with ID {}.", + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso), + dso.getID(), ex); throw new RuntimeException("Error while attempting to retrieve " + "information about a DOI for " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + " with ID " + dso.getID() + ".", ex); @@ -726,17 +726,17 @@ public void delete(Context context, DSpaceObject dso) doi = getDOIOutOfObject(dso); } } catch (AuthorizeException ex) { - log.error("Error while removing a DOI out of the metadata of an " + - contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + - " with ID " + dso.getID() + ".", ex); + log.error("Error while removing a DOI out of the metadata of an {} with ID {}.", + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso), + dso.getID(), ex); throw new RuntimeException("Error while removing a DOI out of the metadata of an " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + " with ID " + dso.getID() + ".", ex); } catch (SQLException ex) { - log.error("Error while removing a DOI out of the metadata of an " + - contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + - " with ID " + dso.getID() + ".", ex); + log.error("Error while removing a DOI out of the metadata of an {} with ID {}.", + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso), + dso.getID(), ex); throw new RuntimeException("Error while removing a DOI out of the " + "metadata of an " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + " with ID " + dso.getID() + ".", ex); @@ -779,8 +779,8 @@ public void delete(Context context, DSpaceObject dso, String identifier) throw new DOIIdentifierException("Not authorized to delete DOI.", ex, DOIIdentifierException.UNAUTHORIZED_METADATA_MANIPULATION); } catch (SQLException ex) { - log.error("SQLException occurred while deleting a DOI out of an item: " - + ex.getMessage()); + log.error("SQLException occurred while deleting a DOI out of an item: {}", + ex::getMessage); throw new RuntimeException("Error while deleting a DOI out of the " + "metadata of an Item " + dso.getID(), ex); } @@ -826,8 +826,9 @@ public void deleteOnline(Context context, String identifier) throws DOIIdentifie DOIIdentifierException.DOI_DOES_NOT_EXIST); } if (!TO_BE_DELETED.equals(doiRow.getStatus())) { - log.error("This identifier: {} couldn't be deleted. Delete it first from metadata.", - DOI.SCHEME + doiRow.getDoi()); + log.error("This identifier: " + DOI.SCHEME + + "{} couldn't be deleted. Delete it first from metadata.", + doiRow::getDoi); throw new IllegalArgumentException("Couldn't delete this identifier:" + DOI.SCHEME + doiRow.getDoi() + ". Delete it first from metadata."); @@ -863,7 +864,7 @@ public DSpaceObject getObjectByDOI(Context context, String identifier) } if (doiRow.getDSpaceObject() == null) { - log.error("Found DOI " + doi + " in database, but no assigned Object could be found."); + log.error("Found DOI {} in database, but no assigned Object could be found.", doi); throw new IllegalStateException("Found DOI " + doi + " in database, but no assigned Object could be found."); } @@ -890,8 +891,9 @@ public String getDOIByObject(Context context, DSpaceObject dso) throws SQLExcept } if (doiRow.getDoi() == null) { - log.error("A DOI with an empty doi column was found in the database. DSO-Type: " + - contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + ", ID: " + dso.getID() + "."); + log.error("A DOI with an empty doi column was found in the database. DSO-Type: {}, ID: {}.", + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso), + dso.getID()); throw new IllegalStateException("A DOI with an empty doi column was found in the database. DSO-Type: " + contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso) + ", ID: " + dso.getID() + "."); } @@ -1134,13 +1136,13 @@ public void checkMintable(Context context, Filter filter, DSpaceObject dso) if (contentServiceFactory.getDSpaceObjectService(dso).getTypeText(dso).equals("ITEM")) { try { boolean result = filter.getResult(context, (Item) dso); - log.debug("Result of filter for " + dso.getHandle() + " is " + result); + log.debug("Result of filter for {} is {}", dso.getHandle(), result); if (!result) { throw new DOIIdentifierNotApplicableException("Item " + dso.getHandle() + " was evaluated as 'false' by the item filter, not minting"); } } catch (LogicalStatementException e) { - log.error("Error evaluating item with logical filter: " + e.getLocalizedMessage()); + log.error("Error evaluating item with logical filter: {}", e::getLocalizedMessage); throw new DOIIdentifierNotApplicableException(e); } } else { diff --git a/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java b/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java index ae2cd248d417..0160c8adca80 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java +++ b/dspace-api/src/main/java/org/dspace/identifier/DataCiteXMLCreator.java @@ -13,6 +13,8 @@ import java.util.HashMap; import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.crosswalk.CrosswalkException; @@ -25,8 +27,6 @@ import org.dspace.utils.DSpace; import org.jdom2.Element; import org.jdom2.output.XMLOutputter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Provide XML based metadata crosswalk for EZID Identifier provider module. @@ -36,9 +36,9 @@ public class DataCiteXMLCreator { /** - * log4j category + * logging category */ - private static final Logger LOG = LoggerFactory.getLogger(DataCiteXMLCreator.class); + private static final Logger LOG = LogManager.getLogger(); /** * Name of crosswalk to convert metadata into DataCite Metadata Scheme. @@ -70,9 +70,8 @@ public String getXMLString(Context context, DSpaceObject dso) { this.prepareXwalk(); if (!this.xwalk.canDisseminate(dso)) { - LOG.error("Crosswalk " + this.CROSSWALK_NAME - + " cannot disseminate DSO with type " + dso.getType() - + " and ID " + dso.getID() + "."); + LOG.error("Crosswalk {} cannot disseminate DSO with type {} and ID {}.", + this.CROSSWALK_NAME, dso.getType(), dso.getID()); return null; } @@ -98,8 +97,8 @@ public String getXMLString(Context context, DSpaceObject dso) { try { root = xwalk.disseminateElement(context, dso, parameters); } catch (CrosswalkException | IOException | SQLException | AuthorizeException e) { - LOG.error("Exception while crosswalking DSO with type " - + dso.getType() + " and ID " + dso.getID() + ".", e); + LOG.error("Exception while crosswalking DSO with type {} and ID {}.", + dso.getType(), dso.getID(), e); return null; } diff --git a/dspace-api/src/main/java/org/dspace/identifier/EZIDIdentifierProvider.java b/dspace-api/src/main/java/org/dspace/identifier/EZIDIdentifierProvider.java index 78ddeb8f909b..ba1688f63580 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/EZIDIdentifierProvider.java +++ b/dspace-api/src/main/java/org/dspace/identifier/EZIDIdentifierProvider.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.Map.Entry; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; @@ -32,8 +34,6 @@ import org.dspace.identifier.ezid.EZIDRequestFactory; import org.dspace.identifier.ezid.EZIDResponse; import org.dspace.identifier.ezid.Transform; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -83,7 +83,7 @@ */ public class EZIDIdentifierProvider extends IdentifierProvider { - private static final Logger log = LoggerFactory.getLogger(EZIDIdentifierProvider.class); + private static final Logger log = LogManager.getLogger(); // Configuration property names static final String CFG_SHOULDER = "identifier.doi.ezid.shoulder"; @@ -184,7 +184,8 @@ public void register(Context context, DSpaceObject object, String identifier) { loadUser(), loadPassword()); response = request.create(identifier, crosswalkMetadata(context, object)); } catch (IdentifierException | IOException | URISyntaxException e) { - log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); + log.error("Identifier '{}' not registered: {}", + () -> identifier, e::getMessage); return; } @@ -201,7 +202,7 @@ public void register(Context context, DSpaceObject object, String identifier) { } } else { log.error("Identifier '{}' not registered -- EZID returned: {}", - identifier, response.getEZIDStatusValue()); + () -> identifier, response::getEZIDStatusValue); } } @@ -218,7 +219,8 @@ public void reserve(Context context, DSpaceObject dso, String identifier) metadata.put("_status", "reserved"); response = request.create(identifier, metadata); } catch (IOException | URISyntaxException e) { - log.error("Identifier '{}' not registered: {}", identifier, e.getMessage()); + log.error("Identifier '{}' not registered: {}", + () -> identifier, e::getMessage); return; } @@ -233,7 +235,7 @@ public void reserve(Context context, DSpaceObject dso, String identifier) } } else { log.error("Identifier '{}' not registered -- EZID returned: {}", - identifier, response.getEZIDStatusValue()); + () -> identifier, response::getEZIDStatusValue); } } @@ -247,7 +249,7 @@ public String mint(Context context, DSpaceObject dso) try { request = requestFactory.getInstance(loadAuthority(), loadUser(), loadPassword()); } catch (URISyntaxException ex) { - log.error(ex.getMessage()); + log.error(ex::getMessage); throw new IdentifierException("DOI request not sent: " + ex.getMessage()); } @@ -256,18 +258,16 @@ public String mint(Context context, DSpaceObject dso) try { response = request.mint(crosswalkMetadata(context, dso)); } catch (IOException | URISyntaxException ex) { - log.error("Failed to send EZID request: {}", ex.getMessage()); + log.error("Failed to send EZID request: {}", ex::getMessage); throw new IdentifierException("DOI request not sent: " + ex.getMessage()); } // Good response? if (HttpURLConnection.HTTP_CREATED != response.getHttpStatusCode()) { log.error("EZID server responded: {} {}: {}", - new String[] { - String.valueOf(response.getHttpStatusCode()), - response.getHttpReasonPhrase(), - response.getEZIDStatusValue() - }); + response::getHttpStatusCode, + response::getHttpReasonPhrase, + response::getEZIDStatusValue); throw new IdentifierException("DOI not created: " + response.getHttpReasonPhrase() + ": " @@ -285,7 +285,7 @@ public String mint(Context context, DSpaceObject dso) log.info("Created {}", doi); return doi; } else { - log.error("EZID responded: {}", response.getEZIDStatusValue()); + log.error("EZID responded: {}", response::getEZIDStatusValue); throw new IdentifierException("No DOI returned"); } } @@ -302,7 +302,7 @@ public DSpaceObject resolve(Context context, String identifier, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, idToDOI(identifier)); } catch (IdentifierException | SQLException | AuthorizeException | IOException ex) { - log.error(ex.getMessage()); + log.error(ex::getMessage); throw new IdentifierNotResolvableException(ex); } if (!found.hasNext()) { @@ -360,24 +360,24 @@ public void delete(Context context, DSpaceObject dso) loadUser(), loadPassword()); response = request.delete(DOIToId(id.getValue())); } catch (URISyntaxException e) { - log.error("Bad URI in metadata value: {}", e.getMessage()); + log.error("Bad URI in metadata value: {}", e::getMessage); remainder.add(id.getValue()); skipped++; continue; } catch (IOException e) { - log.error("Failed request to EZID: {}", e.getMessage()); + log.error("Failed request to EZID: {}", e::getMessage); remainder.add(id.getValue()); skipped++; continue; } if (!response.isSuccess()) { - log.error("Unable to delete {} from DataCite: {}", id.getValue(), - response.getEZIDStatusValue()); + log.error("Unable to delete {} from DataCite: {}", id::getValue, + response::getEZIDStatusValue); remainder.add(id.getValue()); skipped++; continue; } - log.info("Deleted {}", id.getValue()); + log.info("Deleted {}", id::getValue); } // delete from item @@ -386,7 +386,7 @@ public void delete(Context context, DSpaceObject dso) dsoService.addMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, remainder); dsoService.update(context, dso); } catch (SQLException | AuthorizeException e) { - log.error("Failed to re-add identifiers: {}", e.getMessage()); + log.error("Failed to re-add identifiers: {}", e::getMessage); } if (skipped > 0) { @@ -415,25 +415,25 @@ public void delete(Context context, DSpaceObject dso, String identifier) loadUser(), loadPassword()); response = request.delete(DOIToId(id.getValue())); } catch (URISyntaxException e) { - log.error("Bad URI in metadata value {}: {}", id.getValue(), e.getMessage()); + log.error("Bad URI in metadata value {}: {}", id::getValue, e::getMessage); remainder.add(id.getValue()); skipped++; continue; } catch (IOException e) { - log.error("Failed request to EZID: {}", e.getMessage()); + log.error("Failed request to EZID: {}", e::getMessage); remainder.add(id.getValue()); skipped++; continue; } if (!response.isSuccess()) { - log.error("Unable to delete {} from DataCite: {}", id.getValue(), - response.getEZIDStatusValue()); + log.error("Unable to delete {} from DataCite: {}", id::getValue, + response::getEZIDStatusValue); remainder.add(id.getValue()); skipped++; continue; } - log.info("Deleted {}", id.getValue()); + log.info("Deleted {}", id::getValue); } // delete from item @@ -442,7 +442,7 @@ public void delete(Context context, DSpaceObject dso, String identifier) dsoService.addMetadata(context, dso, MD_SCHEMA, DOI_ELEMENT, DOI_QUALIFIER, null, remainder); dsoService.update(context, dso); } catch (SQLException | AuthorizeException e) { - log.error("Failed to re-add identifiers: {}", e.getMessage()); + log.error("Failed to re-add identifiers: {}", e::getMessage); } if (skipped > 0) { @@ -544,12 +544,10 @@ Map crosswalkMetadata(Context context, DSpaceObject dso) { mappedValue = xfrm.transform(value.getValue()); } catch (Exception ex) { log.error("Unable to transform '{}' from {} to {}: {}", - new String[] { - value.getValue(), - value.toString(), - key, - ex.getMessage() - }); + value::getValue, + value::toString, + () -> key, + ex::getMessage); continue; } } else { diff --git a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java index 57136d6143bb..931f1538583e 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java +++ b/dspace-api/src/main/java/org/dspace/identifier/doi/DataCiteConnector.java @@ -33,6 +33,8 @@ import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.content.DSpaceObject; import org.dspace.content.crosswalk.CrosswalkException; @@ -52,8 +54,6 @@ import org.jdom2.input.SAXBuilder; import org.jdom2.output.Format; import org.jdom2.output.XMLOutputter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -62,7 +62,7 @@ public class DataCiteConnector implements DOIConnector { - private static final Logger log = LoggerFactory.getLogger(DataCiteConnector.class); + private static final Logger log = LogManager.getLogger(); // Configuration property names static final String CFG_USER = "identifier.doi.user"; @@ -356,10 +356,8 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) .getDSpaceObjectService(dso); if (!this.xwalk.canDisseminate(dso)) { - log.error("Crosswalk " + this.CROSSWALK_NAME - + " cannot disseminate DSO with type " + dso.getType() - + " and ID " + dso.getID() + ". Giving up reserving the DOI " - + doi + "."); + log.error("Crosswalk {} cannot disseminate DSO with type {} and ID {}. Giving up reserving the DOI {}.", + this.CROSSWALK_NAME, dso.getType(), dso.getID(), doi); throw new DOIIdentifierException("Cannot disseminate " + dSpaceObjectService.getTypeText(dso) + "/" + dso.getID() + " using crosswalk " + this.CROSSWALK_NAME + ".", @@ -390,18 +388,18 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) try { root = xwalk.disseminateElement(context, dso, parameters); } catch (AuthorizeException ae) { - log.error("Caught an AuthorizeException while disseminating DSO " - + "with type " + dso.getType() + " and ID " + dso.getID() - + ". Giving up to reserve DOI " + doi + ".", ae); + log.error("Caught an AuthorizeException while disseminating DSO" + + " with type {} and ID {}. Giving up to reserve DOI {}.", + dso.getType(), dso.getID(), doi, ae); throw new DOIIdentifierException("AuthorizeException occured while " + "converting " + dSpaceObjectService.getTypeText(dso) + "/" + dso .getID() + " using crosswalk " + this.CROSSWALK_NAME + ".", ae, DOIIdentifierException.CONVERSION_ERROR); } catch (CrosswalkException ce) { - log.error("Caught an CrosswalkException while reserving a DOI (" - + doi + ") for DSO with type " + dso.getType() + " and ID " - + dso.getID() + ". Won't reserve the doi.", ce); + log.error("Caught a CrosswalkException while reserving a DOI ({})" + + " for DSO with type {} and ID {}. Won't reserve the doi.", + doi, dso.getType(), dso.getID(), ce); throw new DOIIdentifierException("CrosswalkException occured while " + "converting " + dSpaceObjectService.getTypeText(dso) + "/" + dso .getID() @@ -421,9 +419,8 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) } else if (!metadataDOI.equals(doi.substring(DOI.SCHEME.length()))) { log.error("While reserving a DOI, the " + "crosswalk to generate the metadata used another DOI than " - + "the DOI we're reserving. Cannot reserve DOI " + doi - + " for " + dSpaceObjectService.getTypeText(dso) + " " - + dso.getID() + "."); + + "the DOI we're reserving. Cannot reserve DOI {} for {} {}.", + doi, dSpaceObjectService.getTypeText(dso), dso.getID()); throw new IllegalStateException("An internal error occured while " + "generating the metadata. Unable to reserve doi, see logs " + "for further information."); @@ -440,12 +437,12 @@ public void reserveDOI(Context context, DSpaceObject dso, String doi) // 400 -> invalid XML case (400): { log.warn("DataCite was unable to understand the XML we send."); - log.warn("DataCite Metadata API returned a http status code " - + "400: " + resp.getContent()); + log.warn("DataCite Metadata API returned a http status code" + + " 400: {}", resp::getContent); Format format = Format.getCompactFormat(); format.setEncoding("UTF-8"); XMLOutputter xout = new XMLOutputter(format); - log.info("We send the following XML:\n" + xout.outputString(root)); + log.info("We send the following XML:\n{}", xout.outputString(root)); throw new DOIIdentifierException("Unable to reserve DOI " + doi + ". Please inform your administrator or take a look " + " into the log files.", DOIIdentifierException.BAD_REQUEST); @@ -479,8 +476,8 @@ public void registerDOI(Context context, DSpaceObject dso, String doi) resp = this.sendDOIPostRequest(doi, handleService.resolveToURL(context, dso.getHandle())); } catch (SQLException e) { - log.error("Caught SQL-Exception while resolving handle to URL: " - + e.getMessage()); + log.error("Caught SQL-Exception while resolving handle to URL: {}", + e::getMessage); throw new RuntimeException(e); } @@ -492,7 +489,7 @@ public void registerDOI(Context context, DSpaceObject dso, String doi) // 400 -> wrong domain, wrong prefix, wrong request body case (400): { log.warn("We send an irregular request to DataCite. While " - + "registering a DOI they told us: " + resp.getContent()); + + "registering a DOI they told us: {}", resp::getContent); throw new DOIIdentifierException("Currently we cannot register " + "DOIs. Please inform the administrator or take a look " + " in the DSpace log file.", @@ -501,8 +498,8 @@ public void registerDOI(Context context, DSpaceObject dso, String doi) // 412 Precondition failed: DOI was not reserved before registration! case (412): { log.error("We tried to register a DOI {} that has not been reserved " - + "before! The registration agency told us: {}.", doi, - resp.getContent()); + + "before! The registration agency told us: {}.", + () -> doi, resp::getContent); throw new DOIIdentifierException("There was an error in handling " + "of DOIs. The DOI we wanted to register had not been " + "reserved in advance. Please contact the administrator " @@ -511,7 +508,7 @@ public void registerDOI(Context context, DSpaceObject dso, String doi) } // Catch all other http status code in case we forgot one. default: { - log.warn("While registration of DOI {}, we got a http status code " + log.warn("While registering DOI {}, we got a http status code " + "{} and the message \"{}\".", doi, Integer.toString(resp.statusCode), resp.getContent()); throw new DOIIdentifierException("Unable to parse an answer from " @@ -564,8 +561,8 @@ protected DataCiteResponse sendDOIPostRequest(String doi, String url) try { EntityUtils.consume(reqEntity); } catch (IOException ioe) { - log.info("Caught an IOException while releasing a HTTPEntity:" - + ioe.getMessage()); + log.info("Caught an IOException while releasing a HTTPEntity: {}", + ioe::getMessage); } } } @@ -668,8 +665,8 @@ protected DataCiteResponse sendMetadataPostRequest(String doi, String metadata) try { EntityUtils.consume(reqEntity); } catch (IOException ioe) { - log.info("Caught an IOException while releasing an HTTPEntity:" - + ioe.getMessage()); + log.info("Caught an IOException while releasing an HTTPEntity: {}", + ioe::getMessage); } } } @@ -768,7 +765,7 @@ protected DataCiteResponse sendHttpRequest(HttpUriRequest req, String doi) // 500 is documented and signals an internal server error case (500): { log.warn("Caught an http status code 500 while managing DOI " - + "{}. Message was: " + content); + + "{}. Message was: {}", doi, content); throw new DOIIdentifierException("DataCite API has an internal error. " + "It is temporarily impossible to manage DOIs. " + "Further information can be found in DSpace log file.", @@ -781,7 +778,7 @@ protected DataCiteResponse sendHttpRequest(HttpUriRequest req, String doi) return new DataCiteResponse(statusCode, content); } catch (IOException e) { - log.warn("Caught an IOException: " + e.getMessage()); + log.warn("Caught an IOException: {}", e::getMessage); throw new RuntimeException(e); } finally { try { @@ -790,7 +787,7 @@ protected DataCiteResponse sendHttpRequest(HttpUriRequest req, String doi) EntityUtils.consume(entity); } } catch (IOException e) { - log.warn("Can't release HTTP-Entity: " + e.getMessage()); + log.warn("Can't release HTTP-Entity: {}", e::getMessage); } } } diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java index 525ad46b2554..bf46c3bf59da 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDRequest.java @@ -27,10 +27,10 @@ import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.identifier.DOI; import org.dspace.identifier.IdentifierException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A request to EZID concerning a given (or expected) identifier. @@ -38,7 +38,7 @@ * @author Mark H. Wood */ public class EZIDRequest { - private static final Logger log = LoggerFactory.getLogger(EZIDRequest.class); + private static final Logger log = LogManager.getLogger(); private static final String ID_PATH = "/id/" + DOI.SCHEME; @@ -149,7 +149,7 @@ public EZIDResponse lookup(String name) // GET path HttpGet request; URI uri = new URI(scheme, host, path + ID_PATH + authority + name, null); - log.debug("EZID lookup {}", uri.toASCIIString()); + log.debug("EZID lookup {}", uri::toASCIIString); request = new HttpGet(uri); HttpResponse response = client.execute(request, httpContext); return new EZIDResponse(response); @@ -172,7 +172,7 @@ public EZIDResponse create(String name, Map metadata) // PUT path [+metadata] HttpPut request; URI uri = new URI(scheme, host, path + ID_PATH + authority + '/' + name, null); - log.debug("EZID create {}", uri.toASCIIString()); + log.debug("EZID create {}", uri::toASCIIString); request = new HttpPut(uri); if (null != metadata) { request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8)); @@ -196,7 +196,7 @@ public EZIDResponse mint(Map metadata) // POST path [+metadata] HttpPost request; URI uri = new URI(scheme, host, path + SHOULDER_PATH + authority, null); - log.debug("EZID mint {}", uri.toASCIIString()); + log.debug("EZID mint {}", uri::toASCIIString); request = new HttpPost(uri); if (null != metadata) { request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8)); @@ -225,7 +225,7 @@ public EZIDResponse modify(String name, Map metadata) // POST path +metadata HttpPost request; URI uri = new URI(scheme, host, path + ID_PATH + authority + name, null); - log.debug("EZID modify {}", uri.toASCIIString()); + log.debug("EZID modify {}", uri::toASCIIString); request = new HttpPost(uri); request.setEntity(new StringEntity(formatMetadata(metadata), UTF_8)); HttpResponse response = client.execute(request, httpContext); @@ -246,7 +246,7 @@ public EZIDResponse delete(String name) // DELETE path HttpDelete request; URI uri = new URI(scheme, host, path + ID_PATH + authority + name, null); - log.debug("EZID delete {}", uri.toASCIIString()); + log.debug("EZID delete {}", uri::toASCIIString); request = new HttpDelete(uri); HttpResponse response = client.execute(request, httpContext); return new EZIDResponse(response); diff --git a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java index 9c5ad904846c..4ac975b2db2e 100644 --- a/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java +++ b/dspace-api/src/main/java/org/dspace/identifier/ezid/EZIDResponse.java @@ -19,15 +19,15 @@ import org.apache.http.HttpResponse; import org.apache.http.ParseException; import org.apache.http.util.EntityUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.identifier.IdentifierException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Decoded response data evoked by a request made to EZID. */ public class EZIDResponse { - private static final Logger log = LoggerFactory.getLogger(EZIDResponse.class); + private static final Logger log = LogManager.getLogger(); private static final String UTF_8 = "UTF-8"; @@ -35,7 +35,7 @@ public class EZIDResponse { private final String statusValue; - private final Map attributes = new HashMap(); + private final Map attributes = new HashMap<>(); private final HttpResponse response; @@ -49,12 +49,8 @@ public EZIDResponse(HttpResponse response) String body; try { body = EntityUtils.toString(responseBody, UTF_8); - } catch (IOException ex) { - log.error(ex.getMessage()); - throw new IdentifierException("EZID response not understood: " - + ex.getMessage()); - } catch (ParseException ex) { - log.error(ex.getMessage()); + } catch (IOException | ParseException ex) { + log.error(ex::getMessage); throw new IdentifierException("EZID response not understood: " + ex.getMessage()); } @@ -124,7 +120,7 @@ public String getEZIDStatusValue() { * @return all keys found in the response. */ public List getKeys() { - List keys = new ArrayList(); + List keys = new ArrayList<>(); for (String key : attributes.keySet()) { keys.add(key); } diff --git a/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java b/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java index d177e61607f1..97da341fb811 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java +++ b/dspace-api/src/main/java/org/dspace/orcid/consumer/OrcidQueueConsumer.java @@ -22,6 +22,8 @@ import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.DSpaceObject; import org.dspace.content.Item; import org.dspace.content.MetadataFieldName; @@ -45,8 +47,6 @@ import org.dspace.profile.OrcidProfileSyncPreference; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The consumer to fill the ORCID queue. The addition to the queue is made for @@ -56,7 +56,7 @@ * be synchronized (based on the preferences set by the user) *
  • are publications/fundings related to profile items linked to orcid (based * on the preferences set by the user)
  • - * + * * * * @author Luca Giamminonni (luca.giamminonni at 4science.it) @@ -64,7 +64,7 @@ */ public class OrcidQueueConsumer implements Consumer { - private static final Logger LOGGER = LoggerFactory.getLogger(OrcidQueueConsumer.class); + private static final Logger LOGGER = LogManager.getLogger(); private OrcidQueueService orcidQueueService; @@ -82,7 +82,7 @@ public class OrcidQueueConsumer implements Consumer { private RelationshipService relationshipService; - private List alreadyConsumedItems = new ArrayList<>(); + private final List alreadyConsumedItems = new ArrayList<>(); @Override public void initialize() throws Exception { @@ -263,7 +263,7 @@ private void createDeletionRecordForNoMorePresentSignatures(Context context, Ite if (StringUtils.isBlank(putCode)) { LOGGER.warn("The orcid history record with id {} should have a not blank put code", - historyRecord.getID()); + historyRecord::getID); continue; } diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidFundingFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidFundingFactory.java index 890b54f12b1c..fd2669935cab 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidFundingFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidFundingFactory.java @@ -18,6 +18,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.Relationship; @@ -44,8 +46,6 @@ import org.orcid.jaxb.model.v3.release.record.FundingContributor; import org.orcid.jaxb.model.v3.release.record.FundingContributors; import org.orcid.jaxb.model.v3.release.record.FundingTitle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -57,7 +57,7 @@ */ public class OrcidFundingFactory implements OrcidEntityFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(OrcidFundingFactory.class); + private static final Logger LOGGER = LogManager.getLogger(); @Autowired private ItemService itemService; diff --git a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java index 53b46d8256d1..47619b3c1d63 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java +++ b/dspace-api/src/main/java/org/dspace/orcid/model/factory/impl/OrcidWorkFactory.java @@ -19,6 +19,8 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.EnumUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.MetadataValue; import org.dspace.content.service.ItemService; @@ -42,8 +44,6 @@ import org.orcid.jaxb.model.v3.release.record.Work; import org.orcid.jaxb.model.v3.release.record.WorkContributors; import org.orcid.jaxb.model.v3.release.record.WorkTitle; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -55,7 +55,7 @@ */ public class OrcidWorkFactory implements OrcidEntityFactory { - private static final Logger LOGGER = LoggerFactory.getLogger(OrcidWorkFactory.class); + private static final Logger LOGGER = LogManager.getLogger(); @Autowired private ItemService itemService; @@ -164,7 +164,7 @@ private ExternalIDs getWorkExternalIds(Context context, Item item) { */ private List getWorkSelfExternalIds(Context context, Item item) { - List selfExternalIds = new ArrayList(); + List selfExternalIds = new ArrayList<>(); Map externalIdentifierFields = fieldMapping.getExternalIdentifierFields(); diff --git a/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPush.java b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPush.java index 0e6f856bfcee..5914a2d8df1c 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPush.java +++ b/dspace-api/src/main/java/org/dspace/orcid/script/OrcidBulkPush.java @@ -20,6 +20,8 @@ import org.apache.commons.cli.ParseException; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -36,8 +38,6 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.utils.DSpace; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Script that perform the bulk synchronization with ORCID registry of all the @@ -48,7 +48,7 @@ */ public class OrcidBulkPush extends DSpaceRunnable> { - private static final Logger LOGGER = LoggerFactory.getLogger(OrcidBulkPush.class); + private static final Logger LOGGER = LogManager.getLogger(); private OrcidQueueService orcidQueueService; @@ -63,7 +63,7 @@ public class OrcidBulkPush extends DSpaceRunnable synchronizationModeByProfileItem = new HashMap<>(); + private final Map synchronizationModeByProfileItem = new HashMap<>(); private boolean ignoreMaxAttempts = false; diff --git a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidHistoryServiceImpl.java b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidHistoryServiceImpl.java index 0bec9a12e0ea..0e18b46f5db2 100644 --- a/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidHistoryServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/orcid/service/impl/OrcidHistoryServiceImpl.java @@ -24,6 +24,8 @@ import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Item; import org.dspace.content.MetadataFieldName; import org.dspace.content.MetadataValue; @@ -48,8 +50,6 @@ import org.dspace.orcid.service.OrcidProfileSectionFactoryService; import org.dspace.orcid.service.OrcidTokenService; import org.orcid.jaxb.model.v3.release.record.Activity; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -61,7 +61,7 @@ */ public class OrcidHistoryServiceImpl implements OrcidHistoryService { - private static final Logger LOGGER = LoggerFactory.getLogger(OrcidHistoryServiceImpl.class); + private static final Logger LOGGER = LogManager.getLogger(); @Autowired private OrcidHistoryDAO orcidHistoryDAO; @@ -134,7 +134,7 @@ public Optional findLastPutCode(Context context, Item profileItem, Item @Override public Map findLastPutCodes(Context context, Item entity) throws SQLException { - Map profileItemAndPutCodeMap = new HashMap(); + Map profileItemAndPutCodeMap = new HashMap<>(); List orcidHistoryRecords = findByEntity(context, entity); for (OrcidHistory orcidHistoryRecord : orcidHistoryRecords) { @@ -187,10 +187,12 @@ public OrcidHistory synchronizeWithOrcid(Context context, OrcidQueue orcidQueue, } catch (OrcidValidationException ex) { throw ex; } catch (OrcidClientException ex) { - LOGGER.error("An error occurs during the orcid synchronization of ORCID queue " + orcidQueue, ex); + LOGGER.error("An error occurs during the orcid synchronization of ORCID queue {}", + orcidQueue, ex); return createHistoryRecordFromOrcidError(context, orcidQueue, operation, ex); } catch (RuntimeException ex) { - LOGGER.warn("An unexpected error occurs during the orcid synchronization of ORCID queue " + orcidQueue, ex); + LOGGER.warn("An unexpected error occurs during the orcid synchronization of ORCID queue {}", + orcidQueue, ex); return createHistoryRecordFromGenericError(context, orcidQueue, operation, ex); } diff --git a/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java b/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java index 80bbd68fd19d..639601df976d 100644 --- a/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/profile/ResearcherProfileServiceImpl.java @@ -27,6 +27,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.exception.ResourceAlreadyExistsException; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.service.AuthorizeService; @@ -53,8 +55,6 @@ import org.dspace.profile.service.ResearcherProfileService; import org.dspace.services.ConfigurationService; import org.dspace.util.UUIDUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; @@ -66,7 +66,7 @@ */ public class ResearcherProfileServiceImpl implements ResearcherProfileService { - private static Logger log = LoggerFactory.getLogger(ResearcherProfileServiceImpl.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ItemService itemService; @@ -310,7 +310,7 @@ private Optional findConfiguredProfileCollection(Context context) th if (isNotProfileCollection(collection)) { log.warn("The configured researcher-profile.collection.uuid " - + "has an invalid entity type, expected " + getProfileType()); + + "has an invalid entity type, expected {}", this::getProfileType); return Optional.empty(); } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/AutomaticProcessingAction.java b/dspace-api/src/main/java/org/dspace/qaevent/AutomaticProcessingAction.java new file mode 100644 index 000000000000..771650746d03 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/AutomaticProcessingAction.java @@ -0,0 +1,17 @@ +/** + * 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; + +/** + * Enumeration of possible actions to perform over a {@link org.dspace.content.QAEvent} + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public enum AutomaticProcessingAction { + REJECT, ACCEPT, IGNORE +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QAEventAutomaticProcessingEvaluation.java b/dspace-api/src/main/java/org/dspace/qaevent/QAEventAutomaticProcessingEvaluation.java new file mode 100644 index 000000000000..d7c8f3681e56 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/QAEventAutomaticProcessingEvaluation.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.QAEvent; +import org.dspace.core.Context; + +/** + * This interface allows the implemnetation of Automation Processing rules + * defining which {@link AutomaticProcessingAction} should be eventually + * performed on a specific {@link QAEvent} + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public interface QAEventAutomaticProcessingEvaluation { + + /** + * Evaluate a {@link QAEvent} to decide which, if any, {@link AutomaticProcessingAction} should be performed + * + * @param context the DSpace context + * @param qaEvent the quality assurance event + * @return an action of {@link AutomaticProcessingAction} or null if no automatic action should be performed + */ + AutomaticProcessingAction evaluateAutomaticProcessing(Context context, QAEvent qaEvent); + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QANotifyPatterns.java b/dspace-api/src/main/java/org/dspace/qaevent/QANotifyPatterns.java index 4bf9867a2bfb..ede1990569ce 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/QANotifyPatterns.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/QANotifyPatterns.java @@ -21,10 +21,10 @@ public class QANotifyPatterns { public static final String TOPIC_ENRICH_MORE_ENDORSEMENT = "ENRICH/MORE/ENDORSEMENT"; public static final String TOPIC_ENRICH_MORE_PID = "ENRICH/MORE/PID"; public static final String TOPIC_ENRICH_MISSING_PID = "ENRICH/MISSING/PID"; + public static final String TOPIC_ENRICH_MORE_LINK = "ENRICH/MORE/LINK"; /** * Default constructor */ private QANotifyPatterns() { } - } \ No newline at end of file diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QAScoreAutomaticProcessingEvaluation.java b/dspace-api/src/main/java/org/dspace/qaevent/QAScoreAutomaticProcessingEvaluation.java new file mode 100644 index 000000000000..f685222d3dfc --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/QAScoreAutomaticProcessingEvaluation.java @@ -0,0 +1,151 @@ +/** + * 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.sql.SQLException; +import java.util.UUID; + +import org.dspace.content.Item; +import org.dspace.content.QAEvent; +import org.dspace.content.logic.LogicalStatement; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * A configurable implementation of {@link QAEventAutomaticProcessingEvaluation} allowing to define thresholds for + * automatic acceptance, rejection or ignore of {@link QAEvent} matching a specific, optional, item filter + * {@link LogicalStatement}. If the item filter is not defined only the score threshold will be used. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class QAScoreAutomaticProcessingEvaluation implements QAEventAutomaticProcessingEvaluation { + /** + * The minimum score of QAEvent to be considered for automatic approval (trust must be greater or equals to that) + */ + private double scoreToApprove; + + /** + * The threshold under which QAEvent are considered for automatic ignore (trust must be less or equals to that) + */ + private double scoreToIgnore; + + /** + * The threshold under which QAEvent are considered for automatic rejection (trust must be less or equals to that) + */ + private double scoreToReject; + + /** + * The optional logical statement that must pass for item target of a QAEvent to be considered for automatic + * approval + */ + private LogicalStatement itemFilterToApprove; + + /** + * The optional logical statement that must pass for item target of a QAEvent to be considered for automatic + * ignore + */ + private LogicalStatement itemFilterToIgnore; + + /** + * The optional logical statement that must pass for item target of a QAEvent to be considered for automatic + * rejection + */ + private LogicalStatement itemFilterToReject; + + @Autowired + private ItemService itemService; + + @Override + public AutomaticProcessingAction evaluateAutomaticProcessing(Context context, QAEvent qaEvent) { + Item item = findItem(context, qaEvent.getTarget()); + + if (shouldReject(context, qaEvent.getTrust(), item)) { + return AutomaticProcessingAction.REJECT; + } else if (shouldIgnore(context, qaEvent.getTrust(), item)) { + return AutomaticProcessingAction.IGNORE; + } else if (shouldApprove(context, qaEvent.getTrust(), item)) { + return AutomaticProcessingAction.ACCEPT; + } else { + return null; + } + + } + + private Item findItem(Context context, String uuid) { + try { + return itemService.find(context, UUID.fromString(uuid)); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private boolean shouldReject(Context context, double trust, Item item) { + return trust <= scoreToReject && + (itemFilterToReject == null || itemFilterToReject.getResult(context, item)); + } + + private boolean shouldIgnore(Context context, double trust, Item item) { + return trust <= scoreToIgnore && + (itemFilterToIgnore == null || itemFilterToIgnore.getResult(context, item)); + } + + private boolean shouldApprove(Context context, double trust, Item item) { + return trust >= scoreToApprove && + (itemFilterToApprove == null || itemFilterToApprove.getResult(context, item)); + } + + public double getScoreToApprove() { + return scoreToApprove; + } + + public void setScoreToApprove(double scoreToApprove) { + this.scoreToApprove = scoreToApprove; + } + + public double getScoreToIgnore() { + return scoreToIgnore; + } + + public void setScoreToIgnore(double scoreToIgnore) { + this.scoreToIgnore = scoreToIgnore; + } + + public double getScoreToReject() { + return scoreToReject; + } + + public void setScoreToReject(double scoreToReject) { + this.scoreToReject = scoreToReject; + } + + public LogicalStatement getItemFilterToApprove() { + return itemFilterToApprove; + } + + public void setItemFilterToApprove(LogicalStatement itemFilterToApprove) { + this.itemFilterToApprove = itemFilterToApprove; + } + + public LogicalStatement getItemFilterToIgnore() { + return itemFilterToIgnore; + } + + public void setItemFilterToIgnore(LogicalStatement itemFilterToIgnore) { + this.itemFilterToIgnore = itemFilterToIgnore; + } + + public LogicalStatement getItemFilterToReject() { + return itemFilterToReject; + } + + public void setItemFilterToReject(LogicalStatement itemFilterToReject) { + this.itemFilterToReject = itemFilterToReject; + } +} + diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QASource.java b/dspace-api/src/main/java/org/dspace/qaevent/QASource.java index 506f68e9a2a0..10849b47fc27 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/QASource.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/QASource.java @@ -59,4 +59,9 @@ public UUID getFocus() { public void setFocus(UUID focus) { this.focus = focus; } + + @Override + public String toString() { + return name + focus + totalEvents; + } } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java b/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java index c96847e7a108..92fe3737f450 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/QATopic.java @@ -32,6 +32,13 @@ public class QATopic { private Date lastEvent; private long totalEvents; + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } public String getKey() { return key; } @@ -40,6 +47,14 @@ public void setKey(String key) { this.key = key; } + public void setFocus(UUID focus) { + this.focus = focus; + } + + public UUID getFocus() { + return focus; + } + public long getTotalEvents() { return totalEvents; } @@ -55,20 +70,4 @@ public Date getLastEvent() { public void setLastEvent(Date lastEvent) { this.lastEvent = lastEvent; } - - public String getSource() { - return source; - } - - public void setSource(String source) { - this.source = source; - } - - public UUID getFocus() { - return focus; - } - - public void setFocus(UUID focus) { - this.focus = focus; - } } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/AMetadataMapAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/AMetadataMapAction.java new file mode 100644 index 000000000000..ee81988f635d --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/AMetadataMapAction.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.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.QAMessageDTO; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Implementation of {@link QualityAssuranceAction} that add a specific metadata on the given + * item based on the child class implementation. + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public abstract class AMetadataMapAction 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; + } + + public abstract String extractMetadataType(QAMessageDTO message); + public abstract String extractMetadataValue(QAMessageDTO message); + + /** + * 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) { + + try { + String targetMetadata = types.get(extractMetadataType(message)); + if (targetMetadata == null) { + targetMetadata = types.get(DEFAULT); + } + String[] metadata = splitMetadata(targetMetadata); + itemService.addMetadata(context, item, metadata[0], metadata[1], metadata[2], null, + extractMetadataValue(message)); + 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/ASimpleMetadataAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/ASimpleMetadataAction.java new file mode 100644 index 000000000000..3acaa726e0ea --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/ASimpleMetadataAction.java @@ -0,0 +1,65 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.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.QAMessageDTO; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Abstract class for Simple metadata action. + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public abstract class ASimpleMetadataAction 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]; + } + } + + public abstract String extractMetadataValue(QAMessageDTO message); + + @Override + public void applyCorrection(Context context, Item item, Item relatedItem, QAMessageDTO message) { + try { + String metadataValue = extractMetadataValue(message); + itemService.addMetadata(context, item, metadataSchema, metadataElement, metadataQualifier, null, + metadataValue); + itemService.update(context, item); + } catch (SQLException | AuthorizeException e) { + throw new RuntimeException(e); + } + } +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QANotifyMetadataMapAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QANotifyMetadataMapAction.java new file mode 100644 index 000000000000..a85a38655081 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QANotifyMetadataMapAction.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.action; + +import org.dspace.qaevent.service.dto.NotifyMessageDTO; +import org.dspace.qaevent.service.dto.QAMessageDTO; + +/** + * Notify Implementation {@link AMetadataMapAction} + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class QANotifyMetadataMapAction extends AMetadataMapAction { + + @Override + public String extractMetadataType(QAMessageDTO message) { + return ((NotifyMessageDTO)message).getRelationship(); + } + + @Override + public String extractMetadataValue(QAMessageDTO message) { + return ((NotifyMessageDTO)message).getHref(); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QANotifySimpleMetadataAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QANotifySimpleMetadataAction.java new file mode 100644 index 000000000000..ffb70fce66cb --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QANotifySimpleMetadataAction.java @@ -0,0 +1,26 @@ +/** + * 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 org.dspace.qaevent.QualityAssuranceAction; +import org.dspace.qaevent.service.dto.NotifyMessageDTO; +import org.dspace.qaevent.service.dto.QAMessageDTO; + +/** + * Implementation of {@link QualityAssuranceAction} that add a simple metadata to the given + * item. + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class QANotifySimpleMetadataAction extends ASimpleMetadataAction { + + public String extractMetadataValue(QAMessageDTO message) { + return ((NotifyMessageDTO) message).getHref(); + } +} 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 index e1fa23002fcb..427ad2bfdea0 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireMetadataMapAction.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireMetadataMapAction.java @@ -7,80 +7,25 @@ */ 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) + * Openaire Implementation {@link AMetadataMapAction} + * + * @author Francesco Bacchelli (francesco.bacchelli 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 class QAOpenaireMetadataMapAction extends AMetadataMapAction { - public void setTypes(Map types) { - this.types = types; + @Override + public String extractMetadataType(QAMessageDTO message) { + return ((OpenaireMessageDTO)message).getType(); } - /** - * 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 extractMetadataValue(QAMessageDTO message) { + return ((OpenaireMessageDTO)message).getValue(); } - 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 index 1252462c8183..3baa95ecedb6 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireSimpleMetadataAction.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAOpenaireSimpleMetadataAction.java @@ -7,54 +7,20 @@ */ 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. + * 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 { - - protected String metadata; - protected String metadataSchema; - protected String metadataElement; - protected String metadataQualifier; - - @Autowired - protected ItemService itemService; - - @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.getMessage(), e); - } - } - - public String getMetadata() { - return metadata; - } +public class QAOpenaireSimpleMetadataAction extends ASimpleMetadataAction { - 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]; - } + public String extractMetadataValue(QAMessageDTO message) { + return ((OpenaireMessageDTO) message).getAbstracts(); } } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QAReinstateRequestAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QAReinstateRequestAction.java index cd436f396650..7fa08dc6ecee 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/action/QAReinstateRequestAction.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAReinstateRequestAction.java @@ -9,14 +9,14 @@ import java.sql.SQLException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; 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.QAMessageDTO; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -27,7 +27,7 @@ */ public class QAReinstateRequestAction implements QualityAssuranceAction { - private static final Logger log = LoggerFactory.getLogger(QAReinstateRequestAction.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ItemService itemService; diff --git a/dspace-api/src/main/java/org/dspace/qaevent/action/QAWithdrawnRequestAction.java b/dspace-api/src/main/java/org/dspace/qaevent/action/QAWithdrawnRequestAction.java index 7048a8285ea1..a0463fdb1821 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/action/QAWithdrawnRequestAction.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/action/QAWithdrawnRequestAction.java @@ -9,25 +9,25 @@ import java.sql.SQLException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; 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.QAMessageDTO; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** * QAWithdrawnRequestAction is an implementation of the QualityAssuranceAction interface. * It is responsible for applying a correction to withdraw a specified item. - * + * * @author Mykhaylo Boychuk (mykhaylo.boychuk@4science.com) */ public class QAWithdrawnRequestAction implements QualityAssuranceAction { - private static final Logger log = LoggerFactory.getLogger(QAWithdrawnRequestAction.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ItemService itemService; diff --git a/dspace-api/src/main/java/org/dspace/qaevent/security/QASecurity.java b/dspace-api/src/main/java/org/dspace/qaevent/security/QASecurity.java index 68ef1066654f..44b00e7d9488 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/security/QASecurity.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/security/QASecurity.java @@ -19,6 +19,7 @@ * provide logic to filter and determine visibility of QA events based on the user's permissions. * * @author Andrea Bollini (andrea.bollini at 4science.com) + * */ public interface QASecurity { @@ -51,5 +52,4 @@ public interface QASecurity { * @return true if the user can see the provided qaEvent */ public boolean canSeeQAEvent(Context context, EPerson user, 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 index 9f9fd3eaa5ce..3254aecf7731 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/QAEventService.java @@ -47,7 +47,18 @@ public interface QAEventService { * @return the topics list */ public List findAllTopicsBySource(Context context, String source, long offset, long count, - String orderField, boolean ascending); + String orderField, boolean ascending); + + /** + * Find a specific topic by its name, source and optionally a target. + * + * @param context the DSpace context + * @param sourceName the name of the source + * @param topicName the topic name to search for + * @param target (nullable) the uuid of the target to focus on + * @return the topic + */ + public QATopic findTopicBySourceAndNameAndTarget(Context context, String sourceName, String topicName, UUID target); /** * Count all the event's topics. @@ -91,9 +102,9 @@ public List findEventsByTopic(Context context, String sourceName, Strin public long countEventsByTopic(Context context, String sourceName, String topic); /** - * Find an event by the given id. - * @param id the id of the event to search for + * Find an event by the given id. Please note that no security filter are applied by this method. * + * @param id the id of the event to search for * @return the event */ public QAEvent findEventByEventId(String id); @@ -195,7 +206,7 @@ public List findEventsByTopic(Context context, String sourceName, Strin /** * Find a list of QA events according to the pagination parameters for the specified topic and target sorted by * trust descending - * + * * @param context the DSpace context * @param source the source name * @param topic the topic to search for @@ -229,17 +240,6 @@ public List findEventsByTopicAndTarget(Context context, String source, */ public long countEventsByTopicAndTarget(Context context, String source, String topic, UUID target); - /** - * Find a specific topic by its name, source and optionally a target. - * - * @param context the DSpace context - * @param sourceName the name of the source - * @param topicName the topic name to search for - * @param target (nullable) the uuid of the target to focus on - * @return the topic - */ - public QATopic findTopicBySourceAndNameAndTarget(Context context, String sourceName, String topicName, UUID target); - /** * Find all the event's sources related to a specific item * @@ -262,6 +262,6 @@ public List findEventsByTopicAndTarget(Context context, String source, * @return the topics list */ public List findAllTopicsBySourceAndTarget(Context context, String source, UUID target, long offset, - int pageSize, String orderField, boolean ascending); + long pageSize, String orderField, boolean ascending); } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/dto/NotifyMessageDTO.java b/dspace-api/src/main/java/org/dspace/qaevent/service/dto/NotifyMessageDTO.java new file mode 100644 index 000000000000..2a5842589fde --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/dto/NotifyMessageDTO.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.qaevent.service.dto; + +/** + * Implementation of {@link QAMessageDTO} that model message coming from COAR NOTIFY. + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class NotifyMessageDTO implements QAMessageDTO { + + private String serviceName; + + private String serviceId; + + private String href; + + private String relationship; + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getRelationship() { + return relationship; + } + + public void setRelationship(String relationship) { + this.relationship = relationship; + } + +} 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 index 59b4acf9db21..821f11f86914 100644 --- 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 @@ -7,6 +7,7 @@ */ package org.dspace.qaevent.service.dto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; /** @@ -15,6 +16,7 @@ * @author Luca Giamminonni (luca.giamminonni at 4science.it) * */ +@JsonIgnoreProperties(ignoreUnknown = true) public class OpenaireMessageDTO implements QAMessageDTO { @JsonProperty("pids[0].value") 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 index e25d914de54d..30875a5105b0 100644 --- 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 @@ -80,12 +80,21 @@ public void accept(Context context, QAEvent qaevent) { if (qaevent.getRelated() != null) { related = itemService.find(context, UUID.fromString(qaevent.getRelated())); } + if (topicsToActions.get(qaevent.getTopic()) == null) { + String msg = "Unable to manage QA Event typed " + qaevent.getTopic() + + ". Managed types are: " + topicsToActions; + log.error(msg); + throw new RuntimeException(msg); + } + context.turnOffAuthorisationSystem(); 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); + } finally { + context.restoreAuthSystemState(); } } diff --git a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventSecurityServiceImpl.java b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventSecurityServiceImpl.java index ca689a79e09a..854626b3ba9d 100644 --- a/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventSecurityServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/qaevent/service/impl/QAEventSecurityServiceImpl.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.Optional; +import org.apache.logging.log4j.Logger; import org.dspace.content.QAEvent; import org.dspace.core.Context; import org.dspace.eperson.EPerson; @@ -33,6 +34,8 @@ public class QAEventSecurityServiceImpl implements QAEventSecurityService { /** * A mapping of QA source names to their corresponding QASecurity configurations. */ + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(QAEventSecurityServiceImpl.class); + private Map qaSecurityConfiguration; public void setQaSecurityConfiguration(Map qaSecurityConfiguration) { @@ -43,40 +46,16 @@ public void setDefaultSecurity(QASecurity defaultSecurity) { this.defaultSecurity = defaultSecurity; } - /** - * Generate a query to restrict the qa events returned by other search/find method to the only ones visible to the - * specified user - * - * @param context the context - * @param user the eperson to consider - * @param sourceName the source name - * @return the solr filter query - */ @Override - public Optional generateQAEventFilterQuery(Context context, EPerson user, String sourceName) { - QASecurity qaSecurity = getQASecurity(sourceName); + public Optional generateQAEventFilterQuery(Context context, EPerson user, String qaSource) { + QASecurity qaSecurity = getQASecurity(qaSource); return qaSecurity.generateFilterQuery(context, user); } - /** - * Retrieves the QASecurity configuration for the specified QA source, or uses the default - * configuration if not available. - * - * @param qaSource The name of the QA source. - * @return The QASecurity configuration for the specified QA source, or the default configuration if not available. - */ private QASecurity getQASecurity(String qaSource) { return qaSecurityConfiguration.getOrDefault(qaSource, defaultSecurity); } - /** - * Determines whether the user is authorized to see the specified QA event. - * - * @param context the context - * @param user the eperson to consider - * @param qaEvent the qaevent to check - * @return true if the specified user can see the specified event - */ @Override public boolean canSeeEvent(Context context, EPerson user, QAEvent qaEvent) { String source = qaEvent.getSource(); @@ -84,17 +63,9 @@ public boolean canSeeEvent(Context context, EPerson user, QAEvent qaEvent) { return qaSecurity.canSeeQASource(context, user) && qaSecurity.canSeeQAEvent(context, user, qaEvent); } - /** - * Determines whether the user is authorized to see events from the specified QA source. - * @param context The context. - * @param user The EPerson to consider - * @param sourceName The source name - * - * @return True if the user is authorized to see events from the source, false otherwise. - */ @Override - public boolean canSeeSource(Context context, EPerson user, String sourceName) { - QASecurity qaSecurity = getQASecurity(sourceName); + public boolean canSeeSource(Context context, EPerson user, String qaSource) { + QASecurity qaSecurity = getQASecurity(qaSource); return qaSecurity.canSeeQASource(context, user); } 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 index 817171bc26cf..98077a1c0c76 100644 --- 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 @@ -17,6 +17,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -28,6 +29,8 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.solr.client.solrj.SolrClient; import org.apache.solr.client.solrj.SolrQuery; import org.apache.solr.client.solrj.SolrQuery.ORDER; @@ -41,23 +44,28 @@ 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.core.Email; import org.dspace.core.I18nUtil; import org.dspace.eperson.EPerson; +import org.dspace.handle.service.HandleService; +import org.dspace.qaevent.AutomaticProcessingAction; +import org.dspace.qaevent.QAEventAutomaticProcessingEvaluation; 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.QAEventActionService; import org.dspace.qaevent.service.QAEventSecurityService; import org.dspace.qaevent.service.QAEventService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + /** * Implementation of {@link QAEventService} that use Solr to store events. When @@ -70,7 +78,7 @@ */ public class QAEventServiceImpl implements QAEventService { - private static final Logger log = LoggerFactory.getLogger(QAEventServiceImpl.class); + private static final Logger log = LogManager.getLogger(); public static final String QAEVENTS_SOURCES = "qaevents.sources"; @@ -83,9 +91,19 @@ public class QAEventServiceImpl implements QAEventService { @Autowired(required = true) protected ItemService itemService; + @Autowired + private HandleService handleService; + @Autowired private QAEventsDAOImpl qaEventsDao; + @Autowired(required = false) + @Qualifier("qaAutomaticProcessingMap") + private Map qaAutomaticProcessingMap; + + @Autowired + private QAEventActionService qaEventActionService; + private ObjectMapper jsonMapper; public QAEventServiceImpl() { @@ -148,7 +166,7 @@ public long countTopicsBySource(Context context, String sourceName) { solrQuery.setFacet(true); solrQuery.setFacetMinCount(1); solrQuery.addFacetField(TOPIC); - solrQuery.addFilterQuery("source:" + sourceName); + solrQuery.addFilterQuery(SOURCE + ":\"" + sourceName + "\""); QueryResponse response; try { response = getSolr().query(solrQuery); @@ -159,15 +177,18 @@ public long countTopicsBySource(Context context, String sourceName) { } @Override - public QATopic findTopicBySourceAndNameAndTarget(Context context, String sourceName, String topicName,UUID target) { - var currentUser = context.getCurrentUser(); - if (isNotSupportedSource(sourceName) || !qaSecurityService.canSeeSource(context, currentUser, sourceName)) { + public QATopic findTopicBySourceAndNameAndTarget(Context context, String sourceName, String topicName, + UUID target) { + if (isNotSupportedSource(sourceName) + || !qaSecurityService.canSeeSource(context, context.getCurrentUser(), sourceName)) { return null; } SolrQuery solrQuery = new SolrQuery(); - Optional securityQuery = qaSecurityService.generateQAEventFilterQuery(context, currentUser, sourceName); solrQuery.setRows(0); + Optional securityQuery = qaSecurityService.generateQAEventFilterQuery(context, + context.getCurrentUser(), sourceName); solrQuery.setQuery(securityQuery.orElse("*:*")); + solrQuery.addFilterQuery(SOURCE + ":\"" + sourceName + "\""); solrQuery.addFilterQuery(TOPIC + ":\"" + topicName + "\""); if (target != null) { @@ -249,25 +270,34 @@ public List findAllTopics(Context context, long offset, long count, Str } @Override - public List findAllTopicsBySource(Context context, String sourceName, long offset, long count, - String orderField, boolean ascending) { - var currentUser = context.getCurrentUser(); - if (isNotSupportedSource(sourceName) || !qaSecurityService.canSeeSource(context, currentUser, sourceName)) { + public List findAllTopicsBySource(Context context, String source, long offset, + long count, String orderField, boolean ascending) { + return findAllTopicsBySourceAndTarget(context, source, null, offset, count, orderField, ascending); + } + + @Override + public List findAllTopicsBySourceAndTarget(Context context, String source, UUID target, long offset, + long pageSize, String orderField, boolean ascending) { + if (isNotSupportedSource(source) + || !qaSecurityService.canSeeSource(context, context.getCurrentUser(), source)) { return List.of(); } - - Optional securityQuery = qaSecurityService.generateQAEventFilterQuery(context, currentUser, sourceName); SolrQuery solrQuery = new SolrQuery(); solrQuery.setRows(0); - solrQuery.setSort(orderField, ascending ? ORDER.asc : ORDER.desc); - solrQuery.setFacetSort(FacetParams.FACET_SORT_INDEX); + if (orderField != null) { + solrQuery.setSort(orderField, ascending ? ORDER.asc : ORDER.desc); + solrQuery.setFacetSort(FacetParams.FACET_SORT_INDEX); + } + Optional securityQuery = qaSecurityService.generateQAEventFilterQuery(context, + context.getCurrentUser(), source); solrQuery.setQuery(securityQuery.orElse("*:*")); solrQuery.setFacet(true); solrQuery.setFacetMinCount(1); - solrQuery.setFacetLimit((int) (offset + count)); + solrQuery.setFacetLimit((int) (offset + pageSize)); solrQuery.addFacetField(TOPIC); - if (sourceName != null) { - solrQuery.addFilterQuery(SOURCE + ":" + sourceName); + solrQuery.addFilterQuery(SOURCE + ":\"" + source + "\""); + if (target != null) { + solrQuery.addFilterQuery(RESOURCE_UUID + ":" + target.toString()); } QueryResponse response; List topics = new ArrayList<>(); @@ -281,8 +311,9 @@ public List findAllTopicsBySource(Context context, String sourceName, l continue; } QATopic topic = new QATopic(); - topic.setSource(sourceName); + topic.setSource(source); topic.setKey(c.getName()); + topic.setFocus(target); topic.setTotalEvents(c.getCount()); topic.setLastEvent(new Date()); topics.add(topic); @@ -317,13 +348,46 @@ public void store(Context context, QAEvent dto) { updateRequest.process(getSolr()); getSolr().commit(); - sentEmailToAdminAboutNewRequest(dto); + + performAutomaticProcessingIfNeeded(context, dto); } } catch (Exception e) { throw new RuntimeException(e); } } + private void performAutomaticProcessingIfNeeded(Context context, QAEvent qaEvent) { + if (qaAutomaticProcessingMap == null) { + return; + } + QAEventAutomaticProcessingEvaluation evaluation = qaAutomaticProcessingMap.get(qaEvent.getSource()); + + if (evaluation == null) { + return; + } + + AutomaticProcessingAction action = evaluation.evaluateAutomaticProcessing(context, qaEvent); + + if (action == null) { + return; + } + + switch (action) { + case REJECT: + qaEventActionService.reject(context, qaEvent); + break; + case IGNORE: + qaEventActionService.discard(context, qaEvent); + break; + case ACCEPT: + qaEventActionService.accept(context, qaEvent); + break; + default: + throw new IllegalStateException("Unknown automatic action requested " + action); + } + + } + /** * Sends an email notification to the system administrator about a new * Quality Assurance (QA) request event. The email includes details such as the @@ -341,8 +405,8 @@ public void sentEmailToAdminAboutNewRequest(QAEvent qaEvent) { email.addArgument(parsJson(qaEvent.getMessage())); email.send(); } catch (Exception e) { - log.warn("Error during sending email of Withdrawn/Reinstate request for item with uuid:" - + qaEvent.getTarget(), e); + log.warn("Error during sending email of Withdrawn/Reinstate request for item with uuid: {}", + qaEvent.getTarget(), e); } } @@ -352,7 +416,7 @@ private String parsJson(String jsonString) { JsonNode jsonNode = objectMapper.readTree(jsonString); return jsonNode.get("reason").asText(); } catch (Exception e) { - log.warn("Unable to parse the JSON:" + jsonString); + log.warn("Unable to parse the JSON: {}", jsonString); return jsonString; } } @@ -488,7 +552,7 @@ public QASource findSource(Context context, String sourceName, UUID target) { public List findAllSources(Context context, long offset, int pageSize) { return Arrays.stream(getSupportedSources()) .map((sourceName) -> findSource(context, sourceName)) - .filter(Objects::nonNull) + .filter(Objects::nonNull) .sorted(comparing(QASource::getTotalEvents) .reversed()) .skip(offset) @@ -496,6 +560,24 @@ public List findAllSources(Context context, long offset, int pageSize) .collect(Collectors.toList()); } + @Override + public long countSources(Context context) { + return Arrays.stream(getSupportedSources()) + .map((sourceName) -> findSource(context, sourceName)) + .filter(Objects::nonNull) + .filter(source -> source.getTotalEvents() > 0) + .count(); + } + + @Override + public long countSourcesByTarget(Context context, UUID target) { + return Arrays.stream(getSupportedSources()) + .map((sourceName) -> findSource(context, sourceName, target)) + .filter(Objects::nonNull) + .filter(source -> source.getTotalEvents() > 0) + .count(); + } + @Override public boolean isRelatedItemSupported(QAEvent qaevent) { // Currently only PROJECT topics related to OPENAIRE supports related items @@ -512,11 +594,43 @@ private SolrInputDocument createSolrDocument(Context context, QAEvent dto, Strin doc.addField(TRUST, dto.getTrust()); doc.addField(MESSAGE, dto.getMessage()); doc.addField(LAST_UPDATE, new Date()); - doc.addField(RESOURCE_UUID, dto.getTarget()); + String resourceUUID = getResourceUUID(context, dto.getOriginalId()); + if (resourceUUID == null) { + resourceUUID = dto.getTarget(); + /*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 originalId; + } + } + private QAEvent getQAEventFromSOLR(SolrDocument doc) { QAEvent item = new QAEvent(); item.setSource((String) doc.get(SOURCE)); @@ -615,25 +729,8 @@ private boolean isNotSupportedSource(String source) { } private String[] getSupportedSources() { - return configurationService.getArrayProperty(QAEVENTS_SOURCES, new String[] { QAEvent.OPENAIRE_SOURCE }); - } - - @Override - public long countSources(Context context) { - return Arrays.stream(getSupportedSources()) - .map((sourceName) -> findSource(context, sourceName)) - .filter(Objects::nonNull) - .filter(source -> source.getTotalEvents() > 0) - .count(); - } - - @Override - public long countSourcesByTarget(Context context, UUID target) { - return Arrays.stream(getSupportedSources()) - .map((sourceName) -> findSource(context, sourceName, target)) - .filter(Objects::nonNull) - .filter(source -> source.getTotalEvents() > 0) - .count(); + return configurationService.getArrayProperty(QAEVENTS_SOURCES, + new String[] { QAEvent.OPENAIRE_SOURCE, QAEvent.COAR_NOTIFY_SOURCE }); } @Override @@ -673,49 +770,4 @@ public List findAllSourcesByTarget(Context context, UUID target, long .collect(Collectors.toList()); } - @Override - public List findAllTopicsBySourceAndTarget(Context context, String source, UUID target, long offset, - int pageSize, String orderField, boolean ascending) { - var currentUser = context.getCurrentUser(); - if (isNotSupportedSource(source) || !qaSecurityService.canSeeSource(context, currentUser, source)) { - return List.of(); - } - Optional securityQuery = qaSecurityService.generateQAEventFilterQuery(context, currentUser, source); - SolrQuery solrQuery = new SolrQuery(); - solrQuery.setRows(0); - solrQuery.setSort(orderField, ascending ? ORDER.asc : ORDER.desc); - solrQuery.setQuery(securityQuery.orElse("*:*")); - solrQuery.setFacet(true); - solrQuery.setFacetMinCount(1); - solrQuery.setFacetLimit((int) (offset + pageSize)); - solrQuery.addFacetField(TOPIC); - solrQuery.addFilterQuery(SOURCE + ":\"" + source + "\""); - if (target != null) { - solrQuery.addFilterQuery(RESOURCE_UUID + ":" + target.toString()); - } - try { - List topics = new ArrayList<>(); - QueryResponse response = getSolr().query(solrQuery); - FacetField facetField = response.getFacetField(TOPIC); - int idx = 0; - for (Count c : facetField.getValues()) { - if (idx < offset) { - idx++; - continue; - } - QATopic topic = new QATopic(); - topic.setSource(source); - topic.setKey(c.getName()); - topic.setFocus(target); - topic.setTotalEvents(c.getCount()); - topic.setLastEvent(new Date()); - topics.add(topic); - idx++; - } - return topics; - } catch (SolrServerException | IOException e) { - throw new RuntimeException(e); - } - } - } diff --git a/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java b/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java index abb700cb10c9..1d9773f5d617 100644 --- a/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/scripts/ScriptServiceImpl.java @@ -12,19 +12,19 @@ import java.util.List; import java.util.stream.Collectors; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.Context; import org.dspace.kernel.ServiceManager; import org.dspace.scripts.configuration.ScriptConfiguration; import org.dspace.scripts.service.ScriptService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** * The implementation for the {@link ScriptService} */ public class ScriptServiceImpl implements ScriptService { - private static final Logger log = LoggerFactory.getLogger(ScriptServiceImpl.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ServiceManager serviceManager; @@ -48,7 +48,7 @@ public DSpaceRunnable createDSpaceRunnableForScriptConfiguration(ScriptConfigura try { return (DSpaceRunnable) scriptToExecute.getDspaceRunnableClass().getDeclaredConstructor().newInstance(); } catch (InvocationTargetException | NoSuchMethodException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); throw new RuntimeException(e); } } diff --git a/dspace-api/src/main/java/org/dspace/service/impl/ClientInfoServiceImpl.java b/dspace-api/src/main/java/org/dspace/service/impl/ClientInfoServiceImpl.java index e83aa93e3362..3157e95f6bed 100644 --- a/dspace-api/src/main/java/org/dspace/service/impl/ClientInfoServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/service/impl/ClientInfoServiceImpl.java @@ -15,12 +15,12 @@ import com.google.common.net.InetAddresses; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.Utils; import org.dspace.service.ClientInfoService; import org.dspace.services.ConfigurationService; import org.dspace.statistics.util.IPTable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -32,16 +32,16 @@ public class ClientInfoServiceImpl implements ClientInfoService { private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For"; - private static final Logger log = LoggerFactory.getLogger(ClientInfoServiceImpl.class); + private static final Logger log = LogManager.getLogger(); private Boolean useProxiesEnabled; - private ConfigurationService configurationService; + private final ConfigurationService configurationService; /** * Sparse HashTable structure to hold IP address ranges of trusted proxies */ - private IPTable trustedProxies; + private final IPTable trustedProxies; @Autowired(required = true) public ClientInfoServiceImpl(ConfigurationService configurationService) { @@ -84,7 +84,7 @@ public String getClientIp(String remoteIp, String xForwardedForHeaderValue) { public boolean isUseProxiesEnabled() { if (useProxiesEnabled == null) { useProxiesEnabled = configurationService.getBooleanProperty("useProxies", true); - log.info("Proxies (useProxies) enabled? " + useProxiesEnabled); + log.info("Proxies (useProxies) enabled? {}", useProxiesEnabled); } return useProxiesEnabled; @@ -163,6 +163,7 @@ private IPTable parseTrustedProxyRanges() { * @param ipAddress IP address to check for * @return true if trusted, false otherwise */ + @Override public boolean isRequestFromTrustedProxy(String ipAddress) { try { return trustedProxies != null && trustedProxies.contains(ipAddress); @@ -205,15 +206,15 @@ private String getXForwardedForIpValue(String remoteIp, String xForwardedForValu } /** - * Anonymize the given IP address by setting the last specified bytes to 0 - * @param ipAddress the ip address to be anonymize + * Anonymize the given IP address by setting the last specified bytes to 0. + * @param ipAddress the ip address to be anonymized * @param bytes the number of bytes to be set to 0 * @return the modified ip address */ private String anonymizeIpAddress(String ipAddress, int bytes) { if (bytes > 4) { - log.warn("It is not possible to anonymize " + bytes + " bytes of an IPv4 address."); + log.warn("It is not possible to anonymize {} bytes of an IPv4 address.", bytes); return ipAddress; } diff --git a/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetector.java b/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetector.java index c4c5765e0880..20d042d9d993 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetector.java +++ b/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetector.java @@ -13,8 +13,6 @@ import javax.servlet.http.HttpServletRequest; import org.dspace.statistics.factory.StatisticsServiceFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * SpiderDetector delegates static methods to SpiderDetectorService, which is used to find IP's that are spiders... @@ -26,11 +24,9 @@ */ public class SpiderDetector { - private static final Logger log = LoggerFactory.getLogger(SpiderDetector.class); - //Service where all methods get delegated to, this is instantiated by a spring-bean defined in core-services.xml - private static SpiderDetectorService spiderDetectorService = StatisticsServiceFactory.getInstance() - .getSpiderDetectorService(); + private static final SpiderDetectorService spiderDetectorService + = StatisticsServiceFactory.getInstance().getSpiderDetectorService(); /** * Default constructor diff --git a/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetectorServiceImpl.java b/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetectorServiceImpl.java index 0b5149df7451..2f4a3a61e94a 100644 --- a/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetectorServiceImpl.java +++ b/dspace-api/src/main/java/org/dspace/statistics/util/SpiderDetectorServiceImpl.java @@ -21,10 +21,10 @@ import org.apache.commons.configuration2.ex.ConversionException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.service.ClientInfoService; import org.dspace.services.ConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -39,18 +39,18 @@ */ public class SpiderDetectorServiceImpl implements SpiderDetectorService { - private static final Logger log = LoggerFactory.getLogger(SpiderDetectorServiceImpl.class); + private static final Logger log = LogManager.getLogger(); private Boolean useCaseInsensitiveMatching; private final List agents - = Collections.synchronizedList(new ArrayList()); + = Collections.synchronizedList(new ArrayList<>()); private final List domains - = Collections.synchronizedList(new ArrayList()); + = Collections.synchronizedList(new ArrayList<>()); - private ConfigurationService configurationService; - private ClientInfoService clientInfoService; + private final ConfigurationService configurationService; + private final ClientInfoService clientInfoService; /** * Sparse HashTable structure to hold IP address ranges. @@ -63,6 +63,7 @@ public SpiderDetectorServiceImpl(ConfigurationService configurationService, Clie this.clientInfoService = clientInfoService; } + @Override public IPTable getTable() { return table; } @@ -79,6 +80,7 @@ public IPTable getTable() { * @param agent User-Agent header value, or null. * @return true if the client matches any spider characteristics list. */ + @Override public boolean isSpider(String clientIP, String proxyIPs, String hostname, String agent) { // See if any agent patterns match if (null != agent) { @@ -144,6 +146,7 @@ public boolean isSpider(String clientIP, String proxyIPs, String hostname, Strin * @return a vector full of patterns * @throws IOException could not happen since we check the file be4 we use it */ + @Override public Set readPatterns(File patternFile) throws IOException { Set patterns = new HashSet<>(); @@ -191,7 +194,7 @@ private void loadPatterns(String directory, List patternList) { patterns = readPatterns(file); } catch (IOException ex) { log.error("Patterns not read from {}: {}", - file.getPath(), ex.getMessage()); + file::getPath, ex::getMessage); continue; } //If case insensitive matching is enabled, lowercase the patterns so they can be lowercase matched @@ -203,10 +206,10 @@ private void loadPatterns(String directory, List patternList) { } - log.info("Loaded pattern file: {}", file.getPath()); + log.info("Loaded pattern file: {}", file::getPath); } } else { - log.info("No patterns loaded from {}", patternsDir.getPath()); + log.info("No patterns loaded from {}", patternsDir::getPath); } } @@ -216,6 +219,7 @@ private void loadPatterns(String directory, List patternList) { * @param request * @return true|false if the request was detected to be from a spider. */ + @Override public boolean isSpider(HttpServletRequest request) { return isSpider(request.getRemoteAddr(), request.getHeader("X-Forwarded-For"), @@ -229,6 +233,7 @@ public boolean isSpider(HttpServletRequest request) { * @param ip * @return if is spider IP */ + @Override public boolean isSpider(String ip) { if (table == null) { loadSpiderIpAddresses(); @@ -248,6 +253,7 @@ public boolean isSpider(String ip) { /* * loader to populate the table from files. */ + @Override public synchronized void loadSpiderIpAddresses() { if (table == null) { diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java index 7debf3ba449b..939bd4bdd39a 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/RegistryUpdater.java @@ -14,6 +14,8 @@ import javax.xml.transform.TransformerException; import javax.xml.xpath.XPathExpressionException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.administer.MetadataImporter; import org.dspace.administer.RegistryImportException; import org.dspace.administer.RegistryLoader; @@ -24,8 +26,6 @@ import org.dspace.services.factory.DSpaceServicesFactory; import org.flywaydb.core.api.callback.Callback; import org.flywaydb.core.api.callback.Event; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.xml.sax.SAXException; /** @@ -52,7 +52,7 @@ public class RegistryUpdater implements Callback { /** * logging category */ - private static final Logger log = LoggerFactory.getLogger(RegistryUpdater.class); + private static final Logger log = LogManager.getLogger(); /** * Method to actually update our registries from latest configuration files. diff --git a/dspace-api/src/main/java/org/dspace/storage/rdbms/xmlworkflow/V5_0_2014_11_04__Enable_XMLWorkflow_Migration.java b/dspace-api/src/main/java/org/dspace/storage/rdbms/xmlworkflow/V5_0_2014_11_04__Enable_XMLWorkflow_Migration.java index 0361e6805356..d461da4d2db0 100644 --- a/dspace-api/src/main/java/org/dspace/storage/rdbms/xmlworkflow/V5_0_2014_11_04__Enable_XMLWorkflow_Migration.java +++ b/dspace-api/src/main/java/org/dspace/storage/rdbms/xmlworkflow/V5_0_2014_11_04__Enable_XMLWorkflow_Migration.java @@ -14,8 +14,6 @@ import org.dspace.storage.rdbms.migration.MigrationUtils; import org.flywaydb.core.api.migration.BaseJavaMigration; import org.flywaydb.core.api.migration.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * This class automatically migrates your DSpace Database to use the @@ -35,11 +33,6 @@ */ public class V5_0_2014_11_04__Enable_XMLWorkflow_Migration extends BaseJavaMigration { - /** - * logging category - */ - private static final Logger log = LoggerFactory.getLogger(V5_0_2014_11_04__Enable_XMLWorkflow_Migration.class); - // Size of migration script run Integer migration_file_size = -1; diff --git a/dspace-api/src/main/java/org/dspace/usage/TabFileUsageEventListener.java b/dspace-api/src/main/java/org/dspace/usage/TabFileUsageEventListener.java index dc1aeb56c94b..9ed663c39de2 100644 --- a/dspace-api/src/main/java/org/dspace/usage/TabFileUsageEventListener.java +++ b/dspace-api/src/main/java/org/dspace/usage/TabFileUsageEventListener.java @@ -15,16 +15,16 @@ import java.text.SimpleDateFormat; import java.util.Date; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.core.Constants; import org.dspace.services.ConfigurationService; import org.dspace.services.model.Event; import org.dspace.utils.DSpace; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** - * Serialize {@link UsageEvent} data to a file as Tab delimited. In dspace.cfg - * specify the path to the file as the value of + * Serialize {@link UsageEvent} data to a file as Tab delimited. + * In {@code dspace.cfg} specify the path to the file as the value of * {@code usageEvent.tabFileLogger.file}. If that path is not absolute, it * will be interpreted as relative to the directory named in {@code log.dir}. * If no name is configured, it defaults to "usage-events.tsv". If the file is @@ -38,8 +38,7 @@ public class TabFileUsageEventListener /** * log category. */ - private static final Logger errorLog = LoggerFactory - .getLogger(TabFileUsageEventListener.class); + private static final Logger errorLog = LogManager.getLogger(); /** * ISO 8601 Basic string format for record timestamps. @@ -77,11 +76,11 @@ private void init() { try { eventLog = new PrintWriter(new OutputStreamWriter( new FileOutputStream(logFile, true))); - errorLog.debug("Writing to {}", logFile.getAbsolutePath()); + errorLog.debug("Writing to {}", logFile::getAbsolutePath); } catch (FileNotFoundException e) { errorLog.error("{} cannot open file, will not log events: {}", - TabFileUsageEventListener.class.getName(), - e.getMessage()); + TabFileUsageEventListener.class::getName, + e::getMessage); throw new IllegalArgumentException("Cannot open event log file", e); } @@ -104,9 +103,7 @@ public synchronized void receiveEvent(Event event) { init(); } - if (errorLog.isDebugEnabled()) { - errorLog.debug("got: {}", event.toString()); - } + errorLog.debug("got: {}", event::toString); if (!(event instanceof UsageEvent)) { return; diff --git a/dspace-api/src/main/java/org/dspace/util/DSpacePostgreSQLDialect.java b/dspace-api/src/main/java/org/dspace/util/DSpacePostgreSQLDialect.java new file mode 100644 index 000000000000..0da4f14b7719 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/DSpacePostgreSQLDialect.java @@ -0,0 +1,33 @@ +/** + * 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 org.hibernate.dialect.PostgreSQL94Dialect; +import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.type.StandardBasicTypes; + +/** + * PostgreSQL-specific dialect that adds regular expression support as a JPA function. + * @see org.dspace.contentreport.QueryOperator + * @author Jean-François Morin (Université Laval) + */ +public class DSpacePostgreSQLDialect extends PostgreSQL94Dialect { + + public static final String REGEX_MATCHES = "matches"; + public static final String REGEX_IMATCHES = "imatches"; + public static final String REGEX_NOT_MATCHES = "not_matches"; + public static final String REGEX_NOT_IMATCHES = "not_imatches"; + + public DSpacePostgreSQLDialect() { + registerFunction(REGEX_MATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 ~ ?2")); + registerFunction(REGEX_IMATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 ~* ?2")); + registerFunction(REGEX_NOT_MATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 !~ ?2")); + registerFunction(REGEX_NOT_IMATCHES, new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1 !~* ?2")); + } + +} diff --git a/dspace-api/src/main/java/org/dspace/util/FrontendUrlService.java b/dspace-api/src/main/java/org/dspace/util/FrontendUrlService.java index a50baf910e77..d183ec7eace3 100644 --- a/dspace-api/src/main/java/org/dspace/util/FrontendUrlService.java +++ b/dspace-api/src/main/java/org/dspace/util/FrontendUrlService.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.Optional; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.content.Bitstream; import org.dspace.content.Item; import org.dspace.core.Context; @@ -22,18 +24,16 @@ import org.dspace.discovery.SearchService; import org.dspace.discovery.SearchServiceException; import org.dspace.services.ConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** - * Service class for generation of front-end urls. + * Service class for generation of front-end URLs. */ @Component public class FrontendUrlService { - private static final Logger log = LoggerFactory.getLogger(FrontendUrlService.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ConfigurationService configurationService; @@ -80,7 +80,8 @@ private Optional generateUrlWithSearchService(Item item, String uiURLSte } } } catch (SearchServiceException e) { - log.error("Failed getting entitytype through solr for item " + item.getID() + ": " + e.getMessage()); + log.error("Failed getting entitytype through solr for item {}: {}", + item::getID, e::getMessage); } return Optional.empty(); } diff --git a/dspace-api/src/main/java/org/dspace/util/JpaCriteriaBuilderKit.java b/dspace-api/src/main/java/org/dspace/util/JpaCriteriaBuilderKit.java new file mode 100644 index 000000000000..9bb3a0e442f9 --- /dev/null +++ b/dspace-api/src/main/java/org/dspace/util/JpaCriteriaBuilderKit.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.util; + +import javax.persistence.criteria.AbstractQuery; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Root; + +/** + * Data structure containing the required objects to build criteria + * for a JPA query built using the JPA Criteria API. + * The getters match those generated by the JVM when using a record + * so that no API changes will be required when this class gets converted + * into a record when DSpace gets promoted to Java 17 or later. + * @author Jean-François Morin (Université Laval) + */ +// TODO: Convert this data structure into a record when DSpace gets promoted to Java 17 or later +public class JpaCriteriaBuilderKit { + + private CriteriaBuilder criteriaBuilder; + /** Can be a CriteriaQuery as well as a Subquery - both extend AbstractQuery. */ + private AbstractQuery query; + private Root root; + + public JpaCriteriaBuilderKit(CriteriaBuilder criteriaBuilder, AbstractQuery query, + Root root) { + this.criteriaBuilder = criteriaBuilder; + this.query = query; + this.root = root; + } + + public CriteriaBuilder criteriaBuilder() { + return criteriaBuilder; + } + + public AbstractQuery query() { + return query; + } + + public Root root() { + return root; + } + +} diff --git a/dspace-api/src/main/java/org/dspace/util/MultiFormatDateParser.java b/dspace-api/src/main/java/org/dspace/util/MultiFormatDateParser.java index 87dc6d9db846..126daa035332 100644 --- a/dspace-api/src/main/java/org/dspace/util/MultiFormatDateParser.java +++ b/dspace-api/src/main/java/org/dspace/util/MultiFormatDateParser.java @@ -23,9 +23,9 @@ import java.util.regex.PatternSyntaxException; import javax.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.servicemanager.DSpaceKernelInit; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Attempt to parse date strings in a variety of formats. This uses an external @@ -38,7 +38,7 @@ * @author mwood */ public class MultiFormatDateParser { - private static final Logger log = LoggerFactory.getLogger(MultiFormatDateParser.class); + private static final Logger log = LogManager.getLogger(); /** * A list of rules, each binding a regular expression to a date format. @@ -71,7 +71,7 @@ public void setPatterns(Map patterns) { pattern = Pattern.compile(rule.getKey(), Pattern.CASE_INSENSITIVE); } catch (PatternSyntaxException ex) { log.error("Skipping format with unparseable pattern '{}'", - rule.getKey()); + rule::getKey); continue; } @@ -80,7 +80,7 @@ public void setPatterns(Map patterns) { format = new SimpleDateFormat(rule.getValue()); } catch (IllegalArgumentException ex) { log.error("Skipping uninterpretable date format '{}'", - rule.getValue()); + rule::getValue); continue; } format.setCalendar(Calendar.getInstance(UTC_ZONE)); @@ -107,7 +107,7 @@ static public Date parse(String dateString) { } } catch (ParseException ex) { log.info("Date string '{}' matched pattern '{}' but did not parse: {}", - new String[] {dateString, candidate.format.toPattern(), ex.getMessage()}); + () -> dateString, candidate.format::toPattern, ex::getMessage); continue; } return result; diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.02.14__ldn_tables.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.02.14__ldn_tables.sql new file mode 100644 index 000000000000..f5ea59254f66 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2024.02.14__ldn_tables.sql @@ -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/ +-- + +----------------------------------------------------------------------------------- +-- CREATE notifyservice table +----------------------------------------------------------------------------------- + + +CREATE SEQUENCE if NOT EXISTS notifyservice_id_seq; + +CREATE TABLE notifyservice ( + id INTEGER PRIMARY KEY, + name VARCHAR(255), + description TEXT, + url VARCHAR(255), + ldn_url VARCHAR(255), + enabled BOOLEAN NOT NULL, + score NUMERIC(6, 5), + lower_ip VARCHAR(45), + upper_ip VARCHAR(45), + CONSTRAINT ldn_url_unique UNIQUE (ldn_url) +); + +----------------------------------------------------------------------------------- +-- CREATE notifyservice_inbound_pattern_id_seq table +----------------------------------------------------------------------------------- + +CREATE SEQUENCE if NOT EXISTS notifyservice_inbound_pattern_id_seq; + +CREATE TABLE notifyservice_inbound_pattern ( + id INTEGER PRIMARY KEY, + service_id INTEGER REFERENCES notifyservice(id) ON DELETE CASCADE, + pattern VARCHAR(255), + constraint_name VARCHAR(255), + automatic BOOLEAN +); + +CREATE INDEX notifyservice_inbound_idx ON notifyservice_inbound_pattern (service_id); + + +------------------------------------------------------------------------------- +-- Table to store LDN messages +------------------------------------------------------------------------------- + +CREATE TABLE ldn_message +( + id VARCHAR(255) PRIMARY KEY, + object uuid, + message TEXT, + type VARCHAR(255), + origin INTEGER, + target INTEGER, + inReplyTo VARCHAR(255), + context uuid, + activity_stream_type VARCHAR(255), + coar_notify_type VARCHAR(255), + queue_status INTEGER DEFAULT NULL, + queue_attempts INTEGER DEFAULT 0, + queue_last_start_time TIMESTAMP, + queue_timeout TIMESTAMP, + source_ip VARCHAR(45), + FOREIGN KEY (object) REFERENCES dspaceobject (uuid) ON DELETE SET NULL, + FOREIGN KEY (context) REFERENCES dspaceobject (uuid) ON DELETE SET NULL, + FOREIGN KEY (origin) REFERENCES notifyservice (id) ON DELETE SET NULL, + FOREIGN KEY (target) REFERENCES notifyservice (id) ON DELETE SET NULL, + FOREIGN KEY (inReplyTo) REFERENCES ldn_message (id) ON DELETE SET NULL +); + + +------------------------------------------------------------------------------- +-- Table to store notify patterns that will be triggered +------------------------------------------------------------------------------- + +CREATE SEQUENCE if NOT EXISTS notifypatterns_to_trigger_id_seq; + +CREATE TABLE notifypatterns_to_trigger +( + id INTEGER PRIMARY KEY, + item_id UUID REFERENCES Item(uuid) ON DELETE CASCADE, + service_id INTEGER REFERENCES notifyservice(id) ON DELETE CASCADE, + pattern VARCHAR(255) +); + +CREATE INDEX notifypatterns_to_trigger_item_idx ON notifypatterns_to_trigger (item_id); +CREATE INDEX notifypatterns_to_trigger_service_idx ON notifypatterns_to_trigger (service_id); diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.02.14__ldn_tables.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.02.14__ldn_tables.sql new file mode 100644 index 000000000000..f5ea59254f66 --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2024.02.14__ldn_tables.sql @@ -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/ +-- + +----------------------------------------------------------------------------------- +-- CREATE notifyservice table +----------------------------------------------------------------------------------- + + +CREATE SEQUENCE if NOT EXISTS notifyservice_id_seq; + +CREATE TABLE notifyservice ( + id INTEGER PRIMARY KEY, + name VARCHAR(255), + description TEXT, + url VARCHAR(255), + ldn_url VARCHAR(255), + enabled BOOLEAN NOT NULL, + score NUMERIC(6, 5), + lower_ip VARCHAR(45), + upper_ip VARCHAR(45), + CONSTRAINT ldn_url_unique UNIQUE (ldn_url) +); + +----------------------------------------------------------------------------------- +-- CREATE notifyservice_inbound_pattern_id_seq table +----------------------------------------------------------------------------------- + +CREATE SEQUENCE if NOT EXISTS notifyservice_inbound_pattern_id_seq; + +CREATE TABLE notifyservice_inbound_pattern ( + id INTEGER PRIMARY KEY, + service_id INTEGER REFERENCES notifyservice(id) ON DELETE CASCADE, + pattern VARCHAR(255), + constraint_name VARCHAR(255), + automatic BOOLEAN +); + +CREATE INDEX notifyservice_inbound_idx ON notifyservice_inbound_pattern (service_id); + + +------------------------------------------------------------------------------- +-- Table to store LDN messages +------------------------------------------------------------------------------- + +CREATE TABLE ldn_message +( + id VARCHAR(255) PRIMARY KEY, + object uuid, + message TEXT, + type VARCHAR(255), + origin INTEGER, + target INTEGER, + inReplyTo VARCHAR(255), + context uuid, + activity_stream_type VARCHAR(255), + coar_notify_type VARCHAR(255), + queue_status INTEGER DEFAULT NULL, + queue_attempts INTEGER DEFAULT 0, + queue_last_start_time TIMESTAMP, + queue_timeout TIMESTAMP, + source_ip VARCHAR(45), + FOREIGN KEY (object) REFERENCES dspaceobject (uuid) ON DELETE SET NULL, + FOREIGN KEY (context) REFERENCES dspaceobject (uuid) ON DELETE SET NULL, + FOREIGN KEY (origin) REFERENCES notifyservice (id) ON DELETE SET NULL, + FOREIGN KEY (target) REFERENCES notifyservice (id) ON DELETE SET NULL, + FOREIGN KEY (inReplyTo) REFERENCES ldn_message (id) ON DELETE SET NULL +); + + +------------------------------------------------------------------------------- +-- Table to store notify patterns that will be triggered +------------------------------------------------------------------------------- + +CREATE SEQUENCE if NOT EXISTS notifypatterns_to_trigger_id_seq; + +CREATE TABLE notifypatterns_to_trigger +( + id INTEGER PRIMARY KEY, + item_id UUID REFERENCES Item(uuid) ON DELETE CASCADE, + service_id INTEGER REFERENCES notifyservice(id) ON DELETE CASCADE, + pattern VARCHAR(255) +); + +CREATE INDEX notifypatterns_to_trigger_item_idx ON notifypatterns_to_trigger (item_id); +CREATE INDEX notifypatterns_to_trigger_service_idx ON notifypatterns_to_trigger (service_id); diff --git a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml index 921306ca2b56..0212e5efcca1 100644 --- a/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml +++ b/dspace-api/src/test/data/dspaceFolder/config/item-submission.xml @@ -180,6 +180,12 @@ submission + + submit.progressbar.coarnotify + org.dspace.app.rest.submit.step.NotifyStep + coarnotify + + @@ -212,6 +218,9 @@ + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/local.cfg b/dspace-api/src/test/data/dspaceFolder/config/local.cfg index 05a4cc5add01..3dc4e398c11b 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, qaeventsdelete +event.dispatcher.default.consumers = versioning, discovery, eperson, orcidqueue, iiif, qaeventsdelete, ldnmessage ########################################### # 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, qaeventsdelete +event.dispatcher.exclude-discovery.consumers = versioning, eperson, qaeventsdelete, ldnmessage # Configure authority control for Unit Testing (in DSpaceControlledVocabularyTest) # (This overrides default, commented out settings in dspace.cfg) @@ -174,3 +174,13 @@ authority.controlled.dspace.object.owner = true # Configuration required for thorough testing of browse links webui.browse.link.1 = author:dc.contributor.* webui.browse.link.2 = subject:dc.subject.* + + +########################################### +# LDN CONFIGURATIONS # +########################################### +ldn.enabled = true +qaevents.enabled = true +ldn.ip-range.enabled = true +ldn.notify.inbox.block-untrusted = true +ldn.notify.inbox.block-untrusted-ip = true 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 8bae32eaefd7..b1edb2d62259 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 @@ -367,4 +367,12 @@ + + + + + + + diff --git a/dspace-api/src/test/data/dspaceFolder/config/spring/api/qaevents-test.xml b/dspace-api/src/test/data/dspaceFolder/config/spring/api/qaevents-test.xml new file mode 100644 index 000000000000..8738d6cbef33 --- /dev/null +++ b/dspace-api/src/test/data/dspaceFolder/config/spring/api/qaevents-test.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java index 6884b949a66a..e2cd3e040421 100644 --- a/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java +++ b/dspace-api/src/test/java/org/dspace/AbstractIntegrationTestWithDatabase.java @@ -9,7 +9,10 @@ import static org.junit.Assert.fail; +import java.sql.Connection; import java.sql.SQLException; +import java.sql.Statement; +import javax.sql.DataSource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -91,6 +94,14 @@ public static void initDatabase() { try { // Update/Initialize the database to latest version (via Flyway) DatabaseUtils.updateDatabase(); + + // Register custom functions in the H2 database + DataSource dataSource = DSpaceServicesFactory.getInstance() + .getServiceManager() + .getServiceByName("dataSource", DataSource.class); + try (Connection c = dataSource.getConnection(); Statement stmt = c.createStatement()) { + stmt.execute("CREATE ALIAS IF NOT EXISTS matches FOR 'org.dspace.util.DSpaceH2Dialect.matches'"); + } } catch (SQLException se) { log.error("Error initializing database", se); fail("Error initializing database: " + se.getMessage() diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/LDNMessageConsumerIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/LDNMessageConsumerIT.java new file mode 100644 index 000000000000..305261c7c378 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/ldn/LDNMessageConsumerIT.java @@ -0,0 +1,456 @@ +/** + * 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.ldn; + +import static org.dspace.app.ldn.LDNMessageEntity.QUEUE_STATUS_QUEUED; +import static org.dspace.matcher.NotifyServiceEntityMatcher.matchesNotifyServiceEntity; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +import java.io.InputStream; +import java.sql.SQLException; +import java.util.List; +import java.util.Set; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.ldn.factory.NotifyServiceFactory; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.NotifyServiceBuilder; +import org.dspace.builder.NotifyServiceInboundPatternBuilder; +import org.dspace.builder.WorkspaceItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.WorkspaceItem; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.ItemService; +import org.dspace.core.Constants; +import org.dspace.eperson.EPerson; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.workflow.WorkflowItem; +import org.dspace.workflow.WorkflowService; +import org.dspace.workflow.factory.WorkflowServiceFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration Tests against {@link LDNMessageConsumer} + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class LDNMessageConsumerIT extends AbstractIntegrationTestWithDatabase { + + private Collection collection; + private EPerson submitter; + + private LDNMessageService ldnMessageService = NotifyServiceFactory.getInstance().getLDNMessageService(); + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private WorkflowService workflowService = WorkflowServiceFactory.getInstance().getWorkflowService(); + private ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + + @Before + public void setUp() throws Exception { + super.setUp(); + context.turnOffAuthorisationSystem(); + //** GIVEN ** + //1. create a normal user to use as submitter + submitter = EPersonBuilder.createEPerson(context) + .withEmail("submitter@example.com") + .withPassword(password) + .build(); + + //2. A community with one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmitterGroup(submitter) + .build(); + context.setCurrentUser(submitter); + + context.restoreAuthSystemState(); + } + + @Test + public void testLDNMessageConsumerRequestReview() throws Exception { + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-review") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + + assertThat(notifyService, matchesNotifyServiceEntity(ldnMessage.getTarget())); + assertEquals(workflowItem.getItem().getID(), ldnMessage.getObject().getID()); + assertEquals(QUEUE_STATUS_QUEUED, ldnMessage.getQueueStatus()); + assertNull(ldnMessage.getOrigin()); + assertNotNull(ldnMessage.getMessage()); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + // check id + assertThat(notification.getId(), containsString("urn:uuid:")); + + // check object + assertEquals(notification.getObject().getId(), + configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle()); + assertEquals(notification.getObject().getIetfCiteAs(), + itemService.getMetadataByMetadataString(item, "dc.identifier.uri").get(0).getValue()); + assertEquals(notification.getObject().getUrl().getId(), + configurationService.getProperty("dspace.ui.url") + "/bitstreams/" + + item.getBundles(Constants.CONTENT_BUNDLE_NAME).get(0).getBitstreams().get(0).getID() + "/download"); + + // check target + assertEquals(notification.getTarget().getId(), notifyService.getUrl()); + assertEquals(notification.getTarget().getInbox(), notifyService.getLdnUrl()); + assertEquals(notification.getTarget().getType(), Set.of("Service")); + + // check origin + assertEquals(notification.getOrigin().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getOrigin().getInbox(), configurationService.getProperty("ldn.notify.inbox")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check actor + assertEquals(notification.getActor().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getActor().getName(), configurationService.getProperty("dspace.name")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check types + assertEquals(notification.getType(), Set.of("coar-notify:ReviewAction", "Offer")); + + } + + @Test + public void testLDNMessageConsumerRequestReviewAutomatic() throws Exception { + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyService) + .withPattern("request-review") + .withConstraint("simple-demo_filter") + .isAutomatic(true) + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("demo Item") + .withIssueDate("2023-11-20") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + + assertThat(notifyService, matchesNotifyServiceEntity(ldnMessage.getTarget())); + assertEquals(workflowItem.getItem().getID(), ldnMessage.getObject().getID()); + assertEquals(QUEUE_STATUS_QUEUED, ldnMessage.getQueueStatus()); + assertNull(ldnMessage.getOrigin()); + assertNotNull(ldnMessage.getMessage()); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + // check id + assertThat(notification.getId(), containsString("urn:uuid:")); + + // check object + assertEquals(notification.getObject().getId(), + configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle()); + assertEquals(notification.getObject().getIetfCiteAs(), + itemService.getMetadataByMetadataString(item, "dc.identifier.uri").get(0).getValue()); + assertEquals(notification.getObject().getUrl().getId(), + configurationService.getProperty("dspace.ui.url") + "/bitstreams/" + + item.getBundles(Constants.CONTENT_BUNDLE_NAME).get(0).getBitstreams().get(0).getID() + "/download"); + + // check target + assertEquals(notification.getTarget().getId(), notifyService.getUrl()); + assertEquals(notification.getTarget().getInbox(), notifyService.getLdnUrl()); + assertEquals(notification.getTarget().getType(), Set.of("Service")); + + // check origin + assertEquals(notification.getOrigin().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getOrigin().getInbox(), configurationService.getProperty("ldn.notify.inbox")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check actor + assertEquals(notification.getActor().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getActor().getName(), configurationService.getProperty("dspace.name")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check types + assertEquals(notification.getType(), Set.of("coar-notify:ReviewAction", "Offer")); + + } + + @Test + public void testLDNMessageConsumerRequestEndorsement() throws Exception { + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-endorsement") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + + assertThat(notifyService, matchesNotifyServiceEntity(ldnMessage.getTarget())); + assertEquals(workflowItem.getItem().getID(), ldnMessage.getObject().getID()); + assertEquals(QUEUE_STATUS_QUEUED, ldnMessage.getQueueStatus()); + assertNull(ldnMessage.getOrigin()); + assertNotNull(ldnMessage.getMessage()); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + // check id + assertThat(notification.getId(), containsString("urn:uuid:")); + + // check object + assertEquals(notification.getObject().getId(), + configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle()); + assertEquals(notification.getObject().getIetfCiteAs(), + itemService.getMetadataByMetadataString(item, "dc.identifier.uri").get(0).getValue()); + assertEquals(notification.getObject().getUrl().getId(), + configurationService.getProperty("dspace.ui.url") + "/bitstreams/" + + item.getBundles(Constants.CONTENT_BUNDLE_NAME).get(0).getBitstreams().get(0).getID() + "/download"); + + // check target + assertEquals(notification.getTarget().getId(), notifyService.getUrl()); + assertEquals(notification.getTarget().getInbox(), notifyService.getLdnUrl()); + assertEquals(notification.getTarget().getType(), Set.of("Service")); + + // check origin + assertEquals(notification.getOrigin().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getOrigin().getInbox(), configurationService.getProperty("ldn.notify.inbox")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check actor + assertEquals(notification.getActor().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getActor().getName(), configurationService.getProperty("dspace.name")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check types + assertEquals(notification.getType(), Set.of("coar-notify:EndorsementAction", "Offer")); + + } + + @Test + public void testLDNMessageConsumerRequestIngest() throws Exception { + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-ingest") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + + assertThat(notifyService, matchesNotifyServiceEntity(ldnMessage.getTarget())); + assertEquals(workflowItem.getItem().getID(), ldnMessage.getObject().getID()); + assertEquals(QUEUE_STATUS_QUEUED, ldnMessage.getQueueStatus()); + assertNull(ldnMessage.getOrigin()); + assertNotNull(ldnMessage.getMessage()); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + // check id + assertThat(notification.getId(), containsString("urn:uuid:")); + + // check object + assertEquals(notification.getObject().getId(), + configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle()); + assertEquals(notification.getObject().getIetfCiteAs(), + itemService.getMetadataByMetadataString(item, "dc.identifier.uri").get(0).getValue()); + assertEquals(notification.getObject().getUrl().getId(), + configurationService.getProperty("dspace.ui.url") + "/bitstreams/" + + item.getBundles(Constants.CONTENT_BUNDLE_NAME).get(0).getBitstreams().get(0).getID() + "/download"); + + // check target + assertEquals(notification.getTarget().getId(), notifyService.getUrl()); + assertEquals(notification.getTarget().getInbox(), notifyService.getLdnUrl()); + assertEquals(notification.getTarget().getType(), Set.of("Service")); + + // check origin + assertEquals(notification.getOrigin().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getOrigin().getInbox(), configurationService.getProperty("ldn.notify.inbox")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check actor + assertEquals(notification.getActor().getId(), configurationService.getProperty("dspace.ui.url")); + assertEquals(notification.getActor().getName(), configurationService.getProperty("dspace.name")); + assertEquals(notification.getOrigin().getType(), Set.of("Service")); + + // check types + assertEquals(notification.getType(), Set.of("coar-notify:IngestAction", "Offer")); + + } + + @Test + public void testLDNMessageConsumerRequestFake() throws Exception { + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-fake") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + workflowService.start(context, workspaceItem); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + assertNull(ldnMessage); + + } + + @Test + public void testLDNMessageConsumerNoRequests() throws Exception { + context.turnOffAuthorisationSystem(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .grantLicense() + .build(); + + workflowService.start(context, workspaceItem); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + assertNull(ldnMessage); + } + + @Override + @After + public void destroy() throws Exception { + List ldnMessageEntities = ldnMessageService.findAll(context); + if (CollectionUtils.isNotEmpty(ldnMessageEntities)) { + ldnMessageEntities.forEach(ldnMessage -> { + try { + ldnMessageService.delete(context, ldnMessage); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + super.destroy(); + } +} + diff --git a/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java new file mode 100644 index 000000000000..b1a6db4a0729 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/ldn/action/SendLDNMessageActionIT.java @@ -0,0 +1,255 @@ +/** + * 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.ldn.action; + +import static org.dspace.app.ldn.action.LDNActionStatus.ABORT; +import static org.dspace.app.ldn.action.LDNActionStatus.CONTINUE; +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; +import java.sql.SQLException; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.http.HttpStatus; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicStatusLine; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.factory.NotifyServiceFactory; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.EPersonBuilder; +import org.dspace.builder.NotifyServiceBuilder; +import org.dspace.builder.WorkspaceItemBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.WorkspaceItem; +import org.dspace.eperson.EPerson; +import org.dspace.services.ConfigurationService; +import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.workflow.WorkflowItem; +import org.dspace.workflow.WorkflowService; +import org.dspace.workflow.factory.WorkflowServiceFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Integration Tests against {@link SendLDNMessageAction} + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class SendLDNMessageActionIT extends AbstractIntegrationTestWithDatabase { + + private Collection collection; + private EPerson submitter; + private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private LDNMessageService ldnMessageService = NotifyServiceFactory.getInstance().getLDNMessageService(); + private WorkflowService workflowService = WorkflowServiceFactory.getInstance().getWorkflowService(); + private SendLDNMessageAction sendLDNMessageAction; + + @Before + public void setUp() throws Exception { + super.setUp(); + configurationService.setProperty("ldn.enabled", "true"); + sendLDNMessageAction = new SendLDNMessageAction(); + context.turnOffAuthorisationSystem(); + //** GIVEN ** + //1. create a normal user to use as submitter + submitter = EPersonBuilder.createEPerson(context) + .withEmail("submitter@example.com") + .withPassword(password) + .build(); + + //2. A community with one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .withSubmitterGroup(submitter) + .build(); + context.setCurrentUser(submitter); + + context.restoreAuthSystemState(); + } + + @Test + public void testLDNMessageConsumerRequestReview() throws Exception { + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine sl = mock(BasicStatusLine.class); + when(response.getStatusLine()).thenReturn(sl); + when(sl.getStatusCode()).thenReturn(HttpStatus.SC_ACCEPTED); + CloseableHttpClient mockedClient = mock(CloseableHttpClient.class); + when(mockedClient.execute(any(HttpPost.class))). + thenReturn(response); + ObjectMapper mapper = new ObjectMapper(); + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://www.notify-inbox.info/") + .withLdnUrl("https://notify-inbox.info/inbox/") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-review") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + ldnMessage.getQueueStatus(); + + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + sendLDNMessageAction = new SendLDNMessageAction(mockedClient); + assertEquals(sendLDNMessageAction.execute(context, notification, item), CONTINUE); + mockedClient.close(); + response.close(); + } + + @Test + public void testLDNMessageConsumerRequestReviewGotRedirection() throws Exception { + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine sl = mock(BasicStatusLine.class); + when(response.getStatusLine()).thenReturn(sl); + when(sl.getStatusCode()).thenReturn(HttpStatus.SC_ACCEPTED); + CloseableHttpClient mockedClient = mock(CloseableHttpClient.class); + when(mockedClient.execute(any(HttpPost.class))). + thenReturn(response); + ObjectMapper mapper = new ObjectMapper(); + + context.turnOffAuthorisationSystem(); + + // ldnUrl should be https://notify-inbox.info/inbox/ + // but used https://notify-inbox.info/inbox for redirection + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://www.notify-inbox.info/") + .withLdnUrl("https://notify-inbox.info/inbox") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-review") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + sendLDNMessageAction = new SendLDNMessageAction(mockedClient); + assertEquals(sendLDNMessageAction.execute(context, notification, item), CONTINUE); + mockedClient.close(); + response.close(); + } + + @Test + public void testLDNMessageConsumerRequestReviewWithInvalidLdnUrl() throws Exception { + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine sl = mock(BasicStatusLine.class); + when(response.getStatusLine()).thenReturn(sl); + when(sl.getStatusCode()).thenReturn(HttpStatus.SC_NOT_FOUND); + CloseableHttpClient mockedClient = mock(CloseableHttpClient.class); + when(mockedClient.execute(any(HttpPost.class))). + thenReturn(response); + ObjectMapper mapper = new ObjectMapper(); + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyService = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://www.notify-inbox.info/") + .withLdnUrl("https://notify-inbox.info/invalidLdnUrl/") + .build(); + + //3. a workspace item ready to go + WorkspaceItem workspaceItem = + WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Submission Item") + .withIssueDate("2023-11-20") + .withCOARNotifyService(notifyService, "request-review") + .withFulltext("test.txt", "test", InputStream.nullInputStream()) + .grantLicense() + .build(); + + WorkflowItem workflowItem = workflowService.start(context, workspaceItem); + Item item = workflowItem.getItem(); + context.dispatchEvents(); + context.restoreAuthSystemState(); + + LDNMessageEntity ldnMessage = + ldnMessageService.findAll(context).stream().findFirst().orElse(null); + + Notification notification = mapper.readValue(ldnMessage.getMessage(), Notification.class); + sendLDNMessageAction = new SendLDNMessageAction(mockedClient); + assertEquals(sendLDNMessageAction.execute(context, notification, item), ABORT); + mockedClient.close(); + response.close(); + } + + @Override + @After + public void destroy() throws Exception { + List ldnMessageEntities = ldnMessageService.findAll(context); + if (CollectionUtils.isNotEmpty(ldnMessageEntities)) { + ldnMessageEntities.forEach(ldnMessage -> { + try { + ldnMessageService.delete(context, ldnMessage); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + super.destroy(); + } +} + diff --git a/dspace-api/src/test/java/org/dspace/app/mediafilter/MediaFilterIT.java b/dspace-api/src/test/java/org/dspace/app/mediafilter/MediaFilterIT.java new file mode 100644 index 000000000000..aef2476fdc45 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/app/mediafilter/MediaFilterIT.java @@ -0,0 +1,237 @@ +/** + * 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.mediafilter; + +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.SQLException; +import java.util.Iterator; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.dspace.AbstractIntegrationTestWithDatabase; +import org.dspace.authorize.AuthorizeException; +import org.dspace.builder.BitstreamBuilder; +import org.dspace.builder.CollectionBuilder; +import org.dspace.builder.CommunityBuilder; +import org.dspace.builder.ItemBuilder; +import org.dspace.content.Bitstream; +import org.dspace.content.Bundle; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.DSpaceObject; +import org.dspace.content.Item; +import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamService; +import org.dspace.content.service.ItemService; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests of {@link MediaFilterScript}. + * + * @author Andrea Bollini + */ +public class MediaFilterIT extends AbstractIntegrationTestWithDatabase { + + private ItemService itemService = ContentServiceFactory.getInstance().getItemService(); + private BitstreamService bitstreamService = ContentServiceFactory.getInstance().getBitstreamService(); + protected Community topComm1; + protected Community topComm2; + protected Community childComm1_1; + protected Community childComm1_2; + protected Collection col1_1; + protected Collection col1_2; + protected Collection col1_1_1; + protected Collection col1_1_2; + protected Collection col1_2_1; + protected Collection col1_2_2; + protected Collection col2_1; + protected Item item1_1_a; + protected Item item1_1_b; + protected Item item1_2_a; + protected Item item1_2_b; + protected Item item1_1_1_a; + protected Item item1_1_1_b; + protected Item item1_1_2_a; + protected Item item1_1_2_b; + protected Item item1_2_1_a; + protected Item item1_2_1_b; + protected Item item1_2_2_a; + protected Item item1_2_2_b; + protected Item item2_1_a; + protected Item item2_1_b; + + @Before + public void setup() throws IOException, SQLException, AuthorizeException { + context.turnOffAuthorisationSystem(); + topComm1 = CommunityBuilder.createCommunity(context).withName("Parent Community1").build(); + topComm2 = CommunityBuilder.createCommunity(context).withName("Parent Community2").build(); + childComm1_1 = CommunityBuilder.createCommunity(context).withName("Child Community1_1") + .addParentCommunity(context, topComm1).build(); + childComm1_2 = CommunityBuilder.createCommunity(context).withName("Child Community1_2") + .addParentCommunity(context, topComm1).build(); + col1_1 = CollectionBuilder.createCollection(context, topComm1).withName("Collection 1_1").build(); + col1_2 = CollectionBuilder.createCollection(context, topComm1).withName("Collection 1_2").build(); + col1_1_1 = CollectionBuilder.createCollection(context, childComm1_1).withName("Collection 1_1_1").build(); + col1_1_2 = CollectionBuilder.createCollection(context, childComm1_1).withName("Collection 1_1_2").build(); + col1_2_1 = CollectionBuilder.createCollection(context, childComm1_2).withName("Collection 1_1_1").build(); + col1_2_2 = CollectionBuilder.createCollection(context, childComm1_2).withName("Collection 1_2").build(); + col2_1 = CollectionBuilder.createCollection(context, topComm2).withName("Collection 2_1").build(); + + // Create two items in each collection, one with the test.csv file and one with the test.txt file + item1_1_a = ItemBuilder.createItem(context, col1_1).withTitle("Item 1_1_a").withIssueDate("2017-10-17").build(); + item1_1_b = ItemBuilder.createItem(context, col1_1).withTitle("Item 1_1_b").withIssueDate("2017-10-17").build(); + item1_1_1_a = ItemBuilder.createItem(context, col1_1_1).withTitle("Item 1_1_1_a").withIssueDate("2017-10-17") + .build(); + item1_1_1_b = ItemBuilder.createItem(context, col1_1_1).withTitle("Item 1_1_1_b").withIssueDate("2017-10-17") + .build(); + item1_1_2_a = ItemBuilder.createItem(context, col1_1_2).withTitle("Item 1_1_2_a").withIssueDate("2017-10-17") + .build(); + item1_1_2_b = ItemBuilder.createItem(context, col1_1_2).withTitle("Item 1_1_2_b").withIssueDate("2017-10-17") + .build(); + item1_2_a = ItemBuilder.createItem(context, col1_2).withTitle("Item 1_2_a").withIssueDate("2017-10-17").build(); + item1_2_b = ItemBuilder.createItem(context, col1_2).withTitle("Item 1_2_b").withIssueDate("2017-10-17").build(); + item1_2_1_a = ItemBuilder.createItem(context, col1_2_1).withTitle("Item 1_2_1_a").withIssueDate("2017-10-17") + .build(); + item1_2_1_b = ItemBuilder.createItem(context, col1_2_1).withTitle("Item 1_2_1_b").withIssueDate("2017-10-17") + .build(); + item1_2_2_a = ItemBuilder.createItem(context, col1_2_2).withTitle("Item 1_2_2_a").withIssueDate("2017-10-17") + .build(); + item1_2_2_b = ItemBuilder.createItem(context, col1_2_2).withTitle("Item 1_2_2_b").withIssueDate("2017-10-17") + .build(); + item2_1_a = ItemBuilder.createItem(context, col2_1).withTitle("Item 2_1_a").withIssueDate("2017-10-17").build(); + item2_1_b = ItemBuilder.createItem(context, col2_1).withTitle("Item 2_1_b").withIssueDate("2017-10-17").build(); + addBitstream(item1_1_a, "test.csv"); + addBitstream(item1_1_b, "test.txt"); + addBitstream(item1_2_a, "test.csv"); + addBitstream(item1_2_b, "test.txt"); + addBitstream(item1_1_1_a, "test.csv"); + addBitstream(item1_1_1_b, "test.txt"); + addBitstream(item1_1_2_a, "test.csv"); + addBitstream(item1_1_2_b, "test.txt"); + addBitstream(item1_2_1_a, "test.csv"); + addBitstream(item1_2_1_b, "test.txt"); + addBitstream(item1_2_2_a, "test.csv"); + addBitstream(item1_2_2_b, "test.txt"); + addBitstream(item2_1_a, "test.csv"); + addBitstream(item2_1_b, "test.txt"); + context.restoreAuthSystemState(); + } + + private void addBitstream(Item item, String filename) throws SQLException, AuthorizeException, IOException { + BitstreamBuilder.createBitstream(context, item, getClass().getResourceAsStream(filename)).withName(filename) + .guessFormat().build(); + } + + @Test + public void mediaFilterScriptAllItemsTest() throws Exception { + performMediaFilterScript(null); + Iterator items = itemService.findAll(context); + while (items.hasNext()) { + Item item = items.next(); + checkItemHasBeenProcessed(item); + } + } + + @Test + public void mediaFilterScriptIdentifiersTest() throws Exception { + // process the item 1_1_a and verify that no other items has been processed using the "closer" one + performMediaFilterScript(item1_1_a); + checkItemHasBeenProcessed(item1_1_a); + checkItemHasBeenNotProcessed(item1_1_b); + // process the collection 1_1_1 and verify that items in another collection has not been processed + performMediaFilterScript(col1_1_1); + checkItemHasBeenProcessed(item1_1_1_a); + checkItemHasBeenProcessed(item1_1_1_b); + checkItemHasBeenNotProcessed(item1_1_2_a); + checkItemHasBeenNotProcessed(item1_1_2_b); + // process a top community with only collections + performMediaFilterScript(topComm2); + checkItemHasBeenProcessed(item2_1_a); + checkItemHasBeenProcessed(item2_1_b); + // verify that the other items have not been processed yet + checkItemHasBeenNotProcessed(item1_1_b); + checkItemHasBeenNotProcessed(item1_2_a); + checkItemHasBeenNotProcessed(item1_2_b); + checkItemHasBeenNotProcessed(item1_1_2_a); + checkItemHasBeenNotProcessed(item1_1_2_b); + checkItemHasBeenNotProcessed(item1_2_1_a); + checkItemHasBeenNotProcessed(item1_2_1_b); + checkItemHasBeenNotProcessed(item1_2_2_a); + checkItemHasBeenNotProcessed(item1_2_2_b); + // process a more structured community and verify that all the items at all levels are processed + performMediaFilterScript(topComm1); + // items that were already processed should stay processed + checkItemHasBeenProcessed(item1_1_a); + checkItemHasBeenProcessed(item1_1_1_a); + checkItemHasBeenProcessed(item1_1_1_b); + // residual items should have been processed as well now + checkItemHasBeenProcessed(item1_1_b); + checkItemHasBeenProcessed(item1_2_a); + checkItemHasBeenProcessed(item1_2_b); + checkItemHasBeenProcessed(item1_1_2_a); + checkItemHasBeenProcessed(item1_1_2_b); + checkItemHasBeenProcessed(item1_2_1_a); + checkItemHasBeenProcessed(item1_2_1_b); + checkItemHasBeenProcessed(item1_2_2_a); + checkItemHasBeenProcessed(item1_2_2_b); + } + + private void checkItemHasBeenNotProcessed(Item item) throws IOException, SQLException, AuthorizeException { + List textBundles = item.getBundles("TEXT"); + assertTrue("The item " + item.getName() + " should NOT have the TEXT bundle", textBundles.size() == 0); + } + + private void checkItemHasBeenProcessed(Item item) throws IOException, SQLException, AuthorizeException { + String expectedFileName = StringUtils.endsWith(item.getName(), "_a") ? "test.csv.txt" : "test.txt.txt"; + String expectedContent = StringUtils.endsWith(item.getName(), "_a") ? "data3,3" : "quick brown fox"; + List textBundles = item.getBundles("TEXT"); + assertTrue("The item " + item.getName() + " has NOT the TEXT bundle", textBundles.size() == 1); + List bitstreams = textBundles.get(0).getBitstreams(); + assertTrue("The item " + item.getName() + " has NOT exactly 1 bitstream in the TEXT bundle", + bitstreams.size() == 1); + assertTrue("The text bistream in the " + item.getName() + " is NOT named properly [" + expectedFileName + "]", + StringUtils.equals(bitstreams.get(0).getName(), expectedFileName)); + assertTrue("The text bistream in the " + item.getName() + " doesn't contain the proper content [" + + expectedContent + "]", StringUtils.contains(getContent(bitstreams.get(0)), expectedContent)); + } + + private CharSequence getContent(Bitstream bitstream) throws IOException, SQLException, AuthorizeException { + try (InputStream input = bitstreamService.retrieve(context, bitstream)) { + return IOUtils.toString(input, "UTF-8"); + } + } + + private void performMediaFilterScript(DSpaceObject dso) throws Exception { + if (dso != null) { + runDSpaceScript("filter-media", "-i", dso.getHandle()); + } else { + runDSpaceScript("filter-media"); + } + // reload our items to see the changes + item1_1_a = context.reloadEntity(item1_1_a); + item1_1_b = context.reloadEntity(item1_1_b); + item1_2_a = context.reloadEntity(item1_2_a); + item1_2_b = context.reloadEntity(item1_2_b); + item1_1_1_a = context.reloadEntity(item1_1_1_a); + item1_1_1_b = context.reloadEntity(item1_1_1_b); + item1_1_2_a = context.reloadEntity(item1_1_2_a); + item1_1_2_b = context.reloadEntity(item1_1_2_b); + item1_2_1_a = context.reloadEntity(item1_2_1_a); + item1_2_1_b = context.reloadEntity(item1_2_1_b); + item1_2_2_a = context.reloadEntity(item1_2_2_a); + item1_2_2_b = context.reloadEntity(item1_2_2_b); + item2_1_a = context.reloadEntity(item2_1_a); + item2_1_b = context.reloadEntity(item2_1_b); + + } +} 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 013a18cd526a..c67963f203ac 100644 --- a/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/AbstractBuilder.java @@ -14,6 +14,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.dspace.alerts.service.SystemWideAlertService; +import org.dspace.app.ldn.factory.NotifyServiceFactory; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; import org.dspace.app.requestitem.factory.RequestItemServiceFactory; import org.dspace.app.requestitem.service.RequestItemService; import org.dspace.app.suggestion.SolrSuggestionStorageService; @@ -116,6 +120,10 @@ public abstract class AbstractBuilder { static SubmissionConfigService submissionConfigService; static SubscribeService subscribeService; static SupervisionOrderService supervisionOrderService; + static NotifyService notifyService; + static NotifyServiceInboundPatternService inboundPatternService; + static NotifyPatternToTriggerService notifyPatternToTriggerService; + static QAEventService qaEventService; static SolrSuggestionStorageService solrSuggestionService; @@ -186,6 +194,9 @@ public static void init() { } subscribeService = ContentServiceFactory.getInstance().getSubscribeService(); supervisionOrderService = SupervisionOrderServiceFactory.getInstance().getSupervisionOrderService(); + notifyService = NotifyServiceFactory.getInstance().getNotifyService(); + inboundPatternService = NotifyServiceFactory.getInstance().getNotifyServiceInboundPatternService(); + notifyPatternToTriggerService = NotifyServiceFactory.getInstance().getNotifyPatternToTriggerService(); qaEventService = new DSpace().getSingletonService(QAEventService.class); solrSuggestionService = new DSpace().getSingletonService(SolrSuggestionStorageService.class); } @@ -225,6 +236,9 @@ public static void destroy() { submissionConfigService = null; subscribeService = null; supervisionOrderService = null; + notifyService = null; + inboundPatternService = null; + notifyPatternToTriggerService = null; qaEventService = null; } diff --git a/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java b/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java index 08045325b8a5..96d404c422ed 100644 --- a/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/BitstreamBuilder.java @@ -21,6 +21,7 @@ import org.dspace.content.MetadataField; import org.dspace.content.MetadataValue; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.content.service.BitstreamFormatService; import org.dspace.content.service.DSpaceObjectService; import org.dspace.content.service.MetadataValueService; import org.dspace.core.Constants; @@ -168,6 +169,19 @@ public BitstreamBuilder withMimeType(String mimeType) throws SQLException { return this; } + /** + * Guess the bitstream format as during the submission via the + * {@link BitstreamFormatService#guessFormat(Context, Bitstream)} + * + * @return the BitstreamBuilder with the format set according to + * {@link BitstreamFormatService#guessFormat(Context, Bitstream)} + * @throws SQLException + */ + public BitstreamBuilder guessFormat() throws SQLException { + bitstream.setFormat(context, bitstreamFormatService.guessFormat(context, bitstream)); + return this; + } + public BitstreamBuilder withFormat(String format) throws SQLException { bitstreamService.addMetadata(context, bitstream, "dc", "format", null, null, format); diff --git a/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java new file mode 100644 index 000000000000..9c8de62d8bcc --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java @@ -0,0 +1,149 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.builder; + +import java.math.BigDecimal; +import java.sql.SQLException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; + +/** + * Builder for {@link NotifyServiceEntity} entities. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + * + */ +public class NotifyServiceBuilder extends AbstractBuilder { + + /* Log4j logger*/ + private static final Logger log = LogManager.getLogger(); + + private NotifyServiceEntity notifyServiceEntity; + + protected NotifyServiceBuilder(Context context) { + super(context); + } + + @Override + protected NotifyService getService() { + return notifyService; + } + + @Override + public void cleanup() throws Exception { + try (Context c = new Context()) { + c.setDispatcher("noindex"); + c.turnOffAuthorisationSystem(); + // Ensure object and any related objects are reloaded before checking to see what needs cleanup + notifyServiceEntity = c.reloadEntity(notifyServiceEntity); + if (notifyServiceEntity != null) { + delete(notifyServiceEntity); + } + c.complete(); + indexingService.commit(); + } + } + + @Override + public void delete(Context c, NotifyServiceEntity notifyServiceEntity) throws Exception { + if (notifyServiceEntity != null) { + getService().delete(c, notifyServiceEntity); + } + } + + @Override + public NotifyServiceEntity build() { + try { + + notifyService.update(context, notifyServiceEntity); + context.dispatchEvents(); + + indexingService.commit(); + } catch (SearchServiceException | SQLException e) { + log.error(e); + } + return notifyServiceEntity; + } + + public void delete(NotifyServiceEntity notifyServiceEntity) throws Exception { + try (Context c = new Context()) { + c.turnOffAuthorisationSystem(); + NotifyServiceEntity nsEntity = c.reloadEntity(notifyServiceEntity); + if (nsEntity != null) { + getService().delete(c, nsEntity); + } + c.complete(); + } + + indexingService.commit(); + } + + public static NotifyServiceBuilder createNotifyServiceBuilder(Context context) { + NotifyServiceBuilder notifyServiceBuilder = new NotifyServiceBuilder(context); + return notifyServiceBuilder.create(context); + } + + private NotifyServiceBuilder create(Context context) { + try { + + this.context = context; + this.notifyServiceEntity = notifyService.create(context); + + } catch (SQLException e) { + log.warn("Failed to create the NotifyService", e); + } + + return this; + } + + public NotifyServiceBuilder withName(String name) { + notifyServiceEntity.setName(name); + return this; + } + + public NotifyServiceBuilder withDescription(String description) { + notifyServiceEntity.setDescription(description); + return this; + } + + public NotifyServiceBuilder withUrl(String url) { + notifyServiceEntity.setUrl(url); + return this; + } + + public NotifyServiceBuilder withLdnUrl(String ldnUrl) { + notifyServiceEntity.setLdnUrl(ldnUrl); + return this; + } + + public NotifyServiceBuilder withScore(BigDecimal score) { + notifyServiceEntity.setScore(score); + return this; + } + + public NotifyServiceBuilder isEnabled(boolean enabled) { + notifyServiceEntity.setEnabled(enabled); + return this; + } + + public NotifyServiceBuilder withLowerIp(String lowerIp) { + notifyServiceEntity.setLowerIp(lowerIp); + return this; + } + + public NotifyServiceBuilder withUpperIp(String upperIp) { + notifyServiceEntity.setUpperIp(upperIp); + return this; + } + +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/builder/NotifyServiceInboundPatternBuilder.java b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceInboundPatternBuilder.java new file mode 100644 index 000000000000..5ae20b00016c --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceInboundPatternBuilder.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.builder; + +import java.sql.SQLException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.core.Context; +import org.dspace.discovery.SearchServiceException; + +/** + * Builder for {@link NotifyServiceInboundPattern} entities. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + * + */ +public class NotifyServiceInboundPatternBuilder + extends AbstractBuilder { + + /* Log4j logger*/ + private static final Logger log = LogManager.getLogger(); + + private NotifyServiceInboundPattern notifyServiceInboundPattern; + + protected NotifyServiceInboundPatternBuilder(Context context) { + super(context); + } + + @Override + protected NotifyServiceInboundPatternService getService() { + return inboundPatternService; + } + + @Override + public void cleanup() throws Exception { + try (Context c = new Context()) { + c.setDispatcher("noindex"); + c.turnOffAuthorisationSystem(); + // Ensure object and any related objects are reloaded before checking to see what needs cleanup + notifyServiceInboundPattern = c.reloadEntity(notifyServiceInboundPattern); + if (notifyServiceInboundPattern != null) { + delete(notifyServiceInboundPattern); + } + c.complete(); + indexingService.commit(); + } + } + + @Override + public void delete(Context c, NotifyServiceInboundPattern notifyServiceInboundPattern) throws Exception { + if (notifyServiceInboundPattern != null) { + getService().delete(c, notifyServiceInboundPattern); + } + } + + @Override + public NotifyServiceInboundPattern build() { + try { + + inboundPatternService.update(context, notifyServiceInboundPattern); + context.dispatchEvents(); + + indexingService.commit(); + } catch (SearchServiceException | SQLException e) { + log.error(e); + } + return notifyServiceInboundPattern; + } + + public void delete(NotifyServiceInboundPattern notifyServiceInboundPattern) throws Exception { + try (Context c = new Context()) { + c.turnOffAuthorisationSystem(); + NotifyServiceInboundPattern nsEntity = c.reloadEntity(notifyServiceInboundPattern); + if (nsEntity != null) { + getService().delete(c, nsEntity); + } + c.complete(); + } + + indexingService.commit(); + } + + public static NotifyServiceInboundPatternBuilder createNotifyServiceInboundPatternBuilder( + Context context, NotifyServiceEntity service) { + NotifyServiceInboundPatternBuilder notifyServiceBuilder = new NotifyServiceInboundPatternBuilder(context); + return notifyServiceBuilder.create(context, service); + } + + private NotifyServiceInboundPatternBuilder create(Context context, NotifyServiceEntity service) { + try { + + this.context = context; + this.notifyServiceInboundPattern = inboundPatternService.create(context, service); + + } catch (SQLException e) { + log.warn("Failed to create the NotifyService", e); + } + + return this; + } + + public NotifyServiceInboundPatternBuilder isAutomatic(boolean automatic) { + notifyServiceInboundPattern.setAutomatic(automatic); + return this; + } + + public NotifyServiceInboundPatternBuilder withPattern(String pattern) { + notifyServiceInboundPattern.setPattern(pattern); + return this; + } + + public NotifyServiceInboundPatternBuilder withConstraint(String constraint) { + notifyServiceInboundPattern.setConstraint(constraint); + return this; + } + +} \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java b/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java index 9d786d4761f0..7d844415ab24 100644 --- a/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/WorkspaceItemBuilder.java @@ -11,6 +11,8 @@ import java.io.InputStream; import java.sql.SQLException; +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.NotifyServiceEntity; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Bitstream; import org.dspace.content.Collection; @@ -219,4 +221,20 @@ public WorkspaceItemBuilder withFulltext(String name, String source, InputStream } return this; } + + public WorkspaceItemBuilder withCOARNotifyService(NotifyServiceEntity notifyService, String pattern) { + Item item = workspaceItem.getItem(); + + try { + NotifyPatternToTrigger notifyPatternToTrigger = notifyPatternToTriggerService.create(context); + notifyPatternToTrigger.setItem(item); + notifyPatternToTrigger.setNotifyService(notifyService); + notifyPatternToTrigger.setPattern(pattern); + notifyPatternToTriggerService.update(context, notifyPatternToTrigger); + } catch (Exception e) { + handleException(e); + } + return this; + } + } diff --git a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java index 25eb0361592e..215f26abd612 100644 --- a/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java +++ b/dspace-api/src/test/java/org/dspace/content/service/ItemServiceIT.java @@ -11,7 +11,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.ByteArrayInputStream; @@ -19,6 +21,9 @@ import java.sql.SQLException; import java.util.Comparator; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; import java.util.stream.Collectors; import org.apache.logging.log4j.Logger; @@ -44,11 +49,15 @@ import org.dspace.content.Community; import org.dspace.content.EntityType; import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataSchema; import org.dspace.content.MetadataValue; import org.dspace.content.Relationship; import org.dspace.content.RelationshipType; import org.dspace.content.WorkspaceItem; import org.dspace.content.factory.ContentServiceFactory; +import org.dspace.contentreport.QueryOperator; +import org.dspace.contentreport.QueryPredicate; import org.dspace.core.Constants; import org.dspace.eperson.Group; import org.dspace.eperson.factory.EPersonServiceFactory; @@ -71,6 +80,9 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase { protected ItemService itemService = ContentServiceFactory.getInstance().getItemService(); protected InstallItemService installItemService = ContentServiceFactory.getInstance().getInstallItemService(); protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + protected MetadataSchemaService metadataSchemaService = + ContentServiceFactory.getInstance().getMetadataSchemaService(); + protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService(); protected MetadataValueService metadataValueService = ContentServiceFactory.getInstance().getMetadataValueService(); protected VersioningService versioningService = VersionServiceFactory.getInstance().getVersionService(); protected AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); @@ -78,6 +90,8 @@ public class ItemServiceIT extends AbstractIntegrationTestWithDatabase { Community community; Collection collection1; + MetadataSchema schemaDC; + MetadataField fieldAuthor; Item item; @@ -99,6 +113,9 @@ public void setUp() throws Exception { try { context.turnOffAuthorisationSystem(); + schemaDC = metadataSchemaService.find(context, "dc"); + fieldAuthor = metadataFieldService.findByElement(context, schemaDC, "contributor", "author"); + community = CommunityBuilder.createCommunity(context) .build(); @@ -142,7 +159,7 @@ public void preserveMetadataOrder() throws Exception { // check the correct order using default method `getMetadata` List defaultMetadata = - this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); + itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); assertThat(defaultMetadata,hasSize(3)); @@ -158,7 +175,7 @@ public void preserveMetadataOrder() throws Exception { // check the correct order using the method `getMetadata` without virtual fields List nonVirtualMetadatas = - this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); + itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); // if we don't reload the item the place order is not applied correctly // item = context.reloadEntity(item); @@ -180,19 +197,19 @@ public void preserveMetadataOrder() throws Exception { item = context.reloadEntity(item); // now just add one metadata to be the last - this.itemService.addMetadata( + itemService.addMetadata( context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, latest", null, 0 ); // now just remove first metadata - this.itemService.removeMetadataValues(context, item, List.of(placeZero)); + itemService.removeMetadataValues(context, item, List.of(placeZero)); // now just add one metadata to place 0 - this.itemService.addAndShiftRightMetadata( + itemService.addAndShiftRightMetadata( context, item, dcSchema, contributorElement, authorQualifier, Item.ANY, "test, new", null, 0, 0 ); // check the metadata using method `getMetadata` defaultMetadata = - this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); + itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); // check correct places assertThat(defaultMetadata,hasSize(4)); @@ -212,7 +229,7 @@ public void preserveMetadataOrder() throws Exception { // check metadata using nonVirtualMethod nonVirtualMetadatas = - this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); + itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); // check correct places assertThat(nonVirtualMetadatas,hasSize(4)); @@ -244,7 +261,7 @@ public void preserveMetadataOrder() throws Exception { // check after commit defaultMetadata = - this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); + itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY); // check correct places assertThat(defaultMetadata,hasSize(4)); @@ -264,7 +281,7 @@ public void preserveMetadataOrder() throws Exception { // check metadata using nonVirtualMethod nonVirtualMetadatas = - this.itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); + itemService.getMetadata(item, dcSchema, contributorElement, authorQualifier, Item.ANY, false); // check correct places assertThat(nonVirtualMetadatas,hasSize(4)); @@ -916,4 +933,65 @@ private void assertMetadataValue(String authorQualifier, String contributorEleme assertThat(metadataValue.getAuthority(), equalTo(authority)); assertThat(metadataValue.getPlace(), equalTo(place)); } + + @Test + public void testFindByMetadataQuery() throws Exception { + context.turnOffAuthorisationSystem(); + + // Here we add an author to the item + MetadataValue mv = itemService.addMetadata(context, item, dcSchema, contributorElement, + authorQualifier, null, "test, one"); + context.commit(); + + item = context.reloadEntity(item); + + assertNotNull(mv); + MetadataField mf = mv.getMetadataField(); + assertEquals(fieldAuthor, mf); + MetadataSchema ms = mf.getMetadataSchema(); + assertNotNull(ms); + assertEquals(dcSchema, ms.getName()); + + // We check whether the author metadata was properly added. + List mvs = item.getMetadata(); + MetadataValue mvAuthor1 = mvs.stream() + .filter(mv1 -> Objects.equals(mv1.getMetadataField().getElement(), "contributor")) + .filter(mv1 -> Objects.equals(mv1.getMetadataField().getQualifier(), "author")) + .findFirst() + .orElse(null); + assertNotNull(mvAuthor1); + assertEquals("test, one", mvAuthor1.getValue()); + + assertMetadataValue( + authorQualifier, contributorElement, dcSchema, "test, one", null, 0, mvAuthor1 + ); + + assertEquals(collection1, item.getOwningCollection()); + + List collectionUuids = List.of(collection1.getID()); + + // First test: we should not find anything. + QueryPredicate predicate = QueryPredicate.of(fieldAuthor, QueryOperator.MATCHES, ".*whatever.*"); + List items = itemService.findByMetadataQuery(context, List.of(predicate), collectionUuids, 0, -1); + assertTrue(items.isEmpty()); + + // Second test: we search against the metadata value specified above. + predicate = QueryPredicate.of(fieldAuthor, QueryOperator.EQUALS, "test, one"); + items = itemService.findByMetadataQuery(context, List.of(predicate), collectionUuids, 0, -1); + assertEquals(1, items.size()); + + Item item = items.get(0); + assertNotNull(item); + List allMetadata = item.getMetadata(); + Optional mvAuthor = allMetadata.stream() + .filter(md -> Objects.equals(dcSchema, md.getMetadataField().getMetadataSchema().getName())) + .filter(md -> Objects.equals(contributorElement, md.getMetadataField().getElement())) + .filter(md -> Objects.equals(authorQualifier, md.getMetadataField().getQualifier())) + .findFirst(); + assertTrue(mvAuthor.isPresent()); + assertEquals("test, one", mvAuthor.get().getValue()); + + context.restoreAuthSystemState(); + } + } diff --git a/dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java b/dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java index 489161bf8de7..0f57e187315e 100644 --- a/dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java +++ b/dspace-api/src/test/java/org/dspace/curate/CuratorReportTest.java @@ -14,6 +14,8 @@ import java.util.List; import java.util.regex.Pattern; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.AbstractUnitTest; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Community; @@ -27,8 +29,6 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Drive the Curator and check results. @@ -37,7 +37,7 @@ */ public class CuratorReportTest extends AbstractUnitTest { - Logger LOG = LoggerFactory.getLogger(CuratorReportTest.class); + Logger LOG = LogManager.getLogger(); public CuratorReportTest() { } diff --git a/dspace-api/src/test/java/org/dspace/matcher/NotifyServiceEntityMatcher.java b/dspace-api/src/test/java/org/dspace/matcher/NotifyServiceEntityMatcher.java new file mode 100644 index 000000000000..7b20a1c8be94 --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/matcher/NotifyServiceEntityMatcher.java @@ -0,0 +1,57 @@ +/** + * 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 org.dspace.app.ldn.NotifyServiceEntity; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.TypeSafeMatcher; + +/** + * Implementation of {@link Matcher} to match a NotifyServiceEntity by all its + * attributes. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + * + */ +public class NotifyServiceEntityMatcher extends TypeSafeMatcher { + + private final NotifyServiceEntity expectedEntity; + + private NotifyServiceEntityMatcher(NotifyServiceEntity expectedEntity) { + this.expectedEntity = expectedEntity; + } + + public static NotifyServiceEntityMatcher matchesNotifyServiceEntity(NotifyServiceEntity expectedEntity) { + return new NotifyServiceEntityMatcher(expectedEntity); + } + + @Override + protected boolean matchesSafely(NotifyServiceEntity actualEntity) { + return actualEntity.getName().equals(expectedEntity.getName()) && + actualEntity.getDescription().equals(expectedEntity.getDescription()) && + actualEntity.getUrl().equals(expectedEntity.getUrl()) && + actualEntity.getLdnUrl().equals(expectedEntity.getLdnUrl()) && + actualEntity.getInboundPatterns() == expectedEntity.getInboundPatterns() && + actualEntity.isEnabled() == expectedEntity.isEnabled() && + actualEntity.getScore() == expectedEntity.getScore(); + } + + @Override + public void describeTo(Description description) { + description.appendText("a Notify Service Entity with the following attributes:") + .appendText(", name ").appendValue(expectedEntity.getName()) + .appendText(", description ").appendValue(expectedEntity.getDescription()) + .appendText(", URL ").appendValue(expectedEntity.getUrl()) + .appendText(", LDN URL ").appendValue(expectedEntity.getLdnUrl()) + .appendText(", inbound patterns ").appendValue(expectedEntity.getInboundPatterns()) + .appendText(", enabled ").appendValue(expectedEntity.isEnabled()) + .appendText(", score ").appendValue(expectedEntity.getScore()); + } + +} 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 index 9769f72dcde9..b63c85eeb66e 100644 --- a/dspace-api/src/test/java/org/dspace/qaevent/script/OpenaireEventsImportIT.java +++ b/dspace-api/src/test/java/org/dspace/qaevent/script/OpenaireEventsImportIT.java @@ -8,6 +8,7 @@ package org.dspace.qaevent.script; import static java.util.List.of; +import static org.dspace.content.QAEvent.COAR_NOTIFY_SOURCE; import static org.dspace.content.QAEvent.OPENAIRE_SOURCE; import static org.dspace.matcher.QAEventMatcher.pendingOpenaireEventWith; import static org.dspace.qaevent.service.impl.QAEventServiceImpl.QAEVENTS_SOURCES; @@ -58,6 +59,7 @@ import org.dspace.utils.DSpace; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Test; /** @@ -168,10 +170,12 @@ public void testManyEventsImportFromFile() throws Exception { "Trying to read the QA events from the provided file", "Found 5 events in the given file")); - assertThat(qaEventService.findAllSources(context, 0, 20), hasItem(QASourceMatcher.with(OPENAIRE_SOURCE, 5L))); + assertThat(qaEventService.findAllSources(context, 0, 20), + hasItem(QASourceMatcher.with(OPENAIRE_SOURCE, 5L)) + ); List topicList = qaEventService.findAllTopicsBySource(context, OPENAIRE_SOURCE, 0, 20, - ORDER_FIELD, false); + ORDER_FIELD, false); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_PROJECT, 1L))); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_PID, 1L))); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID, 1L))); @@ -184,18 +188,21 @@ public void testManyEventsImportFromFile() throws Exception { + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths\"}"; - assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, "ENRICH/MORE/PROJECT", 0, 20, - ORDER_FIELD, false), contains( + assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MORE_PROJECT, 0, 20, ORDER_FIELD, true), + contains( pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99998", firstItem, "Egypt, crossroad of translations and literary interweavings", projectMessage, - "ENRICH/MORE/PROJECT", 1.00d))); + QANotifyPatterns.TOPIC_ENRICH_MORE_PROJECT, 1.00d))); String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; - assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE,"ENRICH/MISSING/ABSTRACT", 0, 20, - ORDER_FIELD, false), contains( - pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", secondItem, "Test Publication", - abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 0, 20, ORDER_FIELD, true), + contains( + pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", + secondItem, "Test Publication", + abstractMessage, QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 1.00d))); verifyNoInteractions(mockBrokerClient); } @@ -229,17 +236,18 @@ public void testManyEventsImportFromFileWithUnknownHandle() throws Exception { assertThat(qaEventService.findAllSources(context, 0, 20), hasItem(QASourceMatcher.with(OPENAIRE_SOURCE, 3L))); List topicList = qaEventService.findAllTopicsBySource(context, OPENAIRE_SOURCE, 0, 20, - ORDER_FIELD, false); + ORDER_FIELD, false); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 1L))); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MISSING_PROJECT, 1L))); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_PID, 1L))); String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; - assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, "ENRICH/MISSING/ABSTRACT", 0, 20, + assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 0, 20, ORDER_FIELD, false), contains( pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", item, "Test Publication", - abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + abstractMessage, QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 1.00d))); verifyNoInteractions(mockBrokerClient); } @@ -296,7 +304,7 @@ public void testImportFromFileWithoutEvents() throws Exception { assertThat(handler.getWarningMessages(),empty()); assertThat(handler.getInfoMessages(), contains("Trying to read the QA events from the provided file")); - assertThat(qaEventService.findAllSources(context, 0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 0L))); + assertThat(qaEventService.findAllSources(context, 0, 20), hasItem(QASourceMatcher.with(OPENAIRE_SOURCE, 0L))); assertThat(qaEventService.findAllTopics(context, 0, 20, ORDER_FIELD, false), empty()); @@ -357,20 +365,22 @@ public void testImportFromOpenaireBroker() throws Exception { + "\"projects[0].openaireId\":\"40|corda__h2020::6e32f5eb912688f2424c68b851483ea4\"," + "\"projects[0].title\":\"Tracking Papyrus and Parchment Paths\"}"; - assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, "ENRICH/MORE/PROJECT", 0 , 20, + assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MORE_PROJECT, 0 , 20, ORDER_FIELD, false), contains( pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99998", firstItem, "Egypt, crossroad of translations and literary interweavings", projectMessage, - "ENRICH/MORE/PROJECT", 1.00d))); + QANotifyPatterns.TOPIC_ENRICH_MORE_PROJECT, 1.00d))); String abstractMessage = "{\"abstracts[0]\":\"Missing Abstract\"}"; - assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, "ENRICH/MISSING/ABSTRACT", 0, 20, + assertThat(qaEventService.findEventsByTopic(context, OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 0, 20, ORDER_FIELD, false), containsInAnyOrder( pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/99999", secondItem, "Test Publication", - abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d), + abstractMessage, QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 1.00d), pendingOpenaireEventWith("oai:www.openstarts.units.it:123456789/999991", thirdItem, "Test Publication 2", - abstractMessage, "ENRICH/MISSING/ABSTRACT", 1.00d))); + abstractMessage, QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 1.00d))); verify(mockBrokerClient).listSubscriptions(openaireURL, "user@test.com"); verify(mockBrokerClient).downloadEvents(eq(openaireURL), eq("sub1"), any()); @@ -398,9 +408,9 @@ public void testImportFromOpenaireBrokerWithErrorDuringListSubscription() throws assertThat(handler.getWarningMessages(), empty()); assertThat(handler.getInfoMessages(), contains("Trying to read the QA events from the OPENAIRE broker")); - assertThat(qaEventService.findAllSources(context, 0, 20), contains(QASourceMatcher.with(OPENAIRE_SOURCE, 0L))); + assertThat(qaEventService.findAllSources(context, 0, 20), hasItem(QASourceMatcher.with(OPENAIRE_SOURCE, 0L))); - assertThat(qaEventService.findAllTopics(context, 0, 20,ORDER_FIELD, false), empty()); + assertThat(qaEventService.findAllTopics(context, 0, 20, ORDER_FIELD, false), empty()); verify(mockBrokerClient).listSubscriptions(openaireURL, "user@test.com"); @@ -450,7 +460,8 @@ public void testImportFromOpenaireBrokerWithErrorDuringEventsDownload() throws E assertThat(qaEventService.findAllSources(context, 0, 20), hasItem(QASourceMatcher.with(OPENAIRE_SOURCE, 6L))); - List topicList = qaEventService.findAllTopicsBySource(context, OPENAIRE_SOURCE,0,20,ORDER_FIELD,false); + List topicList = qaEventService.findAllTopicsBySource(context, OPENAIRE_SOURCE, 0, 20, + ORDER_FIELD, false); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_PROJECT, 1L))); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID, 1L))); assertThat(topicList, hasItem(QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_PID, 1L))); @@ -470,6 +481,35 @@ public void testImportFromOpenaireBrokerWithErrorDuringEventsDownload() throws E verifyNoMoreInteractions(mockBrokerClient); } + /** + * Improper test for ENRICH/MORE/REVIEW qa. It has the COAR_NOTIFY source + * which must be tested via LDNMessage at DNInboxControllerIT + */ + @Test + @Ignore + public void testImportFromFileEventMoreReview() 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("event-more-review.json") }; + ScriptLauncher.handleScript(args, ScriptLauncher.getConfig(kernelImpl), handler, kernelImpl); + + assertThat(qaEventService.findAllTopicsBySource(context, COAR_NOTIFY_SOURCE, 0, 20, + ORDER_FIELD, false), contains( + QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW, 1L))); + + assertThat(qaEventService.findAllSources(context, 0, 20), + hasItem(QASourceMatcher.with(COAR_NOTIFY_SOURCE, 1L))); + + verifyNoInteractions(mockBrokerClient); + } + private Item createItem(String title, String handle) { return ItemBuilder.createItem(context, collection) .withTitle(title) @@ -502,3 +542,4 @@ private String getFileLocation(String fileName) throws Exception { return new File(resource.getFile()).getAbsolutePath(); } } + diff --git a/dspace-api/src/test/java/org/dspace/util/DSpaceH2Dialect.java b/dspace-api/src/test/java/org/dspace/util/DSpaceH2Dialect.java new file mode 100644 index 000000000000..9df86c0218de --- /dev/null +++ b/dspace-api/src/test/java/org/dspace/util/DSpaceH2Dialect.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.util; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.type.StandardBasicTypes; + +/** + * H2-specific dialect that adds regular expression support as a function. + * @author Jean-François Morin (Université Laval) + */ +public class DSpaceH2Dialect extends H2Dialect { + + private static Map regexCache = new HashMap<>(); + + public DSpaceH2Dialect() { + registerFunction("matches", new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "matches(?1, ?2)")); + + // The SQL function is registered in AbstractIntegrationTestWithDatabase.initDatabase(). + } + + public static boolean matches(String regex, String value) { + Pattern pattern = regexCache.get(regex); + if (pattern == null) { + pattern = Pattern.compile(regex); + regexCache.put(regex, pattern); + } + return pattern.matcher(value).matches(); + } + +} diff --git a/dspace-iiif/pom.xml b/dspace-iiif/pom.xml index 2f34671139c8..81fa3d2e806a 100644 --- a/dspace-iiif/pom.xml +++ b/dspace-iiif/pom.xml @@ -28,6 +28,12 @@ + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + org.springframework.boot diff --git a/dspace-server-webapp/pom.xml b/dspace-server-webapp/pom.xml index 8e713bb6af8b..93faf48da4e9 100644 --- a/dspace-server-webapp/pom.xml +++ b/dspace-server-webapp/pom.xml @@ -421,7 +421,7 @@ org.springframework.boot spring-boot-starter - ${spring-boot.version} + ${spring-boot.version} org.springframework.boot @@ -433,7 +433,13 @@ org.springframework.boot spring-boot-starter-log4j2 - ${spring-boot.version} + ${spring-boot.version} + + + + org.apache.logging.log4j + log4j-api + ${log4j.version} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java index e8b8eb8e70da..6faade104e18 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/AuthenticationRestController.java @@ -36,8 +36,6 @@ import org.dspace.app.rest.utils.Utils; import org.dspace.core.Context; import org.dspace.service.ClientInfoService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; @@ -64,8 +62,6 @@ @RestController public class AuthenticationRestController implements InitializingBean { - private static final Logger log = LoggerFactory.getLogger(AuthenticationRestController.class); - @Autowired DiscoverableEndpointsService discoverableEndpointsService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/ContentReportRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ContentReportRestController.java new file mode 100644 index 000000000000..dd4196f8910e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/ContentReportRestController.java @@ -0,0 +1,226 @@ +/** + * 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 java.io.IOException; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.model.ContentReportSupportRest; +import org.dspace.app.rest.model.FilteredCollectionsQuery; +import org.dspace.app.rest.model.FilteredCollectionsRest; +import org.dspace.app.rest.model.FilteredItemsQueryPredicate; +import org.dspace.app.rest.model.FilteredItemsQueryRest; +import org.dspace.app.rest.model.FilteredItemsRest; +import org.dspace.app.rest.model.RestModel; +import org.dspace.app.rest.model.hateoas.ContentReportSupportResource; +import org.dspace.app.rest.model.hateoas.FilteredCollectionsResource; +import org.dspace.app.rest.model.hateoas.FilteredItemsResource; +import org.dspace.app.rest.repository.ContentReportRestRepository; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.contentreport.Filter; +import org.dspace.contentreport.service.ContentReportService; +import org.dspace.core.Context; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.rest.webmvc.ControllerUtils; +import org.springframework.hateoas.Link; +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.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * This controller receives and dispatches requests related to the + * contents reports ported from DSpace 6.x (Filtered Collections + * and Filtered Items). + * @author Jean-François Morin (Université Laval) + */ +@RestController +@RequestMapping("/api/" + RestModel.CONTENT_REPORT) +public class ContentReportRestController implements InitializingBean { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); + + @Autowired + private DiscoverableEndpointsService discoverableEndpointsService; + @Autowired + private ConverterService converter; + @Autowired + private ContentReportRestRepository contentReportRestRepository; + @Autowired + private ContentReportService contentReportService; + + @Override + public void afterPropertiesSet() throws Exception { + discoverableEndpointsService + .register(this, List.of(Link.of("/api/" + RestModel.CONTENT_REPORT, RestModel.CONTENT_REPORT))); + } + + @RequestMapping(method = RequestMethod.GET) + public ContentReportSupportResource getContentReportSupport() { + ContentReportSupportRest contentReportSupportRest = contentReportRestRepository.getContentReportSupport(); + return converter.toResource(contentReportSupportRest); + } + + /** + * GET-based endpoint for the Filtered Collections contents report. + * This method also serves as a feed for the HAL Browser infrastructure. + * @param filters querying filters received as a comma-separated string + * or as a multivalued parameter + * @param request HTTP request + * @param response HTTP response + * @return the list of collections with their respective statistics + */ + @PreAuthorize("hasAuthority('ADMIN')") + @GetMapping("/filteredcollections") + public ResponseEntity> getFilteredCollections( + @RequestParam(name = "filters", required = false) List filters, + HttpServletRequest request, HttpServletResponse response) throws IOException { + if (contentReportService.getEnabled()) { + Context context = ContextUtil.obtainContext(request); + Set filtersSet = listToStream(filters) + .map(Filter::get) + .filter(f -> f != null) + .collect(Collectors.toSet()); + FilteredCollectionsQuery query = FilteredCollectionsQuery.of(filtersSet); + return filteredCollectionsReport(context, query); + } + + error404(response); + return null; + } + + + private ResponseEntity> filteredCollectionsReport(Context context, + FilteredCollectionsQuery query) { + FilteredCollectionsRest report = contentReportRestRepository + .findFilteredCollections(context, query); + FilteredCollectionsResource result = converter.toResource(report); + return ControllerUtils.toResponseEntity(HttpStatus.OK, new HttpHeaders(), result); + } + + /** + * Endpoint for the Filtered Items contents report. + * All parameters received as comma-separated lists can also be repeated + * instead (e.g., filters=a&filters=b&...). + * @param collections comma-separated list UUIDs of collections to include in the report + * @param predicates predicates to filter the requested items. + * A given predicate has the form + * field:operator:value (if value is required by the operator), or + * field:operator (if no value is required by the operator). + * The colon is used here as a separator to avoid conflicts with the + * comma, which is already used by Spring as a multi-value separator. + * Predicates are actually retrieved directly through the request to prevent comma-containing + * predicate values from being split by the Spring infrastructure. + * @param pageNumber page number (starting at 0) + * @param pageLimit maximum number of items per page + * @param filters querying filters received as a comma-separated string + * @param additionalFields comma-separated list of extra fields to add to the report + * @param request HTTP request + * @param response HTTP response + * @param pageable paging parameters + * @return the list of items with their respective statistics + */ + @PreAuthorize("hasAuthority('ADMIN')") + @GetMapping("/filtereditems") + public ResponseEntity> getFilteredItems( + @RequestParam(name = "collections", required = false) List collections, + @RequestParam(name = "queryPredicates", required = false) List predicates, + @RequestParam(name = "pageNumber", defaultValue = "0") String pageNumber, + @RequestParam(name = "pageLimit", defaultValue = "10") String pageLimit, + @RequestParam(name = "filters", required = false) List filters, + @RequestParam(name = "additionalFields", required = false) List additionalFields, + HttpServletRequest request, HttpServletResponse response, Pageable pageable) throws IOException { + if (contentReportService.getEnabled()) { + Context context = ContextUtil.obtainContext(request); + String[] realPredicates = request.getParameterValues("queryPredicates"); + List collUuids = Optional.ofNullable(collections).orElseGet(() -> List.of()); + List preds = arrayToStream(realPredicates) + .map(FilteredItemsQueryPredicate::of) + .collect(Collectors.toList()); + int pgLimit = parseInt(pageLimit, 10); + int pgNumber = parseInt(pageNumber, 0); + Pageable myPageable = pageable; + if (pageable == null || pageable.getPageNumber() != pgNumber || pageable.getPageSize() != pgLimit) { + Sort sort = Optional.ofNullable(pageable).map(Pageable::getSort).orElse(Sort.unsorted()); + myPageable = PageRequest.of(pgNumber, pgLimit, sort); + } + Set filtersMap = listToStream(filters) + .map(Filter::get) + .filter(f -> f != null) + .collect(Collectors.toSet()); + List addFields = Optional.ofNullable(additionalFields).orElseGet(() -> List.of()); + FilteredItemsQueryRest query = FilteredItemsQueryRest.of(collUuids, preds, pgLimit, filtersMap, addFields); + + return filteredItemsReport(context, query, myPageable); + } + + error404(response); + return null; + } + + private static Stream listToStream(Collection array) { + return Optional.ofNullable(array) + .stream() + .flatMap(Collection::stream) + .filter(StringUtils::isNotBlank); + } + + private static Stream arrayToStream(String... array) { + return Optional.ofNullable(array) + .stream() + .flatMap(Arrays::stream) + .filter(StringUtils::isNotBlank); + } + + private static int parseInt(String value, int defaultValue) { + return Optional.ofNullable(value) + .stream() + .mapToInt(Integer::parseInt) + .findFirst() + .orElse(defaultValue); + } + + private ResponseEntity> filteredItemsReport(Context context, + FilteredItemsQueryRest query, Pageable pageable) { + FilteredItemsRest report = contentReportRestRepository + .findFilteredItems(context, query, pageable); + FilteredItemsResource result = converter.toResource(report); + return ControllerUtils.toResponseEntity(HttpStatus.OK, new HttpHeaders(), result); + } + + private void error404(HttpServletResponse response) throws IOException { + log.debug("Content Reports are disabled"); + String err = "Content Reports are disabled"; + response.setStatus(404); + response.setContentType("text/html"); + response.setContentLength(err.length()); + response.getWriter().write(err); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java new file mode 100644 index 000000000000..5d18494fc28a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/LDNInboxController.java @@ -0,0 +1,145 @@ +/** + * 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 java.sql.SQLException; +import java.util.regex.Pattern; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.validator.routines.UrlValidator; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.app.ldn.LDNRouter; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.InvalidLDNMessageException; +import org.dspace.core.Context; +import org.dspace.services.ConfigurationService; +import org.dspace.web.ContextUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ResponseStatusException; + +@Controller +@RequestMapping("/ldn") +@ConditionalOnProperty("ldn.enabled") +public class LDNInboxController { + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(); + + @Autowired + private LDNRouter router; + + @Autowired + private LDNMessageService ldnMessageService; + + @Autowired + private ConfigurationService configurationService; + + /** + * LDN DSpace inbox. + * + * @param notification received notification + * @return ResponseEntity 400 not stored, 202 stored + * @throws Exception + */ + @PostMapping(value = "/inbox", consumes = "application/ld+json") + public ResponseEntity inbox(HttpServletRequest request, @RequestBody Notification notification) + throws Exception { + + Context context = ContextUtil.obtainCurrentRequestContext(); + validate(context, notification, request.getRemoteAddr()); + + LDNMessageEntity ldnMsgEntity = ldnMessageService.create(context, notification, request.getRemoteAddr()); + log.info("stored ldn message {}", ldnMsgEntity); + context.commit(); + + return ResponseEntity.accepted() + .body(String.format("Successfully stored notification %s %s", + notification.getId(), notification.getType())); + } + + /** + * LDN DSpace inbox options. + * + * @return ResponseEntity 200 with allow and accept-post headers + */ + @RequestMapping(value = "/inbox", method = RequestMethod.OPTIONS) + public ResponseEntity options() { + return ResponseEntity.ok() + .allow(HttpMethod.OPTIONS, HttpMethod.POST) + .header("Accept-Post", "application/ld+json") + .build(); + } + + /** + * @param e + * @return ResponseEntity + */ + @ExceptionHandler(ResponseStatusException.class) + public ResponseEntity handleResponseStatusException(ResponseStatusException e) { + return ResponseEntity.status(e.getStatus().value()) + .body(e.getMessage()); + } + + private void validate(Context context, Notification notification, String sourceIp) { + String id = notification.getId(); + Pattern URNRegex = + Pattern.compile("^urn:uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"); + + if (!URNRegex.matcher(id).matches() && !new UrlValidator().isValid(id)) { + throw new InvalidLDNMessageException("Invalid URI format for 'id' field."); + } + + if (notification.getOrigin() == null || notification.getTarget() == null || notification.getObject() == null) { + throw new InvalidLDNMessageException("Origin or Target or Object is missing"); + } + + if (configurationService.getBooleanProperty("ldn.notify.inbox.block-untrusted", true)) { + try { + NotifyServiceEntity originNotifyService = + ldnMessageService.findNotifyService(context, notification.getOrigin()); + if (originNotifyService == null) { + throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() + + "] unknown. LDN message can not be received."); + } + } catch (SQLException sqle) { + throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() + + "] unknown. LDN message can not be received."); + } + } + if (configurationService.getBooleanProperty("ldn.notify.inbox.block-untrusted-ip", true)) { + try { + NotifyServiceEntity originNotifyService = + ldnMessageService.findNotifyService(context, notification.getOrigin()); + if (originNotifyService == null) { + throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() + + "] unknown. LDN message can not be received."); + } + boolean isValidIp = ldnMessageService.isValidIp(originNotifyService, sourceIp); + if (!isValidIp) { + throw new DSpaceBadRequestException("Source IP for Incoming LDN Message [" + notification.getId() + + "] out of its Notify Service IP Range. LDN message can not be received."); + } + } catch (SQLException sqle) { + throw new DSpaceBadRequestException("Notify Service [" + notification.getOrigin() + + "] unknown. LDN message can not be received."); + } + } + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/NotifyRequestStatusRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/NotifyRequestStatusRestController.java new file mode 100644 index 000000000000..0c599b03e852 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/NotifyRequestStatusRestController.java @@ -0,0 +1,107 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.dspace.app.rest.utils.RegexUtils.REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID; + +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.dspace.app.ldn.model.NotifyRequestStatus; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.rest.converter.ConverterService; +import org.dspace.app.rest.model.NotifyRequestStatusRest; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.rest.utils.Utils; +import org.dspace.authorize.AuthorizeException; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Item; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.dspace.eperson.EPerson; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.rest.webmvc.ResourceNotFoundException; +import org.springframework.hateoas.Link; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +/** + * Rest Controller for NotifyRequestStatus targeting items + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) + */ +@RestController +@RequestMapping("/api/" + NotifyRequestStatusRest.CATEGORY + "/" + NotifyRequestStatusRest.PLURAL_NAME + + REGEX_REQUESTMAPPING_IDENTIFIER_AS_UUID) +public class NotifyRequestStatusRestController implements InitializingBean { + + private static final Logger log = LogManager.getLogger(NotifyRequestStatusRestController.class); + + @Autowired + private ConverterService converterService; + + @Autowired + private Utils utils; + + @Autowired + private LDNMessageService ldnMessageService; + + @Autowired + private ItemService itemService; + + @Autowired + private AuthorizeService authorizeService; + + @Autowired + private DiscoverableEndpointsService discoverableEndpointsService; + + @Override + public void afterPropertiesSet() { + discoverableEndpointsService.register(this, + List.of(Link.of("/api/" + NotifyRequestStatusRest.CATEGORY + "/" + NotifyRequestStatusRest.NAME, + NotifyRequestStatusRest.NAME))); + } + + @GetMapping(produces = "application/json") + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public ResponseEntity findByItem(@PathVariable UUID uuid) + throws SQLException, AuthorizeException, JsonProcessingException { + + Context context = ContextUtil.obtainCurrentRequestContext(); + Item item = itemService.find(context, uuid); + if (item == null) { + throw new ResourceNotFoundException("No such item: " + uuid); + } + EPerson currentUser = context.getCurrentUser(); + if (!currentUser.equals(item.getSubmitter()) && !authorizeService.isAdmin(context)) { + throw new AuthorizeException("User unauthorized"); + } + NotifyRequestStatus resultRequests = ldnMessageService.findRequestsByItem(context, item); + NotifyRequestStatusRest resultRequestStatusRests = converterService.toRest( + resultRequests, utils.obtainProjection()); + + context.complete(); + String result = new ObjectMapper() + .writerWithDefaultPrettyPrinter().writeValueAsString(resultRequestStatusRests); + + return new ResponseEntity<>(result, HttpStatus.OK); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java index db04b3b7cd95..0c35b4333860 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/OidcRestController.java @@ -14,11 +14,11 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.AuthnRest; import org.dspace.core.Utils; import org.dspace.services.ConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.hateoas.Link; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,7 +27,7 @@ import org.springframework.web.bind.annotation.RestController; /** - * Rest controller that handles redirect after OIDC authentication succeded. + * Rest controller that handles redirect after OIDC authentication succeeded. * * @author Luca Giamminonni (luca.giamminonni at 4science.it) */ @@ -35,7 +35,7 @@ @RequestMapping(value = "/api/" + AuthnRest.CATEGORY + "/oidc") public class OidcRestController { - private static final Logger log = LoggerFactory.getLogger(OidcRestController.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ConfigurationService configurationService; @@ -66,11 +66,12 @@ public void oidc(HttpServletResponse response, } if (StringUtils.equalsAnyIgnoreCase(redirectHostName, allowedHostNames.toArray(new String[0]))) { - log.debug("OIDC redirecting to " + redirectUrl); + log.debug("OIDC redirecting to {}", redirectUrl); response.sendRedirect(redirectUrl); // lgtm [java/unvalidated-url-redirection] } else { - log.error("Invalid OIDC redirectURL=" + redirectUrl + - ". URL doesn't match hostname of server or UI!"); + log.error("Invalid OIDC redirectURL={}." + + " URL doesn't match hostname of server or UI!", + redirectUrl); response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid redirectURL! Must match server or ui hostname."); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java index 401ad626e305..3c79cc18636d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/WebApplication.java @@ -12,6 +12,8 @@ import java.util.List; import javax.servlet.Filter; +import org.dspace.app.ldn.LDNQueueExtractor; +import org.dspace.app.ldn.LDNQueueTimeoutChecker; import org.dspace.app.rest.filter.DSpaceRequestContextFilter; import org.dspace.app.rest.model.hateoas.DSpaceLinkRelationProvider; import org.dspace.app.rest.parameter.resolver.SearchFilterResolver; @@ -22,8 +24,6 @@ import org.dspace.app.util.DSpaceContextListener; import org.dspace.google.GoogleAsyncEventListener; import org.dspace.utils.servlet.DSpaceWebappServletFilter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; @@ -53,8 +53,6 @@ @Configuration public class WebApplication { - private static final Logger log = LoggerFactory.getLogger(WebApplication.class); - @Autowired private ApplicationConfig configuration; @@ -66,6 +64,22 @@ public void generateSitemap() throws IOException, SQLException { GenerateSitemaps.generateSitemapsScheduled(); } + @Scheduled(cron = "${ldn.queue.extractor.cron:-}") + public void ldnExtractFromQueue() throws IOException, SQLException { + if (!configuration.getLdnEnabled()) { + return; + } + LDNQueueExtractor.extractMessageFromQueue(); + } + + @Scheduled(cron = "${ldn.queue.timeout.checker.cron:-}") + public void ldnQueueTimeoutCheck() throws IOException, SQLException { + if (!configuration.getLdnEnabled()) { + return; + } + LDNQueueTimeoutChecker.checkQueueMessageTimeout(); + } + @Scheduled(cron = "${solr-database-resync.cron:-}") public void solrDatabaseResync() throws Exception { SolrDatabaseResyncCli.runScheduled(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java index 9642bb4a2d61..aed82ba866c2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CanClaimItemFeature.java @@ -11,6 +11,8 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.authorization.AuthorizationFeature; import org.dspace.app.rest.authorization.AuthorizationFeatureDocumentation; import org.dspace.app.rest.model.BaseObjectRest; @@ -22,8 +24,6 @@ import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.profile.service.ResearcherProfileService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -43,7 +43,7 @@ public class CanClaimItemFeature implements AuthorizationFeature { public static final String NAME = "canClaimItem"; - private static final Logger LOG = LoggerFactory.getLogger(CanClaimItemFeature.class); + private static final Logger LOG = LogManager.getLogger(); @Autowired private ItemService itemService; @@ -72,7 +72,8 @@ private boolean hasNotAlreadyAProfile(Context context) { try { return researcherProfileService.findById(context, context.getCurrentUser().getID()) == null; } catch (SQLException | AuthorizeException e) { - LOG.warn("Error while checking if eperson has a ResearcherProfileAssociated: {}", e.getMessage(), e); + LOG.warn("Error while checking if eperson has a ResearcherProfileAssociated: {}", + e.getMessage(), e); return false; } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CoarNotifyEnabled.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CoarNotifyEnabled.java new file mode 100644 index 000000000000..5ef8410e08ac --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/authorization/impl/CoarNotifyEnabled.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.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.discovery.SearchServiceException; +import org.dspace.services.ConfigurationService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@AuthorizationFeatureDocumentation(name = CoarNotifyEnabled.NAME, + description = "It can be used to verify if the user can see the coar notify protocol is enabled") +public class CoarNotifyEnabled implements AuthorizationFeature { + + public final static String NAME = "coarNotifyEnabled"; + + @Autowired + private ConfigurationService configurationService; + + @Override + public boolean isAuthorized(Context context, BaseObjectRest object) throws SQLException, SearchServiceException { + return configurationService.getBooleanProperty("ldn.enabled", true); + } + + @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/FilteredItemConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/FilteredItemConverter.java new file mode 100644 index 000000000000..9d42688eb3dc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/FilteredItemConverter.java @@ -0,0 +1,125 @@ +/** + * 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.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.Logger; +import org.dspace.app.rest.model.FilteredItemRest; +import org.dspace.app.rest.model.MetadataValueList; +import org.dspace.app.rest.projection.Projection; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.util.service.MetadataExposureService; +import org.dspace.authorize.service.AuthorizeService; +import org.dspace.content.Item; +import org.dspace.content.MetadataField; +import org.dspace.content.MetadataValue; +import org.dspace.content.service.ItemService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +/** + * This is the converter from/to the Item in the DSpace API data model and the + * REST data model + * + * @author Andrea Bollini (andrea.bollini at 4science.it) + */ +@Component +public class FilteredItemConverter { + + // Must be loaded @Lazy, as ConverterService autowires all DSpaceConverter components + @Lazy + @Autowired + ConverterService converter; + @Autowired + private ItemService itemService; + @Autowired + private CollectionConverter collectionConverter; + @Autowired + AuthorizeService authorizeService; + @Autowired + MetadataExposureService metadataExposureService; + + private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(FilteredItemConverter.class); + + public FilteredItemRest convert(Item obj, Projection projection) { + FilteredItemRest item = new FilteredItemRest(); + + item.setHandle(obj.getHandle()); + if (obj.getID() != null) { + item.setUuid(obj.getID().toString()); + } + item.setName(obj.getName()); + + MetadataValueList metadataValues = getPermissionFilteredMetadata( + ContextUtil.obtainCurrentRequestContext(), obj); + item.setMetadata(converter.toRest(metadataValues, projection)); + + item.setInArchive(obj.isArchived()); + item.setDiscoverable(obj.isDiscoverable()); + item.setWithdrawn(obj.isWithdrawn()); + item.setLastModified(obj.getLastModified()); + + List entityTypes = + itemService.getMetadata(obj, "dspace", "entity", "type", Item.ANY, false); + if (CollectionUtils.isNotEmpty(entityTypes) && StringUtils.isNotBlank(entityTypes.get(0).getValue())) { + item.setEntityType(entityTypes.get(0).getValue()); + } + + Optional.ofNullable(obj.getOwningCollection()) + .map(coll -> collectionConverter.convert(coll, Projection.DEFAULT)) + .ifPresent(item::setOwningCollection); + + return item; + } + + /** + * Retrieves the metadata list filtered according to the hidden metadata configuration + * When the context is null, it will return the metadatalist as for an anonymous user + * Overrides the parent method to include virtual metadata + * @param context The context + * @param obj The object of which the filtered metadata will be retrieved + * @return A list of object metadata (including virtual metadata) filtered based on the hidden metadata + * configuration + */ + private MetadataValueList getPermissionFilteredMetadata(Context context, Item obj) { + List fullList = itemService.getMetadata(obj, Item.ANY, Item.ANY, Item.ANY, Item.ANY, true); + List returnList = new LinkedList<>(); + try { + if (obj.isWithdrawn() && (Objects.isNull(context) || + Objects.isNull(context.getCurrentUser()) || !authorizeService.isAdmin(context))) { + return new MetadataValueList(new ArrayList<>()); + } + if (context != null && (authorizeService.isAdmin(context) || itemService.canEdit(context, obj))) { + return new MetadataValueList(fullList); + } + for (MetadataValue mv : fullList) { + MetadataField metadataField = mv.getMetadataField(); + if (!metadataExposureService + .isHidden(context, metadataField.getMetadataSchema().getName(), + metadataField.getElement(), + metadataField.getQualifier())) { + returnList.add(mv); + } + } + } catch (SQLException e) { + log.error("Error filtering item metadata based on permissions", e); + } + return new MetadataValueList(returnList); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemFilterConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemFilterConverter.java new file mode 100644 index 000000000000..b058ef057c85 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/ItemFilterConverter.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.app.rest.converter; + +import org.dspace.app.ldn.ItemFilter; +import org.dspace.app.rest.model.ItemFilterRest; +import org.dspace.app.rest.projection.Projection; +import org.springframework.stereotype.Component; + +/** + * This is the converter from the ItemFilter to the REST data model + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Component +public class ItemFilterConverter implements DSpaceConverter { + + @Override + public ItemFilterRest convert(ItemFilter obj, Projection projection) { + ItemFilterRest itemFilterRest = new ItemFilterRest(); + itemFilterRest.setProjection(projection); + itemFilterRest.setId(obj.getId()); + return itemFilterRest; + } + + @Override + public Class getModelClass() { + return ItemFilter.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyRequestStatusConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyRequestStatusConverter.java new file mode 100644 index 000000000000..d4a6efceee87 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyRequestStatusConverter.java @@ -0,0 +1,37 @@ +/** + * 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.ldn.model.NotifyRequestStatus; +import org.dspace.app.rest.model.NotifyRequestStatusRest; +import org.dspace.app.rest.projection.Projection; +import org.springframework.stereotype.Component; + +/** + * This is the converter from/to the NotifyRequestStatus in the DSpace API data model and + * the REST data model + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + */ +@Component +public class NotifyRequestStatusConverter implements DSpaceConverter { + + @Override + public NotifyRequestStatusRest convert(NotifyRequestStatus modelObject, Projection projection) { + NotifyRequestStatusRest result = new NotifyRequestStatusRest(); + result.setItemuuid(modelObject.getItemUuid()); + result.setNotifyStatus(modelObject.getNotifyStatus()); + return result; + } + + @Override + public Class getModelClass() { + return NotifyRequestStatus.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java new file mode 100644 index 000000000000..8e0225f43fca --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/NotifyServiceConverter.java @@ -0,0 +1,68 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.converter; + +import java.util.ArrayList; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.rest.model.NotifyServiceInboundPatternRest; +import org.dspace.app.rest.model.NotifyServiceRest; +import org.dspace.app.rest.projection.Projection; +import org.springframework.stereotype.Component; + +/** + * This is the converter from the NotifyServiceEntity to the REST data model + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Component +public class NotifyServiceConverter implements DSpaceConverter { + + @Override + public NotifyServiceRest convert(NotifyServiceEntity obj, Projection projection) { + NotifyServiceRest notifyServiceRest = new NotifyServiceRest(); + + notifyServiceRest.setProjection(projection); + notifyServiceRest.setId(obj.getID()); + notifyServiceRest.setName(obj.getName()); + notifyServiceRest.setDescription(obj.getDescription()); + notifyServiceRest.setUrl(obj.getUrl()); + notifyServiceRest.setLdnUrl(obj.getLdnUrl()); + notifyServiceRest.setEnabled(obj.isEnabled()); + notifyServiceRest.setScore(obj.getScore()); + notifyServiceRest.setLowerIp(obj.getLowerIp()); + notifyServiceRest.setUpperIp(obj.getUpperIp()); + + if (obj.getInboundPatterns() != null) { + notifyServiceRest.setNotifyServiceInboundPatterns( + convertInboundPatternToRest(obj.getInboundPatterns())); + } + + return notifyServiceRest; + } + + private List convertInboundPatternToRest( + List inboundPatterns) { + List inboundPatternRests = new ArrayList<>(); + for (NotifyServiceInboundPattern inboundPattern : inboundPatterns) { + NotifyServiceInboundPatternRest inboundPatternRest = new NotifyServiceInboundPatternRest(); + inboundPatternRest.setPattern(inboundPattern.getPattern()); + inboundPatternRest.setConstraint(inboundPattern.getConstraint()); + inboundPatternRest.setAutomatic(inboundPattern.isAutomatic()); + inboundPatternRests.add(inboundPatternRest); + } + return inboundPatternRests; + } + + @Override + public Class getModelClass() { + return NotifyServiceEntity.class; + } +} 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 index 0cb73d94e318..b908bf3e41f1 100644 --- 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 @@ -17,12 +17,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import org.dspace.app.rest.model.CorrectionTypeQAEventMessageRest; +import org.dspace.app.rest.model.NotifyQAEventMessageRest; 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.CorrectionTypeMessageDTO; +import org.dspace.qaevent.service.dto.NotifyMessageDTO; import org.dspace.qaevent.service.dto.OpenaireMessageDTO; import org.dspace.qaevent.service.dto.QAMessageDTO; import org.dspace.services.ConfigurationService; @@ -78,6 +80,8 @@ public QAEventRest convert(QAEvent modelObject, Projection projection) { private QAEventMessageRest convertMessage(QAMessageDTO dto) { if (dto instanceof OpenaireMessageDTO) { return convertOpenaireMessage(dto); + } else if (dto instanceof NotifyMessageDTO) { + return convertNotifyMessage(dto); } if (dto instanceof CorrectionTypeMessageDTO) { return convertCorrectionTypeMessage(dto); @@ -85,6 +89,16 @@ private QAEventMessageRest convertMessage(QAMessageDTO dto) { throw new IllegalArgumentException("Unknown message type: " + dto.getClass()); } + private QAEventMessageRest convertNotifyMessage(QAMessageDTO dto) { + NotifyMessageDTO notifyDto = (NotifyMessageDTO) dto; + NotifyQAEventMessageRest message = new NotifyQAEventMessageRest(); + message.setServiceName(notifyDto.getServiceName()); + message.setServiceId(notifyDto.getServiceId()); + message.setHref(notifyDto.getHref()); + message.setRelationship(notifyDto.getRelationship()); + return message; + } + private QAEventMessageRest convertCorrectionTypeMessage(QAMessageDTO dto) { CorrectionTypeMessageDTO correctionTypeDto = (CorrectionTypeMessageDTO) dto; CorrectionTypeQAEventMessageRest message = new CorrectionTypeQAEventMessageRest(); 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 index 8b651d821d54..c358c7323e95 100644 --- 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 @@ -31,8 +31,8 @@ public Class getModelClass() { public QASourceRest convert(QASource modelObject, Projection projection) { QASourceRest rest = new QASourceRest(); rest.setProjection(projection); - rest.setId(modelObject.getName() + (modelObject.getFocus() != null ? ":" + modelObject.getFocus().toString() - : "")); + rest.setId(modelObject.getName() + + (modelObject.getFocus() != null ? ":" + modelObject.getFocus().toString() : "")); 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 index a40ae527b192..e6334924c190 100644 --- 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 @@ -31,8 +31,9 @@ public Class getModelClass() { public QATopicRest convert(QATopic modelObject, Projection projection) { QATopicRest rest = new QATopicRest(); rest.setProjection(projection); - rest.setId(modelObject.getSource() + ":" + modelObject.getKey().replace("/", "!") + - (modelObject.getFocus() != null ? ":" + modelObject.getFocus().toString() : "")); + rest.setId(modelObject.getSource() + ":" + + modelObject.getKey().replace("/", "!") + + (modelObject.getFocus() != null ? ":" + modelObject.getFocus().toString() : "")); rest.setName(modelObject.getKey()); rest.setLastEvent(modelObject.getLastEvent()); rest.setTotalEvents(modelObject.getTotalEvents()); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCOARNotifyConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCOARNotifyConverter.java new file mode 100644 index 000000000000..8cfcb6c0639c --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SubmissionCOARNotifyConverter.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.app.rest.converter; + +import org.dspace.app.rest.model.SubmissionCOARNotifyRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.coarnotify.NotifySubmissionConfiguration; +import org.springframework.stereotype.Component; + +/** + * This converter is responsible for transforming the model representation of an COARNotify to the REST + * representation of an COARNotifySubmissionConfiguration and vice versa + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + **/ +@Component +public class SubmissionCOARNotifyConverter + implements DSpaceConverter { + + /** + * Convert a COARNotify to its REST representation + * @param modelObject - the COARNotify to convert + * @param projection - the projection + * @return the corresponding SubmissionCOARNotifyRest object + */ + @Override + public SubmissionCOARNotifyRest convert(final NotifySubmissionConfiguration modelObject, + final Projection projection) { + + SubmissionCOARNotifyRest submissionCOARNotifyRest = new SubmissionCOARNotifyRest(); + submissionCOARNotifyRest.setProjection(projection); + submissionCOARNotifyRest.setId(modelObject.getId()); + submissionCOARNotifyRest.setPatterns(modelObject.getPatterns()); + return submissionCOARNotifyRest; + } + + @Override + public Class getModelClass() { + return NotifySubmissionConfiguration.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java index 4bf4be72263a..df355dac1f15 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/converter/SuggestionTargetConverter.java @@ -27,7 +27,9 @@ public SuggestionTargetRest convert(SuggestionTarget target, Projection projecti SuggestionTargetRest targetRest = new SuggestionTargetRest(); targetRest.setProjection(projection); targetRest.setId(target.getID()); - targetRest.setDisplay(target.getTarget().getName()); + if (target != null && target.getTarget() != null) { + targetRest.setDisplay(target.getTarget().getName()); + } targetRest.setTotal(target.getTotal()); targetRest.setSource(target.getSource()); return targetRest; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java index a65ea13bc2c0..25e11f69164e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/DSpaceApiExceptionControllerAdvice.java @@ -92,7 +92,7 @@ protected void csrfTokenException(HttpServletRequest request, HttpServletRespons HttpServletResponse.SC_FORBIDDEN); } - @ExceptionHandler({IllegalArgumentException.class, MultipartException.class}) + @ExceptionHandler({IllegalArgumentException.class, MultipartException.class, InvalidLDNMessageException.class}) protected void handleWrongRequestException(HttpServletRequest request, HttpServletResponse response, Exception ex) throws IOException { sendErrorResponse(request, response, ex, "Request is invalid or incorrect", HttpServletResponse.SC_BAD_REQUEST); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/InvalidLDNMessageException.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/InvalidLDNMessageException.java new file mode 100644 index 000000000000..0542ef02cdf7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/exception/InvalidLDNMessageException.java @@ -0,0 +1,26 @@ +/** + * 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.exception; + + +/** + * This exception is thrown when the given LDN Message json is invalid + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class InvalidLDNMessageException extends RuntimeException { + + public InvalidLDNMessageException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidLDNMessageException(String message) { + super(message); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/DSpaceRequestContextFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/DSpaceRequestContextFilter.java index a9170652a251..99e65c199239 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/DSpaceRequestContextFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/filter/DSpaceRequestContextFilter.java @@ -17,8 +17,6 @@ import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A Servlet Filter whose only role is to clean up open Context objects in @@ -29,8 +27,6 @@ * @see ContextUtil */ public class DSpaceRequestContextFilter implements Filter { - private static final Logger log = LoggerFactory.getLogger(DSpaceRequestContextFilter.class); - @Override public void init(FilterConfig filterConfig) throws ServletException { //noop diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/contentreport/ContentReportSupportHalLinkFactory.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/contentreport/ContentReportSupportHalLinkFactory.java new file mode 100644 index 000000000000..b31feb3b0267 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/link/contentreport/ContentReportSupportHalLinkFactory.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.app.rest.link.contentreport; + +import java.util.LinkedList; + +import org.dspace.app.rest.ContentReportRestController; +import org.dspace.app.rest.link.HalLinkFactory; +import org.dspace.app.rest.model.hateoas.ContentReportSupportResource; +import org.springframework.data.domain.Pageable; +import org.springframework.hateoas.IanaLinkRelations; +import org.springframework.hateoas.Link; +import org.springframework.stereotype.Component; + +/** + * This class adds the self and report links to the ContentReportSupportResource. + * @author Jean-François Morin (Université Laval) + */ +@Component +public class ContentReportSupportHalLinkFactory + extends HalLinkFactory { + + @Override + protected void addLinks(ContentReportSupportResource halResource, Pageable pageable, LinkedList list) + throws Exception { + + list.add(buildLink(IanaLinkRelations.SELF.value(), getMethodOn().getContentReportSupport())); + list.add(buildLink("filteredcollections", getMethodOn().getFilteredCollections(null, null, null))); + list.add(buildLink("filtereditems", getMethodOn() + .getFilteredItems(null, null, null, null, null, null, null, null, null))); + } + + @Override + protected Class getControllerClass() { + return ContentReportRestController.class; + } + + @Override + protected Class getResourceClass() { + return ContentReportSupportResource.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java index 1450c12f909d..8521da970b27 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/login/impl/ResearcherProfileAutomaticClaim.java @@ -18,6 +18,8 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.login.PostLoggedInAction; import org.dspace.authorize.AuthorizeException; import org.dspace.content.Item; @@ -27,8 +29,6 @@ import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; import org.dspace.profile.service.ResearcherProfileService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.Assert; @@ -43,7 +43,7 @@ */ public class ResearcherProfileAutomaticClaim implements PostLoggedInAction { - private final static Logger LOGGER = LoggerFactory.getLogger(ResearcherProfileAutomaticClaim.class); + private final static Logger LOGGER = LogManager.getLogger(); @Autowired private ResearcherProfileService researcherProfileService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ContentReportSupportRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ContentReportSupportRest.java new file mode 100644 index 000000000000..3062fdf0fade --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ContentReportSupportRest.java @@ -0,0 +1,38 @@ +/** + * 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 org.dspace.app.rest.ContentReportRestController; + +public class ContentReportSupportRest extends BaseObjectRest { + + private static final long serialVersionUID = 9137258312781361906L; + public static final String NAME = "contentreport"; + public static final String CATEGORY = RestModel.CONTENT_REPORT; + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return ContentReportRestController.class; + } + + @Override + public String getType() { + return NAME; + } + + @Override + public String getTypePlural() { + return NAME; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionRest.java new file mode 100644 index 000000000000..47ff61db647d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionRest.java @@ -0,0 +1,104 @@ +/** + * 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.EnumMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.contentreport.Filter; +import org.dspace.contentreport.FilteredCollection; + +/** + * This class serves as a REST representation of a single Collection in a {@link FilteredCollectionsRest} + * from the DSpace statistics. It takes its values from a @link FilteredCollection} instance. + * It must not extend BaseObjectRest. + * + * @author Jean-François Morin (Université Laval) + */ +public class FilteredCollectionRest { + + public static final String NAME = "filtered-collection"; + public static final String CATEGORY = RestModel.CONTENT_REPORT; + + /** Name of the collection */ + private String label; + /** Handle of the collection, used to make it clickable from the generated report */ + private String handle; + /** Name of the owning community */ + @JsonProperty("community_label") + private String communityLabel; + /** Handle of the owning community, used to make it clickable from the generated report */ + @JsonProperty("community_handle") + private String communityHandle; + /** Total number of items in the collection */ + @JsonProperty("nb_total_items") + private int totalItems; + /** Number of filtered items per requested filter in the collection */ + private Map values = new EnumMap<>(Filter.class); + /** Number of items in the collection that match all requested filters */ + @JsonProperty("all_filters_value") + private int allFiltersValue; + + /** + * Builds a FilteredCollectionRest instance from a {@link FilteredCollection} instance. + * @param model the FilteredCollection instance that provides values to the + * FilteredCollectionRest instance to be created + * @return a FilteredCollectionRest instance built from the provided model object + */ + public static FilteredCollectionRest of(FilteredCollection model) { + Objects.requireNonNull(model); + + var coll = new FilteredCollectionRest(); + coll.label = model.getLabel(); + coll.handle = model.getHandle(); + coll.communityLabel = model.getCommunityLabel(); + coll.communityHandle = model.getCommunityHandle(); + coll.totalItems = model.getTotalItems(); + coll.allFiltersValue = model.getAllFiltersValue(); + Optional.ofNullable(model.getValues()).ifPresent(coll.values::putAll); + + return coll; + } + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public Map getValues() { + return values; + } + + public String getLabel() { + return label; + } + + public String getHandle() { + return handle; + } + + public String getCommunityLabel() { + return communityLabel; + } + + public String getCommunityHandle() { + return communityHandle; + } + + public int getTotalItems() { + return totalItems; + } + + public int getAllFiltersValue() { + return allFiltersValue; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionsQuery.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionsQuery.java new file mode 100644 index 000000000000..059516da13e3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionsQuery.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model; + +import java.util.Collection; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.dspace.contentreport.Filter; + +/** + * Structured query contents for the Filtered Collections report + * @author Jean-François Morin (Université Laval) + */ +public class FilteredCollectionsQuery { + + private Set filters = EnumSet.noneOf(Filter.class); + + /** + * Shortcut method that builds a FilteredCollectionsQuery instance + * from its building blocks. + * @param filters filters to apply to existing items. + * The filters mapping to true will be applied, others (either missing or + * mapping to false) will not. + * @return a FilteredCollectionsQuery instance built from the provided parameters + */ + public static FilteredCollectionsQuery of(Collection filters) { + var query = new FilteredCollectionsQuery(); + Optional.ofNullable(filters).ifPresent(query.filters::addAll); + return query; + } + + public Set getFilters() { + return filters; + } + + public void setFilters(Set filters) { + this.filters = filters; + } + + public String toQueryString() { + return filters.stream() + .map(f -> "filters=" + f.getId()) + .collect(Collectors.joining("&")); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionsRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionsRest.java new file mode 100644 index 000000000000..2d808e4a3189 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredCollectionsRest.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.app.rest.model; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import org.dspace.app.rest.ContentReportRestController; +import org.dspace.contentreport.FilteredCollections; + +/** + * This class serves as a REST representation of a Filtered Collections Report. + * The name must match that of the associated resource class (FilteredCollectionsResource) except for + * the suffix. This is why it is not named something like FilteredCollectionsReportRest. + * + * @author Jean-François Morin (Université Laval) + */ +public class FilteredCollectionsRest extends BaseObjectRest { + + private static final long serialVersionUID = -1109226348211060786L; + /** Type of instances of this class, used by the DSpace REST infrastructure */ + public static final String NAME = "filteredcollectionsreport"; + /** Category of instances of this class, used by the DSpace REST infrastructure */ + public static final String CATEGORY = RestModel.CONTENT_REPORT; + + /** Collections included in the report */ + private List collections = new ArrayList<>(); + /** Report summary */ + private FilteredCollectionRest summary; + + /** + * Builds a FilteredCollectionsRest instance from a {@link FilteredCollections} instance. + * Each underlying FilteredCollection is converted to a FilteredCollectionRest instance. + * @param model the FilteredCollections instance that provides values to the + * FilteredCollectionsRest instance to be created + * @return a FilteredCollectionsRest instance built from the provided model object + */ + public static FilteredCollectionsRest of(FilteredCollections model) { + var colls = new FilteredCollectionsRest(); + Optional.ofNullable(model.getCollections()).ifPresent(cs -> + cs.stream() + .map(FilteredCollectionRest::of) + .forEach(colls.collections::add)); + colls.summary = FilteredCollectionRest.of(model.getSummary()); + return colls; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + public Class getController() { + return ContentReportRestController.class; + } + + @Override + public String getType() { + return NAME; + } + + @Override + public String getTypePlural() { + return getType(); + } + + public List getCollections() { + return collections; + } + + public FilteredCollectionRest getSummary() { + return summary; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemRest.java new file mode 100644 index 000000000000..b5d512949210 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemRest.java @@ -0,0 +1,128 @@ +/** + * 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 com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Specialization of ItemRest dedicated to the Filtered Items report. + * This class adds the owning collection property required to properly + * display search results without compromising the expected behaviour + * of standard ItemRest instances, in all other contexts, especially + * when it comes to embedded contents, a criterion that is widely checked + * against in several integration tests. + * + * @author Jean-François Morin (jean-francois.morin@bibl.ulaval.ca) + */ +public class FilteredItemRest { + + public static final String NAME = "filtered-item"; + public static final String CATEGORY = RestAddressableModel.CONTENT_REPORT; + + public static final String OWNING_COLLECTION = "owningCollection"; + + private String uuid; + private String name; + private String handle; + MetadataRest metadata = new MetadataRest(); + private boolean inArchive = false; + private boolean discoverable = false; + private boolean withdrawn = false; + private Date lastModified = new Date(); + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + private String entityType = null; + private CollectionRest owningCollection; + + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHandle() { + return handle; + } + + public void setHandle(String handle) { + this.handle = handle; + } + + public MetadataRest getMetadata() { + return metadata; + } + + public void setMetadata(MetadataRest metadata) { + this.metadata = metadata; + } + + public boolean getInArchive() { + return inArchive; + } + + public void setInArchive(boolean inArchive) { + this.inArchive = inArchive; + } + + public boolean getDiscoverable() { + return discoverable; + } + + public void setDiscoverable(boolean discoverable) { + this.discoverable = discoverable; + } + + public boolean getWithdrawn() { + return withdrawn; + } + + public void setWithdrawn(boolean withdrawn) { + this.withdrawn = withdrawn; + } + + public Date getLastModified() { + return lastModified; + } + + public void setLastModified(Date lastModified) { + this.lastModified = lastModified; + } + + public String getEntityType() { + return entityType; + } + + public void setEntityType(String entityType) { + this.entityType = entityType; + } + + public CollectionRest getOwningCollection() { + return owningCollection; + } + + public void setOwningCollection(CollectionRest owningCollection) { + this.owningCollection = owningCollection; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsQueryPredicate.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsQueryPredicate.java new file mode 100644 index 000000000000..fe7192e56252 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsQueryPredicate.java @@ -0,0 +1,74 @@ +/** + * 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.Optional; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.contentreport.QueryOperator; + +/** + * Data structure representing a query predicate used by the Filtered Items report + * to filter items to retrieve. This version is specific to the REST layer and its + * property types are detached from the persistence layer. + * @see org.dspace.contentreport.QueryPredicate + * @author Jean-François Morin (Université Laval) + */ +public class FilteredItemsQueryPredicate { + + private String field; + private QueryOperator operator; + private String value; + + /** + * Shortcut method that builds a FilteredItemsQueryPredicate from a single field, an operator, and a value. + * @param field Predicate subject + * @param operator Predicate operator + * @param value Predicate object + * @return a FilteredItemsQueryPredicate instance built from the provided parameters + */ + public static FilteredItemsQueryPredicate of(String field, QueryOperator operator, String value) { + var predicate = new FilteredItemsQueryPredicate(); + predicate.field = field; + predicate.operator = operator; + predicate.value = value; + return predicate; + } + + /** + * Shortcut method that builds a FilteredItemsQueryPredicate from a colon-separated string value. + * @param value Colon-separated string value (field:operator:object or field:operator) + * @return a FilteredItemsQueryPredicate instance built from the provided value + */ + public static FilteredItemsQueryPredicate of(String value) { + String[] tokens = value.split("\\:"); + String field = tokens.length > 0 ? tokens[0].trim() : ""; + QueryOperator operator = tokens.length > 1 ? QueryOperator.get(tokens[1].trim()) : null; + String object = tokens.length > 2 ? StringUtils.trimToEmpty(tokens[2]) : ""; + return of(field, operator, object); + } + + public String getField() { + return field; + } + + public QueryOperator getOperator() { + return operator; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + String op = Optional.ofNullable(operator).map(QueryOperator::getCode).orElse(""); + return field + ":" + op + ":" + value; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsQueryRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsQueryRest.java new file mode 100644 index 000000000000..973dc9738b14 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsQueryRest.java @@ -0,0 +1,148 @@ +/** + * 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.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.contentreport.Filter; +import org.dspace.contentreport.QueryOperator; + +/** + * REST-based version of structured query contents for the Filtered Items report + * @author Jean-François Morin (Université Laval) + */ +public class FilteredItemsQueryRest { + + private List collections = new ArrayList<>(); + private List queryPredicates = new ArrayList<>(); + private int pageLimit; + private Set filters = EnumSet.noneOf(Filter.class); + private List additionalFields = new ArrayList<>(); + + /** + * Shortcut method that builds a FilteredItemsQueryRest instance + * from its building blocks. + * @param collectionUuids collection UUIDs to add + * @param predicates query predicates used to filter existing items + * @param pageLimit number of items per page + * @param filters filters to apply to existing items + * The filters mapping to true will be applied, others (either missing or + * mapping to false) will not. + * @param additionalFields additional fields to display in the resulting report + * @return a FilteredItemsQueryRest instance built from the provided parameters + */ + public static FilteredItemsQueryRest of(Collection collectionUuids, + Collection predicates, int pageLimit, + Collection filters, Collection additionalFields) { + var query = new FilteredItemsQueryRest(); + Optional.ofNullable(collectionUuids).ifPresent(query.collections::addAll); + Optional.ofNullable(predicates).ifPresent(query.queryPredicates::addAll); + query.pageLimit = pageLimit; + Optional.ofNullable(filters).ifPresent(query.filters::addAll); + Optional.ofNullable(additionalFields).ifPresent(query.additionalFields::addAll); + return query; + } + + public List getCollections() { + return collections; + } + + public void setCollections(List collections) { + this.collections = collections; + } + + public List getQueryPredicates() { + return queryPredicates; + } + + public void setQueryPredicates(List queryPredicates) { + this.queryPredicates = queryPredicates; + } + + public List getPredicateFields() { + if (queryPredicates == null) { + return Collections.emptyList(); + } + return queryPredicates.stream() + .map(FilteredItemsQueryPredicate::getField) + .collect(Collectors.toList()); + } + + public List getPredicateOperators() { + if (queryPredicates == null) { + return Collections.emptyList(); + } + return queryPredicates.stream() + .map(FilteredItemsQueryPredicate::getOperator) + .collect(Collectors.toList()); + } + + public List getPredicateValues() { + if (queryPredicates == null) { + return Collections.emptyList(); + } + return queryPredicates.stream() + .map(FilteredItemsQueryPredicate::getValue) + .map(s -> s == null ? "" : s) + .collect(Collectors.toList()); + } + + public int getPageLimit() { + return pageLimit; + } + + public void setPageLimit(int pageLimit) { + this.pageLimit = pageLimit; + } + + public Set getFilters() { + return filters; + } + + public void setFilters(Set filters) { + this.filters = filters; + } + + public List getAdditionalFields() { + return additionalFields; + } + + public void setAdditionalFields(List additionalFields) { + this.additionalFields = additionalFields; + } + + public String toQueryString() { + String colls = collections.stream() + .map(coll -> "collection=" + coll) + .collect(Collectors.joining("&")); + String preds = queryPredicates.stream() + .map(pred -> "queryPredicates=" + pred) + .collect(Collectors.joining("&")); + String pgLimit = "pageLimit=" + pageLimit; + String fltrs = filters.stream() + .map(e -> "filters=" + e.getId()) + .collect(Collectors.joining("&")); + String flds = additionalFields.stream() + .map(fld -> "additionalFields=" + fld) + .collect(Collectors.joining("&")); + + return Stream.of(colls, preds, pgLimit, fltrs, flds) + .filter(StringUtils::isNotBlank) + .collect(Collectors.joining("&")); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsRest.java new file mode 100644 index 000000000000..e879a9b9cc31 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/FilteredItemsRest.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.model; + +import java.util.ArrayList; +import java.util.List; + +import org.dspace.app.rest.ContentReportRestController; + +/** + * This class serves as a REST representation of a Filtered Items Report. + * The name must match that of the associated resource class (FilteredItemsResource) except for + * the suffix. This is why it is not named something like FilteredItemsReportRest. + * + * @author Jean-François Morin (Université Laval) + */ +public class FilteredItemsRest extends BaseObjectRest { + + private static final long serialVersionUID = -2483812920345013458L; + /** Type of instances of this class, used by the DSpace REST infrastructure */ + public static final String NAME = "filtereditemsreport"; + /** Category of instances of this class, used by the DSpace REST infrastructure */ + public static final String CATEGORY = RestModel.CONTENT_REPORT; + + /** Items included in the report */ + private List items = new ArrayList<>(); + /** Total item count (for pagination) */ + private long itemCount; + + /** + * Builds a FilteredItemsRest instance from a list of items and an total item count. + * To avoid adding a dependency to any Spring-managed service here, the items + * provided here are already converted to FilteredItemRest instances. + * @param items the items to add to the FilteredItemsRest instance to be created + * @param itemCount total number of items found regardless of any pagination constraint + * @return a FilteredItemsRest instance built from the provided data + */ + public static FilteredItemsRest of(List items, long itemCount) { + var itemsRest = new FilteredItemsRest(); + itemsRest.items.addAll(items); + itemsRest.itemCount = itemCount; + return itemsRest; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + /** + * Return controller class responsible for this Rest object + * + * @return Controller class responsible for this Rest object + */ + @Override + public Class getController() { + return ContentReportRestController.class; + } + + @Override + public String getType() { + return NAME; + } + + @Override + public String getTypePlural() { + return getType(); + } + + /** + * Returns a defensive copy of the items included in this report. + * + * @return the items included in this report + */ + public List getItems() { + return new ArrayList<>(items); + } + + public long getItemCount() { + return itemCount; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemFilterRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemFilterRest.java new file mode 100644 index 000000000000..5d46c62522e8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/ItemFilterRest.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.app.rest.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.app.rest.RestResourceController; + +/** + * The ItemFilter REST Resource + * + * @author mohamed eskander (mohamed.eskander at 4science.com) + */ +public class ItemFilterRest extends BaseObjectRest { + public static final String NAME = "itemfilter"; + public static final String PLURAL_NAME = "itemfilters"; + public static final String CATEGORY = RestAddressableModel.CONFIGURATION; + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public Class getController() { + return RestResourceController.class; + } + + @Override + public String getTypePlural() { + return PLURAL_NAME; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyQAEventMessageRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyQAEventMessageRest.java new file mode 100644 index 000000000000..e9acb3c7757e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyQAEventMessageRest.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.model; + +/** + * Implementation of {@link QAEventMessageRest} related to COAR NOTIFY events. + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ +public class NotifyQAEventMessageRest implements QAEventMessageRest { + + private String serviceName; + + private String serviceId; + + private String href; + + private String relationship; + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public String getServiceId() { + return serviceId; + } + + public void setServiceId(String serviceId) { + this.serviceId = serviceId; + } + + public String getHref() { + return href; + } + + public void setHref(String href) { + this.href = href; + } + + public String getRelationship() { + return relationship; + } + + public void setRelationship(String relationship) { + this.relationship = relationship; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyRequestStatusRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyRequestStatusRest.java new file mode 100644 index 000000000000..c8b5070de351 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyRequestStatusRest.java @@ -0,0 +1,83 @@ +/** + * 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.ArrayList; +import java.util.List; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import org.dspace.app.ldn.model.RequestStatus; +import org.dspace.app.rest.NotifyRequestStatusRestController; + + +/** + * Rest entity for LDN requests targeting items + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) + */ +@JsonPropertyOrder(value = { + "notifyStatus", + "itemuuid" +}) +public class NotifyRequestStatusRest extends RestAddressableModel { + + private static final long serialVersionUID = 1L; + public static final String CATEGORY = RestAddressableModel.LDN; + public static final String NAME = "notifyrequests"; + public static final String PLURAL_NAME = "notifyrequests"; + + private List notifyStatus; + private UUID itemuuid; + + public NotifyRequestStatusRest(NotifyRequestStatusRest instance) { + this.notifyStatus = instance.getNotifyStatus(); + } + + public NotifyRequestStatusRest() { + this.notifyStatus = new ArrayList(); + } + + public UUID getItemuuid() { + return itemuuid; + } + + public void setItemuuid(UUID itemuuid) { + this.itemuuid = itemuuid; + } + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public Class getController() { + return NotifyRequestStatusRestController.class; + } + + public List getNotifyStatus() { + return notifyStatus; + } + + public void setNotifyStatus(List notifyStatus) { + this.notifyStatus = notifyStatus; + } + + @Override + public String getTypePlural() { + return PLURAL_NAME; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceInboundPatternRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceInboundPatternRest.java new file mode 100644 index 000000000000..43090838695b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceInboundPatternRest.java @@ -0,0 +1,57 @@ +/** + * 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; + + +/** + * representation of the Notify Service Inbound Pattern + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceInboundPatternRest { + + /** + * https://notify.coar-repositories.org/patterns/ + */ + private String pattern; + + /** + * the id of a bean implementing the ItemFilter + */ + private String constraint; + + /** + * means that the pattern is triggered automatically + * by dspace if the item respect the filter + */ + private boolean automatic; + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public String getConstraint() { + return constraint; + } + + public void setConstraint(String constraint) { + this.constraint = constraint; + } + + public boolean isAutomatic() { + return automatic; + } + + public void setAutomatic(boolean automatic) { + this.automatic = automatic; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java new file mode 100644 index 000000000000..8a50c913d5c8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/NotifyServiceRest.java @@ -0,0 +1,129 @@ +/** + * 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.math.BigDecimal; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.app.rest.RestResourceController; + +/** + * The NotifyServiceEntity REST Resource + * + * @author mohamed eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceRest extends BaseObjectRest { + public static final String NAME = "ldnservice"; + public static final String PLURAL_NAME = "ldnservices"; + public static final String CATEGORY = RestAddressableModel.LDN; + + private String name; + private String description; + private String url; + private String ldnUrl; + private boolean enabled; + private BigDecimal score; + private String lowerIp; + private String upperIp; + + private List notifyServiceInboundPatterns; + + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + public Class getController() { + return RestResourceController.class; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getLdnUrl() { + return ldnUrl; + } + + public void setLdnUrl(String ldnUrl) { + this.ldnUrl = ldnUrl; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public List getNotifyServiceInboundPatterns() { + return notifyServiceInboundPatterns; + } + + public void setNotifyServiceInboundPatterns( + List notifyServiceInboundPatterns) { + this.notifyServiceInboundPatterns = notifyServiceInboundPatterns; + } + + public BigDecimal getScore() { + return score; + } + + public void setScore(BigDecimal score) { + this.score = score; + } + + public String getLowerIp() { + return lowerIp; + } + + public void setLowerIp(String lowerIp) { + this.lowerIp = lowerIp; + } + + public String getUpperIp() { + return upperIp; + } + + public void setUpperIp(String upperIp) { + this.upperIp = upperIp; + } + + @Override + public String getTypePlural() { + return PLURAL_NAME; + } + +} 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 index f3e99d0aa45f..f62bd19f2d41 100644 --- 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 @@ -25,7 +25,6 @@ public class QASourceRest extends BaseObjectRest { public static final String PLURAL_NAME = "qualityassurancesources"; public static final String CATEGORY = RestAddressableModel.INTEGRATION; - private String id; private Date lastEvent; private long totalEvents; @@ -49,14 +48,6 @@ public Class getController() { return RestResourceController.class; } - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - public Date getLastEvent() { return lastEvent; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java index 5775a4388e72..b575ddb59815 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/RestModel.java @@ -20,6 +20,7 @@ public interface RestModel extends Serializable { public static final String ROOT = "root"; + public static final String CONTENT_REPORT = "contentreport"; public static final String CORE = "core"; public static final String EPERSON = "eperson"; public static final String DISCOVER = "discover"; @@ -33,6 +34,7 @@ public interface RestModel extends Serializable { public static final String VERSIONING = "versioning"; public static final String AUTHENTICATION = "authn"; public static final String TOOLS = "tools"; + public static final String LDN = "ldn"; public static final String PID = "pid"; public String getType(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCOARNotifyRest.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCOARNotifyRest.java new file mode 100644 index 000000000000..3767186f7657 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/SubmissionCOARNotifyRest.java @@ -0,0 +1,70 @@ +/** + * 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.List; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.dspace.app.rest.RestResourceController; +import org.dspace.coarnotify.NotifyPattern; +import org.dspace.coarnotify.NotifySubmissionConfiguration; +/** + * This class is the REST representation of the COARNotifySubmissionConfiguration model object + * and acts as a data object for the SubmissionCOARNotifyResource class. + * + * Refer to {@link NotifySubmissionConfiguration} for explanation of the properties + */ +public class SubmissionCOARNotifyRest extends BaseObjectRest { + public static final String NAME = "submissioncoarnotifyconfig"; + public static final String PLURAL_NAME = "submissioncoarnotifyconfigs"; + public static final String CATEGORY = RestAddressableModel.CONFIGURATION; + + private String id; + + private List patterns; + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + public List getPatterns() { + return patterns; + } + + public void setPatterns(final List patterns) { + this.patterns = patterns; + } + + @JsonIgnore + @Override + public String getCategory() { + return CATEGORY; + } + + @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + public String getType() { + return NAME; + } + + @Override + @JsonIgnore + public Class getController() { + return RestResourceController.class; + } + + @Override + public String getTypePlural() { + return PLURAL_NAME; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ContentReportSupportResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ContentReportSupportResource.java new file mode 100644 index 000000000000..363a5e704a5b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ContentReportSupportResource.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.hateoas; + +import org.dspace.app.rest.model.ContentReportSupportRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; + +@RelNameDSpaceResource(ContentReportSupportRest.NAME) +public class ContentReportSupportResource extends HALResource { + public ContentReportSupportResource(ContentReportSupportRest content) { + super(content); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/FilteredCollectionsResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/FilteredCollectionsResource.java new file mode 100644 index 000000000000..9bb23464856e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/FilteredCollectionsResource.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.app.rest.model.hateoas; + +import org.dspace.app.rest.model.FilteredCollectionsRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +@RelNameDSpaceResource(FilteredCollectionsRest.NAME) +public class FilteredCollectionsResource extends DSpaceResource { + + public FilteredCollectionsResource(FilteredCollectionsRest data, Utils utils) { + super(data, utils); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/FilteredItemsResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/FilteredItemsResource.java new file mode 100644 index 000000000000..8758b9db5a74 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/FilteredItemsResource.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.app.rest.model.hateoas; + +import org.dspace.app.rest.model.FilteredItemsRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +@RelNameDSpaceResource(FilteredItemsRest.NAME) +public class FilteredItemsResource extends DSpaceResource { + + public FilteredItemsResource(FilteredItemsRest data, Utils utils) { + super(data, utils); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ItemFilterResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ItemFilterResource.java new file mode 100644 index 000000000000..666531e816c9 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/ItemFilterResource.java @@ -0,0 +1,25 @@ +/** + * 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.ItemFilterRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * ItemFilter Rest HAL Resource. The HAL Resource wraps the REST Resource adding + * support for the links and embedded resources + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@RelNameDSpaceResource(ItemFilterRest.NAME) +public class ItemFilterResource extends DSpaceResource { + public ItemFilterResource(ItemFilterRest data, Utils utils) { + super(data, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/NotifyServiceResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/NotifyServiceResource.java new file mode 100644 index 000000000000..8b2cf509d701 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/NotifyServiceResource.java @@ -0,0 +1,25 @@ +/** + * 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.NotifyServiceRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * NotifyService Rest HAL Resource. The HAL Resource wraps the REST Resource adding + * support for the links and embedded resources + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@RelNameDSpaceResource(NotifyServiceRest.NAME) +public class NotifyServiceResource extends DSpaceResource { + public NotifyServiceResource(NotifyServiceRest data, Utils utils) { + super(data, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCOARNotifyResource.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCOARNotifyResource.java new file mode 100644 index 000000000000..b49451f8e89b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/hateoas/SubmissionCOARNotifyResource.java @@ -0,0 +1,25 @@ +/** + * 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.SubmissionCOARNotifyRest; +import org.dspace.app.rest.model.hateoas.annotations.RelNameDSpaceResource; +import org.dspace.app.rest.utils.Utils; + +/** + * COARNotify HAL Resource. This resource adds the data from the REST object together with embedded objects + * and a set of links if applicable + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@RelNameDSpaceResource(SubmissionCOARNotifyRest.NAME) +public class SubmissionCOARNotifyResource extends DSpaceResource { + public SubmissionCOARNotifyResource(SubmissionCOARNotifyRest submissionCOARNotifyRest, Utils utils) { + super(submissionCOARNotifyRest, utils); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataNotify.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataNotify.java new file mode 100644 index 000000000000..0ae1fa57174a --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/model/step/DataNotify.java @@ -0,0 +1,49 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.model.step; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Java Bean to expose the COAR Notify Section during in progress submission. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class DataNotify implements SectionData { + + private Map> patterns = new HashMap<>(); + + public DataNotify() { + + } + + @JsonAnySetter + public void add(String key, List values) { + patterns.put(key, values); + } + + public DataNotify(Map> patterns) { + this.patterns = patterns; + } + + @JsonIgnore + public void setPatterns(Map> patterns) { + this.patterns = patterns; + } + + @JsonAnyGetter + public Map> getPatterns() { + return patterns; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java index c5e09757306e..8d4d44f8282f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/AuthorizationRestRepository.java @@ -18,6 +18,8 @@ import java.util.stream.Collectors; import org.apache.commons.lang3.ObjectUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.authorization.Authorization; @@ -34,8 +36,6 @@ import org.dspace.discovery.SearchServiceException; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -52,7 +52,7 @@ @Component(AuthorizationRest.CATEGORY + "." + AuthorizationRest.PLURAL_NAME) public class AuthorizationRestRepository extends DSpaceRestRepository { - private static final Logger log = LoggerFactory.getLogger(AuthorizationRestRepository.class); + private static final Logger log = LogManager.getLogger(); @Autowired private AuthorizationFeatureService authorizationFeatureService; @@ -79,7 +79,7 @@ public AuthorizationRest findOne(Context context, String id) { try { featureName = authorizationRestUtil.getFeatureName(id); } catch (IllegalArgumentException e) { - log.warn(e.getMessage(), e); + log.warn(e::getMessage, e); return null; } try { @@ -87,7 +87,7 @@ public AuthorizationRest findOne(Context context, String id) { try { object = authorizationRestUtil.getObject(context, id); } catch (IllegalArgumentException e) { - log.warn("Object informations not found in the specified id " + id, e); + log.warn("Object informations not found in the specified id {}", id, e); return null; } @@ -104,7 +104,7 @@ public AuthorizationRest findOne(Context context, String id) { try { user = authorizationRestUtil.getEperson(context, id); } catch (IllegalArgumentException e) { - log.warn("Invalid eperson informations in the specified id " + id, e); + log.warn("Invalid eperson informations in the specified id {}", id, e); return null; } EPerson currUser = context.getCurrentUser(); @@ -136,7 +136,7 @@ public AuthorizationRest findOne(Context context, String id) { /** * It returns the list of matching available authorizations granted to the specified eperson or to the anonymous * user. Only administrators and the user identified by the epersonUuid parameter can access this method - * + * * @param uri * the uri of the object to check the authorization against * @param epersonUuid @@ -283,7 +283,7 @@ private List findByObjectAndFeature( /** * Return the user specified in the request parameter if valid - * + * * @param context * @param epersonUuid * @return diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ContentReportRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ContentReportRestRepository.java new file mode 100644 index 000000000000..567fa79a448f --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ContentReportRestRepository.java @@ -0,0 +1,97 @@ +/** + * 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 java.util.Set; +import java.util.stream.Collectors; + +import org.dspace.app.rest.converter.FilteredItemConverter; +import org.dspace.app.rest.model.ContentReportSupportRest; +import org.dspace.app.rest.model.FilteredCollectionsQuery; +import org.dspace.app.rest.model.FilteredCollectionsRest; +import org.dspace.app.rest.model.FilteredItemRest; +import org.dspace.app.rest.model.FilteredItemsQueryPredicate; +import org.dspace.app.rest.model.FilteredItemsQueryRest; +import org.dspace.app.rest.model.FilteredItemsRest; +import org.dspace.app.rest.projection.Projection; +import org.dspace.content.MetadataField; +import org.dspace.contentreport.Filter; +import org.dspace.contentreport.FilteredCollection; +import org.dspace.contentreport.FilteredCollections; +import org.dspace.contentreport.FilteredItems; +import org.dspace.contentreport.FilteredItemsQuery; +import org.dspace.contentreport.QueryPredicate; +import org.dspace.contentreport.service.ContentReportService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +/** + * This repository serves the content reports ported from DSpace 6.x + * (Filtered Collections and Filtered Items). + * @author Jean-François Morin (Université Laval) + */ +@Component(ContentReportSupportRest.CATEGORY + "." + ContentReportSupportRest.NAME) +public class ContentReportRestRepository extends AbstractDSpaceRestRepository { + + @Autowired + private ContentReportService contentReportService; + @Autowired + private FilteredItemConverter itemConverter; + + public ContentReportSupportRest getContentReportSupport() { + return new ContentReportSupportRest(); + } + + public FilteredCollectionsRest findFilteredCollections(Context context, FilteredCollectionsQuery query) { + Set filters = query.getFilters(); + + List colls = contentReportService.findFilteredCollections(context, filters); + FilteredCollections report = FilteredCollections.of(colls); + + FilteredCollectionsRest reportRest = FilteredCollectionsRest.of(report); + reportRest.setId("filteredcollections"); + return reportRest; + } + + public FilteredItemsRest findFilteredItems(Context context, FilteredItemsQueryRest queryRest, Pageable pageable) { + List predicates = queryRest.getQueryPredicates().stream() + .map(pred -> convertPredicate(context, pred)) + .collect(Collectors.toList()); + FilteredItemsQuery query = new FilteredItemsQuery(); + query.setCollections(queryRest.getCollections()); + query.setQueryPredicates(predicates); + query.setFilters(queryRest.getFilters()); + query.setAdditionalFields(queryRest.getAdditionalFields()); + query.setOffset(pageable.getOffset()); + query.setPageLimit(pageable.getPageSize()); + + FilteredItems items = contentReportService.findFilteredItems(context, query); + + List filteredItemsRest = items.getItems().stream() + .map(item -> itemConverter.convert(item, Projection.DEFAULT)) + .collect(Collectors.toList()); + FilteredItemsRest report = FilteredItemsRest.of(filteredItemsRest, items.getItemCount()); + report.setId("filtereditems"); + + return report; + } + + private QueryPredicate convertPredicate(Context context, FilteredItemsQueryPredicate predicate) { + try { + List fields = contentReportService.getMetadataFields(context, predicate.getField()); + return QueryPredicate.of(fields, predicate.getOperator(), predicate.getValue()); + } catch (SQLException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemFilterRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemFilterRestRepository.java new file mode 100644 index 000000000000..cb8dc6112af1 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/ItemFilterRestRepository.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 org.dspace.app.ldn.ItemFilter; +import org.dspace.app.rest.model.ItemFilterRest; +import org.dspace.content.ItemFilterService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +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; + +/** + * This is the repository responsible to manage ItemFilter Rest object + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ + +@Component(ItemFilterRest.CATEGORY + "." + ItemFilterRest.PLURAL_NAME) +public class ItemFilterRestRepository extends DSpaceRestRepository { + + @Autowired + private ItemFilterService itemFilterService; + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public ItemFilterRest findOne(Context context, String id) { + ItemFilter itemFilter = itemFilterService.findOne(id); + + if (itemFilter == null) { + throw new ResourceNotFoundException( + "No such logical item filter: " + id); + } + + return converter.toRest(itemFilter, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + public Page findAll(Context context, Pageable pageable) { + return converter.toRestPage(itemFilterService.findAll(), + pageable, utils.obtainProjection()); + } + + @Override + public Class getDomainClass() { + return ItemFilterRest.class; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java new file mode 100644 index 000000000000..c3b15ad77f20 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/NotifyServiceRestRepository.java @@ -0,0 +1,208 @@ +/** + * 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 static java.lang.String.format; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.Parameter; +import org.dspace.app.rest.SearchRestMethod; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.NotifyServiceInboundPatternRest; +import org.dspace.app.rest.model.NotifyServiceRest; +import org.dspace.app.rest.model.patch.Patch; +import org.dspace.app.rest.repository.patch.ResourcePatch; +import org.dspace.authorize.AuthorizeException; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +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; + + +/** + * This is the repository responsible to manage NotifyService Rest object + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ + +@Component(NotifyServiceRest.CATEGORY + "." + NotifyServiceRest.PLURAL_NAME) +public class NotifyServiceRestRepository extends DSpaceRestRepository { + + @Autowired + private NotifyService notifyService; + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + ResourcePatch resourcePatch; + + @Override + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public NotifyServiceRest findOne(Context context, Integer id) { + try { + NotifyServiceEntity notifyServiceEntity = notifyService.find(context, id); + if (notifyServiceEntity == null) { + throw new ResourceNotFoundException("The notifyService for ID: " + id + " could not be found"); + } + return converter.toRest(notifyServiceEntity, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public Page findAll(Context context, Pageable pageable) { + try { + return converter.toRestPage(notifyService.findAll(context), pageable, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected NotifyServiceRest createAndReturn(Context context) throws AuthorizeException, SQLException { + HttpServletRequest req = getRequestService().getCurrentRequest().getHttpServletRequest(); + ObjectMapper mapper = new ObjectMapper(); + NotifyServiceRest notifyServiceRest; + try { + ServletInputStream input = req.getInputStream(); + notifyServiceRest = mapper.readValue(input, NotifyServiceRest.class); + } catch (IOException e1) { + throw new UnprocessableEntityException("Error parsing request body", e1); + } + + if (notifyServiceRest.getScore() != null) { + if (notifyServiceRest.getScore().compareTo(java.math.BigDecimal.ZERO) == -1 || + notifyServiceRest.getScore().compareTo(java.math.BigDecimal.ONE) == 1) { + throw new UnprocessableEntityException(format("Score out of range [0, 1] %s", + notifyServiceRest.getScore().setScale(4).toPlainString())); + } + } + + if (notifyService.findByLdnUrl(context,notifyServiceRest.getLdnUrl()) != null) { + throw new UnprocessableEntityException(format("LDN url already in use %s", + notifyServiceRest.getLdnUrl())); + } + + NotifyServiceEntity notifyServiceEntity = notifyService.create(context); + notifyServiceEntity.setName(notifyServiceRest.getName()); + notifyServiceEntity.setDescription(notifyServiceRest.getDescription()); + notifyServiceEntity.setUrl(notifyServiceRest.getUrl()); + notifyServiceEntity.setLdnUrl(notifyServiceRest.getLdnUrl()); + notifyServiceEntity.setEnabled(notifyServiceRest.isEnabled()); + notifyServiceEntity.setLowerIp(notifyServiceRest.getLowerIp()); + notifyServiceEntity.setUpperIp(notifyServiceRest.getUpperIp()); + + if (notifyServiceRest.getNotifyServiceInboundPatterns() != null) { + appendNotifyServiceInboundPatterns(context, notifyServiceEntity, + notifyServiceRest.getNotifyServiceInboundPatterns()); + } + + notifyServiceEntity.setScore(notifyServiceRest.getScore()); + + notifyService.update(context, notifyServiceEntity); + + return converter.toRest(notifyServiceEntity, utils.obtainProjection()); + } + + private void appendNotifyServiceInboundPatterns(Context context, NotifyServiceEntity notifyServiceEntity, + List inboundPatternRests) throws SQLException { + + List inboundPatterns = new ArrayList<>(); + + for (NotifyServiceInboundPatternRest inboundPatternRest : inboundPatternRests) { + NotifyServiceInboundPattern inboundPattern = inboundPatternService.create(context, notifyServiceEntity); + inboundPattern.setPattern(inboundPatternRest.getPattern()); + inboundPattern.setConstraint(inboundPatternRest.getConstraint()); + inboundPattern.setAutomatic(inboundPatternRest.isAutomatic()); + + inboundPatterns.add(inboundPattern); + } + + notifyServiceEntity.setInboundPatterns(inboundPatterns); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected void patch(Context context, HttpServletRequest request, String apiCategory, String model, Integer id, + Patch patch) throws AuthorizeException, SQLException { + NotifyServiceEntity notifyServiceEntity = notifyService.find(context, id); + if (notifyServiceEntity == null) { + throw new ResourceNotFoundException( + NotifyServiceRest.CATEGORY + "." + NotifyServiceRest.NAME + " with id: " + id + " not found"); + } + resourcePatch.patch(context, notifyServiceEntity, patch.getOperations()); + notifyService.update(context, notifyServiceEntity); + } + + @Override + @PreAuthorize("hasAuthority('ADMIN')") + protected void delete(Context context, Integer id) throws AuthorizeException { + try { + NotifyServiceEntity notifyServiceEntity = notifyService.find(context, id); + if (notifyServiceEntity == null) { + throw new ResourceNotFoundException(NotifyServiceRest.CATEGORY + "." + + NotifyServiceRest.NAME + " with id: " + id + " not found"); + } + notifyService.delete(context, notifyServiceEntity); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @SearchRestMethod(name = "byLdnUrl") + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public NotifyServiceRest findByLdnUrl(@Parameter(value = "ldnUrl", required = true) String ldnUrl) { + try { + NotifyServiceEntity notifyServiceEntity = notifyService.findByLdnUrl(obtainContext(), ldnUrl); + if (notifyServiceEntity == null) { + return null; + } + return converter.toRest(notifyServiceEntity, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @SearchRestMethod(name = "byInboundPattern") + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public Page findManualServicesByInboundPattern( + @Parameter(value = "pattern", required = true) String pattern, + Pageable pageable) { + try { + List notifyServiceEntities = + notifyService.findManualServicesByInboundPattern(obtainContext(), pattern); + + return converter.toRestPage(notifyServiceEntities, pageable, utils.obtainProjection()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + @Override + public Class getDomainClass() { + return NotifyServiceRest.class; + } +} 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 index 7e80fd1c92fb..44be4ececaa1 100644 --- 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 @@ -91,7 +91,8 @@ public QAEventRest findOne(Context context, String id) { @SearchRestMethod(name = "findByTopic") @PreAuthorize("hasAuthority('AUTHENTICATED')") - public Page findByTopic(@Parameter(value = "topic", required = true) String topic, Pageable pageable) { + public Page findByTopic(@Parameter(value = "topic", required = true) String topic, + Pageable pageable) { Context context = obtainContext(); String[] topicIdSplitted = topic.split(":", 3); if (topicIdSplitted.length < 2) { @@ -101,19 +102,21 @@ public Page findByTopic(@Parameter(value = "topic", required = true String topicName = topicIdSplitted[1].replaceAll("!", "/"); UUID target = topicIdSplitted.length == 3 ? UUID.fromString(topicIdSplitted[2]) : null; List qaEvents = qaEventService.findEventsByTopicAndTarget(context, sourceName, topicName, target, - pageable.getOffset(), - pageable.getPageSize()); + pageable.getOffset(), pageable.getPageSize()); long count = qaEventService.countEventsByTopicAndTarget(context, sourceName, topicName, target); + if (qaEvents == null) { + return null; + } return converter.toRestPage(qaEvents, pageable, count, utils.obtainProjection()); } @Override @PreAuthorize("hasPermission(#id, 'QUALITYASSURANCEEVENT', 'DELETE')") - protected void delete(Context context, String eventId) throws AuthorizeException { - Item item = findTargetItem(context, eventId); + protected void delete(Context context, String id) throws AuthorizeException { + Item item = findTargetItem(context, id); EPerson eperson = context.getCurrentUser(); - qaEventService.deleteEventByEventId(eventId); - qaEventDao.storeEvent(context, eventId, eperson, item); + qaEventService.deleteEventByEventId(id); + qaEventDao.storeEvent(context, id, eperson, item); } @Override 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 index 222fbd218ebe..067958e96c39 100644 --- 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 @@ -54,7 +54,8 @@ public QATopicRest getTopic(@Nullable HttpServletRequest request, String id, @Nu } String source = qaEvent.getSource(); String topicName = qaEvent.getTopic(); - QATopic topic = qaEventService.findTopicBySourceAndNameAndTarget(context, source, topicName, null); + QATopic topic = qaEventService + .findTopicBySourceAndNameAndTarget(context, source, topicName, null); if (topic == null) { throw new ResourceNotFoundException("No topic found with source: " + source + " topic: " + topicName); } 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 index b6542229b15d..ccd85e81e749 100644 --- 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 @@ -10,6 +10,8 @@ import java.util.List; import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.Parameter; import org.dspace.app.rest.SearchRestMethod; import org.dspace.app.rest.exception.RepositoryMethodNotImplementedException; @@ -38,6 +40,8 @@ public class QATopicRestRepository extends DSpaceRestRepository findAll(Context context, Pageable pageable) { throw new RepositoryMethodNotImplementedException("Method not allowed!", ""); @@ -58,7 +62,7 @@ public QATopicRest findOne(Context context, String id) { } @SearchRestMethod(name = "bySource") - @PreAuthorize("hasAuthority('AUTHENTICATED')") + @PreAuthorize("hasPermission(#source, 'QUALITYASSURANCETOPIC', 'READ')") public Page findBySource(@Parameter(value = "source", required = true) String source, Pageable pageable) { Context context = obtainContext(); @@ -76,7 +80,7 @@ public Page findBySource(@Parameter(value = "source", required = tr } @SearchRestMethod(name = "byTarget") - @PreAuthorize("hasAuthority('AUTHENTICATED')") + @PreAuthorize("hasPermission(#target, 'ITEM', 'READ')") public Page findByTarget(@Parameter(value = "target", required = true) UUID target, @Parameter(value = "source", required = true) String source, Pageable pageable) { Context context = obtainContext(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCoarNotifyRestRepository.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCoarNotifyRestRepository.java new file mode 100644 index 000000000000..305c1a4f2912 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/SubmissionCoarNotifyRestRepository.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository; + +import org.dspace.app.rest.model.SubmissionCOARNotifyRest; +import org.dspace.coarnotify.NotifySubmissionConfiguration; +import org.dspace.coarnotify.service.SubmissionNotifyService; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +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; + +/** + * This is the repository that is responsible to manage Submission COAR Notify Rest objects + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Component(SubmissionCOARNotifyRest.CATEGORY + "." + SubmissionCOARNotifyRest.PLURAL_NAME) +public class SubmissionCoarNotifyRestRepository extends DSpaceRestRepository { + + @Autowired + protected SubmissionNotifyService submissionCOARNotifyService; + + @Override + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public SubmissionCOARNotifyRest findOne(final Context context, final String id) { + NotifySubmissionConfiguration coarNotifySubmissionConfiguration = submissionCOARNotifyService.findOne(id); + if (coarNotifySubmissionConfiguration == null) { + throw new ResourceNotFoundException( + "No COAR Notify Submission Configuration found for ID: " + id ); + } + return converter.toRest(coarNotifySubmissionConfiguration, utils.obtainProjection()); + } + + @Override + @PreAuthorize("hasAuthority('AUTHENTICATED')") + public Page findAll(final Context context, final Pageable pageable) { + return converter.toRestPage(submissionCOARNotifyService.findAll(), + pageable, utils.obtainProjection()); + } + + @Override + public Class getDomainClass() { + return SubmissionCOARNotifyRest.class; + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java index 0842746f329d..9864dae09d28 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/PatchOperation.java @@ -59,7 +59,7 @@ public void checkOperationValue(Object value) { * @return the original or derived boolean value * @throws DSpaceBadRequestException */ - Boolean getBooleanOperationValue(Object value) { + protected Boolean getBooleanOperationValue(Object value) { Boolean bool; if (value instanceof String) { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionAddOperation.java new file mode 100644 index 000000000000..0f973a976043 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionAddOperation.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.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Description Add patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "add", + * "path": "/description", + * "value": "description value" + * }]' + * + */ +@Component +public class NotifyServiceDescriptionAddOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/description"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object description = operation.getValue(); + if (description == null | !(description instanceof String)) { + throw new UnprocessableEntityException("The /description value must be a string"); + } + + checkNonExistingDescriptionValue(notifyServiceEntity); + notifyServiceEntity.setDescription((String) description); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Throws PatchBadRequestException if a value is already set in the /description path. + * + * @param notifyServiceEntity the notifyServiceEntity to update + + */ + void checkNonExistingDescriptionValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getDescription() != null) { + throw new DSpaceBadRequestException("Attempting to add a value to an already existing path."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionRemoveOperation.java new file mode 100644 index 000000000000..18e9515b0fb3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionRemoveOperation.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Description Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "/description" + * }]' + * + */ +@Component +public class NotifyServiceDescriptionRemoveOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/description"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + notifyServiceEntity.setDescription(null); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionReplaceOperation.java new file mode 100644 index 000000000000..8f7eedc75831 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceDescriptionReplaceOperation.java @@ -0,0 +1,74 @@ +/** + * 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.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Description Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/description", + * "value": "description value" + * }]' + * + */ +@Component +public class NotifyServiceDescriptionReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/description"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + + Object description = operation.getValue(); + if (description == null | !(description instanceof String)) { + throw new UnprocessableEntityException("The /description value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setDescription((String) description); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the description of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getDescription() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (description)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceEnabledReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceEnabledReplaceOperation.java new file mode 100644 index 000000000000..ccfa6e90200d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceEnabledReplaceOperation.java @@ -0,0 +1,66 @@ +/** + * 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.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Enabled Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/enabled" + * }]' + * + */ +@Component +public class NotifyServiceEnabledReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/enabled"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + Boolean enabled = getBooleanOperationValue(operation.getValue()); + + if (supports(notifyServiceEntity, operation)) { + notifyServiceEntity.setEnabled(enabled); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceEnabledReplaceOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternAutomaticReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternAutomaticReplaceOperation.java new file mode 100644 index 000000000000..0f3d8c394d7d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternAutomaticReplaceOperation.java @@ -0,0 +1,83 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Automatic Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "notifyServiceInboundPatterns[index]/automatic" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternAutomaticReplaceOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/automatic"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + Boolean automatic = getBooleanOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern inboundPattern = inboundPatterns.get(index); + inboundPattern.setAutomatic(automatic); + inboundPatternService.update(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternAutomaticReplaceOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + path.startsWith(NOTIFY_SERVICE_INBOUND_PATTERNS + "[") && + path.endsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintAddOperation.java new file mode 100644 index 000000000000..e82243f9a7ae --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintAddOperation.java @@ -0,0 +1,95 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Constraint Add patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "add", + * "path": "notifyServiceInboundPatterns[index]/constraint" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternConstraintAddOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/constraint"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern inboundPattern = inboundPatterns.get(index); + Object constraint = operation.getValue(); + if (constraint == null | !(constraint instanceof String)) { + throw new UnprocessableEntityException("The /constraint value must be a string"); + } + + checkNonExistingConstraintValue(inboundPattern); + inboundPattern.setConstraint((String) constraint); + inboundPatternService.update(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternConstraintAddOperation does not support this operation"); + } + } + + private void checkNonExistingConstraintValue(NotifyServiceInboundPattern inboundPattern) { + if (inboundPattern.getConstraint() != null) { + throw new DSpaceBadRequestException("Attempting to add a value to an already existing path."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && + path.startsWith(NOTIFY_SERVICE_INBOUND_PATTERNS + "[") && + path.endsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintRemoveOperation.java new file mode 100644 index 000000000000..52d0a6bf730f --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintRemoveOperation.java @@ -0,0 +1,81 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Constraint Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "notifyServiceInboundPatterns[index]/constraint" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternConstraintRemoveOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/constraint"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern inboundPattern = inboundPatterns.get(index); + inboundPattern.setConstraint(null); + inboundPatternService.update(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternConstraintRemoveOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + path.startsWith(NOTIFY_SERVICE_INBOUND_PATTERNS + "[") && + path.endsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintReplaceOperation.java new file mode 100644 index 000000000000..6faaadbfacde --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternConstraintReplaceOperation.java @@ -0,0 +1,95 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Constraint Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "notifyServiceInboundPatterns[index]/constraint" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternConstraintReplaceOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/constraint"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern inboundPattern = inboundPatterns.get(index); + Object constraint = operation.getValue(); + if (constraint == null | !(constraint instanceof String)) { + throw new UnprocessableEntityException("The /constraint value must be a string"); + } + + checkModelForExistingValue(inboundPattern); + inboundPattern.setConstraint((String) constraint); + inboundPatternService.update(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternConstraintReplaceOperation does not support this operation"); + } + } + + private void checkModelForExistingValue(NotifyServiceInboundPattern inboundPattern) { + if (inboundPattern.getConstraint() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (constraint)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + path.startsWith(NOTIFY_SERVICE_INBOUND_PATTERNS + "[") && + path.endsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternPatternAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternPatternAddOperation.java new file mode 100644 index 000000000000..17f92057f900 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternPatternAddOperation.java @@ -0,0 +1,95 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Pattern Add patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "add", + * "path": "notifyServiceInboundPatterns[index]/pattern" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternPatternAddOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/pattern"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern inboundPattern = inboundPatterns.get(index); + Object pattern = operation.getValue(); + if (pattern == null | !(pattern instanceof String)) { + throw new UnprocessableEntityException("The /pattern value must be a string"); + } + + checkNonExistingPatternValue(inboundPattern); + inboundPattern.setPattern((String) pattern); + inboundPatternService.update(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternPatternAddOperation does not support this operation"); + } + } + + private void checkNonExistingPatternValue(NotifyServiceInboundPattern inboundPattern) { + if (inboundPattern.getPattern() != null) { + throw new DSpaceBadRequestException("Attempting to add a value to an already existing path."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && + path.startsWith(NOTIFY_SERVICE_INBOUND_PATTERNS + "[") && + path.endsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternPatternReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternPatternReplaceOperation.java new file mode 100644 index 000000000000..5c32cec62b20 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternPatternReplaceOperation.java @@ -0,0 +1,95 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Pattern Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "notifyServiceInboundPatterns[index]/pattern" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternPatternReplaceOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = "/pattern"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern inboundPattern = inboundPatterns.get(index); + Object pattern = operation.getValue(); + if (pattern == null | !(pattern instanceof String)) { + throw new UnprocessableEntityException("The /pattern value must be a string"); + } + + checkModelForExistingValue(inboundPattern); + inboundPattern.setPattern((String) pattern); + inboundPatternService.update(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternPatternReplaceOperation does not support this operation"); + } + } + + private void checkModelForExistingValue(NotifyServiceInboundPattern inboundPattern) { + if (inboundPattern.getPattern() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (pattern)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + path.startsWith(NOTIFY_SERVICE_INBOUND_PATTERNS + "[") && + path.endsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternRemoveOperation.java new file mode 100644 index 000000000000..45b9dff74d22 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternRemoveOperation.java @@ -0,0 +1,79 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound pattern Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "notifyServiceInboundPatterns[index]" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternRemoveOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = NOTIFY_SERVICE_INBOUND_PATTERNS + "["; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + inboundPatternService.delete(context, inboundPatterns.get(index)); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternRemoveOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + path.startsWith(OPERATION_PATH) && + path.endsWith("]")); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternReplaceOperation.java new file mode 100644 index 000000000000..65e4bd2eedc3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternReplaceOperation.java @@ -0,0 +1,89 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Replace One patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "notifyServiceInboundPatterns[index]", + * "value": {"pattern":"patternA","constraint":"itemFilterA","automatic":"false"} + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternReplaceOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = NOTIFY_SERVICE_INBOUND_PATTERNS + "["; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + int index = notifyServicePatchUtils.extractIndexFromOperation(operation); + + List inboundPatterns = notifyServiceEntity.getInboundPatterns(); + + if (index >= inboundPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceInboundPattern patchInboundPattern = + notifyServicePatchUtils.extractNotifyServiceInboundPatternFromOperation(operation); + + NotifyServiceInboundPattern existedInboundPattern = inboundPatterns.get(index); + + existedInboundPattern.setPattern(patchInboundPattern.getPattern()); + existedInboundPattern.setConstraint(patchInboundPattern.getConstraint()); + existedInboundPattern.setAutomatic(patchInboundPattern.isAutomatic()); + inboundPatternService.update(context, existedInboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternReplaceOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + path.startsWith(OPERATION_PATH) && + path.endsWith("]")); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsAddOperation.java new file mode 100644 index 000000000000..a734bb5a5513 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsAddOperation.java @@ -0,0 +1,87 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; + +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Add patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "add", + * "path": "notifyServiceInboundPatterns/-", + * "value": {"pattern":"patternA","constraint":"itemFilterA","automatic":"false"} + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternsAddOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = NOTIFY_SERVICE_INBOUND_PATTERNS + "/-"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + NotifyServiceInboundPattern patchInboundPattern = + notifyServicePatchUtils.extractNotifyServiceInboundPatternFromOperation(operation); + + NotifyServiceInboundPattern persistInboundPattern = inboundPatternService.findByServiceAndPattern( + context, notifyServiceEntity, patchInboundPattern.getPattern()); + + if (persistInboundPattern != null && (StringUtils.isNotBlank(persistInboundPattern.getConstraint()) + && persistInboundPattern.getConstraint().equals(patchInboundPattern + .getConstraint()))) { + throw new DSpaceBadRequestException("the provided InboundPattern is already existed"); + } + + NotifyServiceInboundPattern inboundPattern = + inboundPatternService.create(context, notifyServiceEntity); + inboundPattern.setPattern(patchInboundPattern.getPattern()); + inboundPattern.setConstraint(patchInboundPattern.getConstraint()); + inboundPattern.setAutomatic(patchInboundPattern.isAutomatic()); + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternsAddOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && + operation.getPath().trim().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsRemoveOperation.java new file mode 100644 index 000000000000..76890f792ea3 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsRemoveOperation.java @@ -0,0 +1,71 @@ +/** + * 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.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Remove All patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "notifyServiceInboundPatterns" + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternsRemoveOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = NOTIFY_SERVICE_INBOUND_PATTERNS; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + if (supports(notifyServiceEntity, operation)) { + try { + for (NotifyServiceInboundPattern inboundPattern : notifyServiceEntity.getInboundPatterns()) { + inboundPatternService.delete(context, inboundPattern); + } + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternsRemoveOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + String path = operation.getPath().trim(); + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + path.startsWith(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsReplaceOperation.java new file mode 100644 index 000000000000..2dd98e7d172e --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceInboundPatternsReplaceOperation.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.patch.operation.ldn; + +import static org.dspace.app.rest.repository.patch.operation.ldn.NotifyServicePatchUtils.NOTIFY_SERVICE_INBOUND_PATTERNS; + +import java.sql.SQLException; +import java.util.List; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.ldn.service.NotifyServiceInboundPatternService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Inbound patterns Replace All patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "notifyServiceInboundPatterns", + * "value": [{"pattern":"patternA","constraint":"itemFilterA","automatic":"false"}] + * }]' + * + */ +@Component +public class NotifyServiceInboundPatternsReplaceOperation extends PatchOperation { + + @Autowired + private NotifyServiceInboundPatternService inboundPatternService; + + @Autowired + private NotifyServicePatchUtils notifyServicePatchUtils; + + private static final String OPERATION_PATH = NOTIFY_SERVICE_INBOUND_PATTERNS; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) { + checkOperationValue(operation.getValue()); + if (supports(notifyServiceEntity, operation)) { + try { + List patchInboundPatterns = + notifyServicePatchUtils.extractNotifyServiceInboundPatternsFromOperation(operation); + + notifyServiceEntity.getInboundPatterns().forEach(inboundPattern -> { + try { + inboundPatternService.delete(context, inboundPattern); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + + for (NotifyServiceInboundPattern patchInboundPattern : patchInboundPatterns) { + NotifyServiceInboundPattern inboundPattern = + inboundPatternService.create(context, notifyServiceEntity); + inboundPattern.setPattern(patchInboundPattern.getPattern()); + inboundPattern.setConstraint(patchInboundPattern.getConstraint()); + inboundPattern.setAutomatic(patchInboundPattern.isAutomatic()); + } + } catch (SQLException e) { + throw new RuntimeException(e.getMessage(), e); + } + return notifyServiceEntity; + } else { + throw new DSpaceBadRequestException( + "NotifyServiceInboundPatternsReplaceOperation does not support this operation"); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLdnUrlReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLdnUrlReplaceOperation.java new file mode 100644 index 000000000000..bad2f280e021 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLdnUrlReplaceOperation.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.app.rest.repository.patch.operation.ldn; + +import static java.lang.String.format; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService ldnUrl Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/ldnurl", + * "value": "ldnurl value" + * }]' + * + */ +@Component +public class NotifyServiceLdnUrlReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/ldnurl"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object ldnUrl = operation.getValue(); + if (ldnUrl == null | !(ldnUrl instanceof String)) { + throw new UnprocessableEntityException("The /ldnurl value must be a string"); + } + + if (notifyService.findByLdnUrl(context,(String) ldnUrl) != null) { + throw new UnprocessableEntityException(format("LDN url already in use %s", + (String) ldnUrl)); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setLdnUrl((String) ldnUrl); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the ldnurl of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getLdnUrl() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (ldnurl)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerIpReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerIpReplaceOperation.java new file mode 100644 index 000000000000..be605f94d834 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerIpReplaceOperation.java @@ -0,0 +1,75 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService lowerIp Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/lowerIp", + * "value": "lowerIp value" + * }]' + * + */ +@Component +public class NotifyServiceLowerIpReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/lowerip"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object lowerIp = operation.getValue(); + if (lowerIp == null | !(lowerIp instanceof String)) { + throw new UnprocessableEntityException("The /lowerIp value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setLowerIp((String) lowerIp); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the lowerIp of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getLowerIp() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (lowerIp)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerOrUpperRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerOrUpperRemoveOperation.java new file mode 100644 index 000000000000..a7425a002796 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceLowerOrUpperRemoveOperation.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.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService LowerIp Or UpperIp Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "/lowerIp" + * }]' + * + */ +@Component +public class NotifyServiceLowerOrUpperRemoveOperation extends PatchOperation { + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + throw new UnprocessableEntityException("/lowerIp or /upperIp are mandatory and can't be removed"); + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + (operation.getPath().trim().toLowerCase().equalsIgnoreCase("/lowerip") || + operation.getPath().trim().toLowerCase().equalsIgnoreCase("/upperip"))); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceNameOrLdnUrlRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceNameOrLdnUrlRemoveOperation.java new file mode 100644 index 000000000000..e1ff7b83ef5b --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceNameOrLdnUrlRemoveOperation.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.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Name Or LdnUrl Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "/name" + * }]' + * + */ +@Component +public class NotifyServiceNameOrLdnUrlRemoveOperation extends PatchOperation { + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + throw new UnprocessableEntityException("/name or /ldnurl are mandatory and can't be removed"); + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + (operation.getPath().trim().toLowerCase().equalsIgnoreCase("/name") || + operation.getPath().trim().toLowerCase().equalsIgnoreCase("/ldnurl"))); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceNameReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceNameReplaceOperation.java new file mode 100644 index 000000000000..48db23544f63 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceNameReplaceOperation.java @@ -0,0 +1,75 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Name Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/name", + * "value": "name value" + * }]' + * + */ +@Component +public class NotifyServiceNameReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/name"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object name = operation.getValue(); + if (name == null | !(name instanceof String)) { + throw new UnprocessableEntityException("The /name value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setName((String) name); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the name of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getName() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (name)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServicePatchUtils.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServicePatchUtils.java new file mode 100644 index 000000000000..5735a7bd5f73 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServicePatchUtils.java @@ -0,0 +1,109 @@ +/** + * 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.ldn; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.dspace.app.ldn.NotifyServiceInboundPattern; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.model.patch.JsonValueEvaluator; +import org.dspace.app.rest.model.patch.Operation; +import org.springframework.stereotype.Component; + +/** + * Util class for shared methods between the NotifyServiceEntity Operations + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +@Component +public final class NotifyServicePatchUtils { + + public static final String NOTIFY_SERVICE_INBOUND_PATTERNS = "notifyServiceInboundPatterns"; + + private ObjectMapper objectMapper = new ObjectMapper(); + + private NotifyServicePatchUtils() { + } + + /** + * Extract NotifyServiceInboundPattern from Operation by parsing the json + * and mapping it to a NotifyServiceInboundPattern + * + * @param operation Operation whose value is being parsed + * @return NotifyServiceInboundPattern extracted from json in operation value + */ + protected NotifyServiceInboundPattern extractNotifyServiceInboundPatternFromOperation(Operation operation) { + NotifyServiceInboundPattern inboundPattern = null; + try { + if (operation.getValue() != null) { + if (operation.getValue() instanceof JsonValueEvaluator) { + inboundPattern = objectMapper.readValue(((JsonValueEvaluator) operation.getValue()) + .getValueNode().toString(), NotifyServiceInboundPattern.class); + } else if (operation.getValue() instanceof String) { + inboundPattern = objectMapper.readValue((String) operation.getValue(), + NotifyServiceInboundPattern.class); + } + } + } catch (IOException e) { + throw new DSpaceBadRequestException("IOException: trying to map json from operation.value" + + " to NotifyServiceInboundPattern class.", e); + } + if (inboundPattern == null) { + throw new DSpaceBadRequestException("Could not extract NotifyServiceInboundPattern Object from Operation"); + } + return inboundPattern; + } + + /** + * Extract list of NotifyServiceInboundPattern from Operation by parsing the json + * and mapping it to a list of NotifyServiceInboundPattern + * + * @param operation Operation whose value is being parsed + * @return list of NotifyServiceInboundPattern extracted from json in operation value + */ + protected List extractNotifyServiceInboundPatternsFromOperation(Operation operation) { + List inboundPatterns = null; + try { + if (operation.getValue() != null) { + if (operation.getValue() instanceof String) { + inboundPatterns = objectMapper.readValue((String) operation.getValue(), + objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, + NotifyServiceInboundPattern.class)); + } + } + } catch (IOException e) { + throw new DSpaceBadRequestException("IOException: trying to map json from operation.value" + + " to List of NotifyServiceInboundPattern class.", e); + } + if (inboundPatterns == null) { + throw new DSpaceBadRequestException("Could not extract list of NotifyServiceInboundPattern " + + "Objects from Operation"); + } + return inboundPatterns; + } + + protected int extractIndexFromOperation(Operation operation) { + String number = ""; + Pattern pattern = Pattern.compile("\\[(\\d+)\\]"); // Pattern to match [i] + Matcher matcher = pattern.matcher(operation.getPath()); + if (matcher.find()) { + number = matcher.group(1); + } + + if (StringUtils.isEmpty(number)) { + throw new DSpaceBadRequestException("path doesn't contain index"); + } + + return Integer.parseInt(number); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreAddOperation.java new file mode 100644 index 000000000000..01c5dd3a66c7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreAddOperation.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.app.rest.repository.patch.operation.ldn; + +import static java.lang.String.format; + +import java.math.BigDecimal; +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Score Add patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "add", + * "path": "/score", + * "value": "score value" + * }]' + * + */ +@Component +public class NotifyServiceScoreAddOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/score"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkNonExistingScoreValue(notifyServiceEntity); + checkOperationValue(operation.getValue()); + Object score = operation.getValue(); + if (score == null) { + throw new DSpaceBadRequestException("The /score value must be a decimal number"); + } + + BigDecimal scoreBigDecimal = null; + try { + scoreBigDecimal = new BigDecimal(score.toString()); + } catch (Exception e) { + throw new DSpaceBadRequestException(format("Score out of range [0, 1] %s", score.toString())); + } + if (scoreBigDecimal.compareTo(java.math.BigDecimal.ZERO) == -1 || + scoreBigDecimal.compareTo(java.math.BigDecimal.ONE) == 1) { + throw new UnprocessableEntityException(format("Score out of range [0, 1] %s", + scoreBigDecimal.setScale(4).toPlainString())); + } + notifyServiceEntity.setScore(scoreBigDecimal); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Throws PatchBadRequestException if a value is already set in the /score path. + * + * @param notifyServiceEntity the notifyServiceEntity to update + + */ + void checkNonExistingScoreValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getScore() != null) { + throw new DSpaceBadRequestException("Attempting to add a value to an already existing path."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreRemoveOperation.java new file mode 100644 index 000000000000..e26d888e9fc7 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreRemoveOperation.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Score Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "/score" + * }]' + * + */ +@Component +public class NotifyServiceScoreRemoveOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/score"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + notifyServiceEntity.setScore(null); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreReplaceOperation.java new file mode 100644 index 000000000000..95824c706364 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceScoreReplaceOperation.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.patch.operation.ldn; + +import static java.lang.String.format; + +import java.math.BigDecimal; +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService Score Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/score", + * "value": "score value" + * }]' + * + */ +@Component +public class NotifyServiceScoreReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/score"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object score = operation.getValue(); + if (score == null) { + throw new DSpaceBadRequestException("The /score value must be a decimal number"); + } + BigDecimal scoreBigDecimal = null; + try { + scoreBigDecimal = new BigDecimal(score.toString()); + } catch (Exception e) { + throw new DSpaceBadRequestException(format("Score out of range [0, 1] %s", score)); + } + if (scoreBigDecimal.compareTo(java.math.BigDecimal.ZERO) == -1 || + scoreBigDecimal.compareTo(java.math.BigDecimal.ONE) == 1) { + throw new UnprocessableEntityException(format("Score out of range [0, 1] %s", score)); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setScore(new BigDecimal((String)score)); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the description of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getDescription() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (description)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUpperIpReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUpperIpReplaceOperation.java new file mode 100644 index 000000000000..7ee362cf97d0 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUpperIpReplaceOperation.java @@ -0,0 +1,75 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService upperIp Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/upperIp", + * "value": "upperIp value" + * }]' + * + */ +@Component +public class NotifyServiceUpperIpReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/upperip"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object upperIp = operation.getValue(); + if (upperIp == null | !(upperIp instanceof String)) { + throw new UnprocessableEntityException("The /upperIp value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setUpperIp((String) upperIp); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the upperIp of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getUpperIp() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (upperIp)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlAddOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlAddOperation.java new file mode 100644 index 000000000000..f09740734393 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlAddOperation.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.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService URL Add patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "add", + * "path": "/url", + * "value": "url value" + * }]' + * + */ +@Component +public class NotifyServiceUrlAddOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/url"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object url = operation.getValue(); + if (url == null | !(url instanceof String)) { + throw new UnprocessableEntityException("The /url value must be a string"); + } + + checkNonExistingUrlValue(notifyServiceEntity); + notifyServiceEntity.setUrl((String) url); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Throws PatchBadRequestException if a value is already set in the /url path. + * + * @param notifyServiceEntity the notifyServiceEntity to update + + */ + void checkNonExistingUrlValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getUrl() != null) { + throw new DSpaceBadRequestException("Attempting to add a value to an already existing path."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_ADD) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlRemoveOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlRemoveOperation.java new file mode 100644 index 000000000000..c2e6fa05bedc --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlRemoveOperation.java @@ -0,0 +1,54 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService URL Remove patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "remove", + * "path": "/url" + * }]' + * + */ +@Component +public class NotifyServiceUrlRemoveOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/url"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + notifyServiceEntity.setUrl(null); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REMOVE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlReplaceOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlReplaceOperation.java new file mode 100644 index 000000000000..53a315d079be --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/repository/patch/operation/ldn/NotifyServiceUrlReplaceOperation.java @@ -0,0 +1,75 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.repository.patch.operation.ldn; + +import java.sql.SQLException; + +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.repository.patch.operation.PatchOperation; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Implementation for NotifyService URL Replace patches. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H " + * Content-Type: application/json" -d ' + * [{ + * "op": "replace", + * "path": "/url", + * "value": "url value" + * }]' + * + */ +@Component +public class NotifyServiceUrlReplaceOperation extends PatchOperation { + + @Autowired + private NotifyService notifyService; + + private static final String OPERATION_PATH = "/url"; + + @Override + public NotifyServiceEntity perform(Context context, NotifyServiceEntity notifyServiceEntity, Operation operation) + throws SQLException { + checkOperationValue(operation.getValue()); + + Object url = operation.getValue(); + if (url == null | !(url instanceof String)) { + throw new UnprocessableEntityException("The /url value must be a string"); + } + + checkModelForExistingValue(notifyServiceEntity); + notifyServiceEntity.setUrl((String) url); + notifyService.update(context, notifyServiceEntity); + return notifyServiceEntity; + } + + /** + * Checks whether the url of notifyServiceEntity has an existing value to replace + * @param notifyServiceEntity Object on which patch is being done + */ + private void checkModelForExistingValue(NotifyServiceEntity notifyServiceEntity) { + if (notifyServiceEntity.getUrl() == null) { + throw new DSpaceBadRequestException("Attempting to replace a non-existent value (url)."); + } + } + + @Override + public boolean supports(Object objectToMatch, Operation operation) { + return (objectToMatch instanceof NotifyServiceEntity && + operation.getOp().trim().equalsIgnoreCase(OPERATION_REPLACE) && + operation.getPath().trim().toLowerCase().equalsIgnoreCase(OPERATION_PATH)); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java index 338eed4a7340..1b251847ba92 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AdminRestPermissionEvaluatorPlugin.java @@ -10,6 +10,8 @@ import java.io.Serializable; import java.sql.SQLException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; import org.dspace.core.Context; @@ -17,8 +19,6 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; @@ -34,7 +34,7 @@ @Order(value = Ordered.HIGHEST_PRECEDENCE) public class AdminRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(RestObjectPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private AuthorizeService authorizeService; @@ -69,7 +69,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java index f0b44187c596..3fdd5158ed1b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/AuthorizeServicePermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.DSpaceObject; @@ -24,21 +26,19 @@ import org.dspace.services.RequestService; import org.dspace.services.model.Request; import org.dspace.util.UUIDUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** - * DSpaceObjectPermissionEvaluatorPlugin will check persmissions based on the DSpace {@link AuthorizeService}. + * DSpaceObjectPermissionEvaluatorPlugin will check permissions based on the DSpace {@link AuthorizeService}. * This service will validate if the authenticated user is allowed to perform an action on the given DSpace Object * based on the resource policies attached to that DSpace object. */ @Component public class AuthorizeServicePermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(AuthorizeServicePermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private AuthorizeService authorizeService; @@ -106,7 +106,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java index b4b08c668b0a..5a596525e4ce 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/BitstreamMetadataReadPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.repository.BitstreamRestRepository; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.DSpaceObjectUtils; @@ -24,8 +26,6 @@ import org.dspace.core.Context; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -39,7 +39,7 @@ @Component public class BitstreamMetadataReadPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(BitstreamMetadataReadPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -66,7 +66,7 @@ public boolean hasPermission(Authentication authentication, Serializable targetI return this.metadataReadPermissionOnBitstream(context, (Bitstream) dso); } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ClaimedTaskRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ClaimedTaskRestPermissionEvaluatorPlugin.java index 1741d94ea834..80019b55fdd0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ClaimedTaskRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ClaimedTaskRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.ClaimedTaskRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; @@ -20,8 +22,6 @@ import org.dspace.services.model.Request; import org.dspace.xmlworkflow.storedcomponents.ClaimedTask; import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -29,13 +29,13 @@ /** * An authenticated user is allowed to interact with a claimed task only if they own it * claim. - * + * * @author Andrea Bollini (andrea.bollini at 4science.it) */ @Component public class ClaimedTaskRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(ClaimedTaskRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -74,7 +74,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java index b3f4a00d379e..2287d30baf68 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/CustomLogoutHandler.java @@ -10,10 +10,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.core.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; @@ -28,7 +28,7 @@ @Component public class CustomLogoutHandler implements LogoutHandler { - private static final Logger log = LoggerFactory.getLogger(CustomLogoutHandler.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RestAuthenticationService restAuthenticationService; @@ -40,6 +40,7 @@ public class CustomLogoutHandler implements LogoutHandler { * @param httpServletResponse * @param authentication */ + @Override public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) { try { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceObjectAdminPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceObjectAdminPermissionEvaluatorPlugin.java index 5420a6e7dc0b..dcf0c34aae32 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceObjectAdminPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/DSpaceObjectAdminPermissionEvaluatorPlugin.java @@ -12,6 +12,8 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.DSpaceObjectUtils; import org.dspace.authorize.service.AuthorizeService; @@ -19,20 +21,19 @@ import org.dspace.core.Context; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; + /** - * {@link RestPermissionEvaluatorPlugin} class that evaluate admin permission against a generic DSpace Object - * + * {@link RestPermissionEvaluatorPlugin} class that evaluate admin permission against a generic DSpace Object. + * * @author Mykhaylo Boychuk (4science.it) */ @Component public class DSpaceObjectAdminPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(DSpaceObjectAdminPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); public static final String DSPACE_OBJECT = "dspaceObject"; @@ -64,7 +65,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t DSpaceObject dso = dspaceObjectUtil.findDSpaceObject(context, dsoUuid); return authorizeService.isAdmin(context, dso); } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java index e55734e513de..a515c6407b10 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestAuthenticationProvider.java @@ -19,6 +19,8 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.login.PostLoggedInAction; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authenticate.AuthenticationMethod; @@ -28,8 +30,6 @@ import org.dspace.core.LogHelper; import org.dspace.eperson.EPerson; import org.dspace.services.RequestService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; @@ -40,7 +40,7 @@ import org.springframework.stereotype.Component; /** - * This class is responsible for authenticating a user via REST + * This class is responsible for authenticating a user via REST. * * @author Frederic Van Reet (frederic dot vanreet at atmire dot com) * @author Tom Desair (tom dot desair at atmire dot com) @@ -48,7 +48,7 @@ @Component public class EPersonRestAuthenticationProvider implements AuthenticationProvider { - private static final Logger log = LoggerFactory.getLogger(EPersonRestAuthenticationProvider.class); + private static final Logger log = LogManager.getLogger(); public static final String MANAGE_ACCESS_GROUP = "MANAGE_ACCESS_GROUP"; @@ -144,9 +144,8 @@ private Authentication authenticateNewLogin(Authentication authentication) { } } else { - log.info(LogHelper.getHeader(newContext, "failed_login", "email=" - + name + ", result=" - + authenticateResult)); + log.info(LogHelper.getHeader(newContext, "failed_login", + "email={}, result={}"), name, authenticateResult); throw new BadCredentialsException("Login failed"); } } @@ -155,7 +154,7 @@ private Authentication authenticateNewLogin(Authentication authentication) { try { newContext.complete(); } catch (SQLException e) { - log.error(e.getMessage() + " occurred while trying to close", e); + log.error("{} occurred while trying to close", e.getMessage(), e); } } } @@ -182,7 +181,7 @@ private Authentication createAuthentication(final Context context) { return new DSpaceAuthentication(ePerson, getGrantedAuthorities(context)); } else { - log.info(LogHelper.getHeader(context, "failed_login", "No eperson with an non-blank e-mail address found")); + log.info(LogHelper.getHeader(context, "failed_login", "No eperson with a non-blank e-mail address found")); throw new BadCredentialsException("Login failed"); } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java index ce6783ae1507..bd2a71288b92 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/EPersonRestPermissionEvaluatorPlugin.java @@ -14,6 +14,8 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.patch.Operation; import org.dspace.app.rest.model.patch.Patch; import org.dspace.app.rest.repository.patch.operation.DSpaceObjectMetadataPatchUtils; @@ -27,8 +29,6 @@ import org.dspace.eperson.EPerson; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -40,7 +40,7 @@ @Component public class EPersonRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(EPersonRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired AuthorizeService authorizeService; @@ -84,7 +84,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } @@ -100,7 +100,7 @@ public boolean hasPatchPermission(Authentication authentication, Serializable ta Request currentRequest = requestService.getCurrentRequest(); if (currentRequest != null) { HttpServletRequest httpServletRequest = currentRequest.getHttpServletRequest(); - if (operations.size() > 0 + if (!operations.isEmpty() && StringUtils.equalsIgnoreCase(operations.get(0).getOp(), PatchOperation.OPERATION_ADD) && StringUtils.equalsIgnoreCase(operations.get(0).getPath(), EPersonPasswordAddOperation.OPERATION_PASSWORD_CHANGE) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java index d6d3bc82c7cf..f15efcccb058 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/GroupRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import java.util.UUID; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.util.AuthorizeUtil; import org.dspace.authorize.service.AuthorizeService; @@ -22,8 +24,6 @@ import org.dspace.eperson.service.GroupService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -35,7 +35,7 @@ @Component public class GroupRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(GroupRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -87,7 +87,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueAndHistoryRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueAndHistoryRestPermissionEvaluatorPlugin.java index 0139f3b3362a..cadd42f13398 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueAndHistoryRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueAndHistoryRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.content.Item; import org.dspace.content.service.ItemService; @@ -22,21 +24,19 @@ import org.dspace.orcid.service.OrcidQueueService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** * Class that evaluate DELETE and READ permissions - * + * * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) */ @Component public class OrcidQueueAndHistoryRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(OrcidQueueAndHistoryRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); public static final String ORCID_QUEUE = "ORCID_QUEUE"; public static final String ORCID_HISTORY = "ORCID_HISTORY"; @@ -97,7 +97,7 @@ private boolean hasAccess(Context context, EPerson currentUser, Integer orcidObj .anyMatch(authority -> currentUser.getID().toString().equals(authority)); } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueSearchRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueSearchRestPermissionEvaluatorPlugin.java index d8566143c437..2666d98bd889 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueSearchRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/OrcidQueueSearchRestPermissionEvaluatorPlugin.java @@ -13,6 +13,8 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.content.Item; import org.dspace.content.MetadataValue; @@ -21,8 +23,6 @@ import org.dspace.eperson.EPerson; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -36,7 +36,7 @@ @Component public class OrcidQueueSearchRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(OrcidQueueAndHistoryRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); public static final String ORCID_QUEUE_SEARCH = "ORCID_QUEUE_SEARCH"; @@ -75,7 +75,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/PoolTaskRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/PoolTaskRestPermissionEvaluatorPlugin.java index d145dae44998..3aa556c91fe0 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/PoolTaskRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/PoolTaskRestPermissionEvaluatorPlugin.java @@ -12,6 +12,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.PoolTaskRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.AuthorizeException; @@ -23,8 +25,6 @@ import org.dspace.xmlworkflow.storedcomponents.PoolTask; import org.dspace.xmlworkflow.storedcomponents.XmlWorkflowItem; import org.dspace.xmlworkflow.storedcomponents.service.PoolTaskService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -37,7 +37,7 @@ @Component public class PoolTaskRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(PoolTaskRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -79,7 +79,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException | AuthorizeException | IOException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ProcessRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ProcessRestPermissionEvaluatorPlugin.java index 94d9694ec422..b476b3fef98d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ProcessRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ProcessRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.ProcessRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -19,8 +21,6 @@ import org.dspace.scripts.service.ProcessService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -33,7 +33,7 @@ @Component public class ProcessRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(ProcessRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -66,7 +66,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QAEventRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QAEventRestPermissionEvaluatorPlugin.java index 8ffc6be5e193..6e0c0482db61 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QAEventRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QAEventRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.util.Objects; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.QAEventRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.content.QAEvent; @@ -19,8 +21,6 @@ import org.dspace.qaevent.service.QAEventService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -33,7 +33,7 @@ @Component public class QAEventRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(QAEventRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private QAEventService qaEventService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QASourceRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QASourceRestPermissionEvaluatorPlugin.java index 3d11b4d099e1..8aca72326efc 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QASourceRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/QASourceRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.util.Objects; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.QASourceRest; import org.dspace.app.rest.model.QATopicRest; import org.dspace.app.rest.utils.ContextUtil; @@ -19,8 +21,6 @@ import org.dspace.qaevent.service.QAEventSecurityService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -33,7 +33,7 @@ @Component public class QASourceRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(QASourceRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private QAEventSecurityService qaEventSecurityService; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ReadAuthorizationPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ReadAuthorizationPermissionEvaluatorPlugin.java index 88670c89fef8..8bf0a1452a61 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ReadAuthorizationPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ReadAuthorizationPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.authorization.AuthorizationRestUtil; import org.dspace.app.rest.model.AuthorizationRest; import org.dspace.app.rest.utils.ContextUtil; @@ -19,21 +21,19 @@ import org.dspace.eperson.EPerson; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** * {@link RestPermissionEvaluatorPlugin} class that evaluate READ permissions for an Authorization - * + * * @author Andrea Bollini (andrea.bollini at 4science.it) */ @Component public class ReadAuthorizationPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(ReadAuthorizationPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired AuthorizeService authorizeService; @@ -75,7 +75,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java index 421d25f9406d..ccf272ecefae 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyAdminPermissionEvalutatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.ResourcePolicy; @@ -20,8 +22,6 @@ import org.dspace.core.Context; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.rest.webmvc.ResourceNotFoundException; import org.springframework.security.core.Authentication; @@ -36,7 +36,7 @@ @Component public class ResourcePolicyAdminPermissionEvalutatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(ResourcePolicyRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); public static final String RESOURCE_POLICY_PATCH = "resourcepolicy"; @@ -74,7 +74,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t DSpaceObject dso = resourcePolicy.getdSpaceObject(); return authorizeService.isAdmin(context, dso); } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java index bf7ce3b53f1a..9a34ca68110d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/ResourcePolicyRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.ResourcePolicyRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -20,21 +22,19 @@ import org.dspace.eperson.service.EPersonService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** - * {@link RestPermissionEvaluatorPlugin} class that evaluate READ, WRITE and DELETE permissions over a ResourcePolicy - * + * {@link RestPermissionEvaluatorPlugin} class that evaluate READ, WRITE and DELETE permissions over a ResourcePolicy. + * * @author Mykhaylo Boychuk (4science.it) */ @Component public class ResourcePolicyRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(ResourcePolicyRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired AuthorizeService authorizeService; @@ -78,7 +78,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java index 964d35f42c34..c774c1836452 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessAuthenticationFilter.java @@ -16,6 +16,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.AuthorizeException; import org.dspace.authorize.factory.AuthorizeServiceFactory; @@ -28,8 +30,6 @@ import org.dspace.services.RequestService; import org.dspace.services.factory.DSpaceServicesFactory; import org.dspace.util.UUIDUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; @@ -47,21 +47,24 @@ */ public class StatelessAuthenticationFilter extends BasicAuthenticationFilter { - private static final Logger log = LoggerFactory.getLogger(StatelessAuthenticationFilter.class); + private static final Logger log = LogManager.getLogger(); private static final String ON_BEHALF_OF_REQUEST_PARAM = "X-On-Behalf-Of"; - private RestAuthenticationService restAuthenticationService; + private final RestAuthenticationService restAuthenticationService; - private EPersonRestAuthenticationProvider authenticationProvider; + private final EPersonRestAuthenticationProvider authenticationProvider; - private RequestService requestService; + private final RequestService requestService; - private AuthorizeService authorizeService = AuthorizeServiceFactory.getInstance().getAuthorizeService(); + private final AuthorizeService authorizeService + = AuthorizeServiceFactory.getInstance().getAuthorizeService(); - private EPersonService ePersonService = EPersonServiceFactory.getInstance().getEPersonService(); + private final EPersonService ePersonService + = EPersonServiceFactory.getInstance().getEPersonService(); - private ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); + private final ConfigurationService configurationService + = DSpaceServicesFactory.getInstance().getConfigurationService(); public StatelessAuthenticationFilter(AuthenticationManager authenticationManager, RestAuthenticationService restAuthenticationService, @@ -124,7 +127,7 @@ private Authentication getAuthentication(HttpServletRequest request, HttpServlet // parse the token. EPerson eperson = restAuthenticationService.getAuthenticatedEPerson(request, res, context); if (eperson != null) { - log.debug("Found authentication data in request for EPerson {}", eperson.getEmail()); + log.debug("Found authentication data in request for EPerson {}", eperson::getEmail); //Pass the eperson ID to the request service requestService.setCurrentUserId(eperson.getID()); @@ -174,7 +177,7 @@ private Authentication getOnBehalfOfAuthentication(Context context, String onBeh requestService.setCurrentUserId(epersonUuid); context.switchContextUser(onBehalfOfEPerson); log.debug("Found 'on-behalf-of' authentication data in request for EPerson {}", - onBehalfOfEPerson.getEmail()); + onBehalfOfEPerson::getEmail); return new DSpaceAuthentication(onBehalfOfEPerson, authenticationProvider.getGrantedAuthorities(context)); } else { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java index c95fce71c425..7dd5ee3beab1 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/StatelessLoginFilter.java @@ -13,8 +13,8 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; @@ -23,7 +23,7 @@ /** * This class will filter /api/authn/login requests to try and authenticate them. Keep in mind, this filter runs *after* - * StatelessAuthenticationFilter (which looks for authentication data in the request itself). So, in some scenarios + * {@link StatelessAuthenticationFilter} (which looks for authentication data in the request itself). So, in some scenarios * (e.g. after a Shibboleth login) the StatelessAuthenticationFilter does the actual authentication, and this Filter * just ensures the auth token (JWT) is sent back in an Authorization header. * @@ -31,7 +31,7 @@ * @author Tom Desair (tom dot desair at atmire dot com) */ public class StatelessLoginFilter extends AbstractAuthenticationProcessingFilter { - private static final Logger log = LoggerFactory.getLogger(StatelessLoginFilter.class); + private static final Logger log = LogManager.getLogger(); protected AuthenticationManager authenticationManager; @@ -97,7 +97,7 @@ protected void successfulAuthentication(HttpServletRequest req, Authentication auth) throws IOException, ServletException { DSpaceAuthentication dSpaceAuthentication = (DSpaceAuthentication) auth; - log.debug("Authentication successful for EPerson {}", dSpaceAuthentication.getName()); + log.debug("Authentication successful for EPerson {}", dSpaceAuthentication::getName); restAuthenticationService.addAuthenticationDataForUser(req, res, dSpaceAuthentication, false); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java index c93d966e73cb..dabb5a03b55f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/SubscriptionRestPermissionEvaluatorPlugin.java @@ -17,6 +17,8 @@ import java.util.Objects; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; import org.dspace.core.Context; @@ -25,21 +27,19 @@ import org.dspace.eperson.service.SubscribeService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** * {@link RestPermissionEvaluatorPlugin} class that evaluate READ, WRITE and DELETE permissions over a Subscription - * + * * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.com) */ @Component public class SubscriptionRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(SubscriptionRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -76,7 +76,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t Subscription subscription = subscribeService.findById(context, Integer.parseInt(targetId.toString())); return Objects.nonNull(subscription) ? currentUser.equals(subscription.getEPerson()) : false; } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/TemplateItemRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/TemplateItemRestPermissionEvaluatorPlugin.java index cb977dff3aef..9416844cbc1b 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/TemplateItemRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/TemplateItemRestPermissionEvaluatorPlugin.java @@ -12,6 +12,8 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.TemplateItemRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -21,8 +23,6 @@ import org.dspace.eperson.EPerson; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -35,7 +35,7 @@ @Component public class TemplateItemRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(TemplateItemRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -76,7 +76,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/UsageReportRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/UsageReportRestPermissionEvaluatorPlugin.java index 06254d561ace..c29abd2f1a71 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/UsageReportRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/UsageReportRestPermissionEvaluatorPlugin.java @@ -13,6 +13,8 @@ import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.UsageReportRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.app.rest.utils.DSpaceObjectUtils; @@ -22,8 +24,6 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -36,7 +36,7 @@ @Component public class UsageReportRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(UsageReportRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ConfigurationService configurationService; @@ -95,7 +95,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } return authorizeService.authorizeActionBoolean(context, dso, restPermission.getDspaceApiActionId()); } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionHistoryRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionHistoryRestPermissionEvaluatorPlugin.java index d69f6a21cad3..de5980e3c314 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionHistoryRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionHistoryRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.VersionHistoryRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -18,8 +20,6 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -31,7 +31,7 @@ @Component public class VersionHistoryRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(VersionHistoryRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -62,7 +62,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } return true; } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPatchPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPatchPermissionEvaluatorPlugin.java index 6cca749cd079..6f1b04b3fd45 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPatchPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPatchPermissionEvaluatorPlugin.java @@ -6,11 +6,14 @@ * http://www.dspace.org/license/ */ package org.dspace.app.rest.security; + import java.io.Serializable; import java.sql.SQLException; import java.util.UUID; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.VersionRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -19,21 +22,19 @@ import org.dspace.services.model.Request; import org.dspace.versioning.Version; import org.dspace.versioning.service.VersioningService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** * This class evaluate ADMIN permissions to patch operation over a Version. - * + * * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) */ @Component public class VersionRestPatchPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(VersionRestPatchPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -78,7 +79,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t return true; } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPermissionEvaluatorPlugin.java index 0badd46e2798..f882ce317d72 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/VersionRestPermissionEvaluatorPlugin.java @@ -12,6 +12,8 @@ import java.util.Objects; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.VersionRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -21,20 +23,18 @@ import org.dspace.services.model.Request; import org.dspace.versioning.Version; import org.dspace.versioning.service.VersioningService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** * This class acts as a PermissionEvaluator to decide whether a given request to a Versioning endpoint is allowed to - * pass through or not + * pass through or not. */ @Component public class VersionRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(VersionRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -77,7 +77,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkflowRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkflowRestPermissionEvaluatorPlugin.java index 626290fdc3bb..b59dbdc2f85a 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkflowRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkflowRestPermissionEvaluatorPlugin.java @@ -12,6 +12,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.WorkflowItemRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.AuthorizeException; @@ -25,8 +27,6 @@ import org.dspace.xmlworkflow.storedcomponents.service.ClaimedTaskService; import org.dspace.xmlworkflow.storedcomponents.service.PoolTaskService; import org.dspace.xmlworkflow.storedcomponents.service.XmlWorkflowItemService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; @@ -34,13 +34,13 @@ /** * An authenticated user is allowed to interact with workflow item only if they belong to a task that they own or could * claim. - * + * * @author Andrea Bollini (andrea.bollini at 4science.it) */ @Component public class WorkflowRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(WorkflowRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -99,7 +99,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException | AuthorizeException | IOException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkspaceItemRestPermissionEvaluatorPlugin.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkspaceItemRestPermissionEvaluatorPlugin.java index c0efbd60f204..ba4387835e24 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkspaceItemRestPermissionEvaluatorPlugin.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/WorkspaceItemRestPermissionEvaluatorPlugin.java @@ -11,6 +11,8 @@ import java.sql.SQLException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.WorkspaceItemRest; import org.dspace.app.rest.utils.ContextUtil; import org.dspace.authorize.service.AuthorizeService; @@ -21,21 +23,19 @@ import org.dspace.services.RequestService; import org.dspace.services.model.Request; import org.dspace.supervision.service.SupervisionOrderService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; /** - * {@link RestPermissionEvaluatorPlugin} class that evaluate READ, WRITE and DELETE permissions over a WorkspaceItem - * + * {@link RestPermissionEvaluatorPlugin} class that evaluate READ, WRITE and DELETE permissions over a WorkspaceItem. + * * @author Mykhaylo Boychuk (mykhaylo.boychuk at 4science.it) */ @Component public class WorkspaceItemRestPermissionEvaluatorPlugin extends RestObjectPermissionEvaluatorPlugin { - private static final Logger log = LoggerFactory.getLogger(WorkspaceItemRestPermissionEvaluatorPlugin.class); + private static final Logger log = LogManager.getLogger(); @Autowired private RequestService requestService; @@ -100,7 +100,7 @@ public boolean hasDSpacePermission(Authentication authentication, Serializable t } } catch (SQLException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } return false; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/AuthenticationMethodClaimProvider.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/AuthenticationMethodClaimProvider.java index 84b8259dce79..dd22a8a1deb7 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/AuthenticationMethodClaimProvider.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/AuthenticationMethodClaimProvider.java @@ -12,10 +12,10 @@ import javax.servlet.http.HttpServletRequest; import com.nimbusds.jwt.JWTClaimsSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authenticate.service.AuthenticationService; import org.dspace.core.Context; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -27,15 +27,17 @@ public class AuthenticationMethodClaimProvider implements JWTClaimProvider { public static final String AUTHENTICATION_METHOD = "authenticationMethod"; - private static final Logger log = LoggerFactory.getLogger(AuthenticationMethodClaimProvider.class); + private static final Logger log = LogManager.getLogger(); @Autowired private AuthenticationService authenticationService; + @Override public String getKey() { return AUTHENTICATION_METHOD; } + @Override public Object getValue(final Context context, final HttpServletRequest request) { if (context.getAuthenticationMethod() != null) { return context.getAuthenticationMethod(); @@ -43,12 +45,13 @@ public Object getValue(final Context context, final HttpServletRequest request) return authenticationService.getAuthenticationMethod(context, request); } + @Override public void parseClaim(final Context context, final HttpServletRequest request, final JWTClaimsSet jwtClaimsSet) throws SQLException { try { context.setAuthenticationMethod(jwtClaimsSet.getStringClaim(AUTHENTICATION_METHOD)); } catch (ParseException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } } } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java index 6beab1aa853d..6143361a373e 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenHandler.java @@ -33,14 +33,14 @@ import com.nimbusds.jwt.util.DateUtils; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authorize.AuthorizeException; import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; import org.dspace.service.ClientInfoService; import org.dspace.services.ConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.crypto.keygen.BytesKeyGenerator; @@ -59,7 +59,7 @@ public abstract class JWTTokenHandler { private static final int MAX_CLOCK_SKEW_SECONDS = 60; private static final String AUTHORIZATION_TOKEN_PARAMETER = "authentication-token"; - private static final Logger log = LoggerFactory.getLogger(JWTTokenHandler.class); + private static final Logger log = LogManager.getLogger(); @Autowired private List jwtClaimProviders; @@ -135,7 +135,7 @@ public EPerson parseEPersonFromToken(String token, HttpServletRequest request, C // As long as the JWT is valid, parse all claims and return the EPerson if (isValidToken(request, signedJWT, jwtClaimsSet, ePerson)) { - log.debug("Received valid token for username: " + ePerson.getEmail()); + log.debug("Received valid token for username: {}", ePerson::getEmail); for (JWTClaimProvider jwtClaimProvider : jwtClaimProviders) { jwtClaimProvider.parseClaim(context, request, jwtClaimsSet); @@ -143,7 +143,7 @@ public EPerson parseEPersonFromToken(String token, HttpServletRequest request, C return ePerson; } else { - log.warn(getIpAddress(request) + " tried to use an expired or non-valid token"); + log.warn("{} tried to use an expired or non-valid token", getIpAddress(request)); return null; } } @@ -155,7 +155,8 @@ public EPerson parseEPersonFromToken(String token, HttpServletRequest request, C * @param request current Request * @param previousLoginDate date of last login (before this one) * @return string version of signed JWT - * @throws JOSEException + * @throws JOSEException passed through. + * @throws SQLException passed through. */ public String createTokenForEPerson(Context context, HttpServletRequest request, Date previousLoginDate) throws JOSEException, SQLException { diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java index c28729ff83a8..aace04af1eae 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/JWTTokenRestAuthenticationServiceImpl.java @@ -18,6 +18,8 @@ import com.nimbusds.jose.JOSEException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.model.wrapper.AuthenticationToken; import org.dspace.app.rest.security.DSpaceAuthentication; import org.dspace.app.rest.security.RestAuthenticationService; @@ -27,8 +29,6 @@ import org.dspace.core.Context; import org.dspace.eperson.EPerson; import org.dspace.eperson.service.EPersonService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -47,7 +47,7 @@ @Component public class JWTTokenRestAuthenticationServiceImpl implements RestAuthenticationService, InitializingBean { - private static final Logger log = LoggerFactory.getLogger(RestAuthenticationService.class); + private static final Logger log = LogManager.getLogger(); private static final String AUTHORIZATION_COOKIE = "Authorization-cookie"; private static final String AUTHORIZATION_HEADER = "Authorization"; private static final String AUTHORIZATION_TYPE = "Bearer"; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SpecialGroupClaimProvider.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SpecialGroupClaimProvider.java index 2ce558133db5..bed8b9a58b5c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SpecialGroupClaimProvider.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/jwt/SpecialGroupClaimProvider.java @@ -17,11 +17,11 @@ import com.nimbusds.jwt.JWTClaimsSet; import org.apache.commons.collections4.CollectionUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.authenticate.service.AuthenticationService; import org.dspace.core.Context; import org.dspace.eperson.Group; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -34,17 +34,19 @@ @Component public class SpecialGroupClaimProvider implements JWTClaimProvider { - private static final Logger log = LoggerFactory.getLogger(SpecialGroupClaimProvider.class); + private static final Logger log = LogManager.getLogger(); public static final String SPECIAL_GROUPS = "sg"; @Autowired private AuthenticationService authenticationService; + @Override public String getKey() { return SPECIAL_GROUPS; } + @Override public Object getValue(Context context, HttpServletRequest request) { List groups = new ArrayList<>(); try { @@ -57,6 +59,7 @@ public Object getValue(Context context, HttpServletRequest request) { return groupIds; } + @Override public void parseClaim(Context context, HttpServletRequest request, JWTClaimsSet jwtClaimsSet) { try { List groupIds = jwtClaimsSet.getStringListClaim(SPECIAL_GROUPS); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/package-info.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/package-info.java new file mode 100644 index 000000000000..229a6d0c12b5 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/security/package-info.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/ + */ +/** + * DSpace-specific concepts and behaviors to support Spring Security. + * These may be used by Spring EL expressions in Spring Security annotations. + * + *

    {@code hasPermission} terms are evaluated by + * {@link DSpacePermissionEvaluator}, an implementation of Spring's + * {@link PermissionEvaluator}. It tests access to specific model objects + * (Item, EPerson etc.) using those objects' policies. It is injected with a + * collection of {@link RestPermissionEvaluatorPlugin}s which do the work. + * + *

    {@code hasAuthority} terms are implemented by {@link GrantedAuthority} + * implementations such as {@link EPersonRestAuthenticationProvider}. These + * test for authorization properties of the session itself, such as membership + * in the site administrators group. + * + *

    {@code *PermissionEvaluatorPlugin} classes test permission for specific + * types of objects. They implement {@link RestPermissionEvaluatorPlugin}. + * + *

    Other classes TBD: + *

      + *
    • *Filter
    • + *
    • *Configuration
    • + *
    + */ +package org.dspace.app.rest.security; + +import org.springframework.security.access.PermissionEvaluator; +import org.springframework.security.core.GrantedAuthority; diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractProcessingStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractProcessingStep.java index 8c03f4ef82f0..bb8c7825e2a2 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractProcessingStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/AbstractProcessingStep.java @@ -7,6 +7,7 @@ */ package org.dspace.app.rest.submit; +import org.dspace.app.rest.submit.factory.impl.NotifySubmissionService; import org.dspace.authorize.factory.AuthorizeServiceFactory; import org.dspace.authorize.service.AuthorizeService; import org.dspace.content.factory.ContentServiceFactory; @@ -19,6 +20,7 @@ import org.dspace.content.service.WorkspaceItemService; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; +import org.dspace.utils.DSpace; /** * Abstract processing class for DSpace Submission Steps. This retrieve several @@ -35,4 +37,6 @@ public abstract class AbstractProcessingStep implements DataProcessingStep { protected MetadataFieldService metadataFieldService = ContentServiceFactory.getInstance().getMetadataFieldService(); protected ConfigurationService configurationService = DSpaceServicesFactory.getInstance().getConfigurationService(); protected WorkspaceItemService workspaceItemService = ContentServiceFactory.getInstance().getWorkspaceItemService(); + protected NotifySubmissionService coarNotifySubmissionService = new DSpace().getServiceManager() + .getServiceByName("coarNotifySubmissionService", NotifySubmissionService.class); } diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java index c6f08c85b36b..f8aeacbf08a4 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/DataProcessingStep.java @@ -42,6 +42,7 @@ public interface DataProcessingStep extends RestProcessingStep { public static final String PRIMARY_FLAG_ENTRY = "primary"; public static final String UPLOAD_STEP_METADATA_PATH = "metadata"; + public static final String COARNOTIFY_STEP_PATH = "coarnotify"; /** * Method to expose data in the a dedicated section of the in progress submission. The step needs to return a diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/AccessConditionReplacePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/AccessConditionReplacePatchOperation.java index d2529cbca303..22ea6a45af4f 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/AccessConditionReplacePatchOperation.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/AccessConditionReplacePatchOperation.java @@ -17,6 +17,8 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.app.rest.exception.UnprocessableEntityException; import org.dspace.app.rest.model.AccessConditionDTO; import org.dspace.app.rest.model.patch.JsonValueEvaluator; @@ -31,8 +33,6 @@ import org.dspace.submit.model.AccessConditionConfigurationService; import org.dspace.submit.model.AccessConditionOption; import org.dspace.util.TimeHelpers; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -42,7 +42,7 @@ */ public class AccessConditionReplacePatchOperation extends ReplacePatchOperation { - private static final Logger log = LoggerFactory.getLogger(AccessConditionReplacePatchOperation.class); + private static final Logger log = LogManager.getLogger(); @Autowired private ResourcePolicyService resourcePolicyService; @@ -157,7 +157,7 @@ private Date parseDate(String date) { try { return pattern.parse(date); } catch (ParseException e) { - log.error(e.getMessage(), e); + log.error(e::getMessage, e); } } throw new UnprocessableEntityException("Provided format of date:" + date + " is not supported!"); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java new file mode 100644 index 000000000000..94f5e94d9b81 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceAddPatchOperation.java @@ -0,0 +1,107 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.submit.factory.impl; + +import java.sql.SQLException; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.patch.LateObjectEvaluator; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Submission "add" PATCH operation + * + * To add the COAR Notify Service of workspace item. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/submission/workspaceitems/31599 -H "Content-Type: + * application/json" -d '[{ "op": "add", "path": "/sections/coarnotify/review/-"}, "value": ["1","2"]' + * + */ +public class NotifyServiceAddPatchOperation extends AddPatchOperation { + + @Autowired + private NotifyPatternToTriggerService notifyPatternToTriggerService; + + @Autowired + private NotifyService notifyService; + + private NotifySubmissionService coarNotifySubmissionService = new DSpace().getServiceManager() + .getServiceByName("coarNotifySubmissionService", NotifySubmissionService.class); + + @Override + protected Class getArrayClassForEvaluation() { + return Integer[].class; + } + + @Override + protected Class getClassForEvaluation() { + return Integer.class; + } + + @Override + void add(Context context, HttpServletRequest currentRequest, InProgressSubmission source, String path, + Object value) throws Exception { + + String pattern = coarNotifySubmissionService.extractPattern(path); + Set servicesIds = new LinkedHashSet<>(evaluateArrayObject((LateObjectEvaluator) value)); + + coarNotifySubmissionService.checkCompatibilityWithPattern(context, pattern, servicesIds); + + List services = + servicesIds.stream() + .map(id -> + findService(context, id)) + .collect(Collectors.toList()); + if (services.isEmpty()) { + createNotifyPattern(context, source.getItem(), null, pattern); + } else { + services.forEach(service -> + createNotifyPattern(context, source.getItem(), service, pattern)); + } + } + + private NotifyServiceEntity findService(Context context, int serviceId) { + try { + NotifyServiceEntity service = notifyService.find(context, serviceId); + if (service == null) { + throw new UnprocessableEntityException("no service found for the provided value: " + serviceId + ""); + } + return service; + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + private void createNotifyPattern(Context context, Item item, NotifyServiceEntity service, String pattern) { + try { + NotifyPatternToTrigger notifyPatternToTrigger = notifyPatternToTriggerService.create(context); + notifyPatternToTrigger.setItem(item); + notifyPatternToTrigger.setNotifyService(service); + notifyPatternToTrigger.setPattern(pattern); + notifyPatternToTriggerService.update(context, notifyPatternToTrigger); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceRemovePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceRemovePatchOperation.java new file mode 100644 index 000000000000..c1304f0b91f1 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceRemovePatchOperation.java @@ -0,0 +1,70 @@ +/** + * 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.submit.factory.impl; + +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.Item; +import org.dspace.core.Context; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Submission "remove" PATCH operation + * + * To remove the COAR Notify Service of workspace item. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/submission/workspaceitems/31599 -H "Content-Type: + * application/json" -d '[{ "op": "remove", "path": "/sections/coarnotify/review/0"}]' + * + */ +public class NotifyServiceRemovePatchOperation extends RemovePatchOperation { + + @Autowired + private NotifyPatternToTriggerService notifyPatternToTriggerService; + + private NotifySubmissionService coarNotifySubmissionService = new DSpace().getServiceManager() + .getServiceByName("coarNotifySubmissionService", NotifySubmissionService.class); + + @Override + protected Class getArrayClassForEvaluation() { + return Integer[].class; + } + + @Override + protected Class getClassForEvaluation() { + return Integer.class; + } + + @Override + void remove(Context context, HttpServletRequest currentRequest, InProgressSubmission source, String path, + Object value) throws Exception { + + Item item = source.getItem(); + + String pattern = coarNotifySubmissionService.extractPattern(path); + int index = coarNotifySubmissionService.extractIndex(path); + + List notifyPatterns = + notifyPatternToTriggerService.findByItemAndPattern(context, item, pattern); + + if (index >= notifyPatterns.size()) { + throw new UnprocessableEntityException("the provided index[" + index + "] is out of the rang"); + } + + notifyPatternToTriggerService.delete(context, notifyPatterns.get(index)); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceReplacePatchOperation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceReplacePatchOperation.java new file mode 100644 index 000000000000..d7988da0029d --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifyServiceReplacePatchOperation.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.submit.factory.impl; + +import java.util.List; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.DSpaceBadRequestException; +import org.dspace.content.InProgressSubmission; +import org.dspace.core.Context; +import org.dspace.utils.DSpace; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Submission "replace" PATCH operation + * + * To replace the COAR Notify Service of workspace item. + * + * Example: + * curl -X PATCH http://${dspace.server.url}/api/submission/workspaceitems/31599 -H "Content-Type: + * application/json" -d '[{ "op": "replace", "path": "/sections/coarnotify/review/0"}, "value": "10"]' + * + */ +public class NotifyServiceReplacePatchOperation extends ReplacePatchOperation { + + @Autowired + private NotifyPatternToTriggerService notifyPatternToTriggerService; + + @Autowired + private NotifyService notifyService; + + private NotifySubmissionService coarNotifySubmissionService = new DSpace().getServiceManager() + .getServiceByName("coarNotifySubmissionService", NotifySubmissionService.class); + + @Override + protected Class getArrayClassForEvaluation() { + return Integer[].class; + } + + @Override + protected Class getClassForEvaluation() { + return Integer.class; + } + + @Override + void replace(Context context, HttpServletRequest currentRequest, InProgressSubmission source, String path, + Object value) throws Exception { + + int index = coarNotifySubmissionService.extractIndex(path); + String pattern = coarNotifySubmissionService.extractPattern(path); + + List notifyPatterns = + notifyPatternToTriggerService.findByItemAndPattern(context, source.getItem(), pattern); + + if (index >= notifyPatterns.size()) { + throw new DSpaceBadRequestException("the provided index[" + index + "] is out of the rang"); + } + + NotifyServiceEntity notifyServiceEntity = notifyService.find(context, Integer.parseInt(value.toString())); + if (notifyServiceEntity == null) { + throw new DSpaceBadRequestException("no service found for the provided value: " + value + ""); + } + + coarNotifySubmissionService.checkCompatibilityWithPattern(context, + pattern, Set.of(notifyServiceEntity.getID())); + + NotifyPatternToTrigger notifyPatternToTriggerOld = notifyPatterns.get(index); + notifyPatternToTriggerOld.setNotifyService(notifyServiceEntity); + + notifyPatternToTriggerService.update(context, notifyPatternToTriggerOld); + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifySubmissionService.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifySubmissionService.java new file mode 100644 index 000000000000..7cebc3341686 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/factory/impl/NotifySubmissionService.java @@ -0,0 +1,152 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.submit.factory.impl; + +import java.io.IOException; +import java.sql.SQLException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.apache.commons.collections4.CollectionUtils; +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.exception.UnprocessableEntityException; +import org.dspace.app.rest.model.step.DataNotify; +import org.dspace.authorize.AuthorizeException; +import org.dspace.coarnotify.NotifyConfigurationService; +import org.dspace.coarnotify.NotifyPattern; +import org.dspace.content.InProgressSubmission; +import org.dspace.core.Context; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * Service to manipulate COAR Notify section of in-progress submissions. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com + */ +@Component +public class NotifySubmissionService { + + @Autowired + private NotifyPatternToTriggerService notifyPatternToTriggerService; + + @Autowired + private NotifyConfigurationService coarNotifyConfigurationService; + + @Autowired + private NotifyService notifyService; + + private NotifySubmissionService() { } + + + /** + * Builds the COAR Notify data of an in-progress submission + * + * @param obj - the in progress submission + * @return an object representing the CC License data + * @throws SQLException + * @throws IOException + * @throws AuthorizeException + */ + public DataNotify getDataCOARNotify(InProgressSubmission obj) throws SQLException { + Context context = new Context(); + + List patternsToTrigger = + notifyPatternToTriggerService.findByItem(context, obj.getItem()); + + Map> data = + patternsToTrigger.stream() + .collect(Collectors.groupingBy( + NotifyPatternToTrigger::getPattern, + Collectors.mapping(patternToTrigger -> + patternToTrigger.getNotifyService().getID(), + Collectors.toList()) + )); + + return new DataNotify(data); + } + + + public int extractIndex(String path) { + Pattern pattern = Pattern.compile("/(\\d+)$"); + Matcher matcher = pattern.matcher(path); + + if (matcher.find()) { + return Integer.parseInt(matcher.group(1)); + } else { + throw new UnprocessableEntityException("Index not found in the path"); + } + } + + /** + * extract pattern from path. see COARNotifyConfigurationService bean + * @param path + * @return the extracted pattern + */ + public String extractPattern(String path) { + Pattern pattern = Pattern.compile("/([^/]+)/([^/]+)/([^/]+)"); + Matcher matcher = pattern.matcher(path); + if (matcher.find()) { + String patternValue = matcher.group(3); + String config = matcher.group(2); + if (!isContainPattern(config, patternValue)) { + throw new UnprocessableEntityException( + "Invalid Pattern (" + patternValue + ") of " + config); + } + return patternValue; + } else { + throw new UnprocessableEntityException("Pattern not found in the path"); + } + } + + private boolean isContainPattern(String config, String pattern) { + List patterns = coarNotifyConfigurationService.getPatterns().get(config); + if (CollectionUtils.isEmpty(patterns)) { + return false; + } + + return patterns.stream() + .map(NotifyPattern::getPattern) + .anyMatch(v -> + v.equals(pattern)); + } + + /** + * check that the provided services ids are compatible + * with the provided inbound pattern + * + * @param context the context + * @param pattern the inbound pattern + * @param servicesIds notify services ids + * @throws SQLException if something goes wrong + */ + public void checkCompatibilityWithPattern(Context context, String pattern, Set servicesIds) + throws SQLException { + + List manualServicesIds = + notifyService.findManualServicesByInboundPattern(context, pattern) + .stream() + .map(NotifyServiceEntity::getID) + .collect(Collectors.toList()); + + for (Integer servicesId : servicesIds) { + if (!manualServicesIds.contains(servicesId)) { + throw new UnprocessableEntityException("notify service with id (" + servicesId + + ") is not compatible with pattern " + pattern); + } + } + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/NotifyStep.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/NotifyStep.java new file mode 100644 index 000000000000..b3e3d06c4727 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/NotifyStep.java @@ -0,0 +1,62 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest.submit.step; + +import javax.servlet.http.HttpServletRequest; + +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.step.DataNotify; +import org.dspace.app.rest.submit.AbstractProcessingStep; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.rest.submit.factory.PatchOperationFactory; +import org.dspace.app.rest.submit.factory.impl.PatchOperation; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.content.InProgressSubmission; +import org.dspace.core.Context; + +/** + * COARNotify Step for DSpace Spring Rest. Expose information about + * the COAR Notify services for the in progress submission. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyStep extends AbstractProcessingStep { + + /** + * Retrieves the COAR Notify services data of the in progress submission + * + * @param submissionService the submission service + * @param obj the in progress submission + * @param config the submission step configuration + * @return the COAR Notify data of the in progress submission + * @throws Exception + */ + @Override + public DataNotify getData(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) throws Exception { + return coarNotifySubmissionService.getDataCOARNotify(obj); + } + + /** + * Processes a patch for the COAR Notify data + * + * @param context the DSpace context + * @param currentRequest the http request + * @param source the in progress submission + * @param op the json patch operation + * @throws Exception + */ + @Override + public void doPatchProcessing(Context context, HttpServletRequest currentRequest, InProgressSubmission source, + Operation op, SubmissionStepConfig stepConf) throws Exception { + + PatchOperation patchOperation = new PatchOperationFactory().instanceOf( + COARNOTIFY_STEP_PATH, op.getOp()); + patchOperation.perform(context, currentRequest, source, op); + } +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/NotifyValidation.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/NotifyValidation.java new file mode 100644 index 000000000000..04a1567fdac8 --- /dev/null +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/submit/step/validation/NotifyValidation.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.app.rest.submit.step.validation; + +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.dspace.app.ldn.NotifyPatternToTrigger; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyPatternToTriggerService; +import org.dspace.app.rest.model.ErrorRest; +import org.dspace.app.rest.repository.WorkspaceItemRestRepository; +import org.dspace.app.rest.submit.SubmissionService; +import org.dspace.app.rest.utils.ContextUtil; +import org.dspace.app.util.DCInputsReaderException; +import org.dspace.app.util.SubmissionStepConfig; +import org.dspace.coarnotify.NotifyConfigurationService; +import org.dspace.coarnotify.NotifyPattern; +import org.dspace.content.InProgressSubmission; +import org.dspace.content.Item; +import org.dspace.content.logic.LogicalStatement; +import org.dspace.core.Context; +import org.dspace.utils.DSpace; + +/** + * Execute check validation of Coar notify services filters + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyValidation extends AbstractValidation { + + private static final String ERROR_VALIDATION_INVALID_FILTER = "error.validation.coarnotify.invalidfilter"; + + private NotifyConfigurationService coarNotifyConfigurationService; + + private NotifyPatternToTriggerService notifyPatternToTriggerService; + + @Override + public List validate(SubmissionService submissionService, InProgressSubmission obj, + SubmissionStepConfig config) throws DCInputsReaderException, SQLException { + + List errors = new ArrayList<>(); + Context context = ContextUtil.obtainCurrentRequestContext(); + Item item = obj.getItem(); + + List patterns = + coarNotifyConfigurationService.getPatterns().getOrDefault(config.getId(), List.of()) + .stream() + .map(NotifyPattern::getPattern) + .collect(Collectors.toList()); + + patterns.forEach(pattern -> { + List services = findByItemAndPattern(context, item, pattern); + IntStream.range(0, services.size()).forEach(i -> + services.get(i) + .getInboundPatterns() + .stream() + .filter(inboundPattern -> inboundPattern.getPattern().equals(pattern)) + .filter(inboundPattern -> !inboundPattern.isAutomatic() && + !inboundPattern.getConstraint().isEmpty()) + .forEach(inboundPattern -> { + LogicalStatement filter = + new DSpace().getServiceManager() + .getServiceByName(inboundPattern.getConstraint(), LogicalStatement.class); + + if (filter == null || !filter.getResult(context, item)) { + addError(errors, ERROR_VALIDATION_INVALID_FILTER, + "/" + WorkspaceItemRestRepository.OPERATION_PATH_SECTIONS + + "/" + config.getId() + + "/" + inboundPattern.getPattern() + + "/" + i + ); + } + })); + }); + + return errors; + } + + private List findByItemAndPattern(Context context, Item item, String pattern) { + try { + return notifyPatternToTriggerService.findByItemAndPattern(context, item, pattern) + .stream() + .map(NotifyPatternToTrigger::getNotifyService) + .collect(Collectors.toList()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + + public NotifyConfigurationService getCoarNotifyConfigurationService() { + return coarNotifyConfigurationService; + } + + public void setCoarNotifyConfigurationService( + NotifyConfigurationService coarNotifyConfigurationService) { + this.coarNotifyConfigurationService = coarNotifyConfigurationService; + } + + public NotifyPatternToTriggerService getNotifyPatternToTriggerService() { + return notifyPatternToTriggerService; + } + + public void setNotifyPatternToTriggerService( + NotifyPatternToTriggerService notifyPatternToTriggerService) { + this.notifyPatternToTriggerService = notifyPatternToTriggerService; + } + +} diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java index 48538deb13d4..ba43fdf9565c 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/ApplicationConfig.java @@ -34,7 +34,8 @@ "org.dspace.app.rest.utils", "org.dspace.app.configuration", "org.dspace.iiif", - "org.dspace.app.iiif" + "org.dspace.app.iiif", + "org.dspace.app.ldn" }) public class ApplicationConfig { // Allowed CORS origins ("Access-Control-Allow-Origin" header) @@ -71,6 +72,10 @@ public class ApplicationConfig { @Value("${dspace.ui.url:http://localhost:4000}") private String uiURL; + // LDN enable status + @Value("${ldn.enabled}") + private boolean ldnEnabled; + /** * Return the array of allowed origins (client URLs) for the CORS "Access-Control-Allow-Origin" header * Used by Application class @@ -129,6 +134,14 @@ public boolean getCorsAllowCredentials() { return corsAllowCredentials; } + /** + * Return the ldn.enabled value + * @return true or false + */ + public boolean getLdnEnabled() { + return this.ldnEnabled; + } + /** * Return whether to allow credentials (cookies) on IIIF requests. This is used to set the * CORS "Access-Control-Allow-Credentials" header in Application class. Defaults to false. diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java index 56b8ae32dce1..d63dc8c55a11 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceConfigurationInitializer.java @@ -11,8 +11,6 @@ import org.apache.commons.configuration2.spring.ConfigurationPropertySource; import org.dspace.services.ConfigurationService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; @@ -29,8 +27,6 @@ */ public class DSpaceConfigurationInitializer implements ApplicationContextInitializer { - private static final Logger log = LoggerFactory.getLogger(DSpaceConfigurationInitializer.class); - @Override public void initialize(final ConfigurableApplicationContext applicationContext) { // Load DSpace Configuration service (requires kernel already initialized) diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java index 73a96259bf21..7dfcd1d76d1d 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/DSpaceKernelInitializer.java @@ -12,13 +12,13 @@ import javax.naming.InitialContext; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.kernel.DSpaceKernel; import org.dspace.kernel.DSpaceKernelManager; import org.dspace.servicemanager.DSpaceKernelImpl; import org.dspace.servicemanager.DSpaceKernelInit; import org.dspace.servicemanager.config.DSpaceConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; @@ -31,7 +31,7 @@ */ public class DSpaceKernelInitializer implements ApplicationContextInitializer { - private static final Logger log = LoggerFactory.getLogger(DSpaceKernelInitializer.class); + private static final Logger log = LogManager.getLogger(); private transient DSpaceKernel dspaceKernel; @@ -119,6 +119,7 @@ public DSpaceKernelDestroyer(DSpaceKernel kernel) { this.kernel = kernel; } + @Override public void onApplicationEvent(final ContextClosedEvent event) { if (this.kernel != null) { this.kernel.destroy(); diff --git a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java index d68c710a3c7a..662223d187f5 100644 --- a/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java +++ b/dspace-server-webapp/src/main/java/org/dspace/app/rest/utils/HttpHeadersInitializer.java @@ -19,9 +19,9 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.apache.tomcat.util.http.FastHttpDateFormat; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; /** @@ -32,7 +32,7 @@ */ public class HttpHeadersInitializer { - protected final Logger log = LoggerFactory.getLogger(this.getClass()); + protected final Logger log = LogManager.getLogger(); private static final String MULTIPART_BOUNDARY = "MULTIPART_BYTERANGES"; private static final String CONTENT_TYPE_MULTITYPE_WITH_BOUNDARY = "multipart/byteranges; boundary=" + diff --git a/dspace-server-webapp/src/main/javadoc/overview.html b/dspace-server-webapp/src/main/javadoc/overview.html new file mode 100644 index 000000000000..5c58d2fc742e --- /dev/null +++ b/dspace-server-webapp/src/main/javadoc/overview.html @@ -0,0 +1,29 @@ + + + + + The DSpace Web API + + +

    + The REST back-end of DSpace. A front-end program such as + dspace-angular + can use this to query, fetch and manipulate DSpace objects and related + data. The REST layer sits between front-ends and the DSpace business + logic (chiefly in {@code dspace-api}). +

    + +

    Where To Find It:

    +
      +
    • Endpoint access authorization: {@link org.dspace.app.rest.security}
    • +
    + + diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml index f39d553c9652..013b406c1af9 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-addon-validation-services.xml @@ -30,4 +30,11 @@ + + + + + + + diff --git a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml index 7238e399675d..e865c0d5e748 100644 --- a/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml +++ b/dspace-server-webapp/src/main/resources/spring/spring-dspace-core-services.xml @@ -55,6 +55,9 @@ + + + @@ -86,6 +89,9 @@ + + + @@ -117,6 +123,9 @@ + + + @@ -127,4 +136,7 @@ + + + diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ContentReportRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ContentReportRestRepositoryIT.java new file mode 100644 index 000000000000..425b5f7a06b4 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ContentReportRestRepositoryIT.java @@ -0,0 +1,263 @@ +/** + * 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 org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +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 java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.dspace.app.rest.matcher.ContentReportMatcher; +import org.dspace.app.rest.matcher.HalMatcher; +import org.dspace.app.rest.model.FilteredCollectionsQuery; +import org.dspace.app.rest.model.FilteredItemsQueryPredicate; +import org.dspace.app.rest.model.FilteredItemsQueryRest; +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.content.Collection; +import org.dspace.content.Item; +import org.dspace.contentreport.Filter; +import org.dspace.contentreport.FilteredCollection; +import org.dspace.contentreport.QueryOperator; +import org.dspace.services.ConfigurationService; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Integration tests for the content reports ported from DSpace 6.x + * (Filtered Collections and Filtered Items). + * @author Jean-François Morin (Université Laval) + */ +public class ContentReportRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + @Test + public void testFilteredCollections() throws Exception { + context.turnOffAuthorisationSystem(); + + configurationService.setProperty("contentreport.enable", Boolean.TRUE); + + TestKit testKit = setupCollectionsAndItems(); + Collection col1 = testKit.collections.get(0); + Collection col2 = testKit.collections.get(1); + + context.restoreAuthSystemState(); + String token = getAuthToken(admin.getEmail(), password); + + Map valuesCol1 = Map.of(Filter.IS_DISCOVERABLE, 1); + FilteredCollection fcol1 = FilteredCollection.of(col1.getName(), col1.getHandle(), + parentCommunity.getName(), parentCommunity.getHandle(), + 1, 1, valuesCol1, true); + Map valuesCol2 = Map.of(Filter.IS_DISCOVERABLE, 2); + FilteredCollection fcol2 = FilteredCollection.of(col2.getName(), col2.getHandle(), + parentCommunity.getName(), parentCommunity.getHandle(), + 2, 2, valuesCol2, true); + + FilteredCollectionsQuery query = FilteredCollectionsQuery.of(Set.of(Filter.IS_DISCOVERABLE)); + + getClient(token).perform(get("/api/contentreport/filteredcollections?" + query.toQueryString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.collections", Matchers.containsInAnyOrder( + ContentReportMatcher.matchFilteredCollectionProperties(fcol1), + ContentReportMatcher.matchFilteredCollectionProperties(fcol2) + ))) + .andExpect(jsonPath("type", is("filteredcollectionsreport"))) + .andExpect(jsonPath("$.summary", + ContentReportMatcher.matchFilteredCollectionSummary(3, 3))) + .andExpect(jsonPath("$._links.self.href", + Matchers.containsString("/api/contentreport/filteredcollections"))); + } + + @Test + public void testFilteredCollectionsUnauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + + configurationService.setProperty("contentreport.enable", Boolean.TRUE); + + setupCollectionsAndItems(); + + context.restoreAuthSystemState(); + + FilteredCollectionsQuery query = FilteredCollectionsQuery.of(Set.of(Filter.IS_DISCOVERABLE)); + + getClient().perform(get("/api/contentreport/filteredcollections?" + query.toQueryString())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testFilteredCollectionsOff() throws Exception { + context.turnOffAuthorisationSystem(); + + configurationService.setProperty("contentreport.enable", Boolean.FALSE); + + setupCollectionsAndItems(); + + context.restoreAuthSystemState(); + String token = getAuthToken(admin.getEmail(), password); + + FilteredCollectionsQuery query = FilteredCollectionsQuery.of(Set.of(Filter.IS_DISCOVERABLE)); + + getClient(token).perform(get("/api/contentreport/filteredcollections?" + query.toQueryString())) + .andExpect(status().isNotFound()); + } + + @Test + public void testFilteredItems() throws Exception { + context.turnOffAuthorisationSystem(); + + configurationService.setProperty("contentreport.enable", Boolean.TRUE); + + TestKit testKit = setupCollectionsAndItems(); + Item publicItem2 = testKit.items.get(1); + Item publicItem3 = testKit.items.get(2); + + context.restoreAuthSystemState(); + String token = getAuthToken(admin.getEmail(), password); + + FilteredItemsQueryRest query = FilteredItemsQueryRest.of(null, + List.of(FilteredItemsQueryPredicate.of("dc.contributor.author", QueryOperator.EQUALS, "Doe, Jane")), + 100, null, List.of("dc.subject")); + + getClient(token).perform(get("/api/contentreport/filtereditems?" + query.toQueryString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", HalMatcher.matchNoEmbeds())) + .andExpect(jsonPath("$.itemCount", is(2))) + .andExpect(jsonPath("$.items", Matchers.containsInAnyOrder( + matchItemProperties(publicItem2), + matchItemProperties(publicItem3) + ))); + } + + @Test + public void testFilteredItemsUnauthorized() throws Exception { + context.turnOffAuthorisationSystem(); + + configurationService.setProperty("contentreport.enable", Boolean.TRUE); + + setupCollectionsAndItems(); + + context.restoreAuthSystemState(); + + FilteredItemsQueryRest query = FilteredItemsQueryRest.of(null, + List.of(FilteredItemsQueryPredicate.of("dc.contributor.author", QueryOperator.EQUALS, "Doe, Jane")), + 100, null, List.of("dc.subject")); + + getClient().perform(get("/api/contentreport/filtereditems?" + query.toQueryString())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void testFilteredItemsOff() throws Exception { + context.turnOffAuthorisationSystem(); + + configurationService.setProperty("contentreport.enable", Boolean.FALSE); + + setupCollectionsAndItems(); + + context.restoreAuthSystemState(); + String token = getAuthToken(admin.getEmail(), password); + + FilteredItemsQueryRest query = FilteredItemsQueryRest.of(null, + List.of(FilteredItemsQueryPredicate.of("dc.contributor.author", QueryOperator.EQUALS, "Doe, Jane")), + 100, null, List.of("dc.subject")); + + getClient(token).perform(get("/api/contentreport/filtereditems?" + query.toQueryString())) + .andExpect(status().isNotFound()); + } + + // Need for a specific filtered item type... + private static Matcher matchItemProperties(Item item) { + return allOf( + hasJsonPath("$.uuid", is(item.getID().toString())), + hasJsonPath("$.name", is(item.getName())), + hasJsonPath("$.handle", is(item.getHandle())), + hasJsonPath("$.inArchive", is(item.isArchived())), + hasJsonPath("$.discoverable", is(item.isDiscoverable())), + hasJsonPath("$.withdrawn", is(item.isWithdrawn())), + hasJsonPath("$.lastModified", is(notNullValue())), + hasJsonPath("$.type", is("filtered-item")) + ); + } + + private TestKit setupCollectionsAndItems() { + //** GIVEN ** + //1. A community with two collections. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("My Community") + .build(); + + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Collection col2 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 2").build(); + + LocalDate today = LocalDate.now(); + LocalDate pastDate = today.minusDays(10); + String fmtPastDate = DateTimeFormatter.ISO_DATE.format(pastDate); + LocalDate futureDate = today.plusDays(10); + String fmtFutureDate = DateTimeFormatter.ISO_DATE.format(futureDate); + + //2. Three items, two of which are available and discoverable... + Item item1 = ItemBuilder.createItem(context, col1) + .withTitle("Public item 1") + .withIssueDate(fmtPastDate) + .withAuthor("Smith, Donald").withAuthor("Doe, John") + .withSubject("ExtraEntry") + .build(); + + Item item2 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 2") + .withIssueDate(fmtPastDate) + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("TestingForMore").withSubject("ExtraEntry") + .build(); + + // ... and one will be available in a few days. + Item item3 = ItemBuilder.createItem(context, col2) + .withTitle("Public item 3") + .withIssueDate(fmtFutureDate) + .withAuthor("Smith, Maria").withAuthor("Doe, Jane") + .withSubject("AnotherTest").withSubject("TestingForMore") + .withSubject("ExtraEntry") + .build(); + + TestKit kit = new TestKit(); + kit.collections.add(col1); + kit.collections.add(col2); + kit.items.add(item1); + kit.items.add(item2); + kit.items.add(item3); + return kit; + } + + /** + * Data structure to help trace back the created collections and items to perform the tests. + * In a future version (Java 17 or later), this class could be turned into a record. + */ + private static class TestKit { + + public final List collections = new ArrayList<>(); + public final List items = new ArrayList<>(); + + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemFilterRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemFilterRestRepositoryIT.java new file mode 100644 index 000000000000..d6ae2e1aa0bd --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/ItemFilterRestRepositoryIT.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; + +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.dspace.app.rest.repository.ItemFilterRestRepository; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.junit.Test; + +/** + * Integration test for {@link ItemFilterRestRepository} + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class ItemFilterRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Test + public void findOneUnauthorizedTest() throws Exception { + getClient().perform(get("/api/config/itemfilters/test")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneForbiddenTest() throws Exception { + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(get("/api/config/itemfilters/test")) + .andExpect(status().isForbidden()); + } + + @Test + public void findOneNotFoundTest() throws Exception { + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/config/itemfilters/test")) + .andExpect(status().isNotFound()); + } + + @Test + public void findOneTest() throws Exception { + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/config/itemfilters/always_true_filter")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is("always_true_filter"))) + .andExpect(jsonPath("$._links.self.href", + containsString("/api/config/itemfilters/always_true_filter"))); + } + + @Test + public void findAllUnauthorizedTest() throws Exception { + getClient().perform(get("/api/config/itemfilters")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAllForbiddenTest() throws Exception { + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(get("/api/config/itemfilters")) + .andExpect(status().isForbidden()); + } + + @Test + public void findAllPaginatedSortedTest() throws Exception { + getClient(getAuthToken(admin.getEmail(), password)) + .perform(get("/api/config/itemfilters") + .param("size", "30")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(5))) + .andExpect(jsonPath("$.page.totalPages", is(1))) + .andExpect(jsonPath("$.page.size", is(30))) + .andExpect(jsonPath("$._embedded.itemfilters", contains( + hasJsonPath("$.id", is("always_true_filter")), + hasJsonPath("$.id", is("demo_filter")), + hasJsonPath("$.id", is("doi-filter")), + hasJsonPath("$.id", is("in-outfit-collection_condition")), + hasJsonPath("$.id", is("type_filter"))))); + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java new file mode 100644 index 000000000000..1437c8dd62ef --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/LDNInboxControllerIT.java @@ -0,0 +1,415 @@ +/** + * 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.content.QAEvent.COAR_NOTIFY_SOURCE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.sql.SQLException; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.io.IOUtils; +import org.dspace.app.ldn.LDNMessageEntity; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +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.NotifyServiceBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.matcher.QASourceMatcher; +import org.dspace.matcher.QATopicMatcher; +import org.dspace.qaevent.QANotifyPatterns; +import org.dspace.qaevent.service.QAEventService; +import org.dspace.services.ConfigurationService; +import org.dspace.utils.DSpace; +import org.junit.After; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + + +/** + * LDN Controller test class. Simulate receiving external LDN messages + * @author Francesco Bacchelli (francesco.bacchelli at 4science.it) + * + */ + +public class LDNInboxControllerIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private LDNMessageService ldnMessageService; + + private QAEventService qaEventService = new DSpace().getSingletonService(QAEventService.class); + + @Test + public void ldnInboxAnnounceEndorsementTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("https://overlay-journal.com/inbox/") + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") + .build(); + context.restoreAuthSystemState(); + + InputStream announceEndorsementStream = getClass().getResourceAsStream("ldn_announce_endorsement.json"); + String announceEndorsement = IOUtils.toString(announceEndorsementStream, Charset.defaultCharset()); + announceEndorsementStream.close(); + String message = announceEndorsement.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + LDNMessageEntity ldnMessage = ldnMessageService.find(context, notification.getId()); + checkStoredLDNMessage(notification, ldnMessage, object); + } + + @Test + public void ldnInboxAnnounceReviewTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + InputStream announceReviewStream = getClass().getResourceAsStream("ldn_announce_review.json"); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://review-service.com/inbox/about/") + .withLdnUrl("https://review-service.com/inbox/") + .withScore(BigDecimal.valueOf(0.6d)) + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") + .build(); + String announceReview = IOUtils.toString(announceReviewStream, Charset.defaultCharset()); + announceReviewStream.close(); + String message = announceReview.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + ldnMessageService.extractAndProcessMessageFromQueue(context); + + assertThat(qaEventService.findAllSources(context, 0, 20), + hasItem(QASourceMatcher.with(COAR_NOTIFY_SOURCE, 1L))); + + assertThat(qaEventService.findAllTopicsBySource(context, COAR_NOTIFY_SOURCE, 0, 20, "topic", true), hasItem( + QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW, 1L))); + + } + + @Test + public void ldnInboxEndorsementActionBadRequestTest() throws Exception { + // id is not an uri + InputStream offerEndorsementStream = getClass().getResourceAsStream("ldn_offer_endorsement_badrequest.json"); + String message = IOUtils.toString(offerEndorsementStream, Charset.defaultCharset()); + offerEndorsementStream.close(); + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isBadRequest()); + } + + @Test + public void ldnInboxOfferReviewAndACKTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://review-service.com/inbox/about/") + .withLdnUrl("https://review-service.com/inbox/") + .withScore(BigDecimal.valueOf(0.6d)) + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") + .build(); + InputStream offerReviewStream = getClass().getResourceAsStream("ldn_offer_review.json"); + String announceReview = IOUtils.toString(offerReviewStream, Charset.defaultCharset()); + offerReviewStream.close(); + String message = announceReview.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + int processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 1); + processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 0); + + InputStream ackReviewStream = getClass().getResourceAsStream("ldn_ack_review_reject.json"); + String ackReview = IOUtils.toString(ackReviewStream, Charset.defaultCharset()); + offerReviewStream.close(); + String ackMessage = ackReview.replaceAll("<>", object); + ackMessage = ackMessage.replaceAll("<>", + "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509de"); + ObjectMapper ackMapper = new ObjectMapper(); + Notification ackNotification = mapper.readValue(ackMessage, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(ackMessage)) + .andExpect(status().isAccepted()); + + int ackProcessed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(ackProcessed, 1); + ackProcessed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(ackProcessed, 0); + + + } + + @Test + public void ldnInboxAnnounceReleaseTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + InputStream announceRelationshipStream = getClass().getResourceAsStream("ldn_announce_release.json"); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://review-service.com/inbox/about/") + .withLdnUrl("https://review-service.com/inbox/") + .withScore(BigDecimal.valueOf(0.6d)) + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.3") + .build(); + String announceRelationship = IOUtils.toString(announceRelationshipStream, Charset.defaultCharset()); + announceRelationshipStream.close(); + String message = announceRelationship.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + ldnMessageService.extractAndProcessMessageFromQueue(context); + + assertThat(qaEventService.findAllSources(context, 0, 20), + hasItem(QASourceMatcher.with(COAR_NOTIFY_SOURCE, 1L))); + + assertThat(qaEventService.findAllTopicsBySource(context, COAR_NOTIFY_SOURCE, 0, 20, "topic", true), hasItem( + QATopicMatcher.with(QANotifyPatterns.TOPIC_ENRICH_MORE_LINK, 1L))); + + } + + private void checkStoredLDNMessage(Notification notification, LDNMessageEntity ldnMessage, String object) + throws Exception { + + ObjectMapper mapper = new ObjectMapper(); + Notification storedMessage = mapper.readValue(ldnMessage.getMessage(), Notification.class); + + assertNotNull(ldnMessage); + assertNotNull(ldnMessage.getObject()); + assertEquals(ldnMessage.getObject() + .getMetadata() + .stream() + .filter(metadataValue -> + metadataValue.getMetadataField().toString('.').equals("dc.identifier.uri")) + .map(metadataValue -> metadataValue.getValue()) + .findFirst().get(), object); + + assertEquals(notification.getId(), storedMessage.getId()); + assertEquals(notification.getOrigin().getInbox(), storedMessage.getOrigin().getInbox()); + assertEquals(notification.getTarget().getInbox(), storedMessage.getTarget().getInbox()); + assertEquals(notification.getObject().getId(), storedMessage.getObject().getId()); + assertEquals(notification.getType(), storedMessage.getType()); + } + + @Test + public void ldnInboxAnnounceEndorsementInvalidIpTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("https://overlay-journal.com/inbox/") + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.1") + .build(); + context.restoreAuthSystemState(); + + InputStream announceEndorsementStream = getClass().getResourceAsStream("ldn_announce_endorsement.json"); + String announceEndorsement = IOUtils.toString(announceEndorsementStream, Charset.defaultCharset()); + announceEndorsementStream.close(); + String message = announceEndorsement.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox").with(remoteHost("mydocker.url", "172.23.0.1")) + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isBadRequest()); + + int processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 0); + LDNMessageEntity ldnMessage = ldnMessageService.find(context, notification.getId()); + assertNull(ldnMessage); + } + + @Test + public void ldnInboxAnnounceEndorsementInvalidInboxTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("https://overlay-journal.com/inbox/") + .withLowerIp("127.0.0.2") + .withUpperIp("127.0.0.5") + .build(); + context.restoreAuthSystemState(); + + InputStream announceEndorsementStream = getClass().getResourceAsStream("ldn_origin_inbox_unregistered.json"); + String announceEndorsement = IOUtils.toString(announceEndorsementStream, Charset.defaultCharset()); + announceEndorsementStream.close(); + String message = announceEndorsement.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isBadRequest()); + + int processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 0); + + } + + @Test + public void ldnInboxOutOfRangeIPwithDisabledCheckTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + configurationService.setProperty("ldn.notify.inbox.block-untrusted-ip", false); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("https://overlay-journal.com/inbox/") + .withLowerIp("127.0.0.1") + .withUpperIp("127.0.0.1") + .build(); + context.restoreAuthSystemState(); + + InputStream announceEndorsementStream = getClass().getResourceAsStream("ldn_announce_endorsement.json"); + String announceEndorsement = IOUtils.toString(announceEndorsementStream, Charset.defaultCharset()); + announceEndorsementStream.close(); + String message = announceEndorsement.replaceAll("<>", object); + message = message.replaceAll("<>", object); + + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox").with(remoteHost("mydocker.url", "172.23.0.1")) + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + LDNMessageEntity ldnMessage = ldnMessageService.find(context, notification.getId()); + checkStoredLDNMessage(notification, ldnMessage, object); + assertEquals(ldnMessage.getQueueStatus(), LDNMessageEntity.QUEUE_STATUS_UNTRUSTED_IP); + } + + private static RequestPostProcessor remoteHost(final String remoteHost, final String remoteAddr) { + return request -> { + request.setRemoteHost(remoteHost); + request.setRemoteAddr(remoteAddr); + return request; + }; + } + + @Override + @After + public void destroy() throws Exception { + List ldnMessageEntities = ldnMessageService.findAll(context); + if (CollectionUtils.isNotEmpty(ldnMessageEntities)) { + ldnMessageEntities.forEach(ldnMessage -> { + try { + ldnMessageService.delete(context, ldnMessage); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + super.destroy(); + } +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyRequestStatusRestControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyRequestStatusRestControllerIT.java new file mode 100644 index 000000000000..a7077f20da18 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyRequestStatusRestControllerIT.java @@ -0,0 +1,166 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +import static org.junit.Assert.assertEquals; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.Charset; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.model.Notification; +import org.dspace.app.ldn.service.LDNMessageService; +import org.dspace.app.rest.model.NotifyRequestStatusRest; +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.NotifyServiceBuilder; +import org.dspace.content.Collection; +import org.dspace.content.Community; +import org.dspace.content.Item; +import org.dspace.services.ConfigurationService; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Rest Controller for NotifyRequestStatus targeting items IT + * class {@link NotifyRequestStatusRestController} + * + * @author Francesco Bacchelli (francesco.bacchelli at 4science dot it) + */ +public class NotifyRequestStatusRestControllerIT extends AbstractControllerIntegrationTest { + + @Autowired + private ConfigurationService configurationService; + + @Autowired + private LDNMessageService ldnMessageService; + + @Test + public void oneStatusReviewedTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://review-service.com/inbox/about/") + .withLdnUrl("https://review-service.com/inbox/") + .withScore(BigDecimal.valueOf(0.6d)) + .build(); + //SEND OFFER REVIEW + InputStream offerReviewStream = getClass().getResourceAsStream("ldn_offer_review.json"); + String announceReview = IOUtils.toString(offerReviewStream, Charset.defaultCharset()); + offerReviewStream.close(); + String message = announceReview.replaceAll("<>", object); + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + int processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 1); + processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 0); + + //CHECK THE SERVICE ON ITS notifystatus ARRAY + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/" + NotifyRequestStatusRest.CATEGORY + "/" + + NotifyRequestStatusRest.NAME + "/" + item.getID()) + .contentType("application/ld+json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyStatus").isArray()) + .andExpect(jsonPath("$.notifyStatus").isNotEmpty()) + .andExpect(jsonPath("$.notifyStatus[0].status").value("REQUESTED")) + .andExpect(jsonPath("$.notifyStatus[0].serviceUrl").value("https://review-service.com/inbox/about/")) + ; + } + + @Test + public void oneStatusRejectedTest() throws Exception { + context.turnOffAuthorisationSystem(); + Community community = CommunityBuilder.createCommunity(context).withName("community").build(); + Collection collection = CollectionBuilder.createCollection(context, community).build(); + Item item = ItemBuilder.createItem(context, collection).build(); + String object = configurationService.getProperty("dspace.ui.url") + "/handle/" + item.getHandle(); + NotifyServiceEntity notifyServiceEntity = NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://review-service.com/inbox/about/") + .withLdnUrl("https://review-service.com/inbox/") + .withScore(BigDecimal.valueOf(0.6d)) + .build(); + //SEND OFFER REVIEW + InputStream offerReviewStream = getClass().getResourceAsStream("ldn_offer_review2.json"); + String announceReview = IOUtils.toString(offerReviewStream, Charset.defaultCharset()); + offerReviewStream.close(); + String message = announceReview.replaceAll("<>", object); + ObjectMapper mapper = new ObjectMapper(); + Notification notification = mapper.readValue(message, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(message)) + .andExpect(status().isAccepted()); + + int processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(processed, 1); + processed = ldnMessageService.extractAndProcessMessageFromQueue(context); + + assertEquals(processed, 0); + //SEND ACK REVIEW REJECTED + InputStream ackReviewStream = getClass().getResourceAsStream("ldn_ack_review_reject.json"); + String ackReview = IOUtils.toString(ackReviewStream, Charset.defaultCharset()); + ackReviewStream.close(); + String ackMessage = ackReview.replaceAll("<>", object); + ackMessage = ackMessage.replaceAll( + "<>", "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509df"); + + ObjectMapper ackMapper = new ObjectMapper(); + Notification ackNotification = ackMapper.readValue(ackMessage, Notification.class); + getClient() + .perform(post("/ldn/inbox") + .contentType("application/ld+json") + .content(ackMessage)) + .andExpect(status().isAccepted()); + + int ackProcessed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(ackProcessed, 1); + ackProcessed = ldnMessageService.extractAndProcessMessageFromQueue(context); + assertEquals(ackProcessed, 0); + + //CHECK THE SERVICE ON ITS notifystatus ARRAY + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/" + NotifyRequestStatusRest.CATEGORY + "/" + + NotifyRequestStatusRest.NAME + "/" + item.getID()) + .contentType("application/ld+json")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyStatus").isArray()) + .andExpect(jsonPath("$.notifyStatus").isNotEmpty()) + .andExpect(jsonPath("$.notifyStatus[0].status").value("REJECTED")) + .andExpect(jsonPath("$.notifyStatus[0].serviceUrl").value("https://review-service.com/inbox/about/")) + .andExpect(jsonPath("$.notifyStatus[0].offerType").value("Review")) + ; + } +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java new file mode 100644 index 000000000000..984b9b573e5c --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java @@ -0,0 +1,2492 @@ +/** + * 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.JsonPath.read; +import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath; +import static org.dspace.app.rest.matcher.NotifyServiceMatcher.matchNotifyService; +import static org.dspace.app.rest.matcher.NotifyServiceMatcher.matchNotifyServicePattern; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.math.BigDecimal; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import javax.ws.rs.core.MediaType; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.RandomUtils; +import org.dspace.app.ldn.NotifyServiceEntity; +import org.dspace.app.ldn.service.NotifyService; +import org.dspace.app.rest.model.NotifyServiceInboundPatternRest; +import org.dspace.app.rest.model.NotifyServiceRest; +import org.dspace.app.rest.model.patch.AddOperation; +import org.dspace.app.rest.model.patch.Operation; +import org.dspace.app.rest.model.patch.RemoveOperation; +import org.dspace.app.rest.model.patch.ReplaceOperation; +import org.dspace.app.rest.repository.NotifyServiceRestRepository; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.builder.NotifyServiceBuilder; +import org.dspace.builder.NotifyServiceInboundPatternBuilder; +import org.junit.After; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; + + +/** + * Integration test class for {@link NotifyServiceRestRepository}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Autowired + private NotifyService notifyService; + + @Test + public void findAllUnAuthorizedTest() throws Exception { + getClient().perform(get("/api/ldn/ldnservices")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAllTest() throws Exception { + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntityOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withDescription("service description one") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceEntityTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name two") + .withDescription("service description two") + .withUrl("https://service2.ldn.org/about") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceEntityThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name three") + .withDescription("service description three") + .withUrl("https://service3.ldn.org/about") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken) + .perform(get("/api/ldn/ldnservices")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$._embedded.ldnservices", containsInAnyOrder( + matchNotifyService(notifyServiceEntityOne.getID(), "service name one", "service description one", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + matchNotifyService(notifyServiceEntityTwo.getID(), "service name two", "service description two", + "https://service2.ldn.org/about", "https://service2.ldn.org/inbox"), + matchNotifyService(notifyServiceEntityThree.getID(), "service name three", "service description three", + "https://service3.ldn.org/about", "https://service3.ldn.org/inbox") + ))); + } + + @Test + public void findOneUnAuthorizedTest() throws Exception { + getClient().perform(get("/api/ldn/ldnservices/1")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneNotFoundTest() throws Exception { + + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(get("/api/ldn/ldnservices/" + RandomUtils.nextInt())) + .andExpect(status().isNotFound()); + } + + @Test + public void findOneTest() throws Exception { + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken) + .perform(get("/api/ldn/ldnservices/" + notifyServiceEntity.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"))); + } + + @Test + public void createForbiddenTest() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + NotifyServiceRest notifyServiceRest = new NotifyServiceRest(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(post("/api/ldn/ldnservices") + .content(mapper.writeValueAsBytes(notifyServiceRest)) + .contentType(contentType)) + .andExpect(status().isForbidden()); + } + + @Test + public void createTestScoreFail() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + NotifyServiceInboundPatternRest inboundPatternRestOne = new NotifyServiceInboundPatternRest(); + inboundPatternRestOne.setPattern("patternA"); + inboundPatternRestOne.setConstraint("itemFilterA"); + inboundPatternRestOne.setAutomatic(true); + + NotifyServiceInboundPatternRest inboundPatternRestTwo = new NotifyServiceInboundPatternRest(); + inboundPatternRestTwo.setPattern("patternB"); + inboundPatternRestTwo.setAutomatic(false); + + NotifyServiceRest notifyServiceRest = new NotifyServiceRest(); + notifyServiceRest.setName("service name"); + notifyServiceRest.setDescription("service description"); + notifyServiceRest.setUrl("service url"); + notifyServiceRest.setLdnUrl("service ldn url"); + notifyServiceRest.setScore(BigDecimal.TEN); + notifyServiceRest.setNotifyServiceInboundPatterns(List.of(inboundPatternRestOne, inboundPatternRestTwo)); + notifyServiceRest.setEnabled(false); + + AtomicReference idRef = new AtomicReference(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(post("/api/ldn/ldnservices") + .content(mapper.writeValueAsBytes(notifyServiceRest)) + .contentType(contentType)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void createTest() throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + NotifyServiceInboundPatternRest inboundPatternRestOne = new NotifyServiceInboundPatternRest(); + inboundPatternRestOne.setPattern("patternA"); + inboundPatternRestOne.setConstraint("itemFilterA"); + inboundPatternRestOne.setAutomatic(true); + + NotifyServiceInboundPatternRest inboundPatternRestTwo = new NotifyServiceInboundPatternRest(); + inboundPatternRestTwo.setPattern("patternB"); + inboundPatternRestTwo.setAutomatic(false); + + NotifyServiceRest notifyServiceRest = new NotifyServiceRest(); + notifyServiceRest.setName("service name"); + notifyServiceRest.setDescription("service description"); + notifyServiceRest.setUrl("https://service.ldn.org/about"); + notifyServiceRest.setLdnUrl("https://service.ldn.org/inbox"); + notifyServiceRest.setNotifyServiceInboundPatterns(List.of(inboundPatternRestOne, inboundPatternRestTwo)); + notifyServiceRest.setEnabled(false); + notifyServiceRest.setLowerIp("192.168.0.1"); + notifyServiceRest.setUpperIp("192.168.0.5"); + + AtomicReference idRef = new AtomicReference(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(post("/api/ldn/ldnservices") + .content(mapper.writeValueAsBytes(notifyServiceRest)) + .contentType(contentType)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$", matchNotifyService("service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox", false, + "192.168.0.1", "192.168.0.5"))) + .andDo(result -> + idRef.set((read(result.getResponse().getContentAsString(), "$.id")))); + + getClient(authToken) + .perform(get("/api/ldn/ldnservices/" + idRef.get())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", allOf( + matchNotifyService(idRef.get(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox", false, + "192.168.0.1", "192.168.0.5"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", true), + matchNotifyServicePattern("patternB", null, false) + ))) + )); + } + + @Test + public void notifyServicePatchOperationForbiddenTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation operation = new AddOperation("/description", "add service description"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isForbidden()); + } + + @Test + public void notifyServiceDescriptionAddOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation operation = new AddOperation("/description", "add service description"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceDescriptionAddOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .isEnabled(false) + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation operation = new AddOperation("/description", "add service description"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "add service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", false)) + ); + } + + @Test + public void notifyServiceDescriptionReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/description", "service description replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceDescriptionReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/description", "service description replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description replaced", "https://service.ldn.org/about", + "https://service.ldn.org/inbox", false)) + ); + } + + @Test + public void notifyServiceDescriptionRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .isEnabled(false) + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/description"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + null, "https://service.ldn.org/about", "https://service.ldn.org/inbox", false)) + ); + } + + @Test + public void notifyServiceUrlAddOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation operation = new AddOperation("/url", "add service url"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceUrlAddOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation operation = new AddOperation("/url", "add service url"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "add service url", "https://service.ldn.org/inbox", false)) + ); + } + + @Test + public void notifyServiceUrlReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/url", "service url replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceUrlReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .isEnabled(true) + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/url", "service url replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "service url replaced", "https://service.ldn.org/inbox", true)) + ); + } + + @Test + public void notifyServiceUrlRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/url"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", null, "https://service.ldn.org/inbox")) + ); + } + + @Test + public void notifyServiceNameReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/name", "service name replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceNameReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/name", "service name replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name replaced", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox")) + ); + } + + @Test + public void notifyServiceLdnUrlReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/ldnurl", "service ldn url replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceLdnUrlReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/ldnurl", "service ldn url replaced"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "service ldn url replaced")) + ); + } + + @Test + public void notifyServiceNameRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/name"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @Ignore + /* + * frabacche senseless because it's a mandatory+unique + * entity field and also table column! + */ + public void notifyServiceLdnUrlRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/ldnurl"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void findByLdnUrlUnAuthorizedTest() throws Exception { + getClient().perform(get("/api/ldn/ldnservices/search/byLdnUrl") + .param("ldnUrl", "test")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findByLdnUrlBadRequestTest() throws Exception { + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(get("/api/ldn/ldnservices/search/byLdnUrl")) + .andExpect(status().isBadRequest()); + } + + @Test + public void findByLdnUrlTest() throws Exception { + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntityOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withDescription("service description one") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceEntityTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name two") + .withDescription("service description two") + .withUrl("https://service2.ldn.org/about") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name three") + .withDescription("service description three") + .withUrl("https://service3.ldn.org/about") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken) + .perform(get("/api/ldn/ldnservices/search/byLdnUrl") + .param("ldnUrl", notifyServiceEntityOne.getLdnUrl())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntityOne.getID(), + "service name one", "service description one", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"))); + } + + @Test + public void deleteUnAuthorizedTest() throws Exception { + getClient().perform(delete("/api/ldn/ldnservices/" + RandomUtils.nextInt())) + .andExpect(status().isUnauthorized()); + } + + @Test + public void deleteForbiddenTest() throws Exception { + getClient(getAuthToken(eperson.getEmail(), password)) + .perform(delete("/api/ldn/ldnservices/" + RandomUtils.nextInt())) + .andExpect(status().isForbidden()); + } + + @Test + public void deleteNotFoundTest() throws Exception { + getClient(getAuthToken(admin.getEmail(), password)) + .perform(delete("/api/ldn/ldnservices/" + RandomUtils.nextInt())) + .andExpect(status().isNotFound()); + } + + @Test + public void deleteTest() throws Exception { + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("service ldnUrl") + .build(); + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(delete("/api/ldn/ldnservices/" + notifyServiceEntity.getID())) + .andExpect(status().isNoContent()); + + getClient(authToken) + .perform(get("/api/ldn/ldnservices/" + notifyServiceEntity.getID())) + .andExpect(status().isNotFound()); + } + + @Test + public void NotifyServiceInboundPatternsAddOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternsAddOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + // patch add operation but pattern is already existed + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + RemoveOperation inboundRemoveOperation = new RemoveOperation("notifyServiceInboundPatterns[0]"); + ops.clear(); + ops.add(inboundRemoveOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(1))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", hasItem( + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternsRemoveOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperation = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + ops.add(inboundAddOperation); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(1))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false) + )) + ))); + + // index out of the range + RemoveOperation inboundRemoveOperation = new RemoveOperation("notifyServiceInboundPatterns[1]"); + ops.clear(); + ops.add(inboundRemoveOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternConstraintAddOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":null,\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", null, false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + AddOperation inboundAddOperation = new AddOperation("notifyServiceInboundPatterns[0]/constraint", + "itemFilterA"); + ops.clear(); + ops.add(inboundAddOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternConstraintAddOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + AddOperation inboundAddOperation = new AddOperation("notifyServiceInboundPatterns[0]/constraint", + "itemFilterA"); + ops.clear(); + ops.add(inboundAddOperation); + patchBody = getPatchContent(ops); + + // constraint at index 0 already has value + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternConstraintReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[0]/constraint", + "itemFilterC"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterC", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternConstraintReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":null,\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", null, false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[0]/constraint", + "itemFilterA"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + // constraint at index 0 is null + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternConstraintRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + RemoveOperation inboundRemoveOperation = new RemoveOperation("notifyServiceInboundPatterns[1]/constraint"); + ops.clear(); + ops.add(inboundRemoveOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", null, true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternConstraintRemoveOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperation = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + ops.add(inboundAddOperation); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(1))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false) + )) + ))); + + // index out of the range + RemoveOperation inboundRemoveOperation = new RemoveOperation("notifyServiceInboundPatterns[1]/constraint"); + ops.clear(); + ops.add(inboundRemoveOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternPatternAddOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":null,\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern(null, "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + AddOperation inboundAddOperation = new AddOperation("notifyServiceInboundPatterns[0]/pattern", + "patternA"); + ops.clear(); + ops.add(inboundAddOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternPatternAddOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + AddOperation inboundAddOperation = new AddOperation("notifyServiceInboundPatterns[0]/pattern", + "patternA"); + ops.clear(); + ops.add(inboundAddOperation); + patchBody = getPatchContent(ops); + + // pattern at index 0 already has value + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternPatternReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[0]/pattern", + "patternC"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternC", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternPatternReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":null,\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern(null, "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[0]/pattern", + "patternA"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + // pattern at index 0 is null + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternAutomaticReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[0]/automatic", + "true"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", true), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternAutomaticReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[0]/automatic", + "test"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + // patch not boolean value + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternsReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns", + "[{\"pattern\":\"patternC\",\"constraint\":\"itemFilterC\",\"automatic\":\"true\"}," + + "{\"pattern\":\"patternD\",\"constraint\":\"itemFilterD\",\"automatic\":\"true\"}]"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternC", "itemFilterC", true), + matchNotifyServicePattern("patternD", "itemFilterD", true) + )) + ))); + } + + @Test + public void NotifyServiceInboundPatternsReplaceWithEmptyArrayOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + // empty array will only remove all old patterns + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns", "[]"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", empty())); + } + + @Test + public void NotifyServiceInboundPatternsReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + // value must be an array not object + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void NotifyServiceInboundPatternsRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", containsInAnyOrder( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + RemoveOperation inboundRemoveOperation = new RemoveOperation("notifyServiceInboundPatterns"); + ops.clear(); + ops.add(inboundRemoveOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", empty())); + } + + @Test + public void NotifyServiceInboundPatternReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation inboundAddOperationOne = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternA\",\"constraint\":\"itemFilterA\",\"automatic\":\"false\"}"); + + AddOperation inboundAddOperationTwo = new AddOperation("notifyServiceInboundPatterns/-", + "{\"pattern\":\"patternB\",\"constraint\":\"itemFilterB\",\"automatic\":\"true\"}"); + + ops.add(inboundAddOperationOne); + ops.add(inboundAddOperationTwo); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternB", "itemFilterB", true) + )) + ))); + + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("notifyServiceInboundPatterns[1]", + "{\"pattern\":\"patternC\",\"constraint\":\"itemFilterC\",\"automatic\":\"false\"}"); + ops.clear(); + ops.add(inboundReplaceOperation); + patchBody = getPatchContent(ops); + + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", hasSize(2))) + .andExpect(jsonPath("$", + allOf( + matchNotifyService(notifyServiceEntity.getID(), "service name", "service description", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + hasJsonPath("$.notifyServiceInboundPatterns", contains( + matchNotifyServicePattern("patternA", "itemFilterA", false), + matchNotifyServicePattern("patternC", "itemFilterC", false) + )) + ))); + } + + @Test + public void findManualServicesByInboundPatternUnAuthorizedTest() throws Exception { + getClient().perform(get("/api/ldn/ldnservices/search/byInboundPattern") + .param("pattern", "pattern")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findManualServicesByInboundPatternBadRequestTest() throws Exception { + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/ldn/ldnservices/search/byInboundPattern")) + .andExpect(status().isBadRequest()); + } + + @Test + public void findManualServicesByInboundPatternTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntityOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withDescription("service description one") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceEntityTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name two") + .withDescription("service description two") + .withUrl("https://service2.ldn.org/about") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceEntityThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name three") + .withDescription("service description") + .withUrl("https://service3.ldn.org/about") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceEntityOne) + .withPattern("review") + .withConstraint("itemFilterA") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceEntityOne) + .withPattern("review") + .withConstraint("itemFilterB") + .isAutomatic(true) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceEntityTwo) + .withPattern("review") + .withConstraint("itemFilterA") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceEntityTwo) + .withPattern("review") + .withConstraint("itemFilterB") + .isAutomatic(true) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceEntityThree) + .withPattern("review") + .withConstraint("itemFilterB") + .isAutomatic(true) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/ldn/ldnservices/search/byInboundPattern") + .param("pattern", "review")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.totalElements", is(2))) + .andExpect(jsonPath("$._embedded.ldnservices", containsInAnyOrder( + matchNotifyService(notifyServiceEntityOne.getID(), "service name one", "service description one", + "https://service.ldn.org/about", "https://service.ldn.org/inbox"), + matchNotifyService(notifyServiceEntityTwo.getID(), "service name two", "service description two", + "https://service2.ldn.org/about", "https://service2.ldn.org/inbox") + ))); + } + + @Test + public void NotifyServiceStatusReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .isEnabled(true) + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/enabled", "false"); + ops.add(inboundReplaceOperation); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.notifyServiceInboundPatterns", empty())) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", false))); + } + + @Test + public void NotifyServiceScoreReplaceOperationTest() throws Exception { + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withScore(BigDecimal.ZERO) + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/score", "0.522"); + ops.add(inboundReplaceOperation); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", false))) + .andExpect(jsonPath("$.score", notNullValue())) + .andExpect(jsonPath("$.score", closeTo(0.522d, 0.001d))); + } + + @Test + public void NotifyServiceScoreReplaceOperationTestUnprocessableTest() throws Exception { + + context.turnOffAuthorisationSystem(); + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("service ldn url") + .withScore(BigDecimal.ZERO) + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/score", "10"); + ops.add(inboundReplaceOperation); + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + + @Test + public void notifyServiceScoreAddOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("service url") + .withLdnUrl("service ldn url") + .isEnabled(false) + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + AddOperation operation = new AddOperation("/score", "1"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "service url", "service ldn url", false))) + .andExpect(jsonPath("$.score", notNullValue())) + .andExpect(jsonPath("$.score", closeTo(1d, 0.001d))) + ; + } + + @Override + @After + public void destroy() throws Exception { + List notifyServiceEntities = notifyService.findAll(context); + if (CollectionUtils.isNotEmpty(notifyServiceEntities)) { + notifyServiceEntities.forEach(notifyServiceEntity -> { + try { + notifyService.delete(context, notifyServiceEntity); + } catch (SQLException e) { + throw new RuntimeException(e); + } + }); + } + + super.destroy(); + } + + @Test + public void notifyServiceLowerIpReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/lowerIp", "192.168.0.1"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceLowerIpReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/lowerIp", "192.168.0.2"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", + false, "192.168.0.2", "192.168.0.5")) + ); + } + + @Test + public void notifyServiceLowerIpRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/lowerIp"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + public void notifyServiceUpperIpReplaceOperationBadRequestTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withUrl("https://service.ldn.org/about") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/lowerIp", "192.168.0.8"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isBadRequest()); + } + + @Test + public void notifyServiceUpperIpReplaceOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + ReplaceOperation operation = new ReplaceOperation("/upperIp", "192.168.0.8"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", matchNotifyService(notifyServiceEntity.getID(), "service name", + "service description", "https://service.ldn.org/about", "https://service.ldn.org/inbox", + false, "192.168.0.1", "192.168.0.8")) + ); + } + + @Test + public void notifyServiceUpperIpRemoveOperationTest() throws Exception { + + context.turnOffAuthorisationSystem(); + + NotifyServiceEntity notifyServiceEntity = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://service.ldn.org/about") + .withLdnUrl("https://service.ldn.org/inbox") + .withLowerIp("192.168.0.1") + .withUpperIp("192.168.0.5") + .build(); + context.restoreAuthSystemState(); + + List ops = new ArrayList(); + RemoveOperation operation = new RemoveOperation("/upperIp"); + ops.add(operation); + + String patchBody = getPatchContent(ops); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + } + +} \ No newline at end of file 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 index 293152dbe88e..5cecff6ef493 100644 --- 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 @@ -10,7 +10,7 @@ import static com.jayway.jsonpath.JsonPath.read; 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.dspace.content.QAEvent.COAR_NOTIFY_SOURCE; import static org.dspace.content.QAEvent.DSPACE_USERS_SOURCE; import static org.dspace.content.QAEvent.OPENAIRE_SOURCE; import static org.dspace.correctiontype.WithdrawnCorrectionType.WITHDRAWAL_REINSTATE_GROUP; @@ -28,6 +28,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -35,10 +36,13 @@ import javax.ws.rs.core.MediaType; import com.fasterxml.jackson.databind.ObjectMapper; +import org.dspace.app.ldn.NotifyServiceEntity; import org.dspace.app.rest.matcher.ItemMatcher; +import org.dspace.app.rest.matcher.MetadataMatcher; 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.repository.QAEventRestRepository; import org.dspace.app.rest.test.AbstractControllerIntegrationTest; import org.dspace.builder.CollectionBuilder; import org.dspace.builder.CommunityBuilder; @@ -46,6 +50,7 @@ import org.dspace.builder.EntityTypeBuilder; import org.dspace.builder.GroupBuilder; import org.dspace.builder.ItemBuilder; +import org.dspace.builder.NotifyServiceBuilder; import org.dspace.builder.QAEventBuilder; import org.dspace.builder.RelationshipTypeBuilder; import org.dspace.content.Collection; @@ -53,9 +58,11 @@ import org.dspace.content.Item; import org.dspace.content.QAEvent; import org.dspace.content.QAEventProcessed; +import org.dspace.content.service.ItemService; import org.dspace.eperson.EPerson; import org.dspace.eperson.Group; import org.dspace.qaevent.QANotifyPatterns; +import org.dspace.qaevent.action.ASimpleMetadataAction; import org.dspace.qaevent.dao.QAEventsDAO; import org.dspace.qaevent.service.dto.CorrectionTypeMessageDTO; import org.dspace.services.ConfigurationService; @@ -76,6 +83,15 @@ public class QAEventRestRepositoryIT extends AbstractControllerIntegrationTest { @Autowired private ConfigurationService configurationService; + @Autowired + private ItemService itemService; + + @Autowired + private ASimpleMetadataAction AddReviewMetadataAction; + + @Autowired + private ASimpleMetadataAction AddEndorsedMetadataAction; + @Test public void findAllNotImplementedTest() throws Exception { String adminToken = getAuthToken(admin.getEmail(), password); @@ -99,19 +115,34 @@ public void findOneTest() throws Exception { 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") + .withTopic(QANotifyPatterns.TOPIC_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") + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic(org.dspace.qaevent.QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT) + .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); + EPerson anotherSubmitter = EPersonBuilder.createEPerson(context).withEmail("another-submitter@example.com") + .withPassword(password).build(); + context.setCurrentUser(anotherSubmitter); + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withSource(COAR_NOTIFY_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) .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())) + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event2.getEventId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event2))); + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event3.getEventId())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event3))); + authToken = getAuthToken(anotherSubmitter.getEmail(), password); + // eperson should be see the coar-notify event related to the item that it has submitted + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event3.getEventId())) .andExpect(status().isOk()) - .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event4))); + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event3))); } @Test @@ -125,12 +156,11 @@ public void findOneWithProjectionTest() throws Exception { .build(); QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PROJECT) .withMessage( "{\"projects[0].acronym\":\"PAThs\"," + "\"projects[0].code\":\"687567\"," @@ -167,7 +197,7 @@ public void findOneUnauthorizedTest() throws Exception { .withName("Collection 1") .build(); QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}") .build(); context.restoreAuthSystemState(); @@ -182,12 +212,139 @@ public void findOneForbiddenTest() throws Exception { 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") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + EPerson anotherSubmitter = EPersonBuilder.createEPerson(context).withEmail("another_submitter@example.com") + .build(); + context.setCurrentUser(anotherSubmitter); + QAEvent event2 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withSource(COAR_NOTIFY_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"href\":\"https://doi.org/10.2307/2144300\"}").build(); context.restoreAuthSystemState(); String authToken = getAuthToken(eperson.getEmail(), password); getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event1.getEventId())) .andExpect(status().isForbidden()); + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event2.getEventId())) + .andExpect(status().isForbidden()); + } + + @Test + public void findByTopicAndTargetTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + String uuid = UUID.randomUUID().toString(); + Item item = ItemBuilder.createItem(context, col1).withTitle("Tracking Papyrus and Parchment Paths") + .build(); + QAEvent event1 = QAEventBuilder.createTarget(context, item) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}") + .build(); + QAEvent event2 = QAEventBuilder.createTarget(context, item) + .withSource(COAR_NOTIFY_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"href\":\"https://doi.org/10.2307/2144301\"}").build(); + EPerson anotherSubmitter = EPersonBuilder.createEPerson(context).withEmail("another-submitter@example.com") + .withPassword(password).build(); + context.setCurrentUser(anotherSubmitter); + // this event is related to a new item not submitted by eperson + QAEvent event3 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withSource(COAR_NOTIFY_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"href\":\"https://doi.org/10.2307/2144300\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID:" + uuid.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + uuid = item.getID().toString(); + // check for an existing item but a different topic + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":not-existing:" + uuid.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + // check for an existing topic but a different source + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.COAR_NOTIFY_SOURCE + ":ENRICH!MISSING!PID")) + .andExpect(status().isOk()).andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + + // check for an existing item and topic + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID:" + uuid.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.contains(QAEventMatcher.matchQAEventEntry(event1)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + // use the coar-notify source that has a custom security + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.COAR_NOTIFY_SOURCE + ":ENRICH!MORE!REVIEW:" + uuid.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.contains(QAEventMatcher.matchQAEventEntry(event2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + // check for an existing topic + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.contains(QAEventMatcher.matchQAEventEntry(event1)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + // use the coar-notify source that has a custom security + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.COAR_NOTIFY_SOURCE + ":ENRICH!MORE!REVIEW")) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(2))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.containsInAnyOrder( + QAEventMatcher.matchQAEventEntry(event2), + QAEventMatcher.matchQAEventEntry(event3)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + // check results for eperson + authToken = getAuthToken(eperson.getEmail(), password); + // check for an item that was submitted by eperson but in a qasource restricted to admins + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID:" + uuid.toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + // use the coar-notify source that has a custom security, only 1 event is related to the item submitted by + // eperson + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.COAR_NOTIFY_SOURCE + ":ENRICH!MORE!REVIEW:" + uuid.toString())) + .andExpect(status().isOk()).andExpect(jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(1))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.contains(QAEventMatcher.matchQAEventEntry(event2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + // check for an existing topic + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } @Test @@ -210,30 +367,30 @@ public void findByTopicTest() throws Exception { context.restoreAuthSystemState(); String authToken = getAuthToken(admin.getEmail(), password); getClient(authToken).perform(get("/api/integration/qualityassuranceevents/search/findByTopic") - .param("topic", OPENAIRE_SOURCE + ":" + "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))); + .param("topic", OPENAIRE_SOURCE + ":" + "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", OPENAIRE_SOURCE + ":" + "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))); + .param("topic", OPENAIRE_SOURCE + ":" + "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))); + .param("topic", "not-existing")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); } @Test @@ -242,124 +399,188 @@ public void findByTopicPaginatedTest() throws Exception { 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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"2144304\"}").build(); + QAEvent event6 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") + .withSource(OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEvent event7 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withSource(OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEvent event8 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withSource(OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEvent event9 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withSource(OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"pmc\",\"pids[0].value\":\"2144303\"}").build(); + QAEvent event10 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withSource(OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"2144304\"}").build(); + context.setCurrentUser(admin); + // this event will be related to an item submitted by the admin + QAEvent event11 = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") + .withSource(OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_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", OPENAIRE_SOURCE + ":" + "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( + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":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=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.next.href", Matchers.allOf( + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("page=1"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), - Matchers.containsString("page=2"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), + Matchers.containsString("page=5"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":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))); + .andExpect(jsonPath("$._links.prev.href").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(6))) + .andExpect(jsonPath("$.page.totalElements", is(11))); - getClient(authToken).perform(get("/api/integration/qualityassuranceevents/search/findByTopic") - .param("topic", OPENAIRE_SOURCE + ":" + "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( + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":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=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("page=1"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.next.href", Matchers.allOf( + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("page=2"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.last.href",Matchers.allOf( + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), - Matchers.containsString("page=2"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), + Matchers.containsString("page=5"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("page=0"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":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))); + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(6))) + .andExpect(jsonPath("$.page.totalElements", is(11))); - getClient(authToken).perform(get("/api/integration/qualityassuranceevents/search/findByTopic") - .param("topic", OPENAIRE_SOURCE + ":" + "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( + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID") + .param("size", "2").param("page", "2")) + .andExpect(status().isOk()).andExpect( + jsonPath("$._embedded.qualityassuranceevents", Matchers.hasSize(2))) + .andExpect(jsonPath("$._embedded.qualityassuranceevents", + Matchers.hasItem( + QAEventMatcher.matchQAEventEntry(event5)))) + .andExpect(jsonPath("$._links.self.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("page=2"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.next.href").doesNotExist()) - .andExpect(jsonPath("$._links.last.href", Matchers.allOf( + .andExpect(jsonPath("$._links.next.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), - Matchers.containsString("page=2"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), + Matchers.containsString("page=3"), + Matchers.containsString("size=2")))) + .andExpect(jsonPath("$._links.last.href", + Matchers.allOf( + Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), + Matchers.containsString("page=5"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.first.href", Matchers.allOf( + .andExpect(jsonPath("$._links.first.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID"), Matchers.containsString("page=0"), Matchers.containsString("size=2")))) - .andExpect(jsonPath("$._links.prev.href", Matchers.allOf( + .andExpect(jsonPath("$._links.prev.href", + Matchers.allOf( Matchers.containsString("/api/integration/qualityassuranceevents/search/findByTopic?"), - Matchers.containsString("topic=" + OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID"), + Matchers.containsString("topic=" + QAEvent.OPENAIRE_SOURCE + ":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))); + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(6))) + .andExpect(jsonPath("$.page.totalElements", is(11))); + + // check if the pagination is working properly also when a security filter is in place + authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken) + .perform( + get("/api/integration/qualityassuranceevents/search/findByTopic") + .param("topic", QAEvent.OPENAIRE_SOURCE + ":ENRICH!MISSING!PID") + .param("size", "2")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasNoJsonPath("$._embedded.qualityassuranceevents"))) + .andExpect(jsonPath("$.page.size", is(2))) + .andExpect(jsonPath("$.page.totalPages", is(0))) + .andExpect(jsonPath("$.page.totalElements", is(0))); } @Test @@ -368,19 +589,18 @@ public void findByTopicUnauthorizedTest() throws Exception { 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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(org.dspace.qaevent.QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT) .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); context.restoreAuthSystemState(); - getClient().perform(get("/api/integration/qualityassuranceevents/search/findByTopic") .param("topic", OPENAIRE_SOURCE + ":" + "ENRICH!MISSING!PID")) .andExpect(status().isUnauthorized()); @@ -392,16 +612,16 @@ public void findByTopicBadRequestTest() throws Exception { 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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(org.dspace.qaevent.QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT) .withMessage("{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}").build(); context.restoreAuthSystemState(); String adminToken = getAuthToken(admin.getEmail(), password); @@ -628,6 +848,91 @@ public void recordDecisionTest() throws Exception { .andExpect(jsonPath("$.totalEvents", is(0))); } + @Test + public void recordDecisionNotifyTest() 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 item = ItemBuilder.createItem(context, colFunding).withTitle("Tracking Papyrus and Parchment Paths") + .build(); + NotifyServiceEntity notifyServiceEntity = NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name") + .withDescription("service description") + .withUrl("https://review-service.com/inbox/about/") + .withLdnUrl("https://review-service.com/inbox/") + .withScore(BigDecimal.valueOf(0.6d)) + .build(); + String href = "EC"; + QAEvent eventMoreReview = QAEventBuilder.createTarget(context, col1, "Science and Freedom with project") + .withSource(COAR_NOTIFY_SOURCE) + .withTopic("ENRICH/MORE/REVIEW") + .withMessage( + "{" + + "\"serviceName\":\"" + notifyServiceEntity.getName() + "\"," + + "\"serviceId\":\"" + notifyServiceEntity.getID() + "\"," + + "\"href\":\"" + href + "\"," + + "\"relationship\":\"H2020\"" + + "}") + .withRelatedItem(item.getID().toString()) + .build(); + QAEvent eventMoreEndorsement = QAEventBuilder.createTarget(context, col1, "Science and Freedom with project") + .withSource(COAR_NOTIFY_SOURCE) + .withTopic("ENRICH/MORE/ENDORSEMENT") + .withMessage( + "{" + + "\"serviceName\":\"" + notifyServiceEntity.getName() + "\"," + + "\"serviceId\":\"" + notifyServiceEntity.getID() + "\"," + + "\"href\":\"" + href + "\"," + + "\"relationship\":\"H2020\"" + + "}") + .withRelatedItem(item.getID().toString()) + .build(); + context.restoreAuthSystemState(); + List acceptOp = new ArrayList(); + acceptOp.add(new ReplaceOperation("/status", QAEvent.ACCEPTED)); + String patchAccept = getPatchContent(acceptOp); + String authToken = getAuthToken(admin.getEmail(), password); + eventMoreEndorsement.setStatus(QAEvent.ACCEPTED); + eventMoreReview.setStatus(QAEvent.ACCEPTED); + // MORE REVIEW + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + eventMoreReview.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventNotifyEntry(eventMoreReview))); + getClient(authToken).perform(get("/api/core/items/" + eventMoreReview.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasJsonPath("$.metadata", + MetadataMatcher.matchMetadata(AddReviewMetadataAction.getMetadata(), href)))); + // MORE ENDORSEMENT + getClient(authToken).perform(patch("/api/integration/qualityassuranceevents/" + + eventMoreEndorsement.getEventId()) + .content(patchAccept) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventNotifyEntry(eventMoreEndorsement))); + + getClient(authToken).perform(get("/api/core/items/" + eventMoreEndorsement.getTarget()) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", + hasJsonPath("$.metadata", + MetadataMatcher.matchMetadata(AddEndorsedMetadataAction.getMetadata(), href)))); + + } + @Test public void setRelatedTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -636,7 +941,7 @@ public void setRelatedTest() throws Exception { Collection colFunding = CollectionBuilder.createCollection(context, parentCommunity) .withName("Collection Fundings").build(); QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom 5") - .withTopic("ENRICH/MISSING/PROJECT") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PROJECT) .withMessage( "{\"projects[0].acronym\":\"PAThs\"," + "\"projects[0].code\":\"687567\"," @@ -685,7 +990,7 @@ public void unsetRelatedTest() throws Exception { 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") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PROJECT) .withMessage( "{\"projects[0].acronym\":\"PAThs\"," + "\"projects[0].code\":\"687567\"," @@ -728,7 +1033,7 @@ public void setInvalidRelatedTest() throws Exception { Collection colFunding = CollectionBuilder.createCollection(context, parentCommunity) .withName("Collection Fundings").build(); QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID") + .withTopic(QANotifyPatterns.TOPIC_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(); @@ -760,11 +1065,11 @@ public void deleteItemWithEventTest() throws Exception { .withName("Collection 1") .build(); QAEvent event1 = QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID") + .withTopic(QANotifyPatterns.TOPIC_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") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}") .build(); context.restoreAuthSystemState(); @@ -812,7 +1117,7 @@ public void testEventDeletion() throws Exception { .build(); QAEvent event = QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}") .build(); @@ -827,7 +1132,7 @@ public void testEventDeletion() throws Exception { getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId())) .andExpect(status().isOk()) - .andExpect(jsonPath("$", matchQAEventEntry(event))); + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventEntry(event))); List processedEvents = qaEventsDao.findAll(context); assertThat(processedEvents, empty()); @@ -867,6 +1172,115 @@ public void createQAEventByCorrectionTypeWithMissingTargetTest() throws Exceptio .andExpect(status().isUnprocessableEntity()); } + @Test + public void createQAEventsAndIgnoreAutomaticallyByScoreAndFilterTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item = ItemBuilder.createItem(context, col1).withTitle("demo").build(); + + QAEvent event = + QAEventBuilder.createTarget(context, item) + .withSource(COAR_NOTIFY_SOURCE) + .withTrust(0.4) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"abstracts[0]\": \"https://doi.org/10.3214/987654\"}") + .build(); + + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/core/items/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata['datacite.relation.isReviewedBy']").doesNotExist()); + + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId())) + .andExpect(status().isNotFound()); + } + + @Test + public void createQAEventsAndRejectAutomaticallyByScoreAndFilterTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item = ItemBuilder.createItem(context, col1).withTitle("demo").build(); + + QAEvent event = + QAEventBuilder.createTarget(context, item) + .withSource(COAR_NOTIFY_SOURCE) + .withTrust(0.3) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"abstracts[0]\": \"https://doi.org/10.3214/987654\"}") + .build(); + + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/core/items/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata['datacite.relation.isReviewedBy']").doesNotExist()); + + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId())) + .andExpect(status().isNotFound()); + } + + @Test + public void createQAEventsAndDoNothingScoreNotInRangTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item = ItemBuilder.createItem(context, col1).withTitle("demo").build(); + + QAEvent event = + QAEventBuilder.createTarget(context, item) + .withSource(COAR_NOTIFY_SOURCE) + .withTrust(0.7) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"abstracts[0]\": \"https://doi.org/10.3214/987654\"}") + .build(); + + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/core/items/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata['datacite.relation.isReviewedBy']").doesNotExist()); + + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + } + + @Test + public void createQAEventsAndDoNothingFilterNotCompatibleWithItemTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context).withName("Parent Community").build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item = ItemBuilder.createItem(context, col1).withTitle("item title").build(); + + QAEvent event = + QAEventBuilder.createTarget(context, item) + .withSource(COAR_NOTIFY_SOURCE) + .withTrust(0.8) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_REVIEW) + .withMessage("{\"abstracts[0]\": \"https://doi.org/10.3214/987654\"}") + .build(); + + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + + getClient(authToken).perform(get("/api/core/items/" + item.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.metadata['datacite.relation.isReviewedBy']").doesNotExist()); + + getClient(authToken).perform(get("/api/integration/qualityassuranceevents/" + event.getEventId()) + .param("projection", "full")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", QAEventMatcher.matchQAEventFullEntry(event))); + } + + @Test public void createQAEventByCorrectionTypeWithdrawnRequestTest() throws Exception { context.turnOffAuthorisationSystem(); 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 index 31a2c618064a..71b87d460a49 100644 --- 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 @@ -16,12 +16,14 @@ 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.repository.QASourceRestRepository; 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.Community; import org.dspace.content.Item; import org.dspace.content.QAEvent; import org.dspace.services.ConfigurationService; @@ -60,47 +62,56 @@ public void setup() { context.restoreAuthSystemState(); - configurationService.setProperty(QAEVENTS_SOURCES, new String[] { "openaire","test-source","test-source-2" }); + configurationService.setProperty(QAEVENTS_SOURCES, + new String[] { QAEvent.OPENAIRE_SOURCE,"coar-notify", "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(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", "Title 1"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", "Title 2"); context.setCurrentUser(eperson); - createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 3"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", "Title 3"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", "Title 4"); - createEvent("test-source", "TOPIC/TEST/1", "Title 4"); createEvent("test-source", "TOPIC/TEST/1", "Title 5"); - + createEvent("test-source", "TOPIC/TEST/1", "Title 6"); + createEvent("coar-notify", "TOPIC", "Title 7"); + context.setCurrentUser(eperson); + createEvent("coar-notify", "TOPIC", "Title 8"); + createEvent("coar-notify", "TOPIC", "Title 9"); context.setCurrentUser(null); context.restoreAuthSystemState(); - String ePersonToken = getAuthToken(eperson.getEmail(), password); - getClient(ePersonToken).perform(get("/api/integration/qualityassurancesources")) - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$.page.size", is(20))) - .andExpect(jsonPath("$.page.totalElements", is(0))); - - String adminToken = getAuthToken(admin.getEmail(), password); - getClient(adminToken).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))); + 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(QAEvent.OPENAIRE_SOURCE, 4), + matchQASourceEntry("coar-notify", 3), + matchQASourceEntry("test-source", 2), + matchQASourceEntry("test-source-2", 0)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(4))); + + // check with our eperson submitter + authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancesources")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancesources", contains( + matchQASourceEntry("coar-notify", 3)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); } @Test public void testFindAllUnauthorized() throws Exception { context.turnOffAuthorisationSystem(); - createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", "Title 1"); createEvent("test-source", "TOPIC/TEST/1", "Title 4"); context.restoreAuthSystemState(); @@ -113,21 +124,35 @@ public void testFindAllUnauthorized() throws Exception { public void testFindOne() throws Exception { context.turnOffAuthorisationSystem(); + Community com = CommunityBuilder.createCommunity(context).withName("Test community").build(); + Collection col = CollectionBuilder.createCollection(context, com).withName("Test collection").build(); - createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); - createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 2"); - createEvent("openaire", "TOPIC/OPENAIRE/2", "Title 3"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", "Title 1"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", "Title 2"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", "Title 3"); createEvent("test-source", "TOPIC/TEST/1", "Title 4"); createEvent("test-source", "TOPIC/TEST/1", "Title 5"); - + context.setCurrentUser(admin); + Item target1 = ItemBuilder.createItem(context, col).withTitle("Title 7").build(); + createEvent(QAEvent.COAR_NOTIFY_SOURCE, "TOPIC", target1); + context.setCurrentUser(eperson); + createEvent(QAEvent.COAR_NOTIFY_SOURCE, "TOPIC", "Title 8"); + createEvent(QAEvent.COAR_NOTIFY_SOURCE, "TOPIC", "Title 9"); + context.setCurrentUser(null); 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))); + .andExpect(jsonPath("$", matchQASourceEntry(QAEvent.OPENAIRE_SOURCE, 3))); + + getClient(authToken).perform(get("/api/integration/qualityassurancesources/coar-notify")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchQASourceEntry("coar-notify", 3))); + getClient(authToken).perform(get("/api/integration/qualityassurancesources/test-source")) .andExpect(status().isOk()) @@ -142,6 +167,18 @@ public void testFindOne() throws Exception { getClient(authToken).perform(get("/api/integration/qualityassurancesources/unknown-test-source")) .andExpect(status().isNotFound()); + authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancesources/openaire")) + .andExpect(status().isForbidden()); + getClient(authToken).perform(get("/api/integration/qualityassurancesources/unknown-test-source")) + .andExpect(status().isForbidden()); + // the eperson will see only 2 events in coar-notify as 1 is related to an item was submitted by other + getClient(authToken).perform(get("/api/integration/qualityassurancesources/coar-notify")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", matchQASourceEntry("coar-notify", 2))); + + } @Test @@ -149,7 +186,7 @@ public void testFindOneForbidden() throws Exception { context.turnOffAuthorisationSystem(); - createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", "Title 1"); createEvent("test-source", "TOPIC/TEST/1", "Title 4"); context.restoreAuthSystemState(); @@ -165,7 +202,7 @@ public void testFindOneUnauthorized() throws Exception { context.turnOffAuthorisationSystem(); - createEvent("openaire", "TOPIC/OPENAIRE/1", "Title 1"); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", "Title 1"); createEvent("test-source", "TOPIC/TEST/1", "Title 4"); context.restoreAuthSystemState(); @@ -175,6 +212,124 @@ public void testFindOneUnauthorized() throws Exception { } + @Test + public void testFindAllByTarget() throws Exception { + + context.turnOffAuthorisationSystem(); + Community com = CommunityBuilder.createCommunity(context).withName("Test community").build(); + Collection col = CollectionBuilder.createCollection(context, com).withName("Test collection").build(); + Item target1 = ItemBuilder.createItem(context, col).withTitle("Test item1").build(); + Item target2 = ItemBuilder.createItem(context, col).withTitle("Test item2").build(); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", target1); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", target1); + createEvent("test-source", "TOPIC/TEST/1", target1); + createEvent("test-source", "TOPIC/TEST/1", target2); + + context.setCurrentUser(eperson); + Item target3 = ItemBuilder.createItem(context, col).withTitle("Test item3").build(); + context.setCurrentUser(null); + createEvent(QAEvent.COAR_NOTIFY_SOURCE, "TOPIC", target3); + createEvent(QAEvent.COAR_NOTIFY_SOURCE, "TOPIC2", target3); + createEvent(QAEvent.COAR_NOTIFY_SOURCE, "TOPIC", target2); + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target1.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancesources", + contains(matchQASourceEntry(QAEvent.OPENAIRE_SOURCE + ":" + target1.getID().toString(), 2), + matchQASourceEntry("test-source:" + target1.getID().toString(), 1)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target2.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancesources", + contains( + matchQASourceEntry(QAEvent.COAR_NOTIFY_SOURCE + ":" + target2.getID().toString(), 1), + matchQASourceEntry("test-source:" + target2.getID().toString(), 1)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target3.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancesources", + contains(matchQASourceEntry("coar-notify:" + target3.getID().toString(), 2)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + + // check with our eperson submitter + authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target1.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target2.getID().toString())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target3.getID().toString())) + .andExpect(status().isOk()).andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancesources", + contains(matchQASourceEntry("coar-notify:" + target3.getID().toString(), 2)))) + .andExpect(jsonPath("$.page.size", is(20))) + .andExpect(jsonPath("$.page.totalElements", is(1))); + } + + @Test + public void testFindByTargetBadRequest() throws Exception { + + context.turnOffAuthorisationSystem(); + Community com = CommunityBuilder.createCommunity(context).withName("Test community").build(); + Collection col = CollectionBuilder.createCollection(context, com).withName("Test collection").build(); + Item target1 = ItemBuilder.createItem(context, col).withTitle("Test item1").build(); + Item target2 = ItemBuilder.createItem(context, col).withTitle("Test item2").build(); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", target1); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", target1); + createEvent("test-source", "TOPIC/TEST/1", target1); + createEvent("test-source", "TOPIC/TEST/1", target2); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken) + .perform(get("/api/integration/qualityassurancesources/search/byTarget")) + .andExpect(status().isBadRequest()); + } + + + @Test + public void testFindByTargetUnauthorized() throws Exception { + + context.turnOffAuthorisationSystem(); + Community com = CommunityBuilder.createCommunity(context).withName("Test community").build(); + Collection col = CollectionBuilder.createCollection(context, com).withName("Test collection").build(); + Item target1 = ItemBuilder.createItem(context, col).withTitle("Test item1").build(); + Item target2 = ItemBuilder.createItem(context, col).withTitle("Test item2").build(); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/1", target1); + createEvent(QAEvent.OPENAIRE_SOURCE, "TOPIC/OPENAIRE/2", target1); + createEvent("test-source", "TOPIC/TEST/1", target1); + createEvent("test-source", "TOPIC/TEST/1", target2); + + context.restoreAuthSystemState(); + + getClient() + .perform(get("/api/integration/qualityassurancesources/search/byTarget").param("target", + target1.getID().toString())) + .andExpect(status().isUnauthorized()); + } + private QAEvent createEvent(String source, String topic, String title) { return QAEventBuilder.createTarget(context, target) .withSource(source) @@ -183,4 +338,12 @@ private QAEvent createEvent(String source, String topic, String title) { .build(); } + private QAEvent createEvent(String source, String topic, Item item) { + return QAEventBuilder.createTarget(context, item) + .withSource(source) + .withTopic(topic) + .withTitle(item.getName()) + .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 index bf3f2f02ed81..7534df6c96f9 100644 --- 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 @@ -15,13 +15,18 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import java.util.UUID; + 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.ItemBuilder; import org.dspace.builder.QAEventBuilder; import org.dspace.content.Collection; +import org.dspace.content.Item; +import org.dspace.content.QAEvent; import org.dspace.qaevent.QANotifyPatterns; import org.dspace.services.ConfigurationService; import org.hamcrest.Matchers; @@ -40,7 +45,7 @@ public class QATopicRestRepositoryIT extends AbstractControllerIntegrationTest { private ConfigurationService configurationService; @Test - public void findAllTest() throws Exception { + public void findAllNotImplementedTest() throws Exception { String adminToken = getAuthToken(admin.getEmail(), password); getClient(adminToken).perform(get("/api/integration/qualityassurancetopics")) .andExpect(status().isMethodNotAllowed()); @@ -97,6 +102,32 @@ public void findOneTest() throws Exception { .andExpect(jsonPath("$",QATopicMatcher.matchQATopicEntry("test-source", "TOPIC/TEST", 1))); } + @Test + public void findOneNotFoundTest() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("qaevent.sources", + new String[] { QAEvent.OPENAIRE_SOURCE }); + 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(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + // using a wrong id + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!PID")) + .andExpect(status().isNotFound()); + // using a plausible id related to an unknown source + getClient(authToken) + .perform(get("/api/integration/qualityassurancetopics/unknown-source:ENRICH!MISSING!ABSTRACT")) + .andExpect(status().isNotFound()); + // using a not existing topic + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/openaire:not-existing-topic")) + .andExpect(status().isNotFound()); + } + @Test public void findOneUnauthorizedTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -105,11 +136,11 @@ public void findOneUnauthorizedTest() throws Exception { .build(); Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID").build(); + .withTopic(QANotifyPatterns.TOPIC_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")) + getClient().perform(get("/api/integration/qualityassurancetopics/openaire:ENRICH!MISSING!PID")) + .andExpect(status().isUnauthorized()); + getClient().perform(get("/api/integration/qualityassurancetopics/openaire:ENRICH!MISSING!ABSTRACT")) .andExpect(status().isUnauthorized()); } @@ -121,12 +152,12 @@ public void findOneForbiddenTest() throws Exception { .build(); Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID").build(); + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID).build(); context.restoreAuthSystemState(); String authToken = getAuthToken(eperson.getEmail(), password); - getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!PID")) + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/openaire:ENRICH!MISSING!PID")) .andExpect(status().isForbidden()); - getClient(authToken).perform(get("/api/integration/qualityassurancetopics/ENRICH!MISSING!ABSTRACT")) + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/openaire:ENRICH!MISSING!ABSTRACT")) .andExpect(status().isForbidden()); } @@ -177,16 +208,16 @@ public void findBySourceTest() throws Exception { String authToken = getAuthToken(admin.getEmail(), password); getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") .param("source", OPENAIRE_SOURCE)) - .andExpect(status().isOk()) - .andExpect(content().contentType(contentType)) - .andExpect(jsonPath("$._embedded.qualityassurancetopics", Matchers.containsInAnyOrder( - QATopicMatcher.matchQATopicEntry(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID, 2), + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", + Matchers.containsInAnyOrder( + QATopicMatcher.matchQATopicEntry(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID, 2), QATopicMatcher.matchQATopicEntry(QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, 1), QATopicMatcher.matchQATopicEntry(QANotifyPatterns.TOPIC_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()) @@ -197,7 +228,6 @@ public void findBySourceTest() throws Exception { ))) .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()) @@ -207,6 +237,73 @@ public void findBySourceTest() throws Exception { .andExpect(jsonPath("$.page.totalElements", is(0))); } + @Test + public void findBySourcePaginationTest() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("qaevent.sources", + new String[] { QAEvent.OPENAIRE_SOURCE, "test-source", "test-source-2" }); + 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(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withSource(QAEvent.OPENAIRE_SOURCE) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 2") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withSource(QAEvent.OPENAIRE_SOURCE) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 3") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MORE_PID) + .withSource(QAEvent.OPENAIRE_SOURCE) + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"10.2307/2144302\"}").build(); + QAEventBuilder.createTarget(context, col1, "Science and Freedom 4") + .withTopic(org.dspace.qaevent.QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT) + .withSource(QAEvent.OPENAIRE_SOURCE) + .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", QAEvent.OPENAIRE_SOURCE) + .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/search/bySource") + .param("source", QAEvent.OPENAIRE_SOURCE) + .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 unsupported + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", "test-source") + .param("size", "2")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(2))).andExpect(jsonPath("$.page.totalElements", is(0))); + } + @Test public void findBySourceUnauthorizedTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -217,7 +314,7 @@ public void findBySourceUnauthorizedTest() throws Exception { .withName("Collection 1") .build(); QAEventBuilder.createTarget(context, col1, "Science and Freedom") - .withTopic("ENRICH/MISSING/PID") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) .build(); context.restoreAuthSystemState(); @@ -226,4 +323,155 @@ public void findBySourceUnauthorizedTest() throws Exception { .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(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID).build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/bySource") + .param("source", QAEvent.OPENAIRE_SOURCE)) + .andExpect(status().isForbidden()); + } + + @Test + public void findByTargetTest() throws Exception { + context.turnOffAuthorisationSystem(); + configurationService.setProperty("qaevent.sources", + new String[] { QAEvent.OPENAIRE_SOURCE, "test-source", "test-source-2" }); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item1 = ItemBuilder.createItem(context, col1).withTitle("Science and Freedom").build(); + Item item2 = ItemBuilder.createItem(context, col1).withTitle("Science and Freedom 2").build(); + QAEventBuilder.createTarget(context, item1) + .withSource(QAEvent.OPENAIRE_SOURCE) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144300\"}").build(); + QAEventBuilder.createTarget(context, item1) + .withSource(QAEvent.OPENAIRE_SOURCE) + .withTopic(org.dspace.qaevent.QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT) + .withMessage( + "{\"abstracts[0]\": \"Descrizione delle caratteristiche...\"}") + .build(); + QAEventBuilder.createTarget(context, item1) + .withTopic("TEST/TOPIC") + .withSource("test-source") + .build(); + QAEventBuilder.createTarget(context, item1) + .withTopic("TEST/TOPIC/2") + .withSource("test-source") + .build(); + QAEventBuilder.createTarget(context, item2) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"doi\",\"pids[0].value\":\"10.2307/2144301\"}").build(); + QAEventBuilder.createTarget(context, item2) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID) + .withMessage("{\"pids[0].type\":\"pmid\",\"pids[0].value\":\"2144301\"}").build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(admin.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("target", item1.getID().toString()) + .param("source", QAEvent.OPENAIRE_SOURCE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", + Matchers.containsInAnyOrder( + QATopicMatcher.matchQATopicEntry(QAEvent.OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MISSING_PID, + item1.getID().toString(), 1), + QATopicMatcher.matchQATopicEntry(QAEvent.OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MISSING_ABSTRACT, + item1.getID().toString(), 1)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(2))); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("target", item2.getID().toString()) + .param("source", QAEvent.OPENAIRE_SOURCE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics", + Matchers.containsInAnyOrder( + QATopicMatcher.matchQATopicEntry(QAEvent.OPENAIRE_SOURCE, + QANotifyPatterns.TOPIC_ENRICH_MISSING_PID, + item2.getID().toString(), 2)))) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(1))); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("target", UUID.randomUUID().toString()) + .param("source", QAEvent.OPENAIRE_SOURCE)) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.qualityassurancetopics").doesNotExist()) + .andExpect(jsonPath("$.page.size", is(20))).andExpect(jsonPath("$.page.totalElements", is(0))); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("target", item2.getID().toString()) + .param("source", "test-source")) + .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 findByTargetZeroEventsOpenaireTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item1 = ItemBuilder.createItem(context, col1).withTitle("Science and Freedom").build(); + QAEventBuilder.createTarget(context, item1) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID).build(); + context.restoreAuthSystemState(); + getClient().perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("source", QAEvent.OPENAIRE_SOURCE) + .param("target", item1.getID().toString())) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findByTargetZeroEventsAnotherSourceTest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item1 = ItemBuilder.createItem(context, col1).withTitle("Science and Freedom").build(); + QAEventBuilder.createTarget(context, item1) + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID).build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("target", item1.getID().toString()) + .param("source", "test-source")) + .andExpect(jsonPath("$.page.totalElements", is(0))); + } + + @Test + public void findByTargetBadRequest() throws Exception { + context.turnOffAuthorisationSystem(); + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, parentCommunity).withName("Collection 1").build(); + Item item1 = ItemBuilder.createItem(context, col1).withTitle("Science and Freedom").build(); + QAEventBuilder.createTarget(context, item1) + .withSource("test-source") + .withTopic(QANotifyPatterns.TOPIC_ENRICH_MISSING_PID).build(); + context.restoreAuthSystemState(); + String authToken = getAuthToken(eperson.getEmail(), password); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("source", "test-source")) + .andExpect(status().isBadRequest()); + getClient(authToken).perform(get("/api/integration/qualityassurancetopics/search/byTarget") + .param("target", item1.getID().toString())) + .andExpect(status().isBadRequest()); + } + } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCOARNotifyRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCOARNotifyRestRepositoryIT.java new file mode 100644 index 000000000000..b2d4a0660633 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionCOARNotifyRestRepositoryIT.java @@ -0,0 +1,81 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.app.rest; + +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 java.util.List; + +import org.dspace.app.rest.matcher.SubmissionCOARNotifyMatcher; +import org.dspace.app.rest.repository.SubmissionCoarNotifyRestRepository; +import org.dspace.app.rest.test.AbstractControllerIntegrationTest; +import org.dspace.coarnotify.NotifyPattern; +import org.hamcrest.Matchers; +import org.junit.Test; + +/** + * Integration test class for {@link SubmissionCoarNotifyRestRepository}. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class SubmissionCOARNotifyRestRepositoryIT extends AbstractControllerIntegrationTest { + + @Test + public void findAllTestUnAuthorized() throws Exception { + getClient().perform(get("/api/config/submissioncoarnotifyconfigs")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findAllTest() throws Exception { + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get("/api/config/submissioncoarnotifyconfigs")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$._embedded.submissioncoarnotifyconfigs", Matchers.containsInAnyOrder( + SubmissionCOARNotifyMatcher.matchCOARNotifyEntry("coarnotify", List.of( + new NotifyPattern("request-review", true), + new NotifyPattern("request-endorsement", true), + new NotifyPattern("request-ingest", false))) + ))); + } + + @Test + public void findOneTestUnAuthorized() throws Exception { + getClient().perform(get("/api/config/submissioncoarnotifyconfigs/coarnotify")) + .andExpect(status().isUnauthorized()); + } + + @Test + public void findOneTestNonExistingCOARNotify() throws Exception { + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get("/api/config/submissioncoarnotifyconfigs/non-existing-coar")) + .andExpect(status().isNotFound()); + } + + @Test + public void findOneTest() throws Exception { + String epersonToken = getAuthToken(eperson.getEmail(), password); + + getClient(epersonToken).perform(get("/api/config/submissioncoarnotifyconfigs/coarnotify")) + .andExpect(status().isOk()) + .andExpect(content().contentType(contentType)) + .andExpect(jsonPath("$", Matchers.is( + SubmissionCOARNotifyMatcher.matchCOARNotifyEntry("coarnotify", List.of( + new NotifyPattern("request-review", true), + new NotifyPattern("request-endorsement", true), + new NotifyPattern("request-ingest", false))) + ))); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java index 1346be3fa902..3b8c87ce5f2c 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/SubmissionDefinitionsControllerIT.java @@ -206,7 +206,7 @@ public void findSections() throws Exception { // We expect the content type to be "application/hal+json;charset=UTF-8" .andExpect(content().contentType(contentType)) // Match only that a section exists with a submission configuration behind - .andExpect(jsonPath("$._embedded.submissionsections", hasSize(9))) + .andExpect(jsonPath("$._embedded.submissionsections", hasSize(10))) .andExpect(jsonPath("$._embedded.submissionsections", Matchers.hasItem( allOf( diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java index 017d13ba2868..84cf6ef0508a 100644 --- a/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/WorkspaceItemRestRepositoryIT.java @@ -52,6 +52,7 @@ import com.jayway.jsonpath.matchers.JsonPathMatchers; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.time.DateUtils; +import org.dspace.app.ldn.NotifyServiceEntity; import org.dspace.app.rest.matcher.CollectionMatcher; import org.dspace.app.rest.matcher.ItemMatcher; import org.dspace.app.rest.matcher.MetadataMatcher; @@ -70,6 +71,8 @@ import org.dspace.builder.EntityTypeBuilder; import org.dspace.builder.GroupBuilder; import org.dspace.builder.ItemBuilder; +import org.dspace.builder.NotifyServiceBuilder; +import org.dspace.builder.NotifyServiceInboundPatternBuilder; import org.dspace.builder.RelationshipBuilder; import org.dspace.builder.RelationshipTypeBuilder; import org.dspace.builder.ResourcePolicyBuilder; @@ -8603,6 +8606,640 @@ public void testSubmissionWithHiddenSections() throws Exception { .andExpect(status().isCreated()); } + + @Test + public void testSubmissionWithCOARNotifyServicesSection() throws Exception { + + context.turnOffAuthorisationSystem(); + + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + + Collection collection = CollectionBuilder.createCollection(context, parentCommunity) + .withName("Collection 1") + .build(); + + // create three notify services + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name three") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + // append the three services to the workspace item with different patterns + WorkspaceItem workspaceItem = WorkspaceItemBuilder.createWorkspaceItem(context, collection) + .withTitle("Workspace Item") + .withIssueDate("2024-10-10") + .withCOARNotifyService(notifyServiceOne, "request-review") + .withCOARNotifyService(notifyServiceTwo, "request-endorsement") + .withCOARNotifyService(notifyServiceThree, "request-endorsement") + .build(); + + context.restoreAuthSystemState(); + String adminToken = getAuthToken(admin.getEmail(), password); + + // check coarnotify section data + getClient(adminToken) + .perform(get("/api/submission/workspaceitems/" + workspaceItem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-endorsement", contains( + notifyServiceTwo.getID(), notifyServiceThree.getID()))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceOne.getID()))); + } + + @Test + public void patchCOARNotifyServiceAddTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name three") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withCOARNotifyService(notifyServiceOne, "request-review") + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceOne) + .withPattern("request-review") + .withConstraint("itemFilterA") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceTwo) + .withPattern("request-review") + .withConstraint("itemFilterA") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceThree) + .withPattern("request-review") + .withConstraint("itemFilterA") + .isAutomatic(false) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // try to add new service of review pattern to witem + List addOpts = new ArrayList(); + addOpts.add(new AddOperation("/sections/coarnotify/request-review/-", + List.of(notifyServiceTwo.getID(), notifyServiceThree.getID()))); + + String patchBody = getPatchContent(addOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(3))) + .andExpect(jsonPath("$.sections.coarnotify.request-review",contains( + notifyServiceOne.getID(), + notifyServiceTwo.getID(), + notifyServiceThree.getID() + ))); + + } + + @Test + public void patchCOARNotifyServiceAddWithInCompatibleServicesTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name three") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withCOARNotifyService(notifyServiceOne, "request-review") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(1))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceOne.getID() + ))); + + // try to add new service of review pattern to witem + List addOpts = new ArrayList(); + addOpts.add(new AddOperation("/sections/coarnotify/request-review/-", + List.of(notifyServiceTwo.getID(), notifyServiceThree.getID()))); + + String patchBody = getPatchContent(addOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + } + + @Test + public void patchCOARNotifyServiceAddWithInvalidPatternTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // try to add new service of unknown pattern to witem + List addOpts = new ArrayList(); + addOpts.add(new AddOperation("/sections/coarnotify/unknown/-", List.of(notifyServiceOne.getID()))); + + String patchBody = getPatchContent(addOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + } + + @Test + public void patchCOARNotifyServiceAddWithInvalidServiceIdTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withCOARNotifyService(notifyServiceOne, "request-review") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceOne.getID() + ))); + + // try to add new service of review pattern to witem but service not exist + List addOpts = new ArrayList(); + addOpts.add(new AddOperation("/sections/coarnotify/request-review/-", List.of("123456789"))); + + String patchBody = getPatchContent(addOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + } + + @Test + public void patchCOARNotifyServiceReplaceTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name three") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceOne) + .withPattern("request-review") + .withConstraint("itemFilterA") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceTwo) + .withPattern("request-review") + .withConstraint("demo_filter") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceThree) + .withPattern("request-review") + .withConstraint("demo_filter") + .isAutomatic(false) + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withCOARNotifyService(notifyServiceOne, "request-review") + .withCOARNotifyService(notifyServiceTwo, "request-review") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(2))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceOne.getID(), + notifyServiceTwo.getID()))); + + // try to replace the notifyServiceOne of witem with notifyServiceThree of review pattern + List replaceOpts = new ArrayList(); + replaceOpts.add(new ReplaceOperation("/sections/coarnotify/request-review/0", notifyServiceThree.getID())); + + String patchBody = getPatchContent(replaceOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(2))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceThree.getID(), notifyServiceTwo.getID() + ))); + + } + + @Test + public void patchCOARNotifyServiceReplaceWithInCompatibleServicesTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name three") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withCOARNotifyService(notifyServiceOne, "request-review") + .withCOARNotifyService(notifyServiceTwo, "request-review") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(2))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceOne.getID(), + notifyServiceTwo.getID()))); + + // try to replace the notifyServiceOne of witem with notifyServiceThree of review pattern + List replaceOpts = new ArrayList(); + replaceOpts.add(new ReplaceOperation("/sections/coarnotify/request-review/0", notifyServiceThree.getID())); + + String patchBody = getPatchContent(replaceOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isUnprocessableEntity()); + + } + + @Test + public void patchCOARNotifyServiceRemoveTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withCOARNotifyService(notifyServiceOne, "request-review") + .withCOARNotifyService(notifyServiceTwo, "request-review") + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(2))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceOne.getID(), notifyServiceTwo.getID() + ))); + + // try to remove the notifyServiceOne of witem + List removeOpts = new ArrayList(); + removeOpts.add(new RemoveOperation("/sections/coarnotify/request-review/0")); + + String patchBody = getPatchContent(removeOpts); + + getClient(authToken).perform(patch("/api/submission/workspaceitems/" + witem.getID()) + .content(patchBody) + .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(1))) + .andExpect(jsonPath("$.sections.coarnotify.request-review",contains( + notifyServiceTwo.getID()))); + + } + + @Test + public void submissionCOARNotifyServicesSectionWithValidationErrorsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceTwo = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name two") + .withLdnUrl("https://service2.ldn.org/inbox") + .build(); + + NotifyServiceEntity notifyServiceThree = + NotifyServiceBuilder.createNotifyServiceBuilder(context).withName("service name three") + .withLdnUrl("https://service3.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withType("Journal Article") + .withCOARNotifyService(notifyServiceOne, "request-endorsement") + .withCOARNotifyService(notifyServiceTwo, "request-review") + .withCOARNotifyService(notifyServiceThree, "request-review") + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceOne) + .withPattern("request-endorsement") + .withConstraint("fakeFilterA") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceTwo) + .withPattern("request-review") + .withConstraint("type_filter") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceThree) + .withPattern("request-review") + .withConstraint("fakeFilterA") + .isAutomatic(false) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem also check the errors + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(2))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", contains( + notifyServiceTwo.getID(), + notifyServiceThree.getID()))) + .andExpect(jsonPath("$.sections.coarnotify.request-endorsement", hasSize(1))) + .andExpect(jsonPath("$.sections.coarnotify.request-endorsement", contains( + notifyServiceOne.getID()))) + .andExpect(jsonPath("$.errors[?(@.message=='error.validation.coarnotify.invalidfilter')]", + Matchers.contains( + hasJsonPath("$.paths", Matchers.containsInAnyOrder( + "/sections/coarnotify/request-review/1", + "/sections/coarnotify/request-endorsement/0"))))); + + } + + @Test + public void submissionCOARNotifyServicesSectionWithoutValidationErrorsTest() throws Exception { + context.turnOffAuthorisationSystem(); + + //** GIVEN ** + //1. A community-collection structure with one parent community with sub-community and one collection. + parentCommunity = CommunityBuilder.createCommunity(context) + .withName("Parent Community") + .build(); + Community child1 = CommunityBuilder.createSubCommunity(context, parentCommunity) + .withName("Sub Community") + .build(); + Collection col1 = CollectionBuilder.createCollection(context, child1).withName("Collection 1").build(); + + + NotifyServiceEntity notifyServiceOne = + NotifyServiceBuilder.createNotifyServiceBuilder(context) + .withName("service name one") + .withLdnUrl("https://service.ldn.org/inbox") + .build(); + + WorkspaceItem witem = WorkspaceItemBuilder.createWorkspaceItem(context, col1) + .withTitle("Test WorkspaceItem") + .withIssueDate("2017-10-17") + .withType("Journal Article") + .withCOARNotifyService(notifyServiceOne, "request-review") + .withCOARNotifyService(notifyServiceOne, "request-endorsement") + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceOne) + .withPattern("request-endorsement") + .withConstraint("type_filter") + .isAutomatic(false) + .build(); + + NotifyServiceInboundPatternBuilder.createNotifyServiceInboundPatternBuilder(context, notifyServiceOne) + .withPattern("request-review") + .withConstraint("type_filter") + .isAutomatic(false) + .build(); + + context.restoreAuthSystemState(); + + String authToken = getAuthToken(eperson.getEmail(), password); + + // check the coar notify services of witem also check the errors + getClient(authToken).perform(get("/api/submission/workspaceitems/" + witem.getID())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.sections.coarnotify.request-review", hasSize(1))) + .andExpect(jsonPath("$.sections.coarnotify.request-review", + contains(notifyServiceOne.getID()))) + .andExpect(jsonPath( + "$.errors[?(@.message=='error.validation.coarnotify.invalidfilter')]") + .doesNotExist()); + } + @Test public void patchAddPrimaryTest() throws Exception { context.turnOffAuthorisationSystem(); @@ -9132,5 +9769,4 @@ public void patchReplaceProvidingWrongPrimaryTest() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.sections.upload.primary", is(idFirstPdf.get()))); } - } diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ContentReportMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ContentReportMatcher.java new file mode 100644 index 000000000000..203aafdba9c6 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/ContentReportMatcher.java @@ -0,0 +1,52 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +package org.dspace.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.content.Item; +import org.dspace.contentreport.FilteredCollection; +import org.hamcrest.Matcher; + +/** + * Utility class to construct a Matcher for a FilteredCollectionRest. + * @author Jean-François Morin (Université Laval) + */ +public class ContentReportMatcher { + + private ContentReportMatcher() { } + + public static Matcher matchFilteredCollectionProperties(FilteredCollection collection) { + return allOf( + hasJsonPath("$.label", is(collection.getLabel())), + hasJsonPath("$.community_label", is(collection.getCommunityLabel())), + hasJsonPath("$.community_handle", is(collection.getCommunityHandle())), + hasJsonPath("$.nb_total_items", is(collection.getTotalItems())), + hasJsonPath("$.all_filters_value", is(collection.getAllFiltersValue())) + ); + } + + public static Matcher matchFilteredCollectionSummary(int nbTotalItems, int nbFilteredItems) { + return allOf( + hasJsonPath("$.nb_total_items", is(nbTotalItems)), + hasJsonPath("$.all_filters_value", is(nbFilteredItems))); + } + + public static Matcher matchFilteredItemProperties(Item item) { + return allOf( + hasJsonPath("$.name", is(item.getName())), + hasJsonPath("$.inArchive", is(item.isArchived())), + hasJsonPath("$.discoverable", is(item.isDiscoverable())), + hasJsonPath("$.withdrawn", is(item.isWithdrawn())), + hasJsonPath("$.type", is("item")) + ); + } + +} diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java new file mode 100644 index 000000000000..377ed043e8c9 --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/NotifyServiceMatcher.java @@ -0,0 +1,132 @@ +/** + * 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.dspace.app.rest.test.AbstractControllerIntegrationTest.REST_SERVER_URL; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; + +import org.hamcrest.Matcher; + +/** + * Class to match JSON NotifyServiceEntity in ITs + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + */ +public class NotifyServiceMatcher { + + private NotifyServiceMatcher() { } + + public static Matcher matchNotifyService(String name, String description, String url, + String ldnUrl) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.description", is(description)), + hasJsonPath("$.url", is(url)), + hasJsonPath("$.ldnUrl", is(ldnUrl)), + hasJsonPath("$._links.self.href", containsString("/api/ldn/ldnservices/")) + ); + } + + public static Matcher matchNotifyServiceWithoutLinks( + String name, String description, String url, String ldnUrl) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.description", is(description)), + hasJsonPath("$.url", is(url)), + hasJsonPath("$.ldnUrl", is(ldnUrl)) + ); + } + + public static Matcher matchNotifyService(String name, String description, String url, + String ldnUrl, boolean enabled) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.description", is(description)), + hasJsonPath("$.url", is(url)), + hasJsonPath("$.ldnUrl", is(ldnUrl)), + hasJsonPath("$.enabled", is(enabled)), + hasJsonPath("$._links.self.href", containsString("/api/ldn/ldnservices/")) + ); + } + + public static Matcher matchNotifyService(String name, String description, String url, + String ldnUrl, boolean enabled, + String lowerIp, String upperIp) { + return allOf( + hasJsonPath("$.name", is(name)), + hasJsonPath("$.description", is(description)), + hasJsonPath("$.url", is(url)), + hasJsonPath("$.ldnUrl", is(ldnUrl)), + hasJsonPath("$.enabled", is(enabled)), + hasJsonPath("$.lowerIp", is(lowerIp)), + hasJsonPath("$.upperIp", is(upperIp)), + hasJsonPath("$._links.self.href", containsString("/api/ldn/ldnservices/")) + ); + } + + public static Matcher matchNotifyService(int id, String name, String description, + String url, String ldnUrl) { + return allOf( + hasJsonPath("$.id", is(id)), + matchNotifyService(name, description, url, ldnUrl), + hasJsonPath("$._links.self.href", startsWith(REST_SERVER_URL)), + hasJsonPath("$._links.self.href", endsWith("/api/ldn/ldnservices/" + id)) + ); + } + + public static Matcher matchNotifyService(int id, String name, String description, + String url, String ldnUrl, boolean enabled) { + return allOf( + hasJsonPath("$.id", is(id)), + matchNotifyService(name, description, url, ldnUrl, enabled), + hasJsonPath("$._links.self.href", startsWith(REST_SERVER_URL)), + hasJsonPath("$._links.self.href", endsWith("/api/ldn/ldnservices/" + id)) + ); + } + + public static Matcher matchNotifyService(int id, String name, String description, + String url, String ldnUrl, boolean enabled, + String lowerIp, String upperIp) { + return allOf( + hasJsonPath("$.id", is(id)), + matchNotifyService(name, description, url, ldnUrl, enabled, lowerIp, upperIp), + hasJsonPath("$._links.self.href", startsWith(REST_SERVER_URL)), + hasJsonPath("$._links.self.href", endsWith("/api/ldn/ldnservices/" + id)) + ); + } + + public static Matcher matchNotifyServiceWithoutLinks( + int id, String name, String description, String url, String ldnUrl) { + return allOf( + hasJsonPath("$.id", is(id)), + matchNotifyServiceWithoutLinks(name, description, url, ldnUrl) + ); + } + + public static Matcher matchNotifyServicePattern(String pattern, String constraint) { + return allOf( + hasJsonPath("$.pattern", is(pattern)), + hasJsonPath("$.constraint", is(constraint)) + ); + } + + public static Matcher matchNotifyServicePattern(String pattern, + String constraint, + Boolean automatic) { + return allOf( + matchNotifyServicePattern(pattern, constraint), + hasJsonPath("$.automatic", is(automatic)) + ); + } + +} 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 index 70e5fe61577b..8eb569468d4b 100644 --- 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 @@ -20,12 +20,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.json.JsonMapper; import org.apache.commons.lang3.StringUtils; +import org.dspace.app.rest.model.hateoas.QAEventResource; import org.dspace.content.QAEvent; +import org.dspace.qaevent.service.dto.NotifyMessageDTO; import org.dspace.qaevent.service.dto.OpenaireMessageDTO; +import org.dspace.qaevent.service.dto.QAMessageDTO; import org.hamcrest.Matcher; import org.hamcrest.Matchers; import org.hamcrest.core.IsAnything; + /** * Matcher related to {@link QAEventResource}. * @@ -69,23 +73,63 @@ public static Matcher matchQAEventEntry(QAEvent event) { } } - 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()))); + + public static Matcher matchQAEventNotifyEntry(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(), + NotifyMessageDTO.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, QAMessageDTO message) { + if (message instanceof OpenaireMessageDTO) { + OpenaireMessageDTO oadto = (OpenaireMessageDTO) message; + if (StringUtils.endsWith(topic, "/ABSTRACT")) { + return allOf(hasJsonPath("$.abstract", is(oadto.getAbstracts()))); + } else if (StringUtils.endsWith(topic, "/PID")) { + return allOf( + hasJsonPath("$.value", is(oadto.getValue())), + hasJsonPath("$.type", is(oadto.getType())), + hasJsonPath("$.pidHref", is(calculateOpenairePidHref(oadto.getType(), oadto.getValue())))); + } else if (StringUtils.endsWith(topic, "/PROJECT")) { + return allOf( + hasJsonPath("$.openaireId", is(oadto.getOpenaireId())), + hasJsonPath("$.acronym", is(oadto.getAcronym())), + hasJsonPath("$.code", is(oadto.getCode())), + hasJsonPath("$.funder", is(oadto.getFunder())), + hasJsonPath("$.fundingProgram", is(oadto.getFundingProgram())), + hasJsonPath("$.jurisdiction", is(oadto.getJurisdiction())), + hasJsonPath("$.title", is(oadto.getTitle()))); + } + } else if (message instanceof NotifyMessageDTO) { + NotifyMessageDTO notifyDTO = (NotifyMessageDTO) message; + if (StringUtils.endsWith(topic, "/REVIEW")) { + return allOf( + hasJsonPath("$.serviceName", is(notifyDTO.getServiceName())), + hasJsonPath("$.serviceId", is(notifyDTO.getServiceId())), + hasJsonPath("$.href", is(notifyDTO.getHref())), + hasJsonPath("$.relationship", is(notifyDTO.getRelationship())) + ); + } else if (StringUtils.endsWith(topic, "/ENDORSEMENT")) { + return allOf( + hasJsonPath("$.serviceName", is(notifyDTO.getServiceName())), + hasJsonPath("$.serviceId", is(notifyDTO.getServiceId())), + hasJsonPath("$.href", is(notifyDTO.getHref())), + hasJsonPath("$.relationship", is(notifyDTO.getRelationship())) + ); + } } return IsAnything.anything(); } 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 index 85ffe6fb44b3..c2a2e76d73c9 100644 --- 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 @@ -43,7 +43,6 @@ public static Matcher matchQATopicEntry(String source, String to ); } - public static Matcher matchQATopicEntry(String source, String topicName) { return allOf( hasJsonPath("$.type", is("qualityassurancetopic")), diff --git a/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCOARNotifyMatcher.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCOARNotifyMatcher.java new file mode 100644 index 000000000000..937b4144863a --- /dev/null +++ b/dspace-server-webapp/src/test/java/org/dspace/app/rest/matcher/SubmissionCOARNotifyMatcher.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.contains; +import static org.hamcrest.Matchers.is; + +import java.util.List; + +import org.dspace.coarnotify.NotifyPattern; +import org.hamcrest.Matcher; + +/** + * Matcher for the Submission COAR Notify. + * + * @author Mohamed Eskander (mohamed.eskander at 4science.com) + * + */ +public class SubmissionCOARNotifyMatcher { + + private SubmissionCOARNotifyMatcher() { + } + + public static Matcher matchCOARNotifyEntry(String id, List patterns) { + return allOf( + hasJsonPath("$.id", is(id)), + hasJsonPath( + "$.patterns", contains( + patterns.stream() + .map(coarPattern -> + allOf( + hasJsonPath("pattern", is(coarPattern.getPattern())), + hasJsonPath("multipleRequest", is(coarPattern.isMultipleRequest())) + )) + .toArray(Matcher[]::new)))); + } + +} diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_ack_review_reject.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_ack_review_reject.json new file mode 100644 index 000000000000..2ac1f1a15a4a --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_ack_review_reject.json @@ -0,0 +1,34 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://generic-service.com", + "name": "Generic Service", + "type": "Service" + }, + "context": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.4598/12123487", + "type": "Document" + }, + "id": "urn:uuid:668f26e0-2c8d-4117-a0d2-ee713523bcb4", + "inReplyTo": "<>", + "object": { + "id": "<>", + "object": "https://some-organisation.org/resource/0021", + "type": "Offer" + }, + "origin": { + "id": "https://generic-service.com/system", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "target": { + "id": "https://some-organisation.org", + "inbox": "hop", + "type": "Organization" + }, + "type": ["TentativeReject", "coar-notify:ReviewAction"] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_endorsement.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_endorsement.json new file mode 100644 index 000000000000..828757c4c1b9 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_endorsement.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://overlay-journal.com", + "name": "Overlay Journal", + "type": ["Service"] + }, + "context": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": ["sorg:AboutPage"], + "url": { + "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", + "mediaType": "application/pdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", + "object": { + "id": "<>", + "id_oai": "oai:www.openstarts.units.it:<>", + "id_old": "https://review-service.com/review/geo/202103/0021", + "ietf:cite-as": "https://overlay-journal.com/articles/00001/", + "type": [ + "Page", + "sorg:WebPage" + ] + }, + "origin": { + "id": "https://overlay-journal.com/system", + "inbox": "https://overlay-journal.com/inbox/", + "type": ["Service"] + }, + "target": { + "id": "https://research-organisation.org/repository", + "inbox": "https://research-organisation.org/inbox/", + "type": ["Service"] + }, + "type": [ + "Announce", + "coar-notify:EndorsementAction" + ] + } \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_release.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_release.json new file mode 100644 index 000000000000..becd3a02c5cf --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_release.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://research-organisation.org", + "name": "Research Organisation", + "type": "Organization" + }, + "context": { + "id": "<>", + "id_handle": "http://localhost:4000/handle/123456789/1119", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": "sorg:AboutPage", + "url": { + "id": "https://another-research-organisation.org/repository/datasets/item/201203421/data_archive.zip", + "mediaType": "application/zip", + "type": [ + "Article", + "sorg:Dataset" + ] + } + }, + "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe24f", + "object": { + "oldas:object": "https://another-research-organisation.org/repository/datasets/item/201203421/", + "as:object": "newValue", + "oldas:relationship": "http://purl.org/vocab/frbr/core#supplement", + "as:relationship": "somethingElse", + "as:subject": "https://research-organisation.org/repository/item/201203/421/", + "id": "<>", + "type": "Relationship" + }, + "origin": { + "id": "https://review-service.com/inbox/about/", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "target": { + "id": "https://another-research-organisation.org/repository", + "inbox": "https://another-research-organisation.org/inbox/", + "type": "Service" + }, + "type": [ + "Announce", + "coar-notify:RelationshipAction" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_review.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_review.json new file mode 100644 index 000000000000..8f422c9039fe --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_announce_review.json @@ -0,0 +1,48 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://review-service.com", + "name": "Review Service", + "type": "Service" + }, + "context": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": "sorg:AboutPage", + "url": { + "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", + "mediaType": "application/pdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "id": "urn:uuid:2f4ec582-109e-4952-a94a-b7d7615a8c69", + "inReplyTo": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", + "object": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.3214/987654", + "type": [ + "Document", + "sorg:Review" + ] + }, + "origin": { + "id": "https://review-service.com/system", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "target": { + "id": "https://generic-service.com/system", + "inbox": "https://generic-service.com/system/inbox/", + "type": "Service" + }, + "type": [ + "Announce", + "coar-notify:ReviewAction" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement.json new file mode 100644 index 000000000000..d977f2e6b7db --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement.json @@ -0,0 +1,39 @@ +{ +"@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" +], +"actor": { + "id": "https://orcid.org/0000-0002-1825-0097", + "name": "Josiah Carberry", + "type": ["Person"] +}, +"id": "123456789", +"object": { + "id": "https://overlay-journal.com/articles/00001/", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": "sorg:AboutPage", + "url": { + "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", + "mediaType": "application/pdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } +}, +"origin": { + "id": "https://research-organisation.org/repository", + "inbox": "https://research-organisation.org/inbox/", + "type": "Service" +}, +"target": { + "id": "https://overlay-journal.com/system", + "inbox": "https://overlay-journal.com/inbox/", + "type": "Service" +}, +"type": [ + "Offer", + "coar-notify:EndorsementAction" +] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement_badrequest.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement_badrequest.json new file mode 100644 index 000000000000..e6e373f1c7cd --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement_badrequest.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://orcid.org/0000-0002-1825-0097", + "name": "Josiah Carberry", + "type": ["Person"] + }, + "id": "123456789", + "object": { + "id": "https://overlay-journal.com/articles/00001/", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": ["sorg:AboutPage"], + "url": { + "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", + "mediaType": "application/pdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "https://research-organisation.org/repository", + "inbox": "https://research-organisation.org/inbox/", + "type": ["Service"] + }, + "target": { + "id": "https://overlay-journal.com/system", + "inbox": "https://overlay-journal.com/inbox/", + "type": ["Service"] + }, + "type": [ + "Offer", + "coar-notify:EndorsementAction" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement_object.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement_object.json new file mode 100644 index 000000000000..8252d7f70102 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_endorsement_object.json @@ -0,0 +1,38 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://orcid.org/0000-0002-1825-0097", + "name": "Josiah Carberry", + "type": ["Person"] + }, + "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd", + "object": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": ["sorg:AboutPage"], + "url": { + "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", + "mediaType": "application/pdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "https://research-organisation.org/repository", + "inbox": "https://research-organisation.org/inbox/", + "type": ["Service"] + }, + "target": { + "id": "https://overlay-journal.com/system", + "inbox": "https://overlay-journal.com/inbox/", + "type": ["Service"] + }, + "type": [ + "Offer", + "coar-notify:EndorsementAction" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_review.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_review.json new file mode 100644 index 000000000000..d1d5ba5601f6 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_review.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://orcid.org/0000-0002-1825-0097", + "name": "Josiah Carberry", + "type": "Person" + }, + "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509de", + "object": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": "sorg:AboutPage", + "url": { + "id": "url.pdf", + "mediaType": "applicationpdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "https://research-organisation.org/repository", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "target": { + "id": "https://review-service.com/system", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "type": [ + "Offer", + "coar-notify:ReviewAction" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_review2.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_review2.json new file mode 100644 index 000000000000..d60e32f04f2b --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_offer_review2.json @@ -0,0 +1,39 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://orcid.org/0000-0002-1825-0097", + "name": "Josiah Carberry", + "type": "Person" + }, + "id": "urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509df", + "object": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": "sorg:AboutPage", + "url": { + "id": "url.pdf", + "mediaType": "applicationpdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "https://research-organisation.org/repository", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "target": { + "id": "https://review-service.com/system", + "inbox": "https://review-service.com/inbox/", + "type": "Service" + }, + "type": [ + "Offer", + "coar-notify:ReviewAction" + ] +} \ No newline at end of file diff --git a/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_origin_inbox_unregistered.json b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_origin_inbox_unregistered.json new file mode 100644 index 000000000000..30c5cc559034 --- /dev/null +++ b/dspace-server-webapp/src/test/resources/org/dspace/app/rest/ldn_origin_inbox_unregistered.json @@ -0,0 +1,49 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "https://overlay-journal.com", + "name": "Overlay Journal", + "type": ["Service"] + }, + "context": { + "id": "<>", + "ietf:cite-as": "https://doi.org/10.5555/12345680", + "type": ["sorg:AboutPage"], + "url": { + "id": "https://research-organisation.org/repository/preprint/201203/421/content.pdf", + "mediaType": "application/pdf", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "id": "urn:uuid:94ecae35-dcfd-4182-8550-22c7164fe23f", + "object": { + "id": "<>", + "id_oai": "oai:www.openstarts.units.it:<>", + "id_old": "https://review-service.com/review/geo/202103/0021", + "ietf:cite-as": "https://overlay-journal.com/articles/00001/", + "type": [ + "Page", + "sorg:WebPage" + ] + }, + "origin": { + "id": "https://overlay-journal.com/system", + "inbox": "https://letsfake.it/inbox/", + "type": ["Service"] + }, + "target": { + "id": "https://research-organisation.org/repository", + "inbox": "https://research-organisation.org/inbox/", + "type": ["Service"] + }, + "type": [ + "Announce", + "coar-notify:EndorsementAction" + ] + } \ No newline at end of file diff --git a/dspace-services/pom.xml b/dspace-services/pom.xml index e3db95abf574..29208064107b 100644 --- a/dspace-services/pom.xml +++ b/dspace-services/pom.xml @@ -79,6 +79,11 @@ + + org.apache.logging.log4j + log4j-api + ${log4j.version} + org.springframework diff --git a/dspace-services/src/main/java/org/dspace/kernel/DSpaceKernelManager.java b/dspace-services/src/main/java/org/dspace/kernel/DSpaceKernelManager.java index 1f86c16b2928..c85df9ee6c34 100644 --- a/dspace-services/src/main/java/org/dspace/kernel/DSpaceKernelManager.java +++ b/dspace-services/src/main/java/org/dspace/kernel/DSpaceKernelManager.java @@ -22,8 +22,8 @@ import javax.management.ReflectionException; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** @@ -32,11 +32,11 @@ * @author Aaron Zeckoski (azeckoski @ gmail.com) */ public final class DSpaceKernelManager { - private static Logger log = LoggerFactory.getLogger(DSpaceKernelManager.class); + private static final Logger log = LogManager.getLogger(); private static DSpaceKernel defaultKernel = null; - private static Map namedKernelMap = new HashMap(); + private static final Map namedKernelMap = new HashMap<>(); public static DSpaceKernel getDefaultKernel() { @@ -50,7 +50,7 @@ public static void setDefaultKernel(DSpaceKernel kernel) { /** * A lock on the kernel to handle multiple threads getting the first item. */ - private Object lock = new Object(); + private final Object lock = new Object(); /** * Get the kernel. This will be a single instance for the JVM, but @@ -126,7 +126,7 @@ public DSpaceKernel getKernel(String name) { /** * Static initialized random default Kernel name */ - private static String defaultKernelName = UUID.randomUUID().toString(); + private static final String defaultKernelName = UUID.randomUUID().toString(); /** * Ensure that we have a name suitable for an mbean. @@ -162,7 +162,8 @@ public static void registerMBean(String mBeanName, DSpaceKernel kernel) { if (!mbs.isRegistered(name)) { // register the MBean mbs.registerMBean(kernel, name); - log.info("Registered new Kernel MBEAN: " + checkedMBeanName + " [" + kernel + "]"); + log.info("Registered new Kernel MBEAN: {} [{}]", + checkedMBeanName, kernel); } } catch (MalformedObjectNameException e) { throw new IllegalStateException(e); @@ -197,7 +198,7 @@ public static boolean unregisterMBean(String mBeanName) { return true; } catch (Exception e) { //log this issue as a System Warning. Also log the underlying error message. - log.warn("Failed to unregister the MBean: " + checkedMBeanName, e); + log.warn("Failed to unregister the MBean: {}", checkedMBeanName, e); return false; } } diff --git a/dspace-services/src/main/java/org/dspace/kernel/ServiceManager.java b/dspace-services/src/main/java/org/dspace/kernel/ServiceManager.java index e4cca677c760..c37b5d9b40e8 100644 --- a/dspace-services/src/main/java/org/dspace/kernel/ServiceManager.java +++ b/dspace-services/src/main/java/org/dspace/kernel/ServiceManager.java @@ -76,6 +76,18 @@ public interface ServiceManager { */ public List getServicesNames(); + /** + * Allows developers to get the desired service singleton by the provided type.
    + * This should return all instantiated objects of the type specified with their names + * (may not all be singletons). + * + * @param Class type + * @param type the type for the requested service (this will typically be the interface class but can be concrete + * as well) + * @return map with service's name and service singletons + */ + public Map getServicesWithNamesByType(Class type); + /** * Allows adding singleton services and providers in at runtime or * after the service manager has started up. diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelImpl.java b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelImpl.java index 461e3fbf0f62..e07e573ace63 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelImpl.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelImpl.java @@ -23,6 +23,8 @@ import javax.management.modelmbean.ModelMBeanInfoSupport; import javax.management.modelmbean.ModelMBeanOperationInfo; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.kernel.DSpaceKernel; import org.dspace.kernel.DSpaceKernelManager; import org.dspace.kernel.ServiceManager; @@ -30,15 +32,13 @@ import org.dspace.services.ConfigurationService; import org.dspace.services.KernelStartupCallbackService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * This is the kernel implementation which starts up the core of DSpace, * registers the mbean, and initializes the DSpace object. * It also loads up the configuration. Sets a JRE shutdown hook. *

    - * Note that this does not start itself and calling the constuctor does + * Note that this does not start itself and calling the constructor does * not actually start it up either. It has to be explicitly started by * calling the start method so something in the system needs to do that. * If the bean is already started then calling start on it again has no @@ -50,7 +50,7 @@ */ public final class DSpaceKernelImpl implements DSpaceKernel, DynamicMBean { - private static Logger log = LoggerFactory.getLogger(DSpaceKernelImpl.class); + private static final Logger log = LogManager.getLogger(); /** * Creates a DSpace Kernel, does not do any checks though. @@ -75,7 +75,7 @@ protected void registerShutdownHook() { synchronized (lock) { // No shutdown hook registered yet this.shutdownHook = new Thread() { - public void run() { + @Override public void run() { doDestroy(); } }; @@ -86,12 +86,14 @@ public void run() { private ConfigurationService configurationService; + @Override public ConfigurationService getConfigurationService() { return configurationService; } private ServiceManagerSystem serviceManagerSystem; + @Override public ServiceManager getServiceManager() { return serviceManagerSystem; } @@ -99,6 +101,7 @@ public ServiceManager getServiceManager() { /* (non-Javadoc) * @see org.dspace.kernel.DSpaceKernel#getMBeanName() */ + @Override public String getMBeanName() { return mBeanName; } @@ -106,6 +109,7 @@ public String getMBeanName() { /* (non-Javadoc) * @see org.dspace.kernel.DSpaceKernel#isRunning() */ + @Override public boolean isRunning() { synchronized (lock) { return running; @@ -115,6 +119,7 @@ public boolean isRunning() { /* (non-Javadoc) * @see org.dspace.kernel.CommonLifecycle#getManagedBean() */ + @Override public DSpaceKernel getManagedBean() { synchronized (lock) { return kernel; @@ -124,6 +129,7 @@ public DSpaceKernel getManagedBean() { /* (non-Javadoc) * @see org.dspace.kernel.CommonLifecycle#start() */ + @Override public void start() { start(null); } @@ -170,12 +176,14 @@ public void start(String dspaceHome) { // add in the shutdown hook registerShutdownHook(); } - log.info("DSpace kernel startup completed in " + loadTime + " ms and registered as MBean: " + mBeanName); + log.info("DSpace kernel startup completed in {} ms and registered as MBean: {}", + loadTime, mBeanName); } /* (non-Javadoc) * @see org.dspace.kernel.CommonLifecycle#stop() */ + @Override public void stop() { if (!running) { //log.warn("Kernel ("+this+") is already stopped"); @@ -251,7 +259,8 @@ protected void finalize() throws Throwable { try { doDestroy(); } catch (Exception e) { - log.error("WARN Failure attempting to cleanup the DSpace kernel: " + e.getMessage(), e); + log.error("WARN Failure attempting to cleanup the DSpace kernel: {}", + e.getMessage(), e); } super.finalize(); } @@ -287,11 +296,13 @@ public long getLoadTime() { return loadTime; } + @Override public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException { return this; } + @Override public MBeanInfo getMBeanInfo() { Descriptor lastLoadDateDesc = new DescriptorSupport(new String[] {"name=LastLoadDate", "descriptorType=attribute", "default=0", "displayName=Last Load Date", @@ -319,6 +330,7 @@ public MBeanInfo getMBeanInfo() { return new ModelMBeanInfoSupport(this.getClass().getName(), "DSpace Kernel", mmbai, null, mmboi, null); } + @Override public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException { if ("LastLoadDate".equals(attribute)) { @@ -329,16 +341,19 @@ public Object getAttribute(String attribute) throw new AttributeNotFoundException("invalid attribute: " + attribute); } + @Override public AttributeList getAttributes(String[] attributes) { // TODO Auto-generated method stub return null; } + @Override public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException { throw new InvalidAttributeValueException("Cannot set attribute: " + attribute); } + @Override public AttributeList setAttributes(AttributeList attributes) { // TODO Auto-generated method stub return null; diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelInit.java b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelInit.java index f8df74d34c65..5a56da3474e4 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelInit.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceKernelInit.java @@ -7,10 +7,10 @@ */ package org.dspace.servicemanager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.kernel.DSpaceKernel; import org.dspace.kernel.DSpaceKernelManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * This class simplifies the handling of lookup, registration, and @@ -21,7 +21,7 @@ */ public class DSpaceKernelInit { - private static Logger log = LoggerFactory.getLogger(DSpaceKernelInit.class); + private static final Logger log = LogManager.getLogger(DSpaceKernelInit.class); private static final Object staticLock = new Object(); @@ -57,7 +57,7 @@ public static DSpaceKernelImpl getKernel(String name) { synchronized (staticLock) { DSpaceKernelImpl kernelImpl = new DSpaceKernelImpl(name); - log.info("Created new kernel: " + kernelImpl); + log.info("Created new kernel: {}", kernelImpl); if (name != null) { DSpaceKernelManager.registerMBean(kernelImpl.getMBeanName(), kernelImpl); diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java index 6cffa7ee66d5..313f676c5f6a 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/DSpaceServiceManager.java @@ -504,6 +504,21 @@ public List getServicesNames() { return beanNames; } + @Override + public Map getServicesWithNamesByType(Class type) { + checkRunning(); + + if (type == null) { + throw new IllegalArgumentException("type cannot be null"); + } + + try { + return applicationContext.getBeansOfType(type, true, true); + } catch (BeansException e) { + throw new RuntimeException("Failed to get beans of type (" + type + "): " + e.getMessage(), e); + } + } + @Override public boolean isServiceExists(String name) { checkRunning(); diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java index c20ee1962868..777fcc8a5406 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceConfigurationService.java @@ -30,9 +30,9 @@ import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler; import org.apache.commons.configuration2.event.Event; import org.apache.commons.configuration2.ex.ConfigurationException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.services.ConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.SimpleTypeConverter; import org.springframework.core.io.ClassPathResource; @@ -47,7 +47,7 @@ */ public final class DSpaceConfigurationService implements ConfigurationService { - private static final Logger log = LoggerFactory.getLogger(DSpaceConfigurationService.class); + private static final Logger log = LogManager.getLogger(); public static final String DSPACE = "dspace"; public static final String EXT_CONFIG = "cfg"; @@ -153,7 +153,7 @@ public Configuration getConfiguration() { try { return this.configurationBuilder.getConfiguration(); } catch (ConfigurationException ce) { - log.error("Unable to get configuration object based on definition at " + this.configDefinition); + log.error("Unable to get configuration object based on definition at {}", this.configDefinition); System.err.println("Unable to get configuration object based on definition at " + this.configDefinition); throw new RuntimeException(ce); } @@ -385,7 +385,7 @@ public synchronized boolean setProperty(String name, Object value) { if (value == null && oldValue != null) { changed = true; getConfiguration().clearProperty(name); - log.info("Cleared the configuration setting for name (" + name + ")"); + log.info("Cleared the configuration setting for name ({})", name); } else if (value != null && !value.equals(oldValue)) { changed = true; getConfiguration().setProperty(name, value); @@ -527,7 +527,8 @@ private void loadInitialConfig(String providedHome) { (Event e) -> this.configurationBuilder.getReloadingController() .checkForReloading(null)); } catch (ConfigurationException ce) { - log.error("Unable to load configurations based on definition at " + this.configDefinition); + log.error("Unable to load configurations based on definition at {}", + this.configDefinition); System.err.println("Unable to load configurations based on definition at " + this.configDefinition); throw new RuntimeException(ce); } @@ -535,7 +536,7 @@ private void loadInitialConfig(String providedHome) { // Finally, set any dynamic, default properties setDynamicProperties(); - log.info("Started up configuration service and loaded settings: " + toString()); + log.info("Started up configuration service and loaded settings: {}", this::toString); } /** @@ -557,9 +558,10 @@ public synchronized void reloadConfig() { // Finally, (re)set any dynamic, default properties setDynamicProperties(); } catch (ConfigurationException ce) { - log.error("Unable to reload configurations based on definition at " + this.configDefinition, ce); + log.error("Unable to reload configurations based on definition at {}", + this.configDefinition, ce); } - log.info("Reloaded configuration service: " + toString()); + log.info("Reloaded configuration service: {}", this::toString); } /** diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceEnvironmentConfiguration.java b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceEnvironmentConfiguration.java index fcfe1ccbe143..14189f622bc8 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceEnvironmentConfiguration.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/config/DSpaceEnvironmentConfiguration.java @@ -12,8 +12,8 @@ import org.apache.commons.configuration2.MapConfiguration; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /** * Bash does not allow environment variables that contain dots in their name. @@ -28,7 +28,7 @@ */ public class DSpaceEnvironmentConfiguration extends MapConfiguration { - private static Logger log = LoggerFactory.getLogger(DSpaceEnvironmentConfiguration.class); + private static final Logger log = LogManager.getLogger(); /** * Create a Configuration based on the environment variables. @@ -52,12 +52,13 @@ public static Map getModifiedEnvMap() { // replace "__D__" with a single dash. String lookup = StringUtils.replace(key, "__P__", "."); lookup = StringUtils.replace(lookup, "__D__", "-"); - if (System.getenv(key) != null) { + String value = System.getenv(key); + if (value != null) { // store the new key with the old value in our new properties map. - env.put(lookup, System.getenv(key)); - log.debug("Found env " + lookup + " = " + System.getenv(key) + "."); + env.put(lookup, value); + log.debug("Found env {} = {}.", lookup, value); } else { - log.debug("Didn't found env " + lookup + "."); + log.debug("Didn't find env {}.", lookup); } } return env; diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/example/RequestInterceptorExample.java b/dspace-services/src/main/java/org/dspace/servicemanager/example/RequestInterceptorExample.java index eb82476bee9e..c1717350575b 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/example/RequestInterceptorExample.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/example/RequestInterceptorExample.java @@ -7,21 +7,20 @@ */ package org.dspace.servicemanager.example; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.services.RequestService; import org.dspace.services.model.RequestInterceptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A sample RequestInterceptor which simply logs request start and end * calls. * * @author Mark Diggory (mdiggory at atmire.com) - * @version $Revision$ */ public final class RequestInterceptorExample implements RequestInterceptor { - private static Logger log = LoggerFactory.getLogger(RequestInterceptorExample.class); + private static final Logger log = LogManager.getLogger(); /** * Constructor which will inject the instantiated @@ -36,12 +35,12 @@ public RequestInterceptorExample(RequestService service) { @Override public void onEnd(String requestId, boolean succeeded, Exception failure) { - log.info("Intercepting End of Request: id=" + requestId + ", succeeded=" + succeeded); + log.info("Intercepting End of Request: id={}, succeeded={}", requestId, succeeded); } @Override public void onStart(String requestId) { - log.info("Intercepting Start of Request: id=" + requestId); + log.info("Intercepting Start of Request: id={}", requestId); } @Override diff --git a/dspace-services/src/main/java/org/dspace/servicemanager/spring/ResourceFinder.java b/dspace-services/src/main/java/org/dspace/servicemanager/spring/ResourceFinder.java index a440d0880618..b48ec6b16998 100644 --- a/dspace-services/src/main/java/org/dspace/servicemanager/spring/ResourceFinder.java +++ b/dspace-services/src/main/java/org/dspace/servicemanager/spring/ResourceFinder.java @@ -14,9 +14,9 @@ import java.util.Arrays; import java.util.List; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.servicemanager.config.DSpaceConfigurationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; @@ -26,13 +26,13 @@ * things (file/IS/resource). * This also allows us to look on a relative or absolute path and will * automatically check the typical places one might expect to put DSpace - * config files. + * configuration files. * * @author Aaron Zeckoski (aaron@caret.cam.ac.uk) */ public class ResourceFinder { - private static Logger log = LoggerFactory.getLogger(ResourceFinder.class); + private static final Logger log = LogManager.getLogger(); public static final String relativePath = DSpaceConfigurationService.DSPACE + "/"; public static final String environmentPathVariable = DSpaceConfigurationService.DSPACE_HOME; @@ -43,7 +43,7 @@ public class ResourceFinder { private ResourceFinder() { } private static List makeResources(List paths) { - List rs = new ArrayList(); + List rs = new ArrayList<>(); if (paths != null && !paths.isEmpty()) { for (String path : paths) { try { @@ -51,7 +51,7 @@ private static List makeResources(List paths) { rs.add(r); } catch (IllegalArgumentException e) { // do not add if not found, just skip - log.error(e.getMessage() + ", continuing..."); + log.error("{}, continuing...", e::getMessage); } } } diff --git a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java index 2a822c7c6eeb..878e34cfade1 100644 --- a/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java +++ b/dspace-services/src/main/java/org/dspace/services/email/EmailServiceImpl.java @@ -18,11 +18,11 @@ import javax.naming.NoInitialContextException; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.services.ConfigurationService; import org.dspace.services.EmailService; import org.dspace.services.factory.DSpaceServicesFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -35,7 +35,7 @@ public class EmailServiceImpl extends Authenticator implements EmailService { - private static final Logger logger = LoggerFactory.getLogger(EmailServiceImpl.class); + private static final Logger logger = LogManager.getLogger(); private Session session = null; diff --git a/dspace-services/src/main/java/org/dspace/services/events/SystemEventService.java b/dspace-services/src/main/java/org/dspace/services/events/SystemEventService.java index 1787c688f6a1..4cd4a523cd3d 100644 --- a/dspace-services/src/main/java/org/dspace/services/events/SystemEventService.java +++ b/dspace-services/src/main/java/org/dspace/services/events/SystemEventService.java @@ -13,14 +13,14 @@ import javax.annotation.PreDestroy; import org.apache.commons.lang3.ArrayUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.services.EventService; import org.dspace.services.RequestService; import org.dspace.services.model.Event; import org.dspace.services.model.Event.Scope; import org.dspace.services.model.EventListener; import org.dspace.services.model.RequestInterceptor; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -32,7 +32,7 @@ */ public final class SystemEventService implements EventService { - private final Logger log = LoggerFactory.getLogger(SystemEventService.class); + private final Logger log = LogManager.getLogger(); /** * Map for holding onto the listeners which is ClassLoader safe. @@ -127,8 +127,8 @@ private void fireLocalEvent(Event event) { */ private void fireClusterEvent(Event event) { log.debug( - "fireClusterEvent is not implemented yet, no support for cluster events yet, could not fire event to the " + - "cluster: " + event); + "fireClusterEvent is not implemented yet, no support for cluster" + + " events yet, could not fire event to the cluster: {}", event); } /** @@ -139,8 +139,9 @@ private void fireClusterEvent(Event event) { */ private void fireExternalEvent(Event event) { log.debug( - "fireExternalEvent is not implemented yet, no support for external events yet, could not fire event to " + - "external listeners: " + event); + "fireExternalEvent is not implemented yet, no support for external" + + " events yet, could not fire event to external listeners: {}", + event); } /** diff --git a/dspace-services/src/main/java/org/dspace/services/sessions/StatelessRequestServiceImpl.java b/dspace-services/src/main/java/org/dspace/services/sessions/StatelessRequestServiceImpl.java index dd66a2e85283..9b67cd4bfcc6 100644 --- a/dspace-services/src/main/java/org/dspace/services/sessions/StatelessRequestServiceImpl.java +++ b/dspace-services/src/main/java/org/dspace/services/sessions/StatelessRequestServiceImpl.java @@ -21,6 +21,8 @@ import javax.servlet.ServletResponse; import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.dspace.services.ConfigurationService; import org.dspace.services.RequestService; import org.dspace.services.model.Request; @@ -29,8 +31,6 @@ import org.dspace.services.sessions.model.HttpRequestImpl; import org.dspace.services.sessions.model.InternalRequestImpl; import org.dspace.utils.servicemanager.OrderedServiceComparator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; /** @@ -45,7 +45,7 @@ */ public final class StatelessRequestServiceImpl implements RequestService { - private static final Logger log = LoggerFactory.getLogger(StatelessRequestServiceImpl.class); + private static final Logger log = LogManager.getLogger(); private ConfigurationService configurationService; @@ -110,8 +110,8 @@ private String startRequest(Request req) { log.warn(message); throw new RequestInterruptionException(message, e); } catch (Exception e) { - log.warn("Request interceptor (" + requestInterceptor + ") failed to execute on start (" + req - .getRequestId() + "): " + e.getMessage()); + log.warn("Request interceptor ({}) failed to execute on start ({}): {}", + () -> requestInterceptor, req::getRequestId, e::getMessage); } } } @@ -149,15 +149,14 @@ private void endRequest(String requestId, Exception failure) { requestInterceptor.onEnd(requestId, (failure == null), failure); } catch (RequestInterruptionException e) { log.warn( - "Attempt to stop request from ending by an exception from the interceptor (" + - requestInterceptor + "), cannot stop requests from ending though so request end " + - "continues, this may be an error: " + e - .getMessage()); + "Attempt to stop request from ending by an exception from the interceptor ({})," + + " cannot stop requests from ending though so request end" + + " continues, this may be an error: {}", + () -> requestInterceptor, e::getMessage); } catch (Exception e) { log.warn( - "Request interceptor (" + requestInterceptor + ") failed to execute on end (" + requestId - + "): " + e - .getMessage()); + "Request interceptor ({}) failed to execute on end ({}): {}", + () -> requestInterceptor, () -> requestId, e::getMessage); } } } diff --git a/dspace-services/src/main/java/org/dspace/utils/servlet/DSpaceWebappServletFilter.java b/dspace-services/src/main/java/org/dspace/utils/servlet/DSpaceWebappServletFilter.java index 122581ebaa93..8707eb5e4c27 100644 --- a/dspace-services/src/main/java/org/dspace/utils/servlet/DSpaceWebappServletFilter.java +++ b/dspace-services/src/main/java/org/dspace/utils/servlet/DSpaceWebappServletFilter.java @@ -37,6 +37,7 @@ public final class DSpaceWebappServletFilter implements Filter { /* (non-Javadoc) * @see javax.servlet.Filter#init(javax.servlet.FilterConfig) */ + @Override public void init(FilterConfig filterConfig) throws ServletException { // ensure the kernel is running, if not then we have to die here try { @@ -54,9 +55,10 @@ public void init(FilterConfig filterConfig) throws ServletException { /* (non-Javadoc) * @see javax.servlet.Filter#destroy() */ + @Override public void destroy() { // clean up the logger for this webapp - // No longer using commons-logging (JCL), use slf4j instead + // No longer using commons-logging (JCL), use Log4J instead //LogFactory.release(Thread.currentThread().getContextClassLoader()); } @@ -64,6 +66,7 @@ public void destroy() { * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet * .FilterChain) */ + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // now do some DSpace stuff diff --git a/dspace-services/src/test/java/org/dspace/servicemanager/MockServiceManagerSystem.java b/dspace-services/src/test/java/org/dspace/servicemanager/MockServiceManagerSystem.java index aecadcdf0239..dc7cd26b516e 100644 --- a/dspace-services/src/test/java/org/dspace/servicemanager/MockServiceManagerSystem.java +++ b/dspace-services/src/test/java/org/dspace/servicemanager/MockServiceManagerSystem.java @@ -80,6 +80,11 @@ public List getServicesNames() { return this.sms.getServicesNames(); } + @Override + public Map getServicesWithNamesByType(Class type) { + return this.sms.getServicesWithNamesByType(type); + } + /* (non-Javadoc) * @see org.dspace.kernel.ServiceManager#isServiceExists(java.lang.String) */ diff --git a/dspace-services/src/test/java/org/dspace/utils/servicemanager/ProviderStackTest.java b/dspace-services/src/test/java/org/dspace/utils/servicemanager/ProviderStackTest.java index 300411c0539a..47c2b302a7d1 100644 --- a/dspace-services/src/test/java/org/dspace/utils/servicemanager/ProviderStackTest.java +++ b/dspace-services/src/test/java/org/dspace/utils/servicemanager/ProviderStackTest.java @@ -13,6 +13,7 @@ import static org.junit.Assert.assertTrue; import java.util.ArrayList; +import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -84,6 +85,10 @@ public List getServicesNames() { return new ArrayList(); } + public Map getServicesWithNamesByType(Class type) { + return new HashMap<>(); + } + public boolean isServiceExists(String name) { return false; } diff --git a/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl b/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl index db67ae91fe69..20b5d22f2c93 100644 --- a/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl +++ b/dspace/config/crosswalks/oai/metadataFormats/rioxx.xsl @@ -785,7 +785,7 @@ http://purl.org/coar/version/c_ab4af688f83e57aa - http://purl.org/coar/resource_type/c_1162 + http://purl.org/coar/version/c_970fb48d4fbd8a85 http://purl.org/coar/version/c_b1a7d7d4d402bcce diff --git a/dspace/config/dspace.cfg b/dspace/config/dspace.cfg index d4be67613ed4..e860d59d1127 100644 --- a/dspace/config/dspace.cfg +++ b/dspace/config/dspace.cfg @@ -84,7 +84,7 @@ db.url = jdbc:postgresql://localhost:5432/dspace db.driver = org.postgresql.Driver # PostgreSQL Database Dialect (for Hibernate) -db.dialect = org.hibernate.dialect.PostgreSQL94Dialect +db.dialect = org.dspace.util.DSpacePostgreSQLDialect # Database username and password db.username = dspace @@ -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, qaeventsdelete +event.dispatcher.default.consumers = versioning, discovery, eperson, submissionconfig, qaeventsdelete, ldnmessage # The noindex dispatcher will not create search or browse indexes (useful for batch item imports) event.dispatcher.noindex.class = org.dspace.event.BasicDispatcher @@ -826,6 +826,10 @@ event.consumer.iiif.filters = Item+Modify:Item+Modify_Metadata:Item+Delete:Item+ event.consumer.orcidqueue.class = org.dspace.orcid.consumer.OrcidQueueConsumer event.consumer.orcidqueue.filters = Item+Install|Modify|Modify_Metadata|Delete|Remove +# consumer to store LDN Messages +event.consumer.ldnmessage.class = org.dspace.app.ldn.LDNMessageConsumer +event.consumer.ldnmessage.filters = Item+Install + # item submission config reload consumer event.consumer.submissionconfig.class = org.dspace.submit.consumer.SubmissionConfigConsumer event.consumer.submissionconfig.filters = Collection+Modify_Metadata @@ -932,7 +936,8 @@ registry.metadata.load = schema-publicationVolume-types.xml registry.metadata.load = openaire4-types.xml registry.metadata.load = dspace-types.xml registry.metadata.load = iiif-types.xml - +registry.metadata.load = datacite-types.xml +registry.metadata.load = coar-types.xml #---------------------------------------------------------------# #-----------------UI-Related CONFIGURATIONS---------------------# @@ -1668,3 +1673,4 @@ include = ${module_dir}/usage-statistics.cfg include = ${module_dir}/versioning.cfg include = ${module_dir}/workflow.cfg include = ${module_dir}/external-providers.cfg +include = ${module_dir}/ldn.cfg diff --git a/dspace/config/emails/coar_notify_accepted b/dspace/config/emails/coar_notify_accepted new file mode 100644 index 000000000000..78ab0da07da3 --- /dev/null +++ b/dspace/config/emails/coar_notify_accepted @@ -0,0 +1,27 @@ +## Notification email sent when a request to review an item has been accepted by the service +## +## Parameters: {0} Service Name +## {1} Item Name +## {2} Service URL +## {3} Item URL +## {4} Submitter's Name +## {5} Date of the received LDN notification +## +## +#set($subject = "DSpace: The Service ${params[0]} has accepted to review the Item ""${params[1]}""") + +An acceptance notification has been received by the service: ${params[0]} +for the Item: ${params[1]} + +Here is a more detailed report: +Item: ${params[1]} +Item URL: ${params[3]} +Submitted by: ${params[4]} + +Has a new status: ONGOING REVIEW + +By Service: ${params[0]} +Service URL: ${params[2]} +Date: ${params[5]} + +The ${config.get('dspace.name')} Team diff --git a/dspace/config/emails/coar_notify_endorsed b/dspace/config/emails/coar_notify_endorsed new file mode 100644 index 000000000000..aaf06b3efde9 --- /dev/null +++ b/dspace/config/emails/coar_notify_endorsed @@ -0,0 +1,27 @@ +## Notification email sent when an item has been endorsed by a service +## +## Parameters: {0} Service Name +## {1} Item Name +## {2} Service URL +## {3} Item URL +## {4} Submitter's Name +## {5} Date of the received LDN notification +## +## +#set($subject = "DSpace: The Service ${params[0]} has endorsed the Item ""${params[1]}""") + +An endorsement announce notification has been received by the service: ${params[0]} +for the Item: ${params[1]} + +Here is a more detailed report: +Item: ${params[1]} +Item URL: ${params[3]} +Submitted by: ${params[4]} + +Has a new status: ENDORSED + +By Service: ${params[0]} +Service URL: ${params[2]} +Date: ${params[5]} + +The ${config.get('dspace.name')} Team diff --git a/dspace/config/emails/coar_notify_rejected b/dspace/config/emails/coar_notify_rejected new file mode 100644 index 000000000000..ddba3366baca --- /dev/null +++ b/dspace/config/emails/coar_notify_rejected @@ -0,0 +1,27 @@ +## Notification email sent when a request to review an item has been rejected by the service +## +## Parameters: {0} Service Name +## {1} Item Name +## {2} Service URL +## {3} Item URL +## {4} Submitter's Name +## {5} Date of the received LDN notification +## +## +#set($subject = "DSpace: The Service ${params[0]} has refused to review the Item ""${params[1]}""") + +A rejection notification has been received by the service: ${params[0]} +for the Item: ${params[1]} + +Here is a more detailed report: +Item: ${params[1]} +Item URL: ${params[3]} +Submitted by: ${params[4]} + +Has a new update: REJECTED REVIEW REQUEST + +By Service: ${params[0]} +Service URL: ${params[2]} +Date: ${params[5]} + +The ${config.get('dspace.name')} Team diff --git a/dspace/config/emails/coar_notify_relationship b/dspace/config/emails/coar_notify_relationship new file mode 100644 index 000000000000..7a8782b5e9be --- /dev/null +++ b/dspace/config/emails/coar_notify_relationship @@ -0,0 +1,29 @@ +## Notification email sent that a resource has been related by a service +## +## Parameters: {0} Service Name +## {1} Item Name +## {2} Service URL +## {3} Item URL +## {4} Submitter's Name +## {5} Date of the received LDN notification +## {6} LDN notification +## {7} Item +## +## +#set($subject = "DSpace: The Service ${params[0]} has related a Resource ""${params[6].object.subject}""") + +A relationship announce notification has been received relating to the Item: ${params[1]} + +Here is a more detailed report: +Item: ${params[1]} +Item URL: ${params[3]} +Submitted by: ${params[4]} +Linked Resource URL: ${params[6].object.subject} + +Has a new status: RELATED + +By Service: ${params[0]} +Service URL: ${params[2]} +Date: ${params[5]} + +The ${config.get('dspace.name')} Team diff --git a/dspace/config/emails/coar_notify_reviewed b/dspace/config/emails/coar_notify_reviewed new file mode 100644 index 000000000000..c25dd9b51d91 --- /dev/null +++ b/dspace/config/emails/coar_notify_reviewed @@ -0,0 +1,27 @@ +## Notification email sent when an item has been reviewed by a service +## +## Parameters: {0} Service Name +## {1} Item Name +## {2} Service URL +## {3} Item URL +## {4} Submitter's Name +## {5} Date of the received LDN notification +## +## +#set($subject = "DSpace: The Service ${params[0]} has reviewed the Item ""${params[1]}""") + +A review announce notification has been received by the service: ${params[0]} +for the Item: ${params[1]} + +Here is a more detailed report: +Item: ${params[1]} +Item URL: ${params[3]} +Submitted by: ${params[4]} + +Has a new status: REVIEWED + +By Service: ${params[0]} +Service URL: ${params[2]} +Date: ${params[5]} + +The ${config.get('dspace.name')} Team diff --git a/dspace/config/hibernate.cfg.xml b/dspace/config/hibernate.cfg.xml index 763a760c0ece..6cc1020c47bf 100644 --- a/dspace/config/hibernate.cfg.xml +++ b/dspace/config/hibernate.cfg.xml @@ -98,5 +98,11 @@ + + + + + + diff --git a/dspace/config/item-submission.xml b/dspace/config/item-submission.xml index c73ea6723851..a2c6c623d45e 100644 --- a/dspace/config/item-submission.xml +++ b/dspace/config/item-submission.xml @@ -283,6 +283,13 @@ org.dspace.app.rest.submit.step.ShowIdentifiersStep identifiers + + + submit.progressbar.coarnotify + org.dspace.app.rest.submit.step.NotifyStep + coarnotify + + @@ -376,6 +383,9 @@ + + + diff --git a/dspace/config/ldn/request-endorsement b/dspace/config/ldn/request-endorsement new file mode 100644 index 000000000000..e885bf88efab --- /dev/null +++ b/dspace/config/ldn/request-endorsement @@ -0,0 +1,52 @@ +## generate LDN message json when request-endorsement of an item +## +## Parameters: {0} config 'dspace.ui.url' +## {1} config 'ldn.notify.inbox' +## {2} config 'dspace.name' +## {3} Notify Service url +## {4} Notify Service ldnUrl +## {5} 'dspace.ui.url'/handle/xxxx/yyy +## {6} metadata value of 'dc.identifier.uri' +## {7} the url to the primary bitstream or the first bitstream in the ORIGINAL bundle if there is no primary bitstream. The url is 'dspace.ui.url'/bitstreams/:uuid/download +## {8} the bitstream MimeType or get User Format MimeType if getFormat is 'Unknown' +## {9} id of the created LDNMessage + +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "${params[0]}", + "name": "${params[2]}", + "type": "Service" + }, + "id": "${params[9]}", + "object": { + "id": "${params[5]}", + "ietf:cite-as": "${params[6]}", + "type": "sorg:AboutPage", + "url": { + "id": "${params[7]}", + "mediaType": "${params[8]}", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "${params[0]}", + "inbox": "${params[1]}", + "type": "Service" + }, + "target": { + "id": "${params[3]}", + "inbox": "${params[4]}", + "type": "Service" + }, + "type": [ + "Offer", + "coar-notify:EndorsementAction" + ] +} \ No newline at end of file diff --git a/dspace/config/ldn/request-ingest b/dspace/config/ldn/request-ingest new file mode 100644 index 000000000000..82bd9a85d90c --- /dev/null +++ b/dspace/config/ldn/request-ingest @@ -0,0 +1,52 @@ +## generate LDN message json when request-ingest of an item +## +## Parameters: {0} config 'dspace.ui.url' +## {1} config 'ldn.notify.inbox' +## {2} config 'dspace.name' +## {3} Notify Service url +## {4} Notify Service ldnUrl +## {5} 'dspace.ui.url'/handle/xxxx/yyy +## {6} metadata value of 'dc.identifier.uri' +## {7} the url to the primary bitstream or the first bitstream in the ORIGINAL bundle if there is no primary bitstream. The url is 'dspace.ui.url'/bitstreams/:uuid/download +## {8} the bitstream MimeType or get User Format MimeType if getFormat is 'Unknown' +## {9} id of the created LDNMessage + +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "${params[0]}", + "name": "${params[2]}", + "type": "Service" + }, + "id": "${params[9]}", + "object": { + "id": "${params[5]}", + "ietf:cite-as": "${params[6]}", + "type": "sorg:AboutPage", + "url": { + "id": "${params[7]}", + "mediaType": "${params[8]}", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "${params[0]}", + "inbox": "${params[1]}", + "type": "Service" + }, + "target": { + "id": "${params[3]}", + "inbox": "${params[4]}", + "type": "Service" + }, + "type": [ + "Offer", + "coar-notify:IngestAction" + ] +} \ No newline at end of file diff --git a/dspace/config/ldn/request-review b/dspace/config/ldn/request-review new file mode 100644 index 000000000000..24c1ac831968 --- /dev/null +++ b/dspace/config/ldn/request-review @@ -0,0 +1,52 @@ +## generate LDN message json when request-review of an item +## +## Parameters: {0} config 'dspace.ui.url' +## {1} config 'ldn.notify.inbox' +## {2} config 'dspace.name' +## {3} Notify Service url +## {4} Notify Service ldnUrl +## {5} 'dspace.ui.url'/handle/xxxx/yyy +## {6} metadata value of 'dc.identifier.uri' +## {7} the url to the primary bitstream or the first bitstream in the ORIGINAL bundle if there is no primary bitstream. The url is 'dspace.ui.url'/bitstreams/:uuid/download +## {8} the bitstream MimeType or get User Format MimeType if getFormat is 'Unknown' +## {9} id of the created LDNMessage + +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://purl.org/coar/notify" + ], + "actor": { + "id": "${params[0]}", + "name": "${params[2]}", + "type": "Service" + }, + "id": "${params[9]}", + "object": { + "id": "${params[5]}", + "ietf:cite-as": "${params[6]}", + "type": "sorg:AboutPage", + "url": { + "id": "${params[7]}", + "mediaType": "${params[8]}", + "type": [ + "Article", + "sorg:ScholarlyArticle" + ] + } + }, + "origin": { + "id": "${params[0]}", + "inbox": "${params[1]}", + "type": "Service" + }, + "target": { + "id": "${params[3]}", + "inbox": "${params[4]}", + "type": "Service" + }, + "type": [ + "Offer", + "coar-notify:ReviewAction" + ] +} \ No newline at end of file diff --git a/dspace/config/local.cfg.EXAMPLE b/dspace/config/local.cfg.EXAMPLE index 7176ed275a51..79ae3b4838cb 100644 --- a/dspace/config/local.cfg.EXAMPLE +++ b/dspace/config/local.cfg.EXAMPLE @@ -76,20 +76,10 @@ dspace.name = DSpace at My University # URL for connecting to database db.url = jdbc:postgresql://localhost:5432/dspace -# JDBC Driver for PostgreSQL -db.driver = org.postgresql.Driver - -# PostgreSQL Database Dialect (for Hibernate) -db.dialect = org.hibernate.dialect.PostgreSQL94Dialect - # Database username and password db.username = dspace db.password = dspace -# Database Schema name -# For PostgreSQL, this is often "public" (default schema) -db.schema = public - ## Connection pool parameters # Maximum number of DB connections in pool (default = 30) @@ -240,4 +230,9 @@ db.schema = public #spring.servlet.multipart.max-file-size = 512MB # Maximum size of a multipart request (i.e. max total size of all files in one request) -#spring.servlet.multipart.max-request-size = 512MB \ No newline at end of file +#spring.servlet.multipart.max-request-size = 512MB + +######################## +# LDN INBOX SETTINGS # +######################## +ldn.enabled = true diff --git a/dspace/config/modules/contentreport.cfg b/dspace/config/modules/contentreport.cfg new file mode 100644 index 000000000000..afba0654fa85 --- /dev/null +++ b/dspace/config/modules/contentreport.cfg @@ -0,0 +1,10 @@ +#---------------------------------------------------------------# +#--------------CONTENT REPORTS CONFIGURATIONS-------------------# +#---------------------------------------------------------------# +# Used by dspace-server-webapp and the Angular UI # +# NOTE: This is currently a beta feature. # +#---------------------------------------------------------------# + +# Configuration setting to trigger showing/hiding the Content Reports +# (REST endpoints on the REST side and menu section on the Angular side) +#contentreport.enable=true diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg new file mode 100644 index 000000000000..88cfcdd59c83 --- /dev/null +++ b/dspace/config/modules/ldn.cfg @@ -0,0 +1,51 @@ +#---------------------------------------------------------------# +#---------------COAR NOTIFY LDN CONFIGURATION-------------------# +#---------------------------------------------------------------# +# Configuration properties used by Coar Notify and ldn # +#---------------------------------------------------------------# + + +# check on the IP number on incoming LDN Messages against the IP Range configured +# on the Notify Service known and found as the message sender +# ldn.ip-range.enabled = false + +#### LDN CONFIGURATION #### +# To enable the LDN service, set to true. +ldn.enabled = false + +#LDN message inbox endpoint +ldn.notify.inbox = ${dspace.server.url}/ldn/inbox + +# List the external services IDs for review/endorsement +# These IDs needs to be configured in the input-form.xml as well +# These IDs must contain only the hostname and the resource path +# Do not include any protocol +# Each IDs must match with the ID returned by the external service +# in the JSON-LD Actor field +#service.service-id.ldn = + +# LDN Queue extractor elaborates LDN Message entities of the queue +ldn.queue.extractor.cron = 0 0/5 * * * ? + +# LDN Queue timeout checks LDN Message Entities relation with the queue +ldn.queue.timeout.checker.cron = 0 0 */1 * * ? + +# LDN Queue extractor elaborates LDN Message entities with max_attempts < than ldn.processor.max.attempts +ldn.processor.max.attempts = 5 + +# LDN Queue extractor sets LDN Message Entity queue_timeout property every time it tryies a new elaboration +# of the message. LDN Message with a future queue_timeout is not elaborated. This property is used to calculateas: +# a new timeout, such as: new_timeout = now + ldn.processor.queue.msg.timeout (in minutes) +ldn.processor.queue.msg.timeout = 60 + +# Blocks the storage of incoming LDN messages with unknown Notify Service (origin) +ldn.notify.inbox.block-untrusted = true + +# Blocks the storage of incoming LDN messages with known Notify Service (origin) +# and out-of-range IP +ldn.notify.inbox.block-untrusted-ip = true + + +# EMAIL CONFIGURATION + +ldn.notification.email = ${mail.admin} diff --git a/dspace/config/modules/qaevents.cfg b/dspace/config/modules/qaevents.cfg index f117bc807f23..9008de06e7eb 100644 --- a/dspace/config/modules/qaevents.cfg +++ b/dspace/config/modules/qaevents.cfg @@ -20,6 +20,12 @@ qaevents.openaire.import.topic = ENRICH/MORE/PID qaevents.openaire.import.topic = ENRICH/MISSING/PROJECT # add more project suggestion qaevents.openaire.import.topic = ENRICH/MORE/PROJECT +# add more review +qaevents.openaire.import.topic = ENRICH/MORE/REVIEW +# add more endorsement +qaevents.openaire.import.topic = ENRICH/MORE/ENDORSEMENT +# add more release/relationship +qaevents.openaire.import.topic = ENRICH/MORE/LINK # The list of the supported pid href for the OPENAIRE events qaevents.openaire.pid-href-prefix.arxiv = https://arxiv.org/abs/ @@ -34,7 +40,7 @@ qaevents.openaire.pid-href-prefix.ncid = https://ci.nii.ac.jp/ncid/ qaevents.openaire.broker-url = http://api.openaire.eu/broker ###### QAEvent source Configuration ###### -qaevents.sources = OpenAIRE, DSpaceUsers +qaevents.sources = openaire, DSpaceUsers, coar-notify ### Withdrawal&Reinstate correction Group ### # Members of this group enabled to make requests for the Withdrawn or Reinstate of an item. diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 0e03d76f7d18..ef4f985f0d78 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -54,4 +54,7 @@ rest.properties.exposed = google.recaptcha.mode rest.properties.exposed = cc.license.jurisdiction rest.properties.exposed = identifiers.item-status.register-doi rest.properties.exposed = authentication-password.domain.valid -rest.properties.exposed = handle.canonical.prefix \ No newline at end of file +rest.properties.exposed = ldn.enabled +rest.properties.exposed = ldn.notify.inbox +rest.properties.exposed = handle.canonical.prefix +rest.properties.exposed = contentreport.enable diff --git a/dspace/config/registries/coar-types.xml b/dspace/config/registries/coar-types.xml new file mode 100644 index 000000000000..39c78e8f8c95 --- /dev/null +++ b/dspace/config/registries/coar-types.xml @@ -0,0 +1,61 @@ + + + + + + + + COAR fields definition + + + + coar + http://dspace.org/coar + + + + coar + notify + review + Reviewed by + + + + coar + notify + endorsement + Endorsement + + + + coar + notify + examination + Examination + + + + coar + notify + refused + Refused by + + + + coar + notify + release + Released by + + + + coar + notify + endorsedBy + Endorsed by + + + diff --git a/dspace/config/registries/datacite-types.xml b/dspace/config/registries/datacite-types.xml new file mode 100644 index 000000000000..b415ff0d2bcd --- /dev/null +++ b/dspace/config/registries/datacite-types.xml @@ -0,0 +1,57 @@ + + + + + + + + OpenAIRE4 Datacite fields definition + + + + datacite + http://datacite.org/schema/kernel-4 + + + + + datacite + geoLocation + Spatial region or named place where the data was gathered or about which the data is focused. + + + + + datacite + subject + fos + Fields of Science and Technology - OECD + + + + datacite + relation + isReviewedBy + Reviewd by + + + + datacite + relation + isReferencedBy + Referenced by + + + + datacite + relation + isSupplementedBy + Supplemented by + + + diff --git a/dspace/config/registries/openaire4-types.xml b/dspace/config/registries/openaire4-types.xml index b46802a46faa..e47e06e0aebf 100644 --- a/dspace/config/registries/openaire4-types.xml +++ b/dspace/config/registries/openaire4-types.xml @@ -15,11 +15,6 @@ oaire http://namespace.openaire.eu/schema/oaire/ - - - datacite - http://datacite.org/schema/kernel-4 - oaire @@ -107,21 +102,4 @@ The date when the conference took place. This property is considered to be part of the bibliographic citation. Recommended best practice for encoding the date value is defined in a profile of ISO 8601 [W3CDTF] and follows the YYYY-MM-DD format. - - - datacite - geoLocation - Spatial region or named place where the data was gathered or about which the data is focused. - - - - - datacite - subject - fos - Fields of Science and Technology - OECD - - diff --git a/dspace/config/spring/api/core-dao-services.xml b/dspace/config/spring/api/core-dao-services.xml index bc62a71c0364..54cfb2df3427 100644 --- a/dspace/config/spring/api/core-dao-services.xml +++ b/dspace/config/spring/api/core-dao-services.xml @@ -69,5 +69,10 @@ + + + + + diff --git a/dspace/config/spring/api/core-factory-services.xml b/dspace/config/spring/api/core-factory-services.xml index acfa0efe6d6c..2c6e60fc1c35 100644 --- a/dspace/config/spring/api/core-factory-services.xml +++ b/dspace/config/spring/api/core-factory-services.xml @@ -57,6 +57,10 @@ + + + + diff --git a/dspace/config/spring/api/core-services.xml b/dspace/config/spring/api/core-services.xml index 3f1f75d7824b..20b5bc74736d 100644 --- a/dspace/config/spring/api/core-services.xml +++ b/dspace/config/spring/api/core-services.xml @@ -77,6 +77,8 @@ + + @@ -152,6 +154,8 @@ + + diff --git a/dspace/config/spring/api/item-filters.xml b/dspace/config/spring/api/item-filters.xml index 1460c19fe423..677357a93e77 100644 --- a/dspace/config/spring/api/item-filters.xml +++ b/dspace/config/spring/api/item-filters.xml @@ -346,4 +346,12 @@ + + + + + + + diff --git a/dspace/config/spring/api/ldn-coar-notify.xml b/dspace/config/spring/api/ldn-coar-notify.xml new file mode 100644 index 000000000000..fcda57a10d77 --- /dev/null +++ b/dspace/config/spring/api/ldn-coar-notify.xml @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + Announce + coar-notify:ReviewAction + + + + + + + + Announce + coar-notify:EndorsementAction + + + + + + + + Accept + coar-notify:ReviewAction + + + + + + + + TentativeReject + coar-notify:ReviewAction + + + + + + + + Accept + coar-notify:EndorsementAction + + + + + + + + TentativeReject + coar-notify:EndorsementAction + + + + + + + + Accept + coar-notify:IngestAction + + + + + + + + TentativeReject + coar-notify:IngestAction + + + + + + + + Announce + coar-notify:RelationshipAction + + + + + + + + Offer + coar-notify:ReviewAction + + + + + + + + Offer + coar-notify:IngestAction + + + + + + + + Offer + coar-notify:EndorsementAction + + + + + + + + + + + + Offer + coar-notify:ReviewAction + + + + + + + + Offer + coar-notify:EndorsementAction + + + + + + + + Offer + coar-notify:IngestAction + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dspace/config/spring/api/qaevents.xml b/dspace/config/spring/api/qaevents.xml index 438178e4fa9c..74b72af61b31 100644 --- a/dspace/config/spring/api/qaevents.xml +++ b/dspace/config/spring/api/qaevents.xml @@ -2,10 +2,13 @@ + http://www.springframework.org/schema/context/spring-context-2.5.xsd + http://www.springframework.org/schema/util + http://www.springframework.org/schema/util/spring-util.xsd"> @@ -19,11 +22,30 @@ - - - - - + + + + + + + + + + + + + + + + + + + + + + + + @@ -53,6 +75,13 @@ + + + + + + + @@ -65,7 +94,44 @@ - + + + + + + + + + + + + + + + '{'!join from=search.resourceid to=resource_uuid fromIndex=${solr.multicorePrefix}search}submitter_authority:{0} + + + + + @@ -77,16 +143,17 @@ + - + - + original_id:{0} - + diff --git a/pom.xml b/pom.xml index a202b9fd6732..8c8fc8d98e46 100644 --- a/pom.xml +++ b/pom.xml @@ -19,12 +19,12 @@ 11 - 5.3.31 + 5.3.32 2.7.18 5.7.11 5.6.15.Final 6.2.5.Final - 42.6.0 + 42.7.2 8.11.2 3.10.8 @@ -37,7 +37,7 @@ 2.3.9 1.1.1 - 9.4.53.v20231009 + 9.4.54.v20240208 2.22.1 2.0.30 1.19.0 @@ -273,7 +273,6 @@ src/main/java ${root.basedir}/checkstyle.xml - ${project.build.sourceEncoding} true true @@ -366,6 +365,11 @@ false + + + https://docs.spring.io/spring-framework/docs/current/javadoc-api/ + https://docs.spring.io/spring-security/site/docs/current/api/ + @@ -1158,6 +1162,12 @@ ${spring.version} + + org.springframework + spring-context-support + ${spring.version} + + spring-tx org.springframework @@ -1260,7 +1270,7 @@ org.apache.james apache-mime4j-core - 0.8.9 + 0.8.10