[CST-10629] Defined the storage layer of the notify feature

This commit is contained in:
eskander
2023-08-24 16:55:24 +03:00
parent 81b22bafac
commit 1c527f1bd2
15 changed files with 499 additions and 27 deletions

View File

@@ -0,0 +1,130 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in 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.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.dspace.content.DSpaceObject;
import org.dspace.core.ReloadableEntity;
/**
* Class representing ldnMessages stored in the DSpace system.
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
*/
@Entity
@Table(name = "ldn_messages")
public class LDNMessage implements ReloadableEntity<String> {
@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;
@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 LDNMessage inReplyTo;
@ManyToOne
@JoinColumn(name = "context", referencedColumnName = "uuid")
private DSpaceObject context;
protected LDNMessage() {
}
protected LDNMessage(String id) {
this.id = id;
}
@Override
public String getID() {
return id;
}
public void setId(String id) {
this.id = id;
}
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 NotifyServiceEntity getOrigin() {
return origin;
}
public void setOrigin(NotifyServiceEntity origin) {
this.origin = origin;
}
public NotifyServiceEntity getTarget() {
return target;
}
public void setTarget(NotifyServiceEntity target) {
this.target = target;
}
public LDNMessage getInReplyTo() {
return inReplyTo;
}
public void setInReplyTo(LDNMessage inReplyTo) {
this.inReplyTo = inReplyTo;
}
public DSpaceObject getContext() {
return context;
}
public void setContext(DSpaceObject context) {
this.context = context;
}
}

View File

@@ -8,24 +8,101 @@
package org.dspace.app.ldn;
import java.sql.SQLException;
import java.util.UUID;
import com.google.gson.Gson;
import org.apache.commons.lang3.StringUtils;
import org.dspace.app.ldn.dao.LDNMessageDao;
import org.dspace.app.ldn.model.Notification;
import org.dspace.app.ldn.model.Service;
import org.dspace.app.ldn.service.LDNMessageService;
import org.dspace.app.ldn.service.NotifyService;
import org.dspace.content.DSpaceObject;
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.com)
* @author Mohamed Eskander (mohamed.eskander at 4science dot it)
*/
public class LDNMessageServiceImpl implements LDNMessageService {
@Autowired(required = true)
private LDNMessageDao ldnMessageDao;
@Autowired(required = true)
private NotifyService notifyService;
@Autowired(required = true)
private ConfigurationService configurationService;
@Autowired(required = true)
private HandleService handleService;
@Autowired(required = true)
private ItemService itemService;
protected LDNMessageServiceImpl() {
}
@Override
public void create(Context context, String id) throws SQLException {
public LDNMessage find(Context context, String id) throws SQLException {
return ldnMessageDao.findByID(context, LDNMessage.class, id);
}
@Override
public LDNMessage create(Context context, String id) throws SQLException {
return ldnMessageDao.create(context, new LDNMessage(id));
}
@Override
public LDNMessage create(Context context, Notification notification) throws SQLException {
LDNMessage ldnMessage = create(context, notification.getId());
ldnMessage.setObject(findDspaceObjectByUrl(context, notification.getId()));
if (null != notification.getContext()) {
ldnMessage.setContext(findDspaceObjectByUrl(context, notification.getContext().getId()));
}
ldnMessage.setOrigin(findNotifyService(context, notification.getOrigin()));
ldnMessage.setTarget(findNotifyService(context, notification.getTarget()));
ldnMessage.setInReplyTo(find(context, notification.getInReplyTo()));
ldnMessage.setMessage(new Gson().toJson(notification));
ldnMessage.setType(StringUtils.joinWith(",", notification.getType()));
update(context, ldnMessage);
return ldnMessage;
}
@Override
public void update(Context context, LDNMessage ldnMessage) throws SQLException {
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;
}
private NotifyServiceEntity findNotifyService(Context context, Service service) throws SQLException {
return notifyService.findByLdnUrl(context, service.getInbox());
}
}

View File

@@ -0,0 +1,23 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in 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 org.dspace.app.ldn.LDNMessage;
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<LDNMessage> {
}

View File

@@ -0,0 +1,23 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in 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 org.dspace.app.ldn.LDNMessage;
import org.dspace.app.ldn.dao.LDNMessageDao;
import org.dspace.core.AbstractHibernateDAO;
/**
* 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<LDNMessage> implements LDNMessageDao {
}

View File

@@ -9,15 +9,53 @@ package org.dspace.app.ldn.service;
import java.sql.SQLException;
import org.dspace.app.ldn.LDNMessage;
import org.dspace.app.ldn.model.Notification;
import org.dspace.core.Context;
/**
* Service interface class for the {@link LDNMessage} object.
*
* @author Mohamed Eskander (mohamed.eskander at 4science.com)
* @author Mohamed Eskander (mohamed.eskander at 4science dot it)
*/
public interface LDNMessageService {
public void create(Context context, String id) throws SQLException;
/**
* 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 LDNMessage find(Context context, String id) 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 LDNMessage create(Context context, String id) throws SQLException;
/**
* Creates a new LDNMessage
*
* @param context The DSpace context
* @param notification the requested notification
* @return the created LDN Message
* @throws SQLException If something goes wrong in the database
*/
public LDNMessage create(Context context, Notification notification) 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, LDNMessage ldnMessage) throws SQLException;
}

