From 5c5d3d88ab205f7f2e3a73adbe9130935cde50e1 Mon Sep 17 00:00:00 2001 From: Ondra Chaloupka Date: Tue, 8 Dec 2020 11:47:58 +0100 Subject: [PATCH] [JBTM-3294] adding version HTTP headers version handling to coordinator API --- pom.xml | 1 + rts/lra/API.adoc | 49 + .../lra/client/NarayanaLRAClient.java | 54 +- .../lra/coordinator/api/Coordinator.java | 355 ++++--- .../api/CoordinatorContainerFilter.java | 52 + .../lra/coordinator/api/JaxRsActivator.java | 43 +- .../domain/model/LRAParticipantRecord.java | 6 +- .../domain/service/LRAService.java | 28 +- .../src/test/resources/arquillian.xml | 1 + .../narayana/lra/filter/ServerLRAFilter.java | 2 +- rts/lra/pom.xml | 18 +- .../lra/proxy/logging/lraI18NLogger.java | 8 +- .../main/java/io/narayana/lra/APIVersion.java | 149 +++ .../java/io/narayana/lra/LRAConstants.java | 14 + .../narayana/lra/logging/lraI18NLogger.java | 123 +-- .../java/io/narayana/lra/APIVersionTest.java | 92 ++ .../io/narayana/lra/LRAConstantsTest.java | 1 - ...ppServerCoordinatorDeploymentObserver.java | 12 +- rts/lra/test/basic/pom.xml | 15 + .../arquillian/api/CoordinatorApi_1_0_IT.java | 937 ++++++++++++++++++ .../services/javax.ws.rs.client.ClientBuilder | 1 + 21 files changed, 1672 insertions(+), 289 deletions(-) create mode 100644 rts/lra/API.adoc create mode 100644 rts/lra/service-base/src/main/java/io/narayana/lra/APIVersion.java create mode 100644 rts/lra/service-base/src/test/java/io/narayana/lra/APIVersionTest.java create mode 100644 rts/lra/test/basic/src/test/java/io/narayana/lra/arquillian/api/CoordinatorApi_1_0_IT.java create mode 100644 rts/lra/test/basic/src/test/resources/META-INF/services/javax.ws.rs.client.ClientBuilder diff --git a/pom.xml b/pom.xml index 745a1ff55f..529be465e1 100644 --- a/pom.xml +++ b/pom.xml @@ -497,6 +497,7 @@ 1.1.2 1.0.10 1.0.0.Beta1 + 2.2 1.5.0 diff --git a/rts/lra/API.adoc b/rts/lra/API.adoc new file mode 100644 index 0000000000..4ed4a20f5a --- /dev/null +++ b/rts/lra/API.adoc @@ -0,0 +1,49 @@ += Versioning of Narayna LRA REST API + +The goal of this document is summarize the approach to REST API versioning +in Narayana LRA coordinator service. +This document is written for developer of Narayana LRA services. + +The version format is `major.minor`[`-preRelease`]. +The `major` and `minor` parts are required and `-preRelease` is used +during development and is optional. + +NOTE: Any final `minor.major` release is considered higher than + the same version with `-preRelease` part. + +Client may demand behaviour based on particular API version +by providing HTTP header link:./service-base/src/main/java/io/narayana/lra/LRAConstants.java[`Narayana-LRA-API-version`] on the call. + +== API definition as Open API document + +The Narayana LRA API is documented with Open API annotations at the java +classes. The Open API definition needs to be published at the http://narayana.io +page. + +== Changes in LRA REST API + +The Narayana LRA REST API is expected to support for at least two previous +`major` versions (ie. support is expected in parallel at least of versions 1.x, +2.x and 3.x until the 4.0 is released). + +Changes which do not make a trouble for backward compatibility +from client perspective (i.e., addition of features or enhancing the API +with return types or similar) are considered to bump a `minor` version. +Incompatible changes needs to bump the `major` version. + +== Unsupported version errors + +When client demands unsupported version from the REST API endpoint +the code returns HTTP status +link:http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.18[`417` EXPECTATION_FAILED`]. + +On returning the `417` error the API is expected to return the header +`Narayana-LRA-API-version` with the highest supported +API version. + +For an unsupported version is considered any request to the REST API endpoint +which demands (via HTTP header `Narayana-LRA-API-version`) version +higher than the current API version (i.e., the highest version that the API +is created for). +The unsupported version could be one that is already deprecated +and not supported anymore. \ No newline at end of file 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 7d8765c793..9baae87059 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; @@ -79,6 +78,7 @@ import static io.narayana.lra.LRAConstants.COMPLETE; import static io.narayana.lra.LRAConstants.FORGET; import static io.narayana.lra.LRAConstants.LEAVE; +import static io.narayana.lra.LRAConstants.LRA_API_VERSION_HEADER_NAME; import static io.narayana.lra.LRAConstants.PARENT_LRA_PARAM_NAME; import static io.narayana.lra.LRAConstants.STATUS; import static io.narayana.lra.LRAConstants.TIMELIMIT_PARAM_NAME; @@ -109,6 +109,11 @@ public class NarayanaLRAClient implements Closeable { */ public static final String LRA_COORDINATOR_URL_KEY = "lra.coordinator.url"; + /** + * Version of Narayana LRA API that client is capable to work with. + */ + private static final String CLIENT_API_VERSION = LRAConstants.NARAYANA_LRA_API_VERSION_1_0; + // LRA Coordinator API private static final String START_PATH = "/start"; private static final String LEAVE_PATH = "/%s/remove"; @@ -139,7 +144,7 @@ public class NarayanaLRAClient implements Closeable { * If not defined as default value is taken {@code http://localhost:8080/lra-coordinator}. * The LRA recovery coordinator will be searched at the sub-path {@value LRAConstants#RECOVERY_COORDINATOR_PATH_NAME}. * - * @throws IllegalStateException thrown when the URL taken from the system property value is not an URI format + * @throws IllegalStateException thrown when the URL taken from the system property value is not a URL format */ public NarayanaLRAClient() { this(System.getProperty(NarayanaLRAClient.LRA_COORDINATOR_URL_KEY, @@ -176,14 +181,14 @@ public NarayanaLRAClient(URI coordinatorUrl) { * The LRA recovery coordinator will be searched at the sub-path {@value LRAConstants#RECOVERY_COORDINATOR_PATH_NAME}. * * @param coordinatorUrl url of the LRA coordinator - * @throws IllegalStateException thrown when the provided URL String is not an URI format + * @throws IllegalStateException thrown when the provided URL String is not a URL format */ public NarayanaLRAClient(String coordinatorUrl) { try { this.coordinatorUrl = new URI(coordinatorUrl); } catch (URISyntaxException use) { throw new IllegalStateException("Cannot convert the provided coordinator url String " - + coordinatorUrl + " to URI format", use); + + coordinatorUrl + " to URL format", use); } } @@ -208,10 +213,11 @@ public List getAllLRAs() { try { client = getClient(); Response response = client.target(coordinatorUrl) - .request() - .async() - .get() - .get(QUERY_TIMEOUT, TimeUnit.SECONDS); + .request() + .header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION) + .async() + .get() + .get(QUERY_TIMEOUT, TimeUnit.SECONDS); if (response.getStatus() != OK.getStatusCode()) { LRALogger.logger.debugf("Error getting all LRAs from the coordinator, response status: %d", response.getStatus()); @@ -315,6 +321,7 @@ public URI startLRA(URI parentLRA, String clientID, Long timeout, ChronoUnit uni .queryParam(TIMELIMIT_PARAM_NAME, Duration.of(timeout, unit).toMillis()) .queryParam(PARENT_LRA_PARAM_NAME, encodedParentLRA) .request() + .header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION) .async() .post(null) .get(START_TIMEOUT, TimeUnit.SECONDS); @@ -344,7 +351,7 @@ public URI startLRA(URI parentLRA, String clientID, Long timeout, ChronoUnit uni } throwGenericLRAException(null, INTERNAL_SERVER_ERROR.getStatusCode(), "Cannot connect to the LRA coordinator: " + coordinatorUrl + " as provided parent LRA URL '" + parentLRA + - "' is not in URI format (" + uee.getClass().getName() + ":" + uee.getCause().getMessage() + ")", uee); + "' is not in URL format (" + uee.getClass().getName() + ":" + uee.getCause().getMessage() + ")", uee); return null; } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new WebApplicationException("start LRA client request timed out, try again later", e, @@ -417,9 +424,10 @@ public void leaveLRA(URI lraId, String body) throws WebApplicationException { response = client.target(coordinatorUrl) .path(String.format(LEAVE_PATH, LRAConstants.getLRAUid(lraId))) .request() + .header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION) .async() .put(body == null ? Entity.text("") : Entity.text(body)) - .get(LEAVE_TIMEOUT, TimeUnit.SECONDS); + .get(LEAVE_TIMEOUT, TimeUnit.SECONDS); if (OK.getStatusCode() != response.getStatus()) { LRALogger.i18NLogger.error_lraLeaveUnexpectedStatus(response.getStatus(), @@ -593,9 +601,10 @@ public LRAStatus getStatus(URI uri) throws WebApplicationException { response = client.target(coordinatorUrl) .path(String.format(STATUS_PATH, LRAConstants.getLRAUid(uri))) .request() - .async() + .header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION) + .async() .get() - .get(QUERY_TIMEOUT, TimeUnit.SECONDS); + .get(QUERY_TIMEOUT, TimeUnit.SECONDS); if (response.getStatus() == NOT_FOUND.getStatusCode()) { String responseEntity = response.hasEntity() ? response.readEntity(String.class) : ""; @@ -722,10 +731,11 @@ private URI enlistCompensator(URI uri, Long timelimit, String linkHeader, String .path(LRAConstants.getLRAUid(uri)) .queryParam(TIMELIMIT_PARAM_NAME, timelimit) .request() + .header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION) .header("Link", linkHeader) - .async() + .async() .put(Entity.text(compensatorData == null ? linkHeader : compensatorData)) - .get(JOIN_TIMEOUT, TimeUnit.SECONDS); + .get(JOIN_TIMEOUT, TimeUnit.SECONDS); String responseEntity = response.hasEntity() ? response.readEntity(String.class) : ""; if (response.getStatus() == Response.Status.PRECONDITION_FAILED.getStatusCode()) { @@ -746,9 +756,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); @@ -778,11 +787,12 @@ private void endLRA(URI lra, boolean confirm) throws WebApplicationException { String lraUid = LRAConstants.getLRAUid(lra); try { response = client.target(coordinatorUrl) - .path(confirm ? String.format(CLOSE_PATH, lraUid) : String.format(CANCEL_PATH, lraUid)) - .request() - .async() - .put(Entity.text("")) - .get(END_TIMEOUT, TimeUnit.SECONDS); + .path(confirm ? String.format(CLOSE_PATH, lraUid) : String.format(CANCEL_PATH, lraUid)) + .request() + .header(LRA_API_VERSION_HEADER_NAME, CLIENT_API_VERSION) + .async() + .put(Entity.text("")) + .get(END_TIMEOUT, TimeUnit.SECONDS); } catch (InterruptedException | ExecutionException | TimeoutException e) { throw new WebApplicationException("end LRA client request timed out, try again later", Response.Status.SERVICE_UNAVAILABLE.getStatusCode()); 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 04040c6c18..1310f458c7 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 @@ -56,9 +56,9 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -90,12 +90,15 @@ 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 io.narayana.lra.LRAConstants.NARAYANA_LRA_API_VERSION_1_0; 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 extends Application { @Context private UriInfo context; @@ -113,107 +116,131 @@ public Coordinator() { lraService = LRARecoveryModule.getService(); } - // 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(type = SchemaType.ARRAY, implementation = LRAData.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME)}), + @APIResponse(responseCode = "400", description = "Provided Status is not recognized as a valid LRA status value", + content = @Content(schema = @Schema(implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME)}), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), + }) + 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(NARAYANA_LRA_API_VERSION_1_0) String version) { LRAStatus requestedLRAStatus = null; - if(!state.isEmpty()) { - requestedLRAStatus = LRAStatus.valueOf(state); + if (!state.isEmpty()) { + try { + requestedLRAStatus = LRAStatus.valueOf(state); + } catch (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); - - 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()); - } + List lras = lraService.getAll(requestedLRAStatus); - 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(implementation = String.class))), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(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 an 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(NARAYANA_LRA_API_VERSION_1_0) String version) + throws NotFoundException { + 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(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(implementation = String.class))), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(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(NARAYANA_LRA_API_VERSION_1_0) String 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 an 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 an 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(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(description = "Message containing problematic LRA id", implementation = String.class))), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), + @APIResponse(responseCode = "500", description = "A new LRA could not be started. Coordinator internal error.", + content = @Content(schema = @Schema(implementation = String.class))) }) public Response startLRA( @Parameter(name = CLIENT_ID_PARAM_NAME, @@ -228,7 +255,9 @@ public Response startLRA( @QueryParam(TIMELIMIT_PARAM_NAME) @DefaultValue("0") Long timelimit, @Parameter(name = PARENT_LRA_PARAM_NAME, description = "The enclosing LRA if this new LRA is nested") - @QueryParam(PARENT_LRA_PARAM_NAME) @DefaultValue("") String parentLRA) throws WebApplicationException { + @QueryParam(PARENT_LRA_PARAM_NAME) @DefaultValue("") String parentLRA, + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(NARAYANA_LRA_API_VERSION_1_0) String version) throws WebApplicationException { URI parentId = (parentLRA == null || parentLRA.trim().isEmpty()) ? null : toURI(parentLRA); String coordinatorUrl = String.format("%s%s", context.getBaseUri(), COORDINATOR_PATH_NAME); @@ -247,18 +276,23 @@ public Response startLRA( client = ClientBuilder.newClient(); response = client.target(parentId) .request() + .header(LRA_API_VERSION_HEADER_NAME, NARAYANA_LRA_API_VERSION_1_0) .async() .put(Entity.text(compensatorUrl)) .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", - parentId, response.getStatus()); + String errMessage = String.format("The coordinator at %s returned an unexpected response: %d" + + "when the LRA '%s' tried to join the parent LRA '%s'", parentId, 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'", + parentId, 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(); @@ -272,27 +306,37 @@ 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(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(implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), }) 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(NARAYANA_LRA_API_VERSION_1_0) String version) { + return Response.status(lraService.renewTimeLimit(toURI(lraId), timeLimit)) + .header(LRA_API_VERSION_HEADER_NAME, version) + .entity(lraId) + .build(); } @GET @@ -350,11 +394,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") @@ -367,15 +414,23 @@ 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(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(implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), }) 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(NARAYANA_LRA_API_VERSION_1_0) String version) { + return Response.ok(endLRA(toURI(txId), false, false).name()) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build(); } @PUT @@ -387,15 +442,24 @@ 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(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(implementation = String.class)), + headers = { @Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) }), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), }) 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(NARAYANA_LRA_API_VERSION_1_0) String version) + throws NotFoundException { + return Response.ok(endLRA(toURI(lraId), true, false).name()) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build(); } @@ -410,48 +474,55 @@ 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 no longer active (ie the complete or compensate message has 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(description = "A 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(implementation = String.class))), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(implementation = String.class))), + @APIResponse(responseCode = "412", + description = "The LRA is not longer active (ie the complete or compensate message has been sent), or wrong format of compensator data", + content = @Content(schema = @Schema(implementation = String.class)), + headers = {@Header(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME)}), + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), + @APIResponse(responseCode = "500", description = "Format of the compensator data (e.g. Link format) could not be processed", + content = @Content(schema = @Schema(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 " + "compensate this work. The application or coordinator may use this information to control the " - + "lifecycle of a LRA.") + + "lifecycle of an LRA.") @QueryParam(TIMELIMIT_PARAM_NAME) @DefaultValue("0") long timeLimit, @Parameter(name = "Link", description = "The resource paths that the coordinator will use to complete or compensate and to request" + " 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(NARAYANA_LRA_API_VERSION_1_0) 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 { // 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 @@ -464,16 +535,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(",") @@ -481,7 +555,7 @@ public Response joinLRAViaBody( compensatorData = linkHeaderValue.toString(); } - return joinLRA(toURI(lraId), timeLimit, null, compensatorData, null); + return joinLRA(toURI(lraId), timeLimit, null, compensatorData, null, version); } @@ -511,7 +585,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); @@ -525,40 +599,49 @@ 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 an 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(implementation = String.class))), + @APIResponse(responseCode = "404", description = "The coordinator has no knowledge of this LRA", + content = @Content(schema = @Schema(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") + @APIResponse(responseCode = "417", description = "The requested version provided in HTTP Header is not supported by this end point", + content = @Content(schema = @Schema(implementation = String.class))), }) 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('/')); + @Parameter(ref = LRAConstants.LRA_API_VERSION_HEADER_NAME) + @HeaderParam(LRAConstants.LRA_API_VERSION_HEADER_NAME) @DefaultValue(NARAYANA_LRA_API_VERSION_1_0) String version, + String participantCompensatorUrl) throws NotFoundException { + int status = lraService.leave(toURI(lraId), participantCompensatorUrl); - int status = 0; - - status = lraService.leave(new URI(reqUri), compensatorUrl); - - return Response.status(status).build(); + return Response.status(status) + .header(LRA_API_VERSION_HEADER_NAME, version) + .build(); } private URI toURI(String lraId) { diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/CoordinatorContainerFilter.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/CoordinatorContainerFilter.java index 7a10f3e2cc..3f83b8abde 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/CoordinatorContainerFilter.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/api/CoordinatorContainerFilter.java @@ -21,7 +21,9 @@ */ package io.narayana.lra.coordinator.api; +import io.narayana.lra.APIVersion; import io.narayana.lra.Current; +import io.narayana.lra.LRAConstants; import javax.ws.rs.WebApplicationException; import javax.ws.rs.container.ContainerRequestContext; @@ -35,16 +37,22 @@ import java.net.URI; import java.net.URISyntaxException; +import static io.narayana.lra.LRAConstants.LRA_API_VERSION_HEADER_NAME; +import static io.narayana.lra.LRAConstants.NARAYANA_LRA_API_VERSION_1_0; +import static javax.ws.rs.core.Response.Status.EXPECTATION_FAILED; import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED; import static org.eclipse.microprofile.lra.annotation.ws.rs.LRA.LRA_HTTP_CONTEXT_HEADER; @Provider public class CoordinatorContainerFilter implements ContainerRequestFilter, ContainerResponseFilter { + @Override public void filter(ContainerRequestContext requestContext) throws IOException { MultivaluedMap headers = requestContext.getHeaders(); URI lraId = null; + verifyHighestSupportedVersion(requestContext); + if (headers.containsKey(LRA_HTTP_CONTEXT_HEADER)) { try { lraId = new URI(Current.getLast(headers.get(LRA_HTTP_CONTEXT_HEADER))); @@ -71,6 +79,50 @@ public void filter(ContainerRequestContext requestContext) throws IOException { @Override public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) throws IOException { + if(!responseContext.getHeaders().containsKey(LRAConstants.LRA_API_VERSION_HEADER_NAME)) { // app code did not provide version to header + // as version using the api version which came at request or the current version of the api in the library + String responseVersion = requestContext.getHeaders().containsKey(LRAConstants.LRA_API_VERSION_HEADER_NAME) ? + requestContext.getHeaderString(LRAConstants.LRA_API_VERSION_HEADER_NAME) : NARAYANA_LRA_API_VERSION_1_0; + responseContext.getHeaders().putSingle(LRAConstants.LRA_API_VERSION_HEADER_NAME, responseVersion); + } + Current.updateLRAContext(responseContext); } + + /** + * Verification if the version in the header is in right format + * and if demanded version is not higher than the supported one + * (ie. if version is lower than {@link LRAConstants#NARAYANA_LRA_API_VERSION_CURRENT}). + */ + private void verifyHighestSupportedVersion(ContainerRequestContext requestContext) { + if (!requestContext.getHeaders().containsKey(LRAConstants.LRA_API_VERSION_HEADER_NAME)) { + // no header specified, going with 'null' further into processing + return; + } + if (requestContext.getHeaders().get(LRAConstants.LRA_API_VERSION_HEADER_NAME).size() > 1) { + String errorMsg = "Multiple headers " + LRAConstants.LRA_API_VERSION_HEADER_NAME + " with API version provided." + +" Please, pass only one version header in the request."; + throw new WebApplicationException(errorMsg, + Response.status(EXPECTATION_FAILED).entity(errorMsg) + .header(LRA_API_VERSION_HEADER_NAME, NARAYANA_LRA_API_VERSION_1_0).build()); + } + + String apiVersionString = requestContext.getHeaderString(LRAConstants.LRA_API_VERSION_HEADER_NAME); + APIVersion apiVersion; + try { + apiVersion = APIVersion.instanceOf(apiVersionString); + } catch (Exception iae) { + String errorMsg = "Wrong format of the provided version " + apiVersionString + ": " + iae.getMessage(); + throw new WebApplicationException(errorMsg, iae, + Response.status(EXPECTATION_FAILED).entity(errorMsg) + .header(LRA_API_VERSION_HEADER_NAME, NARAYANA_LRA_API_VERSION_1_0).build()); + } + if (apiVersion.compareTo(LRAConstants.NARAYANA_LRA_API_VERSION_CURRENT) > 0) { + String errorMsg = "Demanded API version " + apiVersionString + + " is bigger than the supported one " + NARAYANA_LRA_API_VERSION_1_0; + throw new WebApplicationException(errorMsg, + Response.status(EXPECTATION_FAILED).entity(errorMsg) + .header(LRA_API_VERSION_HEADER_NAME, NARAYANA_LRA_API_VERSION_1_0).build()); + } + } } 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..5f0d23c126 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,35 @@ +/* + * 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 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 +37,17 @@ // 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 = LRAConstants.NARAYANA_LRA_API_VERSION_1_0, + 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"; } diff --git a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRAParticipantRecord.java b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRAParticipantRecord.java index c8519b6dfb..8cd9e3f0b6 100644 --- a/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRAParticipantRecord.java +++ b/rts/lra/coordinator/src/main/java/io/narayana/lra/coordinator/domain/model/LRAParticipantRecord.java @@ -113,11 +113,11 @@ public LRAParticipantRecord() { }); 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 { @@ -156,7 +156,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 4e94a92a45..f623239065 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 @@ -272,8 +272,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); @@ -299,16 +300,21 @@ public int leave(URI lraId, String compensatorUrl) { return Response.Status.PRECONDITION_FAILED.getStatusCode(); } + boolean wasForgotten; try { - if (!transaction.forgetParticipant(compensatorUrl)) { - if (LRALogger.logger.isInfoEnabled()) { - LRALogger.logger.infof("LRAServicve.forget %s failed%n", lraId); - } - } - - return Response.Status.OK.getStatusCode(); + wasForgotten = transaction.forgetParticipant(compensatorUrl); } 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()); + } + if (wasForgotten) { + return Response.Status.OK.getStatusCode(); + } else { + String errorMsg = String.format("LRAService.forget %s failed as the participant was not found, compensator url '%s'", + lraId, compensatorUrl); + throw new WebApplicationException(errorMsg, Response.status(Response.Status.BAD_REQUEST) + .entity(errorMsg).build()); } } @@ -404,7 +410,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/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/jaxrs/src/main/java/io/narayana/lra/filter/ServerLRAFilter.java b/rts/lra/jaxrs/src/main/java/io/narayana/lra/filter/ServerLRAFilter.java index ad318e6de4..d820cc8b99 100644 --- a/rts/lra/jaxrs/src/main/java/io/narayana/lra/filter/ServerLRAFilter.java +++ b/rts/lra/jaxrs/src/main/java/io/narayana/lra/filter/ServerLRAFilter.java @@ -540,7 +540,7 @@ public void filter(ContainerRequestContext requestContext, ContainerResponseCont String failureMessage = processLRAOperationFailures(progress); if (failureMessage != null) { - LRALogger.logger.warn(LRALogger.i18NLogger.warn_LRAStatusInDoubt(failureMessage)); + LRALogger.logger.warn(failureMessage); // the actual failure(s) will also have been added to the i18NLogger logs at the time they occured responseContext.setEntity(failureMessage, null, MediaType.TEXT_PLAIN_TYPE); diff --git a/rts/lra/pom.xml b/rts/lra/pom.xml index f92e746087..459b6e1822 100644 --- a/rts/lra/pom.xml +++ b/rts/lra/pom.xml @@ -1,8 +1,10 @@ - + --> + 4.0.0 org.jboss.narayana.rts @@ -80,6 +82,18 @@ + + org.hamcrest + hamcrest + ${version.hamcrest} + test + + + org.hamcrest + hamcrest-library + ${version.hamcrest} + test + junit junit diff --git a/rts/lra/proxy/api/src/main/java/io/narayana/lra/proxy/logging/lraI18NLogger.java b/rts/lra/proxy/api/src/main/java/io/narayana/lra/proxy/logging/lraI18NLogger.java index 0310ad390a..028f40c07f 100644 --- a/rts/lra/proxy/api/src/main/java/io/narayana/lra/proxy/logging/lraI18NLogger.java +++ b/rts/lra/proxy/api/src/main/java/io/narayana/lra/proxy/logging/lraI18NLogger.java @@ -23,7 +23,6 @@ package io.narayana.lra.proxy.logging; import static org.jboss.logging.Logger.Level.ERROR; -import static org.jboss.logging.Logger.Level.WARN; import java.util.concurrent.ExecutionException; @@ -48,17 +47,14 @@ public interface lraI18NLogger { @LogMessage(level = ERROR) void error_cannotSerializeParticipant(String participantToString, @Cause Throwable e); - @Message(id = 25003, value = "Participant '%s' exception during completion") + @Message(id = 25002, value = "Participant '%s' exception during completion") @LogMessage(level = ERROR) void error_participantExceptionOnCompletion(String name, @Cause ExecutionException e); - @Message(id = 25004, value = "Cannot get status of participant '%s' of lra id '%s'") + @Message(id = 25003, value = "Cannot get status of participant '%s' of lra id '%s'") @LogMessage(level = ERROR) void error_gettingParticipantStatus(String participant, String lraId, @Cause Throwable e); - @Message(id = 25005, value = "Participant deserialization failed for LRA '%s' using deserializer class %s: '%s'") - @LogMessage(level = WARN) - void warn_cannotDeserializeParticipant(String lraId, String deserializer, String message); /* Allocate new messages directly above this notice. diff --git a/rts/lra/service-base/src/main/java/io/narayana/lra/APIVersion.java b/rts/lra/service-base/src/main/java/io/narayana/lra/APIVersion.java new file mode 100644 index 0000000000..484de8bbc7 --- /dev/null +++ b/rts/lra/service-base/src/main/java/io/narayana/lra/APIVersion.java @@ -0,0 +1,149 @@ +/* + * 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; + +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 LRAConstants#NARAYANA_LRA_API_VERSION_CURRENT}. + * + * @param versionString version string to be parsed; when null or empty the most up-to-date + * {@link LRAConstants#NARAYANA_LRA_API_VERSION_CURRENT} 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) { + if (versionString == null || versionString.isEmpty()) { + return LRAConstants.NARAYANA_LRA_API_VERSION_CURRENT; + } + Matcher versionMatcher = versionPattern.matcher(versionString); + 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.parseInt(versionMatcher.group(1)); + int minor = Integer.parseInt(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); + } + } + + private APIVersion(int major, int minor, String preRelease) { + this.major = major; + this.minor = minor; + this.preRelease = preRelease; + } + + /** + * The 'major' and 'minor' versions are compared in numeric comparison. + * If the 'preRelease' exists then the version is considered equal + * + * If one version does not contain the 'preRelease' version string + * then it's considered bigger, + * if both versions contains the 'preRelease' strings then they're + * compared with {@link String#compareTo(String)} method. + * + * @param anotherVersion version to compare against + * @return < 0 if this version is lower than otherVersion, > 0 if this version is bigger than otherVersion, + * 0 if the versions are equal + */ + @Override + public int compareTo(APIVersion anotherVersion) { + int result = Integer.compare(major, anotherVersion.major); + if (result == 0) { + result = Integer.compare(minor, anotherVersion.minor); + } + if (result == 0 ) { + if (preRelease == null && anotherVersion.preRelease == null) { + result = 0; + // version without preRelease is bigger than the one with the preRelease + } else if (preRelease == null && anotherVersion.preRelease != null) { + result = 1; + } else if (preRelease != null && anotherVersion.preRelease == null) { + result = -1; + } else { + result = preRelease.compareTo(anotherVersion.preRelease); + } + } + 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/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..06ea2e6b02 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,20 @@ 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 + /* + * Supported Narayana LRA API versions. + */ + public static final String NARAYANA_LRA_API_VERSION_1_0 = "1.0"; + + /** + * The Narayana API version for LRA coordinator supported for the release. + * Any bigger version is considered as unimplemented and unknown. + * Any lower version is considered as older, implemented but it could be deprecated. + */ + public static final APIVersion NARAYANA_LRA_API_VERSION_CURRENT = APIVersion.instanceOf(NARAYANA_LRA_API_VERSION_1_0); + + 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/logging/lraI18NLogger.java b/rts/lra/service-base/src/main/java/io/narayana/lra/logging/lraI18NLogger.java index 79c3139fb0..e8500fa201 100644 --- a/rts/lra/service-base/src/main/java/io/narayana/lra/logging/lraI18NLogger.java +++ b/rts/lra/service-base/src/main/java/io/narayana/lra/logging/lraI18NLogger.java @@ -26,7 +26,6 @@ import static org.jboss.logging.Logger.Level.INFO; import static org.jboss.logging.Logger.Level.WARN; -import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -49,175 +48,105 @@ public interface lraI18NLogger { Allocate new messages by following instructions at the bottom of the file. */ @LogMessage(level = ERROR) - @Message(id = 25001, value = "Can't construct URL from LRA id '%s'") - void error_urlConstructionFromStringLraId(String lraId, @Cause Throwable t); - - @LogMessage(level = ERROR) - @Message(id = 25002, value = "LRA created with an unexpected status code: %d, coordinator response '%s'") + @Message(id = 25001, value = "LRA created with an unexpected status code: %d, coordinator response '%s'") void error_lraCreationUnexpectedStatus(int status, String response); @LogMessage(level = ERROR) - @Message(id = 25004, value = "Cannot create URL from coordinator response '%s'") - void error_cannotCreateUrlFromLCoordinatorResponse(String response, @Cause Throwable t); - - @LogMessage(level = ERROR) - @Message(id = 25005, value = "Error on contacting the LRA coordinator '%s'") - void error_cannotContactLRACoordinator(URI coordinator, @Cause Throwable t); - - @LogMessage(level = ERROR) - @Message(id = 25006, value = "LRA renewal ends with an unexpected status code: %d, coordinator response '%s'") - void error_lraRenewalUnexpectedStatus(int status, String response); - - @LogMessage(level = ERROR) - @Message(id = 25007, value = "Leaving LRA ends with an unexpected status code: %d, coordinator response '%s'") + @Message(id = 25002, value = "Leaving LRA ends with an unexpected status code: %d, coordinator response '%s'") void error_lraLeaveUnexpectedStatus(int status, String response); @LogMessage(level = WARN) - @Message(id = 25008, value = "JAX-RS @Suspended annotation is untested") - void warn_suspendAnnotationIsUntested(); - - @LogMessage(level = WARN) - @Message(id = 25009, value = "LRA participant class '%s' with asynchronous temination but no @Status or @Forget annotations") + @Message(id = 25003, value = "LRA participant class '%s' with asynchronous temination but no @Status or @Forget annotations") void error_asyncTerminationBeanMissStatusAndForget(Class clazz); - @Message(id = 25010, value = "LRA finished with an unexpected status code: %d, coordinator response '%s'") + @Message(id = 25004, value = "LRA finished with an unexpected status code: %d, coordinator response '%s'") String error_lraTerminationUnexpectedStatus(int status, String response); @LogMessage(level = ERROR) - @Message(id = 25011, value = "Cannot access coordinator '%s' when getting status for LRA '%s'") - void error_cannotAccessCoordinatorWhenGettingStatus(URI coordinator, URL lra, @Cause Throwable t); - - @LogMessage(level = ERROR) - @Message(id = 25012, value = "LRA coordinator '%s' returned an invalid status code '%s' for LRA '%s'") + @Message(id = 25005, value = "LRA coordinator '%s' returned an invalid status code '%s' for LRA '%s'") void error_invalidStatusCode(URI coordinator, int status, URL lra); @LogMessage(level = ERROR) - @Message(id = 25013, value = "LRA coordinator '%s' returned no content on #getStatus call for LRA '%s'") + @Message(id = 25006, value = "LRA coordinator '%s' returned no content on #getStatus call for LRA '%s'") void error_noContentOnGetStatus(URI coordinator, URL lra); @LogMessage(level = ERROR) - @Message(id = 25014, value = "LRA coordinator '%s' returned an invalid status for LRA '%s'") + @Message(id = 25007, value = "LRA coordinator '%s' returned an invalid status for LRA '%s'") void error_invalidArgumentOnStatusFromCoordinator(URI coordinator, URL lra, @Cause Throwable t); @LogMessage(level = ERROR) - @Message(id = 25015, value = "Too late to join with the LRA '%s', coordinator response: '%s'") + @Message(id = 25008, value = "Too late to join with the LRA '%s', coordinator response: '%s'") void error_tooLateToJoin(URL lra, String response); @LogMessage(level = ERROR) - @Message(id = 25016, value = "Failed enlisting to LRA '%s', coordinator '%s' responded with status '%s'") + @Message(id = 25009, value = "Failed enlisting to LRA '%s', coordinator '%s' responded with status '%s'") void error_failedToEnlist(URL lra, URI coordinator, int status); @LogMessage(level = ERROR) - @Message(id = 25017, value = "Trying to aquire an in use connection for coordinator '%s'") - void error_connectionInUse(URI coordinator); - - /* - @LogMessage(level = INFO) - @Message(id = 25018, value = "Error parsing json LRAStatus from JSON '%s'") - void warn_failedParsingStatusFromJson(JsonObject json, @Cause Throwable t); - */ - - @LogMessage(level = ERROR) - @Message(id = 25019, value = "Invalid query format '%s' to get lra statuses") - void error_invalidQueryForGettingLraStatuses(String query); - - @LogMessage(level = ERROR) - @Message(id = 25020, value = "Invalid format of coordinator url, was '%s'") - void error_invalidCoordinatorUrl(URL coordinatorUrl, @Cause Throwable t); - - @LogMessage(level = ERROR) - @Message(id = 25022, value = "Error when converting String '%s' to URL") + @Message(id = 25010, value = "Error when converting String '%s' to URL") void error_invalidStringFormatOfUrl(String string, @Cause Throwable t); @LogMessage(level = ERROR) - @Message(id = 25023, value = "Error when encoding LRA id URL '%s' to String") - void error_invalidFormatToEncodeUrl(URL url, @Cause Throwable t); - - @LogMessage(level = ERROR) - @Message(id = 25024, value = "Invalid LRA id format to create LRA record from LRA id '%s', link URI '%s'") + @Message(id = 25011, value = "Invalid LRA id format to create LRA record from LRA id '%s', link URI '%s'") void error_invalidFormatToCreateLRARecord(String lraId, String linkURI); @LogMessage(level = ERROR) - @Message(id = 25025, value = "Cannot get status of nested lra '%s' as outer one '%s' is still active") - void error_cannotGetStatusOfNestedLra(String nestedLraId, URL lraId); - - @LogMessage(level = ERROR) - @Message(id = 25026, value = "Invalid recovery url '%s' to join lra '%s'") - void error_invalidRecoveryUrlToJoinLRA(String recoveryUrl, URL lraId); - - @LogMessage(level = ERROR) - @Message(id = 25027, value = "Cannot found compensator url '%s' for lra '%s'") + @Message(id = 25012, value = "Cannot found compensator url '%s' for lra '%s'") void error_cannotFoundCompensatorUrl(String recoveryUrl, String lraId); - @LogMessage(level = ERROR) - @Message(id = 25028, value = "Invalid format of lra id '%s' to replace compensator '%s'") - void error_invalidFormatOfLraIdReplacingCompensator(String recoveryUrl, String lraId, @Cause MalformedURLException e); - - @LogMessage(level = ERROR) - @Message(id = 25029, value = "Invalid format of request uri '%s' for lra id '%s' to replace compensator '%s'") - void error_invalidFormatOfRequestUri(URI uri, String recoveryUrl, String lraId, @Cause MalformedURLException e); - - @Message(id = 25030, value = "Could not recreate abstract record '%s'") + @Message(id = 25013, value = "Could not recreate abstract record '%s'") @LogMessage(level = WARN) void warn_coordinatorNorecordfound(String recordType, @Cause Throwable t); - @Message(id = 25031, value = "Cannot retrieve compensator status data '%s' of lra id '%s'") - @LogMessage(level = WARN) - void warn_cannotGetCompensatorStatusData(String data, URL lraId, @Cause Throwable t); - - @Message(id = 25032, value = "reason '%s': container request for method '%s': lra: '%s'") + @Message(id = 25014, value = "reason '%s': container request for method '%s': lra: '%s'") @LogMessage(level = WARN) void warn_lraFilterContainerRequest(String reason, String method, String lra); - @Message(id = 25033, value = "trying to aquire an in use connection") - @LogMessage(level = ERROR) - void error_cannotAquireInUseConnection(); - - @Message(id = 25034, value = "LRA participant completion for asynchronous method %s#%s should return %d and not %d") + @Message(id = 25015, value = "LRA participant completion for asynchronous method %s#%s should return %d and not %d") @LogMessage(level = WARN) void warn_lraParticipantqForAsync(String clazz, String method, int statusCorrect, int statusWrong); @LogMessage(level = ERROR) - @Message(id = 25035, value = "Cannot get status of nested lra '%s' as outer one '%s' is still active") + @Message(id = 25016, value = "Cannot get status of nested lra '%s' as outer one '%s' is still active") void error_cannotGetStatusOfNestedLraURI(String nestedLraId, URI lraId); @LogMessage(level = ERROR) - @Message(id = 25036, value = "Invalid recovery url '%s' to join lra '%s'") + @Message(id = 25017, value = "Invalid recovery url '%s' to join lra '%s'") void error_invalidRecoveryUrlToJoinLRAURI(String recoveryUrl, URI lraId); @LogMessage(level = ERROR) - @Message(id = 25037, value = "Invalid format of lra id '%s' to replace compensator '%s'") + @Message(id = 25018, value = "Invalid format of lra id '%s' to replace compensator '%s'") void error_invalidFormatOfLraIdReplacingCompensatorURI(String recoveryUrl, String lraId, @Cause URISyntaxException e); @LogMessage(level = WARN) - @Message(id = 25038, value = "LRA participant `%s` returned immediate state (Compensating/Completing) from CompletionStage. LRA id: %s") + @Message(id = 25019, value = "LRA participant `%s` returned immediate state (Compensating/Completing) from CompletionStage. LRA id: %s") void warn_participantReturnsImmediateStateFromCompletionStage(String participantId, String lraId); @LogMessage(level = ERROR) - @Message(id = 25039, value = "Cannot process non JAX-RS LRA participant") + @Message(id = 25020, value = "Cannot process non JAX-RS LRA participant") void error_cannotProcessParticipant(@Cause ReflectiveOperationException e); @LogMessage(level = WARN) - @Message(id = 25040, value = "CDI cannot be detected, non JAX-RS LRA participants will not be processed") + @Message(id = 25021, value = "CDI cannot be detected, non JAX-RS LRA participants will not be processed") void warn_nonJaxRsParticipantsNotAllowed(); @LogMessage(level = ERROR) - @Message(id = 25041, value = "Invalid format of LRA id to be converted to LRA coordinator url, was '%s'") + @Message(id = 25022, value = "Invalid format of LRA id to be converted to LRA coordinator url, was '%s'") void error_invalidLraIdFormatToConvertToCoordinatorUrl(String lraId, @Cause Throwable t); @LogMessage(level = INFO) - @Message(id = 25042, value = "Failed enlisting to LRA '%s', coordinator '%s' responded with status '%d (%s)'. Returning '%d (%s)'.") + @Message(id = 25023, value = "Failed enlisting to LRA '%s', coordinator '%s' responded with status '%d (%s)'. Returning '%d (%s)'.") void info_failedToEnlistingLRANotFound(URL lraId, URI coordinatorUri, int coordinatorStatusCode, String coordinatorStatusMsg, int returnStatusCode, String returnStatusMsg); - @Message(id = 25043, value = "Could not %s LRA '%s': coordinator '%s' responded with status '%s'") + @Message(id = 25024, value = "Could not %s LRA '%s': coordinator '%s' responded with status '%s'") String get_couldNotCompleteCompensateOnReturnedStatus(String actionName, URI lraId, URI coordinatorUri, String status); @LogMessage(level = ERROR) - @Message(id = 25044, value = "Error when encoding parent LRA id URL '%s' to String") + @Message(id = 25025, value = "Error when encoding parent LRA id URL '%s' to String") void error_invalidFormatToEncodeParentUri(URI parentUri, @Cause Throwable t); - @Message(id = 25145, value = "Unable to process LRA annotations: %s'") + @Message(id = 25026, value = "Unable to process LRA annotations: %s'") String warn_LRAStatusInDoubt(String reason); @LogMessage(level = WARN) diff --git a/rts/lra/service-base/src/test/java/io/narayana/lra/APIVersionTest.java b/rts/lra/service-base/src/test/java/io/narayana/lra/APIVersionTest.java new file mode 100644 index 0000000000..b5be3a5bfd --- /dev/null +++ b/rts/lra/service-base/src/test/java/io/narayana/lra/APIVersionTest.java @@ -0,0 +1,92 @@ +/* + * 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; + +import org.hamcrest.MatcherAssert; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.Matchers.comparesEqualTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThan; + +/** + * 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(LRAConstants.NARAYANA_LRA_API_VERSION_CURRENT, greaterThanOrEqualTo(oneZeroVersion)); + } + + @Test + public void preReleaseIsLowerToFinal() { + APIVersion version = APIVersion.instanceOf("1.1"); + MatcherAssert.assertThat(version, greaterThan(testVersion)); + } + + @Test + public void preReleaseIsNotEqualToFinal() { + APIVersion version = APIVersion.instanceOf("1.1"); + MatcherAssert.assertThat(version, not(comparesEqualTo(testVersion))); + } + + @Test + public void preReleaseIsCompareEqualToSamePreRelease() { + APIVersion version = APIVersion.instanceOf("1.1-Alpha"); + MatcherAssert.assertThat(version, comparesEqualTo(testVersion)); + } + + @Test + public void lowerMajorVersion() { + APIVersion version = APIVersion.instanceOf("0.1"); + MatcherAssert.assertThat(version, lessThan(testVersion)); + } + + @Test + public void biggerMajorVersion() { + 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 biggerMinorVersion() { + 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/service-base/src/test/java/io/narayana/lra/LRAConstantsTest.java b/rts/lra/service-base/src/test/java/io/narayana/lra/LRAConstantsTest.java index 86b3ce436c..e3f94dbbb8 100644 --- a/rts/lra/service-base/src/test/java/io/narayana/lra/LRAConstantsTest.java +++ b/rts/lra/service-base/src/test/java/io/narayana/lra/LRAConstantsTest.java @@ -33,7 +33,6 @@ public class LRAConstantsTest { public void getCoordinatorFromUsualLRAId() { URI lraId = URI.create("http://localhost:8080/lra-coordinator/0_ffff0a28054b_9133_5f855916_a7?query=1#fragment"); URI coordinatorUri = LRAConstants.getLRACoordinatorUrl(lraId); - System.out.println(">>>> " + coordinatorUri); Assert.assertEquals("http", coordinatorUri.getScheme()); Assert.assertEquals("localhost", coordinatorUri.getHost()); Assert.assertEquals(8080, coordinatorUri.getPort()); diff --git a/rts/lra/test/arquillian-extension/src/main/java/io/narayana/lra/arquillian/AppServerCoordinatorDeploymentObserver.java b/rts/lra/test/arquillian-extension/src/main/java/io/narayana/lra/arquillian/AppServerCoordinatorDeploymentObserver.java index 2b485c8c5d..88715b5513 100644 --- a/rts/lra/test/arquillian-extension/src/main/java/io/narayana/lra/arquillian/AppServerCoordinatorDeploymentObserver.java +++ b/rts/lra/test/arquillian-extension/src/main/java/io/narayana/lra/arquillian/AppServerCoordinatorDeploymentObserver.java @@ -53,7 +53,7 @@ public class AppServerCoordinatorDeploymentObserver { * which is deployed when the app server starts. */ public void handleAfterStartup(@Observes AfterStart event, Container container) throws Exception { - if(!container.getName().contains(CONTAINER_NAME_RECOGNITION)) { + if (!container.getName().contains(CONTAINER_NAME_RECOGNITION)) { log.debugf("Handling before after start-up event for container '%s'. The container name does not contain substring '%s' " + "thus skipping execution.", container.getName(), CONTAINER_NAME_RECOGNITION); return; @@ -61,7 +61,7 @@ public void handleAfterStartup(@Observes AfterStart event, Container container) log.debugf("handleAfterStartup for container %s", container.getName()); Archive deployment = createLRACoordinatorDeployment(); - if(deployments.put(deployment.getName(), deployment) == null) { + if (deployments.put(deployment.getName(), deployment) == null) { log.infof("Deploying LRA Coordinator war deployment: %s", deployment.getName()); container.getDeployableContainer() .deploy(deployment); @@ -73,14 +73,14 @@ public void handleAfterStartup(@Observes AfterStart event, Container container) * {@link #handleAfterStartup(AfterStart, Container)}. */ public void handleBeforeStop(@Observes BeforeStop event, Container container) throws Exception { - if(!container.getName().contains(CONTAINER_NAME_RECOGNITION)) { + if (!container.getName().contains(CONTAINER_NAME_RECOGNITION)) { log.debugf("Handling before stop event for container '%s'. The container name does not contain substring '%s' " + "thus skipping execution.", container.getName(), CONTAINER_NAME_RECOGNITION); return; } log.debugf("handleBeforeStop for container %s", container.getName()); - for(Archive deployment: deployments.values()) { + for (Archive deployment: deployments.values()) { log.infof("Undeploying LRA Coordinator war deployment: %s", deployment.getName()); container.getDeployableContainer().undeploy(deployment); } @@ -98,12 +98,12 @@ public static WebArchive createLRACoordinatorDeployment() { .withTransitivity().asFile(); ZipImporter zip = ShrinkWrap.create(ZipImporter.class, LRA_COORDINATOR_DEPLOYMENT_NAME + ".war"); - for(File file: files) { + for (File file: files) { zip.importFrom(file); } WebArchive war = zip.as(WebArchive.class); - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debugf("Content of the LRA Coordinator deployment is:%n%s%n", war.toString(true)); } return war; 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..84d7e97957 --- /dev/null +++ b/rts/lra/test/basic/src/test/java/io/narayana/lra/arquillian/api/CoordinatorApi_1_0_IT.java @@ -0,0 +1,937 @@ +/* + * 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.LRAConstants; +import io.narayana.lra.LRAData; +import io.narayana.lra.arquillian.Deployer; +import io.narayana.lra.client.NarayanaLRAClient; +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.logging.Logger; +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.time.Instant; +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. + *

