diff --git a/core/src/main/java/org/infinispan/encoding/DataConversion.java b/core/src/main/java/org/infinispan/encoding/DataConversion.java index 25fb07858c60..9831ad4040f0 100644 --- a/core/src/main/java/org/infinispan/encoding/DataConversion.java +++ b/core/src/main/java/org/infinispan/encoding/DataConversion.java @@ -170,7 +170,9 @@ private void lookupTranscoder(EncoderRegistry encoderRegistry) { } } if (directTranscoder != null) { - encoder = IdentityEncoder.INSTANCE; + if (encoder.getStorageFormat().equals(MediaType.APPLICATION_OBJECT)) { + encoder = IdentityEncoder.INSTANCE; + } transcoder = directTranscoder; } else { transcoder = encoderRegistry.getTranscoder(requestMediaType, storageMediaType); diff --git a/integrationtests/compatibility-mode-it/pom.xml b/integrationtests/compatibility-mode-it/pom.xml index c13d8b037ef1..6a271ee1641a 100644 --- a/integrationtests/compatibility-mode-it/pom.xml +++ b/integrationtests/compatibility-mode-it/pom.xml @@ -97,6 +97,11 @@ testng test + + org.codehaus.jackson + jackson-mapper-asl + test + \ No newline at end of file diff --git a/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/BaseJsonTest.java b/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/BaseJsonTest.java index 45b5ef33115b..6bc5cbe1e783 100644 --- a/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/BaseJsonTest.java +++ b/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/BaseJsonTest.java @@ -3,6 +3,7 @@ import static org.infinispan.client.hotrod.test.HotRodClientTestingUtil.killServers; import static org.infinispan.client.hotrod.test.HotRodClientTestingUtil.startHotRodServer; +import static org.infinispan.rest.JSONConstants.TYPE; import static org.infinispan.server.core.test.ServerTestingUtil.findFreePort; import static org.infinispan.test.TestingUtil.killCacheManagers; import static org.testng.Assert.assertEquals; @@ -20,6 +21,7 @@ import org.apache.commons.httpclient.methods.StringRequestEntity; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.node.ObjectNode; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.client.hotrod.RemoteCacheManager; import org.infinispan.client.hotrod.Search; @@ -43,11 +45,12 @@ @Test(groups = "functional") public abstract class BaseJsonTest extends AbstractInfinispanTest { - protected RestServer restServer; - protected HttpClient restClient; + RestServer restServer; + HttpClient restClient; private EmbeddedCacheManager cacheManager; private RemoteCacheManager remoteCacheManager; private RemoteCache remoteCache; + private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String CACHE_NAME = "indexed"; @@ -85,8 +88,11 @@ protected void setup() throws Exception { private void writeCurrencyViaJson(String key, String description, int rank) throws IOException { EntityEnclosingMethod put = new PutMethod(restEndpoint + "/" + key); - String json = String.format("{\"_type\":\"%s\",\"description\":\"%s\",\"rank\":%d}", getEntityName(), description, rank); - put.setRequestEntity(new StringRequestEntity(json, "application/json", "UTF-8")); + ObjectNode currency = MAPPER.createObjectNode(); + currency.put(TYPE, getEntityName()); + currency.put("description", description); + currency.put("rank", rank); + put.setRequestEntity(new StringRequestEntity(currency.toString(), "application/json", "UTF-8")); restClient.executeMethod(put); System.out.println(put.getResponseBodyAsString()); diff --git a/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/EmbeddedRestHotRodTest.java b/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/EmbeddedRestHotRodTest.java index 0279d55e2c42..6f5c3bc62024 100644 --- a/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/EmbeddedRestHotRodTest.java +++ b/integrationtests/compatibility-mode-it/src/test/java/org/infinispan/it/compatibility/EmbeddedRestHotRodTest.java @@ -1,5 +1,6 @@ package org.infinispan.it.compatibility; +import static org.infinispan.rest.JSONConstants.TYPE; import static org.testng.AssertJUnit.assertArrayEquals; import static org.testng.AssertJUnit.assertEquals; import static org.testng.AssertJUnit.assertNotNull; @@ -28,6 +29,8 @@ import org.apache.commons.httpclient.methods.HeadMethod; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.PutMethod; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.node.ObjectNode; import org.infinispan.client.hotrod.Flag; import org.infinispan.client.hotrod.RemoteCache; import org.infinispan.commons.dataconversion.IdentityEncoder; @@ -49,6 +52,8 @@ public class EmbeddedRestHotRodTest extends AbstractInfinispanTest { private static final DateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + private static final ObjectMapper MAPPER = new ObjectMapper(); + CompatibilityCacheFactory cacheFactory; @BeforeClass @@ -170,7 +175,7 @@ public void testCustomObjectEmbeddedPutRestGetAcceptJSONAndXML() throws Exceptio getJson.setRequestHeader("Accept", "application/json"); cacheFactory.getRestClient().executeMethod(getJson); assertEquals(getJson.getStatusText(), HttpStatus.SC_OK, getJson.getStatusCode()); - assertEquals("{\"_type\":\"" + Person.class.getName() + "\",\"name\":\"Anna\"}", getJson.getResponseBodyAsString()); + assertEquals(asJson(p), getJson.getResponseBodyAsString()); // 3. Get with REST (accept application/xml) HttpMethod getXml = new GetMethod(cacheFactory.getRestUrl() + "/" + key); @@ -193,7 +198,7 @@ public void testCustomObjectHotRodPutRestGetAcceptJSONAndXML() throws Exception getJson.setRequestHeader("Accept", "application/json"); cacheFactory.getRestClient().executeMethod(getJson); assertEquals(getJson.getStatusText(), HttpStatus.SC_OK, getJson.getStatusCode()); - assertEquals("{\"_type\":\"" + Person.class.getName() + "\",\"name\":\"Jakub\"}", getJson.getResponseBodyAsString()); + assertEquals(asJson(p), getJson.getResponseBodyAsString()); // 3. Get with REST (accept application/xml) HttpMethod getXml = new GetMethod(cacheFactory.getRestUrl() + "/" + key); @@ -397,6 +402,13 @@ public void testHotRodEmbeddedPutRestGetCacheControlHeader() throws Exception { assertEquals("v2", getKey2.getResponseBodyAsString()); } + private String asJson(Person p) { + ObjectNode person = MAPPER.createObjectNode(); + person.put(TYPE, p.getClass().getName()); + person.put("name", p.name); + return person.toString(); + } + /** * The class needs a getter for the attribute "name" so that it can be converted to JSON format * internally by the REST server. diff --git a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/AbstractCompatRemoteQueryManager.java b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/AbstractCompatRemoteQueryManager.java index e47c5c9eb78d..c91611bddf51 100644 --- a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/AbstractCompatRemoteQueryManager.java +++ b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/AbstractCompatRemoteQueryManager.java @@ -6,6 +6,7 @@ import org.infinispan.commons.dataconversion.Encoder; import org.infinispan.configuration.cache.Configuration; import org.infinispan.factories.ComponentRegistry; +import org.infinispan.marshall.core.EncoderRegistry; import org.infinispan.objectfilter.Matcher; import org.infinispan.objectfilter.impl.syntax.parser.EntityNameResolver; import org.infinispan.protostream.SerializationContext; @@ -26,8 +27,10 @@ abstract class AbstractCompatRemoteQueryManager implements RemoteQueryManager { protected final SerializationContext ctx; protected final boolean isIndexed; + final EncoderRegistry encoderRegistry; AbstractCompatRemoteQueryManager(ComponentRegistry cr) { + this.encoderRegistry = cr.getGlobalComponentRegistry().getComponent(EncoderRegistry.class); SearchIntegrator searchIntegrator = cr.getComponent(SearchIntegrator.class); AdvancedCache cache = cr.getComponent(Cache.class).getAdvancedCache(); Configuration cfg = cr.getComponent(Configuration.class); diff --git a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/GenericCompatRemoteQueryManager.java b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/GenericCompatRemoteQueryManager.java index 7e2ba9ba5f21..bde2f86d6eb4 100644 --- a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/GenericCompatRemoteQueryManager.java +++ b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/GenericCompatRemoteQueryManager.java @@ -1,8 +1,14 @@ package org.infinispan.query.remote.impl; +import static java.util.stream.Collectors.toList; +import static org.infinispan.commons.dataconversion.MediaType.APPLICATION_JSON; +import static org.infinispan.commons.dataconversion.MediaType.APPLICATION_OBJECT; + +import java.util.List; import java.util.Set; import org.hibernate.search.spi.SearchIntegrator; +import org.infinispan.commons.dataconversion.Transcoder; import org.infinispan.factories.ComponentRegistry; import org.infinispan.objectfilter.impl.syntax.parser.EntityNameResolver; import org.infinispan.objectfilter.impl.syntax.parser.ReflectionEntityNamesResolver; @@ -10,6 +16,7 @@ import org.infinispan.query.backend.QueryInterceptor; import org.infinispan.query.remote.client.QueryRequest; import org.infinispan.query.remote.client.QueryResponse; +import org.infinispan.query.remote.impl.util.LazyRef; /** * Handle remote queries with deserialized object storage using the configured compat mode marshaller. @@ -18,6 +25,9 @@ */ class GenericCompatRemoteQueryManager extends AbstractCompatRemoteQueryManager { + private LazyRef transcoder = + new LazyRef<>(() -> encoderRegistry.getTranscoder(APPLICATION_OBJECT, APPLICATION_JSON)); + GenericCompatRemoteQueryManager(ComponentRegistry cr) { super(cr); } @@ -55,6 +65,12 @@ public QueryRequest decodeQueryRequest(byte[] queryRequest) { return (QueryRequest) getValueEncoder().toStorage(queryRequest); } + @Override + public List encodeQueryResults(List results) { + return results.stream() + .map(o -> transcoder.get().transcode(o, APPLICATION_OBJECT, APPLICATION_JSON)).collect(toList()); + } + @Override public byte[] encodeQueryResponse(QueryResponse queryResponse) { Object o = this.getValueEncoder().fromStorage(queryResponse); diff --git a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/ProtostreamCompatRemoteQueryManager.java b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/ProtostreamCompatRemoteQueryManager.java index d57d7f2443bd..0f6a54838529 100644 --- a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/ProtostreamCompatRemoteQueryManager.java +++ b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/ProtostreamCompatRemoteQueryManager.java @@ -1,15 +1,22 @@ package org.infinispan.query.remote.impl; +import static java.util.stream.Collectors.toList; +import static org.infinispan.commons.dataconversion.MediaType.APPLICATION_JSON; +import static org.infinispan.commons.dataconversion.MediaType.APPLICATION_OBJECT; + import java.io.IOException; +import java.util.List; import org.hibernate.search.spi.SearchIntegrator; import org.infinispan.commons.CacheException; +import org.infinispan.commons.dataconversion.Transcoder; import org.infinispan.factories.ComponentRegistry; import org.infinispan.objectfilter.impl.syntax.parser.EntityNameResolver; import org.infinispan.protostream.ProtobufUtil; import org.infinispan.protostream.SerializationContext; import org.infinispan.query.remote.client.QueryRequest; import org.infinispan.query.remote.client.QueryResponse; +import org.infinispan.query.remote.impl.util.LazyRef; /** * Handle remote queries with deserialized object storage using the protostream marshaller. @@ -18,6 +25,9 @@ */ class ProtostreamCompatRemoteQueryManager extends AbstractCompatRemoteQueryManager { + private LazyRef transcoder = + new LazyRef<>(() -> encoderRegistry.getTranscoder(APPLICATION_OBJECT, APPLICATION_JSON)); + ProtostreamCompatRemoteQueryManager(ComponentRegistry cr) { super(cr); } @@ -45,6 +55,12 @@ public QueryRequest decodeQueryRequest(byte[] queryRequest) { } } + @Override + public List encodeQueryResults(List results) { + return results.stream() + .map(o -> transcoder.get().transcode(o, APPLICATION_OBJECT, APPLICATION_JSON)).collect(toList()); + } + @Override public byte[] encodeQueryResponse(QueryResponse queryResponse) { try { diff --git a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/RemoteQueryResult.java b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/RemoteQueryResult.java index a46cb2580265..17479bcee21f 100644 --- a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/RemoteQueryResult.java +++ b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/RemoteQueryResult.java @@ -7,7 +7,7 @@ public class RemoteQueryResult { private final int totalResults; private final List results; - public RemoteQueryResult(String[] projections, int totalResults, List results) { + RemoteQueryResult(String[] projections, int totalResults, List results) { this.projections = projections; this.totalResults = totalResults; this.results = results; diff --git a/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/util/LazyRef.java b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/util/LazyRef.java new file mode 100644 index 000000000000..bfb7b9929897 --- /dev/null +++ b/remote-query/remote-query-server/src/main/java/org/infinispan/query/remote/impl/util/LazyRef.java @@ -0,0 +1,30 @@ +package org.infinispan.query.remote.impl.util; + +import java.util.function.Supplier; + +/** + * @since 9.2 + */ +public class LazyRef implements Supplier { + + private final Supplier supplier; + private R supplied; + private volatile boolean available; + + public LazyRef(Supplier supplier) { + this.supplier = supplier; + } + + @Override + public R get() { + if (!available) { + synchronized (this) { + if (!available) { + supplied = supplier.get(); + available = true; + } + } + } + return supplied; + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/Http20RequestHandler.java b/server/rest/src/main/java/org/infinispan/rest/Http20RequestHandler.java index 55496e3dc878..0c2e12716e0c 100644 --- a/server/rest/src/main/java/org/infinispan/rest/Http20RequestHandler.java +++ b/server/rest/src/main/java/org/infinispan/rest/Http20RequestHandler.java @@ -8,7 +8,6 @@ import org.infinispan.rest.context.WrongContextException; import org.infinispan.rest.logging.Log; import org.infinispan.rest.logging.RestAccessLoggingHandler; -import org.infinispan.rest.operations.CacheOperations; import org.infinispan.util.logging.LogFactory; import io.netty.channel.ChannelFutureListener; @@ -28,7 +27,6 @@ public class Http20RequestHandler extends SimpleChannelInboundHandler key; + private final CacheOperations cacheOperations; - InfinispanCacheAPIRequest(FullHttpRequest request, ChannelHandlerContext ctx, Optional cacheName, Optional key, String context) { - super(request, ctx, cacheName.orElse(null), context); + InfinispanCacheAPIRequest(CacheOperations operations, FullHttpRequest request, ChannelHandlerContext ctx, Optional cacheName, Optional key, String context, Map> parameters) { + super(request, ctx, cacheName.orElse(null), context, parameters); + this.cacheOperations = operations; this.key = key; } @@ -35,7 +38,7 @@ public Optional getKey() { } @Override - protected InfinispanResponse execute(CacheOperations cacheOperations) { + protected InfinispanResponse execute() { InfinispanResponse response = InfinispanErrorResponse.asError(this, NOT_IMPLEMENTED, null); if (request.method() == HttpMethod.GET) { @@ -73,7 +76,7 @@ public Optional getTimeToLiveSeconds() { if (timeToLiveSeconds != null) { try { return Optional.of(Long.valueOf(timeToLiveSeconds)); - } catch (NumberFormatException nfe) { + } catch (NumberFormatException ignored) { } } return Optional.empty(); @@ -89,7 +92,7 @@ public Optional getMaxIdleTimeSeconds() { if (maxIdleTimeSeconds != null) { try { return Optional.of(Long.valueOf(maxIdleTimeSeconds)); - } catch (NumberFormatException nfe) { + } catch (NumberFormatException ignored) { } } return Optional.empty(); @@ -145,7 +148,7 @@ public Optional getCacheControl() { * @return true if client wishes to return 'Extended Headers'. */ public Optional getExtended() { - List extendedParameters = queryStringDecoder.parameters().get("extended"); + List extendedParameters = parameters.get("extended"); if (extendedParameters != null && extendedParameters.size() > 0) { return Optional.ofNullable(extendedParameters.get(0)); } diff --git a/server/rest/src/main/java/org/infinispan/rest/InfinispanErrorResponse.java b/server/rest/src/main/java/org/infinispan/rest/InfinispanErrorResponse.java index a0afd441210f..13d2087b5fa1 100644 --- a/server/rest/src/main/java/org/infinispan/rest/InfinispanErrorResponse.java +++ b/server/rest/src/main/java/org/infinispan/rest/InfinispanErrorResponse.java @@ -9,12 +9,12 @@ */ public class InfinispanErrorResponse extends InfinispanResponse { - protected InfinispanErrorResponse(Optional request) { + private InfinispanErrorResponse(Optional request) { super(request); } public static InfinispanErrorResponse asError(InfinispanRequest request, HttpResponseStatus status, String description) { - InfinispanErrorResponse infinispanResponse = new InfinispanErrorResponse(Optional.of(request)); + InfinispanErrorResponse infinispanResponse = new InfinispanErrorResponse(Optional.ofNullable(request)); infinispanResponse.status(status); if (description != null) { infinispanResponse.contentAsText(description); diff --git a/server/rest/src/main/java/org/infinispan/rest/InfinispanRequest.java b/server/rest/src/main/java/org/infinispan/rest/InfinispanRequest.java index 1f91986ecc0a..9154a5aec592 100644 --- a/server/rest/src/main/java/org/infinispan/rest/InfinispanRequest.java +++ b/server/rest/src/main/java/org/infinispan/rest/InfinispanRequest.java @@ -1,14 +1,13 @@ package org.infinispan.rest; +import java.util.List; +import java.util.Map; import java.util.Optional; -import org.infinispan.rest.operations.CacheOperations; - import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.QueryStringDecoder; import io.netty.handler.codec.http2.HttpConversionUtil; /** @@ -23,18 +22,18 @@ public abstract class InfinispanRequest { private final ChannelHandlerContext nettyChannelContext; private final String cacheName; private final String context; - final QueryStringDecoder queryStringDecoder; + protected Map> parameters; - protected InfinispanRequest(FullHttpRequest request, ChannelHandlerContext ctx, String cacheName, String context) { + protected InfinispanRequest(FullHttpRequest request, ChannelHandlerContext ctx, String cacheName, String context, Map> parameters) { this.request = request; - this.queryStringDecoder = new QueryStringDecoder(request.uri()); this.streamId = Optional.ofNullable(request.headers().get(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text())); this.nettyChannelContext = ctx; this.cacheName = cacheName; this.context = context; + this.parameters = parameters; } - protected abstract InfinispanResponse execute(CacheOperations cacheOperations); + protected abstract InfinispanResponse execute(); /** * @return cache name. @@ -46,7 +45,7 @@ public Optional getCacheName() { /*** * @return HTTP/2.0 Stream Id. */ - public Optional getStreamId() { + Optional getStreamId() { return streamId; } @@ -119,4 +118,10 @@ public Optional data() { } return Optional.empty(); } + + protected String getParameterValue(String name) { + List values = parameters.get(name); + if(values == null) return null; + return values.iterator().next(); + } } diff --git a/server/rest/src/main/java/org/infinispan/rest/InfinispanRequestCreator.java b/server/rest/src/main/java/org/infinispan/rest/InfinispanRequestCreator.java deleted file mode 100644 index b68240567095..000000000000 --- a/server/rest/src/main/java/org/infinispan/rest/InfinispanRequestCreator.java +++ /dev/null @@ -1,38 +0,0 @@ -package org.infinispan.rest; - -import java.util.Optional; -import java.util.StringTokenizer; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.QueryStringDecoder; - -/** - * @since 9.2 - */ -class InfinispanRequestCreator { - - /** - * Creates the appropriate {@link InfinispanRequest} instance based on the raw incoming request. - */ - static InfinispanRequest createRequest(FullHttpRequest request, ChannelHandlerContext ctx) { - QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); - - StringTokenizer pathTokenizer = new StringTokenizer(queryStringDecoder.path(), "/"); - String context = pathTokenizer.nextToken(); - Optional cacheName, key; - if (pathTokenizer.hasMoreTokens()) { - String nextToken = pathTokenizer.nextToken(); - cacheName = Optional.of(nextToken); - } else { - cacheName = Optional.empty(); - } - if (pathTokenizer.hasMoreTokens()) { - String next = pathTokenizer.nextToken(); - key = Optional.of(next); - } else { - key = Optional.empty(); - } - return new InfinispanCacheAPIRequest(request, ctx, cacheName, key, context); - } -} diff --git a/server/rest/src/main/java/org/infinispan/rest/InfinispanRequestFactory.java b/server/rest/src/main/java/org/infinispan/rest/InfinispanRequestFactory.java new file mode 100644 index 000000000000..7e9e4e37f99f --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/InfinispanRequestFactory.java @@ -0,0 +1,77 @@ +package org.infinispan.rest; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.infinispan.rest.operations.exceptions.MalformedRequest; +import org.infinispan.rest.search.InfinispanSearchRequest; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.QueryStringDecoder; + +/** + * @since 9.2 + */ +class InfinispanRequestFactory { + + private static final String SEARCH_ACTION = "search"; + private static final String ACTION_PARAMETER = "action"; + + private InfinispanRequestFactory() { + } + + /** + * Creates the appropriate {@link InfinispanRequest} instance based on the raw incoming request. + */ + static InfinispanRequest createRequest(RestServer restServer, FullHttpRequest request, ChannelHandlerContext ctx) { + QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri()); + Map> parameters = queryStringDecoder.parameters(); + + // Obtain each of the path components excluding the context, e.g. from '/rest/cache/k' obtain ['cache', 'k'] + String[] components = queryStringDecoder.path().substring(1).split("/"); + + String context; + Optional key = Optional.empty(); + Optional cacheName = Optional.empty(); + + List actionParameter = parameters.get(ACTION_PARAMETER); + + if (components.length > 3 || components.length == 0) { + throw new MalformedRequest("Invalid request path"); + } + Iterator pathElements = Arrays.stream(components).iterator(); + context = pathElements.next(); + if (pathElements.hasNext()) { + cacheName = Optional.of(pathElements.next()); + } + if (pathElements.hasNext()) { + key = Optional.of(pathElements.next()); + } + + if (actionParameter == null || actionParameter.isEmpty()) { + // Cache API request + return new InfinispanCacheAPIRequest(restServer.getCacheOperations(), request, ctx, cacheName, key, context, parameters); + } + + if (actionParameter.size() > 1) { + throw new MalformedRequest("The 'action' parameter must contain only one value"); + } + + String action = actionParameter.iterator().next(); + + switch (action) { + case SEARCH_ACTION: + if (!cacheName.isPresent()) { + throw new MalformedRequest("Missing cacheName"); + } + return new InfinispanSearchRequest(restServer.getSearchOperations(), request, ctx, cacheName.get(), context, parameters); + default: + throw new MalformedRequest("Invalid action"); + } + + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/JSONConstants.java b/server/rest/src/main/java/org/infinispan/rest/JSONConstants.java new file mode 100644 index 000000000000..683ef37c7f9a --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/JSONConstants.java @@ -0,0 +1,17 @@ +package org.infinispan.rest; + +/** + * @since 9.2 + */ +public interface JSONConstants { + String ERROR = "error"; + String MESSAGE = "message"; + String CAUSE = "cause"; + String HIT = "hit"; + String HITS = "hits"; + String MAX_RESULTS = "max_results"; + String OFFSET = "offset"; + String QUERY_STRING = "query"; + String TOTAL_RESULTS = "total_results"; + String TYPE = "_type"; +} diff --git a/server/rest/src/main/java/org/infinispan/rest/RestServer.java b/server/rest/src/main/java/org/infinispan/rest/RestServer.java index 1a1e14e05a8f..91f2734aaf4a 100644 --- a/server/rest/src/main/java/org/infinispan/rest/RestServer.java +++ b/server/rest/src/main/java/org/infinispan/rest/RestServer.java @@ -8,6 +8,7 @@ import org.infinispan.rest.cachemanager.RestCacheManager; import org.infinispan.rest.configuration.RestServerConfiguration; import org.infinispan.rest.operations.CacheOperations; +import org.infinispan.rest.operations.SearchOperations; import org.infinispan.server.core.AbstractProtocolServer; import org.infinispan.server.core.transport.NettyInitializers; @@ -25,6 +26,7 @@ public class RestServer extends AbstractProtocolServer private Authenticator authenticator = new VoidAuthenticator(); private CacheOperations cacheOperations; + private SearchOperations searchOperations; public RestServer() { super("REST"); @@ -67,6 +69,10 @@ CacheOperations getCacheOperations() { return cacheOperations; } + SearchOperations getSearchOperations() { + return searchOperations; + } + /** * Sets Authentication mechanism. * @@ -79,6 +85,8 @@ public void setAuthenticator(Authenticator authenticator) { @Override protected void startInternal(RestServerConfiguration configuration, EmbeddedCacheManager cacheManager) { super.startInternal(configuration, cacheManager); - this.cacheOperations = new CacheOperations(configuration, new RestCacheManager<>(cacheManager, this::isCacheIgnored)); + RestCacheManager restCacheManager = new RestCacheManager<>(cacheManager, this::isCacheIgnored); + this.cacheOperations = new CacheOperations(configuration, restCacheManager); + this.searchOperations = new SearchOperations(configuration, restCacheManager); } } diff --git a/server/rest/src/main/java/org/infinispan/rest/dataconversion/JsonObjectTranscoder.java b/server/rest/src/main/java/org/infinispan/rest/dataconversion/JsonObjectTranscoder.java index 0ad16a78f4ae..9776941e3669 100644 --- a/server/rest/src/main/java/org/infinispan/rest/dataconversion/JsonObjectTranscoder.java +++ b/server/rest/src/main/java/org/infinispan/rest/dataconversion/JsonObjectTranscoder.java @@ -1,10 +1,14 @@ package org.infinispan.rest.dataconversion; +import static org.infinispan.rest.JSONConstants.TYPE; + import java.io.IOException; import java.util.HashSet; import java.util.Set; +import org.codehaus.jackson.annotate.JsonTypeInfo; import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.type.JavaType; import org.infinispan.commons.CacheException; import org.infinispan.commons.dataconversion.MediaType; import org.infinispan.commons.dataconversion.Transcoder; @@ -18,7 +22,19 @@ public class JsonObjectTranscoder implements Transcoder { protected final static Log logger = LogFactory.getLog(JsonObjectTranscoder.class, Log.class); - private final ObjectMapper jsonMapper = new ObjectMapper().enableDefaultTypingAsProperty(ObjectMapper.DefaultTyping.NON_FINAL, "_type"); + private final ObjectMapper jsonMapper = new ObjectMapper().setDefaultTyping( + new ObjectMapper.DefaultTypeResolverBuilder(ObjectMapper.DefaultTyping.NON_FINAL) { + { + init(JsonTypeInfo.Id.CLASS, null); + inclusion(JsonTypeInfo.As.PROPERTY); + typeProperty(TYPE); + } + + @Override + public boolean useForType(JavaType t) { + return !t.isContainerType() && super.useForType(t); + } + }); private static final Set supportedTypes = new HashSet<>(); diff --git a/server/rest/src/main/java/org/infinispan/rest/operations/AbstractOperations.java b/server/rest/src/main/java/org/infinispan/rest/operations/AbstractOperations.java new file mode 100644 index 000000000000..db4a08c7df55 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/operations/AbstractOperations.java @@ -0,0 +1,69 @@ +package org.infinispan.rest.operations; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +import org.infinispan.commons.dataconversion.EncodingException; +import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.commons.hash.MurmurHash3; +import org.infinispan.marshall.core.EncoderRegistry; +import org.infinispan.rest.InfinispanRequest; +import org.infinispan.rest.RestResponseException; +import org.infinispan.rest.cachemanager.RestCacheManager; +import org.infinispan.rest.configuration.RestServerConfiguration; +import org.infinispan.rest.operations.exceptions.UnacceptableDataFormatException; + +import io.netty.handler.codec.http.HttpResponseStatus; + +abstract class AbstractOperations { + + static final MurmurHash3 hashFunc = MurmurHash3.getInstance(); + + final RestCacheManager restCacheManager; + final RestServerConfiguration restServerConfiguration; + private final Set supported = new HashSet<>(); + + AbstractOperations(RestServerConfiguration configuration, RestCacheManager cacheManager) { + this.restServerConfiguration = configuration; + this.restCacheManager = cacheManager; + EncoderRegistry encoderRegistry = restCacheManager.encoderRegistry(); + supported.addAll(encoderRegistry.getSupportedMediaTypes()); + } + + RestResponseException createResponseException(Throwable exception) { + Throwable rootCauseException = getRootCauseException(exception); + + return new RestResponseException(HttpResponseStatus.INTERNAL_SERVER_ERROR, rootCauseException.getMessage(), rootCauseException); + } + + private Throwable getRootCauseException(Throwable re) { + if (re == null) return null; + Throwable cause = re.getCause(); + if (cause instanceof RuntimeException) + return getRootCauseException(cause); + else + return re; + } + + MediaType getMediaType(InfinispanRequest request) throws UnacceptableDataFormatException { + Optional maybeContentType = request.getAcceptContentType(); + if (maybeContentType.isPresent()) { + try { + String contents = maybeContentType.get(); + if (contents.equals("*/*")) return null; + for (String content : contents.split(" *, *")) { + MediaType mediaType = MediaType.fromString(content); + if (supported.contains(mediaType.getTypeSubtype())) { + return mediaType; + } + } + throw new UnacceptableDataFormatException(); + } catch (EncodingException e) { + throw new UnacceptableDataFormatException(); + } + } + return null; + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/operations/CacheOperations.java b/server/rest/src/main/java/org/infinispan/rest/operations/CacheOperations.java index 458d9666e5cb..c1afe1c9c977 100644 --- a/server/rest/src/main/java/org/infinispan/rest/operations/CacheOperations.java +++ b/server/rest/src/main/java/org/infinispan/rest/operations/CacheOperations.java @@ -1,20 +1,15 @@ package org.infinispan.rest.operations; import java.util.Date; -import java.util.HashSet; import java.util.Optional; import java.util.OptionalInt; -import java.util.Set; import org.infinispan.AdvancedCache; import org.infinispan.CacheSet; import org.infinispan.commons.CacheException; -import org.infinispan.commons.dataconversion.EncodingException; import org.infinispan.commons.dataconversion.MediaType; -import org.infinispan.commons.hash.MurmurHash3; import org.infinispan.container.entries.CacheEntry; import org.infinispan.container.entries.InternalCacheEntry; -import org.infinispan.marshall.core.EncoderRegistry; import org.infinispan.metadata.Metadata; import org.infinispan.rest.CacheControl; import org.infinispan.rest.InfinispanCacheAPIRequest; @@ -27,7 +22,6 @@ import org.infinispan.rest.configuration.RestServerConfiguration; import org.infinispan.rest.operations.exceptions.NoDataFoundException; import org.infinispan.rest.operations.exceptions.NoKeyException; -import org.infinispan.rest.operations.exceptions.UnacceptableDataFormatException; import org.infinispan.rest.operations.mediatypes.Charset; import org.infinispan.rest.operations.mediatypes.EntrySetFormatter; import org.infinispan.rest.operations.mediatypes.OutputPrinter; @@ -40,14 +34,7 @@ * * @author Sebastian Ɓaskawiec */ -public class CacheOperations { - - private static final MurmurHash3 hashFunc = MurmurHash3.getInstance(); - - private final RestCacheManager restCacheManager; - private final RestServerConfiguration restServerConfiguration; - private final EncoderRegistry encoderRegistry; - private final Set supported = new HashSet<>(); +public class CacheOperations extends AbstractOperations { /** * Creates new instance of {@link CacheOperations}. @@ -56,10 +43,7 @@ public class CacheOperations { * @param cacheManager Embedded Cache Manager for storing data. */ public CacheOperations(RestServerConfiguration configuration, RestCacheManager cacheManager) { - this.restServerConfiguration = configuration; - this.restCacheManager = cacheManager; - this.encoderRegistry = restCacheManager.encoderRegistry(); - supported.addAll(encoderRegistry.getSupportedMediaTypes()); + super(configuration, cacheManager); } /** @@ -72,8 +56,7 @@ public CacheOperations(RestServerConfiguration configuration, RestCacheManager cache = restCacheManager.getCache(cacheName, contentType); CacheSet keys = cache.keySet(); MediaType mediaType = getMediaType(request); @@ -165,12 +148,6 @@ public InfinispanResponse getCacheValue(InfinispanCacheAPIRequest request) throw } } - private RestResponseException createResponseException(CacheException cacheException) { - Throwable rootCauseException = getRootCauseException(cacheException); - - return new RestResponseException(HttpResponseStatus.INTERNAL_SERVER_ERROR, rootCauseException.getMessage(), rootCauseException); - } - private void writeValue(Object value, MediaType requested, MediaType configuredMediaType, InfinispanResponse response, boolean returnBody) { String returnType; if (value instanceof byte[]) { @@ -189,39 +166,6 @@ private void writeValue(Object value, MediaType requested, MediaType configuredM } } - private Throwable getRootCauseException(Throwable re) { - if (re == null) return null; - Throwable cause = re.getCause(); - if (cause instanceof RuntimeException) - return getRootCauseException(cause); - else - return re; - } - - private MediaType getMediaType(InfinispanRequest request) throws UnacceptableDataFormatException { - Optional maybeContentType = request.getAcceptContentType(); - if (maybeContentType.isPresent()) { - try { - String contents = maybeContentType.get(); - if (contents.equals("*/*")) return null; - for (String content : contents.split(" *, *")) { - MediaType mediaType = MediaType.fromString(content); - if (supported.contains(mediaType.getTypeSubtype())) { - return mediaType; - } - } - throw new UnacceptableDataFormatException(); - } catch (EncodingException e) { - throw new UnacceptableDataFormatException(); - } - } - return null; - } - - private Object transcode(Object content, MediaType from, MediaType to) { - return encoderRegistry.getTranscoder(from, to).transcode(content, from, to); - } - /** * Implementation of HTTP DELETE request invoked with a key. * @@ -330,8 +274,8 @@ public InfinispanResponse putValueToCache(InfinispanCacheAPIRequest request) thr Optional idle = request.getMaxIdleTimeSeconds(); return putInCache(response, useAsync, cache, key, data, ttl, idle, oldData); } - } catch (CacheException cacheException) { - throw createResponseException(cacheException); + } catch (CacheException | IllegalStateException e) { + throw createResponseException(e); } } diff --git a/server/rest/src/main/java/org/infinispan/rest/operations/SearchOperations.java b/server/rest/src/main/java/org/infinispan/rest/operations/SearchOperations.java new file mode 100644 index 000000000000..c16782d0c6b4 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/operations/SearchOperations.java @@ -0,0 +1,57 @@ +package org.infinispan.rest.operations; + +import java.util.List; +import java.util.stream.Collectors; + +import org.infinispan.AdvancedCache; +import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.objectfilter.ParsingException; +import org.infinispan.query.remote.impl.RemoteQueryManager; +import org.infinispan.query.remote.impl.RemoteQueryResult; +import org.infinispan.rest.cachemanager.RestCacheManager; +import org.infinispan.rest.configuration.RestServerConfiguration; +import org.infinispan.rest.search.Hit; +import org.infinispan.rest.search.InfinispanSearchRequest; +import org.infinispan.rest.search.InfinispanSearchResponse; +import org.infinispan.rest.search.ProjectedResult; +import org.infinispan.rest.search.QueryRequest; +import org.infinispan.rest.search.QueryResult; + +/** + * Handle search related operations via Rest. + * + * @since 9.2 + */ +public class SearchOperations extends AbstractOperations { + + public SearchOperations(RestServerConfiguration configuration, RestCacheManager cacheManager) { + super(configuration, cacheManager); + } + + public InfinispanSearchResponse search(String cacheName, QueryRequest query, InfinispanSearchRequest request) { + InfinispanSearchResponse searchResponse = InfinispanSearchResponse.inReplyTo(request); + AdvancedCache cache = restCacheManager.getCache(cacheName, MediaType.APPLICATION_JSON); + String queryString = query.getQuery(); + try { + RemoteQueryManager remoteQueryManager = cache.getComponentRegistry().getComponent(RemoteQueryManager.class); + RemoteQueryResult remoteQueryResult = remoteQueryManager.executeQuery(queryString, query.getStartOffset(), query.getMaxResults()); + int totalResults = remoteQueryResult.getTotalResults(); + List results = remoteQueryResult.getResults(); + String[] projections = remoteQueryResult.getProjections(); + if (projections == null) { + List hits = results.stream().map(Hit::new).collect(Collectors.toList()); + QueryResult queryResult = new QueryResult(hits, totalResults); + searchResponse.setQueryResult(queryResult); + return searchResponse; + } else { + ProjectedResult projectedResult = new ProjectedResult(totalResults, projections, results); + searchResponse.setQueryResult(projectedResult); + return searchResponse; + } + } catch (IllegalArgumentException | ParsingException | IllegalStateException e) { + return InfinispanSearchResponse.badRequest(request, "Error executing query", e.getMessage()); + } + + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/operations/exceptions/MalformedRequest.java b/server/rest/src/main/java/org/infinispan/rest/operations/exceptions/MalformedRequest.java new file mode 100644 index 000000000000..c36030880479 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/operations/exceptions/MalformedRequest.java @@ -0,0 +1,15 @@ +package org.infinispan.rest.operations.exceptions; + +import org.infinispan.rest.RestResponseException; + +import io.netty.handler.codec.http.HttpResponseStatus; + +/** + * @since 9.2 + */ +public class MalformedRequest extends RestResponseException { + + public MalformedRequest(String description) { + super(HttpResponseStatus.BAD_REQUEST, description); + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/BaseQueryResult.java b/server/rest/src/main/java/org/infinispan/rest/search/BaseQueryResult.java new file mode 100644 index 000000000000..6ec76c3d23cc --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/BaseQueryResult.java @@ -0,0 +1,27 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.TOTAL_RESULTS; + +import org.codehaus.jackson.annotate.JsonCreator; +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * @since 9.2 + */ + +@SuppressWarnings("unused") +class BaseQueryResult implements QueryResponse { + + private int totalResults; + + @JsonCreator + BaseQueryResult(@JsonProperty(TOTAL_RESULTS) int totalResults) { + this.totalResults = totalResults; + } + + @JsonProperty(TOTAL_RESULTS) + public int getTotalResults() { + return totalResults; + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/Hit.java b/server/rest/src/main/java/org/infinispan/rest/search/Hit.java new file mode 100644 index 000000000000..748f41462170 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/Hit.java @@ -0,0 +1,31 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.HIT; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.codehaus.jackson.annotate.JsonPropertyOrder; +import org.codehaus.jackson.annotate.JsonRawValue; +import org.codehaus.jackson.map.annotate.JsonSerialize; + +/** + * Represents each of the search results. + * + * @since 9.2 + */ +@JsonPropertyOrder({HIT}) +public class Hit { + + private final Object value; + + public Hit(Object value) { + this.value = value; + } + + @JsonProperty(HIT) + @JsonRawValue + @JsonSerialize(using = HitSerializer.class) + public Object getValue() { + return value; + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/HitSerializer.java b/server/rest/src/main/java/org/infinispan/rest/search/HitSerializer.java new file mode 100644 index 000000000000..2d13b772069f --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/HitSerializer.java @@ -0,0 +1,28 @@ +package org.infinispan.rest.search; + +import java.io.IOException; + +import org.codehaus.jackson.JsonGenerator; +import org.codehaus.jackson.JsonParser; +import org.codehaus.jackson.map.JsonSerializer; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializerProvider; + +/** + * @since 9.2 + */ +public class HitSerializer extends JsonSerializer { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + public HitSerializer() { + objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); + } + + @Override + public void serialize(String string, JsonGenerator gen, SerializerProvider provider) throws IOException { + Object json = objectMapper.readValue(string, Object.class); + gen.writeObject(json); + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/InfinispanSearchRequest.java b/server/rest/src/main/java/org/infinispan/rest/search/InfinispanSearchRequest.java new file mode 100644 index 000000000000..1194d754aa86 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/InfinispanSearchRequest.java @@ -0,0 +1,76 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.MAX_RESULTS; +import static org.infinispan.rest.JSONConstants.OFFSET; +import static org.infinispan.rest.JSONConstants.QUERY_STRING; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.codehaus.jackson.map.ObjectMapper; +import org.infinispan.rest.InfinispanRequest; +import org.infinispan.rest.operations.SearchOperations; +import org.infinispan.rest.operations.exceptions.NoCacheFoundException; +import org.infinispan.rest.operations.exceptions.NoDataFoundException; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpMethod; + +/** + * @since 9.2 + */ +public class InfinispanSearchRequest extends InfinispanRequest { + + private final ObjectMapper mapper = new ObjectMapper(); + + private final SearchOperations searchOperations; + + public InfinispanSearchRequest(SearchOperations searchOperations, FullHttpRequest request, ChannelHandlerContext ctx, String cacheName, String context, Map> parameters) { + super(request, ctx, cacheName, context, parameters); + this.searchOperations = searchOperations; + } + + @Override + protected InfinispanSearchResponse execute() { + Optional cacheName = getCacheName(); + if (!cacheName.isPresent()) { + throw new NoCacheFoundException("Cache name must be provided"); + } + try { + QueryRequest queryRequest; + queryRequest = getQueryRequest(); + + String queryString = queryRequest.getQuery(); + if (queryString == null || queryString.isEmpty()) { + return InfinispanSearchResponse.badRequest(this, "Invalid search request, missing 'query' parameter", null); + } + return searchOperations.search(cacheName.get(), queryRequest, this); + } catch (IOException e) { + return InfinispanSearchResponse.badRequest(this, "Invalid search request", e.getMessage()); + } + } + + + private QueryRequest getQueryRequest() throws IOException { + QueryRequest queryRequest = null; + if (request.method() == HttpMethod.GET) { + String queryString = getParameterValue(QUERY_STRING); + String strOffset = getParameterValue(OFFSET); + Integer offset = strOffset != null ? Integer.valueOf(strOffset) : null; + String strMaxResults = getParameterValue(MAX_RESULTS); + Integer maxResults = strMaxResults != null ? Integer.valueOf(strMaxResults) : null; + queryRequest = new QueryRequest(queryString, offset, maxResults); + } else if (request.method() == HttpMethod.POST || request.method() == HttpMethod.PUT) { + queryRequest = getQueryFromJSON(); + } + return queryRequest; + } + + private QueryRequest getQueryFromJSON() throws IOException { + byte[] data = data().orElseThrow(NoDataFoundException::new); + return mapper.readValue(data, QueryRequest.class); + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/InfinispanSearchResponse.java b/server/rest/src/main/java/org/infinispan/rest/search/InfinispanSearchResponse.java new file mode 100644 index 000000000000..f20f9843ad1b --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/InfinispanSearchResponse.java @@ -0,0 +1,52 @@ +package org.infinispan.rest.search; + +import java.io.IOException; +import java.util.Optional; + +import org.codehaus.jackson.JsonParser.Feature; +import org.codehaus.jackson.map.ObjectMapper; +import org.infinispan.commons.CacheException; +import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.rest.InfinispanRequest; +import org.infinispan.rest.InfinispanResponse; + +import io.netty.handler.codec.http.HttpResponseStatus; + +/** + * @since 9.2 + */ +public class InfinispanSearchResponse extends InfinispanResponse { + + private static final ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.registerSubtypes(ProjectedResult.class, QueryResult.class); + mapper.configure(Feature.ALLOW_UNQUOTED_CONTROL_CHARS, true); + } + + private InfinispanSearchResponse(InfinispanRequest request) { + super(Optional.of(request)); + contentType(MediaType.APPLICATION_JSON_TYPE); + } + + public static InfinispanSearchResponse inReplyTo(InfinispanSearchRequest infinispanSearchRequest) { + return new InfinispanSearchResponse(infinispanSearchRequest); + } + + public static InfinispanSearchResponse badRequest(InfinispanSearchRequest infinispanSearchRequest, String message, String cause) { + InfinispanSearchResponse searchResponse = new InfinispanSearchResponse(infinispanSearchRequest); + searchResponse.status(HttpResponseStatus.BAD_REQUEST); + searchResponse.setQueryResult(new QueryErrorResult(message, cause)); + return searchResponse; + } + + public void setQueryResult(QueryResponse queryResult) { + try { + byte[] bytes = mapper.writerWithDefaultPrettyPrinter().writeValueAsBytes(queryResult); + contentAsBytes(bytes); + } catch (IOException e) { + throw new CacheException("Invalid query result"); + } + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/ProjectedResult.java b/server/rest/src/main/java/org/infinispan/rest/search/ProjectedResult.java new file mode 100644 index 000000000000..69fb25d3e5c6 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/ProjectedResult.java @@ -0,0 +1,39 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.HITS; +import static org.infinispan.rest.JSONConstants.TOTAL_RESULTS; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.codehaus.jackson.annotate.JsonPropertyOrder; + +/** + * @since 9.2 + */ +@SuppressWarnings("unused") +@JsonPropertyOrder({TOTAL_RESULTS, HITS}) +public class ProjectedResult extends BaseQueryResult { + + private final List hits; + + public ProjectedResult(int totalResults, String[] projections, List values) { + super(totalResults); + hits = new ArrayList<>(projections.length); + for (Object result1 : values) { + Object[] result = (Object[]) result1; + Map p = new HashMap<>(); + for (int j = 0; j < projections.length; j++) { + p.put(projections[j], result[j]); + } + hits.add(new Projection(p)); + } + } + + public List getHits() { + return hits; + } + +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/Projection.java b/server/rest/src/main/java/org/infinispan/rest/search/Projection.java new file mode 100644 index 000000000000..0a20bcaf6425 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/Projection.java @@ -0,0 +1,24 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.HIT; + +import java.util.Map; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * @since 9.2 + */ +public class Projection { + + @JsonProperty(HIT) + private Map value; + + Projection(Map value) { + this.value = value; + } + + public Map getValue() { + return value; + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/QueryErrorResult.java b/server/rest/src/main/java/org/infinispan/rest/search/QueryErrorResult.java new file mode 100644 index 000000000000..5caea25488d1 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/QueryErrorResult.java @@ -0,0 +1,39 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.CAUSE; +import static org.infinispan.rest.JSONConstants.ERROR; +import static org.infinispan.rest.JSONConstants.MESSAGE; + +import org.codehaus.jackson.annotate.JsonProperty; +import org.codehaus.jackson.annotate.JsonPropertyOrder; +import org.codehaus.jackson.annotate.JsonTypeInfo; +import org.codehaus.jackson.annotate.JsonTypeName; + +@JsonPropertyOrder({MESSAGE, CAUSE}) +@JsonTypeInfo(include = JsonTypeInfo.As.WRAPPER_OBJECT, use = JsonTypeInfo.Id.NAME) +@JsonTypeName(ERROR) +@SuppressWarnings("unused") +public class QueryErrorResult implements QueryResponse { + + @JsonProperty(MESSAGE) + private String message; + + @JsonProperty(CAUSE) + private String cause; + + QueryErrorResult(String message, String cause) { + this.message = message; + this.cause = cause; + } + + public QueryErrorResult() { + } + + public String getMessage() { + return message; + } + + public String getCause() { + return cause; + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/QueryRequest.java b/server/rest/src/main/java/org/infinispan/rest/search/QueryRequest.java new file mode 100644 index 000000000000..c03dbc17e4de --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/QueryRequest.java @@ -0,0 +1,54 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.MAX_RESULTS; +import static org.infinispan.rest.JSONConstants.OFFSET; +import static org.infinispan.rest.JSONConstants.QUERY_STRING; + +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * @since 9.2 + */ +@SuppressWarnings("unused") +public class QueryRequest { + + private static final Integer DEFAULT_OFFSET = 0; + private static final Integer DEFAULT_MAX_RESULTS = 10; + + @JsonProperty(QUERY_STRING) + private final String query; + + @JsonProperty(OFFSET) + private final Integer startOffset; + + @JsonProperty(MAX_RESULTS) + private final Integer maxResults; + + QueryRequest(String query, Integer startOffset, Integer maxResults) { + this.query = query; + this.startOffset = startOffset == null ? DEFAULT_OFFSET : startOffset; + this.maxResults = maxResults == null ? DEFAULT_MAX_RESULTS : maxResults; + } + + private QueryRequest(String query) { + this.query = query; + this.startOffset = DEFAULT_OFFSET; + this.maxResults = DEFAULT_MAX_RESULTS; + } + + private QueryRequest() { + this(""); + } + + public String getQuery() { + return query; + } + + public Integer getStartOffset() { + return startOffset; + } + + public Integer getMaxResults() { + return maxResults; + } +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/QueryResponse.java b/server/rest/src/main/java/org/infinispan/rest/search/QueryResponse.java new file mode 100644 index 000000000000..dc1e442e584c --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/QueryResponse.java @@ -0,0 +1,7 @@ +package org.infinispan.rest.search; + +/** + * @since 9.2 + */ +public interface QueryResponse { +} diff --git a/server/rest/src/main/java/org/infinispan/rest/search/QueryResult.java b/server/rest/src/main/java/org/infinispan/rest/search/QueryResult.java new file mode 100644 index 000000000000..8dcba201dc57 --- /dev/null +++ b/server/rest/src/main/java/org/infinispan/rest/search/QueryResult.java @@ -0,0 +1,28 @@ +package org.infinispan.rest.search; + +import static org.infinispan.rest.JSONConstants.HITS; +import static org.infinispan.rest.JSONConstants.TOTAL_RESULTS; + +import java.util.List; + +import org.codehaus.jackson.annotate.JsonPropertyOrder; + +/** + * @since 9.2 + */ +@SuppressWarnings("unused") +@JsonPropertyOrder({TOTAL_RESULTS, HITS}) +public class QueryResult extends BaseQueryResult { + + private final List hits; + + public QueryResult(List hits, int total) { + super(total); + this.hits = hits; + } + + public List getHits() { + return hits; + } + +} diff --git a/server/rest/src/test/java/org/infinispan/rest/BaseRestOperationsTest.java b/server/rest/src/test/java/org/infinispan/rest/BaseRestOperationsTest.java index 61f68e9f070c..6dbd722b34aa 100644 --- a/server/rest/src/test/java/org/infinispan/rest/BaseRestOperationsTest.java +++ b/server/rest/src/test/java/org/infinispan/rest/BaseRestOperationsTest.java @@ -13,6 +13,7 @@ import org.assertj.core.api.Assertions; import org.eclipse.jetty.client.HttpClient; import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.ByteBufferContentProvider; import org.eclipse.jetty.client.util.BytesContentProvider; import org.eclipse.jetty.client.util.StringContentProvider; @@ -881,4 +882,22 @@ public void testWildcardAccept() throws Exception { } + @Test + public void shouldHandleInvalidPath() throws Exception { + Request browserRequest = client.newRequest(String.format("http://localhost:%d/rest/%s", restServer.getPort(), "asdjsad")) + .header(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") + .method(HttpMethod.GET); + + ContentResponse response = browserRequest.send(); + ResponseAssertion.assertThat(response).isNotFound(); + } + + @Test + public void shouldHandleIncompletePath() throws Exception { + Request req = client.newRequest(String.format("http://localhost:%d/rest/%s?action", restServer.getPort(), "default")) + .method(HttpMethod.GET); + + ContentResponse response = req.send(); + ResponseAssertion.assertThat(response).isBadRequest(); + } } diff --git a/server/rest/src/test/java/org/infinispan/rest/RestOperationsTest.java b/server/rest/src/test/java/org/infinispan/rest/RestOperationsTest.java index 3aa14941df9b..105f7e20a554 100644 --- a/server/rest/src/test/java/org/infinispan/rest/RestOperationsTest.java +++ b/server/rest/src/test/java/org/infinispan/rest/RestOperationsTest.java @@ -1,5 +1,7 @@ package org.infinispan.rest; +import static org.infinispan.rest.JSONConstants.TYPE; + import org.eclipse.jetty.client.api.ContentResponse; import org.eclipse.jetty.http.HttpHeader; import org.infinispan.Cache; @@ -43,7 +45,7 @@ public void shouldConvertExistingSerializableObjectToJson() throws Exception { System.out.println(response.getContentAsString()); ResponseAssertion.assertThat(response).isOk(); ResponseAssertion.assertThat(response).hasContentType("application/json"); - ResponseAssertion.assertThat(response).hasReturnedText("{\"_type\":\"" + TestClass.class.getName() + "\",\"name\":\"test\"}"); + ResponseAssertion.assertThat(response).hasReturnedText("{\"" + TYPE + "\":\"" + TestClass.class.getName() + "\",\"name\":\"test\"}"); } @Test @@ -138,7 +140,7 @@ public void shouldReadAsJsonWithCompatMode() throws Exception { //then ResponseAssertion.assertThat(response).isOk(); ResponseAssertion.assertThat(response).hasContentType(MediaType.APPLICATION_JSON_TYPE); - ResponseAssertion.assertThat(response).hasReturnedText("{\"_type\":\"org.infinispan.rest.TestClass\",\"name\":\"test\"}"); + ResponseAssertion.assertThat(response).hasReturnedText("{\"" + TYPE + "\":\"org.infinispan.rest.TestClass\",\"name\":\"test\"}"); } @Test diff --git a/server/rest/src/test/java/org/infinispan/rest/assertion/ResponseAssertion.java b/server/rest/src/test/java/org/infinispan/rest/assertion/ResponseAssertion.java index 9e896206bf07..52ef11992374 100644 --- a/server/rest/src/test/java/org/infinispan/rest/assertion/ResponseAssertion.java +++ b/server/rest/src/test/java/org/infinispan/rest/assertion/ResponseAssertion.java @@ -114,6 +114,11 @@ public ResponseAssertion isNotAcceptable() { return this; } + public ResponseAssertion isBadRequest() { + Assertions.assertThat(response.getStatus()).isEqualTo(400); + return this; + } + public ResponseAssertion hasNoCharset() { Assertions.assertThat(response.getHeaders().get("Content-Type")).doesNotContain("charset"); return this; diff --git a/server/rest/src/test/java/org/infinispan/rest/dataconversion/JsonObjectTranscoderTest.java b/server/rest/src/test/java/org/infinispan/rest/dataconversion/JsonObjectTranscoderTest.java index ca21deaf9781..055fefc0f81a 100644 --- a/server/rest/src/test/java/org/infinispan/rest/dataconversion/JsonObjectTranscoderTest.java +++ b/server/rest/src/test/java/org/infinispan/rest/dataconversion/JsonObjectTranscoderTest.java @@ -1,8 +1,10 @@ package org.infinispan.rest.dataconversion; +import static org.infinispan.rest.JSONConstants.TYPE; import static org.testng.Assert.assertEquals; import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.test.data.Address; import org.infinispan.test.data.Person; import org.testng.annotations.Test; @@ -15,6 +17,9 @@ public class JsonObjectTranscoderTest { @Test public void testJsonObjectTranscoder() throws Exception { Person joe = new Person("joe"); + Address address = new Address(); + address.setCity("London"); + joe.setAddress(address); JsonObjectTranscoder transcoder = new JsonObjectTranscoder(); @@ -23,7 +28,13 @@ public void testJsonObjectTranscoder() throws Exception { Object result = transcoder.transcode(joe, personMediaType, jsonMediaType); - assertEquals(result, "{\"_type\":\"org.infinispan.test.data.Person\",\"name\":\"joe\",\"address\":null}"); + assertEquals(result, + String.format("{\"" + TYPE + "\":\"%s\",\"name\":\"%s\",\"address\":{\"" + TYPE + "\":\"%s\",\"street\":null,\"city\":\"%s\",\"zip\":0}}", + Person.class.getName(), + "joe", + Address.class.getName(), + "London") + ); Object fromJson = transcoder.transcode(result, jsonMediaType, personMediaType); diff --git a/server/rest/src/test/java/org/infinispan/rest/search/BaseRestSearchTest.java b/server/rest/src/test/java/org/infinispan/rest/search/BaseRestSearchTest.java new file mode 100644 index 000000000000..e0a99722ad20 --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/BaseRestSearchTest.java @@ -0,0 +1,289 @@ +package org.infinispan.rest.search; + +import static org.eclipse.jetty.http.HttpMethod.GET; +import static org.eclipse.jetty.http.HttpMethod.POST; +import static org.infinispan.query.remote.client.ProtobufMetadataManagerConstants.PROTOBUF_METADATA_CACHE_NAME; +import static org.infinispan.rest.JSONConstants.HIT; +import static org.infinispan.rest.JSONConstants.TOTAL_RESULTS; +import static org.infinispan.rest.JSONConstants.TYPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.net.URLEncoder; +import java.util.List; + +import org.apache.http.HttpStatus; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.node.ArrayNode; +import org.codehaus.jackson.node.ObjectNode; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.util.StringContentProvider; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpMethod; +import org.infinispan.commons.util.Util; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.rest.assertion.ResponseAssertion; +import org.infinispan.rest.helper.RestServerHelper; +import org.infinispan.test.AbstractInfinispanTest; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * Base class for query over Rest tests. + * + * @since 9.2 + */ +@Test(groups = "functional") +public abstract class BaseRestSearchTest extends AbstractInfinispanTest { + + private static final String CACHE_NAME = "search-rest"; + private static final String PROTO_FILE_NAME = "person.proto"; + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private HttpClient client; + private RestServerHelper restServer; + private String searchUrl; + + @DataProvider(name = "HttpMethodProvider") + protected static Object[][] provideCacheMode() { + return new Object[][]{{GET}, {POST}}; + } + + @BeforeClass + public void setUp() throws Exception { + restServer = RestServerHelper.defaultRestServer("default"); + restServer.defineCache(CACHE_NAME, getConfigBuilder()); + restServer.start(); + + client = new HttpClient(); + client.start(); + + searchUrl = String.format("http://localhost:%d/rest/%s?action=search", restServer.getPort(), CACHE_NAME); + + String protoFile = Util.read(IndexedRestSearchTest.class.getClassLoader().getResourceAsStream(PROTO_FILE_NAME)); + registerProtobuf(PROTO_FILE_NAME, protoFile); + populateData(); + } + + @Test(dataProvider = "HttpMethodProvider") + public void shouldReportInvalidQueries(HttpMethod method) throws Exception { + ContentResponse response; + String wrongQuery = "from Whatever"; + if (method == POST) { + response = client + .newRequest(searchUrl) + .method(POST) + .content(new StringContentProvider("{ \"query\": \"" + wrongQuery + "\"}")) + .send(); + } else { + response = client + .newRequest(searchUrl.concat("&query=").concat(URLEncoder.encode(wrongQuery, "UTF-8"))) + .method(GET) + .send(); + } + assertEquals(response.getStatus(), HttpStatus.SC_BAD_REQUEST); + String contentAsString = response.getContentAsString(); + assertTrue(contentAsString.contains("Message descriptor not found") || + contentAsString.contains("Unknown entity")); + } + + @Test(dataProvider = "HttpMethodProvider") + public void shouldReturnEmptyResults(HttpMethod method) throws Exception { + JsonNode query = query("from org.infinispan.rest.search.entity.Person p where p.name = 'nobody'", method); + + assertZeroHits(query); + } + + @Test(dataProvider = "HttpMethodProvider") + public void testSimpleQuery(HttpMethod method) throws Exception { + JsonNode queryResult = query("from org.infinispan.rest.search.entity.Person p where p.surname = 'Cage'", method); + assertEquals(queryResult.get("total_results").getIntValue(), 1); + + ArrayNode hits = ArrayNode.class.cast(queryResult.get("hits")); + assertEquals(hits.size(), 1); + + JsonNode result = hits.iterator().next(); + JsonNode firstHit = result.get(HIT); + assertEquals(firstHit.get("id").getIntValue(), 2); + assertEquals(firstHit.get("name").asText(), "Luke"); + assertEquals(firstHit.get("surname").asText(), "Cage"); + } + + @Test(dataProvider = "HttpMethodProvider") + public void testMultiResultQuery(HttpMethod method) throws Exception { + JsonNode results = query("from org.infinispan.rest.search.entity.Person p where p.gender = 'MALE'", method); + + assertEquals(results.get(TOTAL_RESULTS).getIntValue(), 3); + + ArrayNode hits = ArrayNode.class.cast(results.get("hits")); + assertEquals(hits.size(), 3); + } + + @Test(dataProvider = "HttpMethodProvider") + public void testProjections(HttpMethod method) throws Exception { + JsonNode results = query("Select name, surname from org.infinispan.rest.search.entity.Person", method); + + assertEquals(results.get(TOTAL_RESULTS).getIntValue(), 4); + + List names = results.findValues("name"); + List surnames = results.findValues("surname"); + List streets = results.findValues("street"); + List gender = results.findValues("gender"); + + assertEquals(4, names.size()); + assertEquals(4, surnames.size()); + assertEquals(0, streets.size()); + assertEquals(0, gender.size()); + } + + @Test(dataProvider = "HttpMethodProvider") + public void testGrouping(HttpMethod method) throws Exception { + JsonNode results = query("select p.gender, count(p.name) from org.infinispan.rest.search.entity.Person p group by p.gender order by p.gender", method); + + assertEquals(results.get(TOTAL_RESULTS).getIntValue(), 2); + + ArrayNode hits = ArrayNode.class.cast(results.get("hits")); + + JsonNode males = hits.get(0); + assertEquals(males.path(HIT).path("name").getIntValue(), 3); + + JsonNode females = hits.get(1); + assertEquals(females.path(HIT).path("name").getIntValue(), 1); + } + + @Test(dataProvider = "HttpMethodProvider") + public void testOffSet(HttpMethod method) throws Exception { + String q = "select p.name from org.infinispan.rest.search.entity.Person p order by p.name desc"; + JsonNode results = query(q, method, 2, 2); + + assertEquals(results.get("total_results").getIntValue(), 4); + ArrayNode hits = ArrayNode.class.cast(results.get("hits")); + assertEquals(hits.size(), 2); + + assertEquals(hits.get(0).path(HIT).path("name").asText(), "Jessica"); + assertEquals(hits.get(1).path(HIT).path("name").asText(), "Danny"); + } + + @Test(dataProvider = "HttpMethodProvider") + public void testIncompleteSearch(HttpMethod method) throws Exception { + ContentResponse response = client.newRequest(searchUrl).method(method).send(); + + ResponseAssertion.assertThat(response).isBadRequest(); + String contentAsString = response.getContentAsString(); + JsonNode jsonNode = MAPPER.readTree(contentAsString); + + assertTrue(jsonNode.get("error").path("message").asText().contains("Invalid search request")); + } + + @AfterClass + public void tearDown() throws Exception { + client.stop(); + restServer.stop(); + } + + protected void populateData() throws Exception { + ObjectNode person1 = createPerson(1, "Jessica", "Jones", "46th St", "NY 10036", "FEMALE", 1111, 2222, 3333); + ObjectNode person2 = createPerson(2, "Luke", "Cage", "Malcolm X Boulevard", "NY 11221", "MALE", 4444, 5555); + ObjectNode person3 = createPerson(3, "Matthew", "Murdock", "57th St", "NY 10019", "MALE"); + ObjectNode person4 = createPerson(4, "Danny", "Randy", "Vanderbilt Av.", "NY 10017", "MALE", 2122561084); + + index(1, person1.toString()); + index(2, person2.toString()); + index(3, person3.toString()); + index(4, person4.toString()); + } + + private void index(int id, String person) throws Exception { + ContentResponse response = client + .newRequest(String.format("http://localhost:%d/rest/%s/%d", restServer.getPort(), CACHE_NAME, id)) + .method(POST) + .content(new StringContentProvider(person)) + .header(HttpHeader.CONTENT_TYPE, "application/json") + .send(); + assertEquals(response.getStatus(), HttpStatus.SC_OK); + } + + private ObjectNode createPerson(int id, String name, String surname, String street, String postCode, String gender, int... phoneNumbers) { + ObjectNode person = MAPPER.createObjectNode(); + person.put(TYPE, "org.infinispan.rest.search.entity.Person"); + person.put("id", id); + person.put("name", name); + person.put("surname", surname); + person.put("gender", gender); + + ObjectNode address = person.putObject("address"); + if (needType()) address.put(TYPE, "org.infinispan.rest.search.entity.Address"); + address.put("street", street); + address.put("postCode", postCode); + + ArrayNode numbers = person.putArray("phoneNumbers"); + for (int phone : phoneNumbers) { + ObjectNode number = numbers.addObject(); + if (needType()) number.put(TYPE, "org.infinispan.rest.search.entity.PhoneNumber"); + number.put("number", phone); + } + return person; + } + + protected void registerProtobuf(String protoFileName, String protoFileContents) throws Exception { + String protobufMetadataUrl = getProtobufMetadataUrl(protoFileName); + ContentResponse response = client + .newRequest(protobufMetadataUrl) + .content(new StringContentProvider(protoFileContents)) + .method(POST) + .send(); + assertEquals(response.getStatus(), HttpStatus.SC_OK); + String errorKey = protoFileName.concat(".error"); + + ContentResponse errorCheck = client.newRequest(getProtobufMetadataUrl(errorKey)).method(GET).send(); + + assertEquals(errorCheck.getStatus(), HttpStatus.SC_NOT_FOUND); + } + + private String getProtobufMetadataUrl(String key) { + return String.format("http://localhost:%d/rest/%s/%s", restServer.getPort(), PROTOBUF_METADATA_CACHE_NAME, key); + } + + private void assertZeroHits(JsonNode queryResponse) throws Exception { + ArrayNode hits = ArrayNode.class.cast(queryResponse.get("hits")); + assertEquals(hits.size(), 0); + } + + private JsonNode query(String q, HttpMethod method) throws Exception { + return query(q, method, 0, 10); + } + + private JsonNode query(String q, HttpMethod method, int offset, int maxResults) throws Exception { + Request request; + if (method == POST) { + ObjectNode queryReq = MAPPER.createObjectNode(); + queryReq.put("query", q); + queryReq.put("offset", offset); + queryReq.put("max_results", maxResults); + request = client.newRequest(searchUrl).method(POST).content(new StringContentProvider(queryReq.toString())); + } else { + StringBuilder queryReq = new StringBuilder(searchUrl); + queryReq.append("&query=").append(URLEncoder.encode(q, "UTF-8")); + queryReq.append("&offset=").append(offset); + queryReq.append("&max_results=").append(maxResults); + request = client.newRequest(queryReq.toString()).method(GET); + } + ContentResponse response = request.send(); + String contentAsString = response.getContentAsString(); + System.out.println(contentAsString); + assertEquals(response.getStatus(), HttpStatus.SC_OK); + return MAPPER.readTree(contentAsString); + } + + protected boolean needType() { + return false; + } + + abstract ConfigurationBuilder getConfigBuilder(); + +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/CompatNonIndexedDefaultMarshallerTest.java b/server/rest/src/test/java/org/infinispan/rest/search/CompatNonIndexedDefaultMarshallerTest.java new file mode 100644 index 000000000000..00a8ecfa2b3d --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/CompatNonIndexedDefaultMarshallerTest.java @@ -0,0 +1,31 @@ +package org.infinispan.rest.search; + +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.testng.annotations.Test; + +/** + * Test for search via rest when using compat mode, with the default marshaller. + * + * @since 9.2 + */ +@Test(groups = "functional", testName = "rest.CompatNonIndexedDefaultMarshallerTest") +public class CompatNonIndexedDefaultMarshallerTest extends BaseRestSearchTest { + + @Override + ConfigurationBuilder getConfigBuilder() { + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.compatibility().enable(); + return configurationBuilder; + } + + @Override + protected void registerProtobuf(String protoFileName, String protoFileContents) throws Exception { + // Not needed + } + + @Override + protected boolean needType() { + return true; + } + +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/IndexedRestOffHeapSearch.java b/server/rest/src/test/java/org/infinispan/rest/search/IndexedRestOffHeapSearch.java new file mode 100644 index 000000000000..2d213683225f --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/IndexedRestOffHeapSearch.java @@ -0,0 +1,25 @@ +package org.infinispan.rest.search; + +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.Index; +import org.infinispan.configuration.cache.StorageType; +import org.testng.annotations.Test; + +/** + * Test for indexed search over Rest when using OFF_HEAP. + * + * @since 9.2 + */ +@Test(groups = "functional", testName = "rest.IndexedRestOffHeapSearch") +public class IndexedRestOffHeapSearch extends BaseRestSearchTest { + + @Override + ConfigurationBuilder getConfigBuilder() { + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.indexing() + .index(Index.PRIMARY_OWNER) + .addProperty("default.directory_provider", "ram"); + configurationBuilder.memory().storageType(StorageType.OFF_HEAP); + return configurationBuilder; + } +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/IndexedRestSearchTest.java b/server/rest/src/test/java/org/infinispan/rest/search/IndexedRestSearchTest.java new file mode 100644 index 000000000000..4ba295e18a62 --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/IndexedRestSearchTest.java @@ -0,0 +1,24 @@ +package org.infinispan.rest.search; + +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.Index; +import org.testng.annotations.Test; + +/** + * Tests for search over rest for indexed caches. + * + * @since 9.2 + */ +@Test(groups = "functional", testName = "rest.IndexedRestSearchTest") +public class IndexedRestSearchTest extends BaseRestSearchTest { + + @Override + ConfigurationBuilder getConfigBuilder() { + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.indexing() + .index(Index.PRIMARY_OWNER) + .addProperty("default.directory_provider", "ram"); + return configurationBuilder; + } + +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/NonIndexedRestOffHeapSearch.java b/server/rest/src/test/java/org/infinispan/rest/search/NonIndexedRestOffHeapSearch.java new file mode 100644 index 000000000000..4f5a3b59417a --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/NonIndexedRestOffHeapSearch.java @@ -0,0 +1,22 @@ +package org.infinispan.rest.search; + +import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.StorageType; +import org.testng.annotations.Test; + +/** + * @since 9.2 + */ +@Test(groups = "functional", testName = "rest.NonIndexedRestOffHeapSearch") +public class NonIndexedRestOffHeapSearch extends BaseRestSearchTest { + + @Override + ConfigurationBuilder getConfigBuilder() { + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.encoding().key().mediaType(MediaType.APPLICATION_PROTOSTREAM_TYPE); + configurationBuilder.encoding().value().mediaType(MediaType.APPLICATION_PROTOSTREAM_TYPE); + configurationBuilder.memory().storageType(StorageType.OFF_HEAP); + return configurationBuilder; + } +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/NonIndexedRestSearchTest.java b/server/rest/src/test/java/org/infinispan/rest/search/NonIndexedRestSearchTest.java new file mode 100644 index 000000000000..06db9e837ef2 --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/NonIndexedRestSearchTest.java @@ -0,0 +1,21 @@ +package org.infinispan.rest.search; + +import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.testng.annotations.Test; + +/** + * @since 9.2 + */ +@Test(groups = "functional", testName = "rest.NonIndexedRestSearchTest") +public class NonIndexedRestSearchTest extends BaseRestSearchTest { + + @Override + ConfigurationBuilder getConfigBuilder() { + ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.encoding().key().mediaType(MediaType.APPLICATION_PROTOSTREAM_TYPE); + configurationBuilder.encoding().value().mediaType(MediaType.APPLICATION_PROTOSTREAM_TYPE); + return configurationBuilder; + } + +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/entity/Address.java b/server/rest/src/test/java/org/infinispan/rest/search/entity/Address.java new file mode 100644 index 000000000000..77a3a85aaffe --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/entity/Address.java @@ -0,0 +1,27 @@ +package org.infinispan.rest.search.entity; + +/** + * @since 9.2 + */ +@SuppressWarnings("unused") +public class Address { + + private String street; + private String postCode; + + public String getStreet() { + return street; + } + + public void setStreet(String street) { + this.street = street; + } + + public String getPostCode() { + return postCode; + } + + public void setPostCode(String postCode) { + this.postCode = postCode; + } +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/entity/Gender.java b/server/rest/src/test/java/org/infinispan/rest/search/entity/Gender.java new file mode 100644 index 000000000000..150a470b4ce2 --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/entity/Gender.java @@ -0,0 +1,10 @@ +package org.infinispan.rest.search.entity; + +/** + * @since 9.2 + */ +@SuppressWarnings("unused") +public enum Gender { + MALE, + FEMALE +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/entity/Person.java b/server/rest/src/test/java/org/infinispan/rest/search/entity/Person.java new file mode 100644 index 000000000000..79b4f4548c3e --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/entity/Person.java @@ -0,0 +1,107 @@ +package org.infinispan.rest.search.entity; + +import java.util.Set; + +import org.hibernate.search.annotations.Field; +import org.hibernate.search.annotations.Indexed; +import org.hibernate.search.annotations.IndexedEmbedded; +import org.hibernate.search.annotations.NumericField; + +/** + * @since 9.2 + */ +@Indexed +@SuppressWarnings("unused") +public class Person { + + @Field + private Integer id; + + @Field + private String name; + + @Field + private String surname; + + @Field + @NumericField + private Integer age; + + @Field + private Address address; + + @Field + private Gender gender; + + @IndexedEmbedded + private Set phoneNumbers; + + public Person() { + } + + public Person(Integer id, String name, String surname, Integer age, Address address, Gender gender, Set phoneNumbers) { + this.id = id; + this.name = name; + this.surname = surname; + this.age = age; + this.address = address; + this.gender = gender; + this.phoneNumbers = phoneNumbers; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getSurname() { + return surname; + } + + public void setSurname(String surname) { + this.surname = surname; + } + + public Gender getGender() { + return gender; + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public Address getAddress() { + return address; + } + + public void setAddress(Address address) { + this.address = address; + } + + public Set getPhoneNumbers() { + return phoneNumbers; + } + + public void setPhoneNumbers(Set phoneNumbers) { + this.phoneNumbers = phoneNumbers; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } +} diff --git a/server/rest/src/test/java/org/infinispan/rest/search/entity/PhoneNumber.java b/server/rest/src/test/java/org/infinispan/rest/search/entity/PhoneNumber.java new file mode 100644 index 000000000000..e7511ec8f692 --- /dev/null +++ b/server/rest/src/test/java/org/infinispan/rest/search/entity/PhoneNumber.java @@ -0,0 +1,18 @@ +package org.infinispan.rest.search.entity; + +/** + * @since 9.2 + */ +@SuppressWarnings("unused") +public class PhoneNumber { + + private String number; + + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } +} diff --git a/server/rest/src/test/resources/person.proto b/server/rest/src/test/resources/person.proto new file mode 100644 index 000000000000..7eaaf948e384 --- /dev/null +++ b/server/rest/src/test/resources/person.proto @@ -0,0 +1,25 @@ +package org.infinispan.rest.search.entity; + +message Address { + required string street = 1; + required string postCode = 2; +} + +message PhoneNumber { + required string number = 1; +} + +message Person { + optional int32 id = 1; + required string name = 2; + required string surname = 3; + optional Address address = 4; + repeated PhoneNumber phoneNumbers = 5; + optional uint32 age = 6; + enum Gender { + MALE = 0; + FEMALE = 1; + } + + optional Gender gender = 7; +}