Skip to content

Commit

Permalink
ISPN-14415 Cache config comparison REST API
Browse files Browse the repository at this point in the history
  • Loading branch information
tristantarrant authored and ryanemerson committed Dec 15, 2022
1 parent cb5a4a1 commit b453a52
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ default CompletionStage<RestResponse> post(String path) {
return post(path, Collections.emptyMap());
}

CompletionStage<RestResponse> postMultipartForm(String url, Map<String, String> headers, Map<String, List<String>> formParameters);

CompletionStage<RestResponse> post(String path, String body, String bodyMediaType);

CompletionStage<RestResponse> post(String path, Map<String, String> headers);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.internal.Util;
Expand Down Expand Up @@ -39,6 +40,18 @@ public CompletionStage<RestResponse> postForm(String url, Map<String, String> he
return restClient.execute(builder);
}

@Override
public CompletionStage<RestResponse> postMultipartForm(String url, Map<String, String> headers, Map<String, List<String>> formParameters) {
Request.Builder builder = new Request.Builder();
builder.url(restClient.getBaseURL() + url);
headers.forEach(builder::header);
MultipartBody.Builder form = new MultipartBody.Builder();
form.setType(MultipartBody.FORM);
formParameters.forEach((k, vs) -> vs.forEach(v -> form.addFormDataPart(k, v)));
builder.post(form.build());
return restClient.execute(builder);
}

