From d6d6d210095b8d50f9eeec9affa2b755b477db7c Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Thu, 17 Oct 2024 19:31:26 +0100 Subject: [PATCH 1/2] JBTM-3550 Coordinator content negotiation (pom changes and bug fixes) --- narayana-bom/pom.xml | 10 +- .../client/internal/NarayanaLRAClient.java | 8 ++ rts/lra/coordinator/pom.xml | 13 ++- .../lra/coordinator/api/Coordinator.java | 25 ++++- .../lra/coordinator/domain/model/LRATest.java | 94 +++++++++++++++++++ 5 files changed, 140 insertions(+), 10 deletions(-) diff --git a/narayana-bom/pom.xml b/narayana-bom/pom.xml index a2d0c7efba..b67723ca1b 100644 --- a/narayana-bom/pom.xml +++ b/narayana-bom/pom.xml @@ -33,7 +33,7 @@ 2.0.1 2.1.0 3.1.0 - 2.0.1 + 2.1.1 2.0.1 3.1.0 10.0.0 @@ -63,6 +63,7 @@ 1.5.1.Final 9.0.1.Final 2.0.0.Final + 1.1.1 2.7.0 3.0.0 1.5.4 @@ -328,7 +329,12 @@ jakarta.json-api ${version.jakarta.json-api} - + + org.eclipse.parsson + parsson + ${version.parsson} + test + jakarta.servlet jakarta.servlet-api diff --git a/rts/lra/client/src/main/java/io/narayana/lra/client/internal/NarayanaLRAClient.java b/rts/lra/client/src/main/java/io/narayana/lra/client/internal/NarayanaLRAClient.java index 14b49a4986..f8ea9eb068 100644 --- a/rts/lra/client/src/main/java/io/narayana/lra/client/internal/NarayanaLRAClient.java +++ b/rts/lra/client/src/main/java/io/narayana/lra/client/internal/NarayanaLRAClient.java @@ -878,6 +878,14 @@ public URI getCurrent() { return Current.peek(); } + public void clearCurrent(boolean all) { + if (all) { + Current.popAll(); + } else { + Current.pop(); + } + } + private void lraTracef(String reasonFormat, Object... parameters) { if (LRALogger.logger.isTraceEnabled()) { LRALogger.logger.tracef(reasonFormat, parameters); diff --git a/rts/lra/coordinator/pom.xml b/rts/lra/coordinator/pom.xml index 368fbca8d7..67174ee9bd 100644 --- a/rts/lra/coordinator/pom.xml +++ b/rts/lra/coordinator/pom.xml @@ -63,10 +63,6 @@ jakarta.enterprise.cdi-api provided - - jakarta.servlet - jakarta.servlet-api - org.jboss.resteasy resteasy-jackson2-provider @@ -77,6 +73,15 @@ lra-client test + + jakarta.json + jakarta.json-api + + + + org.eclipse.parsson + parsson + org.eclipse.microprofile.openapi microprofile-openapi-api diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java index 20f2abed30..c53ad8bc87 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java @@ -15,6 +15,8 @@ import jakarta.enterprise.context.ApplicationScoped; +import jakarta.json.Json; +import jakarta.json.JsonObject; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.DefaultValue; import jakarta.ws.rs.GET; @@ -32,6 +34,7 @@ import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.core.Application; import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.HttpHeaders; import jakarta.ws.rs.core.Link; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; @@ -458,7 +461,7 @@ public Response closeLRA( @PUT @Path("{LraId}/cancel") - @Produces(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "Attempt to cancel an LRA", description = " Trigger the compensation of the LRA. All" + " participants will be triggered by the coordinator (ie the compensate message will be sent to each participants)." @@ -478,13 +481,27 @@ public Response cancelLRA( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) @PathParam("LraId")String lraId, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_LINK_HEADER_NAME) @DefaultValue("") String compensator, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_DATA_HEADER_NAME) @DefaultValue("") String userData) throws NotFoundException { - return Response.ok(endLRA(toURI(lraId), true, false, compensator, userData).name()) - .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) - .build(); + + LRAStatus status = endLRA(toURI(lraId), true, false, compensator, userData); + + if (mediaType.equals(MediaType.APPLICATION_JSON)) { + JsonObject model = Json.createObjectBuilder() + .add("status", status.name()) + .build(); + + return Response.ok(model.toString()) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) + .build(); + } else { // produce MediaType.TEXT_PLAIN + return Response.ok(status.name()) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) + .build(); + } } private LRAStatus endLRA(URI lraId, boolean compensate, boolean fromHierarchy, String compensator, String userData) throws NotFoundException { diff --git a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java index 52f5def67b..45079622f6 100644 --- a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java +++ b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java @@ -29,6 +29,8 @@ import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.narayana.lra.LRAConstants; import org.eclipse.microprofile.lra.annotation.LRAStatus; import org.hamcrest.MatcherAssert; @@ -297,6 +299,98 @@ public void testComplete() throws URISyntaxException { assertTrue("LRA should have closed", status == null || status == LRAStatus.Closed); } + @Test + // validate that the coordinator produces Json if the accepts header specifies Json + public void testJsonContentNegotiation() { + negotiateContent("testJsonContentNegotiation", MediaType.APPLICATION_JSON); + } + + @Test + // validate that the coordinator produces plain text if the accepts header specifies plain text + public void testPlainContentNegotiation() { + negotiateContent("testPlainContentNegotiation", MediaType.TEXT_PLAIN); + } + + private void negotiateContent(String clientId, String acceptMediaType) { + URI lraId = lraClient.startLRA(clientId); + + // cancel and validate that the response reports the status using the requested media type + try (Response r = client + .target(String.format("%s/cancel", lraId)) + .request() + .accept(acceptMediaType) + .put(null)) { + int res = r.getStatus(); + if (res != Response.Status.OK.getStatusCode()) { + fail("unable to cleanup: " + res); + } + try { + if (acceptMediaType.equals(MediaType.TEXT_PLAIN)) { + String status = r.readEntity(String.class); + + assertEquals(LRAStatus.Cancelled.name(), status); + } else if (acceptMediaType.equals(MediaType.APPLICATION_JSON)) { + // {"status":"Active"} + String status = r.readEntity(String.class); + String expected = String.format("{\"status\":\"%s\"}", LRAStatus.Cancelled.name()); + + assertEquals(expected, status); + } + } catch (Exception e) { + fail("Could not read entity body: " + e.getMessage()); + } + } + + // we started the LRA using the client API which associates the LRA with the calling thread + // and since the test then cancelled it directly, ie not via the API, the LRA will still be associated. + // Therefore, it needs to be cleared otherwise subsequent tests will still see the LRA associated: + lraClient.clearCurrent(false); + assertNull("LRA is still associated with the current thread", lraClient.getCurrent()); + } + + @Test + // start an LRA and validate that the coordinator reports its status correctly + public void testLRAInfo() { + URI lraId = lraClient.startLRA("testLRAInfo"); + + try { + // request the status of the LRA that was just started + try (Response r = client + .target(String.format("%s", lraId)) + .request() + .get()) { + int res = r.getStatus(); + + if (res != Response.Status.OK.getStatusCode()) { + fail("unable to read LRAData: HTTP status was " + res); + } + + String info = r.readEntity(String.class); // the entity body should be a Json representation of the LRA + + try { + ObjectMapper objectMapper = new ObjectMapper(); + LRAData data = objectMapper.readValue(info, LRAData.class); + // or Json.createReader(new StringReader(info)).readObject(); for the raw Json + + // validate the LRA id, the client id and the status + assertEquals(lraId, data.getLraId()); + assertEquals("testLRAInfo", data.getClientId()); + assertEquals(LRAStatus.Active, data.getStatus()); + + } catch (JsonProcessingException e) { + fail("Unable to parse JSON response: " + info); + } + } + } finally { + // clean up + try { + lraClient.cancelLRA(lraId); + } catch (WebApplicationException e) { + fail("Could not clean up: " + e); + } + } + } + /* * Participants can update their callbacks to facilitate recovery. * Test that the compensate endpoint can be changed: From b7a0d1229493444fb9502bc9298912089f127fb4 Mon Sep 17 00:00:00 2001 From: Michael Musgrove Date: Fri, 18 Oct 2024 16:28:33 +0100 Subject: [PATCH 2/2] JBTM-3550 add Coordinator content negotiation to remaining methods --- .../lra/coordinator/api/Coordinator.java | 149 ++++++++---- .../lra/coordinator/domain/model/LRATest.java | 224 +++++++++++++++--- 2 files changed, 296 insertions(+), 77 deletions(-) diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java index c53ad8bc87..05ca8094bf 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/Coordinator.java @@ -5,6 +5,8 @@ package io.narayana.lra.coordinator.api; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import io.narayana.lra.Current; import io.narayana.lra.LRAConstants; import io.narayana.lra.LRAData; @@ -39,6 +41,7 @@ import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; + import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -150,7 +153,7 @@ private boolean isAllowParticipantData(String version) { @GET @Path("/") - @Produces(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "Returns all LRAs", description = "Gets both active and recovering LRAs") @APIResponses({ @APIResponse(responseCode = "200", description = "The LRAData json array which is known to coordinator", @@ -165,6 +168,7 @@ private boolean isAllowParticipantData(String version) { public Response getAllLRAs( @Parameter(name = STATUS_PARAM_NAME, description = "Filter the returned LRAs to only those in the give state (see CompensatorStatus)") @QueryParam(STATUS_PARAM_NAME) @DefaultValue("") String state, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version) { LRAStatus requestedLRAStatus = null; @@ -181,14 +185,31 @@ public Response getAllLRAs( List lras = lraService.getAll(requestedLRAStatus); - return Response.ok() - .entity(lras) - .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version).build(); + if (mediaType.equals(MediaType.APPLICATION_JSON)) { + try { + String jsonArray = new ObjectMapper().writeValueAsString(lras); + + return Response.ok() + .entity(jsonArray) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) + .build(); + } catch (JsonProcessingException e) { + return Response.status(INTERNAL_SERVER_ERROR) + .entity(e.getMessage()) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) + .build(); + } + } else { // produce MediaType.TEXT_PLAIN + return Response.ok() + .entity(lras) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) + .build(); + } } @GET @Path("{LraId}/status") - @Produces(MediaType.TEXT_PLAIN) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "Obtain the status of an LRA as a string") @APIResponses({ @APIResponse(responseCode = "200", description = "The LRA exists. The status is reported in the content body.", @@ -204,6 +225,7 @@ public Response getLRAStatus( "Expecting to be a valid URL where the participant can be contacted at. If not in URL format it will be considered " + "to be an id which will be declared to exist at URL where coordinator is deployed at.", required = true) @PathParam("LraId")String lraId, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version) throws NotFoundException { @@ -214,6 +236,14 @@ public Response getLRAStatus( status = LRAStatus.Active; } + if (mediaType.equals(MediaType.APPLICATION_JSON)) { + JsonObject model = Json.createObjectBuilder().add("status", status.name()).build(); + + return Response.ok() + .entity(model) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version).build(); + } + return Response.ok() .entity(status.name()) .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version).build(); @@ -221,7 +251,7 @@ public Response getLRAStatus( @GET @Path("{LraId}") - @Produces(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "Obtain the information about an LRA as a JSON structure") @APIResponses({ @APIResponse(responseCode = "200", description = "The LRA exists and the information is packed as JSON in the content body.", @@ -235,6 +265,7 @@ public Response getLRAStatus( public Response getLRAInfo( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) @PathParam("LraId") String lraId, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version) { URI lraIdURI = toURI(lraId); @@ -253,7 +284,7 @@ public Response getLRAInfo( */ @POST @Path("start") - @Produces(MediaType.TEXT_PLAIN) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Bulkhead @Operation(summary = "Start a new LRA", description = "The LRA model uses a presumed nothing protocol: the coordinator must communicate " @@ -289,6 +320,7 @@ public Response startLRA( @Parameter(name = PARENT_LRA_PARAM_NAME, description = "The enclosing LRA if this new LRA is nested") @QueryParam(PARENT_LRA_PARAM_NAME) @DefaultValue("") String parentLRA, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version) throws WebApplicationException { @@ -330,6 +362,14 @@ public Response startLRA( Current.push(lraId); + if (mediaType.equals(MediaType.APPLICATION_JSON)) { + JsonObject model = Json.createObjectBuilder().add("lraId", lraId.toASCIIString()).build(); + + return Response.ok() + .entity(model) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version).build(); + } + return Response.created(lraId) .entity(lraId) .header(LRA_HTTP_CONTEXT_HEADER, Current.getContexts()) @@ -402,14 +442,26 @@ private ParticipantStatus mapToParticipantStatus(LRAStatus lraStatus) { @PUT @Path("nested/{NestedLraId}/complete") - public Response completeNestedLRA(@PathParam("NestedLraId") String nestedLraId) { - return Response.ok(mapToParticipantStatus(endLRA(toURI(nestedLraId), false, true, null, null)).name()).build(); + public Response completeNestedLRA( + @PathParam("NestedLraId") String nestedLraId, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, + @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version) { + + LRAData lraData = lraService.endLRA(toURI(nestedLraId), false, true, null, null); + + return buildResponse(mapToParticipantStatus(lraData.getStatus()).name(), version, mediaType); } @PUT @Path("nested/{NestedLraId}/compensate") - public Response compensateNestedLRA(@PathParam("NestedLraId") String nestedLraId) { - return Response.ok(mapToParticipantStatus(endLRA(toURI(nestedLraId), true, true, null, null)).name()).build(); + public Response compensateNestedLRA( + @PathParam("NestedLraId") String nestedLraId, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, + @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version) { + + LRAData lraData = lraService.endLRA(toURI(nestedLraId), true, true, null, null); + + return buildResponse(mapToParticipantStatus(lraData.getStatus()).name(), version, mediaType); } @PUT @@ -429,7 +481,7 @@ public Response forgetNestedLRA(@PathParam("NestedLraId") String nestedLraId) { */ @PUT @Path("{LraId}/close") - @Produces(MediaType.TEXT_PLAIN) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "Attempt to close an LRA", description = "Trigger the successful completion of the LRA. All" + " participants will be dropped by the coordinator." @@ -449,14 +501,16 @@ public Response forgetNestedLRA(@PathParam("NestedLraId") String nestedLraId) { }) public Response closeLRA( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) - @PathParam("LraId") String txId, + @PathParam("LraId") String lraId, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_LINK_HEADER_NAME) @DefaultValue("") String compensator, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_DATA_HEADER_NAME) @DefaultValue("") String userData) { - return Response.ok(endLRA(toURI(txId), false, false, compensator, userData).name()) - .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) - .build(); + + LRAData lraData = lraService.endLRA(toURI(lraId), false, false, compensator, userData); + + return buildResponse(lraData.getStatus().name(), version, mediaType); } @PUT @@ -480,39 +534,21 @@ public Response closeLRA( public Response cancelLRA( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) @PathParam("LraId")String lraId, - @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, + @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_LINK_HEADER_NAME) @DefaultValue("") String compensator, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_DATA_HEADER_NAME) @DefaultValue("") String userData) throws NotFoundException { - LRAStatus status = endLRA(toURI(lraId), true, false, compensator, userData); - - if (mediaType.equals(MediaType.APPLICATION_JSON)) { - JsonObject model = Json.createObjectBuilder() - .add("status", status.name()) - .build(); + LRAData lraData = lraService.endLRA(toURI(lraId), true, false, compensator, userData); - return Response.ok(model.toString()) - .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) - .build(); - } else { // produce MediaType.TEXT_PLAIN - return Response.ok(status.name()) - .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, version) - .build(); - } - } - - private LRAStatus endLRA(URI lraId, boolean compensate, boolean fromHierarchy, String compensator, String userData) throws NotFoundException { - LRAData lraData = lraService.endLRA(lraId, compensate, fromHierarchy, compensator, userData); - - return lraData.getStatus(); + return buildResponse(lraData.getStatus().name(), version, mediaType); } @PUT @Path("{LraId}") - @Produces(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "A Compensator can join with the LRA at any time prior to the completion of an activity") @APIResponses({ @APIResponse(responseCode = "200", @@ -554,6 +590,7 @@ public Response joinLRAViaBody( + " the status of the participant. The link rel names are" + " complete, compensate and status.") @HeaderParam("Link") @DefaultValue("") String compensatorLink, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version, @HeaderParam(LRAConstants.NARAYANA_LRA_PARTICIPANT_DATA_HEADER_NAME) @DefaultValue("") String userData, @@ -583,7 +620,7 @@ public Response joinLRAViaBody( sb.append(userData); } - return joinLRA(toURI(lraId), timeLimit, compensatorLink, sb, version); + return joinLRA(toURI(lraId), mediaType, timeLimit, compensatorLink, sb, version); } if (!isLink && !compensatorURL.isEmpty()) { @@ -620,7 +657,7 @@ public Response joinLRAViaBody( compensatorURL = linkHeaderValue.toString(); } - return joinLRA(toURI(lraId), timeLimit, compensatorURL, null, version); + return joinLRA(toURI(lraId), mediaType, timeLimit, compensatorURL, null, version); } @@ -648,7 +685,8 @@ private boolean isLink(String linkString) { } } - private Response joinLRA(URI lraId, long timeLimit, String linkHeader, StringBuilder userData, String version) + private Response joinLRA(URI lraId, String acceptMediaType, long timeLimit, String linkHeader, + StringBuilder userData, String version) throws NotFoundException { final String recoveryUrlBase = String.format("%s%s/%s", context.getBaseUri().toASCIIString(), COORDINATOR_PATH_NAME, RECOVERY_COORDINATOR_PATH_NAME); @@ -659,10 +697,18 @@ private Response joinLRA(URI lraId, long timeLimit, String linkHeader, StringBui StringBuilder recoveryUrl = new StringBuilder(); int status = lraService.joinLRA(recoveryUrl, lraId, timeLimit, null, linkHeader, recoveryUrlBase, userData, version); + String recoveryUrlValue; + + if (acceptMediaType.equals(MediaType.APPLICATION_JSON)) { + JsonObject model = Json.createObjectBuilder().add("recoveryUrl", recoveryUrl.toString()).build(); + recoveryUrlValue = model.toString(); + } else { + recoveryUrlValue = recoveryUrl.toString(); + } try { return Response.status(status) - .entity(recoveryUrl.toString()) + .entity(recoveryUrlValue) .location(new URI(recoveryUrl.toString())) .header(LRA_HTTP_RECOVERY_HEADER, recoveryUrl) .header(NARAYANA_LRA_PARTICIPANT_DATA_HEADER_NAME, userData) @@ -683,7 +729,7 @@ private Response joinLRA(URI lraId, long timeLimit, String linkHeader, StringBui */ @PUT @Path("{LraId}/remove") - @Produces(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN}) @Operation(summary = "A Compensator can resign from the LRA at any time prior to the completion of an activity") @APIResponses({ @APIResponse(responseCode = "200", description = "If the participant was successfully removed from the LRA", @@ -700,6 +746,7 @@ private Response joinLRA(URI lraId, long timeLimit, String linkHeader, StringBui public Response leaveLRA( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) @PathParam("LraId") String lraId, + @HeaderParam(HttpHeaders.ACCEPT) @DefaultValue(MediaType.TEXT_PLAIN) String mediaType, @Parameter(ref = LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @HeaderParam(LRAConstants.NARAYANA_LRA_API_VERSION_HEADER_NAME) @DefaultValue(CURRENT_API_VERSION_STRING) String version, String participantCompensatorUrl) throws NotFoundException { @@ -710,6 +757,22 @@ public Response leaveLRA( .build(); } + private Response buildResponse(String status, String apiVersion, String mediaType) throws NotFoundException { + if (mediaType.equals(MediaType.APPLICATION_JSON)) { + JsonObject model = Json.createObjectBuilder() + .add("status", status) + .build(); + + return Response.ok(model.toString()) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, apiVersion) + .build(); + } else { // produce MediaType.TEXT_PLAIN + return Response.ok(status) + .header(NARAYANA_LRA_API_VERSION_HEADER_NAME, apiVersion) + .build(); + } + } + private URI toURI(String lraId) { URL url; diff --git a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java index 45079622f6..caf5d615c8 100644 --- a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java +++ b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/domain/model/LRATest.java @@ -6,6 +6,7 @@ package io.narayana.lra.coordinator.domain.model; import static io.narayana.lra.LRAConstants.COORDINATOR_PATH_NAME; +import static jakarta.ws.rs.core.Response.Status.OK; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_RECOVERY_HEADER; import static org.hamcrest.Matchers.containsString; @@ -24,12 +25,14 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.TimeUnit; import java.util.stream.IntStream; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import io.narayana.lra.LRAConstants; import org.eclipse.microprofile.lra.annotation.LRAStatus; @@ -139,7 +142,7 @@ public void after() { @Test public void joinWithVersionTest() { - URI lraId = lraClient.startLRA("joinLRAWithBody"); + URI lraId = lraClient.startLRA("joinWithVersionTest"); String version = LRAConstants.API_VERSION_1_2; String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8); // must be valid @@ -150,7 +153,7 @@ public void joinWithVersionTest() { // the request body should correspond to a valid compensator or be empty .put(Entity.text(""))) { Assert.assertEquals("Expected joining LRA succeeded, PUT/200 is expected.", - Response.Status.OK.getStatusCode(), response.getStatus()); + OK.getStatusCode(), response.getStatus()); Assert.assertEquals("Expected API header to be returned with the version provided in request", version, response.getHeaderString(LRA_API_VERSION_HEADER_NAME)); String recoveryHeaderUrlMessage = response.getHeaderString(RECOVERY_HEADER_NAME); @@ -172,7 +175,7 @@ public void joinWithVersionTest() { @Test public void joinWithOldVersionTest() { - URI lraId = lraClient.startLRA("joinLRAWithBody"); + URI lraId = lraClient.startLRA("joinWithOldVersionTest"); String version = LRAConstants.API_VERSION_1_1; String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8); // must be valid @@ -183,7 +186,7 @@ public void joinWithOldVersionTest() { // the request body should correspond to a valid compensator or be empty .put(Entity.text(""))) { Assert.assertEquals("Expected joining LRA succeeded, PUT/200 is expected.", - Response.Status.OK.getStatusCode(), response.getStatus()); + OK.getStatusCode(), response.getStatus()); Assert.assertEquals("Expected API header to be returned with the version provided in request", version, response.getHeaderString(LRA_API_VERSION_HEADER_NAME)); String recoveryHeaderUrlMessage = response.getHeaderString(RECOVERY_HEADER_NAME); @@ -230,7 +233,7 @@ void participantCallbackOrder(boolean cancel) { // (this will cause the participant to be enlisted with the LRA) try (Response r = client.target(TestPortProvider.generateURL(businessMethodName)).request() .header(LRA_HTTP_CONTEXT_HEADER, lraId).get()) { - if (r.getStatus() != Response.Status.OK.getStatusCode()) { + if (r.getStatus() != OK.getStatusCode()) { try { // clean up and fail lraClient.cancelLRA(lraId); @@ -278,7 +281,7 @@ public void testLRAParticipant() { Response r3 = client.target(String.format("%s/close", lraId)).request().put(null); int status = r3.getStatus(); assertTrue("Problem closing LRA: ", - status == Response.Status.OK.getStatusCode() || status == Response.Status.NOT_FOUND.getStatusCode()); + status == OK.getStatusCode() || status == Response.Status.NOT_FOUND.getStatusCode()); // verify that the participant complete request is issued when a method annotated with @LRA returns int completions = completeCount.get(); @@ -321,7 +324,7 @@ private void negotiateContent(String clientId, String acceptMediaType) { .accept(acceptMediaType) .put(null)) { int res = r.getStatus(); - if (res != Response.Status.OK.getStatusCode()) { + if (res != OK.getStatusCode()) { fail("unable to cleanup: " + res); } try { @@ -348,38 +351,76 @@ private void negotiateContent(String clientId, String acceptMediaType) { assertNull("LRA is still associated with the current thread", lraClient.getCurrent()); } + @Test + public void testGetAllLRAsAcceptJson() { + URI lraId = lraClient.startLRA("testGetAllLRAsAcceptJson"); + + // read all LRAs using Json + try (Response response = client.target(coordinatorPath) + .request() + .header(LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING) + .accept(MediaType.APPLICATION_JSON) + .get()) { + if (response.getStatus() != OK.getStatusCode()) { + LRALogger.logger.debugf("Error getting all LRAs from the coordinator, response status: %d", + response.getStatus()); + throw new WebApplicationException(response); + } + + String lrasAsJson = response.readEntity(String.class); // all LRAs as a json string + + try { + // parse the json string into an array of LRAData + LRAData[] lras = new ObjectMapper().readValue(lrasAsJson, LRAData[].class); + // see if lraId is in the returned array + Optional targetLRA = Arrays.stream(lras) + .filter(lra -> lraId.equals(lra.getLraId())) + .findFirst(); + + assertTrue("The LRA is unknown to the coordinator", targetLRA.isPresent()); + } catch (JsonProcessingException e) { + fail("could not read json array: " + e.getMessage()); + } + } finally { + try { + lraClient.cancelLRA(lraId); + } catch (WebApplicationException e) { + fail("Could not clean up: " + e); + } + } + } + @Test // start an LRA and validate that the coordinator reports its status correctly - public void testLRAInfo() { + public void testLRAInfoAcceptJson() { URI lraId = lraClient.startLRA("testLRAInfo"); - try { - // request the status of the LRA that was just started - try (Response r = client - .target(String.format("%s", lraId)) - .request() - .get()) { - int res = r.getStatus(); + // request the status of the LRA that was just started + try (Response r = client + .target(String.format("%s", lraId)) + .request() + .accept(MediaType.APPLICATION_JSON) + .get()) { + int res = r.getStatus(); - if (res != Response.Status.OK.getStatusCode()) { - fail("unable to read LRAData: HTTP status was " + res); - } + if (res != OK.getStatusCode()) { + fail("unable to read LRAData: HTTP status was " + res); + } - String info = r.readEntity(String.class); // the entity body should be a Json representation of the LRA + String info = r.readEntity(String.class); // the entity body should be a Json representation of the LRA - try { - ObjectMapper objectMapper = new ObjectMapper(); - LRAData data = objectMapper.readValue(info, LRAData.class); - // or Json.createReader(new StringReader(info)).readObject(); for the raw Json + try { + ObjectMapper objectMapper = new ObjectMapper(); + LRAData data = objectMapper.readValue(info, LRAData.class); + // or Json.createReader(new StringReader(info)).readObject(); for the raw Json - // validate the LRA id, the client id and the status - assertEquals(lraId, data.getLraId()); - assertEquals("testLRAInfo", data.getClientId()); - assertEquals(LRAStatus.Active, data.getStatus()); + // validate the LRA id, the client id and the status + assertEquals(lraId, data.getLraId()); + assertEquals("testLRAInfo", data.getClientId()); + assertEquals(LRAStatus.Active, data.getStatus()); - } catch (JsonProcessingException e) { - fail("Unable to parse JSON response: " + info); - } + } catch (JsonProcessingException e) { + fail("Unable to parse JSON response: " + info); } } finally { // clean up @@ -391,6 +432,121 @@ public void testLRAInfo() { } } + @Test + // start an LRA and validate that the coordinator reports its status correctly + public void testLRAStatusWithJson() { + URI lraId = lraClient.startLRA("testLRAStatusWithJson"); + + // request the status of the LRA that was just started + try (Response r = client + .target(String.format("%s/status", lraId)) + .request() + .accept(MediaType.APPLICATION_JSON) // MediaType.TEXT_PLAIN, the default, is tested elsewhere + .get()) { + int res = r.getStatus(); + + if (res != OK.getStatusCode()) { + fail("unable to read LRAData: HTTP status was " + res); + } + + String json = r.readEntity(String.class); // the entity body should be a Json representation of the LRA + + try { + JsonNode node = new ObjectMapper().readTree(json); + // read the value + JsonNode n = node.get("status").get("string"); + String v = n.textValue(); + // or Json.createReader(new StringReader(info)).readObject(); for the raw Json + + // validate the LRA status + assertEquals(LRAStatus.Active.name(), v); + + } catch (JsonProcessingException e) { + fail("Unable to parse JSON response: " + json); + } + } finally { + // clean up + try { + lraClient.cancelLRA(lraId); + } catch (WebApplicationException e) { + fail("Could not clean up: " + e); + } + } + } + + @Test + public void testStartAcceptJson() { + try (Response response = client.target(coordinatorPath + "/start") + .request() + .header(LRA_API_VERSION_HEADER_NAME, LRAConstants.CURRENT_API_VERSION_STRING) + .accept(MediaType.APPLICATION_JSON) + .post(null)) { + if (response.getStatus() != OK.getStatusCode()) { + LRALogger.logger.debugf("Error getting all LRAs from the coordinator, response status: %d", + response.getStatus()); + throw new WebApplicationException(response); + } + + String json = ""; + URI lraId = null; + + try { + json = response.readEntity(String.class); + + JsonNode node = new ObjectMapper().readTree(json); + // read the value + JsonNode n = node.get("lraId").get("string"); + String v = n.textValue(); + lraId = new URI(v); + // or Json.createReader(new StringReader(info)).readObject(); for the raw Json + + // clean up + lraClient.closeLRA(lraId); + } catch (JsonProcessingException | URISyntaxException e) { + fail("Unable to parse JSON response: " + json); + } catch (WebApplicationException e) { + fail("Unable to close lra: " + lraId);; + } + } + } + + @Test + public void testJoinLRAViaBody() { + URI lraId = lraClient.startLRA("testJoinLRAViaBody"); + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8); // must be valid + + try (Response response = client.target(coordinatorPath) + .path(encodedLraId) + .request() + .accept(MediaType.APPLICATION_JSON) + // the request body should correspond to a valid compensator or be empty + .put(Entity.text(""))) { + + assertEquals(OK.getStatusCode(), response.getStatus()); + + String recoveryUrl = null; + + try { + String json = response.readEntity(String.class); + JsonNode node = new ObjectMapper().readTree(json); + // read the value + JsonNode n = node.get("recoveryUrl"); + recoveryUrl = n.textValue(); + } catch (JsonProcessingException e) { + fail("could not read json response: " + e.getMessage()); + } + + try { + // just validate that the join request returned a valid URL + new URI(recoveryUrl); + } catch (URISyntaxException e) { + fail("testJoinLRAViaBody returned an invalid recovery URL: " + recoveryUrl); + } + } finally { + lraClient.closeLRA(lraId); + } + } + /* * Participants can update their callbacks to facilitate recovery. * Test that the compensate endpoint can be changed: @@ -417,7 +573,7 @@ public void testReplaceCompensator() throws URISyntaxException { // check that performing a GET on the recovery url returns the participant callbacks: try (Response r1 = client.target(recoveryUrl).request().get()) { int res = r1.getStatus(); - if (res != Response.Status.OK.getStatusCode()) { + if (res != OK.getStatusCode()) { // clean up and fail fail("get recovery url failed: " + res); } @@ -434,10 +590,10 @@ public void testReplaceCompensator() throws URISyntaxException { // use the recovery url to ask the coordinator to compensate on a different endpoint try (Response r1 = client.target(recoveryUrl).request().put(Entity.text(newCompensator))) { int res = r1.getStatus(); - if (res != Response.Status.OK.getStatusCode()) { + if (res != OK.getStatusCode()) { // clean up and fail try (Response r = client.target(String.format("%s/cancel", lraUrl)).request().put(null)) { - if (r.getStatus() != Response.Status.OK.getStatusCode()) { + if (r.getStatus() != OK.getStatusCode()) { fail("move and cancel failed"); } } @@ -448,7 +604,7 @@ public void testReplaceCompensator() throws URISyntaxException { // cancel the LRA try (Response r2 = client.target(String.format("%s/cancel", lraUrl)).request().put(null)) { int res = r2.getStatus(); - if (res != Response.Status.OK.getStatusCode()) { + if (res != OK.getStatusCode()) { fail("unable to cleanup: " + res); } }