Skip to content

Commit

Permalink
added post method for search and count
Browse files Browse the repository at this point in the history
  • Loading branch information
PJGitLan committed Jul 31, 2023
1 parent 2a45e29 commit d4367d5
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,15 +170,15 @@ public static <T> Flow<T, T, NotUsed> throttleByConfig(final ThrottlingConfig th
* @param ctx the request context.
* @param dittoHeaders the extracted Ditto headers.
* @param payloadSource source of the request body.
* @param requestJsonToCommandFunction function converting a string to a command.
* @param requestStringToCommandFunction function converting a string to a command.
* @return the request handling route.
*/
public Route handlePerRequest(final RequestContext ctx,
final DittoHeaders dittoHeaders,
final Source<ByteString, ?> payloadSource,
final Function<String, Command<?>> requestJsonToCommandFunction) {
final Function<String, Command<?>> requestStringToCommandFunction) {

return handlePerRequest(ctx, dittoHeaders, payloadSource, requestJsonToCommandFunction, null);
return handlePerRequest(ctx, dittoHeaders, payloadSource, requestStringToCommandFunction, null);
}

protected Route handlePerRequest(final RequestContext ctx, final Command<?> command) {
Expand All @@ -195,13 +195,13 @@ protected Route handlePerRequest(final RequestContext ctx, final Command<?> comm
protected Route handlePerRequest(final RequestContext ctx,
final DittoHeaders dittoHeaders,
final Source<ByteString, ?> payloadSource,
final Function<String, Command<?>> requestJsonToCommandFunction,
final Function<String, Command<?>> requestStringToCommandFunction,
@Nullable final BiFunction<JsonValue, HttpResponse, HttpResponse> responseTransformFunction) {

return withCustomRequestTimeout(dittoHeaders.getTimeout().orElse(null),
this::validateCommandTimeout,
timeout -> doHandlePerRequest(ctx, dittoHeaders.toBuilder().timeout(timeout).build(), payloadSource,
requestJsonToCommandFunction, responseTransformFunction));
requestStringToCommandFunction, responseTransformFunction));
}

protected <M> M runWithSupervisionStrategy(final RunnableGraph<M> graph) {
Expand All @@ -211,7 +211,7 @@ protected <M> M runWithSupervisionStrategy(final RunnableGraph<M> graph) {
private Route doHandlePerRequest(final RequestContext ctx,
final DittoHeaders dittoHeaders,
final Source<ByteString, ?> payloadSource,
final Function<String, Command<?>> requestJsonToCommandFunction,
final Function<String, Command<?>> requestStringToCommandFunction,
@Nullable final BiFunction<JsonValue, HttpResponse, HttpResponse> responseValueTransformFunction) {

final CompletableFuture<HttpResponse> httpResponseFuture = new CompletableFuture<>();
Expand All @@ -222,7 +222,7 @@ private Route doHandlePerRequest(final RequestContext ctx,
.map(x -> {
try {
// DON'T replace this try-catch by .recover: The supervising strategy is called before recovery!
final Command<?> command = requestJsonToCommandFunction.apply(x);
final Command<?> command = requestStringToCommandFunction.apply(x);
final JsonSchemaVersion schemaVersion =
dittoHeaders.getSchemaVersion().orElse(command.getImplementedSchemaVersion());
return command.implementsSchemaVersion(schemaVersion) ? command
Expand Down Expand Up @@ -300,6 +300,18 @@ protected ActorRef createHttpPerRequestActor(final RequestContext ctx,
return actorSystem.actorOf(props);
}

protected Route ensureMediaTypeFormUrlEncodedThenExtractData(
final RequestContext ctx,
final DittoHeaders dittoHeaders,
final java.util.function.Function<Source<ByteString, Object>, Route> inner
) {
return ContentTypeValidationDirective.ensureValidContentType(Set.of(MediaTypes.APPLICATION_X_WWW_FORM_URLENCODED.toString()), ctx, dittoHeaders,
() -> {
final var res = extractDataBytes(inner);
return res;
});
}

/**
* Provides a composed directive of {@link AllDirectives#extractDataBytes} and
* {@link org.eclipse.ditto.gateway.service.endpoints.directives.ContentTypeValidationDirective#ensureValidContentType}, where the supported media-types are
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,22 @@
*/
package org.eclipse.ditto.gateway.service.endpoints.routes.thingsearch;

import java.util.Arrays;
import java.util.EnumMap;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.annotation.Nullable;

import akka.http.javadsl.server.Directives;
import akka.http.javadsl.server.PathMatchers;
import akka.http.javadsl.server.RequestContext;
import akka.http.javadsl.server.Route;
import org.eclipse.ditto.base.model.headers.DittoHeaders;
import org.eclipse.ditto.base.model.signals.commands.Command;
import org.eclipse.ditto.gateway.service.endpoints.routes.AbstractRoute;
import org.eclipse.ditto.gateway.service.endpoints.routes.RouteBaseProperties;
import org.eclipse.ditto.thingsearch.model.signals.commands.query.CountThings;
import org.eclipse.ditto.thingsearch.model.signals.commands.query.QueryThings;

import akka.http.javadsl.server.Directives;
import akka.http.javadsl.server.PathMatchers;
import akka.http.javadsl.server.RequestContext;
import akka.http.javadsl.server.Route;
import javax.annotation.Nullable;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Builder for creating Akka HTTP routes for {@code /search/things}.
Expand Down Expand Up @@ -78,11 +74,30 @@ public Route buildSearchRoute(final RequestContext ctx, final DittoHeaders ditto
* @return {@code /search/things/count} route.
*/
private Route countThings(final RequestContext ctx, final DittoHeaders dittoHeaders) {
// GET things/count?filter=<filterString>&namespaces=<namespacesString>
return get(() -> thingSearchParameterOptional(params -> handlePerRequest(ctx,
CountThings.of(calculateFilter(params.get(ThingSearchParameter.FILTER)),
calculateNamespaces(params.get(ThingSearchParameter.NAMESPACES)),
dittoHeaders))));
return concat(
// GET things/count?filter=<filterString>&namespaces=<namespacesString>
get(() -> thingSearchParameterOptional(
params -> handlePerRequest(ctx,
CountThings.of(calculateFilter(params.get(ThingSearchParameter.FILTER)),
calculateNamespaces(params.get(ThingSearchParameter.NAMESPACES)),
dittoHeaders)))),
// POST things/count
post(() -> ensureMediaTypeFormUrlEncodedThenExtractData(
ctx,
dittoHeaders,
payloadSource -> handlePerRequest(
ctx,
dittoHeaders,
payloadSource,
requestString -> getFormParameters(
requestString,
params -> CountThings.of(
calculateFilter(params.get(ThingSearchParameter.FILTER)),
calculateNamespaces(params.get(ThingSearchParameter.NAMESPACES)),
dittoHeaders)
)
)))
);
}

/*
Expand All @@ -91,18 +106,70 @@ private Route countThings(final RequestContext ctx, final DittoHeaders dittoHead
* @return {@code /search/things} route.
*/
private Route searchThings(final RequestContext ctx, final DittoHeaders dittoHeaders) {
// GET things?filter=<filterString>
// &options=<optionsString>
// &fields=<fieldsString>
// &namespaces=<namespacesString>
// &nextPageKey=<nextPageKey>
return get(() -> thingSearchParameterOptional(params -> handlePerRequest(ctx,
QueryThings.of(calculateFilter(params.get(ThingSearchParameter.FILTER)),
calculateOptions(params.get(ThingSearchParameter.OPTION)),
AbstractRoute.calculateSelectedFields(params.get(ThingSearchParameter.FIELDS))
.orElse(null),
calculateNamespaces(params.get(ThingSearchParameter.NAMESPACES)),
dittoHeaders))));
return concat(
// GET /search/things?filter=<filterString>
// &options=<optionsString>
// &fields=<fieldsString>
// &namespaces=<namespacesString>
// &nextPageKey=<nextPageKey>
get(() -> thingSearchParameterOptional(
params -> handlePerRequest(ctx,
QueryThings.of(
calculateFilter(params.get(ThingSearchParameter.FILTER)),
calculateOptions(params.get(ThingSearchParameter.OPTION)),
AbstractRoute.calculateSelectedFields(params.get(ThingSearchParameter.FIELDS))
.orElse(null),
calculateNamespaces(params.get(ThingSearchParameter.NAMESPACES)),
dittoHeaders
)
)
)
),
// POST /search/things
post(() -> ensureMediaTypeFormUrlEncodedThenExtractData(
ctx,
dittoHeaders,
payloadSource -> handlePerRequest(
ctx,
dittoHeaders,
payloadSource,
requestString -> getFormParameters(
requestString,
params -> QueryThings.of(
calculateFilter(params.get(ThingSearchParameter.FILTER)),
calculateOptions(params.get(ThingSearchParameter.OPTION)),
AbstractRoute.calculateSelectedFields(params.get(ThingSearchParameter.FIELDS))
.orElse(null),
calculateNamespaces(params.get(ThingSearchParameter.NAMESPACES)),
dittoHeaders
)
)
)))
);

}

private Command<?> getFormParameters(final String requestString, final Function<EnumMap<ThingSearchParameter, List<String>>, Command<?>> inner) {
return inner.apply(parseFormParameters(requestString));
}

private EnumMap<ThingSearchParameter, List<String>> parseFormParameters(final String requestString){
final var thingSearchParameterMap = new EnumMap<ThingSearchParameter, List<String>>(ThingSearchParameter.class);
final var keyValues = Arrays.stream(requestString.split("&")).map(item -> item.split("=")).toList();

Arrays.stream(ThingSearchParameter.values()).forEach(
thingSearchParameter -> {
final var valueList = new ArrayList<String>();
keyValues.forEach( keyValue -> {
if (Objects.equals(keyValue[0], thingSearchParameter.toString())) {
valueList.add(keyValue[1]);
}
});
thingSearchParameterMap.put(thingSearchParameter, valueList);
}
);

return thingSearchParameterMap;
}

private Route thingSearchParameterOptional(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (c) 2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.ditto.gateway.service.endpoints.routes.thingsearch;

import akka.http.javadsl.model.*;
import akka.http.javadsl.server.Route;
import akka.http.javadsl.testkit.TestRoute;
import akka.japi.Pair;
import org.eclipse.ditto.gateway.service.endpoints.EndpointTestBase;
import org.eclipse.ditto.json.JsonKey;
import org.eclipse.ditto.json.JsonObject;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

import static org.eclipse.ditto.json.assertions.DittoJsonAssertions.assertThat;

/**
* Builder for creating Akka HTTP routes for {@code /search/things}.
*/
public final class ThingSearchRouteTest extends EndpointTestBase {

private ThingSearchRoute thingsSearchRoute;
private TestRoute underTest;


@Before
public void setUp() {
thingsSearchRoute = new ThingSearchRoute(routeBaseProperties);
final Route route = extractRequestContext(ctx -> thingsSearchRoute.buildSearchRoute(ctx, dittoHeaders));
underTest = testRoute(handleExceptions(() -> route));
}

@Test
public void postSearchThingsShouldGetParametersFromBody() {
final var formData = FormData.create(
List.of(
new Pair<>("filter", "and(like(definition,\"*test*\"))"),
new Pair<>("option", "sort(+thingId)"),
new Pair<>("option","limit(0,5)"),
new Pair<>("namespaces","org.eclipse.ditto,foo.bar")
));
final var result = underTest.run(HttpRequest.POST("/search/things")
.withEntity(formData.toEntity()));

result.assertStatusCode(StatusCodes.OK);
result.assertEntity("{\"type\":\"thing-search.commands:queryThings\",\"filter\":\"and(and%28like%28definition%2C%22%2Atest%2A%22%29%29)\",\"options\":[\"sort%28%2BthingId%29\",\"limit%280%2C5%29\"],\"namespaces\":[\"org.eclipse.ditto%2Cfoo.bar\"]}");
}

@Test
public void searchThingsShouldGetFilter() {
final var form = "and(and(like(definition,\"*test*\")))";

final var result = underTest.run(
HttpRequest.GET("/search/things?filter=and(like(definition,\"*test*\"))&option=sort(+thingId)&option=limit(0,5)&namespaces=org.eclipse.ditto,foo.bar"));

result.assertStatusCode(StatusCodes.OK);

assertThat(JsonObject.of(result.entityString()))
.contains(
JsonKey.of("filter"),
form
);
}

@Test
public void countThingsShouldGetFilterFromBody() {
final var formData = FormData.create(
List.of(
new Pair<>("filter", "and(like(definition,\"*test*\"))"),
new Pair<>("option", "sort(+thingId)"),
new Pair<>("option","limit(0,5)"),
new Pair<>("namespaces","org.eclipse.ditto,foo.bar")
));
final var result = underTest.run(HttpRequest.POST("/search/things/count")
.withEntity(formData.toEntity()));

result.assertStatusCode(StatusCodes.OK);
assertThat(JsonObject.of(result.entityString()))
.contains(
JsonKey.of("filter"),
"and(and%28like%28definition%2C%22%2Atest%2A%22%29%29)"
);
}

@Test
public void countThingsShouldAssertBadRequest() {
final var result = underTest.run(HttpRequest.POST("/search/things/count"));
result.assertStatusCode(StatusCodes.UNSUPPORTED_MEDIA_TYPE);
}
}

0 comments on commit d4367d5

Please sign in to comment.