@Override
public CompletionStage<RestResponse> post(String url, String body, String bodyMediaType) {
Request.Builder builder = new Request.Builder();
Expand Down
13 changes: 13 additions & 0 deletions documentation/src/main/asciidoc/topics/ref_rest_caches.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ curl localhost:11222/rest/v2/caches?action=convert \
-d '<replicated-cache mode="SYNC" statistics="false"><encoding media-type="application/x-protostream"/><expiration lifespan="300000" /><memory max-size="400MB" when-full="REMOVE"/></replicated-cache>'
----

[id='rest_v2_cache_config_compare']
= Comparing Cache Configurations
Invoke a `POST` request with a `multipart/form-data` body containing two configurations and the `?action=compare`
parameter. {brandname} responds with `204 (No Content)` in case the configurations are equal, and `409 (Conflict)` in case they are different.

[source,options="nowrap",subs=attributes+]
----
POST /rest/v2/caches?action=compare
----

Add the `ignoreMutable=true` parameter to ignore mutable attributes in the comparison.


[id='rest_v2_cache_detail']
= Retrieving All Cache Details
Invoke a `GET` request to retrieve all details for {brandname} caches.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.infinispan.rest.resources;

import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.CONFLICT;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.NO_CONTENT;
Expand Down Expand Up @@ -81,6 +82,7 @@
import org.infinispan.rest.CacheKeyInputStream;
import org.infinispan.rest.EventStream;
import org.infinispan.rest.InvocationHelper;
import org.infinispan.rest.NettyRestRequest;
import org.infinispan.rest.NettyRestResponse;
import org.infinispan.rest.ResponseHeader;
import org.infinispan.rest.RestResponseException;
Expand All @@ -100,7 +102,12 @@
import org.infinispan.topology.LocalTopologyManager;
import org.infinispan.upgrade.RollingUpgradeManager;

import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.multipart.DefaultHttpDataFactory;
import io.netty.handler.codec.http.multipart.HttpPostMultipartRequestDecoder;
import io.netty.handler.codec.http.multipart.InterfaceHttpData;
import io.netty.handler.codec.http.multipart.MemoryAttribute;

/**
* REST resource to manage the caches.
Expand Down Expand Up @@ -168,6 +175,7 @@ public Invocations getInvocations() {
// Misc
.invocation().methods(POST).path("/v2/caches").withAction("toJSON").deprecated().handleWith(this::convertToJson)
.invocation().methods(POST).path("/v2/caches").withAction("convert").handleWith(this::convert)
.invocation().methods(POST).path("/v2/caches").withAction("compare").handleWith(this::compare)

// All details
.invocation().methods(GET).path("/v2/caches/{cacheName}").handleWith(this::getAllDetails)
Expand Down Expand Up @@ -354,6 +362,46 @@ private CompletionStage<RestResponse> convert(RestRequest request) {
return convert(request, negotiateMediaType(request, APPLICATION_JSON, APPLICATION_XML, APPLICATION_YAML));
}

private CompletionStage<RestResponse> compare(RestRequest request) {
boolean ignoreMutable = Boolean.parseBoolean(request.getParameter("ignoreMutable"));
NettyRestResponse.Builder responseBuilder = new NettyRestResponse.Builder();
MediaType contentType = request.contentType();
if (!contentType.match(MediaType.MULTIPART_FORM_DATA)) {
return CompletableFuture.completedFuture(responseBuilder.status(BAD_REQUEST).build());
}
FullHttpRequest nettyRequest = ((NettyRestRequest) request).getFullHttpRequest();
DefaultHttpDataFactory factory = new DefaultHttpDataFactory(false);
HttpPostMultipartRequestDecoder decoder = new HttpPostMultipartRequestDecoder(factory, nettyRequest);
List<InterfaceHttpData> datas = decoder.getBodyHttpDatas();
if (datas.size() != 2) {
return CompletableFuture.completedFuture(responseBuilder.status(BAD_REQUEST).build());
}
MemoryAttribute one = (MemoryAttribute) datas.get(0);
MemoryAttribute two = (MemoryAttribute) datas.get(1);
String s1 = one.content().toString(UTF_8);
String s2 = two.content().toString(UTF_8);
ParserRegistry parserRegistry = invocationHelper.getParserRegistry();
Map<String, ConfigurationBuilder> b1 = parserRegistry.parse(s1, null).getNamedConfigurationBuilders();
Map<String, ConfigurationBuilder> b2 = parserRegistry.parse(s2, null).getNamedConfigurationBuilders();
if (b1.size() != 1 || b2.size() != 1) {
return CompletableFuture.completedFuture(responseBuilder.status(BAD_REQUEST).build());
}
Configuration c1 = b1.values().iterator().next().build();
Configuration c2 = b2.values().iterator().next().build();
boolean result;
if (ignoreMutable) {
try {
c1.validateUpdate(c2);
result = true;
} catch (Throwable t) {
result = false;
}
} else {
result = c1.equals(c2);
}
return CompletableFuture.completedFuture(responseBuilder.status(result ? NO_CONTENT : CONFLICT).build());
}

private CompletionStage<RestResponse> streamKeys(RestRequest request) {
String cacheName = request.variables().get("cacheName");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1076,7 +1076,7 @@ public static <T> T getYamlProperty(Map<String, Object> yaml, String... names) {
}
}
}
return (T) yaml.get(names[names.length -1 ]);
return (T) yaml.get(names[names.length - 1]);
}

@Test
Expand Down Expand Up @@ -1340,6 +1340,45 @@ public void testCacheAvailability() {
ResponseAssertion.assertThat(restResponse).isOk().containsReturnedText("AVAILABLE");
}

@Test
public void testComparison() {
RestRawClient rawClient = client.raw();

String xml = "<distributed-cache name=\"cacheName\" mode=\"SYNC\">\n" +
"<memory>\n" +
"<object size=\"20\"/>\n" +
"</memory>\n" +
"</distributed-cache>";
String json20 = "{\"distributed-cache\":{\"memory\":{\"object\":{\"size\":\"20\"}}}}";
String json30 = "{\"distributed-cache\":{\"memory\":{\"object\":{\"size\":\"30\"}}}}";
String jsonrepl = "{\"replicated-cache\":{\"memory\":{\"object\":{\"size\":\"30\"}}}}";

Map<String, List<String>> form = new HashMap<>();
form.put("one", Collections.singletonList(xml));
form.put("two", Collections.singletonList(json20));

CompletionStage<RestResponse> response = rawClient.postMultipartForm("/rest/v2/caches?action=compare", Collections.emptyMap(), form);
assertThat(response).isOk();

form = new HashMap<>();
form.put("one", Collections.singletonList(xml));
form.put("two", Collections.singletonList(json30));

response = rawClient.postMultipartForm("/rest/v2/caches?action=compare", Collections.emptyMap(), form);
assertThat(response).isConflicted();

response = rawClient.postMultipartForm("/rest/v2/caches?action=compare&ignoreMutable=true", Collections.emptyMap(), form);
assertThat(response).isOk();

form = new HashMap<>();
form.put("one", Collections.singletonList(xml));
form.put("two", Collections.singletonList(jsonrepl));

response = rawClient.postMultipartForm("/rest/v2/caches?action=compare&ignoreMutable=true", Collections.emptyMap(), form);
assertThat(response).isConflicted();

}

private void assertBadResponse(RestCacheClient client, String config) {
RestResponse response = join(client.connectSource(RestEntity.create(APPLICATION_JSON, config)));
ResponseAssertion.assertThat(response).isBadRequest();
Expand Down

0 comments on commit b453a52

Please sign in to comment.