From f60f4bf7ad63ec35490ec29cfa06cf58c02eebbf Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Wed, 21 Aug 2024 16:13:40 +0300 Subject: [PATCH 1/3] Adding Spring Boot enhancements, Sring Data Repository, Testcontainers Signed-off-by: Artur Ciocanu --- .github/workflows/build.yml | 5 +- .../dapr-spring-boot-autoconfigure/pom.xml | 79 ++++ .../client/DaprClientAutoConfiguration.java | 54 +++ .../client/DaprClientBuilderConfigurer.java | 52 +++ .../pubsub/DaprPubSubProperties.java | 37 ++ .../statestore/DaprStateStoreProperties.java | 44 ++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../DaprClientAutoConfigurationTests.java | 47 ++ .../dapr-spring-boot-starter/pom.xml | 35 ++ dapr-spring/dapr-spring-core/pom.xml | 17 + .../core/client/DaprClientCustomizer.java | 31 ++ dapr-spring/dapr-spring-data/pom.xml | 24 ++ .../data/AbstractDaprKeyValueAdapter.java | 147 +++++++ .../data/DaprKeyValueAdapterResolver.java | 97 +++++ .../spring/data/DaprKeyValueTemplate.java | 402 ++++++++++++++++++ .../data/DefaultIdentifierGenerator.java | 98 +++++ .../spring/data/GeneratingIdAccessor.java | 72 ++++ .../spring/data/KeyValueAdapterResolver.java | 20 + .../spring/data/MySQLDaprKeyValueAdapter.java | 224 ++++++++++ .../data/PostgreSQLDaprKeyValueAdapter.java | 215 ++++++++++ .../config/DaprRepositoriesRegistrar.java | 35 ++ .../DaprRepositoryConfigurationExtension.java | 40 ++ .../config/EnableDaprRepositories.java | 139 ++++++ .../data/repository/query/DaprPredicate.java | 66 +++ .../query/DaprPredicateBuilder.java | 173 ++++++++ .../query/DaprPredicateQueryCreator.java | 102 +++++ dapr-spring/dapr-spring-messaging/pom.xml | 17 + .../messaging/DaprMessagingOperations.java | 66 +++ .../messaging/DaprMessagingTemplate.java | 87 ++++ dapr-spring/pom.xml | 169 ++++++++ dapr-spring/spotbugs-exclude.xml | 11 + pom.xml | 4 +- sdk-tests/pom.xml | 78 +++- .../it/methodinvoke/grpc/MethodInvokeIT.java | 7 +- .../it/methodinvoke/http/MethodInvokeIT.java | 4 +- .../it/spring/data/CustomMySQLContainer.java | 15 + .../spring/data/DaprKeyValueRepositoryIT.java | 140 ++++++ .../spring/data/DaprSpringDataConstants.java | 9 + .../data/MySQLDaprKeyValueTemplateIT.java | 240 +++++++++++ .../PostgreSQLDaprKeyValueTemplateIT.java | 222 ++++++++++ .../data/TestDaprSpringDataConfiguration.java | 44 ++ .../java/io/dapr/it/spring/data/TestType.java | 58 +++ .../it/spring/data/TestTypeRepository.java | 24 ++ .../messaging/DaprSpringMessagingIT.java | 100 +++++ .../it/spring/messaging/TestApplication.java | 51 +++ .../spring/messaging/TestRestController.java | 52 +++ .../it/testcontainers/DaprContainerTest.java | 61 ++- .../DaprPlacementContainerTest.java | 4 +- .../DaprTestcontainersModule.java | 39 ++ .../it/testcontainers/DaprWorkflowsTests.java | 89 ++++ .../dapr/it/testcontainers/FirstActivity.java | 28 ++ .../it/testcontainers/SecondActivity.java | 28 ++ .../SubscriptionsRestController.java | 40 ++ .../dapr/it/testcontainers/TestWorkflow.java | 48 +++ .../testcontainers/TestWorkflowPayload.java | 49 +++ .../TestWorkflowsApplication.java | 32 ++ sdk-tests/src/test/resources/query.json | 11 + sdk-workflows/pom.xml | 2 +- sdk/pom.xml | 30 +- testcontainers-dapr/pom.xml | 8 +- .../TestcontainersDaprClientCustomizer.java | 22 + 61 files changed, 4074 insertions(+), 71 deletions(-) create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/pom.xml create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientBuilderConfigurer.java create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/pubsub/DaprPubSubProperties.java create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/statestore/DaprStateStoreProperties.java create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTests.java create mode 100644 dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml create mode 100644 dapr-spring/dapr-spring-core/pom.xml create mode 100644 dapr-spring/dapr-spring-core/src/main/java/io/dapr/spring/core/client/DaprClientCustomizer.java create mode 100644 dapr-spring/dapr-spring-data/pom.xml create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/AbstractDaprKeyValueAdapter.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueAdapterResolver.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueTemplate.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DefaultIdentifierGenerator.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/GeneratingIdAccessor.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/KeyValueAdapterResolver.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/MySQLDaprKeyValueAdapter.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/PostgreSQLDaprKeyValueAdapter.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoriesRegistrar.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoryConfigurationExtension.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/EnableDaprRepositories.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicate.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateBuilder.java create mode 100644 dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateQueryCreator.java create mode 100644 dapr-spring/dapr-spring-messaging/pom.xml create mode 100644 dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java create mode 100644 dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java create mode 100644 dapr-spring/pom.xml create mode 100644 dapr-spring/spotbugs-exclude.xml create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/CustomMySQLContainer.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/DaprSpringDataConstants.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/TestType.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/data/TestTypeRepository.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsTests.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/FirstActivity.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/SecondActivity.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/SubscriptionsRestController.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflow.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowPayload.java create mode 100644 sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowsApplication.java create mode 100644 sdk-tests/src/test/resources/query.json create mode 100644 testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac3d084ee..a19ba54ea 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,9 +117,12 @@ jobs: uses: codecov/codecov-action@v4.1.0 - name: Install jars run: ./mvnw install -q -B -DskipTests + - name: Integration tests with testcontainers using spring boot version ${{ matrix.spring-boot-version }} + id: integration_tests_testcontainers + run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -f sdk-tests/pom.xml verify -Dtest.groups=testcontainers - name: Integration tests using spring boot version ${{ matrix.spring-boot-version }} id: integration_tests - run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -f sdk-tests/pom.xml verify + run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -f sdk-tests/pom.xml verify -Dtest.excluded.groups=testcontainers - name: Upload test report for sdk uses: actions/upload-artifact@v4 with: diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml new file mode 100644 index 000000000..2c43bf8cc --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + io.dapr.spring + dapr-spring-parent + 0.13.0-SNAPSHOT + + + dapr-spring-boot-autoconfigure + dapr-spring-boot-autoconfigure + Dapr Spring Boot Autoconfigure + jar + + + + io.dapr.spring + dapr-spring-core + ${project.parent.version} + true + + + io.dapr.spring + dapr-spring-data + ${project.parent.version} + true + + + io.dapr.spring + dapr-spring-messaging + ${project.parent.version} + true + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + org.springframework.data + spring-data-keyvalue + true + + + org.springframework.boot + spring-boot-starter-web + test + + + org.testcontainers + testcontainers + test + + + org.testcontainers + junit-jupiter + test + + + com.vaadin.external.google + android-json + + + + + io.dapr + testcontainers-dapr + ${dapr.sdk.alpha.version} + test + + + + diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java new file mode 100644 index 000000000..67b31816b --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 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.spring.boot.autoconfigure.client; + +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import io.dapr.spring.core.client.DaprClientCustomizer; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; + +import java.util.stream.Collectors; + +@AutoConfiguration +@ConditionalOnClass(DaprClient.class) +public class DaprClientAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + DaprClientBuilderConfigurer daprClientBuilderConfigurer(ObjectProvider customizerProvider) { + DaprClientBuilderConfigurer configurer = new DaprClientBuilderConfigurer(); + configurer.setDaprClientCustomizer(customizerProvider.orderedStream().collect(Collectors.toList())); + + return configurer; + } + + @Bean + @ConditionalOnMissingBean + DaprClientBuilder daprClientBuilder(DaprClientBuilderConfigurer daprClientBuilderConfigurer) { + DaprClientBuilder builder = new DaprClientBuilder(); + + return daprClientBuilderConfigurer.configure(builder); + } + + @Bean + @ConditionalOnMissingBean + DaprClient daprClient(DaprClientBuilder daprClientBuilder) { + return daprClientBuilder.build(); + } + +} diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientBuilderConfigurer.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientBuilderConfigurer.java new file mode 100644 index 000000000..ca83ab4fd --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/client/DaprClientBuilderConfigurer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.spring.boot.autoconfigure.client; + +import io.dapr.client.DaprClientBuilder; +import io.dapr.spring.core.client.DaprClientCustomizer; + +import java.util.List; + +/** + * Builder for configuring a {@link DaprClientBuilder}. + */ +public class DaprClientBuilderConfigurer { + + private List customizers; + + void setDaprClientCustomizer(List customizers) { + this.customizers = List.copyOf(customizers); + } + + /** + * Configure the specified {@link DaprClientBuilder}. The builder can be further + * tuned and default settings can be overridden. + * + * @param builder the {@link DaprClientBuilder} instance to configure + * @return the configured builder + */ + public DaprClientBuilder configure(DaprClientBuilder builder) { + applyCustomizers(builder); + return builder; + } + + private void applyCustomizers(DaprClientBuilder builder) { + if (this.customizers != null) { + for (DaprClientCustomizer customizer : this.customizers) { + customizer.customize(builder); + } + } + } + +} diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/pubsub/DaprPubSubProperties.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/pubsub/DaprPubSubProperties.java new file mode 100644 index 000000000..9cd038538 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/pubsub/DaprPubSubProperties.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 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.spring.boot.autoconfigure.pubsub; + +import org.springframework.boot.context.properties.ConfigurationProperties; + + +@ConfigurationProperties(prefix = DaprPubSubProperties.CONFIG_PREFIX) +public class DaprPubSubProperties { + + public static final String CONFIG_PREFIX = "dapr.pubsub"; + + /** + * Name of the PubSub Dapr component. + */ + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/statestore/DaprStateStoreProperties.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/statestore/DaprStateStoreProperties.java new file mode 100644 index 000000000..fba5f71f6 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/java/io/dapr/spring/boot/autoconfigure/statestore/DaprStateStoreProperties.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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.spring.boot.autoconfigure.statestore; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = DaprStateStoreProperties.CONFIG_PREFIX) +public class DaprStateStoreProperties { + + public static final String CONFIG_PREFIX = "dapr.statestore"; + + /** + * Name of the StateStore Dapr component. + */ + private String name; + private String binding; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getBinding() { + return binding; + } + + public void setBinding(String binding) { + this.binding = binding; + } +} diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..b583e70bf --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.dapr.spring.boot.autoconfigure.client.DaprClientAutoConfiguration diff --git a/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTests.java b/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTests.java new file mode 100644 index 000000000..5124e6064 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-autoconfigure/src/test/java/io/dapr/spring/boot/autoconfigure/client/DaprClientAutoConfigurationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 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.spring.boot.autoconfigure.client; + +import io.dapr.client.DaprClient; +import io.dapr.client.DaprClientBuilder; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link DaprClientAutoConfiguration}. + */ +class DaprClientAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(DaprClientAutoConfiguration.class)); + + @Test + void daprClientBuilderConfigurer() { + contextRunner.run(context -> assertThat(context).hasSingleBean(DaprClientBuilderConfigurer.class)); + } + + @Test + void daprClientBuilder() { + contextRunner.run(context -> assertThat(context).hasSingleBean(DaprClientBuilder.class)); + } + + @Test + void daprClient() { + contextRunner.run(context -> assertThat(context).hasSingleBean(DaprClient.class)); + } + +} diff --git a/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml new file mode 100644 index 000000000..89480c795 --- /dev/null +++ b/dapr-spring/dapr-spring-boot-starters/dapr-spring-boot-starter/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + io.dapr.spring + dapr-spring-parent + 0.13.0-SNAPSHOT + ../../pom.xml + + + dapr-spring-boot-starter + dapr-spring-boot-starter + Dapr Client Spring Boot Starter + jar + + + + org.springframework.boot + spring-boot-starter + + + io.dapr.spring + dapr-spring-core + ${project.parent.version} + + + io.dapr.spring + dapr-spring-boot-autoconfigure + ${project.parent.version} + + + + diff --git a/dapr-spring/dapr-spring-core/pom.xml b/dapr-spring/dapr-spring-core/pom.xml new file mode 100644 index 000000000..e3b037135 --- /dev/null +++ b/dapr-spring/dapr-spring-core/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + io.dapr.spring + dapr-spring-parent + 0.13.0-SNAPSHOT + + + dapr-spring-core + dapr-spring-core + Dapr Spring Core + jar + + diff --git a/dapr-spring/dapr-spring-core/src/main/java/io/dapr/spring/core/client/DaprClientCustomizer.java b/dapr-spring/dapr-spring-core/src/main/java/io/dapr/spring/core/client/DaprClientCustomizer.java new file mode 100644 index 000000000..425d7cbf8 --- /dev/null +++ b/dapr-spring/dapr-spring-core/src/main/java/io/dapr/spring/core/client/DaprClientCustomizer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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.spring.core.client; + +import io.dapr.client.DaprClientBuilder; + +/** + * Callback interface that can be used to customize a {@link DaprClientBuilder}. + */ +@FunctionalInterface +public interface DaprClientCustomizer { + + /** + * Callback to customize a {@link DaprClientBuilder} instance. + * + * @param daprClientBuilder the client builder to customize + */ + void customize(DaprClientBuilder daprClientBuilder); + +} diff --git a/dapr-spring/dapr-spring-data/pom.xml b/dapr-spring/dapr-spring-data/pom.xml new file mode 100644 index 000000000..98ccdeabd --- /dev/null +++ b/dapr-spring/dapr-spring-data/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + + io.dapr.spring + dapr-spring-parent + 0.13.0-SNAPSHOT + + + dapr-spring-data + dapr-spring-data + Dapr Spring Data + jar + + + + org.springframework.data + spring-data-keyvalue + + + + diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/AbstractDaprKeyValueAdapter.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/AbstractDaprKeyValueAdapter.java new file mode 100644 index 000000000..ecacda243 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/AbstractDaprKeyValueAdapter.java @@ -0,0 +1,147 @@ +/* + * Copyright 2024 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.spring.data; + +import io.dapr.client.DaprClient; +import io.dapr.client.domain.GetStateRequest; +import io.dapr.client.domain.SaveStateRequest; +import io.dapr.client.domain.State; +import io.dapr.utils.TypeRef; +import org.springframework.data.keyvalue.core.KeyValueAdapter; +import org.springframework.data.util.CloseableIterator; +import org.springframework.util.Assert; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.Map; + +public abstract class AbstractDaprKeyValueAdapter implements KeyValueAdapter { + private static final Map CONTENT_TYPE_META = Collections.singletonMap( + "contentType", "application/json"); + + private final DaprClient daprClient; + private final String stateStoreName; + + protected AbstractDaprKeyValueAdapter(DaprClient daprClient, String stateStoreName) { + Assert.notNull(daprClient, "DaprClient must not be null"); + Assert.hasText(stateStoreName, "State store name must not be empty"); + + this.daprClient = daprClient; + this.stateStoreName = stateStoreName; + } + + @Override + public void destroy() throws Exception { + daprClient.close(); + } + + @Override + public void clear() { + // Ignore + } + + @Override + public Object put(Object id, Object item, String keyspace) { + Assert.notNull(id, "Id must not be null"); + Assert.notNull(item, "Item must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + + String key = resolveKey(keyspace, id); + State state = new State<>(key, item, null, CONTENT_TYPE_META, null); + SaveStateRequest request = new SaveStateRequest(stateStoreName).setStates(state); + + daprClient.saveBulkState(request).block(); + + return item; + } + + @Override + public boolean contains(Object id, String keyspace) { + return get(id, keyspace) != null; + } + + @Override + public Object get(Object id, String keyspace) { + Assert.notNull(id, "Id must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + + String key = resolveKey(keyspace, id); + + return resolveValue(daprClient.getState(stateStoreName, key, Object.class)); + } + + @Override + public T get(Object id, String keyspace, Class type) { + Assert.notNull(id, "Id must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + Assert.notNull(type, "Type must not be null"); + + String key = resolveKey(keyspace, id); + GetStateRequest stateRequest = new GetStateRequest(stateStoreName, key).setMetadata(CONTENT_TYPE_META); + + return resolveValue(daprClient.getState(stateRequest, TypeRef.get(type))); + } + + @Override + public Object delete(Object id, String keyspace) { + Object result = get(id, keyspace); + + if (result == null) { + return null; + } + + String key = resolveKey(keyspace, id); + + daprClient.deleteState(stateStoreName, key).block(); + + return result; + } + + @Override + public T delete(Object id, String keyspace, Class type) { + T result = get(id, keyspace, type); + + if (result == null) { + return null; + } + + String key = resolveKey(keyspace, id); + + daprClient.deleteState(stateStoreName, key).block(); + + return result; + } + + @Override + public Iterable getAllOf(String keyspace) { + return getAllOf(keyspace, Object.class); + } + + @Override + public CloseableIterator> entries(String keyspace) { + throw new UnsupportedOperationException("'entries' method is not supported"); + } + + private String resolveKey(String keyspace, Object id) { + return String.format("%s-%s", keyspace, id); + } + + private T resolveValue(Mono> state) { + if (state == null) { + return null; + } + + return state.blockOptional().map(State::getValue).orElse(null); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueAdapterResolver.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueAdapterResolver.java new file mode 100644 index 000000000..48dce9a15 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueAdapterResolver.java @@ -0,0 +1,97 @@ +/* + * Copyright 2024 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.spring.data; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.client.domain.ComponentMetadata; +import io.dapr.client.domain.DaprMetadata; +import org.springframework.data.keyvalue.core.KeyValueAdapter; + +import java.util.List; +import java.util.Set; + +public class DaprKeyValueAdapterResolver implements KeyValueAdapterResolver { + private static final Set MYSQL_MARKERS = Set.of("state.mysql-v1", "bindings.mysql-v1"); + private static final Set POSTGRESQL_MARKERS = Set.of("state.postgresql-v1", "bindings.postgresql-v1"); + private final DaprClient daprClient; + private final ObjectMapper mapper; + private final String stateStoreName; + private final String bindingName; + + /** + * Constructs a {@link DaprKeyValueAdapterResolver}. + * + * @param daprClient The Dapr client. + * @param mapper The object mapper. + * @param stateStoreName The state store name. + * @param bindingName The binding name. + */ + public DaprKeyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper, String stateStoreName, + String bindingName) { + this.daprClient = daprClient; + this.mapper = mapper; + this.stateStoreName = stateStoreName; + this.bindingName = bindingName; + } + + @Override + public KeyValueAdapter resolve() { + DaprMetadata metadata = daprClient.getMetadata().block(); + + if (metadata == null) { + throw new IllegalStateException("No Dapr metadata found"); + } + + List components = metadata.getComponents(); + + if (components == null || components.isEmpty()) { + throw new IllegalStateException("No components found in Dapr metadata"); + } + + if (shouldUseMySQL(components, stateStoreName, bindingName)) { + return new MySQLDaprKeyValueAdapter(daprClient, mapper, stateStoreName, bindingName); + } + + if (shouldUsePostgreSQL(components, stateStoreName, bindingName)) { + return new PostgreSQLDaprKeyValueAdapter(daprClient, mapper, stateStoreName, bindingName); + } + + throw new IllegalStateException("Could find any adapter matching the given state store and binding"); + } + + @SuppressWarnings("AbbreviationAsWordInName") + private boolean shouldUseMySQL(List components, String stateStoreName, String bindingName) { + boolean stateStoreMatched = components.stream().anyMatch(x -> matchBy(stateStoreName, MYSQL_MARKERS, x)); + boolean bindingMatched = components.stream().anyMatch(x -> matchBy(bindingName, MYSQL_MARKERS, x)); + + return stateStoreMatched && bindingMatched; + } + + @SuppressWarnings("AbbreviationAsWordInName") + private boolean shouldUsePostgreSQL(List components, String stateStoreName, String bindingName) { + boolean stateStoreMatched = components.stream().anyMatch(x -> matchBy(stateStoreName, POSTGRESQL_MARKERS, x)); + boolean bindingMatched = components.stream().anyMatch(x -> matchBy(bindingName, POSTGRESQL_MARKERS, x)); + + return stateStoreMatched && bindingMatched; + } + + private boolean matchBy(String name, Set markers, ComponentMetadata componentMetadata) { + return componentMetadata.getName().equals(name) && markers.contains(getTypeAndVersion(componentMetadata)); + } + + private String getTypeAndVersion(ComponentMetadata component) { + return component.getType() + "-" + component.getVersion(); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueTemplate.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueTemplate.java new file mode 100644 index 000000000..c9b7f9f4d --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DaprKeyValueTemplate.java @@ -0,0 +1,402 @@ +/* + * Copyright 2024 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.spring.data; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.support.PersistenceExceptionTranslator; +import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.IdentifierGenerator; +import org.springframework.data.keyvalue.core.KeyValueAdapter; +import org.springframework.data.keyvalue.core.KeyValueCallback; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.core.KeyValuePersistenceExceptionTranslator; +import org.springframework.data.keyvalue.core.event.KeyValueEvent; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity; +import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; +import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +public class DaprKeyValueTemplate implements KeyValueOperations, ApplicationEventPublisherAware { + + private static final PersistenceExceptionTranslator DEFAULT_PERSISTENCE_EXCEPTION_TRANSLATOR = + new KeyValuePersistenceExceptionTranslator(); + + private final KeyValueAdapter adapter; + private final MappingContext, ? extends KeyValuePersistentProperty> + mappingContext; + private final IdentifierGenerator identifierGenerator; + + private PersistenceExceptionTranslator exceptionTranslator = DEFAULT_PERSISTENCE_EXCEPTION_TRANSLATOR; + private @Nullable ApplicationEventPublisher eventPublisher; + private boolean publishEvents = false; + private @SuppressWarnings("rawtypes") Set> eventTypesToPublish = Collections + .emptySet(); + + /** + * Create new {@link DaprKeyValueTemplate} using the given {@link KeyValueAdapterResolver} with a default + * {@link KeyValueMappingContext}. + * + * @param resolver must not be {@literal null}. + */ + public DaprKeyValueTemplate(KeyValueAdapterResolver resolver) { + this(resolver, new KeyValueMappingContext<>()); + } + + /** + * Create new {@link DaprKeyValueTemplate} using the given {@link KeyValueAdapterResolver} and {@link MappingContext}. + * + * @param resolver must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + @SuppressWarnings("LineLength") + public DaprKeyValueTemplate(KeyValueAdapterResolver resolver, + MappingContext, ? extends KeyValuePersistentProperty> mappingContext) { + this(resolver, mappingContext, DefaultIdentifierGenerator.INSTANCE); + } + + /** + * Create new {@link DaprKeyValueTemplate} using the given {@link KeyValueAdapterResolver} and {@link MappingContext}. + * + * @param resolver must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @param identifierGenerator must not be {@literal null}. + */ + @SuppressWarnings("LineLength") + public DaprKeyValueTemplate(KeyValueAdapterResolver resolver, + MappingContext, ? extends KeyValuePersistentProperty> mappingContext, + IdentifierGenerator identifierGenerator) { + Assert.notNull(resolver, "Resolver must not be null"); + Assert.notNull(mappingContext, "MappingContext must not be null"); + Assert.notNull(identifierGenerator, "IdentifierGenerator must not be null"); + + this.adapter = resolver.resolve(); + this.mappingContext = mappingContext; + this.identifierGenerator = identifierGenerator; + } + + private static boolean typeCheck(Class requiredType, @Nullable Object candidate) { + return candidate == null || ClassUtils.isAssignable(requiredType, candidate.getClass()); + } + + public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) { + Assert.notNull(exceptionTranslator, "ExceptionTranslator must not be null"); + this.exceptionTranslator = exceptionTranslator; + } + + /** + * Set the {@link ApplicationEventPublisher} to be used to publish {@link KeyValueEvent}s. + * + * @param eventTypesToPublish must not be {@literal null}. + */ + @SuppressWarnings("rawtypes") + public void setEventTypesToPublish(Set> eventTypesToPublish) { + if (CollectionUtils.isEmpty(eventTypesToPublish)) { + this.publishEvents = false; + } else { + this.publishEvents = true; + this.eventTypesToPublish = Collections.unmodifiableSet(eventTypesToPublish); + } + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.eventPublisher = applicationEventPublisher; + } + + @Override + public T insert(T objectToInsert) { + KeyValuePersistentEntity entity = getKeyValuePersistentEntity(objectToInsert); + GeneratingIdAccessor generatingIdAccessor = new GeneratingIdAccessor( + entity.getPropertyAccessor(objectToInsert), + entity.getIdProperty(), + identifierGenerator + ); + Object id = generatingIdAccessor.getOrGenerateIdentifier(); + + return insert(id, objectToInsert); + } + + @Override + public T insert(Object id, T objectToInsert) { + Assert.notNull(id, "Id for object to be inserted must not be null"); + Assert.notNull(objectToInsert, "Object to be inserted must not be null"); + + String keyspace = resolveKeySpace(objectToInsert.getClass()); + + potentiallyPublishEvent(KeyValueEvent.beforeInsert(id, keyspace, objectToInsert.getClass(), objectToInsert)); + + execute((KeyValueCallback) adapter -> { + + if (adapter.contains(id, keyspace)) { + throw new DuplicateKeyException( + String.format("Cannot insert existing object with id %s; Please use update", id)); + } + + adapter.put(id, objectToInsert, keyspace); + return null; + }); + + potentiallyPublishEvent(KeyValueEvent.afterInsert(id, keyspace, objectToInsert.getClass(), objectToInsert)); + + return objectToInsert; + } + + @Override + public T update(T objectToUpdate) { + KeyValuePersistentEntity entity = getKeyValuePersistentEntity(objectToUpdate); + + if (!entity.hasIdProperty()) { + throw new InvalidDataAccessApiUsageException( + String.format("Cannot determine id for type %s", ClassUtils.getUserClass(objectToUpdate))); + } + + return update(entity.getIdentifierAccessor(objectToUpdate).getRequiredIdentifier(), objectToUpdate); + } + + @Override + public T update(Object id, T objectToUpdate) { + Assert.notNull(id, "Id for object to be inserted must not be null"); + Assert.notNull(objectToUpdate, "Object to be updated must not be null"); + + String keyspace = resolveKeySpace(objectToUpdate.getClass()); + + potentiallyPublishEvent(KeyValueEvent.beforeUpdate(id, keyspace, objectToUpdate.getClass(), objectToUpdate)); + + Object existing = execute(adapter -> adapter.put(id, objectToUpdate, keyspace)); + + potentiallyPublishEvent( + KeyValueEvent.afterUpdate(id, keyspace, objectToUpdate.getClass(), objectToUpdate, existing)); + + return objectToUpdate; + } + + @Override + public Optional findById(Object id, Class type) { + Assert.notNull(id, "Id for object to be found must not be null"); + Assert.notNull(type, "Type to fetch must not be null"); + + String keyspace = resolveKeySpace(type); + + potentiallyPublishEvent(KeyValueEvent.beforeGet(id, keyspace, type)); + + T result = execute(adapter -> { + Object value = adapter.get(id, keyspace, type); + + if (value == null || typeCheck(type, value)) { + return type.cast(value); + } + + return null; + }); + + potentiallyPublishEvent(KeyValueEvent.afterGet(id, keyspace, type, result)); + + return Optional.ofNullable(result); + } + + @Override + public void delete(Class type) { + Assert.notNull(type, "Type to delete must not be null"); + + String keyspace = resolveKeySpace(type); + + potentiallyPublishEvent(KeyValueEvent.beforeDropKeySpace(keyspace, type)); + + execute((KeyValueCallback) adapter -> { + + adapter.deleteAllOf(keyspace); + return null; + }); + + potentiallyPublishEvent(KeyValueEvent.afterDropKeySpace(keyspace, type)); + } + + @SuppressWarnings("unchecked") + @Override + public T delete(T objectToDelete) { + Class type = (Class) ClassUtils.getUserClass(objectToDelete); + KeyValuePersistentEntity entity = getKeyValuePersistentEntity(objectToDelete); + Object id = entity.getIdentifierAccessor(objectToDelete).getIdentifier(); + + if (id == null) { + String error = String.format("Cannot determine id for type %s", ClassUtils.getUserClass(objectToDelete)); + + throw new InvalidDataAccessApiUsageException(error); + } + + return delete(id, type); + } + + @Override + public T delete(Object id, Class type) { + Assert.notNull(id, "Id for object to be deleted must not be null"); + Assert.notNull(type, "Type to delete must not be null"); + + String keyspace = resolveKeySpace(type); + + potentiallyPublishEvent(KeyValueEvent.beforeDelete(id, keyspace, type)); + + T result = execute(adapter -> adapter.delete(id, keyspace, type)); + + potentiallyPublishEvent(KeyValueEvent.afterDelete(id, keyspace, type, result)); + + return result; + } + + @Nullable + @Override + public T execute(KeyValueCallback action) { + Assert.notNull(action, "KeyValueCallback must not be null"); + + try { + return action.doInKeyValue(this.adapter); + } catch (RuntimeException e) { + throw resolveExceptionIfPossible(e); + } + } + + protected T executeRequired(KeyValueCallback action) { + T result = execute(action); + + if (result != null) { + return result; + } + + throw new IllegalStateException(String.format("KeyValueCallback %s returned null value", action)); + } + + @Override + public Iterable find(KeyValueQuery query, Class type) { + return executeRequired((KeyValueCallback>) adapter -> { + Iterable result = adapter.find(query, resolveKeySpace(type), type); + + List filtered = new ArrayList<>(); + + for (Object candidate : result) { + if (typeCheck(type, candidate)) { + filtered.add(type.cast(candidate)); + } + } + + return filtered; + }); + } + + @Override + public Iterable findAll(Class type) { + Assert.notNull(type, "Type to fetch must not be null"); + + return executeRequired(adapter -> { + Iterable values = adapter.getAllOf(resolveKeySpace(type), type); + + ArrayList filtered = new ArrayList<>(); + for (Object candidate : values) { + if (typeCheck(type, candidate)) { + filtered.add(type.cast(candidate)); + } + } + + return filtered; + }); + } + + @SuppressWarnings("rawtypes") + @Override + public Iterable findAll(Sort sort, Class type) { + return find(new KeyValueQuery(sort), type); + } + + @SuppressWarnings("rawtypes") + @Override + public Iterable findInRange(long offset, int rows, Class type) { + return find(new KeyValueQuery().skip(offset).limit(rows), type); + } + + @SuppressWarnings("rawtypes") + @Override + public Iterable findInRange(long offset, int rows, Sort sort, Class type) { + return find(new KeyValueQuery(sort).skip(offset).limit(rows), type); + } + + @Override + public long count(Class type) { + Assert.notNull(type, "Type for count must not be null"); + return adapter.count(resolveKeySpace(type)); + } + + @Override + public long count(KeyValueQuery query, Class type) { + return executeRequired(adapter -> adapter.count(query, resolveKeySpace(type))); + } + + @Override + public boolean exists(KeyValueQuery query, Class type) { + return executeRequired(adapter -> adapter.exists(query, resolveKeySpace(type))); + } + + @Override + public MappingContext getMappingContext() { + return this.mappingContext; + } + + @Override + public KeyValueAdapter getKeyValueAdapter() { + return adapter; + } + + @Override + public void destroy() throws Exception { + this.adapter.destroy(); + } + + private KeyValuePersistentEntity getKeyValuePersistentEntity(Object objectToInsert) { + return this.mappingContext.getRequiredPersistentEntity(ClassUtils.getUserClass(objectToInsert)); + } + + private String resolveKeySpace(Class type) { + return this.mappingContext.getRequiredPersistentEntity(type).getKeySpace(); + } + + private RuntimeException resolveExceptionIfPossible(RuntimeException e) { + DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(e); + + return translatedException != null ? translatedException : e; + } + + @SuppressWarnings("rawtypes") + private void potentiallyPublishEvent(KeyValueEvent event) { + if (eventPublisher == null) { + return; + } + + if (publishEvents && (eventTypesToPublish.isEmpty() || eventTypesToPublish.contains(event.getClass()))) { + eventPublisher.publishEvent(event); + } + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DefaultIdentifierGenerator.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DefaultIdentifierGenerator.java new file mode 100644 index 000000000..bc4f41bca --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/DefaultIdentifierGenerator.java @@ -0,0 +1,98 @@ +/* + * Copyright 2024 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.spring.data; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.keyvalue.core.IdentifierGenerator; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Default implementation of {@link IdentifierGenerator} to generate identifiers of types {@link UUID}. + */ +enum DefaultIdentifierGenerator implements IdentifierGenerator { + + INSTANCE; + + private final AtomicReference secureRandom = new AtomicReference<>(null); + + @Override + @SuppressWarnings("unchecked") + public T generateIdentifierOfType(TypeInformation identifierType) { + + Class type = identifierType.getType(); + + if (ClassUtils.isAssignable(UUID.class, type)) { + return (T) UUID.randomUUID(); + } else if (ClassUtils.isAssignable(String.class, type)) { + return (T) UUID.randomUUID().toString(); + } else if (ClassUtils.isAssignable(Integer.class, type)) { + return (T) Integer.valueOf(getSecureRandom().nextInt()); + } else if (ClassUtils.isAssignable(Long.class, type)) { + return (T) Long.valueOf(getSecureRandom().nextLong()); + } + + throw new InvalidDataAccessApiUsageException( + String.format("Identifier cannot be generated for %s; Supported types are: UUID, String, Integer, and Long", + identifierType.getType().getName())); + } + + private SecureRandom getSecureRandom() { + + SecureRandom secureRandom = this.secureRandom.get(); + if (secureRandom != null) { + return secureRandom; + } + + for (String algorithm : OsTools.secureRandomAlgorithmNames()) { + try { + secureRandom = SecureRandom.getInstance(algorithm); + } catch (NoSuchAlgorithmException e) { + // ignore and try next. + } + } + + if (secureRandom == null) { + throw new InvalidDataAccessApiUsageException( + String.format("Could not create SecureRandom instance for one of the algorithms '%s'", + StringUtils.collectionToCommaDelimitedString(OsTools.secureRandomAlgorithmNames()))); + } + + this.secureRandom.compareAndSet(null, secureRandom); + + return secureRandom; + } + + private static class OsTools { + + private static final String OPERATING_SYSTEM_NAME = System.getProperty("os.name").toLowerCase(); + + private static final List SECURE_RANDOM_ALGORITHMS_LINUX_OSX_SOLARIS = Arrays.asList("NativePRNGBlocking", + "NativePRNGNonBlocking", "NativePRNG", "SHA1PRNG"); + private static final List SECURE_RANDOM_ALGORITHMS_WINDOWS = Arrays.asList("SHA1PRNG", "Windows-PRNG"); + + static List secureRandomAlgorithmNames() { + return OPERATING_SYSTEM_NAME.contains("win") ? SECURE_RANDOM_ALGORITHMS_WINDOWS + : SECURE_RANDOM_ALGORITHMS_LINUX_OSX_SOLARIS; + } + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/GeneratingIdAccessor.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/GeneratingIdAccessor.java new file mode 100644 index 000000000..9374a5c38 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/GeneratingIdAccessor.java @@ -0,0 +1,72 @@ +/* + * Copyright 2024 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.spring.data; + +import org.springframework.data.keyvalue.core.IdentifierGenerator; +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.util.Assert; + +/** + * {@link IdentifierAccessor} adding a {@link #getOrGenerateIdentifier()} to automatically generate an identifier and + * set it on the underling bean instance. + * + * @see #getOrGenerateIdentifier() + */ +class GeneratingIdAccessor implements IdentifierAccessor { + + private final PersistentPropertyAccessor accessor; + private final PersistentProperty identifierProperty; + private final IdentifierGenerator generator; + + /** + * Creates a new {@link GeneratingIdAccessor} using the given {@link PersistentPropertyAccessor}, identifier property + * and {@link IdentifierGenerator}. + * + * @param accessor must not be {@literal null}. + * @param identifierProperty must not be {@literal null}. + * @param generator must not be {@literal null}. + */ + GeneratingIdAccessor(PersistentPropertyAccessor accessor, PersistentProperty identifierProperty, + IdentifierGenerator generator) { + + Assert.notNull(accessor, "PersistentPropertyAccessor must not be null"); + Assert.notNull(identifierProperty, "Identifier property must not be null"); + Assert.notNull(generator, "IdentifierGenerator must not be null"); + + this.accessor = accessor; + this.identifierProperty = identifierProperty; + this.generator = generator; + } + + @Override + public Object getIdentifier() { + return accessor.getProperty(identifierProperty); + } + + Object getOrGenerateIdentifier() { + + Object existingIdentifier = getIdentifier(); + + if (existingIdentifier != null) { + return existingIdentifier; + } + + Object generatedIdentifier = generator.generateIdentifierOfType(identifierProperty.getTypeInformation()); + accessor.setProperty(identifierProperty, generatedIdentifier); + + return generatedIdentifier; + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/KeyValueAdapterResolver.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/KeyValueAdapterResolver.java new file mode 100644 index 000000000..04b64c3ed --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/KeyValueAdapterResolver.java @@ -0,0 +1,20 @@ +/* + * Copyright 2024 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.spring.data; + +import org.springframework.data.keyvalue.core.KeyValueAdapter; + +public interface KeyValueAdapterResolver { + KeyValueAdapter resolve(); +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/MySQLDaprKeyValueAdapter.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/MySQLDaprKeyValueAdapter.java new file mode 100644 index 000000000..ad401817f --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/MySQLDaprKeyValueAdapter.java @@ -0,0 +1,224 @@ +/* + * Copyright 2024 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.spring.data; + +import com.fasterxml.jackson.core.JsonPointer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.utils.TypeRef; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +import java.util.Base64; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A {@link org.springframework.data.keyvalue.core.KeyValueAdapter} implementation for MySQL. + */ +@SuppressWarnings("AbbreviationAsWordInName") +public class MySQLDaprKeyValueAdapter extends AbstractDaprKeyValueAdapter { + private static final String DELETE_BY_KEYSPACE_PATTERN = "delete from state where id LIKE '%s'"; + private static final String SELECT_BY_KEYSPACE_PATTERN = "select value from state where id LIKE '%s'"; + private static final String SELECT_BY_FILTER_PATTERN = + "select value from state where id LIKE '%s' and JSON_EXTRACT(value, %s) = %s"; + private static final String COUNT_BY_KEYSPACE_PATTERN = "select count(*) as value from state where id LIKE '%s'"; + private static final String COUNT_BY_FILTER_PATTERN = + "select count(*) as value from state where id LIKE '%s' and JSON_EXTRACT(value, %s) = %s"; + + private static final TypeRef> FILTER_TYPE_REF = new TypeRef<>() { + }; + private static final TypeRef> COUNT_TYPE_REF = new TypeRef<>() { + }; + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + private static final JsonPointer VALUE_POINTER = JsonPointer.compile("/value"); + + private final DaprClient daprClient; + private final ObjectMapper mapper; + private final String stateStoreName; + private final String bindingName; + + /** + * Constructs a {@link MySQLDaprKeyValueAdapter}. + * + * @param daprClient The Dapr client. + * @param mapper The object mapper. + * @param stateStoreName The state store name. + * @param bindingName The binding name. + */ + public MySQLDaprKeyValueAdapter(DaprClient daprClient, ObjectMapper mapper, String stateStoreName, + String bindingName) { + super(daprClient, stateStoreName); + + Assert.notNull(mapper, "ObjectMapper must not be null"); + Assert.hasText(bindingName, "State store binding must not be empty"); + + this.daprClient = daprClient; + this.mapper = mapper; + this.stateStoreName = stateStoreName; + this.bindingName = bindingName; + } + + + @Override + public Iterable getAllOf(String keyspace, Class type) { + Assert.hasText(keyspace, "Keyspace must not be empty"); + Assert.notNull(type, "Type must not be null"); + + String sql = createSql(SELECT_BY_KEYSPACE_PATTERN, keyspace); + List result = queryUsingBinding(sql, FILTER_TYPE_REF); + + return convertValues(result, type); + } + + @Override + public void deleteAllOf(String keyspace) { + Assert.hasText(keyspace, "Keyspace must not be empty"); + + String sql = createSql(DELETE_BY_KEYSPACE_PATTERN, keyspace); + + execUsingBinding(sql); + } + + @Override + public Iterable find(KeyValueQuery query, String keyspace, Class type) { + Assert.notNull(query, "Query must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + Assert.notNull(type, "Type must not be null"); + + Object criteria = query.getCriteria(); + + if (criteria == null) { + return getAllOf(keyspace, type); + } + + String sql = createSql(SELECT_BY_FILTER_PATTERN, keyspace, criteria); + List result = queryUsingBinding(sql, FILTER_TYPE_REF); + + return convertValues(result, type); + } + + @Override + public long count(String keyspace) { + Assert.hasText(keyspace, "Keyspace must not be empty"); + + String sql = createSql(COUNT_BY_KEYSPACE_PATTERN, keyspace); + List result = queryUsingBinding(sql, COUNT_TYPE_REF); + + return extractCount(result); + } + + @Override + public long count(KeyValueQuery query, String keyspace) { + Assert.notNull(query, "Query must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + + Object criteria = query.getCriteria(); + + if (criteria == null) { + return count(keyspace); + } + + String sql = createSql(COUNT_BY_FILTER_PATTERN, keyspace, criteria); + List result = queryUsingBinding(sql, COUNT_TYPE_REF); + + return extractCount(result); + } + + private String getKeyspaceFilter(String keyspace) { + return String.format("%s||%s-%%", stateStoreName, keyspace); + } + + private String createSql(String sqlPattern, String keyspace) { + String keyspaceFilter = getKeyspaceFilter(keyspace); + + return String.format(sqlPattern, keyspaceFilter); + } + + private String createSql(String sqlPattern, String keyspace, Object criteria) { + String keyspaceFilter = getKeyspaceFilter(keyspace); + SpelExpression expression = PARSER.parseRaw(criteria.toString()); + SpelNode leftNode = expression.getAST().getChild(0); + SpelNode rightNode = expression.getAST().getChild(1); + String left = String.format("'$.%s'", leftNode.toStringAST()); + String right = rightNode.toStringAST(); + + return String.format(sqlPattern, keyspaceFilter, left, right); + } + + private void execUsingBinding(String sql) { + Map meta = Collections.singletonMap("sql", sql); + + daprClient.invokeBinding(bindingName, "exec", null, meta).block(); + } + + private T queryUsingBinding(String sql, TypeRef typeRef) { + Map meta = Collections.singletonMap("sql", sql); + + return daprClient.invokeBinding(bindingName, "query", null, meta, typeRef).block(); + } + + private List convertValues(List values, Class type) { + if (values == null || values.isEmpty()) { + return Collections.emptyList(); + } + + return values.stream() + .map(value -> convertValue(value, type)) + .collect(Collectors.toList()); + } + + private T convertValue(JsonNode value, Class type) { + JsonNode valueNode = value.at(VALUE_POINTER); + + if (valueNode.isMissingNode()) { + throw new IllegalStateException("Value is missing"); + } + + try { + // The value is stored as a base64 encoded string and wrapped in quotes + // hence we need to remove the quotes and then decode + String rawValue = valueNode.toString().replace("\"", ""); + byte[] decodedValue = Base64.getDecoder().decode(rawValue); + + return mapper.readValue(decodedValue, type); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private long extractCount(List values) { + if (values == null || values.isEmpty()) { + return 0; + } + + JsonNode valueNode = values.get(0).at(VALUE_POINTER); + + if (valueNode.isMissingNode()) { + throw new IllegalStateException("Count value is missing"); + } + + if (!valueNode.isNumber()) { + throw new IllegalStateException("Count value is not a number"); + } + + return valueNode.asLong(); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/PostgreSQLDaprKeyValueAdapter.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/PostgreSQLDaprKeyValueAdapter.java new file mode 100644 index 000000000..40c22db2f --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/PostgreSQLDaprKeyValueAdapter.java @@ -0,0 +1,215 @@ +/* + * Copyright 2024 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.spring.data; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.spring.data.repository.query.DaprPredicate; +import io.dapr.utils.TypeRef; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.expression.spel.SpelNode; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * A {@link org.springframework.data.keyvalue.core.KeyValueAdapter} implementation for PostgreSQL. + */ +@SuppressWarnings("AbbreviationAsWordInName") +public class PostgreSQLDaprKeyValueAdapter extends AbstractDaprKeyValueAdapter { + private static final String DELETE_BY_KEYSPACE_PATTERN = "delete from state where key LIKE '%s'"; + private static final String SELECT_BY_KEYSPACE_PATTERN = "select value from state where key LIKE '%s'"; + private static final String SELECT_BY_FILTER_PATTERN = + "select value from state where key LIKE '%s' and JSONB_EXTRACT_PATH_TEXT(value, %s) = %s"; + private static final String COUNT_BY_KEYSPACE_PATTERN = "select count(*) as value from state where key LIKE '%s'"; + private static final String COUNT_BY_FILTER_PATTERN = + "select count(*) as value from state where key LIKE '%s' and JSONB_EXTRACT_PATH_TEXT(value, %s) = %s"; + + private static final TypeRef>> FILTER_TYPE_REF = new TypeRef<>() { + }; + private static final TypeRef>> COUNT_TYPE_REF = new TypeRef<>() { + }; + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + + private final DaprClient daprClient; + private final ObjectMapper mapper; + private final String stateStoreName; + private final String bindingName; + + /** + * Constructs a {@link PostgreSQLDaprKeyValueAdapter}. + * + * @param daprClient The Dapr client. + * @param mapper The object mapper. + * @param stateStoreName The state store name. + * @param bindingName The binding name. + */ + public PostgreSQLDaprKeyValueAdapter(DaprClient daprClient, ObjectMapper mapper, String stateStoreName, + String bindingName) { + super(daprClient, stateStoreName); + + Assert.notNull(mapper, "ObjectMapper must not be null"); + Assert.hasText(bindingName, "State store binding must not be empty"); + + this.daprClient = daprClient; + this.mapper = mapper; + this.stateStoreName = stateStoreName; + this.bindingName = bindingName; + } + + @Override + public Iterable getAllOf(String keyspace, Class type) { + Assert.hasText(keyspace, "Keyspace must not be empty"); + Assert.notNull(type, "Type must not be null"); + + String sql = createSql(SELECT_BY_KEYSPACE_PATTERN, keyspace); + List> result = queryUsingBinding(sql, FILTER_TYPE_REF); + + return convertValues(result, type); + } + + @Override + public void deleteAllOf(String keyspace) { + Assert.hasText(keyspace, "Keyspace must not be empty"); + + String sql = createSql(DELETE_BY_KEYSPACE_PATTERN, keyspace); + + execUsingBinding(sql); + } + + @Override + public Iterable find(KeyValueQuery query, String keyspace, Class type) { + Assert.notNull(query, "Query must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + Assert.notNull(type, "Type must not be null"); + + Object criteria = query.getCriteria(); + + if (criteria == null) { + return getAllOf(keyspace, type); + } + + String sql = createSql(SELECT_BY_FILTER_PATTERN, keyspace, criteria); + List> result = queryUsingBinding(sql, FILTER_TYPE_REF); + + return convertValues(result, type); + } + + @Override + public long count(String keyspace) { + Assert.hasText(keyspace, "Keyspace must not be empty"); + + String sql = createSql(COUNT_BY_KEYSPACE_PATTERN, keyspace); + List> result = queryUsingBinding(sql, COUNT_TYPE_REF); + + return extractCount(result); + } + + @Override + public long count(KeyValueQuery query, String keyspace) { + Assert.notNull(query, "Query must not be null"); + Assert.hasText(keyspace, "Keyspace must not be empty"); + + Object criteria = query.getCriteria(); + + if (criteria == null) { + return count(keyspace); + } + + String sql = createSql(COUNT_BY_FILTER_PATTERN, keyspace, criteria); + List> result = queryUsingBinding(sql, COUNT_TYPE_REF); + + return extractCount(result); + } + + private String getKeyspaceFilter(String keyspace) { + return String.format("%s||%s-%%", stateStoreName, keyspace); + } + + private String createSql(String sqlPattern, String keyspace) { + String keyspaceFilter = getKeyspaceFilter(keyspace); + + return String.format(sqlPattern, keyspaceFilter); + } + + private String createSql(String sqlPattern, String keyspace, Object criteria) { + String keyspaceFilter = getKeyspaceFilter(keyspace); + + if (criteria instanceof DaprPredicate daprPredicate) { + String path = daprPredicate.getPath().toString(); + String pathWithOutType = String.format("'%s'", path.substring(path.indexOf(".") + 1)); + String value = String.format("'%s'", daprPredicate.getValue().toString()); + + return String.format(sqlPattern, keyspaceFilter, pathWithOutType, value); + } else if (criteria instanceof String) { + SpelExpression expression = PARSER.parseRaw(criteria.toString()); + SpelNode leftNode = expression.getAST().getChild(0); + SpelNode rightNode = expression.getAST().getChild(1); + String left = String.format("'%s'", leftNode.toStringAST()); + String right = rightNode.toStringAST(); + + return String.format(sqlPattern, keyspaceFilter, left, right); + } + + return null; + } + + private void execUsingBinding(String sql) { + Map meta = Collections.singletonMap("sql", sql); + + daprClient.invokeBinding(bindingName, "exec", null, meta).block(); + } + + private T queryUsingBinding(String sql, TypeRef typeRef) { + Map meta = Collections.singletonMap("sql", sql); + + return daprClient.invokeBinding(bindingName, "query", null, meta, typeRef).block(); + } + + private Iterable convertValues(List> values, Class type) { + if (values == null || values.isEmpty()) { + return Collections.emptyList(); + } + + return values.stream() + .flatMap(Collection::stream) + .map(value -> convertValue(value, type)) + .collect(Collectors.toList()); + } + + private T convertValue(Object value, Class type) { + try { + return mapper.convertValue(value, type); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private long extractCount(List> values) { + if (values == null || values.isEmpty()) { + return 0; + } + + return values.stream() + .flatMap(Collection::stream) + .collect(Collectors.toList()) + .get(0); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoriesRegistrar.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoriesRegistrar.java new file mode 100644 index 000000000..7946e07b4 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoriesRegistrar.java @@ -0,0 +1,35 @@ +/* + * Copyright 2024 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.spring.data.repository.config; + +import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +import java.lang.annotation.Annotation; + +/** + * Dapr specific {@link RepositoryBeanDefinitionRegistrarSupport} implementation. + */ +public class DaprRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { + + @Override + protected Class getAnnotation() { + return EnableDaprRepositories.class; + } + + @Override + protected RepositoryConfigurationExtension getExtension() { + return new DaprRepositoryConfigurationExtension(); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoryConfigurationExtension.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoryConfigurationExtension.java new file mode 100644 index 000000000..a487d7aa5 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/DaprRepositoryConfigurationExtension.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.spring.data.repository.config; + +import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension; +import org.springframework.data.repository.config.RepositoryConfigurationExtension; + +/** + * {@link RepositoryConfigurationExtension} for Dapr-based repositories. + */ +@SuppressWarnings("unchecked") +public class DaprRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension { + + @Override + public String getModuleName() { + return "Dapr"; + } + + @Override + protected String getModulePrefix() { + return "dapr"; + } + + @Override + protected String getDefaultKeyValueTemplateRef() { + return "daprKeyValueTemplate"; + } + +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/EnableDaprRepositories.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/EnableDaprRepositories.java new file mode 100644 index 000000000..3ede019fc --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/config/EnableDaprRepositories.java @@ -0,0 +1,139 @@ +/* + * Copyright 2024 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.spring.data.repository.config; + +import io.dapr.spring.data.repository.query.DaprPredicateQueryCreator; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Import; +import org.springframework.data.keyvalue.core.KeyValueOperations; +import org.springframework.data.keyvalue.repository.config.QueryCreatorType; +import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean; +import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to activate Dapr repositories. If no base package is configured through either {@link #value()}, + * {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@Import(DaprRepositoriesRegistrar.class) +@QueryCreatorType(DaprPredicateQueryCreator.class) +public @interface EnableDaprRepositories { + + /** + * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.: + * {@code @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(basePackages="org.my.pkg")}. + * + * @return alias of the base package + */ + String[] value() default {}; + + /** + * Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this + * attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names. + * + * @return array of base packages + */ + String[] basePackages() default {}; + + /** + * Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The + * package of each class specified will be scanned. Consider creating a special no-op marker class or interface in + * each package that serves no purpose other than being referenced by this attribute. + * + * @return array of base classes + */ + Class[] basePackageClasses() default {}; + + /** + * Specifies which types are not eligible for component scanning. + * + * @return array of exclusion filters + */ + Filter[] excludeFilters() default {}; + + /** + * Specifies which types are eligible for component scanning. Further narrows the set of candidate components from + * everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters. + * + * @return array of inclusion filters + */ + Filter[] includeFilters() default {}; + + /** + * Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So + * for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning + * for {@code PersonRepositoryImpl}. + * + * @return repository implementation post fix + */ + String repositoryImplementationPostfix() default "Impl"; + + /** + * Configures the location of where to find the Spring Data named queries properties file. + * + * @return named queries location + */ + String namedQueriesLocation() default ""; + + /** + * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to + * {@link Key#CREATE_IF_NOT_FOUND}. + * + * @return key lookup strategy + */ + Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND; + + /** + * Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to + * {@link KeyValueRepositoryFactoryBean}. + * + * @return repository factory bean class + */ + Class repositoryFactoryBeanClass() default KeyValueRepositoryFactoryBean.class; + + /** + * Configure the repository base class to be used to create repository proxies for this particular configuration. + * + * @return repository base class + */ + Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; + + /** + * Configures the name of the {@link KeyValueOperations} bean to be used with the repositories detected. + * + * @return the Key value template bean name + */ + String keyValueTemplateRef() default "daprKeyValueTemplate"; + + /** + * Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the + * repositories infrastructure. + * + * @return whether to consider nested repository interfaces + */ + boolean considerNestedRepositories() default false; +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicate.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicate.java new file mode 100644 index 000000000..d681ce8e6 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicate.java @@ -0,0 +1,66 @@ +package io.dapr.spring.data.repository.query; + +import org.springframework.beans.BeanWrapper; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.util.DirectFieldAccessFallbackBeanWrapper; +import org.springframework.util.ObjectUtils; + +import java.util.function.Function; +import java.util.function.Predicate; + +public class DaprPredicate implements Predicate { + + private final PropertyPath path; + private final Function check; + private final Object value; + + public DaprPredicate(PropertyPath path, Object expected) { + this(path, expected, (valueToCompare) -> ObjectUtils.nullSafeEquals(valueToCompare, expected)); + } + + + /** + * Creates a new {@link DaprPredicate}. + * + * @param path The path to the property to compare. + * @param value The value to compare. + * @param check The function to check the value. + */ + public DaprPredicate(PropertyPath path, Object value, Function check) { + this.path = path; + this.check = check; + this.value = value; + } + + public PropertyPath getPath() { + return path; + } + + public Object getValue() { + return value; + } + + @Override + public boolean test(Object o) { + Object value = getValueByPath(o, path); + return check.apply(value); + } + + private Object getValueByPath(Object root, PropertyPath path) { + Object currentValue = root; + + for (PropertyPath currentPath : path) { + currentValue = wrap(currentValue).getPropertyValue(currentPath.getSegment()); + + if (currentValue == null) { + break; + } + } + + return currentValue; + } + + private BeanWrapper wrap(Object o) { + return new DirectFieldAccessFallbackBeanWrapper(o); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateBuilder.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateBuilder.java new file mode 100644 index 000000000..1eff32b98 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateBuilder.java @@ -0,0 +1,173 @@ +/* + * Copyright 2024 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.spring.data.repository.query; + +import org.springframework.data.repository.query.parser.Part; +import org.springframework.util.ObjectUtils; +import org.springframework.util.comparator.Comparators; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +class DaprPredicateBuilder { + + private final Part part; + + private DaprPredicateBuilder(Part part) { + this.part = part; + } + + static DaprPredicateBuilder propertyValueOf(Part part) { + return new DaprPredicateBuilder(part); + } + + Predicate isTrue() { + return new DaprPredicate(part.getProperty(), true); + } + + public Predicate isFalse() { + return new DaprPredicate(part.getProperty(), false); + } + + public Predicate isEqualTo(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> { + if (!ObjectUtils.nullSafeEquals(Part.IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { + if (o instanceof String s1 && value instanceof String s2) { + return s1.equalsIgnoreCase(s2); + } + } + + return ObjectUtils.nullSafeEquals(o, value); + }); + } + + public Predicate isNull() { + return new DaprPredicate(part.getProperty(), null, Objects::isNull); + } + + public Predicate isNotNull() { + return isNull().negate(); + } + + public Predicate isLessThan(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) < 0); + } + + public Predicate isLessThanEqual(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) <= 0); + } + + public Predicate isGreaterThan(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) > 0); + } + + public Predicate isGreaterThanEqual(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) >= 0); + } + + public Predicate matches(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> { + if (o == null || value == null) { + return ObjectUtils.nullSafeEquals(o, value); + } + + if (value instanceof Pattern pattern) { + return pattern.matcher(o.toString()).find(); + } + + return o.toString().matches(value.toString()); + }); + } + + public Predicate in(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> { + if (value instanceof Collection collection) { + if (o instanceof Collection subSet) { + return collection.containsAll(subSet); + } + + return collection.contains(o); + } + + if (ObjectUtils.isArray(value)) { + return ObjectUtils.containsElement(ObjectUtils.toObjectArray(value), value); + } + + return false; + }); + } + + public Predicate contains(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> { + if (o == null) { + return false; + } + + if (o instanceof Collection collection) { + return collection.contains(value); + } + + if (ObjectUtils.isArray(o)) { + return ObjectUtils.containsElement(ObjectUtils.toObjectArray(o), value); + } + + if (o instanceof Map map) { + return map.containsValue(value); + } + + if (value == null) { + return false; + } + + String s = o.toString(); + + if (ObjectUtils.nullSafeEquals(Part.IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { + return s.contains(value.toString()); + } + + return s.toLowerCase().contains(value.toString().toLowerCase()); + }); + } + + public Predicate startsWith(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> { + if (!(o instanceof String s)) { + return false; + } + + if (ObjectUtils.nullSafeEquals(Part.IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { + return s.startsWith(value.toString()); + } + + return s.toLowerCase().startsWith(value.toString().toLowerCase()); + }); + } + + public Predicate endsWith(Object value) { + return new DaprPredicate(part.getProperty(), value, o -> { + if (!(o instanceof String s)) { + return false; + } + + if (ObjectUtils.nullSafeEquals(Part.IgnoreCaseType.NEVER, part.shouldIgnoreCase())) { + return s.endsWith(value.toString()); + } + + return s.toLowerCase().endsWith(value.toString().toLowerCase()); + }); + } +} diff --git a/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateQueryCreator.java b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateQueryCreator.java new file mode 100644 index 000000000..436f197f7 --- /dev/null +++ b/dapr-spring/dapr-spring-data/src/main/java/io/dapr/spring/data/repository/query/DaprPredicateQueryCreator.java @@ -0,0 +1,102 @@ +/* + * Copyright 2024 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.spring.data.repository.query; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Sort; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.parser.AbstractQueryCreator; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.lang.Nullable; + +import java.util.Iterator; +import java.util.function.Predicate; + +/** + * This class is copied from https://github.com/spring-projects/spring-data-keyvalue/blob/ff441439124585042dd0cbff952f977a343444d2/src/main/java/org/springframework/data/keyvalue/repository/query/PredicateQueryCreator.java#L46 + * because it has private accessors to internal classes, making it impossible to extend or use the original + * This requires to be created from scratch to not use predicates, but this is only worth it if we can prove these + * abstractions are worth the time. + */ +public class DaprPredicateQueryCreator extends AbstractQueryCreator>, Predicate> { + + public DaprPredicateQueryCreator(PartTree tree, ParameterAccessor parameters) { + super(tree, parameters); + } + + @Override + protected Predicate create(Part part, Iterator iterator) { + DaprPredicateBuilder daprPredicateBuilder = DaprPredicateBuilder.propertyValueOf(part); + + switch (part.getType()) { + case TRUE: + return daprPredicateBuilder.isTrue(); + case FALSE: + return daprPredicateBuilder.isFalse(); + case SIMPLE_PROPERTY: + return daprPredicateBuilder.isEqualTo(iterator.next()); + case IS_NULL: + return daprPredicateBuilder.isNull(); + case IS_NOT_NULL: + return daprPredicateBuilder.isNotNull(); + case LIKE: + return daprPredicateBuilder.contains(iterator.next()); + case STARTING_WITH: + return daprPredicateBuilder.startsWith(iterator.next()); + case AFTER: + case GREATER_THAN: + return daprPredicateBuilder.isGreaterThan(iterator.next()); + case GREATER_THAN_EQUAL: + return daprPredicateBuilder.isGreaterThanEqual(iterator.next()); + case BEFORE: + case LESS_THAN: + return daprPredicateBuilder.isLessThan(iterator.next()); + case LESS_THAN_EQUAL: + return daprPredicateBuilder.isLessThanEqual(iterator.next()); + case ENDING_WITH: + return daprPredicateBuilder.endsWith(iterator.next()); + case BETWEEN: + return daprPredicateBuilder.isGreaterThan(iterator.next()) + .and(daprPredicateBuilder.isLessThan(iterator.next())); + case REGEX: + return daprPredicateBuilder.matches(iterator.next()); + case IN: + return daprPredicateBuilder.in(iterator.next()); + default: + throw new InvalidDataAccessApiUsageException(String.format("Found invalid part '%s' in query", part.getType())); + + } + } + + @Override + protected Predicate and(Part part, Predicate base, Iterator iterator) { + return base.and((Predicate) create(part, iterator)); + } + + @Override + protected Predicate or(Predicate base, Predicate criteria) { + return base.or((Predicate) criteria); + } + + @Override + protected KeyValueQuery> complete(@Nullable Predicate criteria, Sort sort) { + if (criteria == null) { + return new KeyValueQuery<>(it -> true, sort); + } + return new KeyValueQuery<>(criteria, sort); + } + +} diff --git a/dapr-spring/dapr-spring-messaging/pom.xml b/dapr-spring/dapr-spring-messaging/pom.xml new file mode 100644 index 000000000..c9b280a47 --- /dev/null +++ b/dapr-spring/dapr-spring-messaging/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + + io.dapr.spring + dapr-spring-parent + 0.13.0-SNAPSHOT + + + dapr-spring-messaging + dapr-spring-messaging + Dapr Spring Messaging + jar + + diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java new file mode 100644 index 000000000..ac1092c9a --- /dev/null +++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingOperations.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024 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.spring.messaging; + +import reactor.core.publisher.Mono; + +public interface DaprMessagingOperations { + + /** + * Sends a message to the specified topic in a blocking manner. + * + * @param topic the topic to send the message to or {@code null} to send to the + * default topic + * @param message the message to send + */ + void send(String topic, T message); + + /** + * Create a {@link SendMessageBuilder builder} for configuring and sending a message. + * + * @param message the payload of the message + * @return the builder to configure and send the message + */ + SendMessageBuilder newMessage(T message); + + /** + * Builder that can be used to configure and send a message. Provides more options + * than the basic send/sendAsync methods provided by {@link DaprMessagingOperations}. + * + * @param the message payload type + */ + interface SendMessageBuilder { + + /** + * Specify the topic to send the message to. + * + * @param topic the destination topic + * @return the current builder with the destination topic specified + */ + SendMessageBuilder withTopic(String topic); + + /** + * Send the message in a blocking manner using the configured specification. + */ + void send(); + + /** + * Uses the configured specification to send the message in a non-blocking manner. + * + * @return a Mono that completes when the message has been sent + */ + Mono sendAsync(); + } + +} diff --git a/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java new file mode 100644 index 000000000..604849b86 --- /dev/null +++ b/dapr-spring/dapr-spring-messaging/src/main/java/io/dapr/spring/messaging/DaprMessagingTemplate.java @@ -0,0 +1,87 @@ +/* + * Copyright 2024 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.spring.messaging; + +import io.dapr.client.DaprClient; +import io.dapr.client.domain.Metadata; +import reactor.core.publisher.Mono; + +import java.util.Collections; + +public class DaprMessagingTemplate implements DaprMessagingOperations { + + private static final String MESSAGE_TTL_IN_SECONDS = "10"; + + private final DaprClient daprClient; + private final String pubsubName; + + public DaprMessagingTemplate(DaprClient daprClient, String pubsubName) { + this.daprClient = daprClient; + this.pubsubName = pubsubName; + } + + @Override + public void send(String topic, T message) { + doSend(topic, message); + } + + @Override + public SendMessageBuilder newMessage(T message) { + return new SendMessageBuilderImpl<>(this, message); + } + + private void doSend(String topic, T message) { + doSendAsync(topic, message).block(); + } + + private Mono doSendAsync(String topic, T message) { + return daprClient.publishEvent(pubsubName, + topic, + message, + Collections.singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS)); + } + + private static class SendMessageBuilderImpl implements SendMessageBuilder { + + private final DaprMessagingTemplate template; + + private final T message; + + private String topic; + + SendMessageBuilderImpl(DaprMessagingTemplate template, T message) { + this.template = template; + this.message = message; + } + + @Override + public SendMessageBuilder withTopic(String topic) { + this.topic = topic; + return this; + } + + + @Override + public void send() { + this.template.doSend(this.topic, this.message); + } + + @Override + public Mono sendAsync() { + return this.template.doSendAsync(this.topic, this.message); + } + + } + +} diff --git a/dapr-spring/pom.xml b/dapr-spring/pom.xml new file mode 100644 index 000000000..6f6083abd --- /dev/null +++ b/dapr-spring/pom.xml @@ -0,0 +1,169 @@ + + 4.0.0 + + + io.dapr + dapr-sdk-parent + 1.13.0-SNAPSHOT + + + io.dapr.spring + dapr-spring-parent + pom + 0.13.0-SNAPSHOT + dapr-spring-parent + SDK extension for Spring and Spring Boot + + + dapr-spring-core + dapr-spring-data + dapr-spring-messaging + dapr-spring-boot-autoconfigure + dapr-spring-boot-starters/dapr-spring-boot-starter + + + + 3.2.6 + 17 + 17 + 17 + + + + + + org.springframework.boot + spring-boot-dependencies + ${springboot.version} + pom + import + + + + + + + + io.dapr + dapr-sdk + ${dapr.sdk.version} + + + io.dapr + dapr-sdk-actors + ${dapr.sdk.version} + + + + + org.springframework + spring-web + true + + + org.springframework + spring-context + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + com.github.gmazzo.okhttp.mock + mock-client + 2.0.0 + test + + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.3.1 + + + attach-sources + + jar-no-fork + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.7.0 + + + attach-javadocs + + jar + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + default-prepare-agent + + prepare-agent + + + + report + test + + report + + + target/jacoco-report/ + + + + check + + check + + + + + BUNDLE + + io.dapr.springboot.DaprBeanPostProcessor + + + + LINE + COVEREDRATIO + 80% + + + + + + + + + + + diff --git a/dapr-spring/spotbugs-exclude.xml b/dapr-spring/spotbugs-exclude.xml new file mode 100644 index 000000000..fc80592a1 --- /dev/null +++ b/dapr-spring/spotbugs-exclude.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 44e3868f7..946c5f2e4 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,6 @@ 4.0.0 @@ -18,6 +18,7 @@ 3.25.0 protoc https://raw.githubusercontent.com/dapr/dapr/v1.14.0-rc.2/dapr/proto + 1.13.0-SNAPSHOT 0.13.0-SNAPSHOT 1.7.1 3.1.1 @@ -323,6 +324,7 @@ sdk-actors sdk-workflows sdk-springboot + dapr-spring examples testcontainers-dapr diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index e1175974a..88acba63c 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -25,7 +25,6 @@ 3.3.1 1.4.12 3.9.1 - ${dapr.sdk.alpha.version} 1.20.0 @@ -137,14 +136,14 @@ io.dapr - testcontainers-dapr - ${dapr.sdk.alpha.version} + dapr-sdk-actors + ${dapr.sdk.version} test io.dapr - dapr-sdk-actors - ${dapr.sdk.version} + dapr-sdk-workflows + ${dapr.sdk.alpha.version} test @@ -153,11 +152,44 @@ ${dapr.sdk.version} test + + io.dapr.spring + dapr-spring-core + ${dapr.sdk.alpha.version} + test + + + io.dapr.spring + dapr-spring-data + ${dapr.sdk.alpha.version} + test + + + io.dapr.spring + dapr-spring-messaging + ${dapr.sdk.alpha.version} + test + + + io.dapr.spring + dapr-spring-boot-autoconfigure + ${dapr.sdk.alpha.version} + test + + + org.springframework.data + spring-data-keyvalue + org.springframework.boot spring-boot-starter-test test + + org.springframework.boot + spring-boot-testcontainers + test + org.wiremock wiremock-standalone @@ -187,6 +219,29 @@ 3.9 test + + io.dapr + testcontainers-dapr + ${dapr.sdk.alpha.version} + + + org.testcontainers + junit-jupiter + ${testcontainers-test.version} + test + + + org.testcontainers + postgresql + ${testcontainers-test.version} + test + + + org.testcontainers + mysql + ${testcontainers-test.version} + test + jakarta.annotation jakarta.annotation-api @@ -216,17 +271,6 @@ 4.0.1 compile - - io.dapr - testcontainers-dapr - ${testcontainers-dapr.version} - - - org.testcontainers - junit-jupiter - ${testcontainers-test.version} - test - @@ -289,6 +333,8 @@ ${skipITs} + ${test.groups} + ${test.excluded.groups} diff --git a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java index d5bb45669..ed9609cc9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/grpc/MethodInvokeIT.java @@ -88,11 +88,12 @@ public void testInvokeTimeout() throws Exception { MethodInvokeServiceGrpc.MethodInvokeServiceBlockingStub stub = createGrpcStub(client); long started = System.currentTimeMillis(); SleepRequest req = SleepRequest.newBuilder().setSeconds(1).build(); - String message = assertThrows(StatusRuntimeException.class, () -> stub.sleep(req)).getMessage(); + StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, () -> stub.sleep(req)); long delay = System.currentTimeMillis() - started; + Status.Code code = exception.getStatus().getCode(); + assertTrue(delay >= TIMEOUT_MS, "Delay: " + delay + " is not greater than timeout: " + TIMEOUT_MS); - assertTrue(message.contains("DEADLINE_EXCEEDED"), "The message contains DEADLINE_EXCEEDED: " + message); - assertTrue(message.contains("CallOptions deadline exceeded after"), "The message contains DEADLINE_EXCEEDED: " + message); + assertEquals(Status.DEADLINE_EXCEEDED.getCode(), code, "Expected timeout error"); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java index 6068db2f0..1b5ab7577 100644 --- a/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/methodinvoke/http/MethodInvokeIT.java @@ -117,8 +117,10 @@ public void testInvokeTimeout() throws Exception { client.invokeMethod(daprRun.getAppName(), "sleep", 1, HttpExtension.POST) .block(Duration.ofMillis(10)); }).getMessage(); + long delay = System.currentTimeMillis() - started; - assertTrue(delay <= 200); // 200 ms is a reasonable delay if the request timed out. + + assertTrue(delay <= 200, "Delay: " + delay + " is not less than timeout: 200"); assertEquals("Timeout on blocking read for 10000000 NANOSECONDS", message); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/CustomMySQLContainer.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/CustomMySQLContainer.java new file mode 100644 index 000000000..9ebb6364c --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/CustomMySQLContainer.java @@ -0,0 +1,15 @@ +package io.dapr.it.spring.data; + +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.utility.DockerImageName; + +public class CustomMySQLContainer> extends MySQLContainer { + + public CustomMySQLContainer(String dockerImageName) { + super(DockerImageName.parse(dockerImageName)); + } + + protected void waitUntilContainerStarted() { + this.getWaitStrategy().waitUntilReady(this); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java new file mode 100644 index 000000000..9caa726a4 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java @@ -0,0 +1,140 @@ +/* + * Copyright 2024 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.it.spring.data; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static io.dapr.it.spring.data.DaprSpringDataConstants.BINDING_NAME; +import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Integration tests for {@link DaprKeyValueRepositoryIT}. + */ + +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestDaprSpringDataConfiguration.class) +@Testcontainers +@Tag("testcontainers") +public class DaprKeyValueRepositoryIT { + private static final String CONNECTION_STRING = + "host=postgres-repository user=postgres password=password port=5432 connect_timeout=10 database=dapr_db_repository"; + private static final Map STATE_STORE_PROPERTIES = createStateStoreProperties(); + + private static final Map BINDING_PROPERTIES = Collections.singletonMap("connectionString", CONNECTION_STRING); + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + @Container + private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>("postgres:16-alpine") + .withNetworkAliases("postgres-repository") + .withDatabaseName("dapr_db_repository") + .withUsername("postgres") + .withPassword("password") + .withNetwork(DAPR_NETWORK); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2") + .withAppName("postgresql-repository-dapr-app") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component(STATE_STORE_NAME, "state.postgresql", "v1", STATE_STORE_PROPERTIES)) + .withComponent(new Component(BINDING_NAME, "bindings.postgresql", "v1", BINDING_PROPERTIES)) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .dependsOn(POSTGRE_SQL_CONTAINER); + + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); + registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); + } + + private static Map createStateStoreProperties() { + Map result = new HashMap<>(); + + result.put("keyPrefix", "name"); + result.put("actorStateStore", String.valueOf(true)); + result.put("connectionString", CONNECTION_STRING); + + return result; + } + + @Autowired + private TestTypeRepository repository; + + @BeforeEach + public void setUp() { + repository.deleteAll(); + } + + @Test + public void testFindById() { + TestType saved = repository.save(new TestType(3, "test")); + TestType byId = repository.findById(3).get(); + + assertEquals(saved, byId); + } + + @Test + public void testExistsById() { + repository.save(new TestType(3, "test")); + + boolean existsById = repository.existsById(3); + assertTrue(existsById); + + boolean existsById2 = repository.existsById(4); + assertFalse(existsById2); + } + + @Test + public void testFindAll() { + repository.save(new TestType(3, "test")); + repository.save(new TestType(4, "test2")); + + Iterable all = repository.findAll(); + + assertEquals(2, all.spliterator().getExactSizeIfKnown()); + } + + @Test + public void testFinUsingQuery() { + repository.save(new TestType(3, "test")); + repository.save(new TestType(4, "test2")); + + List byContent = repository.findByContent("test2"); + + assertEquals(1, byContent.size()); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprSpringDataConstants.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprSpringDataConstants.java new file mode 100644 index 000000000..2711052c0 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprSpringDataConstants.java @@ -0,0 +1,9 @@ +package io.dapr.it.spring.data; + +public class DaprSpringDataConstants { + private DaprSpringDataConstants() { + } + + public static final String STATE_STORE_NAME = "kvstore"; + public static final String BINDING_NAME = "kvbinding"; +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java new file mode 100644 index 000000000..e50da050f --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java @@ -0,0 +1,240 @@ +/* + * Copyright 2024 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.it.spring.data; + +import io.dapr.client.DaprClient; +import io.dapr.spring.data.DaprKeyValueTemplate; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.containers.wait.strategy.WaitStrategy; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME; +import static io.dapr.it.spring.data.DaprSpringDataConstants.BINDING_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Integration tests for {@link MySQLDaprKeyValueTemplateIT}. + */ +@SuppressWarnings("AbbreviationAsWordInName") +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestDaprSpringDataConfiguration.class) +@Testcontainers +@Tag("testcontainers") +public class MySQLDaprKeyValueTemplateIT { + private static final String STATE_STORE_DSN = "mysql:password@tcp(mysql:3306)/"; + private static final String BINDING_DSN = "mysql:password@tcp(mysql:3306)/dapr_db"; + private static final Map STATE_STORE_PROPERTIES = createStateStoreProperties(); + + private static final Map BINDING_PROPERTIES = Collections.singletonMap("url", BINDING_DSN); + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + private static final WaitStrategy MYSQL_WAIT_STRATEGY = Wait + .forLogMessage(".*port: 3306 MySQL Community Server \\(GPL\\).*", 1) + .withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)); + + @Container + private static final MySQLContainer MY_SQL_CONTAINER = new CustomMySQLContainer<>("mysql:5.7.34") + .withNetworkAliases("mysql") + .withDatabaseName("dapr_db") + .withUsername("mysql") + .withPassword("password") + .withNetwork(DAPR_NETWORK) + .waitingFor(MYSQL_WAIT_STRATEGY); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2") + .withAppName("mysql-dapr-app") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component(STATE_STORE_NAME, "state.mysql", "v1", STATE_STORE_PROPERTIES)) + .withComponent(new Component(BINDING_NAME, "bindings.mysql", "v1", BINDING_PROPERTIES)) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .dependsOn(MY_SQL_CONTAINER); + + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); + registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); + } + + private static Map createStateStoreProperties() { + Map result = new HashMap<>(); + + result.put("keyPrefix", "name"); + result.put("schemaName", "dapr_db"); + result.put("actorStateStore", "true"); + result.put("connectionString", STATE_STORE_DSN); + + return result; + } + + @Autowired + private DaprClient daprClient; + + @Autowired + private DaprKeyValueTemplate keyValueTemplate; + + /** + * Cleans up the state store after each test. + */ + @AfterEach + public void tearDown() { + var meta = Collections.singletonMap("sql", "delete from state"); + + daprClient.invokeBinding(BINDING_NAME, "exec", null, meta).block(); + } + + @Test + public void testInsertAndQueryDaprKeyValueTemplate() { + int itemId = 3; + TestType savedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(savedType).isNotNull(); + + Optional findById = keyValueTemplate.findById(itemId, TestType.class); + assertThat(findById.isEmpty()).isFalse(); + assertThat(findById.get()).isEqualTo(savedType); + + KeyValueQuery keyValueQuery = new KeyValueQuery<>("content == 'test'"); + + Iterable myTypes = keyValueTemplate.find(keyValueQuery, TestType.class); + assertThat(myTypes.iterator().hasNext()).isTrue(); + + TestType item = myTypes.iterator().next(); + assertThat(item.getId()).isEqualTo(Integer.valueOf(itemId)); + assertThat(item.getContent()).isEqualTo("test"); + + keyValueQuery = new KeyValueQuery<>("content == 'asd'"); + + myTypes = keyValueTemplate.find(keyValueQuery, TestType.class); + assertThat(!myTypes.iterator().hasNext()).isTrue(); + } + + @Test + public void testInsertMoreThan10AndQueryDaprKeyValueTemplate() { + int count = 10; + List items = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + items.add(keyValueTemplate.insert(new TestType(i, "test"))); + } + + KeyValueQuery keyValueQuery = new KeyValueQuery<>("content == 'test'"); + keyValueQuery.setRows(100); + keyValueQuery.setOffset(0); + + Iterable foundItems = keyValueTemplate.find(keyValueQuery, TestType.class); + assertThat(foundItems.iterator().hasNext()).isTrue(); + + int index = 0; + + for (TestType foundItem : foundItems) { + TestType item = items.get(index); + + assertEquals(item.getId(), foundItem.getId()); + assertEquals(item.getContent(), foundItem.getContent()); + + index++; + } + + assertEquals(index, items.size()); + } + + @Test + public void testUpdateDaprKeyValueTemplate() { + int itemId = 2; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + TestType updatedType = keyValueTemplate.update(new TestType(itemId, "test2")); + assertThat(updatedType).isNotNull(); + } + + @Test + public void testDeleteAllOfDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + keyValueTemplate.delete(TestType.class); + + Optional result = keyValueTemplate.findById(itemId, TestType.class); + + assertThat(result).isEmpty(); + } + + @Test + public void testGetAllOfDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + Iterable result = keyValueTemplate.findAll(TestType.class); + + assertThat(result.iterator().hasNext()).isTrue(); + } + + @Test + public void testCountDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + long result = keyValueTemplate.count(TestType.class); + + assertThat(result).isEqualTo(1); + } + + @Test + public void testCountWithQueryDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + KeyValueQuery keyValueQuery = new KeyValueQuery<>("content == 'test'"); + keyValueQuery.setRows(100); + keyValueQuery.setOffset(0); + + long result = keyValueTemplate.count(keyValueQuery, TestType.class); + + assertThat(result).isEqualTo(1); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java new file mode 100644 index 000000000..7b45e9708 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java @@ -0,0 +1,222 @@ +/* + * Copyright 2024 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.it.spring.data; + +import io.dapr.client.DaprClient; +import io.dapr.spring.data.DaprKeyValueTemplate; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.keyvalue.core.query.KeyValueQuery; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.*; + +import static io.dapr.it.spring.data.DaprSpringDataConstants.BINDING_NAME; +import static io.dapr.it.spring.data.DaprSpringDataConstants.STATE_STORE_NAME; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Integration tests for {@link PostgreSQLDaprKeyValueTemplateIT}. + */ +@SuppressWarnings("AbbreviationAsWordInName") +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = TestDaprSpringDataConfiguration.class) +@Testcontainers +@Tag("testcontainers") +public class PostgreSQLDaprKeyValueTemplateIT { + private static final String CONNECTION_STRING = + "host=postgres user=postgres password=password port=5432 connect_timeout=10 database=dapr_db"; + private static final Map STATE_STORE_PROPERTIES = createStateStoreProperties(); + + private static final Map BINDING_PROPERTIES = Collections.singletonMap("connectionString", CONNECTION_STRING); + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + @Container + private static final PostgreSQLContainer POSTGRE_SQL_CONTAINER = new PostgreSQLContainer<>("postgres:16-alpine") + .withNetworkAliases("postgres") + .withDatabaseName("dapr_db") + .withUsername("postgres") + .withPassword("password") + .withNetwork(DAPR_NETWORK); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2") + .withAppName("postgresql-dapr-app") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component(STATE_STORE_NAME, "state.postgresql", "v1", STATE_STORE_PROPERTIES)) + .withComponent(new Component(BINDING_NAME, "bindings.postgresql", "v1", BINDING_PROPERTIES)) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .dependsOn(POSTGRE_SQL_CONTAINER); + + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); + registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); + } + + private static Map createStateStoreProperties() { + Map result = new HashMap<>(); + + result.put("keyPrefix", "name"); + result.put("actorStateStore", String.valueOf(true)); + result.put("connectionString", CONNECTION_STRING); + + return result; + } + + @Autowired + private DaprClient daprClient; + + @Autowired + private DaprKeyValueTemplate keyValueTemplate; + + @BeforeEach + public void setUp() { + var meta = Collections.singletonMap("sql", "delete from state"); + + daprClient.invokeBinding(BINDING_NAME, "exec", null, meta).block(); + } + + @Test + public void testInsertAndQueryDaprKeyValueTemplate() { + int itemId = 3; + TestType savedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(savedType).isNotNull(); + + Optional findById = keyValueTemplate.findById(itemId, TestType.class); + assertThat(findById.isEmpty()).isFalse(); + assertThat(findById.get()).isEqualTo(savedType); + + KeyValueQuery keyValueQuery = new KeyValueQuery<>("content == 'test'"); + + Iterable myTypes = keyValueTemplate.find(keyValueQuery, TestType.class); + assertThat(myTypes.iterator().hasNext()).isTrue(); + + TestType item = myTypes.iterator().next(); + assertThat(item.getId()).isEqualTo(Integer.valueOf(itemId)); + assertThat(item.getContent()).isEqualTo("test"); + + keyValueQuery = new KeyValueQuery<>("content == 'asd'"); + + myTypes = keyValueTemplate.find(keyValueQuery, TestType.class); + assertThat(!myTypes.iterator().hasNext()).isTrue(); + } + + @Test + public void testInsertMoreThan10AndQueryDaprKeyValueTemplate() { + int count = 10; + List items = new ArrayList<>(); + + for (int i = 0; i < count; i++) { + items.add(keyValueTemplate.insert(new TestType(i, "test"))); + } + + KeyValueQuery keyValueQuery = new KeyValueQuery<>("content == 'test'"); + keyValueQuery.setRows(100); + keyValueQuery.setOffset(0); + + Iterable foundItems = keyValueTemplate.find(keyValueQuery, TestType.class); + assertThat(foundItems.iterator().hasNext()).isTrue(); + + int index = 0; + + for (TestType foundItem : foundItems) { + TestType item = items.get(index); + + assertEquals(item.getId(), foundItem.getId()); + assertEquals(item.getContent(), foundItem.getContent()); + + index++; + } + + assertEquals(index, items.size()); + } + + @Test + public void testUpdateDaprKeyValueTemplate() { + int itemId = 2; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + TestType updatedType = keyValueTemplate.update(new TestType(itemId, "test2")); + assertThat(updatedType).isNotNull(); + } + + @Test + public void testDeleteAllOfDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + keyValueTemplate.delete(TestType.class); + + Optional result = keyValueTemplate.findById(itemId, TestType.class); + + assertThat(result).isEmpty(); + } + + @Test + public void testGetAllOfDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + Iterable result = keyValueTemplate.findAll(TestType.class); + + assertThat(result.iterator().hasNext()).isTrue(); + } + + @Test + public void testCountDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + long result = keyValueTemplate.count(TestType.class); + + assertThat(result).isEqualTo(1); + } + + @Test + public void testCountWithQueryDaprKeyValueTemplate() { + int itemId = 1; + TestType insertedType = keyValueTemplate.insert(new TestType(itemId, "test")); + assertThat(insertedType).isNotNull(); + + KeyValueQuery keyValueQuery = new KeyValueQuery<>("content == 'test'"); + keyValueQuery.setRows(100); + keyValueQuery.setOffset(0); + + long result = keyValueTemplate.count(keyValueQuery, TestType.class); + + assertThat(result).isEqualTo(1); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java new file mode 100644 index 000000000..4604bb12c --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java @@ -0,0 +1,44 @@ +package io.dapr.it.spring.data; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.client.DaprClient; +import io.dapr.spring.boot.autoconfigure.client.DaprClientAutoConfiguration; +import io.dapr.spring.core.client.DaprClientCustomizer; +import io.dapr.spring.data.DaprKeyValueAdapterResolver; +import io.dapr.spring.data.DaprKeyValueTemplate; +import io.dapr.spring.data.KeyValueAdapterResolver; +import io.dapr.spring.data.repository.config.EnableDaprRepositories; +import io.dapr.testcontainers.TestcontainersDaprClientCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@EnableDaprRepositories +@Import(DaprClientAutoConfiguration.class) +public class TestDaprSpringDataConfiguration { + @Bean + public ObjectMapper mapper() { + return new ObjectMapper(); + } + + @Bean + public DaprClientCustomizer daprClientCustomizer(@Value("${dapr.http.port:0000}") String daprHttpPort, + @Value("${dapr.grpc.port:0000}") String daprGrpcPort){ + return new TestcontainersDaprClientCustomizer(daprHttpPort, daprGrpcPort); + } + + @Bean + public KeyValueAdapterResolver keyValueAdapterResolver(DaprClient daprClient, ObjectMapper mapper) { + String storeName = DaprSpringDataConstants.STATE_STORE_NAME; + String bindingName = DaprSpringDataConstants.BINDING_NAME; + + return new DaprKeyValueAdapterResolver(daprClient, mapper, storeName, bindingName); + } + + @Bean + public DaprKeyValueTemplate daprKeyValueTemplate(KeyValueAdapterResolver keyValueAdapterResolver) { + return new DaprKeyValueTemplate(keyValueAdapterResolver); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/TestType.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestType.java new file mode 100644 index 000000000..25a404737 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestType.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.it.spring.data; + +import org.springframework.data.annotation.Id; + +import java.util.Objects; + +public class TestType { + + @Id + private Integer id; + private String content; + + public TestType() { + } + + public TestType(Integer id, String content) { + this.id = id; + this.content = content; + } + + public String getContent() { + return content; + } + + public Integer getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + TestType testType = (TestType) o; + return Objects.equals(id, testType.id) && Objects.equals(content, testType.content); + } + + @Override + public int hashCode() { + return Objects.hash(id, content); + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/TestTypeRepository.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestTypeRepository.java new file mode 100644 index 000000000..65bdbdaaf --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestTypeRepository.java @@ -0,0 +1,24 @@ +/* + * Copyright 2024 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.it.spring.data; + +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface TestTypeRepository extends CrudRepository { + List findByContent(String content); +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java new file mode 100644 index 000000000..ae9006fbd --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java @@ -0,0 +1,100 @@ +/* + * Copyright 2024 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.it.spring.messaging; + +import io.dapr.client.domain.CloudEvent; +import io.dapr.spring.boot.autoconfigure.client.DaprClientAutoConfiguration; +import io.dapr.spring.messaging.DaprMessagingTemplate; +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.Network; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest( + webEnvironment = WebEnvironment.DEFINED_PORT, + classes = { + DaprClientAutoConfiguration.class, + TestApplication.class + }, + properties = {"dapr.pubsub.name=pubsub"} +) +@Testcontainers +@Tag("testcontainers") +public class DaprSpringMessagingIT { + + private static final Logger logger = LoggerFactory.getLogger(DaprSpringMessagingIT.class); + + private static final String TOPIC = "mockTopic"; + + private static final Network DAPR_NETWORK = Network.newNetwork(); + + @Container + private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd:1.13.2") + .withAppName("messaging-dapr-app") + .withNetwork(DAPR_NETWORK) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) + .withAppPort(8080) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String())) + .withAppChannelAddress("host.testcontainers.internal"); + + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + org.testcontainers.Testcontainers.exposeHostPorts(8080); + DAPR_CONTAINER.start(); + registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); + registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); + } + + @Autowired + private DaprMessagingTemplate messagingTemplate; + + @Autowired + private TestRestController testRestController; + + @Test + public void testDaprMessagingTemplate() throws InterruptedException { + for (int i = 0; i < 10; i++) { + var msg = "ProduceAndReadWithPrimitiveMessageType:" + i; + + messagingTemplate.send(TOPIC, msg); + + logger.info("++++++PRODUCE {}------", msg); + } + + // Wait for the messages to arrive + Thread.sleep(1000); + + List> events = testRestController.getEvents(); + + assertThat(events.size()).isEqualTo(10); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java new file mode 100644 index 000000000..f814eae0f --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 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.it.spring.messaging; + +import io.dapr.client.DaprClient; +import io.dapr.spring.boot.autoconfigure.pubsub.DaprPubSubProperties; +import io.dapr.spring.core.client.DaprClientCustomizer; +import io.dapr.spring.messaging.DaprMessagingTemplate; +import io.dapr.testcontainers.TestcontainersDaprClientCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@SpringBootApplication +public class TestApplication { + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + + @Configuration + @EnableConfigurationProperties(DaprPubSubProperties.class) + static class DaprSpringMessagingConfiguration { + + @Bean + public DaprClientCustomizer daprClientCustomizer(@Value("${dapr.http.port:0000}") String daprHttpPort, + @Value("${dapr.grpc.port:0000}") String daprGrpcPort){ + return new TestcontainersDaprClientCustomizer(daprHttpPort, daprGrpcPort); + } + + @Bean + public DaprMessagingTemplate messagingTemplate(DaprClient daprClient, + DaprPubSubProperties daprPubSubProperties) { + return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName()); + } + + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java new file mode 100644 index 000000000..a5d12093c --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestRestController.java @@ -0,0 +1,52 @@ +/* + * Copyright 2024 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.it.spring.messaging; + +import io.dapr.Topic; +import io.dapr.client.domain.CloudEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +@RestController +public class TestRestController { + + public static final String pubSubName = "pubsub"; + public static final String topicName = "mockTopic"; + private static final Logger LOG = LoggerFactory.getLogger(TestRestController.class); + private final List> events = new ArrayList<>(); + + @GetMapping("/") + public String ok() { + return "OK"; + } + + @Topic(name = topicName, pubsubName = pubSubName) + @PostMapping("/subscribe") + public void handleMessages(@RequestBody CloudEvent event) { + LOG.info("++++++CONSUME {}------", event); + events.add(event); + } + + public List> getEvents() { + return events; + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerTest.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerTest.java index aecad75b2..86c91210d 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerTest.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprContainerTest.java @@ -19,11 +19,13 @@ import io.dapr.client.domain.Metadata; import io.dapr.client.domain.State; +import io.dapr.config.Properties; import io.dapr.testcontainers.DaprContainer; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -36,19 +38,18 @@ import static com.github.tomakehurst.wiremock.client.WireMock.configureFor; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; import static com.github.tomakehurst.wiremock.client.WireMock.get; -import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.post; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; import static com.github.tomakehurst.wiremock.client.WireMock.verify; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static java.util.Collections.singletonMap; +import static org.junit.jupiter.api.Assertions.*; @Testcontainers @WireMockTest(httpPort = 8081) +@Tag("testcontainers") public class DaprContainerTest { // Time-to-live for messages published. @@ -59,7 +60,7 @@ public class DaprContainerTest { private static final String PUBSUB_TOPIC_NAME = "topic"; @Container - public static DaprContainer daprContainer = new DaprContainer("daprio/daprd") + private static final DaprContainer DAPR_CONTAINER = new DaprContainer("daprio/daprd") .withAppName("dapr-app") .withAppPort(8081) .withAppChannelAddress("host.testcontainers.internal"); @@ -67,15 +68,13 @@ public class DaprContainerTest { /** * Sets the Dapr properties for the test. */ - @BeforeAll - public static void setDaprProperties() { + @BeforeEach + public void setDaprProperties() { configStub(); org.testcontainers.Testcontainers.exposeHostPorts(8081); - System.setProperty("dapr.grpc.port", Integer.toString(daprContainer.getGrpcPort())); - System.setProperty("dapr.http.port", Integer.toString(daprContainer.getHttpPort())); } - private static void configStub() { + private void configStub() { stubFor(any(urlMatching("/dapr/subscribe")) .willReturn(aResponse().withBody("[]").withStatus(200))); @@ -94,22 +93,22 @@ private static void configStub() { @Test public void testDaprContainerDefaults() { - assertEquals( - 2, - daprContainer.getComponents().size(), + assertEquals(2, + DAPR_CONTAINER.getComponents().size(), "The pubsub and kvstore component should be configured by default" ); assertEquals( 1, - daprContainer.getSubscriptions().size(), - "A subscription should be configured by default if none is provided"); + DAPR_CONTAINER.getSubscriptions().size(), + "A subscription should be configured by default if none is provided" + ); } @Test public void testStateStore() throws Exception { - try (DaprClient client = (new DaprClientBuilder()).build()) { - client.waitForSidecar(1000).block(); + DaprClientBuilder builder = createDaprClientBuilder(); + try (DaprClient client = (builder).build()) { String value = "value"; // Save state client.saveState(STATE_STORE_NAME, KEY, value).block(); @@ -117,24 +116,19 @@ public void testStateStore() throws Exception { // Get the state back from the state store State retrievedState = client.getState(STATE_STORE_NAME, KEY, String.class).block(); - assertEquals("The value retrieved should be the same as the one stored", value, retrievedState.getValue()); + assertNotNull(retrievedState); + assertEquals(value, retrievedState.getValue(), "The value retrieved should be the same as the one stored"); } } @Test public void testPlacement() throws Exception { - // Here we are just waiting for Dapr to be ready - try (DaprClient client = (new DaprClientBuilder()).build()) { - client.waitForSidecar(1000).block(); - } - - OkHttpClient client = new OkHttpClient.Builder().build(); - - String url = "http://" + daprContainer.getHost() + ":" + daprContainer.getMappedPort(3500); + OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); + String url = "http://" + DAPR_CONTAINER.getHost() + ":" + DAPR_CONTAINER.getHttpPort(); Request request = new Request.Builder().url(url + "/v1.0/metadata").build(); - try (Response response = client.newCall(request).execute()) { - if (response.isSuccessful()) { + try (Response response = okHttpClient.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { assertTrue(response.body().string().contains("placement: connected")); } else { @@ -145,15 +139,20 @@ public void testPlacement() throws Exception { @Test public void testPubSub() throws Exception { - try (DaprClient client = (new DaprClientBuilder()).build()) { - client.waitForSidecar(1000).block(); + DaprClientBuilder builder = createDaprClientBuilder(); + try (DaprClient client = (builder).build()) { String message = "message content"; Map metadata = singletonMap(Metadata.TTL_IN_SECONDS, MESSAGE_TTL_IN_SECONDS); client.publishEvent(PUBSUB_NAME, PUBSUB_TOPIC_NAME, message, metadata).block(); } - verify(getRequestedFor(urlMatching("/dapr/config"))); verify(postRequestedFor(urlEqualTo("/events")).withHeader("Content-Type", equalTo("application/cloudevents+json"))); } + + private DaprClientBuilder createDaprClientBuilder() { + return new DaprClientBuilder() + .withPropertyOverride(Properties.HTTP_PORT, String.valueOf(DAPR_CONTAINER.getHttpPort())) + .withPropertyOverride(Properties.GRPC_PORT, String.valueOf(DAPR_CONTAINER.getGrpcPort())); + } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprPlacementContainerTest.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprPlacementContainerTest.java index 0283c881a..29284d434 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprPlacementContainerTest.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprPlacementContainerTest.java @@ -14,6 +14,7 @@ package io.dapr.it.testcontainers; import io.dapr.testcontainers.DaprPlacementContainer; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -21,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @Testcontainers +@Tag("testcontainers") public class DaprPlacementContainerTest { @Container @@ -28,6 +30,6 @@ public class DaprPlacementContainerTest { @Test public void testDaprPlacementContainerDefaults() { - assertEquals(50005, PLACEMENT_CONTAINER.getPort(), "The default port is set"); + assertEquals(50005, PLACEMENT_CONTAINER.getPort(), "The default port is not set"); } } diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java new file mode 100644 index 000000000..d7a88a894 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java @@ -0,0 +1,39 @@ +package io.dapr.it.testcontainers; + +import io.dapr.testcontainers.Component; +import io.dapr.testcontainers.DaprContainer; +import io.dapr.testcontainers.DaprLogLevel; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.Testcontainers; +import org.testcontainers.junit.jupiter.Container; + +import java.util.Collections; + +public interface DaprTestcontainersModule { + + @Container + DaprContainer dapr = new DaprContainer("daprio/daprd:1.13.2") + .withAppName("workflow-dapr-app") + //Enable Workflows + .withComponent(new Component("kvstore", "state.in-memory", "v1", + Collections.singletonMap("actorStateStore", "true"))) + .withComponent(new Component("pubsub", "pubsub.in-memory", "v1", Collections.emptyMap())) + .withAppPort(8080) + .withDaprLogLevel(DaprLogLevel.DEBUG) + .withAppChannelAddress("host.testcontainers.internal"); + + /** + * Expose the Dapr ports to the host. + * + * @param registry the dynamic property registry + */ + @DynamicPropertySource + static void daprProperties(DynamicPropertyRegistry registry) { + Testcontainers.exposeHostPorts(8080); + dapr.start(); + registry.add("dapr.grpc.port", dapr::getGrpcPort); + registry.add("dapr.http.port", dapr::getHttpPort); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsTests.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsTests.java new file mode 100644 index 000000000..287dab28e --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprWorkflowsTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dapr.workflows.client.DaprWorkflowClient; +import io.dapr.workflows.client.WorkflowInstanceStatus; +import io.dapr.workflows.runtime.WorkflowRuntime; +import io.dapr.workflows.runtime.WorkflowRuntimeBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.Duration; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@SpringBootTest( + webEnvironment = WebEnvironment.RANDOM_PORT, + classes = TestWorkflowsApplication.class +) +@Testcontainers +@Tag("testcontainers") +public class DaprWorkflowsTests { + + private DaprWorkflowClient workflowClient; + + /** + * Initializes the test. + */ + @BeforeEach + public void init() { + WorkflowRuntimeBuilder builder = new WorkflowRuntimeBuilder().registerWorkflow(TestWorkflow.class); + builder.registerActivity(FirstActivity.class); + builder.registerActivity(SecondActivity.class); + + try (WorkflowRuntime runtime = builder.build()) { + System.out.println("Start workflow runtime"); + runtime.start(false); + } + } + + @Test + public void myWorkflowTest() throws Exception { + workflowClient = new DaprWorkflowClient(); + + TestWorkflowPayload payload = new TestWorkflowPayload(new ArrayList<>()); + String instanceId = workflowClient.scheduleNewWorkflow(TestWorkflow.class, payload); + + workflowClient.waitForInstanceStart(instanceId, Duration.ofSeconds(10), false); + + workflowClient.raiseEvent(instanceId, "MoveForward", payload); + + WorkflowInstanceStatus workflowStatus = workflowClient.waitForInstanceCompletion(instanceId, + Duration.ofSeconds(10), + true); + + // The workflow completed before 10 seconds + assertNotNull(workflowStatus); + + String workflowPlayloadJson = workflowStatus.getSerializedOutput(); + + ObjectMapper mapper = new ObjectMapper(); + TestWorkflowPayload workflowOutput = mapper.readValue(workflowPlayloadJson, TestWorkflowPayload.class); + + assertEquals(2, workflowOutput.getPayloads().size()); + assertEquals("First Activity", workflowOutput.getPayloads().get(0)); + assertEquals("Second Activity", workflowOutput.getPayloads().get(1)); + assertEquals(instanceId, workflowOutput.getWorkflowId()); + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/FirstActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/FirstActivity.java new file mode 100644 index 000000000..d3bda7df6 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/FirstActivity.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import io.dapr.workflows.runtime.WorkflowActivity; +import io.dapr.workflows.runtime.WorkflowActivityContext; + +public class FirstActivity implements WorkflowActivity { + + @Override + public Object run(WorkflowActivityContext ctx) { + TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class); + workflowPayload.getPayloads().add("First Activity"); + return workflowPayload; + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/SecondActivity.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/SecondActivity.java new file mode 100644 index 000000000..e3a83c293 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/SecondActivity.java @@ -0,0 +1,28 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import io.dapr.workflows.runtime.WorkflowActivity; +import io.dapr.workflows.runtime.WorkflowActivityContext; + +public class SecondActivity implements WorkflowActivity { + + @Override + public Object run(WorkflowActivityContext ctx) { + TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class); + workflowPayload.getPayloads().add("Second Activity"); + return workflowPayload; + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/SubscriptionsRestController.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/SubscriptionsRestController.java new file mode 100644 index 000000000..2d41de6aa --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/SubscriptionsRestController.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import io.dapr.client.domain.CloudEvent; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +@RestController +public class SubscriptionsRestController { + + private final List> events = new ArrayList<>(); + + @PostMapping(path = "/events", consumes = "application/cloudevents+json") + public void receiveEvents(@RequestBody CloudEvent event) { + events.add(event); + } + + @GetMapping(path = "/events", produces = "application/cloudevents+json") + public List> getAllEvents() { + return events; + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflow.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflow.java new file mode 100644 index 000000000..1011c7b6a --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflow.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import io.dapr.workflows.Workflow; +import io.dapr.workflows.WorkflowStub; +import org.slf4j.Logger; + +import java.time.Duration; + +public class TestWorkflow extends Workflow { + + @Override + public WorkflowStub create() { + return ctx -> { + Logger logger = ctx.getLogger(); + String instanceId = ctx.getInstanceId(); + logger.info("Starting Workflow: " + ctx.getName()); + logger.info("Instance ID: " + instanceId); + logger.info("Current Orchestration Time: " + ctx.getCurrentInstant()); + + TestWorkflowPayload workflowPayload = ctx.getInput(TestWorkflowPayload.class); + workflowPayload.setWorkflowId(instanceId); + + TestWorkflowPayload payloadAfterFirst = + ctx.callActivity(FirstActivity.class.getName(), workflowPayload, TestWorkflowPayload.class).await(); + + ctx.waitForExternalEvent("MoveForward", Duration.ofSeconds(3), TestWorkflowPayload.class).await(); + + TestWorkflowPayload payloadAfterSecond = + ctx.callActivity(SecondActivity.class.getName(), payloadAfterFirst, TestWorkflowPayload.class).await(); + + ctx.complete(payloadAfterSecond); + }; + } + +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowPayload.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowPayload.java new file mode 100644 index 000000000..38e9e7f96 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowPayload.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import java.util.List; + +public class TestWorkflowPayload { + private List payloads; + private String workflowId; + + public TestWorkflowPayload() { + } + + public TestWorkflowPayload(List payloads, String workflowId) { + this.payloads = payloads; + this.workflowId = workflowId; + } + + public TestWorkflowPayload(List payloads) { + this.payloads = payloads; + } + + public List getPayloads() { + return payloads; + } + + public void setPayloads(List payloads) { + this.payloads = payloads; + } + + public String getWorkflowId() { + return workflowId; + } + + public void setWorkflowId(String workflowId) { + this.workflowId = workflowId; + } +} diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowsApplication.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowsApplication.java new file mode 100644 index 000000000..2ae63a4e4 --- /dev/null +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/TestWorkflowsApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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.it.testcontainers; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.testcontainers.context.ImportTestcontainers; + +@SpringBootApplication +public class TestWorkflowsApplication { + + public static void main(String[] args) { + SpringApplication.run(TestWorkflowsApplication.class, args); + } + + @ImportTestcontainers(DaprTestcontainersModule.class) + static class DaprTestConfiguration { + + } + +} diff --git a/sdk-tests/src/test/resources/query.json b/sdk-tests/src/test/resources/query.json new file mode 100644 index 000000000..0a92e82cd --- /dev/null +++ b/sdk-tests/src/test/resources/query.json @@ -0,0 +1,11 @@ +{ + "filter": { + "EQ": { + "content": "test" + } + }, + "page": { + "limit": 0, + "token": "0" + } +} diff --git a/sdk-workflows/pom.xml b/sdk-workflows/pom.xml index d4f58436f..64c59aecd 100644 --- a/sdk-workflows/pom.xml +++ b/sdk-workflows/pom.xml @@ -1,6 +1,6 @@ 4.0.0 diff --git a/sdk/pom.xml b/sdk/pom.xml index 70b59b357..66095e0b2 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -1,7 +1,7 @@ + xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 @@ -143,20 +143,20 @@ - src/main/resources - true - - **/sdk_version.properties - + src/main/resources + true + + **/sdk_version.properties + - src/main/resources - false - - **/sdk_version.properties - + src/main/resources + false + + **/sdk_version.properties + - + org.apache.maven.plugins @@ -215,7 +215,7 @@ BUNDLE - + LINE COVEREDRATIO 80% diff --git a/testcontainers-dapr/pom.xml b/testcontainers-dapr/pom.xml index ca146fc01..73fbbcd27 100644 --- a/testcontainers-dapr/pom.xml +++ b/testcontainers-dapr/pom.xml @@ -34,7 +34,13 @@ io.dapr dapr-sdk ${project.parent.version} - test + compile + + + io.dapr.spring + dapr-spring-core + ${project.version} + compile diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java new file mode 100644 index 000000000..abbe62cb8 --- /dev/null +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java @@ -0,0 +1,22 @@ +package io.dapr.testcontainers; + +import io.dapr.client.DaprClientBuilder; +import io.dapr.config.Properties; +import io.dapr.spring.core.client.DaprClientCustomizer; + +public class TestcontainersDaprClientCustomizer implements DaprClientCustomizer { + + private final String daprHttpPort; + private final String daprGrpcPort; + + public TestcontainersDaprClientCustomizer(String daprHttpPort, String daprGrpcPort) { + this.daprHttpPort = daprHttpPort; + this.daprGrpcPort = daprGrpcPort; + } + + @Override + public void customize(DaprClientBuilder daprClientBuilder) { + daprClientBuilder.withPropertyOverride(Properties.HTTP_PORT, daprHttpPort); + daprClientBuilder.withPropertyOverride(Properties.GRPC_PORT, daprGrpcPort); + } +} From dd725d818a84849962203f44f57f3e35d5f13080 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Thu, 29 Aug 2024 10:35:38 +0300 Subject: [PATCH 2/3] Try running ITs all at once Signed-off-by: Artur Ciocanu --- .github/workflows/build.yml | 5 +---- sdk-tests/pom.xml | 2 -- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a19ba54ea..ac3d084ee 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -117,12 +117,9 @@ jobs: uses: codecov/codecov-action@v4.1.0 - name: Install jars run: ./mvnw install -q -B -DskipTests - - name: Integration tests with testcontainers using spring boot version ${{ matrix.spring-boot-version }} - id: integration_tests_testcontainers - run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -f sdk-tests/pom.xml verify -Dtest.groups=testcontainers - name: Integration tests using spring boot version ${{ matrix.spring-boot-version }} id: integration_tests - run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -f sdk-tests/pom.xml verify -Dtest.excluded.groups=testcontainers + run: PRODUCT_SPRING_BOOT_VERSION=${{ matrix.spring-boot-version }} ./mvnw -B -f sdk-tests/pom.xml verify - name: Upload test report for sdk uses: actions/upload-artifact@v4 with: diff --git a/sdk-tests/pom.xml b/sdk-tests/pom.xml index 88acba63c..679c3821a 100644 --- a/sdk-tests/pom.xml +++ b/sdk-tests/pom.xml @@ -333,8 +333,6 @@ ${skipITs} - ${test.groups} - ${test.excluded.groups} From 22203f756af40ec25179e2f4d9aed0839e682d37 Mon Sep 17 00:00:00 2001 From: Artur Ciocanu Date: Thu, 29 Aug 2024 17:21:59 +0300 Subject: [PATCH 3/3] Ensure HTTP and GRPC endpoints are overriden Signed-off-by: Artur Ciocanu --- .../spring/data/DaprKeyValueRepositoryIT.java | 2 ++ .../data/MySQLDaprKeyValueTemplateIT.java | 2 ++ .../PostgreSQLDaprKeyValueTemplateIT.java | 2 ++ .../data/TestDaprSpringDataConfiguration.java | 10 +++++++--- .../messaging/DaprSpringMessagingIT.java | 4 +++- .../it/spring/messaging/TestApplication.java | 10 +++++++--- .../DaprTestcontainersModule.java | 3 +++ .../io/dapr/testcontainers/DaprContainer.java | 4 ++++ .../TestcontainersDaprClientCustomizer.java | 20 ++++++++++++++++++- 9 files changed, 49 insertions(+), 8 deletions(-) diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java index 9caa726a4..446b3b9ff 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/DaprKeyValueRepositoryIT.java @@ -76,6 +76,8 @@ public class DaprKeyValueRepositoryIT { @DynamicPropertySource static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java index e50da050f..6ac618cc4 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/MySQLDaprKeyValueTemplateIT.java @@ -91,6 +91,8 @@ public class MySQLDaprKeyValueTemplateIT { @DynamicPropertySource static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java index 7b45e9708..84fa705c8 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/PostgreSQLDaprKeyValueTemplateIT.java @@ -77,6 +77,8 @@ public class PostgreSQLDaprKeyValueTemplateIT { @DynamicPropertySource static void daprProperties(DynamicPropertyRegistry registry) { + registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java index 4604bb12c..5485ff924 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/data/TestDaprSpringDataConfiguration.java @@ -24,9 +24,13 @@ public ObjectMapper mapper() { } @Bean - public DaprClientCustomizer daprClientCustomizer(@Value("${dapr.http.port:0000}") String daprHttpPort, - @Value("${dapr.grpc.port:0000}") String daprGrpcPort){ - return new TestcontainersDaprClientCustomizer(daprHttpPort, daprGrpcPort); + public DaprClientCustomizer daprClientCustomizer( + @Value("${dapr.http.endpoint}") String daprHttpEndpoint, + @Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint, + @Value("${dapr.http.port}") String daprHttpPort, + @Value("${dapr.grpc.port}") String daprGrpcPort + ){ + return new TestcontainersDaprClientCustomizer(daprHttpEndpoint, daprGrpcEndpoint, daprHttpPort, daprGrpcPort); } @Bean diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java index ae9006fbd..94b00e8d9 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/DaprSpringMessagingIT.java @@ -68,7 +68,9 @@ public class DaprSpringMessagingIT { @DynamicPropertySource static void daprProperties(DynamicPropertyRegistry registry) { org.testcontainers.Testcontainers.exposeHostPorts(8080); - DAPR_CONTAINER.start(); + + registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint); registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort); registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort); } diff --git a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java index f814eae0f..8db079ce2 100644 --- a/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java +++ b/sdk-tests/src/test/java/io/dapr/it/spring/messaging/TestApplication.java @@ -36,9 +36,13 @@ public static void main(String[] args) { static class DaprSpringMessagingConfiguration { @Bean - public DaprClientCustomizer daprClientCustomizer(@Value("${dapr.http.port:0000}") String daprHttpPort, - @Value("${dapr.grpc.port:0000}") String daprGrpcPort){ - return new TestcontainersDaprClientCustomizer(daprHttpPort, daprGrpcPort); + public DaprClientCustomizer daprClientCustomizer( + @Value("${dapr.http.endpoint}") String daprHttpEndpoint, + @Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint, + @Value("${dapr.http.port}") String daprHttpPort, + @Value("${dapr.grpc.port}") String daprGrpcPort + ){ + return new TestcontainersDaprClientCustomizer(daprHttpEndpoint, daprGrpcEndpoint, daprHttpPort, daprGrpcPort); } @Bean diff --git a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java index d7a88a894..cbdc30222 100644 --- a/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java +++ b/sdk-tests/src/test/java/io/dapr/it/testcontainers/DaprTestcontainersModule.java @@ -32,6 +32,9 @@ public interface DaprTestcontainersModule { static void daprProperties(DynamicPropertyRegistry registry) { Testcontainers.exposeHostPorts(8080); dapr.start(); + + registry.add("dapr.http.endpoint", dapr::getHttpEndpoint); + registry.add("dapr.grpc.endpoint", dapr::getGrpcEndpoint); registry.add("dapr.grpc.port", dapr::getGrpcPort); registry.add("dapr.http.port", dapr::getHttpPort); } diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java index d18e72fb6..cc1899fc6 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/DaprContainer.java @@ -161,6 +161,10 @@ public int getGrpcPort() { return getMappedPort(DAPRD_DEFAULT_GRPC_PORT); } + public String getGrpcEndpoint() { + return ":" + getMappedPort(DAPRD_DEFAULT_GRPC_PORT); + } + public DaprContainer withAppChannelAddress(String appChannelAddress) { this.appChannelAddress = appChannelAddress; return this; diff --git a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java index abbe62cb8..06a568d58 100644 --- a/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java +++ b/testcontainers-dapr/src/main/java/io/dapr/testcontainers/TestcontainersDaprClientCustomizer.java @@ -6,16 +6,34 @@ public class TestcontainersDaprClientCustomizer implements DaprClientCustomizer { + private final String httpEndpoint; + private final String grpcEndpoint; private final String daprHttpPort; private final String daprGrpcPort; - public TestcontainersDaprClientCustomizer(String daprHttpPort, String daprGrpcPort) { + /** + * Constructor for TestcontainersDaprClientCustomizer. + * @param httpEndpoint HTTP endpoint. + * @param grpcEndpoint GRPC endpoint. + * @param daprHttpPort Dapr HTTP port. + * @param daprGrpcPort Dapr GRPC port. + */ + public TestcontainersDaprClientCustomizer( + String httpEndpoint, + String grpcEndpoint, + String daprHttpPort, + String daprGrpcPort + ) { + this.httpEndpoint = httpEndpoint; + this.grpcEndpoint = grpcEndpoint; this.daprHttpPort = daprHttpPort; this.daprGrpcPort = daprGrpcPort; } @Override public void customize(DaprClientBuilder daprClientBuilder) { + daprClientBuilder.withPropertyOverride(Properties.HTTP_ENDPOINT, httpEndpoint); + daprClientBuilder.withPropertyOverride(Properties.GRPC_ENDPOINT, grpcEndpoint); daprClientBuilder.withPropertyOverride(Properties.HTTP_PORT, daprHttpPort); daprClientBuilder.withPropertyOverride(Properties.GRPC_PORT, daprGrpcPort); }