Merged in CST-12178-coar-add-level-of-trust-score (pull request #1205)

CST-12178 coar add level of trust score

Approved-by: Andrea Bollini
This commit is contained in:
Francesco Bacchelli
2023-10-31 09:57:14 +00:00
committed by Andrea Bollini
11 changed files with 418 additions and 8 deletions

View File

@@ -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<Integer> {
@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<Integer> {
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public BigDecimal getScore() {
return score;
}
public void setScore(BigDecimal score) {
this.score = score;
}
}

View File

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

View File

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

View File

@@ -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<NotifyServiceEntity, N
return this;
}
public NotifyServiceBuilder withScore(BigDecimal score) {
notifyServiceEntity.setScore(score);
return this;
}
public NotifyServiceBuilder isEnabled(boolean enabled) {
notifyServiceEntity.setEnabled(enabled);
return this;

View File

@@ -38,6 +38,7 @@ public class NotifyServiceConverter implements DSpaceConverter<NotifyServiceEnti
notifyServiceRest.setUrl(obj.getUrl());
notifyServiceRest.setLdnUrl(obj.getLdnUrl());
notifyServiceRest.setEnabled(obj.isEnabled());
notifyServiceRest.setScore(obj.getScore());
if (obj.getInboundPatterns() != null) {
notifyServiceRest.setNotifyServiceInboundPatterns(

View File

@@ -7,11 +7,13 @@
*/
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;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* The NotifyServiceEntity REST Resource
*
@@ -27,6 +29,7 @@ public class NotifyServiceRest extends BaseObjectRest<Integer> {
private String url;
private String ldnUrl;
private boolean enabled;
private BigDecimal score;
private List<NotifyServiceInboundPatternRest> notifyServiceInboundPatterns;
private List<NotifyServiceOutboundPatternRest> notifyServiceOutboundPatterns;
@@ -102,4 +105,14 @@ public class NotifyServiceRest extends BaseObjectRest<Integer> {
List<NotifyServiceOutboundPatternRest> notifyServiceOutboundPatterns) {
this.notifyServiceOutboundPatterns = notifyServiceOutboundPatterns;
}
public BigDecimal getScore() {
return score;
}
public void setScore(BigDecimal score) {
this.score = score;
}
}

View File

@@ -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;
@@ -15,6 +17,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;
@@ -35,8 +38,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
@@ -96,12 +101,21 @@ public class NotifyServiceRestRepository extends DSpaceRestRepository<NotifyServ
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()));
}
}
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());
if (notifyServiceRest.getNotifyServiceInboundPatterns() != null) {
appendNotifyServiceInboundPatterns(context, notifyServiceEntity,
notifyServiceRest.getNotifyServiceInboundPatterns());
@@ -111,8 +125,8 @@ public class NotifyServiceRestRepository extends DSpaceRestRepository<NotifyServ
appendNotifyServiceOutboundPatterns(context, notifyServiceEntity,
notifyServiceRest.getNotifyServiceOutboundPatterns());
}
notifyServiceEntity.setScore(notifyServiceRest.getScore());
notifyServiceEntity.setEnabled(notifyServiceRest.isEnabled());
notifyService.update(context, notifyServiceEntity);
return converter.toRest(notifyServiceEntity, utils.obtainProjection());

View File

@@ -0,0 +1,90 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.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: <code>
* 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"
* }]'
* </code>
*/
@Component
public class NotifyServiceScoreAddOperation extends PatchOperation<NotifyServiceEntity> {
@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));
}
}

View File

@@ -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: <code>
* curl -X PATCH http://${dspace.server.url}/api/ldn/ldnservices/<:id-notifyService> -H "
* Content-Type: application/json" -d '
* [{
* "op": "remove",
* "path": "/score"
* }]'
* </code>
*/
@Component
public class NotifyServiceScoreRemoveOperation extends PatchOperation<NotifyServiceEntity> {
@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));
}
}

View File

@@ -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: <code>
* 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"
* }]'
* </code>
*/
@Component
public class NotifyServiceScoreReplaceOperation extends PatchOperation<NotifyServiceEntity> {
@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));
}
}

View File

@@ -12,11 +12,13 @@ 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.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;
@@ -24,12 +26,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;
@@ -44,6 +47,8 @@ import org.dspace.app.rest.test.AbstractControllerIntegrationTest;
import org.dspace.builder.NotifyServiceBuilder;
import org.junit.Test;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Integration test class for {@link NotifyServiceRestRepository}.
*
@@ -145,6 +150,41 @@ public class NotifyServiceRestRepositoryIT extends AbstractControllerIntegration
.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);
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<Integer> idRef = new AtomicReference<Integer>();
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 {
@@ -3249,7 +3289,37 @@ 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)
.build();
context.restoreAuthSystemState();
List<Operation> ops = new ArrayList<Operation>();
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", "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 =
@@ -3258,21 +3328,57 @@ public class NotifyServiceRestRepositoryIT extends AbstractControllerIntegration
.withDescription("service description")
.withUrl("service url")
.withLdnUrl("service ldn url")
.withScore(BigDecimal.ZERO)
.build();
context.restoreAuthSystemState();
List<Operation> ops = new ArrayList<Operation>();
ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/enabled", "test");
ReplaceOperation inboundReplaceOperation = new ReplaceOperation("/score", "10");
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().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<Operation> ops = new ArrayList<Operation>();
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)))
;
}
}