From 06d92dafca62a6b48e74ccf939feeac7189e360f Mon Sep 17 00:00:00 2001 From: Mukundan Sundararajan <65565396+mukundansundar@users.noreply.github.com> Date: Mon, 21 Mar 2022 09:33:11 +0530 Subject: [PATCH] Query State Preview API implementation. (#697) * Query State Preview API implementation. Signed-off-by: Mukundan Sundararajan * Use latest dapr ref and fix grpc query state api Signed-off-by: Mukundan Sundararajan * fix service invocation automatic unesacpe Signed-off-by: Mukundan Sundararajan * add more unit tests Signed-off-by: Mukundan Sundararajan * Add query state API docs Signed-off-by: Mukundan Sundararajan * Fix example to be user friendly Signed-off-by: Mukundan Sundararajan * Fix example in docs Signed-off-by: Mukundan Sundararajan * make pagination immutable Signed-off-by: Mukundan Sundararajan Co-authored-by: Artur Souza --- .github/workflows/build.yml | 8 +- .github/workflows/validate.yml | 14 +- daprdocs/content/en/java-sdk-docs/_index.md | 73 +++++ examples/components/state/mongo.yaml | 14 + examples/pom.xml | 4 +- .../io/dapr/examples/querystate/Listing.java | 98 ++++++ .../examples/querystate/QuerySavedState.java | 114 +++++++ .../io/dapr/examples/querystate/README.md | 279 ++++++++++++++++++ .../java/io/dapr/examples/state/README.md | 19 +- pom.xml | 6 +- sdk-actors/pom.xml | 2 +- sdk-autogen/pom.xml | 2 +- sdk-tests/components/mongo-statestore.yml | 14 + sdk-tests/deploy/local-test-mongo.yml | 6 + sdk-tests/pom.xml | 8 +- .../src/test/java/io/dapr/it/BaseIT.java | 2 + .../dapr/it/state/AbstractStateClientIT.java | 86 ++++++ sdk/pom.xml | 2 +- .../io/dapr/client/AbstractDaprClient.java | 103 ++++++- .../java/io/dapr/client/DaprClientGrpc.java | 112 ++++++- .../java/io/dapr/client/DaprClientHttp.java | 188 +++++++++--- .../java/io/dapr/client/DaprClientProxy.java | 2 + .../main/java/io/dapr/client/DaprHttp.java | 6 +- .../io/dapr/client/DaprPreviewClient.java | 120 ++++++++ .../io/dapr/client/domain/QueryStateItem.java | 167 +++++++++++ .../dapr/client/domain/QueryStateRequest.java | 92 ++++++ .../client/domain/QueryStateResponse.java | 50 ++++ .../dapr/client/domain/query/Pagination.java | 39 +++ .../io/dapr/client/domain/query/Query.java | 75 +++++ .../io/dapr/client/domain/query/Sorting.java | 62 ++++ .../domain/query/filters/AndFilter.java | 68 +++++ .../client/domain/query/filters/EqFilter.java | 62 ++++ .../client/domain/query/filters/Filter.java | 52 ++++ .../client/domain/query/filters/InFilter.java | 81 +++++ .../client/domain/query/filters/OrFilter.java | 70 +++++ .../client/DaprPreviewClientGrpcTest.java | 123 +++++++- .../client/DaprPreviewClientHttpTest.java | 82 ++++- .../client/domain/QueryStateRequestTest.java | 54 ++++ .../dapr/client/domain/query/QueryTest.java | 118 ++++++++ .../domain/query/filters/AndFilterTest.java | 59 ++++ .../domain/query/filters/EqFilterTest.java | 45 +++ .../domain/query/filters/InFilterTest.java | 48 +++ .../domain/query/filters/OrFilterTest.java | 57 ++++ 43 files changed, 2560 insertions(+), 126 deletions(-) create mode 100644 examples/components/state/mongo.yaml create mode 100644 examples/src/main/java/io/dapr/examples/querystate/Listing.java create mode 100644 examples/src/main/java/io/dapr/examples/querystate/QuerySavedState.java create mode 100644 examples/src/main/java/io/dapr/examples/querystate/README.md create mode 100644 sdk-tests/components/mongo-statestore.yml create mode 100644 sdk-tests/deploy/local-test-mongo.yml create mode 100644 sdk/src/main/java/io/dapr/client/domain/QueryStateItem.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/QueryStateRequest.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/QueryStateResponse.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/Pagination.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/Query.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/Sorting.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/filters/AndFilter.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/filters/EqFilter.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/filters/Filter.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/filters/InFilter.java create mode 100644 sdk/src/main/java/io/dapr/client/domain/query/filters/OrFilter.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/QueryStateRequestTest.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/query/QueryTest.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/query/filters/AndFilterTest.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/query/filters/EqFilterTest.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/query/filters/InFilterTest.java create mode 100644 sdk/src/test/java/io/dapr/client/domain/query/filters/OrFilterTest.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6bd1b413..915127e22 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,7 +20,7 @@ jobs: matrix: java: [ 11, 13, 15, 16 ] env: - GOVER: 1.15.0 + GOVER: 1.17.7 GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org @@ -29,7 +29,7 @@ jobs: DAPR_RUNTIME_VER: 1.6.0-rc.2 DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.6.0-rc.1/install/install.sh DAPR_CLI_REF: - DAPR_REF: + DAPR_REF: 5a307f3deaa1b322f7945179adad0403de80eb7e steps: - uses: actions/checkout@v3 - name: Set up OpenJDK ${{ env.JDK_VER }} @@ -91,6 +91,10 @@ jobs: run: | docker-compose -f ./sdk-tests/deploy/local-test-vault.yml up -d docker ps + - name: Install Local mongo database using docker-compose + run: | + docker-compose -f ./sdk-tests/deploy/local-test-mongo.yml up -d + docker ps - name: Clean up files run: mvn clean - name: Build sdk diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index a11ac3565..67f8cd106 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -31,7 +31,7 @@ jobs: matrix: java: [ 11, 13, 15, 16 ] env: - GOVER: 1.15.0 + GOVER: 1.17.7 GOOS: linux GOARCH: amd64 GOPROXY: https://proxy.golang.org @@ -40,7 +40,7 @@ jobs: DAPR_RUNTIME_VER: 1.6.0-rc.2 DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.6.0-rc.1/install/install.sh DAPR_CLI_REF: - DAPR_REF: + DAPR_REF: 5a307f3deaa1b322f7945179adad0403de80eb7e steps: - uses: actions/checkout@v3 - name: Set up OpenJDK ${{ env.JDK_VER }} @@ -108,6 +108,10 @@ jobs: sudo apt-get install vault # Verify vault is installed vault -h + - name: Install Local mongo database using docker-compose + run: | + docker-compose -f ./sdk-tests/deploy/local-test-mongo.yml up -d + docker ps - name: Clean up files run: mvn clean - name: Build sdk @@ -153,4 +157,8 @@ jobs: - name: Validate Configuration API example working-directory: ./examples run: | - mm.py ./src/main/java/io/dapr/examples/configuration/grpc/README.md \ No newline at end of file + mm.py ./src/main/java/io/dapr/examples/configuration/grpc/README.md + - name: Validate query state HTTP example + working-directory: ./examples + run: | + mm.py ./src/main/java/io/dapr/examples/querystate/README.md \ No newline at end of file diff --git a/daprdocs/content/en/java-sdk-docs/_index.md b/daprdocs/content/en/java-sdk-docs/_index.md index ef107020d..84035e782 100644 --- a/daprdocs/content/en/java-sdk-docs/_index.md +++ b/daprdocs/content/en/java-sdk-docs/_index.md @@ -241,6 +241,8 @@ public interface DemoActor { ### Get & Subscribe to application configurations +> Note this is a preview API and thus will only be accessible via the DaprPreviewClient interface and not the normal DaprClient interface + ```java import io.dapr.client.DaprClientBuilder; import io.dapr.client.DaprPreviewClient; @@ -267,5 +269,76 @@ try (DaprPreviewClient client = (new DaprClientBuilder()).buildPreviewClient()) - For a full list of configuration operations visit [How-To: Manage configuration from a store]({{< ref howto-manage-configuration.md >}}). - Visit [Java SDK examples](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/configuration) for code samples and instructions to try out different configuration operations. +### Query saved state + +> Note this is a preview API and thus will only be accessible via the DaprPreviewClient interface and not the normal DaprClient interface + +```java +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.client.DaprPreviewClient; +import io.dapr.client.domain.QueryStateItem; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; +import io.dapr.client.domain.query.Query; +import io.dapr.client.domain.query.Sorting; +import io.dapr.client.domain.query.filters.EqFilter; + +try (DaprClient client = builder.build(); DaprPreviewClient previewClient = builder.buildPreviewClient()) { + String searchVal = args.length == 0 ? "searchValue" : args[0]; + + // Create JSON data + Listing first = new Listing(); + first.setPropertyType("apartment"); + first.setId("1000"); + ... + Listing second = new Listing(); + second.setPropertyType("row-house"); + second.setId("1002"); + ... + Listing third = new Listing(); + third.setPropertyType("apartment"); + third.setId("1003"); + ... + Listing fourth = new Listing(); + fourth.setPropertyType("apartment"); + fourth.setId("1001"); + ... + Map meta = new HashMap<>(); + meta.put("contentType", "application/json"); + + // Save state + SaveStateRequest request = new SaveStateRequest(STATE_STORE_NAME).setStates( + new State<>("1", first, null, meta, null), + new State<>("2", second, null, meta, null), + new State<>("3", third, null, meta, null), + new State<>("4", fourth, null, meta, null) + ); + client.saveBulkState(request).block(); + + + // Create query and query state request + + Query query = new Query() + .setFilter(new EqFilter<>("propertyType", "apartment")) + .setSort(Arrays.asList(new Sorting("id", Sorting.Order.DESC))); + QueryStateRequest request = new QueryStateRequest(STATE_STORE_NAME) + .setQuery(query); + + // Use preview client to call query state API + QueryStateResponse result = previewClient.queryState(request, MyData.class).block(); + + // View Query state response + System.out.println("Found " + result.getResults().size() + " items."); + for (QueryStateItem item : result.getResults()) { + System.out.println("Key: " + item.getKey()); + System.out.println("Data: " + item.getValue()); + } +} + +``` +- For a full list of configuration operations visit [How-To: Query state]({{< ref howto-state-query-api.md >}}). +- Visit [Java SDK examples](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples/querystate) for complete code sample. + ## Related links - [Java SDK examples](https://github.com/dapr/java-sdk/tree/master/examples/src/main/java/io/dapr/examples) diff --git a/examples/components/state/mongo.yaml b/examples/components/state/mongo.yaml new file mode 100644 index 000000000..b20547d0c --- /dev/null +++ b/examples/components/state/mongo.yaml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: mongo-statestore +spec: + type: state.mongodb + version: v1 + metadata: + - name: host + value: localhost:27017 + - name: databaseName + value: local + - name: collectionName + value: propertyCollection \ No newline at end of file diff --git a/examples/pom.xml b/examples/pom.xml index 6531807d4..29f8e75a5 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -61,7 +61,7 @@ com.github.os72 protoc-jar-maven-plugin - 3.10.1 + 3.11.4 org.springframework.boot @@ -130,7 +130,7 @@ com.github.os72 protoc-jar-maven-plugin - 3.10.1 + 3.11.4 generate-sources diff --git a/examples/src/main/java/io/dapr/examples/querystate/Listing.java b/examples/src/main/java/io/dapr/examples/querystate/Listing.java new file mode 100644 index 000000000..3269843ed --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/querystate/Listing.java @@ -0,0 +1,98 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.querystate; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.Objects; + +public class Listing { + + @JsonProperty + private String propertyType; + + @JsonProperty + private String id; + + @JsonProperty + private String city; + + @JsonProperty + private String state; + + public Listing() { + } + + public String getPropertyType() { + return propertyType; + } + + public void setPropertyType(String propertyType) { + this.propertyType = propertyType; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @Override + public String toString() { + return "Listing{" + + "propertyType='" + propertyType + '\'' + + ", id=" + id + + ", city='" + city + '\'' + + ", state='" + state + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Listing listing = (Listing) o; + return id == listing.id + && propertyType.equals(listing.propertyType) + && Objects.equals(city, listing.city) + && Objects.equals(state, listing.state); + } + + @Override + public int hashCode() { + return Objects.hash(propertyType, id, city, state); + } +} diff --git a/examples/src/main/java/io/dapr/examples/querystate/QuerySavedState.java b/examples/src/main/java/io/dapr/examples/querystate/QuerySavedState.java new file mode 100644 index 000000000..9045d28d2 --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/querystate/QuerySavedState.java @@ -0,0 +1,114 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.examples.querystate; + +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.client.DaprPreviewClient; +import io.dapr.client.domain.QueryStateItem; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; +import io.dapr.client.domain.SaveStateRequest; +import io.dapr.client.domain.State; +import io.dapr.client.domain.query.Query; +import io.dapr.client.domain.query.Sorting; +import io.dapr.client.domain.query.filters.EqFilter; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * 1. Build and install jars: + * mvn clean install + * 2. cd [repo root]/examples + * 3. send a message to be saved as state: + * dapr run --components-path ./components/state -- \ + * java -Ddapr.api.protocol=HTTP -jar target/dapr-java-sdk-examples-exec.jar \ + * io.dapr.examples.querystate.QuerySavedState 'my message' + */ +public class QuerySavedState { + + private static final String STATE_STORE_NAME = "mongo-statestore"; + + /** + * Executes the sate actions. + * @param args messages to be sent as state value. + */ + public static void main(String[] args) throws Exception { + DaprClientBuilder builder = new DaprClientBuilder(); + try (DaprClient client = builder.build(); DaprPreviewClient previewClient = builder.buildPreviewClient()) { + System.out.println("Waiting for Dapr sidecar ..."); + client.waitForSidecar(10000).block(); + System.out.println("Dapr sidecar is ready."); + + Listing first = new Listing(); + first.setPropertyType("apartment"); + first.setId("1000"); + first.setCity("Seattle"); + first.setState("WA"); + Listing second = new Listing(); + second.setPropertyType("row-house"); + second.setId("1002"); + second.setCity("Seattle"); + second.setState("WA"); + Listing third = new Listing(); + third.setPropertyType("apartment"); + third.setId("1003"); + third.setCity("Portland"); + third.setState("OR"); + Listing fourth = new Listing(); + fourth.setPropertyType("apartment"); + fourth.setId("1001"); + fourth.setCity("Portland"); + fourth.setState("OR"); + Map meta = new HashMap<>(); + meta.put("contentType", "application/json"); + + SaveStateRequest request = new SaveStateRequest(STATE_STORE_NAME).setStates( + new State<>("1", first, null, meta, null), + new State<>("2", second, null, meta, null), + new State<>("3", third, null, meta, null), + new State<>("4", fourth, null, meta, null) + ); + client.saveBulkState(request).block(); + + System.out.println("Insert key: 1" + ", data: " + first); + System.out.println("Insert key: 2" + ", data: " + second); + System.out.println("Insert key: 3" + ", data: " + third); + System.out.println("Insert key: 4" + ", data: " + fourth); + + + Query query = new Query() + .setFilter(new EqFilter<>("propertyType", "apartment")) + .setSort(Arrays.asList(new Sorting("id", Sorting.Order.DESC))); + + QueryStateRequest queryStateRequest = new QueryStateRequest(STATE_STORE_NAME) + .setQuery(query); + + QueryStateResponse result = previewClient.queryState(queryStateRequest, Listing.class).block(); + + System.out.println("Found " + result.getResults().size() + " items."); + for (QueryStateItem item : result.getResults()) { + System.out.println("Key: " + item.getKey()); + System.out.println("Data: " + item.getValue()); + } + + // This is an example, so for simplicity we are just exiting here. + // Normally a dapr app would be a web service and not exit main. + System.out.println("Done"); + System.exit(0); + } + } +} diff --git a/examples/src/main/java/io/dapr/examples/querystate/README.md b/examples/src/main/java/io/dapr/examples/querystate/README.md new file mode 100644 index 000000000..03c6e800a --- /dev/null +++ b/examples/src/main/java/io/dapr/examples/querystate/README.md @@ -0,0 +1,279 @@ +## State management sample + +This sample illustrates the capabilities provided by Dapr Java SDK for querying states. For further information about querying saved state please refer to [this link](https://docs.dapr.io/developing-applications/building-blocks/state-management/howto-state-query-api/) + +## Pre-requisites + +* [Dapr and Dapr Cli](https://docs.dapr.io/getting-started/install-dapr/). +* Java JDK 11 (or greater): [Oracle JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html#JDK11) or [OpenJDK](https://jdk.java.net/13/). +* [Apache Maven](https://maven.apache.org/install.html) version 3.x. + +### Checking out the code + +Clone this repository: + +```sh +git clone https://github.com/dapr/java-sdk.git +cd java-sdk +``` + +Then build the Maven project: + +```sh +# make sure you are in the `java-sdk` directory. +mvn install +``` + +Then change into the `examples` directory: +```sh +cd examples +``` + +### Running the StateClient +This example uses the Java SDK Dapr client in order to save bulk state and query state, in this case, an instance of a class. See the code snippets below: + +The class saved and queried for is as below: + +```java +public class Listing { + + @JsonProperty + private String propertyType; + + @JsonProperty + private String id; + + @JsonProperty + private String city; + + @JsonProperty + private String state; + + public Listing() { + } + + public String getPropertyType() { + return propertyType; + } + + public void setPropertyType(String propertyType) { + this.propertyType = propertyType; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } + + @Override + public String toString() { + return "Listing{" + + "propertyType='" + propertyType + '\'' + + ", id=" + id + + ", city='" + city + '\'' + + ", state='" + state + '\'' + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Listing listing = (Listing) o; + return id == listing.id + && propertyType.equals(listing.propertyType) + && Objects.equals(city, listing.city) + && Objects.equals(state, listing.state); + } + + @Override + public int hashCode() { + return Objects.hash(propertyType, id, city, state); + } +} +``` + +The main application class for the example is as follows: + +```java +public class QuerySavedState { + + public static class MyData { + ///... + } + + private static final String STATE_STORE_NAME = "querystatestore"; + + private static final String FIRST_KEY_NAME = "key1"; + + private static final String SECOND_KEY_NAME = "key2"; + + private static final String THIRD_KEY_NAME = "key3"; + + /** + * Executes the sate actions. + * @param args messages to be sent as state value. + */ + public static void main(String[] args) throws Exception { + DaprClientBuilder builder = new DaprClientBuilder(); + try (DaprClient client = builder.build(); DaprPreviewClient previewClient = builder.buildPreviewClient()) { + System.out.println("Waiting for Dapr sidecar ..."); + client.waitForSidecar(10000).block(); + System.out.println("Dapr sidecar is ready."); + + Listing first = new Listing(); + first.setPropertyType("apartment"); + first.setId("1000"); + first.setCity("Seattle"); + first.setState("WA"); + Listing second = new Listing(); + second.setPropertyType("row-house"); + second.setId("1002"); + second.setCity("Seattle"); + second.setState("WA"); + Listing third = new Listing(); + third.setPropertyType("apartment"); + third.setId("1003"); + third.setCity("Portland"); + third.setState("OR"); + Listing fourth = new Listing(); + fourth.setPropertyType("apartment"); + fourth.setId("1001"); + fourth.setCity("Portland"); + fourth.setState("OR"); + Map meta = new HashMap<>(); + meta.put("contentType", "application/json"); + + SaveStateRequest request = new SaveStateRequest(STATE_STORE_NAME).setStates( + new State<>("1", first, null, meta, null), + new State<>("2", second, null, meta, null), + new State<>("3", third, null, meta, null), + new State<>("4", fourth, null, meta, null) + ); + client.saveBulkState(request).block(); + + System.out.println("Insert key: 1" + ", data: " + first); + System.out.println("Insert key: 2" + ", data: " + second); + System.out.println("Insert key: 3" + ", data: " + third); + System.out.println("Insert key: 4" + ", data: " + fourth); + + + Query query = new Query() + .setFilter(new EqFilter<>("propertyType", "apartment")) + .setSort(Arrays.asList(new Sorting("id", Sorting.Order.DESC))); + + QueryStateRequest queryStateRequest = new QueryStateRequest(STATE_STORE_NAME) + .setQuery(query); + + QueryStateResponse result = previewClient.queryState(queryStateRequest, Listing.class).block(); + + System.out.println("Found " + result.getResults().size() + " items."); + for (QueryStateItem item : result.getResults()) { + System.out.println("Key: " + item.getKey()); + System.out.println("Data: " + item.getValue()); + } + + // This is an example, so for simplicity we are just exiting here. + // Normally a dapr app would be a web service and not exit main. + System.out.println("Done"); + System.exit(0); + } + } +} +``` +The code uses the `DaprClient` created by the `DaprClientBuilder` for waiting for sidecar to start as well as to save state. Notice that this builder uses default settings. Internally, it is using `DefaultObjectSerializer` for two properties: `objectSerializer` is for Dapr's sent and received objects, and `stateSerializer` is for objects to be persisted. + +The code uses the `DaprPreviewClient` created by the `DaprClientBuilder` is used for the `queryState` preview API. + +This example performs multiple operations: +* `client.waitForSidecar(...)` for waiting until Dapr sidecar is ready. +* `client.saveBulkState(...)` for persisting an instance of `Listing`. +* `client.query(...)` operation in order to query for persisted state. + +The Dapr clients are also within a try-with-resource block to properly close the clients at the end. + +### Running the example + + +Run this example with the following command: +```bash +dapr run --components-path ./components/state --app-id query_state_example -H 3600 -- java -Ddapr.api.protocol=HTTP -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.querystate.QuerySavedState +``` + + + +Once running, the QuerySaveState example should print the output as follows: + +```txt +== APP == Waiting for Dapr sidecar ... +== APP == Dapr sidecar is ready. +== APP == Insert key: 1, data: Listing{propertyType='apartment', id=1000, city='Seattle', state='WA'} +== APP == Insert key: 2, data: Listing{propertyType='row-house', id=1002, city='Seattle', state='WA'} +== APP == Insert key: 3, data: Listing{propertyType='apartment', id=1003, city='Portland', state='OR'} +== APP == Insert key: 4, data: Listing{propertyType='apartment', id=1001, city='Portland', state='OR'} +== APP == Found 3 items. +== APP == Key: 3 +== APP == Data: Listing{propertyType='apartment', id=1003, city='Portland', state='OR'} +== APP == Key: 4 +== APP == Data: Listing{propertyType='apartment', id=1001, city='Portland', state='OR'} +== APP == Key: 1 +== APP == Data: Listing{propertyType='apartment', id=1000, city='Seattle', state='WA'} +== APP == Done +``` +Note that the output is got in the descending order of the field `id` and all the `propertyType` field values are the same `apartment`. + +### Cleanup + +To close the app either press `CTRL+C` or run + + + +```bash +dapr stop --app-id query_state_example +``` + + diff --git a/examples/src/main/java/io/dapr/examples/state/README.md b/examples/src/main/java/io/dapr/examples/state/README.md index a002c41a6..f9b5f4a57 100644 --- a/examples/src/main/java/io/dapr/examples/state/README.md +++ b/examples/src/main/java/io/dapr/examples/state/README.md @@ -167,43 +167,26 @@ dapr run --components-path ./components/state --app-id state_example -- java -ja -Once running, the OutputBindingExample should print the output as follows: +Once running, the StateClient should print the output as follows: ```txt == APP == Waiting for Dapr sidecar ... - == APP == Dapr sidecar is ready. - == APP == Saving class with message: my message - == APP == Retrieved class message from state: my message - == APP == Updating previous state and adding another state 'test state'... - == APP == Saving updated class with message: my message updated - == APP == Retrieved messages using bulk get: - == APP == StateKeyValue{key='myKey', value=my message updated, etag='2', metadata={'{}'}, error='null', options={'null'}} - == APP == StateKeyValue{key='myKey2', value=test message, etag='1', metadata={'{}'}, error='null', options={'null'}} - == APP == Deleting states... - == APP == Verify delete key request is aborted if an etag different from stored is passed. - == APP == Expected failure. ABORTED: failed deleting state with key myKey: possible etag mismatch. error from state store: ERR Error running script (call to f_9b5da7354cb61e2ca9faff50f6c43b81c73c0b94): @user_script:1: user_script:1: failed to delete Tailmad-Fang||myKey - == APP == Trying to delete again with correct etag. - == APP == Trying to retrieve deleted states: - == APP == StateKeyValue{key='myKey', value=null, etag='null', metadata={'{}'}, error='null', options={'null'}} - == APP == StateKeyValue{key='myKey2', value=null, etag='null', metadata={'{}'}, error='null', options={'null'}} - == APP == Done - ``` ### Cleanup diff --git a/pom.xml b/pom.xml index 33bafc2f7..e4479225c 100644 --- a/pom.xml +++ b/pom.xml @@ -14,9 +14,9 @@ UTF-8 - 1.39.0 - 3.13.0 - https://raw.githubusercontent.com/dapr/dapr/v1.6.0-rc.3/dapr/proto + 1.42.1 + 3.17.3 + https://raw.githubusercontent.com/dapr/dapr/5a307f3deaa1b322f7945179adad0403de80eb7e/dapr/proto 1.6.2 3.1.1 1.8 diff --git a/sdk-actors/pom.xml b/sdk-actors/pom.xml index 6ed057cf9..6b8fb9133 100644 --- a/sdk-actors/pom.xml +++ b/sdk-actors/pom.xml @@ -18,7 +18,7 @@ false - 1.39.0 + 1.42.1 diff --git a/sdk-autogen/pom.xml b/sdk-autogen/pom.xml index 1ccc2dadd..0d3cff964 100644 --- a/sdk-autogen/pom.xml +++ b/sdk-autogen/pom.xml @@ -20,7 +20,7 @@ ${project.build.directory}/generated-sources ${project.build.directory}/proto false - 1.39.0 + 1.42.1 diff --git a/sdk-tests/components/mongo-statestore.yml b/sdk-tests/components/mongo-statestore.yml new file mode 100644 index 000000000..ace53887a --- /dev/null +++ b/sdk-tests/components/mongo-statestore.yml @@ -0,0 +1,14 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: mongo-statestore +spec: + type: state.mongodb + version: v1 + metadata: + - name: host + value: localhost:27017 + - name: databaseName + value: local + - name: collectionName + value: testCollection \ No newline at end of file diff --git a/sdk-tests/deploy/local-test-mongo.yml b/sdk-tests/deploy/local-test-mongo.yml new file mode 100644 index 000000000..54e211f1e --- /dev/null +++ b/sdk-tests/deploy/local-test-mongo.yml @@ -0,0 +1,6 @@ +version: '2' +services: + mongo: + image: mongo + ports: + - "27017:27017" \ No newline at end of file diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index 26c958ef9..97915a0d5 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -17,8 +17,8 @@ 1.5.0-SNAPSHOT ${project.build.directory}/generated-sources ${project.basedir}/proto - 1.39.0 - 3.13.0 + 1.42.1 + 3.17.3 0.14.0 2.3.5.RELEASE @@ -52,7 +52,7 @@ com.github.os72 protoc-jar-maven-plugin - 3.10.1 + 3.11.4 io.opentelemetry @@ -149,7 +149,7 @@ com.github.os72 protoc-jar-maven-plugin - 3.10.1 + 3.11.4 generate-sources diff --git a/sdk-tests/src/test/java/io/dapr/it/BaseIT.java b/sdk-tests/src/test/java/io/dapr/it/BaseIT.java index fba18c19e..b35d1ce6b 100644 --- a/sdk-tests/src/test/java/io/dapr/it/BaseIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/BaseIT.java @@ -30,6 +30,8 @@ public abstract class BaseIT { protected static final String STATE_STORE_NAME = "statestore"; + protected static final String QUERY_STATE_STORE = "mongo-statestore"; + private static final Map DAPR_RUN_BUILDERS = new HashMap<>(); private static final Queue TO_BE_STOPPED = new LinkedList<>(); diff --git a/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java b/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java index abaeff244..b5835439e 100644 --- a/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/state/AbstractStateClientIT.java @@ -13,10 +13,19 @@ limitations under the License. package io.dapr.it.state; +import com.fasterxml.jackson.core.JsonProcessingException; import io.dapr.client.DaprClient; +import io.dapr.client.DaprPreviewClient; +import io.dapr.client.domain.QueryStateItem; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; +import io.dapr.client.domain.SaveStateRequest; import io.dapr.client.domain.State; import io.dapr.client.domain.StateOptions; import io.dapr.client.domain.TransactionalStateOperation; +import io.dapr.client.domain.query.Query; +import io.dapr.client.domain.query.Sorting; +import io.dapr.client.domain.query.filters.EqFilter; import io.dapr.exceptions.DaprException; import io.dapr.it.BaseIT; import org.junit.Test; @@ -24,8 +33,11 @@ import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.logging.Logger; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -39,6 +51,7 @@ import static org.junit.Assert.assertTrue; * Common test cases for Dapr client (GRPC and HTTP). */ public abstract class AbstractStateClientIT extends BaseIT { + private static final Logger logger = Logger.getLogger(AbstractStateClientIT.class.getName()); @Test public void saveAndGetState() { @@ -126,6 +139,78 @@ public abstract class AbstractStateClientIT extends BaseIT { assertNull(result.stream().skip(2).findFirst().get().getError()); } + @Test + public void saveAndQueryAndDeleteState() throws JsonProcessingException { + final String stateKeyOne = UUID.randomUUID().toString(); + final String stateKeyTwo = UUID.randomUUID().toString(); + final String stateKeyThree = UUID.randomUUID().toString(); + final String commonSearchValue = UUID.randomUUID().toString(); + Map meta = new HashMap<>(); + meta.put("contentType", "application/json"); + + DaprClient daprClient = buildDaprClient(); + DaprPreviewClient previewApiClient = (DaprPreviewClient) daprClient; + + //saves the states. + MyData data = new MyData(); + data.setPropertyA(commonSearchValue); + data.setPropertyB("query"); + State state = new State<>(stateKeyOne, data, null, meta, null ); + SaveStateRequest request = new SaveStateRequest(QUERY_STATE_STORE).setStates(state); + daprClient.saveBulkState(request).block(); + data = new MyData(); + data.setPropertyA(commonSearchValue); + data.setPropertyB("query"); + state = new State<>(stateKeyTwo, data, null, meta, null ); + request = new SaveStateRequest(QUERY_STATE_STORE).setStates(state); + daprClient.saveBulkState(request).block(); + data = new MyData(); + data.setPropertyA("CA"); + data.setPropertyB("no query"); + state = new State<>(stateKeyThree, data, null, meta, null ); + request = new SaveStateRequest(QUERY_STATE_STORE).setStates(state); + daprClient.saveBulkState(request).block(); + + + QueryStateRequest queryStateRequest = new QueryStateRequest(QUERY_STATE_STORE); + Query query = new Query().setFilter(new EqFilter<>("propertyA", commonSearchValue)) + .setSort(Arrays.asList(new Sorting("propertyB", Sorting.Order.ASC))); + queryStateRequest.setQuery(query).setMetadata(meta); + + Mono> response = previewApiClient.queryState(queryStateRequest, MyData.class); + QueryStateResponse result = response.block(); + + // Assert that the response is not null + assertNotNull(result); + List> items = result.getResults(); + assertNotNull(items); + + QueryStateItem item; + //Assert that the response is the correct one + assertEquals(2, items.size()); + assertTrue(items.stream().anyMatch(f -> f.getKey().equals(stateKeyOne))); + item = items.stream().filter(f -> f.getKey().equals(stateKeyOne)).findFirst().get(); + assertNotNull(item); + assertEquals(commonSearchValue, item.getValue().getPropertyA()); + assertEquals("query", item.getValue().getPropertyB()); + assertNull(item.getError()); + + assertTrue(items.stream().anyMatch(f -> f.getKey().equals(stateKeyTwo))); + item = items.stream().filter(f -> f.getKey().equals(stateKeyTwo)).findFirst().get(); + assertEquals(commonSearchValue, item.getValue().getPropertyA()); + assertEquals("query", item.getValue().getPropertyB()); + assertNull(item.getError()); + + assertFalse(items.stream().anyMatch(f -> f.getKey().equals(stateKeyThree))); + + assertEquals(2L, items.stream().filter(f -> f.getValue().getPropertyB().equals("query")).count()); + + //delete all states + daprClient.deleteState(QUERY_STATE_STORE, stateKeyOne).block(); + daprClient.deleteState(QUERY_STATE_STORE, stateKeyTwo).block(); + daprClient.deleteState(QUERY_STATE_STORE, stateKeyThree).block(); + } + @Test public void saveUpdateAndGetState() { @@ -159,6 +244,7 @@ public abstract class AbstractStateClientIT extends BaseIT { State myDataResponse = response.block(); //review that the update was success action + assertNotNull("expected non null response", myDataResponse); assertEquals("data in property A", myDataResponse.getValue().getPropertyA()); assertEquals("data in property B2", myDataResponse.getValue().getPropertyB()); } diff --git a/sdk/pom.xml b/sdk/pom.xml index 08a9027b3..d8d4e77f4 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -18,7 +18,7 @@ false - 1.39.0 + 1.42.1 --add-opens java.base/java.util=ALL-UNNAMED diff --git a/sdk/src/main/java/io/dapr/client/AbstractDaprClient.java b/sdk/src/main/java/io/dapr/client/AbstractDaprClient.java index cfc4b4733..843992619 100644 --- a/sdk/src/main/java/io/dapr/client/AbstractDaprClient.java +++ b/sdk/src/main/java/io/dapr/client/AbstractDaprClient.java @@ -13,6 +13,7 @@ limitations under the License. package io.dapr.client; +import com.fasterxml.jackson.databind.ObjectMapper; import io.dapr.client.domain.ConfigurationItem; import io.dapr.client.domain.DeleteStateRequest; import io.dapr.client.domain.ExecuteStateTransactionRequest; @@ -25,11 +26,14 @@ import io.dapr.client.domain.HttpExtension; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.client.domain.PublishEventRequest; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SaveStateRequest; import io.dapr.client.domain.State; import io.dapr.client.domain.StateOptions; import io.dapr.client.domain.SubscribeConfigurationRequest; import io.dapr.client.domain.TransactionalStateOperation; +import io.dapr.client.domain.query.Query; import io.dapr.serializer.DaprObjectSerializer; import io.dapr.utils.TypeRef; import reactor.core.publisher.Flux; @@ -50,6 +54,11 @@ import java.util.stream.Collectors; */ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { + /** + * A mapper to serialize JSON request objects. + */ + protected static final ObjectMapper JSON_REQUEST_MAPPER = new ObjectMapper(); + /** * A utility class for serialize and deserialize the transient objects. */ @@ -130,7 +139,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeMethod( - String appId, String methodName, HttpExtension httpExtension, Map metadata, TypeRef type) { + String appId, String methodName, HttpExtension httpExtension, Map metadata, TypeRef type) { return this.invokeMethod(appId, methodName, null, httpExtension, metadata, type); } @@ -139,7 +148,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeMethod( - String appId, String methodName, HttpExtension httpExtension, Map metadata, Class clazz) { + String appId, String methodName, HttpExtension httpExtension, Map metadata, Class clazz) { return this.invokeMethod(appId, methodName, null, httpExtension, metadata, TypeRef.get(clazz)); } @@ -174,7 +183,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeMethod( - String appId, String methodName, Object request, HttpExtension httpExtension, Map metadata) { + String appId, String methodName, Object request, HttpExtension httpExtension, Map metadata) { return this.invokeMethod(appId, methodName, request, httpExtension, metadata, TypeRef.BYTE_ARRAY).then(); } @@ -183,7 +192,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeMethod( - String appId, String methodName, HttpExtension httpExtension, Map metadata) { + String appId, String methodName, HttpExtension httpExtension, Map metadata) { return this.invokeMethod(appId, methodName, null, httpExtension, metadata, TypeRef.BYTE_ARRAY).then(); } @@ -192,7 +201,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeMethod( - String appId, String methodName, byte[] request, HttpExtension httpExtension, Map metadata) { + String appId, String methodName, byte[] request, HttpExtension httpExtension, Map metadata) { return this.invokeMethod(appId, methodName, request, httpExtension, metadata, TypeRef.BYTE_ARRAY); } @@ -233,7 +242,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeBinding( - String bindingName, String operation, Object data, Map metadata, TypeRef type) { + String bindingName, String operation, Object data, Map metadata, TypeRef type) { InvokeBindingRequest request = new InvokeBindingRequest(bindingName, operation) .setData(data) .setMetadata(metadata); @@ -246,7 +255,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono invokeBinding( - String bindingName, String operation, Object data, Map metadata, Class clazz) { + String bindingName, String operation, Object data, Map metadata, Class clazz) { return this.invokeBinding(bindingName, operation, data, metadata, TypeRef.get(clazz)); } @@ -287,7 +296,7 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono> getState( - String storeName, String key, StateOptions options, TypeRef type) { + String storeName, String key, StateOptions options, TypeRef type) { GetStateRequest request = new GetStateRequest(storeName, key) .setStateOptions(options); return this.getState(request, type); @@ -299,10 +308,86 @@ abstract class AbstractDaprClient implements DaprClient, DaprPreviewClient { */ @Override public Mono> getState( - String storeName, String key, StateOptions options, Class clazz) { + String storeName, String key, StateOptions options, Class clazz) { return this.getState(storeName, key, options, TypeRef.get(clazz)); } + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, String query, Map metadata, + Class clazz) { + return this.queryState(new QueryStateRequest(storeName).setQueryString(query).setMetadata(metadata), clazz); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, String query, Map metadata, + TypeRef type) { + return this.queryState(new QueryStateRequest(storeName).setQueryString(query).setMetadata(metadata), type); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, String query, Class clazz) { + return this.queryState(new QueryStateRequest(storeName).setQueryString(query), clazz); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, String query, TypeRef type) { + return this.queryState(new QueryStateRequest(storeName).setQueryString(query), type); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, Query query, Map metadata, + Class clazz) { + return this.queryState(new QueryStateRequest(storeName).setQuery(query).setMetadata(metadata), clazz); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, Query query, Map metadata, + TypeRef type) { + return this.queryState(new QueryStateRequest(storeName).setQuery(query).setMetadata(metadata), type); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, Query query, Class clazz) { + return this.queryState(new QueryStateRequest(storeName).setQuery(query), clazz); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(String storeName, Query query, TypeRef type) { + return this.queryState(new QueryStateRequest(storeName).setQuery(query), type); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(QueryStateRequest request, Class clazz) { + return this.queryState(request, TypeRef.get(clazz)); + } + /** * {@inheritDoc} */ diff --git a/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java b/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java index 401b7690d..39f0d227b 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java @@ -29,6 +29,9 @@ import io.dapr.client.domain.HttpExtension; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.client.domain.PublishEventRequest; +import io.dapr.client.domain.QueryStateItem; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SaveStateRequest; import io.dapr.client.domain.State; import io.dapr.client.domain.StateOptions; @@ -88,7 +91,7 @@ public class DaprClientGrpc extends AbstractDaprClient { * Default access level constructor, in order to create an instance of this class use io.dapr.client.DaprClientBuilder * * @param closeableChannel A closeable for a Managed GRPC channel - * @param asyncStub async gRPC stub + * @param asyncStub async gRPC stub * @param objectSerializer Serializer for transient request/response objects. * @param stateSerializer Serializer for state objects. * @see DaprClientBuilder @@ -167,10 +170,10 @@ public class DaprClientGrpc extends AbstractDaprClient { } return Mono.subscriberContext().flatMap( - context -> - this.createMono( - it -> intercept(context, asyncStub).publishEvent(envelopeBuilder.build(), it) - ) + context -> + this.createMono( + it -> intercept(context, asyncStub).publishEvent(envelopeBuilder.build(), it) + ) ).then(); } catch (Exception ex) { return DaprException.wrapMono(ex); @@ -204,7 +207,7 @@ public class DaprClientGrpc extends AbstractDaprClient { it -> { try { return Mono.justOrEmpty(objectSerializer.deserialize(it.getData().getValue().toByteArray(), type)); - } catch (IOException e) { + } catch (IOException e) { throw DaprException.propagate(e); } } @@ -247,7 +250,7 @@ public class DaprClientGrpc extends AbstractDaprClient { context -> this.createMono( it -> intercept(context, asyncStub).invokeBinding(envelope, it) ) - ).flatMap( + ).flatMap( it -> { try { return Mono.justOrEmpty(objectSerializer.deserialize(it.getData().toByteArray(), type)); @@ -343,7 +346,7 @@ public class DaprClientGrpc extends AbstractDaprClient { context -> this.createMono(it -> intercept(context, asyncStub) .getBulkState(envelope, it) ) - ).map( + ).map( it -> it .getItemsList() @@ -414,7 +417,7 @@ public class DaprClientGrpc extends AbstractDaprClient { if (metadata != null) { builder.putAllMetadata(metadata); } - for (TransactionalStateOperation operation: operations) { + for (TransactionalStateOperation operation : operations) { DaprProtos.TransactionalStateOperation.Builder operationBuilder = DaprProtos.TransactionalStateOperation .newBuilder(); operationBuilder.setOperationType(operation.getOperation().toString().toLowerCase()); @@ -603,8 +606,8 @@ public class DaprClientGrpc extends AbstractDaprClient { } DaprProtos.GetSecretRequest.Builder requestBuilder = DaprProtos.GetSecretRequest.newBuilder() - .setStoreName(secretStoreName) - .setKey(key); + .setStoreName(secretStoreName) + .setKey(key); if (metadata != null) { requestBuilder.putAllMetadata(metadata); @@ -639,7 +642,7 @@ public class DaprClientGrpc extends AbstractDaprClient { return Mono.subscriberContext().flatMap( context -> this.createMono( - it -> intercept(context, asyncStub).getBulkSecret(envelope, it) + it -> intercept(context, asyncStub).getBulkSecret(envelope, it) ) ).map(it -> { Map secretsMap = it.getDataMap(); @@ -656,10 +659,87 @@ public class DaprClientGrpc extends AbstractDaprClient { } } + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(QueryStateRequest request, TypeRef type) { + try { + if (request == null) { + throw new IllegalArgumentException("Query state request cannot be null."); + } + final String storeName = request.getStoreName(); + final Map metadata = request.getMetadata(); + if ((storeName == null) || (storeName.trim().isEmpty())) { + throw new IllegalArgumentException("State store name cannot be null or empty."); + } + + String queryString; + if (request.getQuery() != null) { + queryString = JSON_REQUEST_MAPPER.writeValueAsString(request.getQuery()); + } else if (request.getQueryString() != null) { + queryString = request.getQueryString(); + } else { + throw new IllegalArgumentException("Both query and queryString fields are not set."); + } + + DaprProtos.QueryStateRequest.Builder builder = DaprProtos.QueryStateRequest.newBuilder() + .setStoreName(storeName) + .setQuery(queryString); + if (metadata != null) { + builder.putAllMetadata(metadata); + } + + DaprProtos.QueryStateRequest envelope = builder.build(); + + return Mono.subscriberContext().flatMap( + context -> this.createMono( + it -> intercept(context, asyncStub).queryStateAlpha1(envelope, it) + ) + ).map( + it -> { + Map resultMeta = it.getMetadataMap(); + String token = it.getToken(); + List> res = it.getResultsList() + .stream() + .map(v -> { + try { + return buildQueryStateKeyValue(v, type); + } catch (Exception e) { + throw DaprException.propagate(e); + } + }) + .collect(Collectors.toList()); + return new QueryStateResponse<>(res, token).setMetadata(metadata); + }); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + } + + private QueryStateItem buildQueryStateKeyValue( + DaprProtos.QueryStateItem item, + TypeRef type) throws IOException { + String key = item.getKey(); + String error = item.getError(); + if (!Strings.isNullOrEmpty(error)) { + return new QueryStateItem<>(key, null, error); + } + ByteString payload = item.getData(); + byte[] data = payload == null ? null : payload.toByteArray(); + T value = stateSerializer.deserialize(data, type); + String etag = item.getEtag(); + if (etag.equals("")) { + etag = null; + } + return new QueryStateItem<>(key, value, etag); + } + /** * Closes the ManagedChannel for GRPC. - * @see io.grpc.ManagedChannel#shutdown() + * * @throws IOException on exception. + * @see io.grpc.ManagedChannel#shutdown() */ @Override public void close() throws Exception { @@ -677,8 +757,8 @@ public class DaprClientGrpc extends AbstractDaprClient { @Override public Mono shutdown() { return Mono.subscriberContext().flatMap( - context -> this.createMono( - it -> intercept(context, asyncStub).shutdown(Empty.getDefaultInstance(), it)) + context -> this.createMono( + it -> intercept(context, asyncStub).shutdown(Empty.getDefaultInstance(), it)) ).then(); } @@ -810,7 +890,7 @@ public class DaprClientGrpc extends AbstractDaprClient { * Populates GRPC client with interceptors for telemetry. * * @param context Reactor's context. - * @param client GRPC client for Dapr. + * @param client GRPC client for Dapr. * @return Client after adding interceptors. */ private static DaprGrpc.DaprStub intercept(Context context, DaprGrpc.DaprStub client) { diff --git a/sdk/src/main/java/io/dapr/client/DaprClientHttp.java b/sdk/src/main/java/io/dapr/client/DaprClientHttp.java index ed4522c99..f325c0e03 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientHttp.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientHttp.java @@ -27,6 +27,9 @@ import io.dapr.client.domain.HttpExtension; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.client.domain.PublishEventRequest; +import io.dapr.client.domain.QueryStateItem; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SaveStateRequest; import io.dapr.client.domain.State; import io.dapr.client.domain.StateOptions; @@ -44,6 +47,7 @@ import reactor.core.publisher.Mono; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; @@ -162,7 +166,7 @@ public class DaprClientHttp extends AbstractDaprClient { Map> queryArgs = metadataToQueryArgs(metadata); return Mono.subscriberContext().flatMap( context -> this.client.invokeApi( - DaprHttp.HttpMethods.POST.name(), pathSegments, queryArgs, serializedEvent, headers, context + DaprHttp.HttpMethods.POST.name(), pathSegments, queryArgs, serializedEvent, headers, context ) ).then(); } catch (Exception ex) { @@ -192,17 +196,21 @@ public class DaprClientHttp extends AbstractDaprClient { if (method == null || method.trim().isEmpty()) { throw new IllegalArgumentException("Method name cannot be null or empty."); } + + + String[] methodSegments = method.split("/"); + + List pathSegments = new ArrayList<>(Arrays.asList(DaprHttp.API_VERSION, "invoke", appId, "method")); + pathSegments.addAll(Arrays.asList(methodSegments)); + byte[] serializedRequestBody = objectSerializer.serialize(request); - - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "invoke", appId, "method", method }; - final Map headers = new HashMap<>(); if (contentType != null && !contentType.isEmpty()) { headers.put("content-type", contentType); } headers.putAll(httpExtension.getHeaders()); Mono response = Mono.subscriberContext().flatMap( - context -> this.client.invokeApi(httpMethod, pathSegments, + context -> this.client.invokeApi(httpMethod, pathSegments.toArray(new String[0]), httpExtension.getQueryParams(), serializedRequestBody, headers, context) ); return response.flatMap(r -> getMono(type, r)); @@ -282,7 +290,7 @@ public class DaprClientHttp extends AbstractDaprClient { return DaprException.wrapMono(ex); } } - + /** * {@inheritDoc} */ @@ -310,25 +318,25 @@ public class DaprClientHttp extends AbstractDaprClient { byte[] requestBody = INTERNAL_SERIALIZER.serialize(jsonMap); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, "bulk"}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, "bulk" }; Map> queryArgs = metadataToQueryArgs(metadata); return Mono.subscriberContext().flatMap( - context -> this.client - .invokeApi(DaprHttp.HttpMethods.POST.name(), pathSegments, queryArgs, requestBody, null, context) - ).flatMap(s -> { - try { - return Mono.just(buildStates(s, type)); - } catch (Exception ex) { - return DaprException.wrapMono(ex); - } - }); + context -> this.client + .invokeApi(DaprHttp.HttpMethods.POST.name(), pathSegments, queryArgs, requestBody, null, context) + ).flatMap(s -> { + try { + return Mono.just(buildStates(s, type)); + } catch (Exception ex) { + return DaprException.wrapMono(ex); + } + }); } catch (Exception ex) { return DaprException.wrapMono(ex); } } - + /** * {@inheritDoc} @@ -356,11 +364,11 @@ public class DaprClientHttp extends AbstractDaprClient { queryParams.putAll(optionsMap.entrySet().stream().collect( Collectors.toMap(kv -> kv.getKey(), kv -> Collections.singletonList(kv.getValue())))); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, key}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, key }; return Mono.subscriberContext().flatMap( - context -> this.client - .invokeApi(DaprHttp.HttpMethods.GET.name(), pathSegments, queryParams, null, context) + context -> this.client + .invokeApi(DaprHttp.HttpMethods.GET.name(), pathSegments, queryParams, null, context) ).flatMap(s -> { try { return Mono.justOrEmpty(buildState(s, key, options, type)); @@ -414,7 +422,7 @@ public class DaprClientHttp extends AbstractDaprClient { TransactionalStateRequest req = new TransactionalStateRequest<>(internalOperationObjects, metadata); byte[] serializedOperationBody = INTERNAL_SERIALIZER.serialize(req); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, "transaction"}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, "transaction" }; return Mono.subscriberContext().flatMap( context -> this.client.invokeApi( @@ -458,11 +466,11 @@ public class DaprClientHttp extends AbstractDaprClient { byte[] data = this.stateSerializer.serialize(state.getValue()); // Custom serializer, so everything is byte[]. internalStateObjects.add(new State<>(state.getKey(), data, state.getEtag(), state.getMetadata(), - state.getOptions())); + state.getOptions())); } byte[] serializedStateBody = INTERNAL_SERIALIZER.serialize(internalStateObjects); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName }; return Mono.subscriberContext().flatMap( context -> this.client.invokeApi( @@ -505,7 +513,7 @@ public class DaprClientHttp extends AbstractDaprClient { queryParams.putAll(optionsMap.entrySet().stream().collect( Collectors.toMap(kv -> kv.getKey(), kv -> Collections.singletonList(kv.getValue())))); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, key}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "state", stateStoreName, key }; return Mono.subscriberContext().flatMap( context -> this.client.invokeApi( @@ -521,7 +529,7 @@ public class DaprClientHttp extends AbstractDaprClient { * * @param response The response of the HTTP Call * @param requestedKey The Key Requested. - * @param type The Class of the Value of the state + * @param type The Class of the Value of the state * @param The Type of the Value of the state * @return A State instance * @throws IOException If there's a issue deserializing the response. @@ -540,9 +548,9 @@ public class DaprClientHttp extends AbstractDaprClient { /** * Builds a State object based on the Response. * - * @param response The response of the HTTP Call - * @param type The Class of the Value of the state - * @param The Type of the Value of the state + * @param response The response of the HTTP Call + * @param type The Class of the Value of the state + * @param The Type of the Value of the state * @return A list of states. * @throws IOException If there's a issue deserializing the response. */ @@ -593,24 +601,24 @@ public class DaprClientHttp extends AbstractDaprClient { } Map> queryArgs = metadataToQueryArgs(metadata); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "secrets", secretStoreName, key}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "secrets", secretStoreName, key }; return Mono.subscriberContext().flatMap( - context -> this.client - .invokeApi(DaprHttp.HttpMethods.GET.name(), pathSegments, queryArgs, (String)null, null, context) - ).flatMap(response -> { - try { - Map m = INTERNAL_SERIALIZER.deserialize(response.getBody(), Map.class); - if (m == null) { - return Mono.just(Collections.EMPTY_MAP); - } + context -> this.client + .invokeApi(DaprHttp.HttpMethods.GET.name(), pathSegments, queryArgs, (String) null, null, context) + ).flatMap(response -> { + try { + Map m = INTERNAL_SERIALIZER.deserialize(response.getBody(), Map.class); + if (m == null) { + return Mono.just(Collections.EMPTY_MAP); + } - return Mono.just(m); - } catch (IOException e) { - return DaprException.wrapMono(e); - } - }) - .map(m -> (Map)m); + return Mono.just(m); + } catch (IOException e) { + return DaprException.wrapMono(e); + } + }) + .map(m -> (Map) m); } /** @@ -629,14 +637,14 @@ public class DaprClientHttp extends AbstractDaprClient { } Map> queryArgs = metadataToQueryArgs(metadata); - String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "secrets", secretStoreName, "bulk"}; + String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "secrets", secretStoreName, "bulk" }; return Mono.subscriberContext().flatMap( context -> this.client - .invokeApi(DaprHttp.HttpMethods.GET.name(), pathSegments, queryArgs, (String)null, null, context) + .invokeApi(DaprHttp.HttpMethods.GET.name(), pathSegments, queryArgs, (String) null, null, context) ).flatMap(response -> { try { - Map m = INTERNAL_SERIALIZER.deserialize(response.getBody(), Map.class); + Map m = INTERNAL_SERIALIZER.deserialize(response.getBody(), Map.class); if (m == null) { return Mono.just(Collections.EMPTY_MAP); } @@ -646,7 +654,47 @@ public class DaprClientHttp extends AbstractDaprClient { return DaprException.wrapMono(e); } }) - .map(m -> (Map>)m); + .map(m -> (Map>) m); + } + + /** + * {@inheritDoc} + */ + @Override + public Mono> queryState(QueryStateRequest request, TypeRef type) { + try { + if (request == null) { + throw new IllegalArgumentException("Query state request cannot be null."); + } + String stateStoreName = request.getStoreName(); + Map metadata = request.getMetadata(); + if ((stateStoreName == null) || (stateStoreName.trim().isEmpty())) { + throw new IllegalArgumentException("State store name cannot be null or empty."); + } + Map> queryArgs = metadataToQueryArgs(metadata); + String[] pathSegments = new String[]{ DaprHttp.ALPHA_1_API_VERSION, "state", stateStoreName, "query" }; + String serializedRequest; + if (request.getQuery() != null) { + serializedRequest = JSON_REQUEST_MAPPER.writeValueAsString(request.getQuery()); + } else if (request.getQueryString() != null) { + serializedRequest = request.getQueryString(); + } else { + throw new IllegalArgumentException("Both query and queryString fields are not set."); + } + return Mono.subscriberContext().flatMap( + context -> this.client + .invokeApi(DaprHttp.HttpMethods.POST.name(), pathSegments, + queryArgs, serializedRequest, null, context) + ).flatMap(response -> { + try { + return Mono.justOrEmpty(buildQueryStateResponse(response, type)); + } catch (Exception e) { + return DaprException.wrapMono(e); + } + }); + } catch (Exception e) { + return DaprException.wrapMono(e); + } } /** @@ -665,8 +713,49 @@ public class DaprClientHttp extends AbstractDaprClient { String[] pathSegments = new String[]{ DaprHttp.API_VERSION, "shutdown" }; return Mono.subscriberContext().flatMap( context -> client.invokeApi(DaprHttp.HttpMethods.POST.name(), pathSegments, - null, null, context)) - .then(); + null, null, context)) + .then(); + } + + private QueryStateResponse buildQueryStateResponse(DaprHttp.Response response, + TypeRef type) throws IOException { + JsonNode root = INTERNAL_SERIALIZER.parseNode(response.getBody()); + if (!root.has("results")) { + return new QueryStateResponse<>(Collections.emptyList(), null); + } + String token = null; + if (root.has("token")) { + token = root.path("token").asText(); + } + Map metadata = new HashMap<>(); + if (root.has("metadata")) { + for (Iterator> it = root.get("metadata").fields(); it.hasNext(); ) { + Map.Entry entry = it.next(); + metadata.put(entry.getKey(), entry.getValue().asText()); + } + } + List> result = new ArrayList<>(); + for (Iterator it = root.get("results").elements(); it.hasNext(); ) { + JsonNode node = it.next(); + String key = node.path("key").asText(); + String error = node.path("error").asText(); + if (!Strings.isNullOrEmpty(error)) { + result.add(new QueryStateItem<>(key, null, error)); + continue; + } + + String etag = node.path("etag").asText(); + if (etag.equals("")) { + etag = null; + } + // TODO(artursouza): JSON cannot differentiate if data returned is String or byte[], it is ambiguous. + // This is not a high priority since GRPC is the default (and recommended) client implementation. + byte[] data = node.path("data").toString().getBytes(Properties.STRING_CHARSET.get()); + T value = stateSerializer.deserialize(data, type); + result.add(new QueryStateItem<>(key, value, etag)); + } + + return new QueryStateResponse<>(result, token).setMetadata(metadata); } /** @@ -687,6 +776,7 @@ public class DaprClientHttp extends AbstractDaprClient { /** * Converts metadata map into Query params. + * * @param metadata metadata map * @return Query params */ diff --git a/sdk/src/main/java/io/dapr/client/DaprClientProxy.java b/sdk/src/main/java/io/dapr/client/DaprClientProxy.java index 53f619a9c..32f1c5e8f 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientProxy.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientProxy.java @@ -23,6 +23,8 @@ import io.dapr.client.domain.HttpExtension; import io.dapr.client.domain.InvokeBindingRequest; import io.dapr.client.domain.InvokeMethodRequest; import io.dapr.client.domain.PublishEventRequest; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SaveStateRequest; import io.dapr.client.domain.State; import io.dapr.client.domain.StateOptions; diff --git a/sdk/src/main/java/io/dapr/client/DaprHttp.java b/sdk/src/main/java/io/dapr/client/DaprHttp.java index 2b823ce48..0172304d3 100644 --- a/sdk/src/main/java/io/dapr/client/DaprHttp.java +++ b/sdk/src/main/java/io/dapr/client/DaprHttp.java @@ -31,7 +31,6 @@ import reactor.core.publisher.Mono; import reactor.util.context.Context; import java.io.IOException; -import java.lang.reflect.Array; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -51,6 +50,11 @@ public class DaprHttp implements AutoCloseable { */ public static final String API_VERSION = "v1.0"; + /** + * Dapr alpha API used in this client. + */ + public static final String ALPHA_1_API_VERSION = "v1.0-alpha1"; + /** * Header used for request id in Dapr. */ diff --git a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java index 5e41f5e35..2928ba747 100644 --- a/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java +++ b/sdk/src/main/java/io/dapr/client/DaprPreviewClient.java @@ -15,7 +15,11 @@ package io.dapr.client; import io.dapr.client.domain.ConfigurationItem; import io.dapr.client.domain.GetConfigurationRequest; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SubscribeConfigurationRequest; +import io.dapr.client.domain.query.Query; +import io.dapr.utils.TypeRef; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -103,4 +107,120 @@ public interface DaprPreviewClient extends AutoCloseable { * @return Flux of List of configuration items */ Flux> subscribeToConfiguration(SubscribeConfigurationRequest request); + + /** + * Query for states using a query string. + * + * @param storeName Name of the state store to query. + * @param query String value of the query. + * @param metadata Optional metadata passed to the state store. + * @param clazz The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, String query, + Map metadata, Class clazz); + + /** + * Query for states using a query string. + * + * @param storeName Name of the state store to query. + * @param query String value of the query. + * @param metadata Optional metadata passed to the state store. + * @param type The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, String query, + Map metadata, TypeRef type); + + /** + * Query for states using a query string. + * + * @param storeName Name of the state store to query. + * @param query String value of the query. + * @param clazz The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, String query, Class clazz); + + /** + * Query for states using a query string. + * + * @param storeName Name of the state store to query. + * @param query String value of the query. + * @param type The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, String query, TypeRef type); + + /** + * Query for states using a query domain object. + * + * @param storeName Name of the state store to query. + * @param query Query value domain object. + * @param metadata Optional metadata passed to the state store. + * @param clazz The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, Query query, + Map metadata, Class clazz); + + /** + * Query for states using a query domain object. + * + * @param storeName Name of the state store to query. + * @param query Query value domain object. + * @param metadata Optional metadata passed to the state store. + * @param type The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, Query query, + Map metadata, TypeRef type); + + /** + * Query for states using a query domain object. + * + * @param storeName Name of the state store to query. + * @param query Query value domain object. + * @param clazz The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, Query query, Class clazz); + + /** + * Query for states using a query domain object. + * + * @param storeName Name of the state store to query. + * @param query Query value domain object. + * @param type The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(String storeName, Query query, TypeRef type); + + /** + * Query for states using a query request. + * + * @param request Query request object. + * @param clazz The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(QueryStateRequest request, Class clazz); + + /** + * Query for states using a query request. + * + * @param request Query request object. + * @param type The type needed as return for the call. + * @param The Type of the return, use byte[] to skip serialization. + * @return A Mono of QueryStateResponse of type T. + */ + Mono> queryState(QueryStateRequest request, TypeRef type); } diff --git a/sdk/src/main/java/io/dapr/client/domain/QueryStateItem.java b/sdk/src/main/java/io/dapr/client/domain/QueryStateItem.java new file mode 100644 index 000000000..093299eed --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/QueryStateItem.java @@ -0,0 +1,167 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + + +public class QueryStateItem { + + + /** + * The value of the state. + */ + private final T value; + + /** + * The key of the state. + */ + private final String key; + + /** + * The ETag to be used + * Keep in mind that for some state stores (like redis) only numbers are supported. + */ + private final String etag; + + /** + * The error in case the key could not be retrieved. + */ + private final String error; + + /** + * Create an immutable state reference to be retrieved or deleted. + * This Constructor CAN be used anytime you need to retrieve or delete a state. + * + * @param key - The key of the state + */ + public QueryStateItem(String key) { + this.key = key; + this.value = null; + this.etag = null; + this.error = null; + } + + /** + * Create an immutable state reference to be retrieved or deleted. + * This Constructor CAN be used anytime you need to retrieve or delete a state. + * + * @param key - The key of the state + * @param etag - The etag of the state - Keep in mind that for some state stores (like redis) only numbers + * are supported. + * @param error - Error when fetching the state. + */ + public QueryStateItem(String key, String etag, String error) { + this.value = null; + this.key = key; + this.etag = etag; + this.error = error; + } + + /** + * Create an immutable state. + * This Constructor CAN be used anytime you want the state to be saved. + * + * @param key - The key of the state. + * @param value - The value of the state. + * @param etag - The etag of the state - for some state stores (like redis) only numbers are supported. + */ + public QueryStateItem(String key, T value, String etag) { + this.value = value; + this.key = key; + this.etag = etag; + this.error = null; + } + + /** + * Retrieves the Value of the state. + * + * @return The value of the state + */ + public T getValue() { + return value; + } + + /** + * Retrieves the Key of the state. + * + * @return The key of the state + */ + public String getKey() { + return key; + } + + /** + * Retrieve the ETag of this state. + * + * @return The etag of the state + */ + public String getEtag() { + return etag; + } + + /** + * Retrieve the error for this state. + * + * @return The error for this state. + */ + + public String getError() { + return error; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof QueryStateItem)) { + return false; + } + + QueryStateItem that = (QueryStateItem) o; + + if (getValue() != null ? !getValue().equals(that.getValue()) : that.getValue() != null) { + return false; + } + + if (getKey() != null ? !getKey().equals(that.getKey()) : that.getKey() != null) { + return false; + } + + if (getEtag() != null ? !getEtag().equals(that.getEtag()) : that.getEtag() != null) { + return false; + } + + return getError() != null ? getError().equals(that.getError()) : that.getError() == null; + } + + @Override + public int hashCode() { + int result = getValue() != null ? getValue().hashCode() : 0; + result = 31 * result + (getKey() != null ? getKey().hashCode() : 0); + result = 31 * result + (getEtag() != null ? getEtag().hashCode() : 0); + result = 31 * result + (getError() != null ? getError().hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "QueryStateItem{" + + "key='" + key + "'" + + ", value=" + value + + ", etag='" + etag + "'" + + ", error='" + error + "'" + + "}"; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/QueryStateRequest.java b/sdk/src/main/java/io/dapr/client/domain/QueryStateRequest.java new file mode 100644 index 000000000..9e148eb7a --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/QueryStateRequest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.dapr.client.domain.query.Query; + +import java.util.Collections; +import java.util.Map; + +public class QueryStateRequest { + + @JsonIgnore + private final String storeName; + + private Query query; + + private String queryString; + + @JsonIgnore + private Map metadata; + + public QueryStateRequest(String storeName) { + this.storeName = storeName; + } + + public String getStoreName() { + return storeName; + } + + public Query getQuery() { + return query; + } + + /** + * Validate and set the query field. Mutually exclusive with the queryString field in this instance. + * + * @param query Valid Query domain object. + * @return This instance. + */ + public QueryStateRequest setQuery(Query query) { + if (this.queryString != null) { + throw new IllegalArgumentException("queryString filed is already set in the request. query field cannot be set."); + } + if (query == null || query.getFilter() == null) { + throw new IllegalArgumentException("query cannot be null or with null filter"); + } + this.query = query; + return this; + } + + public String getQueryString() { + return queryString; + } + + /** + * Validate and set the queryString field. Mutually exclusive with the query field in this instance. + * + * @param queryString String value of the query. + * @return This request object for fluent API. + */ + public QueryStateRequest setQueryString(String queryString) { + if (this.query != null) { + throw new IllegalArgumentException("query filed is already set in the request. queryString field cannot be set."); + } + if (queryString == null || queryString.isEmpty()) { + throw new IllegalArgumentException("queryString cannot be null or blank"); + } + this.queryString = queryString; + return this; + } + + public Map getMetadata() { + return metadata; + } + + public QueryStateRequest setMetadata(Map metadata) { + this.metadata = metadata == null ? null : Collections.unmodifiableMap(metadata); + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/QueryStateResponse.java b/sdk/src/main/java/io/dapr/client/domain/QueryStateResponse.java new file mode 100644 index 000000000..e084a4566 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/QueryStateResponse.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain; + + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class QueryStateResponse { + + private final List> results; + + private final String token; + + private Map metadata; + + public QueryStateResponse(List> results, String token) { + this.results = results == null ? null : Collections.unmodifiableList(results); + this.token = token; + } + + public List> getResults() { + return results; + } + + public String getToken() { + return token; + } + + public Map getMetadata() { + return metadata; + } + + public QueryStateResponse setMetadata(Map metadata) { + this.metadata = metadata == null ? null : Collections.unmodifiableMap(metadata); + return this; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/Pagination.java b/sdk/src/main/java/io/dapr/client/domain/query/Pagination.java new file mode 100644 index 000000000..c515b1ab4 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/Pagination.java @@ -0,0 +1,39 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Pagination { + private int limit; + private String token; + + Pagination() { + // For JSON + } + + public Pagination(int limit, String token) { + this.limit = limit; + this.token = token; + } + + public int getLimit() { + return limit; + } + + public String getToken() { + return token; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/Query.java b/sdk/src/main/java/io/dapr/client/domain/query/Query.java new file mode 100644 index 000000000..98f54e3b2 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/Query.java @@ -0,0 +1,75 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dapr.client.domain.query.filters.Filter; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class Query { + private Filter filter; + + @JsonProperty + private Sorting[] sort = new Sorting[]{}; + + @JsonProperty("page") + private Pagination pagination = new Pagination(); + + public Filter getFilter() { + return filter; + } + + /** + * Set the filter field in the instance. + * @param filter Valid filter value. + * @return this instance. + */ + public Query setFilter(Filter filter) { + if (!filter.isValid()) { + throw new IllegalArgumentException("the given filter is invalid configuration"); + } + this.filter = filter; + return this; + } + + public List getSort() { + return Collections.unmodifiableList(Arrays.asList(sort)); + } + + /** + * Validate and set sorting field. + * + * @param sort List of sorting objects. + * @return This instance. + */ + public Query setSort(List sort) { + if (sort == null || sort.size() == 0) { + throw new IllegalArgumentException("Sorting list is null or empty"); + } + this.sort = sort.toArray(new Sorting[0]); + return this; + } + + public Pagination getPagination() { + return pagination; + } + + public Query setPagination(Pagination pagination) { + this.pagination = pagination; + return this; + } +} \ No newline at end of file diff --git a/sdk/src/main/java/io/dapr/client/domain/query/Sorting.java b/sdk/src/main/java/io/dapr/client/domain/query/Sorting.java new file mode 100644 index 000000000..755f8ca5a --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/Sorting.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonValue; + +@JsonInclude(JsonInclude.Include.NON_EMPTY) +public class Sorting { + private String key; + private Order order; + + Sorting() { + // For JSON + } + + public Sorting(String key, Order order) { + this.key = key; + this.order = order; + } + + public String getKey() { + return key; + } + + public Order getOrder() { + return order; + } + + public enum Order { + ASC("ASC"), + DESC("DESC"); + + private String name; + + Order(String name) { + this.name = name; + } + + @JsonValue + public String getValue() { + return this.name; + } + + @JsonCreator + public static Order fromValue(String value) { + return Order.valueOf(value); + } + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/filters/AndFilter.java b/sdk/src/main/java/io/dapr/client/domain/query/filters/AndFilter.java new file mode 100644 index 000000000..4e98a3615 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/filters/AndFilter.java @@ -0,0 +1,68 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class AndFilter extends Filter { + @JsonIgnore + private final List> and; + + public AndFilter() { + super("AND"); + this.and = new ArrayList<>(); + } + + @JsonCreator + AndFilter(Filter[] filters) { + super("AND"); + this.and = Arrays.asList(filters); + } + + public > AndFilter addClause(V filter) { + this.and.add(filter); + return this; + } + + @JsonValue + public Filter[] getClauses() { + return this.and.toArray(new Filter[0]); + } + + @Override + @JsonIgnore + public String getRepresentation() { + return this.and.stream().map(Filter::getRepresentation).collect(Collectors.joining(" AND ")); + } + + @Override + public Boolean isValid() { + boolean validAnd = and != null && and.size() >= 2; + if (validAnd) { + for (Filter filter : and) { + if (!filter.isValid()) { + return false; + } + } + } + return validAnd; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/filters/EqFilter.java b/sdk/src/main/java/io/dapr/client/domain/query/filters/EqFilter.java new file mode 100644 index 000000000..8607f48fe --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/filters/EqFilter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.AbstractMap; +import java.util.Map; + +public class EqFilter extends Filter { + + @JsonValue + private Map.Entry eq; + + public EqFilter() { + super("EQ"); + } + + public EqFilter(String key, T value) { + super("EQ"); + eq = new AbstractMap.SimpleImmutableEntry<>(key, value); + } + + @JsonCreator + EqFilter(Map.Entry eq) { + super("EQ"); + this.eq = eq; + } + + @JsonIgnore + public String getKey() { + return eq != null ? eq.getKey() : null; + } + + @JsonIgnore + public T getValue() { + return eq != null ? eq.getValue() : null; + } + + @Override + public String getRepresentation() { + return this.getKey() + " EQ " + this.getValue(); + } + + @Override + public Boolean isValid() { + return eq != null && eq.getKey() != null && !eq.getKey().isEmpty() && !eq.getKey().trim().isEmpty(); + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/filters/Filter.java b/sdk/src/main/java/io/dapr/client/domain/query/filters/Filter.java new file mode 100644 index 000000000..f6c0b9131 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/filters/Filter.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.ObjectMapper; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.WRAPPER_OBJECT) +@JsonSubTypes({ + @JsonSubTypes.Type(value = AndFilter.class, name = "AND"), + @JsonSubTypes.Type(value = InFilter.class, name = "IN"), + @JsonSubTypes.Type(value = OrFilter.class, name = "OR"), + @JsonSubTypes.Type(value = EqFilter.class, name = "EQ") +}) +public abstract class Filter { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @JsonIgnore + private String name; + + Filter() { + // For JSON Serialization + } + + public Filter(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @JsonIgnore + public abstract String getRepresentation(); + + @JsonIgnore + public abstract Boolean isValid(); +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/filters/InFilter.java b/sdk/src/main/java/io/dapr/client/domain/query/filters/InFilter.java new file mode 100644 index 000000000..199ebb1be --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/filters/InFilter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class InFilter extends Filter { + @JsonValue + private Map.Entry> in; + + public InFilter() { + super("IN"); + } + + public InFilter(String key, List value) { + super("IN"); + in = new AbstractMap.SimpleEntry<>(key, value); + } + + @JsonCreator + InFilter(Map.Entry> in) { + super("IN"); + this.in = in; + } + + /** + * constructor for InFilter. + * @param key value of the key in the state store. + * @param values var args values list. + */ + public InFilter(String key, T... values) { + super("IN"); + if (values == null || values.length == 0) { + throw new IllegalArgumentException("list of values must be at least 1"); + } + in = new AbstractMap.SimpleImmutableEntry<>(key, Collections.unmodifiableList(Arrays.asList(values))); + } + + @JsonIgnore + public String getKey() { + return in != null ? in.getKey() : null; + } + + @JsonIgnore + public List getValues() { + return in != null ? in.getValue() : null; + } + + @Override + public String getRepresentation() { + return this.getKey() + " IN [" + + this.getValues().stream().map(Object::toString).collect(Collectors.joining(",")) + + "]"; + } + + @Override + public Boolean isValid() { + return in != null && in.getKey() != null && !in.getKey().isEmpty() && !in.getKey().trim().isEmpty() + && in.getValue() != null && in.getValue().size() > 0; + } +} diff --git a/sdk/src/main/java/io/dapr/client/domain/query/filters/OrFilter.java b/sdk/src/main/java/io/dapr/client/domain/query/filters/OrFilter.java new file mode 100644 index 000000000..345351e0b --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/domain/query/filters/OrFilter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ + +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@SuppressWarnings("rawtypes") +public class OrFilter extends Filter { + + @JsonIgnore + private List> or; + + public OrFilter() { + super("OR"); + this.or = new ArrayList<>(); + } + + @JsonCreator + OrFilter(Filter[] filters) { + super("OR"); + this.or = Arrays.asList(filters); + } + + public OrFilter addClause(V filter) { + this.or.add(filter); + return this; + } + + @JsonValue + public Filter[] getClauses() { + return this.or.toArray(new Filter[0]); + } + + @Override + @JsonIgnore + public String getRepresentation() { + return this.or.stream().map(Filter::getRepresentation).collect(Collectors.joining(" OR ")); + } + + @Override + public Boolean isValid() { + boolean validAnd = or != null && or.size() >= 2; + if (validAnd) { + for (Filter filter : or) { + if (!filter.isValid()) { + return false; + } + } + } + return validAnd; + } +} diff --git a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java index 2252074e3..8219ac8f5 100644 --- a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java @@ -14,9 +14,16 @@ limitations under the License. package io.dapr.client; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.protobuf.ByteString; import io.dapr.client.domain.ConfigurationItem; import io.dapr.client.domain.GetConfigurationRequest; +import io.dapr.client.domain.QueryStateItem; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; import io.dapr.client.domain.SubscribeConfigurationRequest; +import io.dapr.client.domain.query.Query; import io.dapr.serializer.DefaultObjectSerializer; import io.dapr.v1.CommonProtos; import io.dapr.v1.DaprGrpc; @@ -29,6 +36,7 @@ import org.mockito.stubbing.Answer; import java.io.Closeable; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; @@ -37,14 +45,24 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import static io.dapr.utils.TestUtils.assertThrowsDaprException; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; public class DaprPreviewClientGrpcTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); private static final String CONFIG_STORE_NAME = "MyConfigStore"; + private static final String QUERY_STORE_NAME = "testQueryStore"; private Closeable closeable; private DaprGrpc.DaprStub daprStub; @@ -269,4 +287,105 @@ public class DaprPreviewClientGrpcTest { .build(); return responseEnvelope; } + + @Test + public void queryStateExceptionsTest() { + assertThrows(IllegalArgumentException.class, () -> { + previewClient.queryState("", "query", String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + previewClient.queryState("storeName", "", String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + previewClient.queryState("storeName", (Query) null, String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + previewClient.queryState("storeName", (String) null, String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + previewClient.queryState(new QueryStateRequest("storeName"), String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + previewClient.queryState(null, String.class).block(); + }); + } + + @Test + public void queryState() throws JsonProcessingException { + List> resp = new ArrayList<>(); + resp.add(new QueryStateItem("1", (Object)"testData", "6f54ad94-dfb9-46f0-a371-e42d550adb7d")); + DaprProtos.QueryStateResponse responseEnvelope = buildQueryStateResponse(resp, ""); + doAnswer((Answer) invocation -> { + DaprProtos.QueryStateRequest req = invocation.getArgument(0); + assertEquals(QUERY_STORE_NAME, req.getStoreName()); + assertEquals("query", req.getQuery()); + assertEquals(0, req.getMetadataCount()); + + StreamObserver observer = (StreamObserver) + invocation.getArguments()[1]; + observer.onNext(responseEnvelope); + observer.onCompleted(); + return null; + }).when(daprStub).queryStateAlpha1(any(DaprProtos.QueryStateRequest.class), any()); + + QueryStateResponse response = previewClient.queryState(QUERY_STORE_NAME, "query", String.class).block(); + assertNotNull(response); + assertEquals("result size must be 1", 1, response.getResults().size()); + assertEquals("result must be same", "1", response.getResults().get(0).getKey()); + assertEquals("result must be same", "testData", response.getResults().get(0).getValue()); + assertEquals("result must be same", "6f54ad94-dfb9-46f0-a371-e42d550adb7d", response.getResults().get(0).getEtag()); + } + + @Test + public void queryStateMetadataError() throws JsonProcessingException { + List> resp = new ArrayList<>(); + resp.add(new QueryStateItem("1", null, "error data")); + DaprProtos.QueryStateResponse responseEnvelope = buildQueryStateResponse(resp, ""); + doAnswer((Answer) invocation -> { + DaprProtos.QueryStateRequest req = invocation.getArgument(0); + assertEquals(QUERY_STORE_NAME, req.getStoreName()); + assertEquals("query", req.getQuery()); + assertEquals(1, req.getMetadataCount()); + assertEquals(1, req.getMetadataCount()); + + StreamObserver observer = (StreamObserver) + invocation.getArguments()[1]; + observer.onNext(responseEnvelope); + observer.onCompleted(); + return null; + }).when(daprStub).queryStateAlpha1(any(DaprProtos.QueryStateRequest.class), any()); + + QueryStateResponse response = previewClient.queryState(QUERY_STORE_NAME, "query", + new HashMap(){{ put("key", "error"); }}, String.class).block(); + assertNotNull(response); + assertEquals("result size must be 1", 1, response.getResults().size()); + assertEquals("result must be same", "1", response.getResults().get(0).getKey()); + assertEquals("result must be same", "error data", response.getResults().get(0).getError()); + } + + private DaprProtos.QueryStateResponse buildQueryStateResponse(List> resp,String token) + throws JsonProcessingException { + List items = new ArrayList<>(); + for (QueryStateItem item: resp) { + items.add(buildQueryStateItem(item)); + } + return DaprProtos.QueryStateResponse.newBuilder() + .addAllResults(items) + .setToken(token) + .build(); + } + + private DaprProtos.QueryStateItem buildQueryStateItem(QueryStateItem item) throws JsonProcessingException { + DaprProtos.QueryStateItem.Builder it = DaprProtos.QueryStateItem.newBuilder().setKey(item.getKey()); + if (item.getValue() != null) { + it.setData(ByteString.copyFrom(MAPPER.writeValueAsBytes(item.getValue()))); + } + if (item.getEtag() != null) { + it.setEtag(item.getEtag()); + } + if (item.getError() != null) { + it.setError(item.getError()); + } + return it.build(); + } } diff --git a/sdk/src/test/java/io/dapr/client/DaprPreviewClientHttpTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientHttpTest.java index b70e223f4..121db19b4 100644 --- a/sdk/src/test/java/io/dapr/client/DaprPreviewClientHttpTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientHttpTest.java @@ -1,30 +1,32 @@ /* - * Copyright (c) Microsoft Corporation and Dapr Contributors. - * Licensed under the MIT License. - */ + * Copyright 2021 The Dapr Authors + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and +limitations under the License. +*/ package io.dapr.client; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import io.dapr.client.domain.QueryStateRequest; +import io.dapr.client.domain.QueryStateResponse; +import io.dapr.client.domain.query.Query; import io.dapr.config.Properties; import io.dapr.exceptions.DaprException; -import io.dapr.serializer.DaprObjectSerializer; import io.dapr.utils.TypeRef; import okhttp3.OkHttpClient; import okhttp3.mock.Behavior; import okhttp3.mock.MockInterceptor; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; -import reactor.core.publisher.Mono; -import java.io.IOException; -import java.net.ServerSocket; -import java.net.Socket; - -import static io.dapr.utils.TestUtils.findFreePort; -import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; public class DaprPreviewClientHttpTest { @@ -66,4 +68,56 @@ public class DaprPreviewClientHttpTest { daprPreviewClientHttp.subscribeToConfiguration(CONFIG_STORE_NAME, "key1", "key2").blockFirst(); }); } + + @Test + public void queryStateExceptionsTest() { + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("", "query", TypeRef.BOOLEAN).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("", "query", String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("storeName", "", TypeRef.BOOLEAN).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("storeName", "", String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("storeName", (Query) null, TypeRef.BOOLEAN).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("storeName", (Query) null, String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("storeName", (String) null, TypeRef.BOOLEAN).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState("storeName", (String) null, String.class).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState(null, TypeRef.BOOLEAN).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState(new QueryStateRequest("storeName"), TypeRef.BOOLEAN).block(); + }); + assertThrows(IllegalArgumentException.class, () -> { + daprPreviewClientHttp.queryState(null, String.class).block(); + }); + } + + @Test + public void queryStateTest() { + mockInterceptor.addRule() + .post() + .path("/v1.0-alpha1/state/testStore/query") + .respond("{\"results\": [{\"key\": \"1\",\"data\": \"testData\"," + + "\"etag\": \"6f54ad94-dfb9-46f0-a371-e42d550adb7d\"}]}"); + QueryStateResponse response = daprPreviewClientHttp.queryState("testStore", "query", String.class).block(); + assertNotNull(response); + assertEquals("result size must be 1", 1, response.getResults().size()); + assertEquals("result must be same", "1", response.getResults().get(0).getKey()); + assertEquals("result must be same", "testData", response.getResults().get(0).getValue()); + assertEquals("result must be same", "6f54ad94-dfb9-46f0-a371-e42d550adb7d", response.getResults().get(0).getEtag()); + } } diff --git a/sdk/src/test/java/io/dapr/client/domain/QueryStateRequestTest.java b/sdk/src/test/java/io/dapr/client/domain/QueryStateRequestTest.java new file mode 100644 index 000000000..0c88e1aa9 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/QueryStateRequestTest.java @@ -0,0 +1,54 @@ +package io.dapr.client.domain; + +import io.dapr.client.domain.query.Query; +import io.dapr.client.domain.query.filters.EqFilter; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertNull; + +public class QueryStateRequestTest { + + private String STORE_NAME = "STORE"; + private String KEY = "KEY"; + + @Test + public void testSetMetadata() { + QueryStateRequest request = new QueryStateRequest(STORE_NAME); + // Null check + request.setMetadata(null); + assertNull(request.getMetadata()); + // Modifiability check + Map metadata = new HashMap<>(); + metadata.put("test", "testval"); + request.setMetadata(metadata); + Map initial = request.getMetadata(); + request.setMetadata(metadata); + Assert.assertNotSame("Should not be same map", request.getMetadata(), initial); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetNullQuery() { + QueryStateRequest request = new QueryStateRequest(STORE_NAME); + request.setQuery(null); + } + + @Test(expected = IllegalArgumentException.class) + public void testSetNullFilterQuery() { + QueryStateRequest request = new QueryStateRequest(STORE_NAME); + Query query = new Query(); + request.setQuery(query); + } + + @Test + public void testSetQuery() { + QueryStateRequest request = new QueryStateRequest(STORE_NAME); + Query query = new Query(); + query.setFilter(new EqFilter<>("key", "value")); + request.setQuery(query); + Assert.assertEquals(query, request.getQuery()); + } +} diff --git a/sdk/src/test/java/io/dapr/client/domain/query/QueryTest.java b/sdk/src/test/java/io/dapr/client/domain/query/QueryTest.java new file mode 100644 index 000000000..74602cf3f --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/query/QueryTest.java @@ -0,0 +1,118 @@ +package io.dapr.client.domain.query; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.domain.query.filters.*; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class QueryTest { + + ObjectMapper mapper = new ObjectMapper(); + String json = "{\"filter\":{\"AND\":[{\"EQ\":{\"key\":\"value\"}},{\"IN\":{\"key2\":[\"v1\",\"v2\"]}}," + + "{\"OR\":[{\"EQ\":{\"v2\":true}},{\"IN\":{\"v3\":[1.3,1.5]}}]}]}," + + "\"sort\":[{\"key\":\"value.person.org\",\"order\":\"ASC\"},{\"key\":\"value.state\",\"order\":\"DESC\"}]," + + "\"page\":{\"limit\":10,\"token\":\"test-token\"}}"; + + @Test + public void testQuerySerialize() throws JsonProcessingException { + Query q = new Query(); + + AndFilter filter = new AndFilter(); + filter.addClause(new EqFilter<>("key", "value")); + filter.addClause(new InFilter<>("key2", "v1", "v2")); + + OrFilter orFilter = new OrFilter(); + orFilter.addClause(new EqFilter<>("v2", true)); + orFilter.addClause(new InFilter<>("v3", 1.3, 1.5)); + + filter.addClause(orFilter); + + // Add Filter + q.setFilter(filter); + q.setPagination(new Pagination(10, "test-token")); + q.setSort(Arrays.asList(new Sorting("value.person.org", Sorting.Order.ASC), + new Sorting("value.state", Sorting.Order.DESC))); + Assert.assertEquals(json, mapper.writeValueAsString(q)); + } + + + @Test + public void testQueryDeserialize() throws JsonProcessingException { + + + Query query = mapper.readValue(json, Query.class); + Assert.assertNotNull(query.getPagination()); + Assert.assertNotNull(query.getFilter()); + Assert.assertNotNull(query.getSort()); + + // Assert Pagination + Assert.assertEquals(10, query.getPagination().getLimit()); + Assert.assertEquals("test-token", query.getPagination().getToken()); + + // Assert Sort + Assert.assertEquals(2, query.getSort().size()); + Assert.assertEquals("value.person.org", query.getSort().get(0).getKey()); + Assert.assertEquals(Sorting.Order.ASC, query.getSort().get(0).getOrder()); + Assert.assertEquals("value.state", query.getSort().get(1).getKey()); + Assert.assertEquals(Sorting.Order.DESC, query.getSort().get(1).getOrder()); + + // Assert Filter + // Top level AND filter + Assert.assertEquals("AND", query.getFilter().getName()); + // Type cast to AND filter + AndFilter filter = (AndFilter) query.getFilter(); + // Assert 3 AND clauses + Assert.assertEquals(3, filter.getClauses().length); + Filter[] andClauses = filter.getClauses(); + // First EQ + Assert.assertEquals("EQ", andClauses[0].getName()); + Assert.assertSame(EqFilter.class, andClauses[0].getClass()); + EqFilter eq = (EqFilter) andClauses[0]; + Assert.assertEquals("key", eq.getKey()); + Assert.assertEquals("value", eq.getValue()); + // Second IN + Assert.assertEquals("IN", andClauses[1].getName()); + Assert.assertSame(InFilter.class, andClauses[1].getClass()); + InFilter in = (InFilter) andClauses[1]; + Assert.assertEquals("key2", in.getKey()); + Assert.assertArrayEquals(new String[]{ "v1", "v2" }, in.getValues().toArray()); + // Third OR + Assert.assertEquals("OR", andClauses[2].getName()); + Assert.assertSame(OrFilter.class, andClauses[2].getClass()); + OrFilter orFilter = (OrFilter) andClauses[2]; + Filter[] orClauses = orFilter.getClauses(); + // First EQ in OR + Assert.assertEquals("EQ", orClauses[0].getName()); + Assert.assertSame(EqFilter.class, orClauses[0].getClass()); + eq = (EqFilter) orClauses[0]; + Assert.assertEquals("v2", eq.getKey()); + Assert.assertEquals(true, eq.getValue()); + // Second IN in OR + Assert.assertEquals("IN", orClauses[1].getName()); + Assert.assertSame(InFilter.class, orClauses[1].getClass()); + in = (InFilter) orClauses[1]; + Assert.assertEquals("v3", in.getKey()); + Assert.assertArrayEquals(new Double[]{ 1.3, 1.5 }, in.getValues().toArray()); + } + + @Test(expected = IllegalArgumentException.class) + public void testQueryInValidFilter() throws JsonProcessingException { + Query q = new Query(); + + AndFilter filter = new AndFilter(); + filter.addClause(new EqFilter<>("key", "value")); + filter.addClause(new InFilter<>("key2", "v1", "v2")); + + OrFilter orFilter = new OrFilter(); + orFilter.addClause(new EqFilter<>("v2", true)); + // invalid OR filter + + filter.addClause(orFilter); + + // Add Filter + q.setFilter(filter); + } +} \ No newline at end of file diff --git a/sdk/src/test/java/io/dapr/client/domain/query/filters/AndFilterTest.java b/sdk/src/test/java/io/dapr/client/domain/query/filters/AndFilterTest.java new file mode 100644 index 000000000..36133fd3d --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/query/filters/AndFilterTest.java @@ -0,0 +1,59 @@ +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +public class AndFilterTest { + + private final static ObjectMapper MAPPER = new ObjectMapper(); + + private String json = "{\"AND\":[{\"EQ\":{\"key\":\"value\"}},{\"IN\":{\"key2\":[\"v1\",\"v2\"]}}]}"; + + @SuppressWarnings("rawtypes") + @Test + public void testSerialization() throws JsonProcessingException { + AndFilter filter = new AndFilter(); + filter.addClause(new EqFilter<>("key", "value")); + filter.addClause(new InFilter<>("key2", "v1", "v2")); + + Assert.assertEquals(json, MAPPER.writeValueAsString((Filter) filter)); + } + + @Test + public void testDeserialization() throws JsonProcessingException { + Filter res = MAPPER.readValue(json, Filter.class); + + // Check for AndFilter + Assert.assertEquals("AND", res.getName()); + Assert.assertSame(AndFilter.class, res.getClass()); + + AndFilter filter = (AndFilter) res; + // Check 2 clauses + Assert.assertEquals(2, filter.getClauses().length); + // First EQ + Assert.assertSame(EqFilter.class, filter.getClauses()[0].getClass()); + EqFilter eq = (EqFilter) filter.getClauses()[0]; + Assert.assertEquals("key", eq.getKey()); + Assert.assertEquals("value", eq.getValue()); + // Second IN + Assert.assertSame(InFilter.class, filter.getClauses()[1].getClass()); + InFilter in = (InFilter) filter.getClauses()[1]; + Assert.assertEquals("key2", in.getKey()); + Assert.assertArrayEquals(new String[]{ "v1", "v2" }, in.getValues().toArray()); + } + + @Test + public void testValidation() { + AndFilter filter = new AndFilter(); + Assert.assertFalse(filter.isValid()); + + filter.addClause(new EqFilter<>("key1", "v2")); + Assert.assertFalse(filter.isValid()); + + filter.addClause(new EqFilter<>("key2", "v3")); + Assert.assertTrue(filter.isValid()); + } + +} diff --git a/sdk/src/test/java/io/dapr/client/domain/query/filters/EqFilterTest.java b/sdk/src/test/java/io/dapr/client/domain/query/filters/EqFilterTest.java new file mode 100644 index 000000000..ac7611bc3 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/query/filters/EqFilterTest.java @@ -0,0 +1,45 @@ +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +public class EqFilterTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + String json = "{\"EQ\":{\"key\":1.5}}"; + + @Test + public void testSerialization() throws JsonProcessingException { + EqFilter filter = new EqFilter<>("key", 1.5); + + Assert.assertEquals(json, MAPPER.writeValueAsString(filter)); + } + + @Test + public void testDeserialization() throws JsonProcessingException { + EqFilter filter = MAPPER.readValue(json, EqFilter.class); + Assert.assertEquals("key", filter.getKey()); + Assert.assertEquals(1.5, filter.getValue()); + } + + @Test + public void testValidation() { + EqFilter filter = new EqFilter<>(null, "val"); + Assert.assertFalse(filter.isValid()); + + + filter = new EqFilter<>("", ""); + Assert.assertFalse(filter.isValid()); + + filter = new EqFilter<>("", true); + Assert.assertFalse(filter.isValid()); + + filter = new EqFilter<>(" ", "valid"); + Assert.assertFalse(filter.isValid()); + + filter = new EqFilter<>("valid", ""); + Assert.assertTrue(filter.isValid()); + } +} diff --git a/sdk/src/test/java/io/dapr/client/domain/query/filters/InFilterTest.java b/sdk/src/test/java/io/dapr/client/domain/query/filters/InFilterTest.java new file mode 100644 index 000000000..47aa6bcbc --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/query/filters/InFilterTest.java @@ -0,0 +1,48 @@ +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +public class InFilterTest { + private static final ObjectMapper MAPPER = new ObjectMapper(); + + String json = "{\"IN\":{\"key\":[1.5,44.0]}}"; + + @Test + public void testSerialization() throws JsonProcessingException { + InFilter filter = new InFilter<>("key", 1.5, 44.0); + + Assert.assertEquals(json, MAPPER.writeValueAsString(filter)); + } + + @Test + public void testDeserialization() throws JsonProcessingException { + InFilter filter = MAPPER.readValue(json, InFilter.class); + Assert.assertEquals("key", filter.getKey()); + Assert.assertArrayEquals(new Double[]{ 1.5, 44.0 }, filter.getValues().toArray()); + } + + @Test + public void testValidation() { + InFilter filter = new InFilter<>(null, "val"); + Assert.assertFalse(filter.isValid()); + + + filter = new InFilter<>("", ""); + Assert.assertFalse(filter.isValid()); + + filter = new InFilter<>("", true); + Assert.assertFalse(filter.isValid()); + + filter = new InFilter<>(" ", "valid"); + Assert.assertFalse(filter.isValid()); + + filter = new InFilter<>("valid", ""); + Assert.assertTrue(filter.isValid()); + + filter = new InFilter<>("valid", "1.5", "2.5"); + Assert.assertTrue(filter.isValid()); + } +} diff --git a/sdk/src/test/java/io/dapr/client/domain/query/filters/OrFilterTest.java b/sdk/src/test/java/io/dapr/client/domain/query/filters/OrFilterTest.java new file mode 100644 index 000000000..202d33336 --- /dev/null +++ b/sdk/src/test/java/io/dapr/client/domain/query/filters/OrFilterTest.java @@ -0,0 +1,57 @@ +package io.dapr.client.domain.query.filters; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Assert; +import org.junit.Test; + +public class OrFilterTest { + private final static ObjectMapper MAPPER = new ObjectMapper(); + + private String json = "{\"OR\":[{\"EQ\":{\"key\":\"value\"}},{\"IN\":{\"key2\":[\"v1\",\"v2\"]}}]}"; + + @SuppressWarnings("rawtypes") + @Test + public void testSerialization() throws JsonProcessingException { + OrFilter filter = new OrFilter(); + filter.addClause(new EqFilter<>("key", "value")); + filter.addClause(new InFilter<>("key2", "v1", "v2")); + + Assert.assertEquals(json, MAPPER.writeValueAsString((Filter) filter)); + } + + @Test + public void testDeserialization() throws JsonProcessingException { + Filter res = MAPPER.readValue(json, Filter.class); + + // Check for AndFilter + Assert.assertEquals("OR", res.getName()); + Assert.assertSame(OrFilter.class, res.getClass()); + + OrFilter filter = (OrFilter) res; + // Check 2 clauses + Assert.assertEquals(2, filter.getClauses().length); + // First EQ + Assert.assertSame(EqFilter.class, filter.getClauses()[0].getClass()); + EqFilter eq = (EqFilter) filter.getClauses()[0]; + Assert.assertEquals("key", eq.getKey()); + Assert.assertEquals("value", eq.getValue()); + // Second IN + Assert.assertSame(InFilter.class, filter.getClauses()[1].getClass()); + InFilter in = (InFilter) filter.getClauses()[1]; + Assert.assertEquals("key2", in.getKey()); + Assert.assertArrayEquals(new String[]{ "v1", "v2" }, in.getValues().toArray()); + } + + @Test + public void testValidation() { + OrFilter filter = new OrFilter(); + Assert.assertFalse(filter.isValid()); + + filter.addClause(new EqFilter<>("key1", "v2")); + Assert.assertFalse(filter.isValid()); + + filter.addClause(new EqFilter<>("key2", "v3")); + Assert.assertTrue(filter.isValid()); + } +}