diff --git a/dspace-api/src/main/java/org/dspace/app/ldn/LDNBusinessDelegate.java b/dspace-api/src/main/java/org/dspace/app/ldn/LDNBusinessDelegate.java index 6890d8b0fb..8fa38645b9 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/LDNBusinessDelegate.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/LDNBusinessDelegate.java @@ -87,7 +87,7 @@ public class LDNBusinessDelegate { String dspaceServerUrl = configurationService.getProperty("dspace.server.url"); String dspaceUIUrl = configurationService.getProperty("dspace.ui.url"); String dspaceName = configurationService.getProperty("dspace.name"); - String dspaceLdnInboxUrl = configurationService.getProperty("ldn.notify.local-inbox-endpoint"); + String dspaceLdnInboxUrl = configurationService.getProperty("ldn.notify.inbox"); log.info("DSpace Server URL {}", dspaceServerUrl); log.info("DSpace UI URL {}", dspaceUIUrl); 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 index 9a7e9c7caf..e15267b480 100644 --- a/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java +++ b/dspace-api/src/main/java/org/dspace/app/ldn/NotifyServiceEntity.java @@ -7,6 +7,7 @@ */ package org.dspace.app.ldn; +import java.math.BigDecimal; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; @@ -56,6 +57,9 @@ public class NotifyServiceEntity implements ReloadableEntity { @Column(name = "enabled") private boolean enabled = false; + @Column(name = "score") + private BigDecimal score; + public void setId(Integer id) { this.id = id; } @@ -129,4 +133,12 @@ public class NotifyServiceEntity implements ReloadableEntity { public void setEnabled(boolean enabled) { this.enabled = enabled; } + + public BigDecimal getScore() { + return score; + } + + public void setScore(BigDecimal score) { + this.score = score; + } } diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2023.10.13__add_score_column_notifyservices_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2023.10.13__add_score_column_notifyservices_table.sql new file mode 100644 index 0000000000..2be6684f9c --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/h2/V8.0_2023.10.13__add_score_column_notifyservices_table.sql @@ -0,0 +1,13 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- edit notifyservice table add score column +----------------------------------------------------------------------------------- + +ALTER TABLE notifyservice ADD COLUMN score NUMERIC(6, 5); \ No newline at end of file diff --git a/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2023.10.13__add_score_column_notifyservices_table.sql b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2023.10.13__add_score_column_notifyservices_table.sql new file mode 100644 index 0000000000..2be6684f9c --- /dev/null +++ b/dspace-api/src/main/resources/org/dspace/storage/rdbms/sqlmigration/postgres/V8.0_2023.10.13__add_score_column_notifyservices_table.sql @@ -0,0 +1,13 @@ +-- +-- The contents of this file are subject to the license and copyright +-- detailed in the LICENSE and NOTICE files at the root of the source +-- tree and available online at +-- +-- http://www.dspace.org/license/ +-- + +----------------------------------------------------------------------------------- +-- edit notifyservice table add score column +----------------------------------------------------------------------------------- + +ALTER TABLE notifyservice ADD COLUMN score NUMERIC(6, 5); \ No newline at end of file diff --git a/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java index c9e1b4825f..a7886ebe51 100644 --- a/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java +++ b/dspace-api/src/test/java/org/dspace/builder/NotifyServiceBuilder.java @@ -7,6 +7,7 @@ */ package org.dspace.builder; +import java.math.BigDecimal; import java.sql.SQLException; import org.apache.logging.log4j.LogManager; @@ -125,6 +126,11 @@ public class NotifyServiceBuilder extends AbstractBuilder { private String url; private String ldnUrl; private boolean enabled; + private BigDecimal score; private List notifyServiceInboundPatterns; private List notifyServiceOutboundPatterns; @@ -102,4 +105,14 @@ public class NotifyServiceRest extends BaseObjectRest { List notifyServiceOutboundPatterns) { this.notifyServiceOutboundPatterns = notifyServiceOutboundPatterns; } + + public BigDecimal getScore() { + return score; + } + + public void setScore(BigDecimal score) { + this.score = score; + } + + } 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 index 28a21725ab..f966e2997e 100644 --- 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 @@ -7,6 +7,8 @@ */ package org.dspace.app.rest.repository; +import static java.lang.String.format; + import java.io.IOException; import java.sql.SQLException; import java.util.ArrayList; @@ -16,6 +18,7 @@ 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.NotifyServiceOutboundPattern; @@ -36,8 +39,10 @@ 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.http.HttpStatus; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; /** * This is the repository responsible to manage NotifyService Rest object @@ -97,12 +102,21 @@ public class NotifyServiceRestRepository extends DSpaceRestRepository + * 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 0000000000..e26d888e9f --- /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 0000000000..21ecc7db1b --- /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.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]", (String)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/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java b/dspace-server-webapp/src/test/java/org/dspace/app/rest/NotifyServiceRestRepositoryIT.java index 77d3583d73..c99d93631c 100644 --- 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 @@ -12,12 +12,14 @@ 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; @@ -25,12 +27,13 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder 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.concurrent.atomic.AtomicReference; + import javax.ws.rs.core.MediaType; -import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.RandomUtils; import org.dspace.app.ldn.NotifyServiceEntity; import org.dspace.app.rest.model.NotifyServiceInboundPatternRest; @@ -46,6 +49,8 @@ import org.dspace.builder.NotifyServiceBuilder; import org.dspace.builder.NotifyServiceInboundPatternBuilder; import org.junit.Test; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Integration test class for {@link NotifyServiceRestRepository}. * @@ -148,6 +153,41 @@ public class NotifyServiceRestRepositoryIT extends AbstractControllerIntegration .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); + + NotifyServiceOutboundPatternRest outboundPatternRest = new NotifyServiceOutboundPatternRest(); + outboundPatternRest.setPattern("patternC"); + outboundPatternRest.setConstraint("itemFilterC"); + + 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.setNotifyServiceOutboundPatterns(List.of(outboundPatternRest)); + 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(); @@ -3342,30 +3382,98 @@ public class NotifyServiceRestRepositoryIT extends AbstractControllerIntegration } @Test - public void NotifyServiceStatusReplaceOperationTestBadRequestTest() throws Exception { - + public void NotifyServiceScoreReplaceOperationTest() 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) .withUrl("https://service.ldn.org/about") .withLdnUrl("https://service.ldn.org/inbox") .build(); context.restoreAuthSystemState(); List ops = new ArrayList(); - ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/enabled", "test"); + ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/score", "0.522"); ops.add(inboundReplaceOperation); String patchBody = getPatchContent(ops); String authToken = getAuthToken(admin.getEmail(), password); - // patch not boolean value getClient(authToken) .perform(patch("/api/ldn/ldnservices/" + notifyServiceEntity.getID()) .content(patchBody) .contentType(MediaType.APPLICATION_JSON_PATCH_JSON)) - .andExpect(status().isBadRequest()); + .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(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))) + ; + } + + } \ No newline at end of file diff --git a/dspace/config/modules/ldn.cfg b/dspace/config/modules/ldn.cfg index a58ba4a0b7..688a337edf 100644 --- a/dspace/config/modules/ldn.cfg +++ b/dspace/config/modules/ldn.cfg @@ -2,9 +2,8 @@ # To enable the LDN service, set to true. ldn.enabled = true - -ldn.notify.local-inbox-endpoint = ${dspace.server.url}/ldn/inbox - +#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 diff --git a/dspace/config/modules/rest.cfg b/dspace/config/modules/rest.cfg index 846d587b31..f349c40e7b 100644 --- a/dspace/config/modules/rest.cfg +++ b/dspace/config/modules/rest.cfg @@ -53,6 +53,7 @@ rest.properties.exposed = cc.license.jurisdiction rest.properties.exposed = identifiers.item-status.register-doi rest.properties.exposed = authentication-password.domain.valid rest.properties.exposed = coar-notify.enabled +rest.properties.exposed = ldn.notify.inbox #---------------------------------------------------------------# # These configs are used by the deprecated REST (v4-6) module #