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 KeyValuePersistentEntity, ?>, ? 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 KeyValuePersistentEntity, ?>, ? 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 KeyValuePersistentEntity, ?>, ? 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 extends Annotation> 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);
+ }
+}