+ *

+ * The tests serves to verify the compliance of the {@link io.narayana.lra.coordinator.api.Coordinator} + * REST API methods with the Narayana LRA API version {@code API_VERSION_1_0}.
+ * Each test case corresponds by name with the method in the {@link io.narayana.lra.coordinator.api.Coordinator}. + * Then it verifies the expected API respond on particular state. It verifies if the status code + * was correct, if the return data was in correct format, it check the returned headers etc. + *

+ *

+ * This class is not expected to be changed. The Narayana LRA API of version 1.0 is not expected to be changed.
+ * When Coordinator changes the API there will be updated the API version for such change. + * The new or changed behaviour belongs to a separate test class + * which verifies the behaviour of the particular API version. + *

+ */ +@RunWith(Arquillian.class) +@RunAsClient +public class CoordinatorApi_1_0_IT { + private static final Logger log = Logger.getLogger(CoordinatorApi_1_0_IT.class); + private 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 = Integer.MAX_VALUE + ".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() { + log.info("Running test " + testName.getMethodName()); + 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(); + } + } + + /** + * GET - / + * To gets all active LRAs. + */ + @Test + public void getAllLRAs() { + // be aware of risk of non monotonic java time, ie. https://www.javaadvent.com/2019/12/measuring-time-from-java-to-kernel-and-back.html + long beforeTime = Instant.now().toEpochMilli(); + + 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>() {}); + } + + Optional lraTopOptional = data.stream().filter(record -> record.getLraId().equals(lraId1)).findFirst(); + Assert.assertTrue("Expected to find the top-level LRA id " + lraId1 + " from REST get all call", lraTopOptional.isPresent()); + LRAData lraTop = lraTopOptional.get(); + Optional lraNestedOptional = data.stream().filter(record -> record.getLraId().equals(lraId2)).findFirst(); + Assert.assertTrue("Expected to find the nested LRA id " + lraId2 + " from REST get all call", lraNestedOptional.isPresent()); + LRAData lraNested = lraNestedOptional.get(); + + Assert.assertEquals("Expected top-level LRA '" + lraTop + "' being active", + LRAStatus.Active, lraTop.getStatus()); + Assert.assertEquals("Expected top-level LRA '" + lraTop + "' being active, HTTP status 204.", + Status.NO_CONTENT.getStatusCode(), lraTop.getHttpStatus()); + Assert.assertFalse("Expected top-level LRA '" + lraTop + "' not being recovering", lraTop.isRecovering()); + Assert.assertTrue("Expected top-level LRA '" + lraTop + "' to be top level", lraTop.isTopLevel()); + MatcherAssert.assertThat("Expected the start time of top-level LRA '" + lraTop + "' is after the test start time", + beforeTime, lessThan(lraTop.getStartTime())); + + Assert.assertEquals("Expected nested 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 nested LRA '" + lraNested + "' not being recovering", lraNested.isRecovering()); + Assert.assertFalse("Expected nested LRA '" + lraNested + "' to be nested", lraNested.isTopLevel()); + MatcherAssert.assertThat("Expected the start time of nested LRA '" + lraNested + "' is after the test start time", + beforeTime, lessThan(lraNested.getStartTime())); + } + + /** + * GET - /?Status=Active + * To gets active LRAs with status. + */ + @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 top-level LRA", + returnedLraIds, hasItem(lraId1)); + MatcherAssert.assertThat("Expected the coordinator filtered out the non-active nested LRA", + returnedLraIds, not(hasItem(lraId2))); + } + } + + /** + * GET - /?Status=NonExistingStatus + * Asking for LRAs with status while providing a wrong status. + */ + @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)); + } + } + + /** + * GET - /{lraId}/status + * Finding a status of a started LRA. + */ + @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)); + } + } + + /** + * GET - /{lraId}/status + * Finding a status of a non existing LRA or wrong LRA id. + */ + @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)); + } + } + + /** + * GET - /{lraId} + * Obtaining info of a started LRA. + */ + @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()); + } + } + + /** + * GET - /{lraId} + * Obtaining info of a non-existing LRA. + */ + @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)); + } + } + + /** + * POST - /start?TimeLimit=...&ClientID=...&ParentLRA=... + * PUT - /{lraId}/close + * Starting and closing an LRA. + */ + @Test + 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 = URI.create(response.readEntity(String.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 = URI.create(response.readEntity(String.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)) + .filter(data -> data.getStatus() != LRAStatus.Closing && data.getStatus() != LRAStatus.Closed) + .collect(Collectors.toList()); + MatcherAssert.assertThat("Expecting the started LRAs are no more active after closing the top-level one", + activeLRAsAfterClosing, emptyCollectionOf(LRAData.class)); + } + + /** + * POST - /start?ClientID=... + * PUT - /{lraId}/cancel + * Starting and canceling an LRA. + */ + @Test + 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 = URI.create(response.readEntity(String.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", + LRAConstants.NARAYANA_LRA_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 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", + LRAConstants.NARAYANA_LRA_API_VERSION_1_0, 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)); + } + + /** + * POST - /start?ClientId=...&ParentLRA=... + * Starting a nested LRA with a non-existing parent. + */ + @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)); + } + } + + /** + * PUT - /{lraId}/close + * Closing a non-existing LRA. + */ + @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)); + } + } + + /** + * PUT - /{lraId}/cancel + * Canceling a non-existing LRA. + */ + @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)); + } + } + + /** + * PUT - /renew?TimeLimit= + * Renewing the time limit of the started LRA. + */ + @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)); + } + + /** + * PUT - /renew?TimeLimit= + * Renewing the time limit of a non-existing LRA. + */ + @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)); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via entity body. + */ + @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)); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via link header. + */ + @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)); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via link header with link rel specified. + */ + @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", + LRAConstants.NARAYANA_LRA_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); + 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")); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via link header with link after specified. + */ + @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")); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via link header with wrong link format. + */ + @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()); + } + } + + /** + * PUT - /{lraId} + * Joining a non-existing LRA. + */ + @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)); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via entity body of a wrong format. + */ + @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())); + } + } + + /** + * PUT - /{lraId} + * Joining an LRA participant via link header missing required rel items. + */ + @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())); + } + } + + /** + * PUT - /{lraId}/remove + * Leaving an LRA as participant. + */ + @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())); + } + } + + /** + * PUT - /{lraId}/remove + * Leaving a non-existing LRA as participant. + */ + @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 methods down here verifies a wrong version being provided to the API + + @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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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/417.", + Status.EXPECTATION_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