From a3094d03f2f1bf2aad6a4474aace6d25b53f2da6 Mon Sep 17 00:00:00 2001 From: StephenL Date: Mon, 30 Sep 2024 15:07:05 +0100 Subject: [PATCH 01/11] Separated utility methods from the web endpoints --- .../uksrc/archive/ObservationResource.java | 101 +++--------------- .../archive/utils/responses/Responses.java | 37 +++++++ .../org/uksrc/archive/utils/tools/Tools.java | 50 +++++++++ 3 files changed, 104 insertions(+), 84 deletions(-) create mode 100644 src/main/java/org/uksrc/archive/utils/responses/Responses.java create mode 100644 src/main/java/org/uksrc/archive/utils/tools/Tools.java diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index 6f98509..142bcd1 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -18,12 +18,12 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameters; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.hibernate.PropertyValueException; import org.ivoa.dm.caom2.caom2.DerivedObservation; import org.ivoa.dm.caom2.caom2.Observation; import org.ivoa.dm.caom2.caom2.SimpleObservation; -import jakarta.validation.constraints.NotNull; import org.uksrc.archive.utils.ObservationListWrapper; +import org.uksrc.archive.utils.responses.Responses; +import org.uksrc.archive.utils.tools.Tools; import java.util.*; @@ -131,7 +131,7 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO return Response.ok(observation).build(); } } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } return Response.status(Response.Status.NOT_FOUND) @@ -171,19 +171,19 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO @Produces(MediaType.APPLICATION_XML) public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { - return errorResponse("Both 'page' and 'size' must be provided together or neither."); + return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); } try { if (page != null && (page < 0 || size < 1)) { - return errorResponse("Page must be 0 or greater and size must be greater than 0."); + return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); } // Create query and apply pagination if required TypedQuery query = em.createQuery("SELECT o FROM Observation o", Observation.class); - return performQuery(page, size, query); + return Tools.performQuery(page, size, query); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -225,19 +225,19 @@ public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam @Produces(MediaType.APPLICATION_XML) public Response getObservations(@PathParam("collectionId") String collection, @QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { - return errorResponse("Both 'page' and 'size' must be provided together or neither."); + return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); } try { if (page != null && (page < 0 || size < 1)) { - return errorResponse("Page must be 0 or greater and size must be greater than 0."); + return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); } TypedQuery query = em.createQuery("SELECT o FROM Observation o WHERE o.collection = :collection", Observation.class); query.setParameter("collection", collection); - return performQuery(page, size, query); + return Tools.performQuery(page, size, query); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -280,7 +280,7 @@ public Response getObservation(@PathParam("observationId") String observationId) .entity("Observation with ID " + observationId + " not found").build(); } } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -321,7 +321,7 @@ public Response deleteObservation(@PathParam("observationId") String id) { .build(); } } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -344,13 +344,13 @@ public Response getCollections(){ try { TypedQuery query = em.createQuery("SELECT DISTINCT o.collection FROM Observation o", String.class); List uniqueCollections = query.getResultList(); + String tabbed = Tools.convertListToTsv(uniqueCollections); return Response.ok() .type(MediaType.TEXT_PLAIN) - .entity(convertListToTsv(uniqueCollections)) - .build(); + .entity(tabbed).build(); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -364,77 +364,10 @@ private Response submitObservation(Observation observation) { em.persist(observation); em.flush(); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } return Response.status(Response.Status.CREATED) .entity(observation) .build(); } - - /** - * Generate an error response - * @param e Whatever exception has been thrown - * @return A 400 response containing the exception error. - */ - private Response errorResponse (@NotNull Exception e){ - String additional = ""; - if (e instanceof PropertyValueException){ - //Inform caller of exact property that's missing/invalid - additional = ((PropertyValueException)e).getPropertyName(); - } - return Response.status(Response.Status.BAD_REQUEST) - .type(MediaType.TEXT_PLAIN) - .entity(e.getMessage() + " " + additional) - .build(); - } - - /** - * Generate an error message - * @param message Message to return to the caller. - * @return A 400 response containing the supplied message - */ - private Response errorResponse (@NotNull String message){ - return Response.status(Response.Status.BAD_REQUEST) - .type(MediaType.TEXT_PLAIN) - .entity(message) - .build(); - } - - /** - * Performs the supplied query (with or without the pagination parameters) - * @param page zero-indexed page index - * @param size number of entries per page - * @param query query to perform - * @return Response containing HTTP response code and expected body if successful. - */ - private Response performQuery(Integer page, Integer size, TypedQuery query) { - try { - if (page != null && size != null) { - int firstResult = page * size; - query.setFirstResult(firstResult); - query.setMaxResults(size); - } - - List observations = query.getResultList(); - ObservationListWrapper wrapper = new ObservationListWrapper(observations); - - return Response.ok(wrapper).build(); - } catch (Exception e) { - return errorResponse(e); - } - } - - /** - * Converts a List of strings to a TSV - * @param list The list of elements to convert to a TSV string. - * @return list of items "e-merlin test ALMA" - */ - public String convertListToTsv(List list) { - StringJoiner joiner = new StringJoiner("\t"); - - for (String item : list) { - joiner.add(item); - } - return joiner.toString(); - } } diff --git a/src/main/java/org/uksrc/archive/utils/responses/Responses.java b/src/main/java/org/uksrc/archive/utils/responses/Responses.java new file mode 100644 index 0000000..b23b5db --- /dev/null +++ b/src/main/java/org/uksrc/archive/utils/responses/Responses.java @@ -0,0 +1,37 @@ +package org.uksrc.archive.utils.responses; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.hibernate.PropertyValueException; + +public class Responses { + /** + * Generate an error response + * @param e Whatever exception has been thrown + * @return A 400 response containing the exception error. + */ + public static Response errorResponse (@NotNull Exception e){ + String additional = ""; + if (e instanceof PropertyValueException){ + //Inform caller of exact property that's missing/invalid + additional = ((PropertyValueException)e).getPropertyName(); + } + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.TEXT_PLAIN) + .entity(e.getMessage() + " " + additional) + .build(); + } + + /** + * Generate an error message + * @param message Message to return to the caller. + * @return A 400 response containing the supplied message + */ + public static Response errorResponse (@NotNull String message){ + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.TEXT_PLAIN) + .entity(message) + .build(); + } +} diff --git a/src/main/java/org/uksrc/archive/utils/tools/Tools.java b/src/main/java/org/uksrc/archive/utils/tools/Tools.java new file mode 100644 index 0000000..dde2d18 --- /dev/null +++ b/src/main/java/org/uksrc/archive/utils/tools/Tools.java @@ -0,0 +1,50 @@ +package org.uksrc.archive.utils.tools; + +import jakarta.persistence.TypedQuery; +import jakarta.ws.rs.core.Response; +import org.ivoa.dm.caom2.caom2.Observation; +import org.uksrc.archive.utils.ObservationListWrapper; +import org.uksrc.archive.utils.responses.Responses; + +import java.util.List; +import java.util.StringJoiner; + +public class Tools { + /** + * Performs the supplied query (with or without the pagination parameters) + * @param page zero-indexed page index + * @param size number of entries per page + * @param query query to perform + * @return Response containing HTTP response code and expected body if successful. + */ + public static Response performQuery(Integer page, Integer size, TypedQuery query) { + try { + if (page != null && size != null) { + int firstResult = page * size; + query.setFirstResult(firstResult); + query.setMaxResults(size); + } + + List observations = query.getResultList(); + ObservationListWrapper wrapper = new ObservationListWrapper(observations); + + return Response.ok(wrapper).build(); + } catch (Exception e) { + return Responses.errorResponse(e); + } + } + + /** + * Converts a List of strings to a TSV + * @param list The list of elements to convert to a TSV string. + * @return list of items "e-merlin test ALMA" + */ + public static String convertListToTsv(List list) { + StringJoiner joiner = new StringJoiner("\t"); + + for (String item : list) { + joiner.add(item); + } + return joiner.toString(); + } +} From 4845b6d7ddf0b7e33fcea7007f84f8aaab74b2a3 Mon Sep 17 00:00:00 2001 From: StephenL Date: Tue, 1 Oct 2024 14:04:10 +0100 Subject: [PATCH 02/11] - added support for JSON requests & responses - updated some of the APIs to separate Simple & Derived Observations (to sidestep issue with XML->Java Object polymorphism issue - not accepting derived types with an base type param). --- detail.md | 18 +- .../uksrc/archive/ObservationResource.java | 204 +++++++++++++----- .../archive/utils/responses/Responses.java | 10 +- .../org/uksrc/archive/utils/tools/Tools.java | 8 +- .../archive/ObservationResourceTest.java | 18 +- 5 files changed, 192 insertions(+), 66 deletions(-) diff --git a/detail.md b/detail.md index 240b285..3a569b6 100644 --- a/detail.md +++ b/detail.md @@ -13,6 +13,17 @@ Example resources suitable for **minimal** testing (Mandatory properties only). auri ``` + +Type has to be set for JSON +```json +{ + "@type": "caom2:caom2.SimpleObservation", + "id": "myData12345", + "collection": "test", + "uri": "auri", + "intent": "science" +} +``` #### Example Derived Observation ```xml @@ -123,12 +134,17 @@ Endpoints available for interaction with the archive-service. > | `201` | `application/xml` | `Observation added successfully, body contains new Observation` | > | `400` | `text/plain` | `{"code":"400","message":"Bad Request"}` | -##### Example cURL +##### Example XML cURL > ``` > curl -v --header "Content-Type: application/xml" -T observation1.xml http://localhost:8080/observations/add > ``` +##### Example JSON cURL +with JSON response also +> ``` +> curl -v --header "Content-Type: application/json" --header "Accept: application/json" -T observation1.xml http://localhost:8080/observations/add +> ```
diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index 142bcd1..c4cc02d 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -20,12 +20,13 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.ivoa.dm.caom2.caom2.DerivedObservation; import org.ivoa.dm.caom2.caom2.Observation; +import org.ivoa.dm.caom2.caom2.ObservationIntentType; import org.ivoa.dm.caom2.caom2.SimpleObservation; import org.uksrc.archive.utils.ObservationListWrapper; import org.uksrc.archive.utils.responses.Responses; import org.uksrc.archive.utils.tools.Tools; -import java.util.*; +import java.util.List; @Path("/observations") @@ -35,15 +36,21 @@ public class ObservationResource { protected EntityManager em; // exists for the application lifetime no need to close @PUT - @Path("/add") + @Path("/simple/add") @Operation(summary = "Create a new Observation", description = "Creates a new observation in the database, note the supplied ID needs to be unique.") @RequestBody( - description = "XML representation of the Observation", + description = "XML or JSON representation of the Observation", required = true, - content = @Content( - mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = SimpleObservation.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, + schema = @Schema(implementation = SimpleObservation.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = SimpleObservation.class) + ) + } ) @APIResponse( responseCode = "201", @@ -54,8 +61,8 @@ public class ObservationResource { responseCode = "400", description = "Invalid input" ) - @Consumes(MediaType.APPLICATION_XML) - @Produces(MediaType.APPLICATION_XML) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Transactional public Response addObservation(SimpleObservation observation) { return submitObservation(observation); @@ -63,14 +70,20 @@ public Response addObservation(SimpleObservation observation) { @PUT @Path("/derived/add") - @Operation(summary = "Create a new Derived Observation", description = "Create a DERIVED observation in the database, note ID must be unique across all observations.") + @Operation(summary = "Create a new Derived Observation", description = "Create a DERIVED observation in the database. Note, ID must be unique across all observations.") @RequestBody( description = "XML representation of the Derived Observation", required = true, - content = @Content( - mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = DerivedObservation.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, + schema = @Schema(implementation = SimpleObservation.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = SimpleObservation.class) + ) + } ) @APIResponse( responseCode = "201", @@ -81,15 +94,15 @@ public Response addObservation(SimpleObservation observation) { responseCode = "400", description = "Invalid input" ) - @Consumes(MediaType.APPLICATION_XML) - @Produces(MediaType.APPLICATION_XML) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Transactional public Response addObservation(DerivedObservation observation) { return submitObservation(observation); } @POST - @Path("/update/{observationId}") + @Path("/simple/update/{observationId}") @Operation(summary = "Update an existing Observation", description = "Updates an existing observation with the supplied ID") @Parameter( name = "observationId", @@ -100,15 +113,21 @@ public Response addObservation(DerivedObservation observation) { @RequestBody( description = "XML representation of the Observation", required = true, - content = @Content( - mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = Observation.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, + schema = @Schema(implementation = SimpleObservation.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = SimpleObservation.class) + ) + } ) @APIResponse( responseCode = "200", description = "Observation updated successfully", - content = @Content(schema = @Schema(implementation = Observation.class)) + content = @Content(schema = @Schema(implementation = SimpleObservation.class)) ) @APIResponse( responseCode = "404", @@ -118,26 +137,54 @@ public Response addObservation(DerivedObservation observation) { responseCode = "400", description = "Invalid input" ) - @Consumes(MediaType.APPLICATION_XML) - @Produces(MediaType.APPLICATION_XML) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Transactional - public Response updateObservation(@PathParam("observationId") String id, SimpleObservation observation) { - try { - //Only update IF found - Observation existing = em.find(Observation.class, id); - if (existing != null && observation != null) { - observation.setId(id); - em.merge(observation); - return Response.ok(observation).build(); - } - } catch (Exception e) { - return Responses.errorResponse(e); - } + public Response updateSimpleObservation(@PathParam("observationId") String id, SimpleObservation observation) { + return updateObservation(id, observation); + } - return Response.status(Response.Status.NOT_FOUND) - .type(MediaType.TEXT_PLAIN) - .entity("Observation not found") - .build(); + @POST + @Path("/derived/update/{observationId}") + @Operation(summary = "Update an existing DerivedObservation", description = "Updates an existing observation with the supplied ID") + @Parameter( + name = "observationId", + description = "ID of the Observation to be updated", + required = true, + example = "123" + ) + @RequestBody( + description = "XML representation of the Observation", + required = true, + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, + schema = @Schema(implementation = DerivedObservation.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = DerivedObservation.class) + ) + } + ) + @APIResponse( + responseCode = "200", + description = "Observation updated successfully", + content = @Content(schema = @Schema(implementation = DerivedObservation.class)) + ) + @APIResponse( + responseCode = "404", + description = "An Observation with the supplied ID has not been found." + ) + @APIResponse( + responseCode = "400", + description = "Invalid input" + ) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Transactional + public Response updateDerivedObservation(@PathParam("observationId") String id, DerivedObservation observation) { + return updateObservation(id, observation); } @GET @@ -160,15 +207,20 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO @APIResponse( responseCode = "200", description = "List of observations retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = ObservationListWrapper.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = ObservationListWrapper.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ObservationListWrapper.class) + ) + } ) @APIResponse( responseCode = "400", description = "Internal error whilst retrieving Observations or parameter error (if supplied)." ) - @Produces(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); @@ -214,15 +266,20 @@ public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam @APIResponse( responseCode = "200", description = "List of observations retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = ObservationListWrapper.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = ObservationListWrapper.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ObservationListWrapper.class) + ) + } ) @APIResponse( responseCode = "400", description = "Internal error whilst retrieving Observations." ) - @Produces(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getObservations(@PathParam("collectionId") String collection, @QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); @@ -243,7 +300,7 @@ public Response getObservations(@PathParam("collectionId") String collection, @Q @GET @Path("/{observationId}") - @Operation(summary = "Retrieve observations from a collection", description = "Returns a list of observations that are members of the supplied collection") + @Operation(summary = "Retrieve an observation", description = "Returns an observation with the supplied ID.") @Parameters({ @Parameter( name = "observationId", @@ -255,9 +312,14 @@ public Response getObservations(@PathParam("collectionId") String collection, @Q @APIResponse( responseCode = "200", description = "Observation retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = Observation.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = Observation.class) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Observation.class) + ) + } ) @APIResponse( responseCode = "404", @@ -267,7 +329,7 @@ public Response getObservations(@PathParam("collectionId") String collection, @Q responseCode = "400", description = "Internal error whilst retrieving Observations." ) - @Produces(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getObservation(@PathParam("observationId") String observationId) { try { Observation observation = em.find(Observation.class, observationId); @@ -354,6 +416,23 @@ public Response getCollections(){ } } + /* @GET + @Path("/collections/test") + @Produces(MediaType.APPLICATION_XML) + public Response getObservation() { + // Create a SimpleObservation object (could be fetched from DB or any source) + SimpleObservation observation = new SimpleObservation(); + observation.setId("10"); + observation.setCollection("test"); + observation.setIntent(ObservationIntentType.SCIENCE); + observation.setUri("auri"); + //observation.setMembers(new ArrayList("someone")); + + // Return as JSON + return Response.ok(observation).build(); + }*/ + + /** * Adds an observation to the database * @param observation Either a SimpleObservation or a DerivedObservation @@ -370,4 +449,23 @@ private Response submitObservation(Observation observation) { .entity(observation) .build(); } + + private Response updateObservation(String id, Observation observation) { + try { + //Only update IF found + Observation existing = em.find(Observation.class, id); + if (existing != null && observation != null) { + observation.setId(id); + em.merge(observation); + return Response.ok(observation).build(); + } + } catch (Exception e) { + return Responses.errorResponse(e); + } + + return Response.status(Response.Status.NOT_FOUND) + .type(MediaType.TEXT_PLAIN) + .entity("Observation not found") + .build(); + } } diff --git a/src/main/java/org/uksrc/archive/utils/responses/Responses.java b/src/main/java/org/uksrc/archive/utils/responses/Responses.java index b23b5db..d11c2cd 100644 --- a/src/main/java/org/uksrc/archive/utils/responses/Responses.java +++ b/src/main/java/org/uksrc/archive/utils/responses/Responses.java @@ -5,9 +5,15 @@ import jakarta.ws.rs.core.Response; import org.hibernate.PropertyValueException; -public class Responses { +/** + *

Utility class providing common helper methods for generating error messages. + *

+ * + *

Static members intended to assist with human-readable responses for the caller.

+ */ +public final class Responses { /** - * Generate an error response + * Generate an error response (also details which property if missing) * @param e Whatever exception has been thrown * @return A 400 response containing the exception error. */ diff --git a/src/main/java/org/uksrc/archive/utils/tools/Tools.java b/src/main/java/org/uksrc/archive/utils/tools/Tools.java index dde2d18..804a610 100644 --- a/src/main/java/org/uksrc/archive/utils/tools/Tools.java +++ b/src/main/java/org/uksrc/archive/utils/tools/Tools.java @@ -9,7 +9,13 @@ import java.util.List; import java.util.StringJoiner; -public class Tools { +/** + *

Utility class providing common helper methods for web requests. + *

+ * + *

Intended to contain supportive functionality.

+ */ +public final class Tools { /** * Performs the supplied query (with or without the pagination parameters) * @param page zero-indexed page index diff --git a/src/test/java/org/uksrc/archive/ObservationResourceTest.java b/src/test/java/org/uksrc/archive/ObservationResourceTest.java index 5663dca..a89b98b 100644 --- a/src/test/java/org/uksrc/archive/ObservationResourceTest.java +++ b/src/test/java/org/uksrc/archive/ObservationResourceTest.java @@ -96,7 +96,7 @@ public void testGettingObservationsNonEmpty() { @ParameterizedTest @DisplayName("Add an observation and check that part of the response body matches.") - @ValueSource(strings = {XML_OBSERVATION, XML_DERIVED_OBSERVATION}) + @ValueSource(strings = {XML_OBSERVATION/*, XML_DERIVED_OBSERVATION*/}) public void testAddingObservation(String observation) { String uniqueObservation = String.format(observation, "123", COLLECTION1); @@ -105,7 +105,7 @@ public void testAddingObservation(String observation) { .header("Content-Type", "application/xml") .body(uniqueObservation) .when() - .put("/observations/add") + .put("/observations/simple/add") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is("123")) // XML expectation (remove 'simpleObservation.' for JSON) @@ -122,7 +122,7 @@ public void testAddingDuplicateObservation() { .header("Content-Type", "application/xml") .body(duplicateObservation) .when() - .put("/observations/add") + .put("/observations/derived/add") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is("256")); @@ -132,7 +132,7 @@ public void testAddingDuplicateObservation() { .header("Content-Type", "application/xml") .body(duplicateObservation) .when() - .put("/observations/add") + .put("/observations/derived/add") .then() .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) .body(containsString("duplicate key value violates unique constraint")); @@ -147,7 +147,7 @@ public void testAddingJunkObservation() { .header("Content-Type", "application/xml") .body(junkData) .when() - .put("/observations/add") + .put("/observations/simple/add") .then() .statusCode(Response.Status.BAD_REQUEST.getStatusCode()); } @@ -166,7 +166,7 @@ public void testAddingIncompleteObservation() { .header("Content-Type", "application/xml") .body(INCOMPLETE_XML_OBSERVATION) .when() - .put("/observations/add") + .put("/observations/simple/add") .then() .statusCode(Response.Status.BAD_REQUEST.getStatusCode()); } @@ -182,7 +182,7 @@ public void testUpdatingObservation() { .header("Content-Type", "application/xml") .body(uniqueObservation) .when() - .put("/observations/add") + .put("/observations/simple/add") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is(ID)) @@ -194,7 +194,7 @@ public void testUpdatingObservation() { .header("Content-Type", "application/xml") .body(updatedObservation) .when() - .post(("/observations/update/" + ID)) + .post(("/observations/simple/update/" + ID)) .then() .statusCode(Response.Status.OK.getStatusCode()) .body("simpleObservation.id", is(ID)) @@ -348,7 +348,7 @@ private Response addObservationToDatabase(String observationId, String collectio .header("Content-Type", "application/xml") .body(uniqueObservation) .when() - .put("/observations/add") + .put("/observations/simple/add") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is(observationId)); From f5ad1a8f492e77fefafb6f046a9f08b165cac668 Mon Sep 17 00:00:00 2001 From: StephenL Date: Wed, 2 Oct 2024 16:36:31 +0100 Subject: [PATCH 03/11] - Updated examples on details.md - body reader added to assist with conversion from observation to derived (XML->Java) - Helper exception handler to catch JAXB conversion errors (and others) - Simple "submit either Simple or Derived" observation --- detail.md | 24 +++++---- .../uksrc/archive/ObservationResource.java | 25 +++++++-- .../archive/utils/CustomObjectMapper.java | 3 ++ .../archive/utils/GenericExceptionMapper.java | 18 +++++++ .../utils/ObservationMessageBodyReader.java | 53 +++++++++++++++++++ 5 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java create mode 100644 src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java diff --git a/detail.md b/detail.md index 3a569b6..3453c60 100644 --- a/detail.md +++ b/detail.md @@ -5,16 +5,17 @@ Details of the functionality of the archive-service endpoints. ------------------------------------------------------------------------------------------ Example resources suitable for **minimal** testing (Mandatory properties only). #### Example Simple Observation +Namespace details must conform with the current vo-dml model used. ```xml - - 123456 + + 988 e-merlin - science auri - + science + ``` -Type has to be set for JSON +*@type* has to be set for JSON ```json { "@type": "caom2:caom2.SimpleObservation", @@ -26,13 +27,14 @@ Type has to be set for JSON ``` #### Example Derived Observation ```xml - - 999 - e-merlin - science + + 10 + test auri - anyURI - + science + jbo-simple1 + jbo-simple2 + ``` ------------------------------------------------------------------------------------------ ### REST API details diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index c4cc02d..f19cc2b 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -20,7 +20,6 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.ivoa.dm.caom2.caom2.DerivedObservation; import org.ivoa.dm.caom2.caom2.Observation; -import org.ivoa.dm.caom2.caom2.ObservationIntentType; import org.ivoa.dm.caom2.caom2.SimpleObservation; import org.uksrc.archive.utils.ObservationListWrapper; import org.uksrc.archive.utils.responses.Responses; @@ -416,22 +415,36 @@ public Response getCollections(){ } } + //Test method only - remove when required /* @GET @Path("/collections/test") @Produces(MediaType.APPLICATION_XML) public Response getObservation() { // Create a SimpleObservation object (could be fetched from DB or any source) - SimpleObservation observation = new SimpleObservation(); + DerivedObservation observation = new DerivedObservation(); observation.setId("10"); observation.setCollection("test"); observation.setIntent(ObservationIntentType.SCIENCE); observation.setUri("auri"); - //observation.setMembers(new ArrayList("someone")); + + List members = new ArrayList<>(); + members.add("jbo-simple1"); + members.add("jbo-simple2"); + observation.setMembers(members); // Return as JSON return Response.ok(observation).build(); }*/ + @PUT + @Path("/addany") + @Consumes(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML}) + @Transactional + public Response addAny(Observation observation) { + return submitObservation(observation); + } + /** * Adds an observation to the database @@ -450,6 +463,12 @@ private Response submitObservation(Observation observation) { .build(); } + /** + * Updates the observation with the supplied Id with the details supplied + * @param id The observation to update + * @param observation The body of the observation to update + * @return Response containing the status of operation + */ private Response updateObservation(String id, Observation observation) { try { //Only update IF found diff --git a/src/main/java/org/uksrc/archive/utils/CustomObjectMapper.java b/src/main/java/org/uksrc/archive/utils/CustomObjectMapper.java index 7124e5c..5f854f7 100644 --- a/src/main/java/org/uksrc/archive/utils/CustomObjectMapper.java +++ b/src/main/java/org/uksrc/archive/utils/CustomObjectMapper.java @@ -7,6 +7,9 @@ import jakarta.inject.Singleton; import org.ivoa.dm.caom2.Caom2Model; +/** + * Helper class for the automatic xml/json to Java object + */ public class CustomObjectMapper { private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory .getLogger(CustomObjectMapper.class); diff --git a/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java b/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java new file mode 100644 index 0000000..e978ca7 --- /dev/null +++ b/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java @@ -0,0 +1,18 @@ +package org.uksrc.archive.utils; + +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class GenericExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(Exception exception) { + exception.printStackTrace(); + + return Response.status(Response.Status.BAD_REQUEST) + .entity("Error: " + exception.getMessage()) + .build(); + } +} diff --git a/src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java b/src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java new file mode 100644 index 0000000..459452b --- /dev/null +++ b/src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java @@ -0,0 +1,53 @@ +package org.uksrc.archive.utils; + +import jakarta.ws.rs.BadRequestException; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.MultivaluedMap; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.MessageBodyReader; +import jakarta.ws.rs.ext.Provider; +import jakarta.xml.bind.*; +import org.ivoa.dm.caom2.caom2.DerivedObservation; +import org.ivoa.dm.caom2.caom2.Observation; +import org.ivoa.dm.caom2.caom2.SimpleObservation; + +import java.io.*; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +/** + * Custom "interceptor" for Observation messages to assist with the instantiation of Observation derived classes. + * As Observation is an abstract class, JAXB requires assistance determining which subtype to instantiate. + * Called when an Observation (subtype typically) is posted to one of the endpoints @see ObservationResource. + */ +@Provider +@Consumes(MediaType.APPLICATION_XML) +public class ObservationMessageBodyReader implements MessageBodyReader { + + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Observation.class.isAssignableFrom(type); + } + + @Override + public Observation readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) { + try { + JAXBContext context = JAXBContext.newInstance(Observation.class, SimpleObservation.class, DerivedObservation.class); + Unmarshaller unmarshaller = context.createUnmarshaller(); + + Object result = unmarshaller.unmarshal(entityStream); + if (result instanceof JAXBElement element) { + if (element.getValue() instanceof Observation) { + return (Observation) element.getValue(); + } + } + throw new BadRequestException("Invalid XML: Unable to parse Observation object"); + } catch (JAXBException e) { + throw new WebApplicationException("Error deserializing XML", e, Response.Status.BAD_REQUEST); + } + } +} + From 45d017390ebdce76b48aa6048ce1725098bec2bc Mon Sep 17 00:00:00 2001 From: StephenL Date: Thu, 3 Oct 2024 14:53:34 +0100 Subject: [PATCH 04/11] - example resource added - Single API for observation added --- .../uksrc/archive/ObservationResource.java | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index f19cc2b..f0fcd18 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -13,6 +13,7 @@ import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.Parameters; @@ -43,7 +44,16 @@ public class ObservationResource { content = { @Content( mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = SimpleObservation.class) + schema = @Schema(implementation = SimpleObservation.class), + examples = @ExampleObject( + name = "XML Example - SimpleObservation", + value = "\n" + + " 1\n" + + " test\n" + + " auri\n" + + " science\n" + + "" + ) ), @Content( mediaType = MediaType.APPLICATION_JSON, @@ -100,6 +110,39 @@ public Response addObservation(DerivedObservation observation) { return submitObservation(observation); } + @POST + @Path("/add") + @Operation(summary = "Create a new Observation", description = "Create an observation in the database. Note, ID must be unique across all observations. The observation can be either Simple or Derived but the XML namespaces/JSON types must be present.") + @RequestBody( + description = "XML or JSON representation of the Observation", + required = true, + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, + schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ) + } + ) + @APIResponse( + responseCode = "201", + description = "Observation created successfully", + content = @Content(schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class})) + ) + @APIResponse( + responseCode = "400", + description = "Invalid input" + ) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Transactional + public Response addAny(Observation observation) { + return submitObservation(observation); + } + @POST @Path("/simple/update/{observationId}") @Operation(summary = "Update an existing Observation", description = "Updates an existing observation with the supplied ID") @@ -436,16 +479,6 @@ public Response getObservation() { return Response.ok(observation).build(); }*/ - @PUT - @Path("/addany") - @Consumes(MediaType.APPLICATION_XML) - @Produces({MediaType.APPLICATION_XML}) - @Transactional - public Response addAny(Observation observation) { - return submitObservation(observation); - } - - /** * Adds an observation to the database * @param observation Either a SimpleObservation or a DerivedObservation From afbc0156564970dac02723f553985fe26793f0ab Mon Sep 17 00:00:00 2001 From: StephenL Date: Thu, 3 Oct 2024 15:30:28 +0100 Subject: [PATCH 05/11] Some merge conflict mistakes resolved --- .../uksrc/archive/ObservationResource.java | 140 +++--------------- .../archive/ObservationResourceTest.java | 24 +-- 2 files changed, 32 insertions(+), 132 deletions(-) diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index fff8a36..41cf84f 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -13,17 +13,17 @@ import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.ExampleObject; import org.eclipse.microprofile.openapi.annotations.media.Schema; import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import org.eclipse.microprofile.openapi.annotations.parameters.Parameters; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; -import org.hibernate.PropertyValueException; -import org.ivoa.dm.caom2.caom2.DerivedObservation; import org.ivoa.dm.caom2.caom2.Observation; import org.ivoa.dm.caom2.caom2.SimpleObservation; -import jakarta.validation.constraints.NotNull; import org.uksrc.archive.utils.ObservationListWrapper; +import org.uksrc.archive.utils.responses.Responses; +import org.uksrc.archive.utils.tools.Tools; import java.util.*; @@ -71,40 +71,7 @@ public class ObservationResource { @Consumes(MediaType.APPLICATION_XML) @Produces(MediaType.APPLICATION_XML) @Transactional - public Response addObservation(SimpleObservation observation) { - return submitObservation(observation); - } - - @PUT - @Path("/derived/add") - @Operation(summary = "Create a new Derived Observation", description = "Create a DERIVED observation in the database, note ID must be unique across all observations.") - @RequestBody( - description = "XML representation of the Derived Observation", - required = true, - content = { - @Content( - mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = SimpleObservation.class) - ), - @Content( - mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = SimpleObservation.class) - ) - } - ) - @APIResponse( - responseCode = "201", - description = "Observation created successfully", - content = @Content(schema = @Schema(implementation = DerivedObservation.class)) - ) - @APIResponse( - responseCode = "400", - description = "Invalid input" - ) - @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) - @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) - @Transactional - public Response addObservation(DerivedObservation observation) { + public Response addObservation(Observation observation) { return submitObservation(observation); } @@ -151,7 +118,7 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO return Response.ok(observation).build(); } } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } return Response.status(Response.Status.NOT_FOUND) @@ -191,19 +158,19 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO @Produces(MediaType.APPLICATION_XML) public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { - return errorResponse("Both 'page' and 'size' must be provided together or neither."); + return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); } try { if (page != null && (page < 0 || size < 1)) { - return errorResponse("Page must be 0 or greater and size must be greater than 0."); + return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); } // Create query and apply pagination if required TypedQuery query = em.createQuery("SELECT o FROM Observation o", Observation.class); - return performQuery(page, size, query); + return Tools.performQuery(page, size, query); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -245,25 +212,25 @@ public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam @Produces(MediaType.APPLICATION_XML) public Response getObservations(@PathParam("collectionId") String collection, @QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { - return errorResponse("Both 'page' and 'size' must be provided together or neither."); + return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); } try { if (page != null && (page < 0 || size < 1)) { - return errorResponse("Page must be 0 or greater and size must be greater than 0."); + return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); } TypedQuery query = em.createQuery("SELECT o FROM Observation o WHERE o.collection = :collection", Observation.class); query.setParameter("collection", collection); - return performQuery(page, size, query); + return Tools.performQuery(page, size, query); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @GET @Path("/{observationId}") - @Operation(summary = "Retrieve observations from a collection", description = "Returns a list of observations that are members of the supplied collection") + @Operation(summary = "Retrieve an observation", description = "Returns an observation with the supplied ID.") @Parameters({ @Parameter( name = "observationId", @@ -300,12 +267,12 @@ public Response getObservation(@PathParam("observationId") String observationId) .entity("Observation with ID " + observationId + " not found").build(); } } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @DELETE - @Path("/delete/{observationId}") + @Path("/{observationId}") @Operation(summary = "Delete an existing observation") @Parameters({ @Parameter( @@ -341,7 +308,7 @@ public Response deleteObservation(@PathParam("observationId") String id) { .build(); } } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -367,10 +334,10 @@ public Response getCollections(){ return Response.ok() .type(MediaType.TEXT_PLAIN) - .entity(convertListToTsv(uniqueCollections)) + .entity(Tools.convertListToTsv(uniqueCollections)) .build(); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } } @@ -384,77 +351,10 @@ private Response submitObservation(Observation observation) { em.persist(observation); em.flush(); } catch (Exception e) { - return errorResponse(e); + return Responses.errorResponse(e); } return Response.status(Response.Status.CREATED) .entity(observation) .build(); } - - /** - * Generate an error response - * @param e Whatever exception has been thrown - * @return A 400 response containing the exception error. - */ - private Response errorResponse (@NotNull Exception e){ - String additional = ""; - if (e instanceof PropertyValueException){ - //Inform caller of exact property that's missing/invalid - additional = ((PropertyValueException)e).getPropertyName(); - } - return Response.status(Response.Status.BAD_REQUEST) - .type(MediaType.TEXT_PLAIN) - .entity(e.getMessage() + " " + additional) - .build(); - } - - /** - * Generate an error message - * @param message Message to return to the caller. - * @return A 400 response containing the supplied message - */ - private Response errorResponse (@NotNull String message){ - return Response.status(Response.Status.BAD_REQUEST) - .type(MediaType.TEXT_PLAIN) - .entity(message) - .build(); - } - - /** - * Performs the supplied query (with or without the pagination parameters) - * @param page zero-indexed page index - * @param size number of entries per page - * @param query query to perform - * @return Response containing HTTP response code and expected body if successful. - */ - private Response performQuery(Integer page, Integer size, TypedQuery query) { - try { - if (page != null && size != null) { - int firstResult = page * size; - query.setFirstResult(firstResult); - query.setMaxResults(size); - } - - List observations = query.getResultList(); - ObservationListWrapper wrapper = new ObservationListWrapper(observations); - - return Response.ok(wrapper).build(); - } catch (Exception e) { - return errorResponse(e); - } - } - - /** - * Converts a List of strings to a TSV - * @param list The list of elements to convert to a TSV string. - * @return list of items "e-merlin test ALMA" - */ - public String convertListToTsv(List list) { - StringJoiner joiner = new StringJoiner("\t"); - - for (String item : list) { - joiner.add(item); - } - return joiner.toString(); - } } diff --git a/src/test/java/org/uksrc/archive/ObservationResourceTest.java b/src/test/java/org/uksrc/archive/ObservationResourceTest.java index 4a0d127..b952225 100644 --- a/src/test/java/org/uksrc/archive/ObservationResourceTest.java +++ b/src/test/java/org/uksrc/archive/ObservationResourceTest.java @@ -31,20 +31,20 @@ public class ObservationResourceTest { //Caution with the id value if re-using. - private static final String XML_OBSERVATION = "" + + private static final String XML_OBSERVATION = "" + "%s" + "%s" + "science" + "auri" + - ""; + ""; - private static final String XML_DERIVED_OBSERVATION = "" + + private static final String XML_DERIVED_OBSERVATION = "" + "%s" + "e-merlin" + "science" + "auri" + "someone" + - ""; + ""; private static final String COLLECTION1 = "e-merlin"; private static final String COLLECTION2 = "testCollection"; @@ -105,7 +105,7 @@ public void testAddingObservation(String observation) { .header("Content-Type", "application/xml") .body(uniqueObservation) .when() - .post("/observations/add") + .post("/observations") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is("123")) // XML expectation (remove 'simpleObservation.' for JSON) @@ -122,7 +122,7 @@ public void testAddingDuplicateObservation() { .header("Content-Type", "application/xml") .body(duplicateObservation) .when() - .post("/observations/add") + .post("/observations") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is("256")); @@ -132,7 +132,7 @@ public void testAddingDuplicateObservation() { .header("Content-Type", "application/xml") .body(duplicateObservation) .when() - .post("/observations/add") + .post("/observations") .then() .statusCode(Response.Status.BAD_REQUEST.getStatusCode()) .body(containsString("duplicate key value violates unique constraint")); @@ -182,7 +182,7 @@ public void testUpdatingObservation() { .header("Content-Type", "application/xml") .body(uniqueObservation) .when() - .post("/observations/add") + .post("/observations") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is(ID)) @@ -194,7 +194,7 @@ public void testUpdatingObservation() { .header("Content-Type", "application/xml") .body(updatedObservation) .when() - .put(("/observations/update/" + ID)) + .put(("/observations/" + ID)) .then() .statusCode(Response.Status.OK.getStatusCode()) .body("simpleObservation.id", is(ID)) @@ -224,7 +224,7 @@ public void testUpdatingNonExistingObservation() { .header("Content-Type", "application/xml") .body(updatedObservation) .when() - .put(("/observations/update/" + ID)) + .put(("/observations/" + ID)) .then() .statusCode(Response.Status.NOT_FOUND.getStatusCode()); } @@ -329,7 +329,7 @@ public void testDeletingNonExistingObservation() { given() .header("Content-Type", "application/xml") .when() - .delete(("/observations/delete/" + "9876")) + .delete(("/observations/" + "9876")) .then() .statusCode(Response.Status.NOT_FOUND.getStatusCode()); } @@ -348,7 +348,7 @@ private Response addObservationToDatabase(String observationId, String collectio .header("Content-Type", "application/xml") .body(uniqueObservation) .when() - .post("/observations/add") + .post("/observations") .then() .statusCode(Response.Status.CREATED.getStatusCode()) .body("simpleObservation.id", is(observationId)); From 6c6fc6ea7afe7865c7acb8081944220898bd2e3f Mon Sep 17 00:00:00 2001 From: StephenL Date: Fri, 4 Oct 2024 11:08:53 +0100 Subject: [PATCH 06/11] Organise method headers --- .../uksrc/archive/ObservationResource.java | 187 ++++++++++++++---- 1 file changed, 153 insertions(+), 34 deletions(-) diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index 41cf84f..3920f94 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -19,6 +19,7 @@ import org.eclipse.microprofile.openapi.annotations.parameters.Parameters; import org.eclipse.microprofile.openapi.annotations.parameters.RequestBody; import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.ivoa.dm.caom2.caom2.DerivedObservation; import org.ivoa.dm.caom2.caom2.Observation; import org.ivoa.dm.caom2.caom2.SimpleObservation; import org.uksrc.archive.utils.ObservationListWrapper; @@ -35,41 +36,84 @@ public class ObservationResource { protected EntityManager em; // exists for the application lifetime no need to close @POST - @Operation(summary = "Create a new Observation", description = "Creates a new observation in the database, note the supplied ID needs to be unique.") + @Operation(summary = "Create a new Observation", description = "Creates a new observation in the database, note the supplied ID needs to be unique and XML namespace/JSON type supplied.") @RequestBody( description = "XML representation of the Observation", required = true, content = { @Content( mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = SimpleObservation.class), - examples = @ExampleObject( - name = "XML Example - SimpleObservation", - value = "\n" + - " 1\n" + - " test\n" + - " auri\n" + - " science\n" + - "" - ) + schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}), + examples = { + @ExampleObject( + name = "XML Example - SimpleObservation", + value = "" + + "1" + + "test" + + "auri" + + "science" + + "" + ), + @ExampleObject( + name = "XML Example - DerivedObservation", + value = "" + + "1" + + "test" + + "auri" + + "science" + + "jbo-simple1" + + "jbo-simple2" + + "" + ) + } ), @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = SimpleObservation.class) + schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}), + examples = { + @ExampleObject( + name = "JSON Example - SimpleObservation", + value = """ + { + "@type": "caom2:caom2.SimpleObservation", + "id": "3", + "collection": "test", + "uri": "auri", + "intent": "science", + } + """ + ), + @ExampleObject( + name = "JSON Example - DerivedObservation", + value = """ + { + "@type": "caom2:caom2.DerivedObservation", + "id": "3", + "collection": "test", + "uri": "auri", + "intent": "science", + "members": [ + "jbo-simple1", + "jbo-simple2" + ] + } + """ + ) + } ) } ) @APIResponse( responseCode = "201", description = "Observation created successfully", - content = @Content(schema = @Schema(implementation = SimpleObservation.class)) + content = @Content(schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class})) ) @APIResponse( responseCode = "400", description = "Invalid input" ) - @Consumes(MediaType.APPLICATION_XML) - @Produces(MediaType.APPLICATION_XML) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Transactional public Response addObservation(Observation observation) { return submitObservation(observation); @@ -87,15 +131,73 @@ public Response addObservation(Observation observation) { @RequestBody( description = "XML representation of the Observation", required = true, - content = @Content( - mediaType = MediaType.APPLICATION_XML, - schema = @Schema(implementation = Observation.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, + schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}), + examples = { + @ExampleObject( + name = "XML Example - SimpleObservation", + value = "" + + "1" + + "test" + + "auri" + + "science" + + "" + ), + @ExampleObject( + name = "XML Example - DerivedObservation", + value = "" + + "1" + + "test" + + "auri" + + "science" + + "jbo-simple1" + + "jbo-simple2" + + "" + ) + } + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}), + examples = { + @ExampleObject( + name = "JSON Example - SimpleObservation", + value = """ + { + "@type": "caom2:caom2.SimpleObservation", + "id": "3", + "collection": "test", + "uri": "auri", + "intent": "science", + } + """ + ), + @ExampleObject( + name = "JSON Example - DerivedObservation", + value = """ + { + "@type": "caom2:caom2.DerivedObservation", + "id": "3", + "collection": "test", + "uri": "auri", + "intent": "science", + "members": [ + "jbo-simple1", + "jbo-simple2" + ] + } + """ + ) + } + ) + } ) @APIResponse( responseCode = "200", description = "Observation updated successfully", - content = @Content(schema = @Schema(implementation = Observation.class)) + content = @Content(schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class})) ) @APIResponse( responseCode = "404", @@ -105,8 +207,8 @@ public Response addObservation(Observation observation) { responseCode = "400", description = "Invalid input" ) - @Consumes(MediaType.APPLICATION_XML) - @Produces(MediaType.APPLICATION_XML) + @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Transactional public Response updateObservation(@PathParam("observationId") String id, SimpleObservation observation) { try { @@ -147,15 +249,21 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO @APIResponse( responseCode = "200", description = "List of observations retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = ObservationListWrapper.class) - ) + content = { + @Content( + //Technically it should be ObservationListWrapper containing a list of: + mediaType = MediaType.APPLICATION_XML, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ) + } ) @APIResponse( responseCode = "400", description = "Internal error whilst retrieving Observations or parameter error (if supplied)." ) - @Produces(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); @@ -201,15 +309,21 @@ public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam @APIResponse( responseCode = "200", description = "List of observations retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = ObservationListWrapper.class) - ) + content = { + @Content( + //Technically it should be ObservationListWrapper containing a list of: + mediaType = MediaType.APPLICATION_XML, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ) + } ) @APIResponse( responseCode = "400", description = "Internal error whilst retrieving Observations." ) - @Produces(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getObservations(@PathParam("collectionId") String collection, @QueryParam("page") Integer page, @QueryParam("size") Integer size) { if ((page != null && size == null) || (page == null && size != null)) { return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); @@ -242,9 +356,14 @@ public Response getObservations(@PathParam("collectionId") String collection, @Q @APIResponse( responseCode = "200", description = "Observation retrieved successfully", - content = @Content( - mediaType = MediaType.APPLICATION_XML, schema = @Schema(implementation = Observation.class) - ) + content = { + @Content( + mediaType = MediaType.APPLICATION_XML, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ), + @Content( + mediaType = MediaType.APPLICATION_JSON, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) + ) + } ) @APIResponse( responseCode = "404", @@ -254,7 +373,7 @@ public Response getObservations(@PathParam("collectionId") String collection, @Q responseCode = "400", description = "Internal error whilst retrieving Observations." ) - @Produces(MediaType.APPLICATION_XML) + @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public Response getObservation(@PathParam("observationId") String observationId) { try { Observation observation = em.find(Observation.class, observationId); From ab0c1e2dc26ab3e8c54287c93d76b75b9dcce102 Mon Sep 17 00:00:00 2001 From: StephenL Date: Fri, 4 Oct 2024 12:01:58 +0100 Subject: [PATCH 07/11] Removed default test class Updated XML examples for consistency with the JSON ones Removed the overkill object mapper for subtypes of observation --- .../uksrc/archive/ObservationResource.java | 61 ++++++++++--------- .../utils/ObservationMessageBodyReader.java | 53 ---------------- ...urceIT.java => ObservationResourceIT.java} | 2 +- .../uksrc/archive/GreetingResourceTest.java | 20 ------ 4 files changed, 33 insertions(+), 103 deletions(-) delete mode 100644 src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java rename src/native-test/java/org/uksrc/archive/{GreetingResourceIT.java => ObservationResourceIT.java} (72%) delete mode 100644 src/test/java/org/uksrc/archive/GreetingResourceTest.java diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index 3920f94..b15eb11 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -22,7 +22,6 @@ import org.ivoa.dm.caom2.caom2.DerivedObservation; import org.ivoa.dm.caom2.caom2.Observation; import org.ivoa.dm.caom2.caom2.SimpleObservation; -import org.uksrc.archive.utils.ObservationListWrapper; import org.uksrc.archive.utils.responses.Responses; import org.uksrc.archive.utils.tools.Tools; @@ -47,23 +46,25 @@ public class ObservationResource { examples = { @ExampleObject( name = "XML Example - SimpleObservation", - value = "" + - "1" + - "test" + - "auri" + - "science" + - "" + value = """ + + 1 + test + auri + science + """ ), @ExampleObject( name = "XML Example - DerivedObservation", - value = "" + - "1" + - "test" + - "auri" + - "science" + - "jbo-simple1" + - "jbo-simple2" + - "" + value = """ + + 1 + test + auri + science + jbo-simple1 + jbo-simple2 + """ ) } ), @@ -138,23 +139,25 @@ public Response addObservation(Observation observation) { examples = { @ExampleObject( name = "XML Example - SimpleObservation", - value = "" + - "1" + - "test" + - "auri" + - "science" + - "" + value = """ + + 1 + test + auri + science + """ ), @ExampleObject( name = "XML Example - DerivedObservation", - value = "" + - "1" + - "test" + - "auri" + - "science" + - "jbo-simple1" + - "jbo-simple2" + - "" + value = """ + + 1 + test + auri + science + jbo-simple1 + jbo-simple2 + """ ) } ), diff --git a/src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java b/src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java deleted file mode 100644 index 459452b..0000000 --- a/src/main/java/org/uksrc/archive/utils/ObservationMessageBodyReader.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.uksrc.archive.utils; - -import jakarta.ws.rs.BadRequestException; -import jakarta.ws.rs.Consumes; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.MultivaluedMap; -import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.ext.MessageBodyReader; -import jakarta.ws.rs.ext.Provider; -import jakarta.xml.bind.*; -import org.ivoa.dm.caom2.caom2.DerivedObservation; -import org.ivoa.dm.caom2.caom2.Observation; -import org.ivoa.dm.caom2.caom2.SimpleObservation; - -import java.io.*; -import java.lang.annotation.Annotation; -import java.lang.reflect.Type; - -/** - * Custom "interceptor" for Observation messages to assist with the instantiation of Observation derived classes. - * As Observation is an abstract class, JAXB requires assistance determining which subtype to instantiate. - * Called when an Observation (subtype typically) is posted to one of the endpoints @see ObservationResource. - */ -@Provider -@Consumes(MediaType.APPLICATION_XML) -public class ObservationMessageBodyReader implements MessageBodyReader { - - @Override - public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { - return Observation.class.isAssignableFrom(type); - } - - @Override - public Observation readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, - MultivaluedMap httpHeaders, InputStream entityStream) { - try { - JAXBContext context = JAXBContext.newInstance(Observation.class, SimpleObservation.class, DerivedObservation.class); - Unmarshaller unmarshaller = context.createUnmarshaller(); - - Object result = unmarshaller.unmarshal(entityStream); - if (result instanceof JAXBElement element) { - if (element.getValue() instanceof Observation) { - return (Observation) element.getValue(); - } - } - throw new BadRequestException("Invalid XML: Unable to parse Observation object"); - } catch (JAXBException e) { - throw new WebApplicationException("Error deserializing XML", e, Response.Status.BAD_REQUEST); - } - } -} - diff --git a/src/native-test/java/org/uksrc/archive/GreetingResourceIT.java b/src/native-test/java/org/uksrc/archive/ObservationResourceIT.java similarity index 72% rename from src/native-test/java/org/uksrc/archive/GreetingResourceIT.java rename to src/native-test/java/org/uksrc/archive/ObservationResourceIT.java index b1341ae..2cccafa 100644 --- a/src/native-test/java/org/uksrc/archive/GreetingResourceIT.java +++ b/src/native-test/java/org/uksrc/archive/ObservationResourceIT.java @@ -3,6 +3,6 @@ import io.quarkus.test.junit.QuarkusIntegrationTest; @QuarkusIntegrationTest -class GreetingResourceIT extends GreetingResourceTest { +class ObservationResourceIT extends ObservationResourceTest { // Execute the same tests but in packaged mode. } diff --git a/src/test/java/org/uksrc/archive/GreetingResourceTest.java b/src/test/java/org/uksrc/archive/GreetingResourceTest.java deleted file mode 100644 index e3ad839..0000000 --- a/src/test/java/org/uksrc/archive/GreetingResourceTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.uksrc.archive; - -import io.quarkus.test.junit.QuarkusTest; -import org.junit.jupiter.api.Test; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.CoreMatchers.is; - -@QuarkusTest -class GreetingResourceTest { - @Test - void testHelloEndpoint() { - given() - .when().get("/hello") - .then() - .statusCode(200) - .body(is("Hello from RESTEasy Reactive")); - } - -} \ No newline at end of file From b60914703feaec675db81a5ba4aa6b809fddf2a8 Mon Sep 17 00:00:00 2001 From: StephenL Date: Fri, 4 Oct 2024 15:08:38 +0100 Subject: [PATCH 08/11] Separated Collections from Observations Integrated retrieving a subset of Observations based on a collections ID into a single function (either supply an id for a subset or not for all observations) --- .../org/uksrc/archive/CollectionResource.java | 52 +++++++++ .../uksrc/archive/ObservationResource.java | 109 +++--------------- .../uksrc/archive/CollectionResourceTest.java | 57 +++++++++ .../archive/ObservationResourceTest.java | 62 +--------- .../org/uksrc/archive/utils/Utilities.java | 44 +++++++ 5 files changed, 174 insertions(+), 150 deletions(-) create mode 100644 src/main/java/org/uksrc/archive/CollectionResource.java create mode 100644 src/test/java/org/uksrc/archive/CollectionResourceTest.java create mode 100644 src/test/java/org/uksrc/archive/utils/Utilities.java diff --git a/src/main/java/org/uksrc/archive/CollectionResource.java b/src/main/java/org/uksrc/archive/CollectionResource.java new file mode 100644 index 0000000..3f1f3bc --- /dev/null +++ b/src/main/java/org/uksrc/archive/CollectionResource.java @@ -0,0 +1,52 @@ +package org.uksrc.archive; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.TypedQuery; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.eclipse.microprofile.openapi.annotations.media.Content; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; +import org.uksrc.archive.utils.responses.Responses; +import org.uksrc.archive.utils.tools.Tools; + +import java.util.List; + +@Path("/collections") +public class CollectionResource { + + @PersistenceContext + protected EntityManager em; + + @GET + @Path("/") + @Operation(summary = "Retrieve all collection IDs", description = "Returns a list of unique collectionIds as a TSV (Tab Separated List).") + @APIResponse( + responseCode = "200", + description = "CollectionIds retrieved successfully", + content = @Content( + mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class) + ) + ) + @APIResponse( + responseCode = "400", + description = "Internal error whilst retrieving collectionIds." + ) + @Produces(MediaType.TEXT_PLAIN) + public Response getCollections(){ + try { + TypedQuery query = em.createQuery("SELECT DISTINCT o.collection FROM Observation o", String.class); + List uniqueCollections = query.getResultList(); + + return Response.ok() + .type(MediaType.TEXT_PLAIN) + .entity(Tools.convertListToTsv(uniqueCollections)) + .build(); + } catch (Exception e) { + return Responses.errorResponse(e); + } + } +} diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index b15eb11..3c712a7 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -235,66 +235,12 @@ public Response updateObservation(@PathParam("observationId") String id, SimpleO @GET @Path("/") @Operation(summary = "Retrieve list(s) of observations", description = "Returns either all the Observations currently stored or a subset using pagination IF page AND size are supplied.") - @Parameters({ - @Parameter( - name = "page", - description = "The page number to retrieve, zero-indexed. If not provided, ALL results are returned.", - in = ParameterIn.QUERY, - schema = @Schema(type = SchemaType.INTEGER, minimum = "0") - ), - @Parameter( - name = "size", - description = "The number of observations per page. If not provided, ALL results are returned.", - in = ParameterIn.QUERY, - schema = @Schema(type = SchemaType.INTEGER, minimum = "1") - ) - }) - @APIResponse( - responseCode = "200", - description = "List of observations retrieved successfully", - content = { - @Content( - //Technically it should be ObservationListWrapper containing a list of: - mediaType = MediaType.APPLICATION_XML, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) - ), - @Content( - mediaType = MediaType.APPLICATION_JSON, schema = @Schema(oneOf = {SimpleObservation.class, DerivedObservation.class}) - ) - } - ) - @APIResponse( - responseCode = "400", - description = "Internal error whilst retrieving Observations or parameter error (if supplied)." - ) - @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) - public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam("size") Integer size) { - if ((page != null && size == null) || (page == null && size != null)) { - return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); - } - - try { - if (page != null && (page < 0 || size < 1)) { - return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); - } - - // Create query and apply pagination if required - TypedQuery query = em.createQuery("SELECT o FROM Observation o", Observation.class); - return Tools.performQuery(page, size, query); - } catch (Exception e) { - return Responses.errorResponse(e); - } - } - - @GET - @Path("/collection/{collectionId}") - @Operation(summary = "Retrieve observations from a collection", description = "Returns a list of observations that are members of the supplied collection") @Parameters({ @Parameter( name = "collectionId", - description = "The collection name to retrieve observations for", - in = ParameterIn.PATH, - required = true, - example = "e-merlin" + description = "Filter the results by a collection id if required.", + in = ParameterIn.QUERY, + schema = @Schema(type = SchemaType.STRING) ), @Parameter( name = "page", @@ -324,21 +270,25 @@ public Response getAllObservations(@QueryParam("page") Integer page, @QueryParam ) @APIResponse( responseCode = "400", - description = "Internal error whilst retrieving Observations." + description = "Internal error whilst retrieving Observations or parameter error (if supplied)." ) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) - public Response getObservations(@PathParam("collectionId") String collection, @QueryParam("page") Integer page, @QueryParam("size") Integer size) { - if ((page != null && size == null) || (page == null && size != null)) { + public Response getAllObservations(@QueryParam("collectionId") String collection, @QueryParam("page") Integer page, @QueryParam("size") Integer size) { + //Both page and size need to be supplied OR neither + if ((page != null) ^ (size != null)) { return Responses.errorResponse("Both 'page' and 'size' must be provided together or neither."); + } else if ((page != null && page < 0) || (size != null && size < 1)) { + return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); } try { - if (page != null && (page < 0 || size < 1)) { - return Responses.errorResponse("Page must be 0 or greater and size must be greater than 0."); + TypedQuery query; + if (collection != null && !collection.isEmpty()) { + query = em.createQuery("SELECT o FROM Observation o WHERE o.collection = :collection", Observation.class); + query.setParameter("collection", collection); + } else { + query = em.createQuery("SELECT o FROM Observation o", Observation.class); } - - TypedQuery query = em.createQuery("SELECT o FROM Observation o WHERE o.collection = :collection", Observation.class); - query.setParameter("collection", collection); return Tools.performQuery(page, size, query); } catch (Exception e) { return Responses.errorResponse(e); @@ -434,35 +384,6 @@ public Response deleteObservation(@PathParam("observationId") String id) { } } - @GET - @Path("/collections") - @Operation(summary = "Retrieve all collection IDs", description = "Returns a list of unique collectionIds as a TSV (Tab Separated List).") - @APIResponse( - responseCode = "200", - description = "CollectionIds retrieved successfully", - content = @Content( - mediaType = MediaType.TEXT_PLAIN, schema = @Schema(implementation = String.class) - ) - ) - @APIResponse( - responseCode = "400", - description = "Internal error whilst retrieving collectionIds." - ) - @Produces(MediaType.TEXT_PLAIN) - public Response getCollections(){ - try { - TypedQuery query = em.createQuery("SELECT DISTINCT o.collection FROM Observation o", String.class); - List uniqueCollections = query.getResultList(); - - return Response.ok() - .type(MediaType.TEXT_PLAIN) - .entity(Tools.convertListToTsv(uniqueCollections)) - .build(); - } catch (Exception e) { - return Responses.errorResponse(e); - } - } - /** * Adds an observation to the database * @param observation Either a SimpleObservation or a DerivedObservation diff --git a/src/test/java/org/uksrc/archive/CollectionResourceTest.java b/src/test/java/org/uksrc/archive/CollectionResourceTest.java new file mode 100644 index 0000000..d4a18ab --- /dev/null +++ b/src/test/java/org/uksrc/archive/CollectionResourceTest.java @@ -0,0 +1,57 @@ +package org.uksrc.archive; + +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.uksrc.archive.utils.Utilities; + +import java.util.Arrays; +import java.util.List; + +import static io.restassured.RestAssured.when; + +@QuarkusTest +public class CollectionResourceTest { + + @Inject + EntityManager em; + + @BeforeEach + @Transactional + public void clearDatabase() { + // Clear the table + em.createQuery("DELETE FROM Observation").executeUpdate(); + } + + @Test + @DisplayName("Test retrieving collection Ids") + public void testRetrievingCollectionIds() { + for (int i = 0; i < 5; i++){ + Utilities.addObservationToDatabase(String.valueOf(i), Utilities.COLLECTION1); + } + + for (int i = 5; i < 12; i++){ + Utilities.addObservationToDatabase(String.valueOf(i), Utilities.COLLECTION2); + } + + String collections = when() + .get("/collections") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract() + .asString(); + + //Split the response on the tab separator + String[] collectionIds = collections.split("\t"); + List names = Arrays.asList(collectionIds); + + assert(names.size() == 2); + assert(names.contains(Utilities.COLLECTION1)); + assert(names.contains(Utilities.COLLECTION2)); + } +} diff --git a/src/test/java/org/uksrc/archive/ObservationResourceTest.java b/src/test/java/org/uksrc/archive/ObservationResourceTest.java index b952225..362602e 100644 --- a/src/test/java/org/uksrc/archive/ObservationResourceTest.java +++ b/src/test/java/org/uksrc/archive/ObservationResourceTest.java @@ -13,6 +13,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.uksrc.archive.utils.ObservationListWrapper; +import org.uksrc.archive.utils.Utilities; import java.util.Arrays; import java.util.List; @@ -77,8 +78,8 @@ public void testGettingObservations() { @Test @DisplayName("Add two observation and check two are returned.") public void testGettingObservationsNonEmpty() { - try(Response res1 = addObservationToDatabase("1234", COLLECTION1); - Response res2 = addObservationToDatabase("6789", COLLECTION1)) { + try(Response res1 = Utilities.addObservationToDatabase("1234", COLLECTION1); + Response res2 = Utilities.addObservationToDatabase("6789", COLLECTION1)) { assert (res1.getStatus() == Response.Status.CREATED.getStatusCode() && res2.getStatus() == Response.Status.CREATED.getStatusCode()); @@ -233,7 +234,7 @@ public void testUpdatingNonExistingObservation() { @DisplayName("Attempt to delete an observation.") public void testDeletingObservation() { final String ID = "256"; - try(Response res = addObservationToDatabase(ID, COLLECTION1)) { + try(Response res = Utilities.addObservationToDatabase(ID, COLLECTION1)) { assert (res.getStatus() == Response.Status.CREATED.getStatusCode()); // Check it exists @@ -258,7 +259,7 @@ public void testDeletingObservation() { @DisplayName("Test paging results, first page") public void testPagingResults() { for (int i = 0; i < 15; i++){ - addObservationToDatabase(String.valueOf(i), COLLECTION1); + Utilities.addObservationToDatabase(String.valueOf(i), COLLECTION1); } ObservationListWrapper wrapper = when() @@ -272,38 +273,11 @@ public void testPagingResults() { assert(wrapper.getObservations().size() == 10); } - @Test - @DisplayName("Test retrieving collection Ids") - public void testRetrievingCollectionIds() { - for (int i = 0; i < 5; i++){ - addObservationToDatabase(String.valueOf(i), COLLECTION1); - } - - for (int i = 5; i < 12; i++){ - addObservationToDatabase(String.valueOf(i), COLLECTION2); - } - - String collections = when() - .get("/observations/collections") - .then() - .statusCode(Response.Status.OK.getStatusCode()) - .extract() - .asString(); - - //Split the response on the tab separator - String[] collectionIds = collections.split("\t"); - List names = Arrays.asList(collectionIds); - - assert(names.size() == 2); - assert(names.contains(COLLECTION1)); - assert(names.contains(COLLECTION2)); - } - @Test @DisplayName("Test paging results, second page") public void testPagingResults2() { for (int i = 0; i < 15; i++){ - addObservationToDatabase(String.valueOf(i), COLLECTION1); + Utilities.addObservationToDatabase(String.valueOf(i), COLLECTION1); } ObservationListWrapper wrapper = when() @@ -333,28 +307,4 @@ public void testDeletingNonExistingObservation() { .then() .statusCode(Response.Status.NOT_FOUND.getStatusCode()); } - - /** - * Adds a SimpleObservation to the database with the supplied observationId - * @param observationId unique identifier for the observation - * @param collectionId identifier for the collection to add this observation to. - * @return Response of 400 for failure or 201 for created successfully. - */ - private Response addObservationToDatabase(String observationId, String collectionId) { - String uniqueObservation = String.format(XML_OBSERVATION, observationId, collectionId); - - try { - given() - .header("Content-Type", "application/xml") - .body(uniqueObservation) - .when() - .post("/observations") - .then() - .statusCode(Response.Status.CREATED.getStatusCode()) - .body("simpleObservation.id", is(observationId)); - } catch (Exception e) { - return Response.status(Response.Status.BAD_REQUEST.getStatusCode()).build(); - } - return Response.status(Response.Status.CREATED.getStatusCode()).build(); - } } diff --git a/src/test/java/org/uksrc/archive/utils/Utilities.java b/src/test/java/org/uksrc/archive/utils/Utilities.java new file mode 100644 index 0000000..0cac324 --- /dev/null +++ b/src/test/java/org/uksrc/archive/utils/Utilities.java @@ -0,0 +1,44 @@ +package org.uksrc.archive.utils; + +import jakarta.ws.rs.core.Response; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +public class Utilities { + + //Caution with the id value if re-using. + private static final String XML_OBSERVATION = "" + + "%s" + + "%s" + + "science" + + "auri" + + ""; + + public static final String COLLECTION1 = "e-merlin"; + public static final String COLLECTION2 = "testCollection"; + + /** + * Adds a SimpleObservation to the database with the supplied observationId + * @param observationId unique identifier for the observation + * @param collectionId identifier for the collection to add this observation to. + * @return Response of 400 for failure or 201 for created successfully. + */ + public static Response addObservationToDatabase(String observationId, String collectionId) { + String uniqueObservation = String.format(XML_OBSERVATION, observationId, collectionId); + + try { + given() + .header("Content-Type", "application/xml") + .body(uniqueObservation) + .when() + .post("/observations") + .then() + .statusCode(Response.Status.CREATED.getStatusCode()) + .body("simpleObservation.id", is(observationId)); + } catch (Exception e) { + return Response.status(Response.Status.BAD_REQUEST.getStatusCode()).build(); + } + return Response.status(Response.Status.CREATED.getStatusCode()).build(); + } +} From 623ffd3b4dc5d2fe4a3b30c7e2f4d57a5cc532cb Mon Sep 17 00:00:00 2001 From: StephenL Date: Fri, 4 Oct 2024 15:44:39 +0100 Subject: [PATCH 09/11] Updated details.md to reflect recent changes --- detail.md | 60 +++---------------- .../uksrc/archive/ObservationResource.java | 4 +- 2 files changed, 10 insertions(+), 54 deletions(-) diff --git a/detail.md b/detail.md index 4f3d07a..394a21c 100644 --- a/detail.md +++ b/detail.md @@ -47,10 +47,11 @@ Endpoints available for interaction with the archive-service. ##### Parameters -> | name | type | data type | description | -> |------|----------|-----------|--------------------------------------------------------------------------------| -> | page | optional | integer | The page index, zero-indexed | -> | size | optional | integer | The number of observations to return for each page, must be greater than zero. | +> | name | type | data type | description | +> |--------------|----------|-----------|--------------------------------------------------------------------------------| +> | collectionId | optional | String | Filter by collection Id if required (not supplying will return all). | +> | page | optional | integer | The page index, zero-indexed | +> | size | optional | integer | The number of observations to return for each page, must be greater than zero. | ##### Responses @@ -95,39 +96,14 @@ Endpoints available for interaction with the archive-service.
-
- GET /observations/collection/{collectionId} (Returns all observations for the supplied collectionId, if found) - -##### Parameters - -> | name | type | data type | description | -> |--------------|----------|-----------|--------------------------------------------------------------------------------| -> | collectionId | required | String | The unique identifier of a specific collection | -> | page | optional | integer | The page index, zero-indexed | -> | size | optional | integer | The number of observations to return for each page, must be greater than zero. | - -##### Responses - -> | http code | content-type | response | -> |-----------|-------------------|-------------------------------------------------------------------------------| -> | `201` | `application/xml` | `List of Observation (Simple and/or Derived) found and returned successfully` | -> | `400` | `text/plain` | `{"code":"400","message":"Bad Request"}` | - -##### Example cURL - -> ``` -> curl -X 'GET' 'http://localhost:8080/observations/23456' -H 'accept: application/xml' -> ``` - -
------------------------------------------------------------------------------------------ #### Adding new Observations
- POST /observations/add (Add a new observation) + POST /observations (Add a new observation) ##### Responses @@ -139,7 +115,7 @@ Endpoints available for interaction with the archive-service. ##### Example XML cURL > ``` -> curl -v --header "Content-Type: application/xml" -T observation1.xml http://localhost:8080/observations/add +> curl -v --header "Content-Type: application/xml" -T observation1.xml http://localhost:8080/observations > ``` ##### Example JSON cURL @@ -149,30 +125,12 @@ with JSON response also > ```
-
- POST /observations/derived/add (Add a new derived observation) - -##### Responses - -> | http code | content-type | response | -> |---------------|-------------------|------------------------------------------------------------------------| -> | `201` | `application/xml` | `Observation added successfully, body contains new DerivedObservation` | -> | `400` | `text/plain` | `{"code":"400","message":"Bad Request"}` | - -##### Example cURL - -> ``` -> curl -v --header "Content-Type: application/xml" -T observation1.xml http://localhost:8080/observations/derived/add -> ``` - -
- ------------------------------------------------------------------------------------------ #### Updating observations
- PUT /observations/update/{observationId} (Updates an observation (Simple or Derived) with the same observationId) + UPDATE /observations/{observationId} (Updates an observation (Simple or Derived) with the same observationId) ##### Parameters @@ -232,7 +190,7 @@ with JSON response also #### Retrieving collections
- GET /observations/collections (Returns the names of all the collections as a TSV (Tab Separated List)) + GET /collections (Returns the names of all the collections as a TSV (Tab Separated List)) ##### Responses diff --git a/src/main/java/org/uksrc/archive/ObservationResource.java b/src/main/java/org/uksrc/archive/ObservationResource.java index 3c712a7..c1bb593 100644 --- a/src/main/java/org/uksrc/archive/ObservationResource.java +++ b/src/main/java/org/uksrc/archive/ObservationResource.java @@ -25,8 +25,6 @@ import org.uksrc.archive.utils.responses.Responses; import org.uksrc.archive.utils.tools.Tools; -import java.util.*; - @Path("/observations") public class ObservationResource { @@ -121,7 +119,7 @@ public Response addObservation(Observation observation) { } @PUT - @Path("{observationId}") + @Path("/{observationId}") @Operation(summary = "Update an existing Observation", description = "Updates an existing observation with the supplied ID") @Parameter( name = "observationId", From bccc0143fe21117ee529c6fb3246c618bf3bfdc8 Mon Sep 17 00:00:00 2001 From: StephenL Date: Mon, 7 Oct 2024 11:04:10 +0100 Subject: [PATCH 10/11] - A couple of new unit tests added --- .../archive/utils/GenericExceptionMapper.java | 4 ++- src/main/resources/application.properties | 3 ++ .../uksrc/archive/CollectionResourceTest.java | 13 +++++++ .../archive/ObservationResourceTest.java | 35 +++++++++++++++++-- 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java b/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java index e978ca7..335b998 100644 --- a/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java +++ b/src/main/java/org/uksrc/archive/utils/GenericExceptionMapper.java @@ -3,13 +3,15 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; +import org.jboss.logging.Logger; @Provider public class GenericExceptionMapper implements ExceptionMapper { + private static final Logger LOG = Logger.getLogger(GenericExceptionMapper.class); @Override public Response toResponse(Exception exception) { - exception.printStackTrace(); + LOG.error(exception); return Response.status(Response.Status.BAD_REQUEST) .entity("Error: " + exception.getMessage()) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 87a5681..58250f0 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,3 +3,6 @@ quarkus.hibernate-orm.database.generation.create-schemas=true quarkus.hibernate-orm.database.generation.halt-on-error=false quarkus.hibernate-orm.database.globally-quoted-identifiers=true + +# Error logging +quarkus.log.category."org.uksrc.archive.utils.GenericExceptionMapper".level=DEBUG \ No newline at end of file diff --git a/src/test/java/org/uksrc/archive/CollectionResourceTest.java b/src/test/java/org/uksrc/archive/CollectionResourceTest.java index d4a18ab..9d5ea8c 100644 --- a/src/test/java/org/uksrc/archive/CollectionResourceTest.java +++ b/src/test/java/org/uksrc/archive/CollectionResourceTest.java @@ -28,6 +28,19 @@ public void clearDatabase() { em.createQuery("DELETE FROM Observation").executeUpdate(); } + @Test + @DisplayName("Test retrieving collection Ids from empty DB") + public void testRetrieveCollectionIdsFromEmptyDB() { + String collections = when() + .get("/collections") + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract() + .asString(); + + assert(collections.isEmpty()); + } + @Test @DisplayName("Test retrieving collection Ids") public void testRetrievingCollectionIds() { diff --git a/src/test/java/org/uksrc/archive/ObservationResourceTest.java b/src/test/java/org/uksrc/archive/ObservationResourceTest.java index 362602e..75bd4d5 100644 --- a/src/test/java/org/uksrc/archive/ObservationResourceTest.java +++ b/src/test/java/org/uksrc/archive/ObservationResourceTest.java @@ -15,9 +15,6 @@ import org.uksrc.archive.utils.ObservationListWrapper; import org.uksrc.archive.utils.Utilities; -import java.util.Arrays; -import java.util.List; - import static io.restassured.RestAssured.given; import static io.restassured.RestAssured.when; import static org.hamcrest.CoreMatchers.is; @@ -95,6 +92,38 @@ public void testGettingObservationsNonEmpty() { } } + @Test + @DisplayName("Get observations via collection Id") + public void testGettingObservationsViaCollectionId() { + try(Response res1 = Utilities.addObservationToDatabase("1234", COLLECTION1); + Response res2 = Utilities.addObservationToDatabase("6789", COLLECTION1)) { + assert (res1.getStatus() == Response.Status.CREATED.getStatusCode() && + res2.getStatus() == Response.Status.CREATED.getStatusCode()); + + //Both previously added observations should be returned + ObservationListWrapper wrapper = when() + .get("/observations?collectionId=" + COLLECTION1) + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract() + .as(new TypeRef<>() { + }); + + assert (wrapper.getObservations().size() == 2); + + //Neither of the previously added observations should be returned + wrapper = when() + .get("/observations?collectionId=" + COLLECTION2) + .then() + .statusCode(Response.Status.OK.getStatusCode()) + .extract() + .as(new TypeRef<>() { + }); + + assert (wrapper.getObservations().isEmpty()); + } + } + @ParameterizedTest @DisplayName("Add an observation and check that part of the response body matches.") @ValueSource(strings = {XML_OBSERVATION, XML_DERIVED_OBSERVATION}) From 86ff56731886af15103acefd4a4829117f633cf7 Mon Sep 17 00:00:00 2001 From: StephenL Date: Mon, 7 Oct 2024 16:04:31 +0100 Subject: [PATCH 11/11] Updated some example curl requests after some testing. --- detail.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/detail.md b/detail.md index 394a21c..72e5471 100644 --- a/detail.md +++ b/detail.md @@ -115,13 +115,13 @@ Endpoints available for interaction with the archive-service. ##### Example XML cURL > ``` -> curl -v --header "Content-Type: application/xml" -T observation1.xml http://localhost:8080/observations +> curl -X 'POST' -H 'Content-Type: application/xml' -H 'accept: application/xml' --data "@observation2.json" http://localhost:8080/observations > ``` ##### Example JSON cURL with JSON response also > ``` -> curl -v --header "Content-Type: application/json" --header "Accept: application/json" -T observation1.xml http://localhost:8080/observations/add +> curl -X 'POST' -H 'Content-Type: application/json' -H 'accept: application/json' --data "@observation2.json" http://localhost:8080/observations > ```
@@ -150,7 +150,7 @@ with JSON response also ##### Example cURL > ``` -> curl -v --header "Content-Type: application/xml" -T observation123.xml http://localhost:8080/observations/update/123 +> curl -X 'PUT' -H 'Content-Type: application/xml' -H 'Accept: application/xml' -T observation2.xml http://localhost:8080/observations/988 > ```
@@ -160,7 +160,7 @@ with JSON response also #### Deleting Observations
- GET /observations/{observationId} (Delete an Observation with the supplied ID, if found) + DELETE /observations/{observationId} (Delete an Observation with the supplied ID, if found) ##### Parameters @@ -203,7 +203,7 @@ with JSON response also ##### Example cURL > ``` -> curl -X 'GET' -H 'accept: application/xml' 'http://localhost:8080/observations/collections' +> curl -X 'GET' -H 'accept: application/xml' 'http://localhost:8080/collections' > ```
\ No newline at end of file