View File

@@ -102,6 +102,16 @@ public abstract class AbstractHibernateDAO<T> implements GenericDAO<T> {
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<T> findMany(Context context, String query) throws SQLException {
@SuppressWarnings("unchecked")

View File

@@ -102,6 +102,17 @@ public interface GenericDAO<T> {
*/
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.
*

View File

@@ -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/
--
-------------------------------------------------------------------------------
-- Table to store LDN messages
-------------------------------------------------------------------------------
CREATE TABLE ldn_messages
(
id VARCHAR(255) PRIMARY KEY,
object uuid,
message TEXT,
type VARCHAR(255),
origin INTEGER,
target INTEGER,
inReplyTo VARCHAR(255),
context uuid,
FOREIGN KEY (object) REFERENCES dspaceobject (uuid) ON DELETE SET NULL,
FOREIGN KEY (context) REFERENCES dspaceobject (uuid) ON DELETE SET NULL,
FOREIGN KEY (origin) REFERENCES notifyservices (id) ON DELETE SET NULL,
FOREIGN KEY (target) REFERENCES notifyservices (id) ON DELETE SET NULL,
FOREIGN KEY (inReplyTo) REFERENCES ldn_messages (id) ON DELETE SET NULL
);

View File

@@ -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/
--
-------------------------------------------------------------------------------
-- Table to store LDN messages
-------------------------------------------------------------------------------
CREATE TABLE ldn_messages
(
id VARCHAR(255) PRIMARY KEY,
object uuid,
message TEXT,
type VARCHAR(255),
origin INTEGER,
target INTEGER,
inReplyTo VARCHAR(255),
context uuid,
FOREIGN KEY (object) REFERENCES dspaceobject (uuid) ON DELETE SET NULL,
FOREIGN KEY (context) REFERENCES dspaceobject (uuid) ON DELETE SET NULL,
FOREIGN KEY (origin) REFERENCES notifyservices (id) ON DELETE SET NULL,
FOREIGN KEY (target) REFERENCES notifyservices (id) ON DELETE SET NULL,
FOREIGN KEY (inReplyTo) REFERENCES ldn_messages (id) ON DELETE SET NULL
);

View File

@@ -7,17 +7,17 @@
*/
package org.dspace.app.rest;
import java.net.URI;
import java.util.regex.Pattern;
import org.apache.commons.validator.routines.UrlValidator;
import org.apache.logging.log4j.Logger;
import org.dspace.app.ldn.LDNRouter;
import org.dspace.app.ldn.model.Notification;
import org.dspace.app.ldn.service.LDNMessageService;
import org.dspace.app.rest.exception.InvalidLDNMessageException;
import org.dspace.core.Context;
import org.dspace.web.ContextUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
@@ -35,10 +35,6 @@ public class LDNInboxController {
private static final Logger log = org.apache.logging.log4j.LogManager.getLogger();
@Lazy
@Autowired
private LDNRouter router;
@Autowired
private LDNMessageService ldnMessageService;
@@ -46,24 +42,19 @@ public class LDNInboxController {
* LDN DSpace inbox.
*
* @param notification received notification
* @return ResponseEntity 400 not stored, 201 stored
* @return ResponseEntity 400 not stored, 202 stored
* @throws Exception
*/
@PostMapping(value = "/inbox", consumes = "application/ld+json")
public ResponseEntity<Object> inbox(@RequestBody Notification notification) throws Exception {
Context context = ContextUtil.obtainCurrentRequestContext();
ldnMessageService.create(context, notification.getId());
log.info("stored notification {} {}",
notification.getId(),
notification.getType());
URI target = new URI(notification.getTarget().getInbox());
return ResponseEntity.created(target)
validate(notification);
ldnMessageService.create(context, notification);
log.info("stored notification {} {}", notification.getId(), notification.getType());
context.commit();
return ResponseEntity.accepted()
.body(String.format("Successfully stored notification %s %s",
notification.getId(), notification.getType()));
notification.getId(), notification.getType()));
}
/**
@@ -89,4 +80,18 @@ public class LDNInboxController {
.body(e.getMessage());
}
private void validate(Notification notification) {
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");
}
}
}

View File

@@ -92,7 +92,7 @@ public class DSpaceApiExceptionControllerAdvice extends ResponseEntityExceptionH
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);

View File

@@ -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);
}
}

View File

@@ -11,12 +11,32 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.builder.CollectionBuilder;
import org.dspace.builder.CommunityBuilder;
import org.dspace.builder.ItemBuilder;
import org.dspace.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;
public class LDNInboxControllerIT extends AbstractControllerIntegrationTest {
@Autowired
private ConfigurationService configurationService;
@Test
public void ldnInboxEndorsementActionTest() 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();
context.restoreAuthSystemState();
String message = "{\n" +
" \"@context\": [\n" +
" \"https://www.w3.org/ns/activitystreams\",\n" +
@@ -29,7 +49,7 @@ public class LDNInboxControllerIT extends AbstractControllerIntegrationTest {
" },\n" +
" \"id\": \"urn:uuid:0370c0fb-bb78-4a9b-87f5-bed307a509dd\",\n" +
" \"object\": {\n" +
" \"id\": \"https://research-organisation.org/repository/preprint/201203/421/\",\n" +
" \"id\": \"" + object + "\",\n" +
" \"ietf:cite-as\": \"https://doi.org/10.5555/12345680\",\n" +
" \"type\": \"sorg:AboutPage\",\n" +
" \"url\": {\n" +
@@ -61,7 +81,7 @@ public class LDNInboxControllerIT extends AbstractControllerIntegrationTest {
.perform(post("/ldn/inbox")
.contentType("application/ld+json")
.content(message))
.andExpect(status().isCreated());
.andExpect(status().isAccepted());
}
@Test
@@ -119,7 +139,57 @@ public class LDNInboxControllerIT extends AbstractControllerIntegrationTest {
.perform(post("/ldn/inbox")
.contentType("application/ld+json")
.content(message))
.andExpect(status().isCreated());
.andExpect(status().isAccepted());
}
@Test
public void ldnInboxEndorsementActionBadRequestTest() throws Exception {
// id is not an uri
String message = "{\n" +
" \"@context\": [\n" +
" \"https://www.w3.org/ns/activitystreams\",\n" +
" \"https://purl.org/coar/notify\"\n" +
" ],\n" +
" \"actor\": {\n" +
" \"id\": \"https://orcid.org/0000-0002-1825-0097\",\n" +
" \"name\": \"Josiah Carberry\",\n" +
" \"type\": \"Person\"\n" +
" },\n" +
" \"id\": \"123456789\",\n" +
" \"object\": {\n" +
" \"id\": \"https://overlay-journal.com/articles/00001/\",\n" +
" \"ietf:cite-as\": \"https://doi.org/10.5555/12345680\",\n" +
" \"type\": \"sorg:AboutPage\",\n" +
" \"url\": {\n" +
" \"id\": \"https://research-organisation.org/repository/preprint/201203/421/content.pdf\",\n" +
" \"mediaType\": \"application/pdf\",\n" +
" \"type\": [\n" +
" \"Article\",\n" +
" \"sorg:ScholarlyArticle\"\n" +
" ]\n" +
" }\n" +
" },\n" +
" \"origin\": {\n" +
" \"id\": \"https://research-organisation.org/repository\",\n" +
" \"inbox\": \"https://research-organisation.org/inbox/\",\n" +
" \"type\": \"Service\"\n" +
" },\n" +
" \"target\": {\n" +
" \"id\": \"https://overlay-journal.com/system\",\n" +
" \"inbox\": \"https://overlay-journal.com/inbox/\",\n" +
" \"type\": \"Service\"\n" +
" },\n" +
" \"type\": [\n" +
" \"Offer\",\n" +
" \"coar-notify:EndorsementAction\"\n" +
" ]\n" +
"}";
getClient(getAuthToken(admin.getEmail(), password))
.perform(post("/ldn/inbox")
.contentType("application/ld+json")
.content(message))
.andExpect(status().isBadRequest());
}
}

View File

@@ -100,5 +100,7 @@
<mapping class="org.dspace.app.ldn.NotifyServiceInboundPattern"/>
<mapping class="org.dspace.app.ldn.NotifyServiceOutboundPattern"/>
<mapping class="org.dspace.app.ldn.LDNMessage"/>
</session-factory>
</hibernate-configuration>

View File

@@ -72,5 +72,6 @@
<bean class="org.dspace.app.ldn.dao.impl.NotifyServiceDaoImpl"/>
<bean class="org.dspace.app.ldn.dao.impl.NotifyServiceInboundPatternDaoImpl"/>
<bean class="org.dspace.app.ldn.dao.impl.NotifyServiceOutboundPatternDaoImpl"/>
<bean class="org.dspace.app.ldn.dao.impl.LDNMessageDaoImpl"/>
</beans>