diff --git a/pom.xml b/pom.xml index 745a1ff55f..c2f100bf13 100644 --- a/pom.xml +++ b/pom.xml @@ -497,6 +497,7 @@ 1.1.2 1.0.10 1.0.0.Beta1 + 1.3 1.5.0 diff --git a/rts/lra/client/src/main/java/io/narayana/lra/client/NarayanaLRAClient.java b/rts/lra/client/src/main/java/io/narayana/lra/client/NarayanaLRAClient.java index 5564d054e5..342207360b 100644 --- a/rts/lra/client/src/main/java/io/narayana/lra/client/NarayanaLRAClient.java +++ b/rts/lra/client/src/main/java/io/narayana/lra/client/NarayanaLRAClient.java @@ -60,7 +60,6 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.net.URLDecoder; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -744,9 +743,8 @@ private URI enlistCompensator(URI uri, Long timelimit, String linkHeader, String String recoveryUrl = null; try { recoveryUrl = response.getHeaderString(LRA_HTTP_RECOVERY_HEADER); - String url = URLDecoder.decode(recoveryUrl, StandardCharsets.UTF_8.name()); - return new URI(url); - } catch (URISyntaxException | UnsupportedEncodingException e) { + return new URI(recoveryUrl); + } catch (URISyntaxException e) { LRALogger.logger.infof(e,"join %s returned an invalid recovery URI '%s': %s", lraId, recoveryUrl, responseEntity); throwGenericLRAException(null, Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), "join " + lraId + " returned an invalid recovery URI '" + recoveryUrl + "' : " + responseEntity, e); 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 dc088f57d0..d792621702 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 @@ -27,6 +27,7 @@ import io.narayana.lra.LRAData; import io.narayana.lra.coordinator.domain.model.LongRunningAction; import io.narayana.lra.coordinator.domain.service.LRAService; +import io.narayana.lra.coordinator.internal.APIVersion; import io.narayana.lra.logging.LRALogger; import javax.enterprise.context.ApplicationScoped; @@ -58,8 +59,8 @@ import java.net.URL; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -90,121 +91,143 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR; import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; +import static javax.ws.rs.core.Response.Status.OK; +import static io.narayana.lra.LRAConstants.LRA_API_VERSION_HEADER_NAME; 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; @ApplicationScoped @Path(COORDINATOR_PATH_NAME) -@Tag(name = "LRA Coordinator") +@Tag(name = "LRA Coordinator", description = "Operations to work with active LRAs (to start, to get a status, to finish etc.)") public class Coordinator { + private static final APIVersion currentAPIVersion = APIVersion.instanceOf("1.0"); @Context private UriInfo context; @Inject // Will not work in an async scenario: CDI-452 - LRAService lraService; + private LRAService lraService; - // Performing a GET on /lra-io.narayana.lra.coordinator returns a list of all LRAs. @GET @Path("/") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Returns all LRAs", - description = "Gets both active and recovering LRAs") - @APIResponse(description = "The LRA", - content = @Content(schema = @Schema(type = SchemaType.ARRAY, implementation = LRAData.class)) - ) - public Collection getAllLRAs( + @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", + content = @Content(schema = @Schema(title = "LRAData array", type = SchemaType.ARRAY, implementation = LRAData.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME)}), + @APIResponse(responseCode = "400", description = "", + content = @Content(schema = @Schema(title = "Error description", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME)}), + }) + 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) { + @QueryParam(STATUS_PARAM_NAME) @DefaultValue("") String state, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) { + verifyVersion(version); LRAStatus requestedLRAStatus = null; if(!state.isEmpty()) { - requestedLRAStatus = LRAStatus.valueOf(state); + try { + requestedLRAStatus = LRAStatus.valueOf(state); + } catch (NullPointerException | IllegalArgumentException e) { + String errorMsg = "Status " + state + " is not a valid LRAStatus value"; + throw new WebApplicationException(errorMsg, e, + Response.status(BAD_REQUEST).header(LRA_API_VERSION_HEADER_NAME, version).entity(errorMsg).build()); + } } - Collection lras = lraService.getAll(requestedLRAStatus); + List lras = lraService.getAll(requestedLRAStatus); - if (lras == null) { - LRALogger.i18NLogger.error_invalidQueryForGettingLraStatuses(state); - String errMsg = String.format("Invalid query '%s' to get LRAs", state); - throw new WebApplicationException(errMsg, - Response.status(BAD_REQUEST).entity(errMsg).build()); - } - - return lras; + return Response.ok() + .entity(lras) + .header(LRA_API_VERSION_HEADER_NAME, version).build(); } @GET @Path("{LraId}/status") @Produces(MediaType.TEXT_PLAIN) @Operation(summary = "Obtain the status of an LRA as a string") - @Schema(implementation = String.class) @APIResponses({ - @APIResponse(responseCode = "404", - description = "The coordinator has no knowledge of this LRA"), - @APIResponse(responseCode = "204", - description = "The LRA exists and has not yet been asked to close or cancel"), - @APIResponse(responseCode = "200", - description = "The LRA exists. The status is reported in the content body.") + @APIResponse(responseCode = "200", description = "The LRA exists. The status is reported in the content body.", + content = @Content(schema = @Schema(implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(title = "Unknown LRA error", implementation = String.class))), }) public Response getLRAStatus( - @Parameter(name = "LraId", - description = "The unique identifier of the LRA", required = true) - @PathParam("LraId")String lraId) throws NotFoundException { - LongRunningAction transaction = lraService.getTransaction(toURI(lraId)); + @Parameter(name = "LraId", description = "The unique identifier of the LRA." + + "Expecting to be a valid URL where the participant can be contacted at. If not in URL format it will be considered " + + "to be a id which will be declared to exist at URL where coordinator is deployed at.", required = true) + @PathParam("LraId")String lraId, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) + throws NotFoundException { + verifyVersion(version); + LongRunningAction transaction = lraService.getTransaction(toURI(lraId)); // throws NotFoundException -> response 404 LRAStatus status = transaction.getLRAStatus(); if (status == null) { status = LRAStatus.Active; } - return Response.ok(status.name()).build(); + return Response.ok() + .entity(status.name()) + .header(LRA_API_VERSION_HEADER_NAME, version).build(); } @GET @Path("{LraId}") @Produces(MediaType.APPLICATION_JSON) - @Operation(summary = "Obtain the status of an LRA as a JSON structure") + @Operation(summary = "Obtain the information about an LRA as a JSON structure") @APIResponses({ - @APIResponse(responseCode = "404", - description = "The coordinator has no knowledge of this LRA", - content = @Content(schema = @Schema(implementation = LRAData.class))), - @APIResponse(responseCode = "204", - description = "The LRA exists and has not yet been asked to close or cancel", - content = @Content(schema = @Schema(implementation = LRAData.class))), - @APIResponse(responseCode = "200", - description = "The LRA exists. The status is reported in the content body.", - content = @Content(schema = @Schema(implementation = LRAData.class))) + @APIResponse(responseCode = "200", description = "The LRA exists and the information is packed as JSON in the content body.", + content = @Content(schema = @Schema(title = "LRAData", implementation = LRAData.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(title = "Error description", implementation = String.class))) }) - public LRAData getLRAInfo( + public Response getLRAInfo( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) - @PathParam("LraId") String lraId) throws NotFoundException { - - return lraService.getLRA(toURI(lraId)); + @PathParam("LraId") String lraId, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) { + verifyVersion(version); + URI lraIdURI = toURI(lraId); + LRAData lraData = lraService.getLRA(lraIdURI); + return Response.status(OK).entity(lraData) + .header(LRA_API_VERSION_HEADER_NAME, version).build(); } - // Performing a POST on /lra-io.narayana.lra.coordinator/start?ClientID= will start a new lra with a default timeout and - // return a lra URL of the form /lra-io.narayana.lra.coordinator/. - // Adding a query parameter, timeout=, will start a new lra with the specified timeout. - // If the lra is terminated because of a timeout, the lra URL is deleted and all further invocations on the URL will return 404. - // The invoker can assume this was equivalent to a compensate operation. + /** + * Performing a POST on {@value LRAConstants#COORDINATOR_PATH_NAME}/start?ClientID= + * will start a new lra with a default timeout and return a lra URL + * of the form /{@value LRAConstants#COORDINATOR_PATH_NAME}/. + * Adding a query parameter, {@value LRAConstants#TIMELIMIT_PARAM_NAME}=, will start a new lra with the specified timeout. + * If the lra is terminated because of a timeout, the lra URL is deleted and all further invocations on the URL will return 404. + * The invoker can assume this was equivalent to a compensate operation. + */ @POST @Path("start") @Produces(MediaType.TEXT_PLAIN) @Bulkhead @Operation(summary = "Start a new LRA", - description = "The LRA model uses a presumed nothing protocol: the coordinator must communicate\n" - + "with Compensators in order to inform them of the LRA activity. Every time a\n" - + "Compensator is enrolled with a LRA, the coordinator must make information about\n" - + "it durable so that the Compensator can be contacted when the LRA terminates,\n" - + "even in the event of subsequent failures. Compensators, clients and coordinators\n" - + "cannot make any presumption about the state of the global transaction without\n" + description = "The LRA model uses a presumed nothing protocol: the coordinator must communicate " + + "with Compensators in order to inform them of the LRA activity. Every time a " + + "Compensator is enrolled with a LRA, the coordinator must make information about " + + "it durable so that the Compensator can be contacted when the LRA terminates, " + + "even in the event of subsequent failures. Compensators, clients and coordinators " + + "cannot make any presumption about the state of the global transaction without " + "consulting the coordinator and all compensators, respectively.") @APIResponses({ @APIResponse(responseCode = "201", description = "The request was successful and the response body contains the id of the new LRA", - content = @Content(schema = @Schema(title = "An LRA id", description = "An URI of the new LRA"))), - @APIResponse(responseCode = "500", - description = "A new LRA could not be started") + content = @Content(schema = @Schema(title = "An LRA id", description = "An URI of the new LRA", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "404", description = "Parent LRA id cannot be joint to the started LRA", + content = @Content(schema = @Schema(title = "Failure description", description = "Message containing problematic LRA id", implementation = String.class))), + @APIResponse(responseCode = "500", description = "A new LRA could not be started. Coordinator internal error.", + content = @Content(schema = @Schema(title = "LRA cannot be started error", implementation = String.class))) }) public Response startLRA( @Parameter(name = CLIENT_ID_PARAM_NAME, @@ -220,8 +243,9 @@ 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(LRA_HTTP_CONTEXT_HEADER) String parentId) throws WebApplicationException { - + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) throws WebApplicationException { + verifyVersion(version); URI parentLRAUrl = null; if (parentLRA != null && !parentLRA.isEmpty()) { @@ -236,12 +260,13 @@ public Response startLRA( String compensatorUrl = String.format("%s/nested/%s", coordinatorUrl, LRAConstants.getLRAUid(lraId)); if (lraService.hasTransaction(parentLRAUrl)) { - Response response = joinLRAViaBody(parentLRAUrl.toASCIIString(), timelimit, null, compensatorUrl); + Response response = joinLRAViaBody(parentLRAUrl.toASCIIString(), timelimit, null, version, compensatorUrl); if (response.getStatus() != Response.Status.OK.getStatusCode()) { - return Response.status(response.getStatus()).build(); + return Response.status(response.getStatus()).header(LRA_API_VERSION_HEADER_NAME, version).build(); } } else { + // TODO: investigate on reasons why starting the nested transaction goes to parent participant URL Client client = null; Response response = null; @@ -254,13 +279,17 @@ public Response startLRA( .get(PARTICIPANT_TIMEOUT, TimeUnit.SECONDS); if (response.getStatus() != Response.Status.OK.getStatusCode()) { - String errMessage = String.format("The coordinator at %s returned an unexpected response: %d", - parentLRAUrl, response.getStatus()); + String errMessage = String.format("The coordinator at %s returned an unexpected response: %d" + + "when trying the LRA '%s' to join the parent LRA id '%s'", parentLRAUrl, response.getStatus(), lraId, parentLRA); return Response.status(response.getStatus()).entity(errMessage).build(); } } catch (Exception e) { - LRALogger.logger.debugf("Cannot contact the LRA Coordinator at %s", parentLRA); - throw new WebApplicationException(e); + String errorMsg = String.format("Cannot contact the LRA Coordinator at '%s' for LRA '%s' joining parent LRA '%s'", + parentLRAUrl, lraId, parentLRA); + LRALogger.logger.debugf(errorMsg); + throw new WebApplicationException(errorMsg, e, + Response.status(INTERNAL_SERVER_ERROR).header(LRA_API_VERSION_HEADER_NAME, version) + .entity(errorMsg).build()); } finally { if (client != null) { client.close(); @@ -274,27 +303,36 @@ public Response startLRA( return Response.created(lraId) .entity(lraId) .header(LRA_HTTP_CONTEXT_HEADER, Current.getContexts()) + .header(LRA_API_VERSION_HEADER_NAME, version) .build(); } @PUT @Path("{LraId}/renew") @Operation(summary = "Update the TimeLimit for an existing LRA", - description = "LRAs can be automatically cancelled if they aren't closed or cancelled before the TimeLimit\n" - + "specified at creation time is reached.\n" - + "The time limit can be updated.\n") + description = "LRAs can be automatically cancelled if they aren't closed or cancelled before the TimeLimit " + + "specified at creation time is reached. The time limit can be updated.") @APIResponses({ - @APIResponse(responseCode = "200", description = "If the LRA timelimit has been updated"), - @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA"), - @APIResponse(responseCode = "412", - description = "The LRA is not longer active (ie the complete or compensate messages have been sent") + @APIResponse(responseCode = "200", description = "If the LRA time limit has been updated", + content = @Content(schema = @Schema(title = "Renewed LRA id", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA or " + + "the LRA is not longer active (ie the complete or compensate messages have been sent", + content = @Content(schema = @Schema(title = "Unknown LRA error", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), }) public Response renewTimeLimit( + @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) + @PathParam("LraId") String lraId, @Parameter(name = TIMELIMIT_PARAM_NAME, description = "The new time limit for the LRA", required = true) - @QueryParam(TIMELIMIT_PARAM_NAME) @DefaultValue("0") Long timelimit, - @PathParam("LraId")String lraId) throws NotFoundException { - - return Response.status(lraService.renewTimeLimit(toURI(lraId), timelimit)).build(); + @QueryParam(TIMELIMIT_PARAM_NAME) @DefaultValue("0") Long timeLimit, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) { + verifyVersion(version); + return Response.status(lraService.renewTimeLimit(toURI(lraId), timeLimit)) + .header(LRA_API_VERSION_HEADER_NAME, version) + .entity(lraId) + .build(); } @GET @@ -352,11 +390,14 @@ public Response forgetNestedLRA(@PathParam("NestedLraId") String nestedLraId) { return Response.ok().build(); } - // Performing a PUT on lra-coordinator//close will trigger the successful completion of the lra and all - // compensators will be dropped by the io.narayana.lra.coordinator. - // The complete message will be sent to the compensators. Question: is this message best effort or at least once? - // Upon termination, the URL is implicitly deleted. If it no longer exists, then 404 will be returned. - // The invoker cannot know for sure whether the lra completed or compensated without enlisting a participant. + /** + * Performing a PUT on {@value LRAConstants#COORDINATOR_PATH_NAME}//close will trigger the successful completion + * of the LRA and all compensators will be dropped by the LRA Coordinator. + * The complete message will be sent to the compensators. + * Upon termination, the URL is implicitly deleted. If it no longer exists, then 404 will be returned. + * The invoker cannot know for sure whether the lra completed or compensated without enlisting a participant. + */ + // TODO: Question: is this message best effort or at least once? // TODO rework spec to allow an LRAStatus header everywhere @PUT @Path("{LraId}/close") @@ -369,15 +410,22 @@ public Response forgetNestedLRA(@PathParam("NestedLraId") String nestedLraId) { + " The invoker cannot know for sure whether the lra completed" + " or compensated without enlisting a participant.") @APIResponses({ - @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA"), - @APIResponse(responseCode = "200", description = "The complete message was sent to all coordinators", - content = @Content( - schema = @Schema(title = "one of the LRAStatus enum values", implementation = String.class))) + @APIResponse(responseCode = "200", description = "The complete message was sent to all coordinators", + content = @Content(schema = @Schema(title = "LRAStatus enum value", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(title = "Unknown LRA error", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), }) public Response closeLRA( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) - @PathParam("LraId")String txId) throws NotFoundException { - return Response.ok(endLRA(toURI(txId), false, false).name()).build(); + @PathParam("LraId") String txId, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) { + verifyVersion(version); + return Response.ok(endLRA(toURI(txId), false, false).name()) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build(); } @PUT @@ -389,15 +437,23 @@ public Response closeLRA( + " Upon termination, the URL is implicitly deleted." + " The invoker cannot know for sure whether the lra completed or compensated without enlisting a participant.") @APIResponses({ - @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA"), @APIResponse(responseCode = "200", description = "The compensate message was sent to all coordinators", - content = @Content( - schema = @Schema(title = "one of the LRAStatus enum values", implementation = String.class))) + content = @Content(schema = @Schema(title = "LRAStatus enum value", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(title = "Unknown LRA error", implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), }) public Response cancelLRA( - @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) - @PathParam("LraId")String lraId) throws NotFoundException { - return Response.ok(endLRA(toURI(lraId), true, false).name()).build(); + @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) + @PathParam("LraId")String lraId, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version) + throws NotFoundException { + verifyVersion(version); + return Response.ok(endLRA(toURI(lraId), true, false).name()) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build(); } @@ -412,28 +468,33 @@ private LRAStatus endLRA(URI lraId, boolean compensate, boolean fromHierarchy) t @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "A Compensator can join with the LRA at any time prior to the completion of an activity") @APIResponses({ - @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA"), - @APIResponse(responseCode = "412", - description = "The LRA is not longer active (ie in the complete or compensate messages have been sent"), @APIResponse(responseCode = "200", - description = "The participant was successfully registered with the LRA and" - + " the response body contains a unique resource reference for that participant:\n" - + " - HTTP GET on the reference returns the original participant URL;\n" - + " - HTTP PUT on the reference will overwrite the old participant URL with the new one supplied.", - headers = @Header(name = LRA_HTTP_RECOVERY_HEADER, - description = "If the participant is successfully registered with the LRA then this header\n" - + " will contain a unique resource reference for that participant:\n" - + " - HTTP GET on the reference returns the original participant URL;\n" - + " - HTTP PUT on the reference will overwrite the old participant URL with the new one supplied.", - schema = @Schema(implementation = String.class)), - content = @Content(schema = @Schema(title = "A new LRA recovery id", description = "An URI representing the " + - "recovery id of this join request"))) + description = "The participant was successfully registered with the LRA", + content = @Content(schema = @Schema(title = "A new LRA recovery id", + description = "An URI representing the recovery id of this join request",implementation = String.class)), + headers = { + @Header(name = LRA_HTTP_RECOVERY_HEADER, description = "It contains a unique resource reference for that participant:\n" + + " - HTTP GET on the reference returns the original participant URL;\n" // TODO: verify recovery coordinator works this way + + " - HTTP PUT on the reference will overwrite the old participant URL with the new one supplied.", + schema = @Schema(implementation = String.class)), + @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "400", description = "Link does not contain all required fields for joining the LRA. " + + "Probably no compensator or after 'rel' is available.", + content = @Content(schema = @Schema(title = "Error to enlist", implementation = String.class))), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(title = "Unknown LRA error", implementation = String.class))), + @APIResponse(responseCode = "412", + description = "The LRA is not longer active, or wrong format of compensator data", + content = @Content(schema = @Schema(title = "Wrong format LRA error", implementation = String.class)), + headers = {@Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME)}), + @APIResponse(responseCode = "500", description = "Format of the compensator data (e.g. Link format) could not be processed", + content = @Content(schema = @Schema(title = "Internal failure", implementation = String.class))), }) public Response joinLRAViaBody( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) @PathParam("LraId")String lraId, @Parameter(name = TIMELIMIT_PARAM_NAME, - description = "The time limit (in seconds) that the Compensator can guarantee that it can compensate " + description = "The time limit in milliseconds that the Compensator can guarantee that it can compensate " + "the work performed by the service. After this time period has elapsed, it may no longer be " + "possible to undo the work within the scope of this (or any enclosing) LRA. It may therefore " + "be necessary for the application or service to start other activities to explicitly try to " @@ -445,15 +506,17 @@ public Response joinLRAViaBody( + " the status of the participant. The link rel names are" + " complete, compensate and status.") @HeaderParam("Link") @DefaultValue("") String compensatorLink, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version, @RequestBody(name = "Compensator data", - description = "opaque data that will be stored with the coordinator and passed back to\n" - + "the participant when the LRA is closed or cancelled.\n") - String compensatorData) throws NotFoundException { + description = "opaque data that will be stored with the coordinator and passed back to " + + "the participant when the LRA is closed or cancelled.") String compensatorData) throws NotFoundException { + verifyVersion(version); // test to see if the join request contains any participant specific data boolean isLink = isLink(compensatorData); if (compensatorLink != null && !compensatorLink.isEmpty()) { - return joinLRA(toURI(lraId), timeLimit, null, compensatorLink, compensatorData); + return joinLRA(toURI(lraId), timeLimit, null, compensatorLink, compensatorData, version); } if (!isLink) { // interpret the content as a standard participant url @@ -466,16 +529,19 @@ public Response joinLRAViaBody( terminateURIs.put(COMPLETE, new URL(compensatorData + "complete").toExternalForm()); terminateURIs.put(STATUS, new URL(compensatorData + "status").toExternalForm()); } catch (MalformedURLException e) { + String errorMsg = String.format("Cannot join to LRA id '%s' with body as compensator url '%s' is invalid", + lraId, compensatorData); if (LRALogger.logger.isTraceEnabled()) { - LRALogger.logger.tracef(e, "Cannot join to LRA id '%s' with body as compensator url '%s' is invalid", - lraId, compensatorData); + LRALogger.logger.trace(errorMsg, e); } - return Response.status(PRECONDITION_FAILED).build(); + return Response.status(PRECONDITION_FAILED) + .header(LRA_API_VERSION_HEADER_NAME, version) + .entity(errorMsg) + .build(); } - // register with the coordinator - // put the lra id in an http header + // register with the coordinator, put the lra id in an http header StringBuilder linkHeaderValue = new StringBuilder(); terminateURIs.forEach((k, v) -> makeLink(linkHeaderValue, "", k, v)); // or use Collectors.joining(",") @@ -483,7 +549,7 @@ public Response joinLRAViaBody( compensatorData = linkHeaderValue.toString(); } - return joinLRA(toURI(lraId), timeLimit, null, compensatorData, null); + return joinLRA(toURI(lraId), timeLimit, null, compensatorData, null, version); } @@ -513,7 +579,7 @@ private boolean isLink(String linkString) { } } - private Response joinLRA(URI lraId, long timeLimit, String compensatorUrl, String linkHeader, String userData) + private Response joinLRA(URI lraId, long timeLimit, String compensatorUrl, String linkHeader, String userData, String version) throws NotFoundException { final String recoveryUrlBase = String.format("%s%s/%s", context.getBaseUri().toASCIIString(), COORDINATOR_PATH_NAME, RECOVERY_COORDINATOR_PATH_NAME); @@ -527,40 +593,65 @@ private Response joinLRA(URI lraId, long timeLimit, String compensatorUrl, Strin .entity(recoveryUrl.toString()) .location(new URI(recoveryUrl.toString())) .header(LRA_HTTP_RECOVERY_HEADER, recoveryUrl) + .header(LRA_API_VERSION_HEADER_NAME, version) .build(); } catch (URISyntaxException e) { LRALogger.i18NLogger.error_invalidRecoveryUrlToJoinLRAURI(recoveryUrl.toString(), lraId); String errorMsg = lraId + ": Invalid recovery URL " + recoveryUrl.toString(); throw new WebApplicationException(errorMsg, e , - Response.status(INTERNAL_SERVER_ERROR).entity(errorMsg).build()); + Response.status(INTERNAL_SERVER_ERROR).entity(errorMsg) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build()); } } - // A participant can resign from a lra at any time prior to the completion of an activity by performing a - // PUT on lra-coordinator//remove with the URL of the participant. + /** + * A participant can resign from a lra at any time prior to the completion of an activity by performing a PUT + * PUT on {@value LRAConstants#COORDINATOR_PATH_NAME}//remove with the URL of the participant. + */ @PUT @Path("{LraId}/remove") @Produces(MediaType.APPLICATION_JSON) @Operation(summary = "A Compensator can resign from the LRA at any time prior to the completion of an activity") @APIResponses({ - @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA"), + @APIResponse(responseCode = "200", description = "If the participant was successfully removed from the LRA", + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "400", description = "The coordinator has no knowledge of this participant compensator URL", + content = @Content(schema = @Schema(title = "Unknown participant error", implementation = String.class))), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(title = "Unknown LRA id error", implementation = String.class))), @APIResponse(responseCode = "412", description = "The LRA is not longer active (ie in the complete or compensate messages have been sent"), - @APIResponse(responseCode = "200", description = "If the participant was successfully removed from the LRA") }) public Response leaveLRA( @Parameter(name = "LraId", description = "The unique identifier of the LRA", required = true) @PathParam("LraId") String lraId, - String compensatorUrl) throws NotFoundException, URISyntaxException { - String reqUri = context.getRequestUri().toString(); - - reqUri = reqUri.substring(0, reqUri.lastIndexOf('/')); - - int status = 0; - - status = lraService.leave(new URI(reqUri), compensatorUrl); + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(JaxRsActivator.LRA_API_VERSION_STRING) String version, + String participantCompensatorUrl) throws NotFoundException, URISyntaxException { + verifyVersion(version); + int status = lraService.leave(toURI(lraId), participantCompensatorUrl); + + return Response.status(status) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build(); + } - return Response.status(status).build(); + private void verifyVersion(String versionString) { + APIVersion apiVersion = null; + try { + apiVersion = APIVersion.instanceOf(versionString); + if (apiVersion.compareTo(currentAPIVersion) > 0) { + String errorMsg = "Demanded API version " + versionString + + " is bigger than the supported one " + currentAPIVersion; + throw new WebApplicationException(errorMsg, + Response.status(PRECONDITION_FAILED).entity(errorMsg).build()); + } + } catch (Exception iae) { + String errorMsg = "Wrong format of the provided version " + versionString + ": " + iae.getMessage(); + throw new WebApplicationException(errorMsg, iae, + Response.status(PRECONDITION_FAILED).entity(errorMsg).build()); + } } private URI toDecodedURI(String lraId) { diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/JaxRsActivator.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/JaxRsActivator.java index 2a0963a327..38e73cb7ed 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/JaxRsActivator.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/JaxRsActivator.java @@ -1,8 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + package io.narayana.lra.coordinator.api; +import io.narayana.lra.LRAConstants; +import io.narayana.lra.coordinator.internal.APIVersion; +import org.eclipse.microprofile.openapi.annotations.Components; import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; +import org.eclipse.microprofile.openapi.annotations.headers.Header; +import org.eclipse.microprofile.openapi.annotations.info.Contact; import org.eclipse.microprofile.openapi.annotations.info.Info; -import org.eclipse.microprofile.openapi.annotations.tags.Tag; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; import javax.ws.rs.ApplicationPath; import javax.ws.rs.core.Application; @@ -10,9 +38,24 @@ // mark the war as a JAX-RS archive @ApplicationPath("/") @OpenAPIDefinition( - info = @Info(title = "LRA Coordinator", version = JaxRsActivator.LRA_API_VERSION), - tags = @Tag(name = "LRA Coordinator") + info = @Info(title = "LRA Coordinator", version = JaxRsActivator.LRA_API_VERSION_STRING, + contact = @Contact(name = "Narayana", url = "https://narayana.io")), + components = @Components( + parameters = { + @Parameter(name = LRAConstants.LRA_API_VERSION_HEADER_NAME, in = ParameterIn.HEADER, + description = "API version string in format [major].[minor]-[prerelease]. Major and minor are required and to be numbers, prerelease part is optional.") + }, + headers = { + @Header(name = LRAConstants.LRA_API_VERSION_HEADER_NAME, description = "Narayana LRA API version that processed the request") + } + ) ) public class JaxRsActivator extends Application { - static final String LRA_API_VERSION = "1.0-RC1"; + /** + * The LRA API version supported for the release. + * Any bigger version is considered as unimplemented and unknown. + * Any lower version is considered as older, implemented but deprecated and in case not supported. + */ + public static final String LRA_API_VERSION_STRING = "1.0-RC1"; + public static final APIVersion LRA_API_VERSION = APIVersion.instanceOf(LRA_API_VERSION_STRING); } diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRARecord.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRARecord.java index 7d499d747c..fc1e18a0b8 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRARecord.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRARecord.java @@ -112,11 +112,11 @@ public LRARecord() { }); if (parseException[0] != null) { - String errorMsg = lraId + ": Invalid link URI: " + parseException[0]; + String errorMsg = lra.getId() + ": Invalid link URI: " + parseException[0]; throw new WebApplicationException(errorMsg, parseException[0], Response.status(BAD_REQUEST).entity(errorMsg).build()); } else if (compensateURI == null && afterURI == null) { - String errorMsg = lraId + ": Invalid link URI: missing compensator"; + String errorMsg = lra.getId() + ": Invalid link URI: missing compensator or after LRA callback"; throw new WebApplicationException(errorMsg, Response.status(BAD_REQUEST).entity(errorMsg).build()); } } else { @@ -155,7 +155,7 @@ String getParticipantPath() { static String cannonicalForm(String linkStr) throws URISyntaxException { if (!linkStr.contains(">;")) { - return cannonicalURI(new URI(linkStr)).toASCIIString(); + return new URI(linkStr).toASCIIString(); } SortedMap lm = new TreeMap<>(); diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/service/LRAService.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/service/LRAService.java index f42cfba42c..a4afdddd78 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/service/LRAService.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/service/LRAService.java @@ -276,8 +276,9 @@ public LRAData endLRA(URI lraId, boolean compensate, boolean fromHierarchy) { LongRunningAction transaction = getTransaction(lraId); if (transaction.getLRAStatus() != LRAStatus.Active && !transaction.isRecovering() && transaction.isTopLevel()) { - throw new WebApplicationException(Response.status(Response.Status.PRECONDITION_FAILED) - .entity(String.format("%s: LRA is closing or closed: endLRA", lraId)).build()); + String errorMsg = String.format("%s: LRA is closing or closed: endLRA", lraId); + throw new WebApplicationException(errorMsg, Response.status(Response.Status.PRECONDITION_FAILED) + .entity(errorMsg).build()); } transaction.finishLRA(compensate); @@ -305,14 +306,16 @@ public int leave(URI lraId, String compensatorUrl) { try { if (!transaction.forgetParticipant(compensatorUrl)) { - if (LRALogger.logger.isInfoEnabled()) { - LRALogger.logger.infof("LRAServicve.forget %s failed%n", lraId); - } + String errorMsg = String.format("LRAService.forget %s failed on participant compensator url '%s'", + lraId, compensatorUrl); + throw new WebApplicationException(errorMsg, Response.status(Response.Status.BAD_REQUEST) + .entity(errorMsg).build()); } - return Response.Status.OK.getStatusCode(); } catch (Exception e) { - return Response.Status.BAD_REQUEST.getStatusCode(); + String errorMsg = String.format("LRAService.forget %s failed on finding participant '%s'", lraId, compensatorUrl); + throw new WebApplicationException(errorMsg, e, Response.status(Response.Status.BAD_REQUEST) + .entity(errorMsg).build()); } } @@ -408,7 +411,7 @@ public int renewTimeLimit(URI lraId, Long timelimit) { LongRunningAction lra = lras.get(lraId); if (lra == null) { - return Response.Status.PRECONDITION_FAILED.getStatusCode(); + return NOT_FOUND.getStatusCode(); } return lra.setTimeLimit(timelimit); diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/internal/APIVersion.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/internal/APIVersion.java new file mode 100644 index 0000000000..3031e6117d --- /dev/null +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/internal/APIVersion.java @@ -0,0 +1,125 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package io.narayana.lra.coordinator.internal; + +import io.narayana.lra.coordinator.api.JaxRsActivator; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + *

+ * The LRA API version. The most probably provided in header {@code io.narayana.lra.LRAConstants#LRA_API_VERSION_HEADER_NAME}. + * The supported format is {@code #expectedFormat}. + *

+ *

+ * The major and minor parts are numbers, the preRelease part is an arbitrary string. + * Two instances of the {@link APIVersion} may be compared. + * There is taken into account only the major and minor parts, + * the preRelease is ignored. + * But two {@link APIVersion} instances are {@link Object#equals(Object)} only if all three parts are the same. + *

+ */ +public class APIVersion implements Comparable { + private static final String expectedFormat = "major.minor-preRelease"; + private static final Pattern versionPattern = Pattern.compile("^(\\d+)\\.(\\d+)(?:-(.+))?"); + + private final int major, minor; + private final String preRelease; + + /** + * Parsing the version string and returns a {@link APIVersion} instance. + * The expected version format is {@code #expectedFormat}. + * If null is provided as String to parse then the most up-to-date + * LRA API version is taken from {@link JaxRsActivator#LRA_API_VERSION}. + * + * @param versionString version string to be parsed; when null or empty the most up-to-date + * {@link JaxRsActivator#LRA_API_VERSION} is returned + * @return instance of the {@link APIVersion} class based on the parsed String + * @throws IllegalArgumentException thrown when version string has a wrong format + */ + public static APIVersion instanceOf(String versionString) { + Matcher versionMatcher = versionPattern.matcher(versionString); + if(versionString == null || versionString.isEmpty()) { + return JaxRsActivator.LRA_API_VERSION; + } + if(!versionMatcher.matches()) { + throw new IllegalArgumentException("Cannot parse provided version string " + versionString + + " as it does not match the expected format '" + expectedFormat + "'"); + } + try { + int major = Integer.valueOf(versionMatcher.group(1)); + int minor = Integer.valueOf(versionMatcher.group(2)); + String preRelease = versionMatcher.group(3); + return new APIVersion(major, minor, preRelease); + } catch (NumberFormatException nfe) { + throw new IllegalArgumentException("The version string " + versionString + " matches the expected format " + expectedFormat + + " but the major.minor cannot be converted to numbers", nfe); + } + } + + public APIVersion(int major, int minor, String preRelease) { + this.major = major; + this.minor = minor; + this.preRelease = preRelease; + } + + @Override + public int compareTo(APIVersion anotherVersion) { + int result = Integer.compare(major, anotherVersion.major); + if (result == 0) { + result = Integer.compare(minor, anotherVersion.minor); + } + return result; + } + @Override + public String toString() { + StringBuilder sb = new StringBuilder() + .append(major).append(".").append(minor); + if (preRelease != null) { + sb.append("-").append(preRelease); + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + APIVersion that = (APIVersion) o; + return major == that.major && + minor == that.minor && + ((preRelease == null && that.preRelease == null) + || (preRelease != null && preRelease.equals(that.preRelease))); + } + + @Override + public int hashCode() { + if (preRelease == null) { + return Objects.hash(major, minor); + } else { + return Objects.hash(major, minor, preRelease); + } + } +} diff --git a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery1TestCase.java b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery1TestCase.java index 785f04d610..09514c15fa 100644 --- a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery1TestCase.java +++ b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery1TestCase.java @@ -21,15 +21,7 @@ */ package io.narayana.lra.coordinator; -import com.arjuna.ats.arjuna.recovery.RecoveryModule; -import io.narayana.lra.Current; -import io.narayana.lra.client.NarayanaLRAClient; import io.narayana.lra.client.internal.proxy.nonjaxrs.LRAParticipantRegistry; -import io.narayana.lra.coordinator.api.Coordinator; -import io.narayana.lra.LRAData; -import io.narayana.lra.coordinator.domain.model.LongRunningAction; -import io.narayana.lra.coordinator.domain.service.LRAService; -import io.narayana.lra.coordinator.internal.LRARecoveryModule; import io.narayana.lra.filter.ServerLRAFilter; import io.narayana.lra.logging.LRALogger; import org.eclipse.microprofile.lra.annotation.LRAStatus; @@ -72,20 +64,8 @@ @RunWith(Arquillian.class) @RunAsClient public class LRACoordinatorRecovery1TestCase extends TestBase { - private static final Package[] coordinatorPackages = { - RecoveryModule.class.getPackage(), - Coordinator.class.getPackage(), - LRAData.class.getPackage(), - LRAStatus.class.getPackage(), - LRALogger.class.getPackage(), - NarayanaLRAClient.class.getPackage(), - Current.class.getPackage(), - LRAService.class.getPackage(), - LRARecoveryModule.class.getPackage(), - LongRunningAction.class.getPackage() - }; - private static final Package[] participantPackages = { + static final Package[] participantPackages = { LRAListener.class.getPackage(), LRA.class.getPackage(), ServerLRAFilter.class.getPackage(), diff --git a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery2TestCase.java b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery2TestCase.java index 9e3e596e0f..8ae5f179b4 100644 --- a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery2TestCase.java +++ b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/LRACoordinatorRecovery2TestCase.java @@ -21,15 +21,7 @@ */ package io.narayana.lra.coordinator; -import com.arjuna.ats.arjuna.recovery.RecoveryModule; -import io.narayana.lra.Current; -import io.narayana.lra.client.NarayanaLRAClient; import io.narayana.lra.client.internal.proxy.nonjaxrs.LRAParticipantRegistry; -import io.narayana.lra.coordinator.api.Coordinator; -import io.narayana.lra.LRAData; -import io.narayana.lra.coordinator.domain.model.LongRunningAction; -import io.narayana.lra.coordinator.domain.service.LRAService; -import io.narayana.lra.coordinator.internal.LRARecoveryModule; import io.narayana.lra.filter.ServerLRAFilter; import io.narayana.lra.logging.LRALogger; import org.eclipse.microprofile.lra.annotation.LRAStatus; @@ -76,19 +68,6 @@ public class LRACoordinatorRecovery2TestCase extends TestBase { private static final Long LONG_TIMEOUT = TimeoutValueAdjuster.adjustTimeout(600000L); // 10 minutes private static final Long SHORT_TIMEOUT = 10000L; // 10 seconds - private static final Package[] coordinatorPackages = { - RecoveryModule.class.getPackage(), - Coordinator.class.getPackage(), - LRAData.class.getPackage(), - LRAStatus.class.getPackage(), - LRALogger.class.getPackage(), - NarayanaLRAClient.class.getPackage(), - Current.class.getPackage(), - LRAService.class.getPackage(), - LRARecoveryModule.class.getPackage(), - LongRunningAction.class.getPackage() - }; - private static final Package[] participantPackages = { LRAListener.class.getPackage(), LRA.class.getPackage(), diff --git a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/TestBase.java b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/TestBase.java index f67e0a65c8..d493255a09 100644 --- a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/TestBase.java +++ b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/TestBase.java @@ -21,8 +21,14 @@ */ package io.narayana.lra.coordinator; +import com.arjuna.ats.arjuna.recovery.RecoveryModule; +import io.narayana.lra.Current; +import io.narayana.lra.LRAData; import io.narayana.lra.client.NarayanaLRAClient; +import io.narayana.lra.coordinator.api.Coordinator; import io.narayana.lra.coordinator.domain.model.LongRunningAction; +import io.narayana.lra.coordinator.domain.service.LRAService; +import io.narayana.lra.coordinator.internal.LRARecoveryModule; import io.narayana.lra.logging.LRALogger; import org.eclipse.microprofile.lra.annotation.LRAStatus; import org.jboss.arquillian.container.test.api.Config; @@ -57,9 +63,8 @@ public abstract class TestBase { @Rule public TestName testName = new TestName(); - private static final String COORDINATOR_CONTAINER = "lra-coordinator"; - - static final String COORDINATOR_DEPLOYMENT = COORDINATOR_CONTAINER; + public static final String COORDINATOR_CONTAINER = "lra-coordinator"; + public static final String COORDINATOR_DEPLOYMENT = COORDINATOR_CONTAINER; private static Path storeDir; @@ -76,6 +81,19 @@ public static void beforeClass() { storeDir = Paths.get(String.format("%s/standalone/data/tx-object-store", System.getProperty("env.JBOSS_HOME", "null"))); } + public static final Package[] coordinatorPackages = { + RecoveryModule.class.getPackage(), + Coordinator.class.getPackage(), + LRAData.class.getPackage(), + LRAStatus.class.getPackage(), + LRALogger.class.getPackage(), + NarayanaLRAClient.class.getPackage(), + Current.class.getPackage(), + LRAService.class.getPackage(), + LRARecoveryModule.class.getPackage(), + LongRunningAction.class.getPackage() + }; + @Before public void before() throws URISyntaxException, MalformedURLException { LRALogger.logger.debugf("Starting test %s", testName); @@ -88,7 +106,7 @@ public void after() { lraClient.close(); } - void startContainer(String bytemanScript) { + public void startContainer(String bytemanScript) { Config config = new Config(); String javaVmArguments = System.getProperty("server.jvm.args"); @@ -106,7 +124,7 @@ void startContainer(String bytemanScript) { deployer.deploy(COORDINATOR_DEPLOYMENT); } - void restartContainer() { + public void restartContainer() { try { // ensure that the controller is not running containerController.kill(COORDINATOR_CONTAINER); @@ -121,7 +139,7 @@ void restartContainer() { containerController.start(COORDINATOR_CONTAINER); } - void stopContainer() { + public void stopContainer() { if (containerController.isStarted(COORDINATOR_CONTAINER)) { LRALogger.logger.debug("Stopping container"); diff --git a/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/internal/APIVersionTest.java b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/internal/APIVersionTest.java new file mode 100644 index 0000000000..8c1968edf4 --- /dev/null +++ b/rts/lra/coordinator/src/test/java/io/narayana/lra/coordinator/internal/APIVersionTest.java @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package io.narayana.lra.coordinator.internal; + +import io.narayana.lra.coordinator.api.JaxRsActivator; +import org.hamcrest.CoreMatchers; +import org.junit.Test; + +import org.hamcrest.MatcherAssert; +import static org.hamcrest.Matchers.comparesEqualTo; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; + +/** + * Unit test for version class. + */ +public class APIVersionTest { + private static final APIVersion testVersion = APIVersion.instanceOf("1.1-Alpha"); + + @Test + public void upToDateVersionIsBiggerOrEqual() { + APIVersion oneZeroVersion = APIVersion.instanceOf("1.0"); + MatcherAssert.assertThat(JaxRsActivator.LRA_API_VERSION, greaterThanOrEqualTo(oneZeroVersion)); + } + + // Object.equal does not match version with pre-release part + @Test + public void preReleaseIsNotEqualToFinal() { + APIVersion version = APIVersion.instanceOf("1.1"); + MatcherAssert.assertThat(version, CoreMatchers.not(testVersion)); + } + + // Comparable.compareTo matches version with and without pre-release part + @Test + public void preReleaseIsCompareEqualToFinal() { + APIVersion version = APIVersion.instanceOf("1.1"); + MatcherAssert.assertThat(version, comparesEqualTo(testVersion)); + } + + @Test + public void lowerMajorVersion() { + APIVersion version = APIVersion.instanceOf("0.1"); + MatcherAssert.assertThat(version, lessThan(testVersion)); + } + + @Test + public void biggerMinorVersion() { + APIVersion version = APIVersion.instanceOf("2.0"); + MatcherAssert.assertThat(version, greaterThan(testVersion)); + } + + @Test + public void lowerMinorVersion() { + APIVersion version = APIVersion.instanceOf("1.0"); + MatcherAssert.assertThat(version, lessThan(testVersion)); + } + + @Test + public void biggerMajorVersion() { + APIVersion version = APIVersion.instanceOf("1.2"); + MatcherAssert.assertThat(version, greaterThan(testVersion)); + } + + @Test(expected = IllegalArgumentException.class) + public void incorrectVersion() { + APIVersion.instanceOf("1,3"); + } +} \ No newline at end of file diff --git a/rts/lra/coordinator/src/test/resources/arquillian.xml b/rts/lra/coordinator/src/test/resources/arquillian.xml index 7ca0fe8967..22bc106baf 100644 --- a/rts/lra/coordinator/src/test/resources/arquillian.xml +++ b/rts/lra/coordinator/src/test/resources/arquillian.xml @@ -28,6 +28,7 @@ ${lra.coordinator.host} ${server.startup.timeout:120} + true diff --git a/rts/lra/lra-service-base/src/main/java/io/narayana/lra/LRAConstants.java b/rts/lra/lra-service-base/src/main/java/io/narayana/lra/LRAConstants.java new file mode 100644 index 0000000000..13f652a20e --- /dev/null +++ b/rts/lra/lra-service-base/src/main/java/io/narayana/lra/LRAConstants.java @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2019, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package io.narayana.lra; + +public abstract class LRAConstants { + public static final String COORDINATOR_PATH_NAME = "lra-coordinator"; + public static final String RECOVERY_COORDINATOR_PATH_NAME = "lra-recovery-coordinator"; + public static final String RECOVERY_COORDINATOR_SUB_RESOURCE_NAME = "recovery"; + + public static final String COMPLETE = "complete"; + public static final String COMPENSATE = "compensate"; + public static final String STATUS = "status"; + public static final String LEAVE = "leave"; + public static final String AFTER = "after"; + public static final String FORGET = "forget"; + + public static final String STATUS_PARAM_NAME = "Status"; + public static final String CLIENT_ID_PARAM_NAME = "ClientID"; + public static final String TIMELIMIT_PARAM_NAME = "TimeLimit"; + public static final String PARENT_LRA_PARAM_NAME = "ParentLRA"; + public static final String RECOVERY_PARAM = "recoveryCount"; + public static final String HTTP_METHOD_NAME = "method"; // the name of the HTTP method used to invoke participants + + public static final String LRA_API_VERSION_HEADER_NAME = "Narayana-LRA-API-version"; +} diff --git a/rts/lra/pom.xml b/rts/lra/pom.xml index dcf33a5413..9e24ebe0de 100644 --- a/rts/lra/pom.xml +++ b/rts/lra/pom.xml @@ -1,8 +1,10 @@ - + --> + 4.0.0 org.jboss.narayana.rts @@ -32,6 +34,7 @@ 3.1.1.Final 1.2.6 + 2.2 UTF-8 @@ -80,6 +83,18 @@ + + org.hamcrest + hamcrest + ${version.hamcrest} + test + + + org.hamcrest + hamcrest-library + ${version.hamcrest} + test + junit junit diff --git a/rts/lra/service-base/src/main/java/io/narayana/lra/LRAConstants.java b/rts/lra/service-base/src/main/java/io/narayana/lra/LRAConstants.java index c92de639c0..bc37bb1499 100644 --- a/rts/lra/service-base/src/main/java/io/narayana/lra/LRAConstants.java +++ b/rts/lra/service-base/src/main/java/io/narayana/lra/LRAConstants.java @@ -45,6 +45,8 @@ public final class LRAConstants { public static final String RECOVERY_PARAM = "recoveryCount"; public static final String HTTP_METHOD_NAME = "method"; // the name of the HTTP method used to invoke participants + public static final String LRA_API_VERSION_HEADER_NAME = "Narayana-LRA-API-version"; + /** * Number of seconds to wait for requests to participant. * The timeout is hardcoded as the protocol expects retry in case of failure and timeout. diff --git a/rts/lra/service-base/src/main/java/io/narayana/lra/LRAData.java b/rts/lra/service-base/src/main/java/io/narayana/lra/LRAData.java index 7a0f28cc3e..f54d373d47 100644 --- a/rts/lra/service-base/src/main/java/io/narayana/lra/LRAData.java +++ b/rts/lra/service-base/src/main/java/io/narayana/lra/LRAData.java @@ -60,8 +60,9 @@ public URI getLraId() { return this.lraId; } - public void setLraId(URI lraId) { + public LRAData setLraId(URI lraId) { this.lraId = lraId; + return this; } @Transient @@ -73,56 +74,63 @@ public String getClientId() { return this.clientId; } - public void setClientId(String clientId) { + public LRAData setClientId(String clientId) { this.clientId = clientId; + return this; } public LRAStatus getStatus() { return this.status; } - public void setStatus(LRAStatus status) { + public LRAData setStatus(LRAStatus status) { this.status = status; + return this; } public boolean isTopLevel() { return this.isTopLevel; } - public void setTopLevel(boolean topLevel) { + public LRAData setTopLevel(boolean topLevel) { isTopLevel = topLevel; + return this; } public boolean isRecovering() { return this.isRecovering; } - public void setRecovering(boolean recovering) { + public LRAData setRecovering(boolean recovering) { isRecovering = recovering; + return this; } public long getStartTime() { return startTime; } - public void setStartTime(long startTime) { + public LRAData setStartTime(long startTime) { this.startTime = startTime; + return this; } public long getFinishTime() { return finishTime; } - public void setFinishTime(long finishTime) { + public LRAData setFinishTime(long finishTime) { this.finishTime = finishTime; + return this; } public int getHttpStatus() { return this.httpStatus; } - public void setHttpStatus(int httpStatus) { + public LRAData setHttpStatus(int httpStatus) { this.httpStatus = httpStatus; + return this; } public boolean equals(Object o) { diff --git a/rts/lra/test/basic/pom.xml b/rts/lra/test/basic/pom.xml index 0b8c93c9b5..9c65d7971a 100644 --- a/rts/lra/test/basic/pom.xml +++ b/rts/lra/test/basic/pom.xml @@ -51,6 +51,21 @@ ${version.org.codehaus.jettison} test + + + org.jboss.resteasy + resteasy-client + ${version.org.jboss.resteasy} + test + + + + org.jboss.resteasy + resteasy-json-binding-provider + ${version.org.jboss.resteasy} + test + + diff --git a/rts/lra/test/basic/src/test/java/io/narayana/lra/arquillian/api/CoordinatorApi_1_0_IT.java b/rts/lra/test/basic/src/test/java/io/narayana/lra/arquillian/api/CoordinatorApi_1_0_IT.java new file mode 100644 index 0000000000..9323173465 --- /dev/null +++ b/rts/lra/test/basic/src/test/java/io/narayana/lra/arquillian/api/CoordinatorApi_1_0_IT.java @@ -0,0 +1,830 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2021, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ + +package io.narayana.lra.arquillian.api; + +import io.narayana.lra.LRAData; +import io.narayana.lra.arquillian.Deployer; +import io.narayana.lra.client.NarayanaLRAClient; +import io.narayana.lra.coordinator.api.JaxRsActivator; +import org.eclipse.microprofile.lra.annotation.LRAStatus; +import org.hamcrest.MatcherAssert; +import org.jboss.arquillian.container.test.api.Deployment; +import org.jboss.arquillian.container.test.api.RunAsClient; +import org.jboss.arquillian.junit.Arquillian; +import org.jboss.shrinkwrap.api.spec.WebArchive; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Link; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.emptyCollectionOf; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.hamcrest.core.IsCollectionContaining.hasItems; +import static org.hamcrest.core.IsNot.not; + +/** + * REST API tests against the LRA coordinator in version for LRA 1.0. + */ +@RunWith(Arquillian.class) +@RunAsClient +public class CoordinatorApi_1_0_IT { + static final String API_VERSION_1_0 = "1.0"; + + private Client client; + private NarayanaLRAClient lraClient; + private String coordinatorUrl; + private List lrasToAfterFinish; + + static final String NOT_SUPPORTED_FUTURE_LRA_VERSION = "42.1"; + + static final String LRA_API_VERSION_HEADER_NAME_V1_0 = "Narayana-LRA-API-version"; + static final String RECOVERY_HEADER_NAME_V1_0 = "Long-Running-Action-Recovery"; + static final String STATUS_PARAM_NAME_V1_0 = "Status"; + static final String CLIENT_ID_PARAM_NAME_V1_0 = "ClientID"; + static final String TIME_LIMIT_PARAM_NAME_V1_0 = "TimeLimit"; + static final String PARENT_LRA_PARAM_NAME_V1_0 = "ParentLRA"; + + @Rule + public TestName testName = new TestName(); + + @Deployment + public static WebArchive deploy() { + return Deployer.deploy(CoordinatorApi_1_0_IT.class.getSimpleName()); + } + + @Before + public void before() { + client = ClientBuilder.newClient(); + lraClient = new NarayanaLRAClient(); + coordinatorUrl = lraClient.getCoordinatorUrl(); + lrasToAfterFinish = new ArrayList<>(); + } + + @After + public void after() { + for (URI lraToFinish: lrasToAfterFinish) { + lraClient.cancelLRA(lraToFinish); + } + if (client != null) { + client.close(); + } + } + + @Test + public void getAllLRAs() { + Long timeBefore = System.currentTimeMillis(); + + String clientId1 = testName.getMethodName() + "_OK_1"; + String clientId2 = testName.getMethodName() + "_OK_2"; + URI lraId1 = lraClient.startLRA(clientId1); + URI lraId2 = lraClient.startLRA(lraId1, clientId2, 0L, null); + lrasToAfterFinish.add(lraId1); // lraId2 is nested and will be closed in regards to lraId1 + + List data; + try (Response response = client.target(coordinatorUrl) + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0).get()) { + Assert.assertEquals("Expected that the call succeeds, GET/200.", Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Provided API header, expected that one is returned", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + data = response.readEntity(new GenericType>() {}); + } + + Collection returnedLraIds = data.stream().map(LRAData::getLraId).collect(Collectors.toList()); + MatcherAssert.assertThat("Expected the coordinator returns the first started LRA", + returnedLraIds, hasItem(lraId1)); + MatcherAssert.assertThat("Expected the coordinator returns the second started LRA", + returnedLraIds, hasItem(lraId2)); + Collection returnedClientIds = data.stream().map(LRAData::getClientId).collect(Collectors.toList()); + MatcherAssert.assertThat("Expected the coordinator returns the first started LRA client id", + returnedClientIds, hasItem(clientId1)); + MatcherAssert.assertThat("Expected the coordinator returns the second started LRA client id", + returnedClientIds, hasItem(clientId2)); + + Optional lraTopOptional = data.stream().filter(LRAData::isTopLevel).findFirst(); + Assert.assertTrue("Expected to find one LRA from '" + data + "' to be top level", lraTopOptional.isPresent()); + LRAData lraTop = lraTopOptional.get(); + Optional lraNestedOptional = data.stream().filter(lraData -> !lraData.isTopLevel()).findFirst(); + Assert.assertTrue("Expected to find one LRA from '" + data + "' to be nested", lraNestedOptional.isPresent()); + LRAData lraNested = lraNestedOptional.get(); + + MatcherAssert.assertThat("Expected the start time of LRA '" + lraTop + "' is after the test start time", + timeBefore, lessThan(lraTop.getStartTime())); // expecting no time shift + Assert.assertEquals("Expected LRA '" + lraTop + "' being active", + LRAStatus.Active, lraTop.getStatus()); + Assert.assertEquals("Expected top-level LRA '" + lraTop + "' being active, HTTP status 204.", + Status.NO_CONTENT.getStatusCode(), lraNested.getHttpStatus()); + Assert.assertFalse("Expected LRA '" + lraTop + "' not being recovering", lraTop.isRecovering()); + Assert.assertTrue("Expected LRA '" + lraTop + "' to be top level", lraTop.isTopLevel()); + + MatcherAssert.assertThat("Expected the start time of LRA '" + lraNested + "' is after the test start time", + timeBefore, lessThan(lraNested.getStartTime())); // expecting no time shift + Assert.assertEquals("Expected LRA '" + lraNested + "' being active", + LRAStatus.Active, lraNested.getStatus()); + Assert.assertEquals("Expected nested LRA '" + lraNested + "' being active, HTTP status 204.", + Status.NO_CONTENT.getStatusCode(), lraNested.getHttpStatus()); + Assert.assertFalse("Expected LRA '" + lraNested + "' not being recovering", lraNested.isRecovering()); + Assert.assertFalse("Expected LRA '" + lraNested + "' to be nested", lraNested.isTopLevel()); + } + + @Test + public void getAllLRAsStatusFilter() { + String clientId1 = testName.getMethodName() + "_1"; + String clientId2 = testName.getMethodName() + "_2"; + URI lraId1 = lraClient.startLRA(clientId1); + URI lraId2 = lraClient.startLRA(lraId1, clientId2, 0L, null); + lrasToAfterFinish.add(lraId1); + lraClient.closeLRA(lraId2); + + try (Response response = client.target(coordinatorUrl).request().get()) { + Assert.assertEquals("Expected that the call succeeds, GET/200.", Status.OK.getStatusCode(), response.getStatus()); + List data = response.readEntity(new GenericType>() {}); + Collection returnedLraIds = data.stream().map(LRAData::getLraId).collect(Collectors.toList()); + MatcherAssert.assertThat("Expected the coordinator returns the first started and second closed LRA", + returnedLraIds, hasItems(lraId1, lraId2)); + } + try (Response response = client.target(coordinatorUrl) + .queryParam(STATUS_PARAM_NAME_V1_0, "Active").request().get()) { + Assert.assertEquals("Expected that the call succeeds, GET/200.", Status.OK.getStatusCode(), response.getStatus()); + List data = response.readEntity(new GenericType>() {}); + Collection returnedLraIds = data.stream().map(LRAData::getLraId).collect(Collectors.toList()); + MatcherAssert.assertThat("Expected the coordinator returns the first started LRA", + returnedLraIds, hasItem(lraId1)); + MatcherAssert.assertThat("Expected the coordinator filtered out the non-active nested LRA", + returnedLraIds, not(hasItem(lraId2))); + } + } + + @Test + public void getAllLRAsFailedStatus() { + String nonExistingStatusValue = "NotExistingStatusValue"; + try (Response response = client.target(coordinatorUrl) + .queryParam(STATUS_PARAM_NAME_V1_0, nonExistingStatusValue).request().get()) { + Assert.assertEquals("Expected that the call fails on wrong status, GET/500.", + Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure to contain the wrong status value", + response.readEntity(String.class), containsString(nonExistingStatusValue)); + } + } + + @Test + public void getLRAStatus() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl).path(encodedLraId).path("status") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0).get()) { + Assert.assertEquals("Expected that the get status call succeeds, GET/200.", Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expected API header, the latest one version to be returned", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + Assert.assertEquals("Expected the returned LRA status is Active", + "Active", response.readEntity(String.class)); + } + } + + @Test + public void getLRAStatusFailed() { + String nonExistingLRAUrl = "http://localhost:1234/Non-Existing-LRA-id"; + try (Response response = client.target(coordinatorUrl).path(nonExistingLRAUrl).path("status").request().get()) { + Assert.assertEquals("Expected that the call finds not found of " + nonExistingLRAUrl + ", GET/404.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure message to contain the wrong LRA id", + response.readEntity(String.class), containsString(nonExistingLRAUrl)); + } + + String nonExistingLRAWrongUrlFormat = "Non-Existing-LRA-id"; + try (Response response = client.target(coordinatorUrl).path(nonExistingLRAWrongUrlFormat).path("status").request().get()) { + Assert.assertEquals("Expected that the call fails on LRA not found of " + nonExistingLRAWrongUrlFormat + " , GET/404.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure message to contain the wrong LRA id", + response.readEntity(String.class), containsString(lraClient.getCoordinatorUrl() + "/" + nonExistingLRAWrongUrlFormat)); + } + } + + @Test + public void getLRAInfo() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl).path(encodedLraId) + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0).get()) { + Assert.assertEquals("Expected that the get status call succeeds, GET/200.", Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expected API header, the latest one version to be returned", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + LRAData data = response.readEntity(new GenericType() {}); + Assert.assertEquals("Expected returned LRA is started one", lraId, data.getLraId()); + Assert.assertEquals("Expected the returned LRA being Active", LRAStatus.Active, data.getStatus()); + Assert.assertTrue("Expected the returned LRA is top-level", data.isTopLevel()); + Assert.assertEquals("Expected the returned LRA get HTTP status as active, HTTP status 204.", + Status.NO_CONTENT.getStatusCode(), data.getHttpStatus()); + } + } + + @Test + public void getLRAInfoNotExisting() { + String nonExistingLRA = "Non-Existing-LRA-id"; + try (Response response = client.target(coordinatorUrl).path(nonExistingLRA).request().get()) { + Assert.assertEquals("Expected that the call fails on LRA not found, GET/404.", Status.NOT_FOUND.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure message to contain the wrong LRA id", + response.readEntity(String.class), containsString(nonExistingLRA)); + } + } + + @Test // TODO: delete me + public void startCloseLRA() throws UnsupportedEncodingException { + URI lraId1, lraId2; + + try (Response response = client.target(coordinatorUrl) + .path("start") + .queryParam(CLIENT_ID_PARAM_NAME_V1_0, testName.getMethodName() + "_1") + .queryParam(TIME_LIMIT_PARAM_NAME_V1_0, "-42") // negative time limit is permitted by spec + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .post(null)) { + Assert.assertEquals("Creating top-level LRA should be successful, POST/201 is expected.", + Status.CREATED.getStatusCode(), response.getStatus()); + lraId1 = response.readEntity(URI.class); + Assert.assertNotNull("Expected non null LRA id from entity of response '" + response + "'", lraId1); + lrasToAfterFinish.add(lraId1); + + URI lraIdFromLocationHeader = URI.create(response.getHeaderString(HttpHeaders.LOCATION)); + Assert.assertEquals("Expecting the LOCATION header configures the same LRA id as entity content on starting top-level LRA", + lraId1, lraIdFromLocationHeader); + // context header is returned strangely to client, some investigation will be needed + // URI lraIdFromLRAContextHeader = URI.create(response.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER)); + // Assert.assertEquals("Expecting the LRA context header configures the same LRA id as entity content on starting top-level LRA", + // lraId1, lraIdFromLRAContextHeader); + Assert.assertEquals("Expecting to get the same API version as used for the request on top-level LRA start", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + } + + String encodedLraId1 = URLEncoder.encode(lraId1.toString(), StandardCharsets.UTF_8.name()); + try(Response response = client.target(coordinatorUrl) + .path("start") + .queryParam(CLIENT_ID_PARAM_NAME_V1_0, testName.getMethodName() + "_2") + .queryParam(PARENT_LRA_PARAM_NAME_V1_0, encodedLraId1) + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .post(null)) { + Assert.assertEquals("Creating nested LRA should be successful, POST/201 is expected.", + Status.CREATED.getStatusCode(), response.getStatus()); + lraId2 = response.readEntity(URI.class); + Assert.assertNotNull("Expected non null nested LRA id from entity of response '" + response + "'", lraId2); + + // the nested LRA id is in format ?ParentLRA= + URI lraIdFromLocationHeader = URI.create(response.getHeaderString(HttpHeaders.LOCATION)); + Assert.assertEquals("Expecting the LOCATION header configures the same LRA id as entity content on starting nested LRA", + lraId2, lraIdFromLocationHeader); + // context header is returned strangely to client, some investigation will be needed + // String lraContextHeader = response.getHeaderString(LRA.LRA_HTTP_CONTEXT_HEADER); + // the context header is in format ,?ParentLRA= + // MatcherAssert.assertThat("Expected the nested LRA context header gives the parent LRA id at first", + // lraContextHeader, startsWith(lraId1.toASCIIString())); + // MatcherAssert.assertThat("Expected the nested LRA context header provides LRA id of started nested LRA", + // lraContextHeader, containsString("," + lraId2.toASCIIString())); + Assert.assertEquals("Expecting to get the same API version as used for the request on nested LRA start", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + } + + Collection returnedLraIds = lraClient.getAllLRAs().stream().map(LRAData::getLraId).collect(Collectors.toList()); + MatcherAssert.assertThat("Expected the coordinator knows about the top-level LRA", returnedLraIds, hasItem(lraId1)); + MatcherAssert.assertThat("Expected the coordinator knows about the nested LRA", returnedLraIds, hasItem(lraId2)); + + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId1 + "/close") + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .put(null)) { + lrasToAfterFinish.clear(); // we've closed the LRA manually here, skipping the @After + Assert.assertEquals("Closing top-level LRA should be successful, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Closing top-level LRA should return the right status.", + LRAStatus.Closed.name(), response.readEntity(String.class)); + Assert.assertEquals("Expecting to get the same API version as used for the request to close top-level LRA", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + } + + Collection activeLRAsAfterClosing = lraClient.getAllLRAs().stream() + .filter(data -> data.getLraId().equals(lraId1) || data.getLraId().equals(lraId2)).collect(Collectors.toList()); + MatcherAssert.assertThat("Expecting the started LRAs are not more active after closing the top-level one", + activeLRAsAfterClosing, emptyCollectionOf(LRAData.class)); + } + + @Test // TODO: delete me + public void startCancelLRA() throws UnsupportedEncodingException { + URI lraId; + try (Response response = client.target(coordinatorUrl) + .path("start") + .queryParam(CLIENT_ID_PARAM_NAME_V1_0, testName.getMethodName()) + .request() + .post(null)) { + Assert.assertEquals("Creating top-level LRA should be successful, POST/201 is expected.", + Status.CREATED.getStatusCode(), response.getStatus()); + lraId = response.readEntity(URI.class); + Assert.assertNotNull("Expected non null LRA id from entity of response '" + response + "'", lraId); + lrasToAfterFinish.add(lraId); + Assert.assertEquals("Expecting to get the most up-to-date API version when passed no one on POST query", + JaxRsActivator.LRA_API_VERSION_STRING, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + } + + Collection returnedLraIds = lraClient.getAllLRAs().stream().map(LRAData::getLraId).collect(Collectors.toList()); + MatcherAssert.assertThat("Expected the coordinator knows about the LRA", returnedLraIds, hasItem(lraId)); + try (Response response = client.target(coordinatorUrl) + .path(URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()) + "/cancel") + .request() + .put(null)) { + lrasToAfterFinish.clear(); // we've closed the LRA manually just now, skipping the @After + Assert.assertEquals("Closing LRA should be successful, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Canceling top-level LRA should return the right status.", + LRAStatus.Cancelled.name(), response.readEntity(String.class)); + Assert.assertEquals("Expecting to get the most up-to-date API version when passed no one on POST query", + JaxRsActivator.LRA_API_VERSION_STRING, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + } + + Collection activeLRAsAfterClosing = lraClient.getAllLRAs().stream() + .filter(data -> data.getLraId().equals(lraId)).collect(Collectors.toList()); + MatcherAssert.assertThat("Expecting the started LRA is no more active after closing it", + activeLRAsAfterClosing, emptyCollectionOf(LRAData.class)); + } + + @Test + public void startLRANotExistingParentLRA() { + String notExistingParentLRA = "not-existing-parent-lra-id"; + try (Response response = client.target(coordinatorUrl) + .path("start") + .queryParam(CLIENT_ID_PARAM_NAME_V1_0, testName.getMethodName()) + .queryParam(PARENT_LRA_PARAM_NAME_V1_0, notExistingParentLRA) + .request() + .post(null)) { + Assert.assertEquals("Expected failure on non-existing parent LRA, POST/404 is expected.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + String errorMsg = response.readEntity(String.class); + MatcherAssert.assertThat("Expected error message to contain the not found parent LRA id", + errorMsg, containsString(notExistingParentLRA)); + } + } + + @Test + public void closeNotExistingLRA() { + String notExistingLRAid = "not-existing-lra-id"; + try (Response response = client.target(coordinatorUrl) + .path(notExistingLRAid) + .path("close") + .request() + .put(null)) { + Assert.assertEquals("Expected failure on non-existing LRA id, PUT/404 is expected.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + String errorMsg = response.readEntity(String.class); + MatcherAssert.assertThat("Expected error message to contain the not found LRA id", + errorMsg, containsString(notExistingLRAid)); + } + } + + @Test + public void cancelNotExistingLRA() { + String notExistingLRAid = "not-existing-lra-id"; + try (Response response = client.target(coordinatorUrl) + .path(notExistingLRAid) + .path("cancel") + .request() + .put(null)) { + Assert.assertEquals("Expected failure on non-existing LRA id, PUT/404 is expected.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + String errorMsg = response.readEntity(String.class); + MatcherAssert.assertThat("Expected error message to contain the not found LRA id", + errorMsg, containsString(notExistingLRAid)); + } + } + + @Test + public void renewTimeLimit() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + Optional data = lraClient.getAllLRAs().stream().filter(l -> l.getLraId().equals(lraId)).findFirst(); + Assert.assertTrue("Expected the started LRA will retrieved by LRA client get", data.isPresent()); + Assert.assertEquals("Expected not defined finish time", 0L, data.get().getFinishTime()); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .path("renew") + .queryParam(TIME_LIMIT_PARAM_NAME_V1_0, Integer.MAX_VALUE) + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .put(null)) { + Assert.assertEquals("Expected time limit request to succeed, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expecting to get the most up-to-date API version when passed no one on POST query", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + MatcherAssert.assertThat("Expected the found LRA id is returned", + response.readEntity(String.class), containsString(lraId.toString())); + } + + data = lraClient.getAllLRAs().stream().filter(l -> l.getLraId().equals(lraId)).findFirst(); + Assert.assertTrue("Expected the started LRA will retrieved by LRA client get", data.isPresent()); + MatcherAssert.assertThat("Expected finish time to not be 0 as time limit was defined", + data.get().getFinishTime(), greaterThan(0L)); + } + + @Test + public void renewTimeLimitNotExistingLRA() { + String notExistingLRAid = "not-existing-lra-id"; + try (Response response = client.target(coordinatorUrl) + .path(notExistingLRAid) + .path("renew") + .queryParam(TIME_LIMIT_PARAM_NAME_V1_0, Integer.MAX_VALUE) + .request() + .put(null)) { + Assert.assertEquals("Expected time limit request to succeed, PUT/404 is expected.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + String errorMsg = response.readEntity(String.class); + MatcherAssert.assertThat("Expected error message to contain the not found LRA id", + errorMsg, containsString(notExistingLRAid)); + } + } + + @Test + public void joinLRAWithBody() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .put(Entity.text("http://compensator.url:8080"))) { + Assert.assertEquals("Expected joining LRA succeeded, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expecting API version header", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + String recoveryHeaderUrlMessage = response.getHeaderString(RECOVERY_HEADER_NAME_V1_0); + String recoveryUrlBody = response.readEntity(String.class); + URI recoveryUrlLocation = response.getLocation(); + Assert.assertEquals("Expecting returned body and recovery header has the same content", + recoveryUrlBody, recoveryHeaderUrlMessage); + Assert.assertEquals("Expecting returned body and location has the same content", + recoveryUrlBody, recoveryUrlLocation.toString()); + MatcherAssert.assertThat("Expected returned message contains the subpath of LRA recovery URL", + recoveryUrlBody, containsString("lra-coordinator/recovery")); + MatcherAssert.assertThat("Expected returned message contains the LRA id", + recoveryUrlBody, containsString(encodedLraId)); + } + } + + @Test + public void joinLRAWithLinkSimple() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .header("Link", "http://compensator.url:8080") + .put(null)) { + Assert.assertEquals("Expected joining LRA succeeded, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expecting API version header", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + String recoveryHeaderUrlMessage = response.getHeaderString(RECOVERY_HEADER_NAME_V1_0); + String recoveryUrlBody = response.readEntity(String.class); + URI recoveryUrlLocation = response.getLocation(); + Assert.assertEquals("Expecting returned body and recovery header has the same content", + recoveryUrlBody, recoveryHeaderUrlMessage); + Assert.assertEquals("Expecting returned body and location has the same content", + recoveryUrlBody, recoveryUrlLocation.toString()); + MatcherAssert.assertThat("Expected returned message contains the subpath of LRA recovery URL", + recoveryUrlBody, containsString("lra-coordinator/recovery")); + MatcherAssert.assertThat("Expected returned message contains the LRA id", + recoveryUrlBody, containsString(encodedLraId)); + } + } + + + @Test + public void joinLRAWithLinkCompensate() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + Link link = Link.fromUri("http://compensate.url:8080").rel("compensate").build(); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .header("Link", link.toString()) + .put(null)) { + Assert.assertEquals("Expected joining LRA succeeded, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expecting the most up-to-date API version header", + JaxRsActivator.LRA_API_VERSION_STRING, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + String recoveryHeaderUrlMessage = response.getHeaderString(RECOVERY_HEADER_NAME_V1_0); + String recoveryUrlBody = response.readEntity(String.class); + Assert.assertEquals("Expecting returned body and recovery header has the same content", + recoveryUrlBody, recoveryHeaderUrlMessage); + MatcherAssert.assertThat("Expected returned message contains the subpath of LRA recovery URL", + recoveryUrlBody, containsString("lra-coordinator/recovery")); + } + } + + @Test + public void joinLRAWithLinkAfter() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + Link afterLink = Link.fromUri("http://after.url:8080").rel("after").build(); + Link unknownLink = Link.fromUri("http://unknow.url:8080").rel("uknown").build(); + String linkList = afterLink.toString() + "," + unknownLink.toString(); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .header("Link", linkList) + .put(null)) { + Assert.assertEquals("Expected joining LRA succeeded, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + String recoveryHeaderUrlMessage = response.getHeaderString(RECOVERY_HEADER_NAME_V1_0); + String recoveryUrlBody = response.readEntity(String.class); + Assert.assertEquals("Expecting returned body and recovery header has the same content", + recoveryUrlBody, recoveryHeaderUrlMessage); + MatcherAssert.assertThat("Expected returned message contains the subpath of LRA recovery URL", + URLDecoder.decode(recoveryUrlBody, StandardCharsets.UTF_8.name()), containsString("lra-coordinator/recovery")); + } + } + + @Test + public void joinLRAIncorrectLinkFormat() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .header("Link", ";rel=myrel;") + .put(null)) { + Assert.assertEquals("Expected the join failing, PUT/500 is expected.", + Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + } + + @Test + public void joinLRAUnknownLRA() { + String notExistingLRAid = "not-existing-lra-id"; + try (Response response = client.target(coordinatorUrl) + .path(notExistingLRAid) + .request() + .put(Entity.text("http://localhost:8080"))) { + Assert.assertEquals("Expected the join failing on unknown LRA id, PUT/404 is expected.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected error message to contain the LRA id where enlist failed", + response.readEntity(String.class), containsString(notExistingLRAid)); + } + } + + @Test + public void joinLRAWrongCompensatorData() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .put(Entity.text("this-is-not-an-url::::"))) { + Assert.assertEquals("Expected the join failing on wrong compensator data format, PUT/412 is expected.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected error message to contain the LRA id where enlist failed", + response.readEntity(String.class), containsString(lraId.toString())); + } + } + + @Test + public void joinLRAWithLinkNotEnoughData() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + + String encodedLraId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + Link link = Link.fromUri("http://complete.url:8080").rel("complete").build(); + try (Response response = client.target(coordinatorUrl) + .path(encodedLraId) + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .header("Link", link.toString()) + .put(null)) { + Assert.assertEquals("Expected the joining fails as no compensate in link, PUT/400 is expected.", + Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + String errorMsg = response.readEntity(String.class); + MatcherAssert.assertThat("Expected error message to contain the LRA id where enlist failed", + errorMsg, containsString(lraId.toString())); + } + } + + @Test + public void leaveLRA() throws UnsupportedEncodingException { + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + URI recoveryUri = lraClient.joinLRA(lraId, 0L, URI.create("http://localhost:8080"), ""); + + String encodedLRAId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl) + .path(encodedLRAId) + .path("remove") + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .put(Entity.text(recoveryUri.toString()))) { + Assert.assertEquals("Expected leaving of LRA to succeed, PUT/200 is expected.", + Status.OK.getStatusCode(), response.getStatus()); + Assert.assertEquals("Expecting API version header", + API_VERSION_1_0, response.getHeaderString(LRA_API_VERSION_HEADER_NAME_V1_0)); + Assert.assertFalse("Expecting 'remove' API call returns no entity body", response.hasEntity()); + } + + try (Response response = client.target(coordinatorUrl) + .path(encodedLRAId) + .path("remove") + .request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, API_VERSION_1_0) + .put(Entity.text(recoveryUri.toString()))) { + Assert.assertEquals("Expected leaving of LRA to fail as it was removed just before, PUT/400 is expected.", + Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure message to contain the non existing participant id", + response.readEntity(String.class), containsString(recoveryUri.toASCIIString())); + } + } + + @Test + public void leaveLRANonExistingFailure() throws UnsupportedEncodingException { + String nonExistingLRAId = "http://localhost:1234/Non-Existing-LRA-id"; + String encodedNonExistingLRAId = URLEncoder.encode(nonExistingLRAId, StandardCharsets.UTF_8.name()); + try (Response response = client.target(coordinatorUrl).path(encodedNonExistingLRAId).path("remove").request().put(Entity.text("nothing"))) { + Assert.assertEquals("Expected that the call finds not found of " + encodedNonExistingLRAId + ", PUT/404.", + Status.NOT_FOUND.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure message to contain the wrong LRA id", + response.readEntity(String.class), containsString(nonExistingLRAId)); + } + + URI lraId = lraClient.startLRA(testName.getMethodName()); + lrasToAfterFinish.add(lraId); + String encodedLRAId = URLEncoder.encode(lraId.toString(), StandardCharsets.UTF_8.name()); + String nonExistingParticipantUrl = "http://localhost:1234/Non-Existing-participant-LRA"; + try (Response response = client.target(coordinatorUrl).path(encodedLRAId).path("remove").request() + .put(Entity.text(nonExistingParticipantUrl))) { + Assert.assertEquals("Expected that the call fails on LRA participant " + nonExistingParticipantUrl + " not found , PUT/400.", + Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the failure message to contain the wrong participant id", + response.readEntity(String.class), containsString(nonExistingParticipantUrl)); + } + } + + + // ---------------------------------------------------------------------------------- + // ------------------------ VERSION HEADER VERIFICATION ----------------------------- + // ---------------------------------------------------------------------------------- + + @Test + public void getAllLRAsWrongVersion() { + try (Response response = client.target(coordinatorUrl) + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).get()) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void getLRAStatusWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("status") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).get()) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void getLRAInfoWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("lra-id") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).get()) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void startLRAWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("start") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).post(null)) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void renewTimeLimitWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("lra-id").path("renew") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).put(null)) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void closeLRAWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("lra-id").path("close") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).put(null)) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void cancelLRAWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("lra-id").path("cancel") + .request().header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).put(null)) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + + @Test + public void joinViaBodyWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("lra-id").request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).put(Entity.text("compensator-url"))) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + @Test + public void leaveLRAWrongVersion() { + try (Response response = client.target(coordinatorUrl).path("lra-id").path("remove").request() + .header(LRA_API_VERSION_HEADER_NAME_V1_0, NOT_SUPPORTED_FUTURE_LRA_VERSION).put(Entity.text("participant-url"))) { + Assert.assertEquals("Expected version on method call is not supported, GET/412.", + Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); + MatcherAssert.assertThat("Expected the response to contain the wrong version", + response.readEntity(String.class), containsString(NOT_SUPPORTED_FUTURE_LRA_VERSION)); + } + } + +} diff --git a/rts/lra/test/basic/src/test/resources/META-INF/services/javax.ws.rs.client.ClientBuilder b/rts/lra/test/basic/src/test/resources/META-INF/services/javax.ws.rs.client.ClientBuilder new file mode 100644 index 0000000000..114f3fc97a --- /dev/null +++ b/rts/lra/test/basic/src/test/resources/META-INF/services/javax.ws.rs.client.ClientBuilder @@ -0,0 +1 @@ +org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder \ No newline at end of file