From e1284286763c32ac679727a2b061f2e37b77f16e Mon Sep 17 00:00:00 2001 From: southeo Date: Thu, 1 Aug 2024 19:49:30 +0200 Subject: [PATCH 1/2] use remote schema --- pom.xml | 1 + .../component/SchemaValidatorComponent.java | 39 +-- .../SchemaValidatorConfiguration.java | 2 +- .../controller/AnnotationController.java | 7 +- .../batch/AnnotationEventRequest.java | 4 +- .../backend/service/AnnotationService.java | 59 ++--- .../annotation-processing-request.json | 85 +++++++ .../json-schema/annotation-request.json | 237 ------------------ .../resources/json-schema/annotation.json | 191 +------------- .../SchemaValidatorComponentTest.java | 93 ++----- .../controller/AnnotationControllerTest.java | 4 +- .../dissco/backend/utils/AnnotationUtils.java | 42 ++-- 12 files changed, 160 insertions(+), 604 deletions(-) create mode 100644 src/main/resources/json-schema/annotation-processing-request.json delete mode 100644 src/main/resources/json-schema/annotation-request.json diff --git a/pom.xml b/pom.xml index da5972f7..3fc032b8 100644 --- a/pom.xml +++ b/pom.xml @@ -260,6 +260,7 @@ https://schemas.dissco.tech/schemas/fdo-type/digital-media/0.3.0/digital-media.json https://schemas.dissco.tech/schemas/fdo-type/machine-annotation-service/0.3.0/machine-annotation-service.json https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation.json + https://schemas.dissco.tech/schemas/developer-schema/annotation/0.3.0/annotation-processing-request.json diff --git a/src/main/java/eu/dissco/backend/component/SchemaValidatorComponent.java b/src/main/java/eu/dissco/backend/component/SchemaValidatorComponent.java index d4c20bba..bc92457f 100644 --- a/src/main/java/eu/dissco/backend/component/SchemaValidatorComponent.java +++ b/src/main/java/eu/dissco/backend/component/SchemaValidatorComponent.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.networknt.schema.JsonSchema; +import eu.dissco.backend.domain.annotation.batch.AnnotationEvent; import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; import eu.dissco.backend.exceptions.InvalidAnnotationRequestException; -import eu.dissco.backend.schema.AnnotationRequest; +import eu.dissco.backend.schema.AnnotationProcessingRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -14,21 +15,6 @@ @RequiredArgsConstructor public class SchemaValidatorComponent { - private final JsonSchema annotationSchema; - private final ObjectMapper mapper; - - public void validateAnnotationRequest(AnnotationRequest annotationRequest, Boolean isNew) - throws InvalidAnnotationRequestException { - validateId(annotationRequest, isNew); - var annotationRequestNode = mapper.valueToTree(annotationRequest); - var errors = annotationSchema.validate(annotationRequestNode); - if (errors.isEmpty()) { - return; - } - log.error("Invalid annotationRequests request. Errors {}", errors); - throw new InvalidAnnotationRequestException(errors.toString()); - } - public void validateAnnotationEventRequest(AnnotationEventRequest event, boolean isNew) throws InvalidAnnotationRequestException { if (event.annotationRequests().size() != 1 || event.batchMetadata().size() != 1 @@ -42,20 +28,21 @@ public void validateAnnotationEventRequest(AnnotationEventRequest event, boolean throw new InvalidAnnotationRequestException( "Event can only contain: 1 annotationRequests, 1 batch metadata, and minimum 1 search param"); } - validateAnnotationRequest(event.annotationRequests().get(0), isNew); + validateId(event, isNew); } - void validateId(AnnotationRequest annotation, Boolean isNew) + private void validateId(AnnotationEventRequest eventRequest, Boolean isNew) throws InvalidAnnotationRequestException { - if (Boolean.TRUE.equals(isNew) && annotation.getOdsID() != null) { - throw new InvalidAnnotationRequestException( - "Attempting overwrite annotationRequests with \"ods:id\" " + annotation.getOdsID()); + for (var annotation : eventRequest.annotationRequests() ){ + if (Boolean.TRUE.equals(isNew) && annotation.getOdsID() != null) { + throw new InvalidAnnotationRequestException( + "Attempting overwrite annotationRequests with \"ods:id\" " + annotation.getOdsID()); + } + if (Boolean.FALSE.equals(isNew) && annotation.getOdsID() == null) { + throw new InvalidAnnotationRequestException( + "\"ods:id\" not provided for annotationRequests update"); + } } - if (Boolean.FALSE.equals(isNew) && annotation.getOdsID() == null) { - throw new InvalidAnnotationRequestException( - "\"ods:id\" not provided for annotationRequests update"); } - } - } diff --git a/src/main/java/eu/dissco/backend/configuration/SchemaValidatorConfiguration.java b/src/main/java/eu/dissco/backend/configuration/SchemaValidatorConfiguration.java index 39acf214..eacb8124 100644 --- a/src/main/java/eu/dissco/backend/configuration/SchemaValidatorConfiguration.java +++ b/src/main/java/eu/dissco/backend/configuration/SchemaValidatorConfiguration.java @@ -15,7 +15,7 @@ public class SchemaValidatorConfiguration { public JsonSchema annotationSchema() throws IOException { var factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); try (InputStream inputStream = Thread.currentThread().getContextClassLoader() - .getResourceAsStream("json-schema/annotation-request.json")) { + .getResourceAsStream("json-schema/annotation-processing-request.json")) { return factory.getSchema(inputStream); } } diff --git a/src/main/java/eu/dissco/backend/controller/AnnotationController.java b/src/main/java/eu/dissco/backend/controller/AnnotationController.java index 755fb395..6dfccad1 100644 --- a/src/main/java/eu/dissco/backend/controller/AnnotationController.java +++ b/src/main/java/eu/dissco/backend/controller/AnnotationController.java @@ -13,7 +13,7 @@ import eu.dissco.backend.exceptions.NoAnnotationFoundException; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.properties.ApplicationProperties; -import eu.dissco.backend.schema.AnnotationRequest; +import eu.dissco.backend.schema.AnnotationProcessingRequest; import eu.dissco.backend.service.AnnotationService; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; @@ -103,7 +103,6 @@ public ResponseEntity createAnnotation(Authentication authentica @RequestBody JsonApiRequestWrapper requestBody, HttpServletRequest request) throws JsonProcessingException, ForbiddenException, InvalidAnnotationRequestException { var annotation = getAnnotationFromRequest(requestBody); - schemaValidator.validateAnnotationRequest(annotation, true); var userId = authentication.getName(); log.info("Received new annotationRequests from user: {}", userId); var annotationResponse = service.persistAnnotation(annotation, userId, getPath(request)); @@ -200,13 +199,13 @@ public ResponseEntity deleteAnnotation(Authentication authentication, } } - private AnnotationRequest getAnnotationFromRequest(JsonApiRequestWrapper requestBody) + private AnnotationProcessingRequest getAnnotationFromRequest(JsonApiRequestWrapper requestBody) throws JsonProcessingException { if (!requestBody.data().type().equals(ANNOTATION_TYPE)) { throw new IllegalArgumentException( "Invalid type. Type must be " + ANNOTATION_TYPE + " but was " + requestBody.data().type()); } - return mapper.treeToValue(requestBody.data().attributes(), AnnotationRequest.class); + return mapper.treeToValue(requestBody.data().attributes(), AnnotationProcessingRequest.class); } private AnnotationEventRequest getAnnotationFromRequestEvent(JsonApiRequestWrapper requestBody) diff --git a/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java b/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java index b0b850ad..d37bb342 100644 --- a/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java +++ b/src/main/java/eu/dissco/backend/domain/annotation/batch/AnnotationEventRequest.java @@ -1,9 +1,9 @@ package eu.dissco.backend.domain.annotation.batch; -import eu.dissco.backend.schema.AnnotationRequest; +import eu.dissco.backend.schema.AnnotationProcessingRequest; import java.util.List; -public record AnnotationEventRequest(List annotationRequests, +public record AnnotationEventRequest(List annotationRequests, List batchMetadata) { } diff --git a/src/main/java/eu/dissco/backend/service/AnnotationService.java b/src/main/java/eu/dissco/backend/service/AnnotationService.java index 55fee0b3..fb8c0246 100644 --- a/src/main/java/eu/dissco/backend/service/AnnotationService.java +++ b/src/main/java/eu/dissco/backend/service/AnnotationService.java @@ -27,12 +27,7 @@ import eu.dissco.backend.schema.Agent.Type; import eu.dissco.backend.schema.Annotation; import eu.dissco.backend.schema.Annotation.OaMotivation; -import eu.dissco.backend.schema.AnnotationRequest; -import eu.dissco.backend.schema.OaHasBody; -import eu.dissco.backend.schema.OaHasBody__1; -import eu.dissco.backend.schema.OaHasSelector__1; -import eu.dissco.backend.schema.OaHasTarget; -import eu.dissco.backend.schema.OaHasTarget__1; +import eu.dissco.backend.schema.AnnotationProcessingRequest; import java.io.IOException; import java.time.Instant; import java.util.ArrayList; @@ -61,15 +56,6 @@ public class AnnotationService { private final UserService userService; private final ObjectMapper mapper; - private static OaHasSelector__1 buildSelector(OaHasTarget oaHasTarget) { - var selector = new OaHasSelector__1(); - for (var entry : oaHasTarget.getOaHasSelector() - .getAdditionalProperties().entrySet()) { - selector.setAdditionalProperty(entry.getKey(), entry.getValue()); - } - return selector; - } - public JsonApiWrapper getAnnotation(String id, String path) { var annotation = repository.getAnnotation(id); var dataNode = new JsonApiData(id, ANNOTATION, mapper.valueToTree(annotation)); @@ -97,10 +83,10 @@ public JsonApiListResponseWrapper getAnnotations(int pageNumber, int pageSize, return wrapListResponse(annotationsPlusOne, pageNumber, pageSize, path); } - public JsonApiWrapper persistAnnotation(AnnotationRequest annotationRequest, String userId, + public JsonApiWrapper persistAnnotation(AnnotationProcessingRequest annotationProcessingRequest, String userId, String path) throws ForbiddenException, JsonProcessingException { var user = getUserInformation(userId); - var annotation = buildAnnotation(annotationRequest, user, false); + var annotation = buildAnnotation(annotationProcessingRequest, user, false); var response = annotationClient.postAnnotation(annotation); return formatResponse(response, path); } @@ -166,55 +152,38 @@ private User getUserInformation(String userId) throws ForbiddenException { return user; } - private Annotation buildAnnotation(AnnotationRequest annotationRequest, User user, + private Annotation buildAnnotation(AnnotationProcessingRequest annotationProcessingRequest, User user, boolean isUpdate) { var annotation = new Annotation() - .withOaMotivation(OaMotivation.fromValue(annotationRequest.getOaMotivation().value())) - .withOaMotivatedBy(annotationRequest.getOaMotivatedBy()) - .withOaHasBody(buildBody(annotationRequest.getOaHasBody())) - .withOaHasTarget(buildTarget(annotationRequest.getOaHasTarget())) + .withOaMotivation(OaMotivation.fromValue(annotationProcessingRequest.getOaMotivation().value())) + .withOaMotivatedBy(annotationProcessingRequest.getOaMotivatedBy()) + .withOaHasBody(annotationProcessingRequest.getOaHasBody()) + .withOaHasTarget(annotationProcessingRequest.getOaHasTarget()) .withDctermsCreated(Date.from(Instant.now())) .withDctermsCreator(new Agent().withType(Type.SCHEMA_PERSON).withId(user.orcid()) .withSchemaName(user.lastName())); if (isUpdate) { - annotation.setId(annotationRequest.getOdsID()); - annotation.setOdsID(annotationRequest.getOdsID()); + annotation.setId(annotationProcessingRequest.getOdsID()); + annotation.setOdsID(annotationProcessingRequest.getOdsID()); } return annotation; } - private OaHasTarget__1 buildTarget(OaHasTarget oaHasTarget) { - return new OaHasTarget__1() - .withId(oaHasTarget.getId()) - .withOdsID(oaHasTarget.getOdsID()) - .withType(oaHasTarget.getType()) - .withOdsType(oaHasTarget.getOdsType()) - .withOaHasSelector(buildSelector(oaHasTarget)); - } - - private OaHasBody__1 buildBody(OaHasBody annotationRequestBody) { - return new OaHasBody__1() - .withType(annotationRequestBody.getType()) - .withOaValue(annotationRequestBody.getOaValue()) - .withDctermsReferences(annotationRequestBody.getDctermsReferences()) - .withOdsScore(annotationRequestBody.getOdsScore()); - } - private Annotation parseToAnnotation(JsonNode response) throws JsonProcessingException { return mapper.treeToValue(response, Annotation.class); } - public JsonApiWrapper updateAnnotation(String id, AnnotationRequest annotationRequest, + public JsonApiWrapper updateAnnotation(String id, AnnotationProcessingRequest annotationProcessingRequest, String userId, String path, String prefix, String suffix) throws NoAnnotationFoundException, ForbiddenException, JsonProcessingException { var user = getUserInformation(userId); var result = repository.getAnnotationForUser(id, user.orcid()); if (result > 0) { - if (annotationRequest.getOdsID() == null) { - annotationRequest.setOdsID(id); + if (annotationProcessingRequest.getOdsID() == null) { + annotationProcessingRequest.setOdsID(id); } - var annotation = buildAnnotation(annotationRequest, user, true); + var annotation = buildAnnotation(annotationProcessingRequest, user, true); var response = annotationClient.updateAnnotation(prefix, suffix, annotation); return formatResponse(response, path); } else { diff --git a/src/main/resources/json-schema/annotation-processing-request.json b/src/main/resources/json-schema/annotation-processing-request.json new file mode 100644 index 00000000..c78da86c --- /dev/null +++ b/src/main/resources/json-schema/annotation-processing-request.json @@ -0,0 +1,85 @@ +{ + "$id": "https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation-processing-request.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$comment": "Annotation Version 0.3.0", + "title": "Annotation", + "description": "Annotation sent to the processing service from DiSSCover or a MAS", + "type": "object", + "properties": { + "@id": { + "type": "string", + "description": "The unique identifier (handle) of the Annotation object", + "pattern": "^https:\/\/hdl\\.handle\\.net\/[\\w.]+\/(.){3}-(.){3}-(.){3}", + "examples": [ + "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX" + ] + }, + "@type": { + "type": "string", + "description": "The type of the object, in this case ods:Annotation", + "const": "ods:Annotation" + }, + "ods:ID": { + "type": "string", + "description": "The handle of the annotation. It is a unique identifier for the annotation. It is composed of the handle of the document followed by a slash and a unique identifier for the annotation.", + "pattern": "^https:\/\/hdl\\.handle\\.net\/[\\w.]+\/(.){3}-(.){3}-(.){3}", + "examples": [ + "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX" + ] + }, + "oa:motivation": { + "description": "The motivation for the annotation. Based on a selection of https://www.w3.org/TR/annotation-model/#motivation-and-purpose. The motivation ods:adding is added for DiSSCo's purposes.", + "enum": [ + "ods:adding", + "ods:deleting", + "oa:assessing", + "oa:editing", + "oa:commenting" + ] + }, + "oa:motivatedBy": { + "type": "string", + "description": "Describes the reason for the annotation. https://www.w3.org/TR/annotation-vocab/#motivatedby", + "examples": [ + "The country is incorrect" + ] + }, + "oa:hasTarget": { + "type": "object", + "description": "Indicates the particular object and part of the object on which the annotation has been made.", + "$ref": "https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation-target.json" + }, + "oa:hasBody": { + "type": "object", + "description": "The body of the annotation contains the specific value of the annotation", + "$ref": "https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation-body.json" + }, + "dcterms:creator": { + "type": "object", + "description": "Contains an ods:Agent object", + "$ref": "https://schemas.dissco.tech/schemas/fdo-type/shared-model/0.3.0/agent.json" + }, + "dcterms:created": { + "type": "string", + "format": "date-time", + "description": "The date and time when the annotation was created. https://purl.org/dc/terms/created" + }, + "ods:placeInBatch": { + "type": "integer", + "description": "The place of the annotation in the batch" + }, + "ods:batchID": { + "type": "string", + "format": "uuid", + "description": "Internally generated PID to identify the batch the annotation was generated by" + } + }, + "required": [ + "oa:motivation", + "oa:hasTarget", + "oa:hasBody", + "dcterms:creator", + "dcterms:created" + ], + "additionalProperties": false +} diff --git a/src/main/resources/json-schema/annotation-request.json b/src/main/resources/json-schema/annotation-request.json deleted file mode 100644 index 4273a66b..00000000 --- a/src/main/resources/json-schema/annotation-request.json +++ /dev/null @@ -1,237 +0,0 @@ -{ - "$id": "https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation-request.json", - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$comment": "Annotation Version 0.3.0", - "title": "Annotation", - "description": "Information about the Annotation data model. This model has been based on the W3C Web Annotation model", - "type": "object", - "properties": { - "ods:ID": { - "type": "string", - "description": "The handle of the annotation. It is a unique identifier for the annotation. It is composed of the handle of the document followed by a slash and a unique identifier for the annotation.", - "pattern": "^https:\/\/hdl\\.handle\\.net\/[\\w.]+\/(.){3}-(.){3}-(.){3}", - "examples": [ - "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX" - ] - }, - "oa:motivation": { - "description": "The motivation for the annotation. Based on a selection of https://www.w3.org/TR/annotation-model/#motivation-and-purpose. The motivation ods:adding is added for DiSSCo's purposes.", - "enum": [ - "ods:adding", - "ods:deleting", - "oa:assessing", - "oa:editing", - "oa:commenting" - ] - }, - "oa:motivatedBy": { - "type": "string", - "description": "Describes the reason for the annotation. https://www.w3.org/TR/annotation-vocab/#motivatedby", - "examples": [ - "The country is incorrect" - ] - }, - "oa:hasTarget": { - "type": "object", - "description": "Indicates the particular object and part of the object on which the annotation has been made.", - "properties": { - "@id": { - "type": "string", - "description": "This is the PID of the target object. Valid targets are the Digital Specimen, Digital Media Object or another annotation.", - "examples": [ - "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX", - "https://doi.org/10.22/XXX-XXX-XXX" - ] - }, - "@type": { - "type": "string", - "description": "The type of the target object", - "examples": [ - "ods:DigitalSpecimen", - "ods:MachineAnnotationService" - ] - }, - "ods:ID": { - "type": "string", - "description": "This is the PID of the target object. Valid targets are the Digital Specimen, Digital Media Object or another annotation.", - "examples": [ - "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX", - "https://doi.org/10.22/XXX-XXX-XXX" - ] - }, - "ods:type": { - "type": "string", - "description": "This is the handle to the type of the target object.", - "pattern": "^https:\/\/doi\\.org\/[\\w\\.]+/[\\w\\.]+", - "examples": [ - "https://doi.org/21.T11148/bbad8c4e101e8af01115", - "https://doi.org/21.T11148/894b1e6cad57e921764e" - ] - }, - "oa:hasSelector": { - "type": "object", - "description": "Optional field to indicate the part of the target object that is being annotated. It can be a field, a class or a region of interest.", - "oneOf": [ - { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "ods:FieldSelector", - "description": "A selector for an individual field." - }, - "ods:field": { - "type": "string", - "description": "The full jsonPath of the field being annotated. Following: https://goessner.net/articles/JsonPath/index.html#e2", - "examples": [ - "$.hasEvent[0].location.dwc:country" - ] - } - }, - "required": [ - "@type", - "ods:field" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "ods:ClassSelector", - "description": "A selector for an individual class." - }, - "ods:class": { - "type": "string", - "description": "The full jsonPath of the class being annotated. Following: https://goessner.net/articles/JsonPath/index.html#e2", - "examples": [ - "$.hasEvent[0].location.georeference" - ] - } - }, - "required": [ - "@type", - "ods:class" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "oa:FragmentSelector", - "description": "A selector for an specific Region of Interest (Roi). Only applicable on media objects." - }, - "ac:hasROI": { - "type": "object", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_hasROI", - "properties": { - "ac:xFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_xFrac", - "minimum": 0, - "maximum": 1 - }, - "ac:yFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_yFrac", - "minimum": 0, - "maximum": 1 - }, - "ac:widthFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_widthFrac", - "minimum": 0, - "maximum": 1 - }, - "ac:heightFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_heightFrac", - "minimum": 0, - "maximum": 1 - } - }, - "required": [ - "ac:xFrac", - "ac:yFrac", - "ac:widthFrac", - "ac:heightFrac" - ], - "additionalProperties": false - }, - "dcterms:conformsTo": { - "type": "string", - "constant": "https://ac.tdwg.org/termlist/#711-region-of-interest-vocabulary", - "description": "https://purl.org/dc/terms/conformsTo" - } - }, - "required": [ - "@type", - "ac:hasROI", - "dcterms:conformsTo" - ], - "additionalProperties": false - } - ] - } - }, - "required": [ - "@id", - "@type", - "ods:ID", - "ods:type" - ], - "additionalProperties": false - }, - "oa:hasBody": { - "type": "object", - "description": "The body of the annotation contains the specific value of the annotation", - "properties": { - "@type": { - "type": "string", - "constant": "oa:TextualBody", - "description": "https://www.w3.org/TR/annotation-vocab/#textualbody" - }, - "oa:value": { - "type": "array", - "description": "An array of multiple values in string representation specific for the particular selector", - "items": { - "type": "string", - "description": "The textual content of the body. This could be a simple string value for commenting or editing of a single field. It could also be the string representation of the json for a class", - "examples": [ - "This is a comment", - "Venezuela", - "{'entityRelationship': {'entityRelationshipType': 'hasGbifID', 'objectEntityIri': 'https://www.gbif.org/occurrence/144870459', 'entityRelationshipDate': '2023-11-29T07:18:20.588Z', 'entityRelationshipCreatorName': 'GBIF occurrence linker', 'entityRelationshipCreatorId': 'https://hdl.handle.net/enrichment-service-pid'}}" - ] - } - }, - "dcterms:references": { - "type": "string", - "description": "Indicates how the value came to be. https://purl.org/dc/terms/references", - "examples": [ - "https://api.gbif.org/v1/occurrence/search?occurrenceID=https://herbarium.bgbm.org/object/BW00965020&catalogNumber=B -W 00965 -02 0&basisOfRecord=PreservedSpecimen" - ] - }, - "ods:score": { - "type": "number", - "description": "A score between 0 and 1 indicating the confidence in the value. 1 is the highest confidence and 0 is the lowest.", - "minimum": 0, - "maximum": 1 - } - }, - "required": [ - "@type", - "oa:value" - ], - "additionalProperties": false - } - }, - "required": [ - "oa:motivation", - "oa:hasTarget", - "oa:hasBody" - ], - "additionalProperties": false -} diff --git a/src/main/resources/json-schema/annotation.json b/src/main/resources/json-schema/annotation.json index 3b8ccf6f..e5e5dd6c 100644 --- a/src/main/resources/json-schema/annotation.json +++ b/src/main/resources/json-schema/annotation.json @@ -76,198 +76,12 @@ "oa:hasTarget": { "type": "object", "description": "Indicates the particular object and part of the object on which the annotation has been made.", - "properties": { - "@id": { - "type": "string", - "description": "This is the PID of the target object. Valid targets are the Digital Specimen, Digital Media Object or another annotation.", - "examples": [ - "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX", - "https://doi.org/10.22/XXX-XXX-XXX" - ] - }, - "@type": { - "type": "string", - "description": "The type of the target object", - "examples": [ - "ods:DigitalSpecimen", - "ods:MachineAnnotationService" - ] - }, - "ods:ID": { - "type": "string", - "description": "This is the PID of the target object. Valid targets are the Digital Specimen, Digital Media Object or another annotation.", - "examples": [ - "https://hdl.handle.net/20.5000.1025/XXX-XXX-XXX", - "https://doi.org/10.22/XXX-XXX-XXX" - ] - }, - "ods:type": { - "type": "string", - "description": "This is the handle to the type of the target object.", - "pattern": "^https:\/\/doi\\.org\/[\\w\\.]+/[\\w\\.]+", - "examples": [ - "https://doi.org/21.T11148/bbad8c4e101e8af01115", - "https://doi.org/21.T11148/894b1e6cad57e921764e" - ] - }, - "oa:hasSelector": { - "type": "object", - "description": "Optional field to indicate the part of the target object that is being annotated. It can be a field, a class or a region of interest.", - "oneOf": [ - { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "ods:FieldSelector", - "description": "A selector for an individual field." - }, - "ods:field": { - "type": "string", - "description": "The full jsonPath of the field being annotated. Following: https://goessner.net/articles/JsonPath/index.html#e2", - "examples": [ - "$.hasEvent[0].location.dwc:country" - ] - } - }, - "required": [ - "@type", - "ods:field" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "ods:ClassSelector", - "description": "A selector for an individual class." - }, - "ods:class": { - "type": "string", - "description": "The full jsonPath of the class being annotated. Following: https://goessner.net/articles/JsonPath/index.html#e2", - "examples": [ - "$.hasEvent[0].location.georeference" - ] - } - }, - "required": [ - "@type", - "ods:class" - ], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "@type": { - "type": "string", - "const": "oa:FragmentSelector", - "description": "A selector for an specific Region of Interest (Roi). Only applicable on media objects." - }, - "ac:hasROI": { - "type": "object", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_hasROI", - "properties": { - "ac:xFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_xFrac", - "minimum": 0, - "maximum": 1 - }, - "ac:yFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_yFrac", - "minimum": 0, - "maximum": 1 - }, - "ac:widthFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_widthFrac", - "minimum": 0, - "maximum": 1 - }, - "ac:heightFrac": { - "type": "number", - "description": "https://ac.tdwg.org/termlist/2023-02-24#ac_heightFrac", - "minimum": 0, - "maximum": 1 - } - }, - "required": [ - "ac:xFrac", - "ac:yFrac", - "ac:widthFrac", - "ac:heightFrac" - ], - "additionalProperties": false - }, - "dcterms:conformsTo": { - "type": "string", - "constant": "https://ac.tdwg.org/termlist/#711-region-of-interest-vocabulary", - "description": "https://purl.org/dc/terms/conformsTo" - } - }, - "required": [ - "@type", - "ac:hasROI", - "dcterms:conformsTo" - ], - "additionalProperties": false - } - ] - } - }, - "required": [ - "@id", - "@type", - "ods:ID", - "ods:type" - ], - "additionalProperties": false + "$ref": "https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation-target.json" }, "oa:hasBody": { "type": "object", "description": "The body of the annotation contains the specific value of the annotation", - "properties": { - "@type": { - "type": "string", - "constant": "oa:TextualBody", - "description": "https://www.w3.org/TR/annotation-vocab/#textualbody" - }, - "oa:value": { - "type": "array", - "description": "An array of multiple values in string representation specific for the particular selector", - "items": { - "type": "string", - "description": "The textual content of the body. This could be a simple string value for commenting or editing of a single field. It could also be the string representation of the json for a class", - "examples": [ - "This is a comment", - "Venezuela", - "{'entityRelationship': {'entityRelationshipType': 'hasGbifID', 'objectEntityIri': 'https://www.gbif.org/occurrence/144870459', 'entityRelationshipDate': '2023-11-29T07:18:20.588Z', 'entityRelationshipCreatorName': 'GBIF occurrence linker', 'entityRelationshipCreatorId': 'https://hdl.handle.net/enrichment-service-pid'}}" - ] - } - }, - "dcterms:references": { - "type": "string", - "description": "Indicates how the value came to be. https://purl.org/dc/terms/references", - "examples": [ - "https://api.gbif.org/v1/occurrence/search?occurrenceID=https://herbarium.bgbm.org/object/BW00965020&catalogNumber=B -W 00965 -02 0&basisOfRecord=PreservedSpecimen" - ] - }, - "ods:score": { - "type": "number", - "description": "A score between 0 and 1 indicating the confidence in the value. 1 is the highest confidence and 0 is the lowest.", - "minimum": 0, - "maximum": 1 - } - }, - "required": [ - "@type", - "oa:value" - ], - "additionalProperties": false + "$ref": "https://schemas.dissco.tech/schemas/fdo-type/annotation/0.3.0/annotation-body.json" }, "dcterms:creator": { "type": "object", @@ -367,6 +181,7 @@ "ods:version", "oa:motivation", "oa:hasTarget", + "oa:hasBody", "dcterms:creator", "dcterms:created", "dcterms:modified", diff --git a/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java b/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java index 8fe3301a..5163feab 100644 --- a/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java +++ b/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java @@ -1,23 +1,15 @@ package eu.dissco.backend.component; -import static eu.dissco.backend.TestUtils.HANDLE; -import static eu.dissco.backend.TestUtils.ID; -import static eu.dissco.backend.TestUtils.MAPPER; import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationEventRequest; import static eu.dissco.backend.utils.AnnotationUtils.givenAnnotationRequest; import static eu.dissco.backend.utils.AnnotationUtils.givenBatchMetadata; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.networknt.schema.JsonSchemaFactory; -import com.networknt.schema.SpecVersion.VersionFlag; import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; import eu.dissco.backend.domain.annotation.batch.BatchMetadata; import eu.dissco.backend.exceptions.InvalidAnnotationRequestException; -import eu.dissco.backend.schema.AnnotationRequest; import java.io.IOException; -import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.stream.Stream; @@ -34,59 +26,9 @@ class SchemaValidatorComponentTest { private SchemaValidatorComponent schemaValidator; - private static Stream validAnnotations() { - return Stream.of( - Arguments.of(givenAnnotationRequest(), true), - Arguments.of(givenAnnotationRequest().withOdsID(HANDLE + ID), false) - ); - } - - private static Stream invalidAnnotations() { - return Stream.of( -// Arguments.of(givenAnnotationRequest().setOaCreator(givenCreator(ORCID)), "oa:creator"), -// Arguments.of(givenAnnotationRequest().setDcTermsCreated(CREATED), "dcterms:created"), -// Arguments.of(givenAnnotationRequest().setOaGenerated(CREATED), "oa:generated"), -// Arguments.of(givenAnnotationRequest().setAsGenerator(givenGenerator()), "as:generator"), - Arguments.of(givenAnnotationRequest().withOdsID(ID), "ods:id"), - Arguments.of(givenAnnotationRequest().withOaHasBody(null), "oa:hasBody"), - Arguments.of(givenAnnotationRequest().withOaHasTarget(null), "oa:hasTarget"), - Arguments.of(givenAnnotationRequest().withOaMotivation(null), "oa:motivation") - ); - } - - private static Stream invalidEvents() { - return Stream.of( - Arguments.of(new AnnotationEventRequest(Collections.nCopies(2, givenAnnotationRequest()), - List.of(givenBatchMetadata()))), - Arguments.of(new AnnotationEventRequest(List.of(givenAnnotationRequest()), - Collections.nCopies(2, givenBatchMetadata()))), - Arguments.of(new AnnotationEventRequest(List.of(givenAnnotationRequest()), - List.of(new BatchMetadata(Collections.emptyList())))), - Arguments.of(new AnnotationEventRequest(List.of(givenAnnotationRequest()), - Collections.emptyList())) - ); - } - @BeforeEach - void setup() throws IOException { - var factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); - try (InputStream inputStream = Thread.currentThread().getContextClassLoader() - .getResourceAsStream("json-schema/annotation-request.json")) { - var schema = factory.getSchema(inputStream); - schemaValidator = new SchemaValidatorComponent(schema, MAPPER); - } - } - - @ParameterizedTest - @MethodSource("invalidAnnotations") - void testInvalidAnnotations(AnnotationRequest annotationRequest, String targetIssue) { - // When - var e = assertThrows(InvalidAnnotationRequestException.class, - () -> schemaValidator.validateAnnotationRequest(annotationRequest, - true)); - - // Then - assertThat(e.getMessage()).contains(targetIssue); + void setup() { + schemaValidator = new SchemaValidatorComponent(); } @Test @@ -104,25 +46,18 @@ void testValidateAnnotationEventRequestInvalid(AnnotationEventRequest event) { () -> schemaValidator.validateAnnotationEventRequest(event, true)); } - @Test - void testUpdateMissingId() { - // Given - var annotationRequest = givenAnnotationRequest(); - - // When - var e = assertThrows(InvalidAnnotationRequestException.class, - () -> schemaValidator.validateAnnotationRequest(annotationRequest, - false)); - - // Then - assertThat(e.getMessage()).contains("ods:id"); + private static Stream invalidEvents() { + return Stream.of( + Arguments.of(new AnnotationEventRequest(Collections.nCopies(2, givenAnnotationRequest()), + List.of(givenBatchMetadata()))), + Arguments.of(new AnnotationEventRequest(List.of(givenAnnotationRequest()), + Collections.nCopies(2, givenBatchMetadata()))), + Arguments.of(new AnnotationEventRequest(List.of(givenAnnotationRequest()), + List.of(new BatchMetadata(Collections.emptyList())))), + Arguments.of(new AnnotationEventRequest(List.of(givenAnnotationRequest()), + Collections.emptyList())) + ); } - @ParameterizedTest - @MethodSource("validAnnotations") - void testValidAnnotation(AnnotationRequest annotationRequest, Boolean isNew) { - // Then - assertDoesNotThrow(() -> - schemaValidator.validateAnnotationRequest(annotationRequest, isNew)); - } + } diff --git a/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java b/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java index d5544c00..10872682 100644 --- a/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java +++ b/src/test/java/eu/dissco/backend/controller/AnnotationControllerTest.java @@ -21,7 +21,7 @@ import eu.dissco.backend.exceptions.NoAnnotationFoundException; import eu.dissco.backend.exceptions.NotFoundException; import eu.dissco.backend.properties.ApplicationProperties; -import eu.dissco.backend.schema.AnnotationRequest; +import eu.dissco.backend.schema.AnnotationProcessingRequest; import eu.dissco.backend.service.AnnotationService; import java.io.IOException; import org.junit.jupiter.api.BeforeEach; @@ -182,7 +182,7 @@ void testCreateAnnotationNullResponse() throws Exception { givenAuthentication(USER_ID_TOKEN); var annotation = givenAnnotationRequest(); var request = givenJsonApiAnnotationRequest(annotation); - given(service.persistAnnotation(any(AnnotationRequest.class), any(), any())).willReturn(null); + given(service.persistAnnotation(any(AnnotationProcessingRequest.class), any(), any())).willReturn(null); // When var receivedResponse = controller.createAnnotation(authentication, request, mockRequest); diff --git a/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java b/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java index e8981314..1a8889f2 100644 --- a/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java +++ b/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java @@ -24,14 +24,16 @@ import eu.dissco.backend.schema.Agent; import eu.dissco.backend.schema.Agent.Type; import eu.dissco.backend.schema.Annotation; -import eu.dissco.backend.schema.AnnotationRequest; -import eu.dissco.backend.schema.AnnotationRequest.OaMotivation; -import eu.dissco.backend.schema.OaHasBody; -import eu.dissco.backend.schema.OaHasBody__1; +import eu.dissco.backend.schema.AnnotationBody; +import eu.dissco.backend.schema.AnnotationProcessingRequest; +import eu.dissco.backend.schema.AnnotationProcessingRequest; +import eu.dissco.backend.schema.AnnotationProcessingRequest.OaMotivation; +import eu.dissco.backend.schema.AnnotationBody; +import eu.dissco.backend.schema.AnnotationBody; import eu.dissco.backend.schema.OaHasSelector; -import eu.dissco.backend.schema.OaHasSelector__1; -import eu.dissco.backend.schema.OaHasTarget; -import eu.dissco.backend.schema.OaHasTarget__1; +import eu.dissco.backend.schema.OaHasSelector; +import eu.dissco.backend.schema.AnnotationTarget; +import eu.dissco.backend.schema.AnnotationTarget; import eu.dissco.backend.schema.SchemaAggregateRating; import java.util.ArrayList; import java.util.Collections; @@ -95,19 +97,19 @@ public static Annotation givenAnnotationProcessingRequest(boolean isUpdate) { return annotation; } - public static AnnotationRequest givenAnnotationRequest(String targetId) { - return new AnnotationRequest() + public static AnnotationProcessingRequest givenAnnotationRequest(String targetId) { + return new AnnotationProcessingRequest() .withOaHasBody(givenOaBody()) .withOaHasTarget(givenOaTarget(targetId)) .withOaMotivation(OaMotivation.OA_COMMENTING); } - public static AnnotationRequest givenAnnotationRequest() { + public static AnnotationProcessingRequest givenAnnotationRequest() { return givenAnnotationRequest(TARGET_ID); } - public static OaHasBody givenOaBody() { - return new OaHasBody() + public static AnnotationBody givenOaBody() { + return new AnnotationBody() .withType("ods:DigitalSpecimen") .withOaValue(new ArrayList<>(List.of("a comment"))) .withDctermsReferences( @@ -115,8 +117,8 @@ public static OaHasBody givenOaBody() { .withOdsScore(0.99); } - public static OaHasBody__1 givenOaBodyAnnotation() { - return new OaHasBody__1() + public static AnnotationBody givenOaBodyAnnotation() { + return new AnnotationBody() .withType("ods:DigitalSpecimen") .withOaValue(new ArrayList<>(List.of("a comment"))) .withDctermsReferences( @@ -124,8 +126,8 @@ public static OaHasBody__1 givenOaBodyAnnotation() { .withOdsScore(0.99); } - public static OaHasTarget givenOaTarget(String targetId) { - return new OaHasTarget() + public static AnnotationTarget givenOaTarget(String targetId) { + return new AnnotationTarget() .withId(targetId) .withOdsID(targetId) .withType("ods:DigitalSpecimen") @@ -133,8 +135,8 @@ public static OaHasTarget givenOaTarget(String targetId) { .withOaHasSelector(givenSelector()); } - public static OaHasTarget__1 givenOaTargetAnnotation(String targetId) { - return new OaHasTarget__1() + public static AnnotationTarget givenOaTargetAnnotation(String targetId) { + return new AnnotationTarget() .withId(targetId) .withOdsID(targetId) .withType("ods:DigitalSpecimen") @@ -148,8 +150,8 @@ public static OaHasSelector givenSelector() { .withAdditionalProperty("ods:field", "ods:specimenName"); } - public static OaHasSelector__1 givenSelectorAnnotation() { - return new OaHasSelector__1() + public static OaHasSelector givenSelectorAnnotation() { + return new OaHasSelector() .withAdditionalProperty("@type", "ods:FieldSelector") .withAdditionalProperty("field", "ods:specimenName"); } From 47d354cdfe057cfdc8e25e19c5456362492ec77d Mon Sep 17 00:00:00 2001 From: southeo Date: Fri, 2 Aug 2024 13:45:29 +0200 Subject: [PATCH 2/2] imports --- .../backend/component/SchemaValidatorComponentTest.java | 1 - src/test/java/eu/dissco/backend/utils/AnnotationUtils.java | 5 ----- 2 files changed, 6 deletions(-) diff --git a/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java b/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java index 5163feab..8e2335d2 100644 --- a/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java +++ b/src/test/java/eu/dissco/backend/component/SchemaValidatorComponentTest.java @@ -9,7 +9,6 @@ import eu.dissco.backend.domain.annotation.batch.AnnotationEventRequest; import eu.dissco.backend.domain.annotation.batch.BatchMetadata; import eu.dissco.backend.exceptions.InvalidAnnotationRequestException; -import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.stream.Stream; diff --git a/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java b/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java index 1a8889f2..f6ea772d 100644 --- a/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java +++ b/src/test/java/eu/dissco/backend/utils/AnnotationUtils.java @@ -26,13 +26,8 @@ import eu.dissco.backend.schema.Annotation; import eu.dissco.backend.schema.AnnotationBody; import eu.dissco.backend.schema.AnnotationProcessingRequest; -import eu.dissco.backend.schema.AnnotationProcessingRequest; import eu.dissco.backend.schema.AnnotationProcessingRequest.OaMotivation; -import eu.dissco.backend.schema.AnnotationBody; -import eu.dissco.backend.schema.AnnotationBody; import eu.dissco.backend.schema.OaHasSelector; -import eu.dissco.backend.schema.OaHasSelector; -import eu.dissco.backend.schema.AnnotationTarget; import eu.dissco.backend.schema.AnnotationTarget; import eu.dissco.backend.schema.SchemaAggregateRating; import java.util.ArrayList;