diff --git a/pom.xml b/pom.xml
index 745a1ff55f..529be465e1 100644
--- a/pom.xml
+++ b/pom.xml
@@ -497,6 +497,7 @@
1.1.21.0.101.0.0.Beta1
+ 2.21.5.0
diff --git a/rts/lra/API.adoc b/rts/lra/API.adoc
new file mode 100644
index 0000000000..fbfd653e2f
--- /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 (ie. 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 (ie. 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..56194b6edc 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,9 +321,10 @@ 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);
+ .get(START_TIMEOUT, TimeUnit.SECONDS);
// validate the HTTP status code says an LRA resource was created
if (isUnexpectedResponseStatus(response, Response.Status.CREATED)) {
@@ -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,6 +424,7 @@ 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);
@@ -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,8 +731,9 @@ 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);
@@ -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..4ee6db4259 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 567592563c..3c43672eed 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
@@ -271,8 +271,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);
@@ -298,16 +299,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());
}
}
@@ -403,7 +409,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.0org.jboss.narayana.rts
@@ -80,6 +82,18 @@
+
+ org.hamcrest
+ hamcrest
+ ${version.hamcrest}
+ test
+
+
+ org.hamcrest
+ hamcrest-library
+ ${version.hamcrest}
+ test
+ junitjunit
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 d80c512b21..da186c27df 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);
@Message(id = 25146, value = "Missing recovery module for LRAs")
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