Adding Spring Boot enhancements, Sring Data Repository, Testcontainers (#1089)

* Adding Spring Boot enhancements, Sring Data Repository, Testcontainers

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

* Try running ITs all at once

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

* Ensure HTTP and GRPC endpoints are overriden

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>

---------

Signed-off-by: Artur Ciocanu <ciocanu@adobe.com>
Co-authored-by: Artur Ciocanu <ciocanu@adobe.com>
This commit is contained in:
artur-ciocanu 2024-08-30 02:09:48 +03:00 committed by GitHub
parent a6923ed75c
commit 935f3be367
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 4109 additions and 70 deletions

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.13.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-boot-autoconfigure</artifactId>
<name>dapr-spring-boot-autoconfigure</name>
<description>Dapr Spring Boot Autoconfigure</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-core</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-data</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-messaging</artifactId>
<version>${project.parent.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.vaadin.external.google</groupId>
<artifactId>android-json</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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<DaprClientCustomizer> 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();
}
}

View File

@ -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<DaprClientCustomizer> customizers;
void setDaprClientCustomizer(List<DaprClientCustomizer> 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);
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
io.dapr.spring.boot.autoconfigure.client.DaprClientAutoConfiguration

View File

@ -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));
}
}

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.13.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>dapr-spring-boot-starter</artifactId>
<name>dapr-spring-boot-starter</name>
<description>Dapr Client Spring Boot Starter</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-core</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-autoconfigure</artifactId>
<version>${project.parent.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.13.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-core</artifactId>
<name>dapr-spring-core</name>
<description>Dapr Spring Core</description>
<packaging>jar</packaging>
</project>

View File

@ -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);
}

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.13.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-data</artifactId>
<name>dapr-spring-data</name>
<description>Dapr Spring Data</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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<String, String> 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<Object> 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> T get(Object id, String keyspace, Class<T> 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> T delete(Object id, String keyspace, Class<T> 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<Map.Entry<Object, Object>> 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> T resolveValue(Mono<State<T>> state) {
if (state == null) {
return null;
}
return state.blockOptional().map(State::getValue).orElse(null);
}
}

View File

@ -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<String> MYSQL_MARKERS = Set.of("state.mysql-v1", "bindings.mysql-v1");
private static final Set<String> 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<ComponentMetadata> 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<ComponentMetadata> 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<ComponentMetadata> 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<String> markers, ComponentMetadata componentMetadata) {
return componentMetadata.getName().equals(name) && markers.contains(getTypeAndVersion(componentMetadata));
}
private String getTypeAndVersion(ComponentMetadata component) {
return component.getType() + "-" + component.getVersion();
}
}

View File

@ -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<Class<? extends KeyValueEvent>> 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<Class<? extends KeyValueEvent>> 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> 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> 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<Void>) 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> 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> 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 <T> Optional<T> findById(Object id, Class<T> 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<Void>) adapter -> {
adapter.deleteAllOf(keyspace);
return null;
});
potentiallyPublishEvent(KeyValueEvent.afterDropKeySpace(keyspace, type));
}
@SuppressWarnings("unchecked")
@Override
public <T> T delete(T objectToDelete) {
Class<T> type = (Class<T>) 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> T delete(Object id, Class<T> 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> T execute(KeyValueCallback<T> action) {
Assert.notNull(action, "KeyValueCallback must not be null");
try {
return action.doInKeyValue(this.adapter);
} catch (RuntimeException e) {
throw resolveExceptionIfPossible(e);
}
}
protected <T> T executeRequired(KeyValueCallback<T> action) {
T result = execute(action);
if (result != null) {
return result;
}
throw new IllegalStateException(String.format("KeyValueCallback %s returned null value", action));
}
@Override
public <T> Iterable<T> find(KeyValueQuery<?> query, Class<T> type) {
return executeRequired((KeyValueCallback<Iterable<T>>) adapter -> {
Iterable<?> result = adapter.find(query, resolveKeySpace(type), type);
List<T> filtered = new ArrayList<>();
for (Object candidate : result) {
if (typeCheck(type, candidate)) {
filtered.add(type.cast(candidate));
}
}
return filtered;
});
}
@Override
public <T> Iterable<T> findAll(Class<T> type) {
Assert.notNull(type, "Type to fetch must not be null");
return executeRequired(adapter -> {
Iterable<?> values = adapter.getAllOf(resolveKeySpace(type), type);
ArrayList<T> filtered = new ArrayList<>();
for (Object candidate : values) {
if (typeCheck(type, candidate)) {
filtered.add(type.cast(candidate));
}
}
return filtered;
});
}
@SuppressWarnings("rawtypes")
@Override
public <T> Iterable<T> findAll(Sort sort, Class<T> type) {
return find(new KeyValueQuery(sort), type);
}
@SuppressWarnings("rawtypes")
@Override
public <T> Iterable<T> findInRange(long offset, int rows, Class<T> type) {
return find(new KeyValueQuery().skip(offset).limit(rows), type);
}
@SuppressWarnings("rawtypes")
@Override
public <T> Iterable<T> findInRange(long offset, int rows, Sort sort, Class<T> 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);
}
}
}

View File

@ -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> secureRandom = new AtomicReference<>(null);
@Override
@SuppressWarnings("unchecked")
public <T> T generateIdentifierOfType(TypeInformation<T> 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<String> SECURE_RANDOM_ALGORITHMS_LINUX_OSX_SOLARIS = Arrays.asList("NativePRNGBlocking",
"NativePRNGNonBlocking", "NativePRNG", "SHA1PRNG");
private static final List<String> SECURE_RANDOM_ALGORITHMS_WINDOWS = Arrays.asList("SHA1PRNG", "Windows-PRNG");
static List<String> secureRandomAlgorithmNames() {
return OPERATING_SYSTEM_NAME.contains("win") ? SECURE_RANDOM_ALGORITHMS_WINDOWS
: SECURE_RANDOM_ALGORITHMS_LINUX_OSX_SOLARIS;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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<List<JsonNode>> FILTER_TYPE_REF = new TypeRef<>() {
};
private static final TypeRef<List<JsonNode>> 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 <T> Iterable<T> getAllOf(String keyspace, Class<T> 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<JsonNode> 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 <T> Iterable<T> find(KeyValueQuery<?> query, String keyspace, Class<T> 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<JsonNode> 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<JsonNode> 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<JsonNode> 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<String, String> meta = Collections.singletonMap("sql", sql);
daprClient.invokeBinding(bindingName, "exec", null, meta).block();
}
private <T> T queryUsingBinding(String sql, TypeRef<T> typeRef) {
Map<String, String> meta = Collections.singletonMap("sql", sql);
return daprClient.invokeBinding(bindingName, "query", null, meta, typeRef).block();
}
private <T> List<T> convertValues(List<JsonNode> values, Class<T> type) {
if (values == null || values.isEmpty()) {
return Collections.emptyList();
}
return values.stream()
.map(value -> convertValue(value, type))
.collect(Collectors.toList());
}
private <T> T convertValue(JsonNode value, Class<T> 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<JsonNode> 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();
}
}

View File

@ -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<List<List<Object>>> FILTER_TYPE_REF = new TypeRef<>() {
};
private static final TypeRef<List<List<Long>>> 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 <T> Iterable<T> getAllOf(String keyspace, Class<T> 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<List<Object>> 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 <T> Iterable<T> find(KeyValueQuery<?> query, String keyspace, Class<T> 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<List<Object>> 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<List<Long>> 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<List<Long>> 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<String, String> meta = Collections.singletonMap("sql", sql);
daprClient.invokeBinding(bindingName, "exec", null, meta).block();
}
private <T> T queryUsingBinding(String sql, TypeRef<T> typeRef) {
Map<String, String> meta = Collections.singletonMap("sql", sql);
return daprClient.invokeBinding(bindingName, "query", null, meta, typeRef).block();
}
private <T> Iterable<T> convertValues(List<List<Object>> values, Class<T> 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> T convertValue(Object value, Class<T> type) {
try {
return mapper.convertValue(value, type);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private long extractCount(List<List<Long>> values) {
if (values == null || values.isEmpty()) {
return 0;
}
return values.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList())
.get(0);
}
}

View File

@ -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();
}
}

View File

@ -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";
}
}

View File

@ -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;
}

View File

@ -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<Object> {
private final PropertyPath path;
private final Function<Object, Boolean> 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<Object, Boolean> 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);
}
}

View File

@ -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<Object> isTrue() {
return new DaprPredicate(part.getProperty(), true);
}
public Predicate<Object> isFalse() {
return new DaprPredicate(part.getProperty(), false);
}
public Predicate<Object> 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<Object> isNull() {
return new DaprPredicate(part.getProperty(), null, Objects::isNull);
}
public Predicate<Object> isNotNull() {
return isNull().negate();
}
public Predicate<Object> isLessThan(Object value) {
return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) < 0);
}
public Predicate<Object> isLessThanEqual(Object value) {
return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) <= 0);
}
public Predicate<Object> isGreaterThan(Object value) {
return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) > 0);
}
public Predicate<Object> isGreaterThanEqual(Object value) {
return new DaprPredicate(part.getProperty(), value, o -> Comparators.nullsHigh().compare(o, value) >= 0);
}
public Predicate<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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());
});
}
}

View File

@ -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<KeyValueQuery<Predicate<?>>, Predicate<?>> {
public DaprPredicateQueryCreator(PartTree tree, ParameterAccessor parameters) {
super(tree, parameters);
}
@Override
protected Predicate<?> create(Part part, Iterator<Object> 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<Object> iterator) {
return base.and((Predicate) create(part, iterator));
}
@Override
protected Predicate<?> or(Predicate<?> base, Predicate<?> criteria) {
return base.or((Predicate) criteria);
}
@Override
protected KeyValueQuery<Predicate<?>> complete(@Nullable Predicate<?> criteria, Sort sort) {
if (criteria == null) {
return new KeyValueQuery<>(it -> true, sort);
}
return new KeyValueQuery<>(criteria, sort);
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<version>0.13.0-SNAPSHOT</version>
</parent>
<artifactId>dapr-spring-messaging</artifactId>
<name>dapr-spring-messaging</name>
<description>Dapr Spring Messaging</description>
<packaging>jar</packaging>
</project>

View File

@ -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<T> {
/**
* 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<T> 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 <T> the message payload type
*/
interface SendMessageBuilder<T> {
/**
* Specify the topic to send the message to.
*
* @param topic the destination topic
* @return the current builder with the destination topic specified
*/
SendMessageBuilder<T> 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<Void> sendAsync();
}
}

View File

@ -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<T> implements DaprMessagingOperations<T> {
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<T> newMessage(T message) {
return new SendMessageBuilderImpl<>(this, message);
}
private void doSend(String topic, T message) {
doSendAsync(topic, message).block();
}
private Mono<Void> 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<T> implements SendMessageBuilder<T> {
private final DaprMessagingTemplate<T> template;
private final T message;
private String topic;
SendMessageBuilderImpl(DaprMessagingTemplate<T> template, T message) {
this.template = template;
this.message = message;
}
@Override
public SendMessageBuilder<T> withTopic(String topic) {
this.topic = topic;
return this;
}
@Override
public void send() {
this.template.doSend(this.topic, this.message);
}
@Override
public Mono<Void> sendAsync() {
return this.template.doSendAsync(this.topic, this.message);
}
}
}

169
dapr-spring/pom.xml Normal file
View File

@ -0,0 +1,169 @@
<project
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-parent</artifactId>
<version>1.13.0-SNAPSHOT</version>
</parent>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-parent</artifactId>
<packaging>pom</packaging>
<version>0.13.0-SNAPSHOT</version>
<name>dapr-spring-parent</name>
<description>SDK extension for Spring and Spring Boot</description>
<modules>
<module>dapr-spring-core</module>
<module>dapr-spring-data</module>
<module>dapr-spring-messaging</module>
<module>dapr-spring-boot-autoconfigure</module>
<module>dapr-spring-boot-starters/dapr-spring-boot-starter</module>
</modules>
<properties>
<springboot.version>3.2.6</springboot.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${springboot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Dapr dependencies -->
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${dapr.sdk.version}</version>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>${dapr.sdk.version}</version>
</dependency>
<!-- Spring dependencies -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.gmazzo.okhttp.mock</groupId>
<artifactId>mock-client</artifactId>
<version>2.0.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.7.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<outputDirectory>target/jacoco-report/</outputDirectory>
</configuration>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<includes>
<include>io.dapr.springboot.DaprBeanPostProcessor</include>
</includes>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>80%</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,11 @@
<FindBugsFilter>
<Match>
<Package name="~io\.dapr\.spring.*"/>
<Bug pattern="EI_EXPOSE_REP"/>
</Match>
<Match>
<Package name="~io\.dapr\.spring.*"/>
<Bug pattern="EI_EXPOSE_REP2"/>
</Match>
</FindBugsFilter>

View File

@ -1,6 +1,6 @@
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
@ -18,6 +18,7 @@
<protobuf.version>3.25.0</protobuf.version>
<protocCommand>protoc</protocCommand>
<dapr.proto.baseurl>https://raw.githubusercontent.com/dapr/dapr/v1.14.0-rc.2/dapr/proto</dapr.proto.baseurl>
<dapr.sdk.version>1.13.0-SNAPSHOT</dapr.sdk.version>
<dapr.sdk.alpha.version>0.13.0-SNAPSHOT</dapr.sdk.alpha.version>
<os-maven-plugin.version>1.7.1</os-maven-plugin.version>
<maven-dependency-plugin.version>3.1.1</maven-dependency-plugin.version>
@ -323,6 +324,7 @@
<module>sdk-actors</module>
<module>sdk-workflows</module>
<module>sdk-springboot</module>
<module>dapr-spring</module>
<module>examples</module>
<!-- We are following test containers artifact convention on purpose, don't rename -->
<module>testcontainers-dapr</module>

View File

@ -25,7 +25,6 @@
<springboot.version>3.3.1</springboot.version>
<logback-classic.version>1.4.12</logback-classic.version>
<wiremock.version>3.9.1</wiremock.version>
<testcontainers-dapr.version>${dapr.sdk.alpha.version}</testcontainers-dapr.version>
<testcontainers-test.version>1.20.0</testcontainers-test.version>
</properties>
@ -137,14 +136,14 @@
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<artifactId>dapr-sdk-actors</artifactId>
<version>${dapr.sdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk-actors</artifactId>
<version>${dapr.sdk.version}</version>
<artifactId>dapr-sdk-workflows</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
@ -153,11 +152,44 @@
<version>${dapr.sdk.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-core</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-data</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-messaging</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-boot-autoconfigure</artifactId>
<version>${dapr.sdk.alpha.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-keyvalue</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock-standalone</artifactId>
@ -187,6 +219,29 @@
<version>3.9</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>${dapr.sdk.alpha.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers-test.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers-test.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>${testcontainers-test.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
@ -216,17 +271,6 @@
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.dapr</groupId>
<artifactId>testcontainers-dapr</artifactId>
<version>${testcontainers-dapr.version}</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers-test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View File

@ -88,11 +88,12 @@ public class MethodInvokeIT extends BaseIT {
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");
}
}

View File

@ -117,8 +117,10 @@ public class MethodInvokeIT extends BaseIT {
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);
}
}

View File

@ -0,0 +1,15 @@
package io.dapr.it.spring.data;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
public class CustomMySQLContainer<SELF extends MySQLContainer<SELF>> extends MySQLContainer<SELF> {
public CustomMySQLContainer(String dockerImageName) {
super(DockerImageName.parse(dockerImageName));
}
protected void waitUntilContainerStarted() {
this.getWaitStrategy().waitUntilReady(this);
}
}

View File

@ -0,0 +1,142 @@
/*
* 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<String, String> STATE_STORE_PROPERTIES = createStateStoreProperties();
private static final Map<String, String> 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.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort);
registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort);
}
private static Map<String, String> createStateStoreProperties() {
Map<String, String> 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<TestType> 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<TestType> byContent = repository.findByContent("test2");
assertEquals(1, byContent.size());
}
}

View File

@ -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";
}

View File

@ -0,0 +1,242 @@
/*
* 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<String, String> STATE_STORE_PROPERTIES = createStateStoreProperties();
private static final Map<String, String> 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.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort);
registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort);
}
private static Map<String, String> createStateStoreProperties() {
Map<String, String> 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<TestType> findById = keyValueTemplate.findById(itemId, TestType.class);
assertThat(findById.isEmpty()).isFalse();
assertThat(findById.get()).isEqualTo(savedType);
KeyValueQuery<String> keyValueQuery = new KeyValueQuery<>("content == 'test'");
Iterable<TestType> 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<TestType> items = new ArrayList<>();
for (int i = 0; i < count; i++) {
items.add(keyValueTemplate.insert(new TestType(i, "test")));
}
KeyValueQuery<String> keyValueQuery = new KeyValueQuery<>("content == 'test'");
keyValueQuery.setRows(100);
keyValueQuery.setOffset(0);
Iterable<TestType> 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<TestType> 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<TestType> 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<String> keyValueQuery = new KeyValueQuery<>("content == 'test'");
keyValueQuery.setRows(100);
keyValueQuery.setOffset(0);
long result = keyValueTemplate.count(keyValueQuery, TestType.class);
assertThat(result).isEqualTo(1);
}
}

View File

@ -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.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<String, String> STATE_STORE_PROPERTIES = createStateStoreProperties();
private static final Map<String, String> 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.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort);
registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort);
}
private static Map<String, String> createStateStoreProperties() {
Map<String, String> 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<TestType> findById = keyValueTemplate.findById(itemId, TestType.class);
assertThat(findById.isEmpty()).isFalse();
assertThat(findById.get()).isEqualTo(savedType);
KeyValueQuery<String> keyValueQuery = new KeyValueQuery<>("content == 'test'");
Iterable<TestType> 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<TestType> items = new ArrayList<>();
for (int i = 0; i < count; i++) {
items.add(keyValueTemplate.insert(new TestType(i, "test")));
}
KeyValueQuery<String> keyValueQuery = new KeyValueQuery<>("content == 'test'");
keyValueQuery.setRows(100);
keyValueQuery.setOffset(0);
Iterable<TestType> 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<TestType> 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<TestType> 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<String> keyValueQuery = new KeyValueQuery<>("content == 'test'");
keyValueQuery.setRows(100);
keyValueQuery.setOffset(0);
long result = keyValueTemplate.count(keyValueQuery, TestType.class);
assertThat(result).isEqualTo(1);
}
}

View File

@ -0,0 +1,48 @@
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.endpoint}") String daprHttpEndpoint,
@Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint,
@Value("${dapr.http.port}") String daprHttpPort,
@Value("${dapr.grpc.port}") String daprGrpcPort
){
return new TestcontainersDaprClientCustomizer(daprHttpEndpoint, daprGrpcEndpoint, daprHttpPort, daprGrpcPort);
}
@Bean
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);
}
}

View File

@ -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);
}
}

View File

@ -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<TestType, Integer> {
List<TestType> findByContent(String content);
}

View File

@ -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.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);
registry.add("dapr.http.endpoint", DAPR_CONTAINER::getHttpEndpoint);
registry.add("dapr.grpc.endpoint", DAPR_CONTAINER::getGrpcEndpoint);
registry.add("dapr.grpc.port", DAPR_CONTAINER::getGrpcPort);
registry.add("dapr.http.port", DAPR_CONTAINER::getHttpPort);
}
@Autowired
private DaprMessagingTemplate<String> 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<CloudEvent<String>> events = testRestController.getEvents();
assertThat(events.size()).isEqualTo(10);
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.endpoint}") String daprHttpEndpoint,
@Value("${dapr.grpc.endpoint}") String daprGrpcEndpoint,
@Value("${dapr.http.port}") String daprHttpPort,
@Value("${dapr.grpc.port}") String daprGrpcPort
){
return new TestcontainersDaprClientCustomizer(daprHttpEndpoint, daprGrpcEndpoint, daprHttpPort, daprGrpcPort);
}
@Bean
public DaprMessagingTemplate<String> messagingTemplate(DaprClient daprClient,
DaprPubSubProperties daprPubSubProperties) {
return new DaprMessagingTemplate<>(daprClient, daprPubSubProperties.getName());
}
}
}

View File

@ -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<CloudEvent<String>> events = new ArrayList<>();
@GetMapping("/")
public String ok() {
return "OK";
}
@Topic(name = topicName, pubsubName = pubSubName)
@PostMapping("/subscribe")
public void handleMessages(@RequestBody CloudEvent<String> event) {
LOG.info("++++++CONSUME {}------", event);
events.add(event);
}
public List<CloudEvent<String>> getEvents() {
return events;
}
}

View File

@ -19,11 +19,13 @@ import io.dapr.client.DaprClientBuilder;
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.any;
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 @@ public class DaprContainerTest {
@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 class DaprContainerTest {
// Get the state back from the state store
State<String> 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 class DaprContainerTest {
@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<String, String> 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()));
}
}

View File

@ -14,6 +14,7 @@ limitations under the License.
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 org.testcontainers.junit.jupiter.Testcontainers;
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");
}
}

View File

@ -0,0 +1,42 @@
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.http.endpoint", dapr::getHttpEndpoint);
registry.add("dapr.grpc.endpoint", dapr::getGrpcEndpoint);
registry.add("dapr.grpc.port", dapr::getGrpcPort);
registry.add("dapr.http.port", dapr::getHttpPort);
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<CloudEvent<?>> 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<CloudEvent<?>> getAllEvents() {
return events;
}
}

View File

@ -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);
};
}
}

View File

@ -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<String> payloads;
private String workflowId;
public TestWorkflowPayload() {
}
public TestWorkflowPayload(List<String> payloads, String workflowId) {
this.payloads = payloads;
this.workflowId = workflowId;
}
public TestWorkflowPayload(List<String> payloads) {
this.payloads = payloads;
}
public List<String> getPayloads() {
return payloads;
}
public void setPayloads(List<String> payloads) {
this.payloads = payloads;
}
public String getWorkflowId() {
return workflowId;
}
public void setWorkflowId(String workflowId) {
this.workflowId = workflowId;
}
}

View File

@ -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 {
}
}

View File

@ -0,0 +1,11 @@
{
"filter": {
"EQ": {
"content": "test"
}
},
"page": {
"limit": 0,
"token": "0"
}
}

View File

@ -1,6 +1,6 @@
<project
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>

View File

@ -1,7 +1,7 @@
<project
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">
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">
<modelVersion>4.0.0</modelVersion>
<parent>
@ -143,20 +143,20 @@
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/sdk_version.properties</include>
</includes>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/sdk_version.properties</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/sdk_version.properties</exclude>
</excludes>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<excludes>
<exclude>**/sdk_version.properties</exclude>
</excludes>
</resource>
</resources>
</resources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@ -215,7 +215,7 @@
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>80%</minimum>

View File

@ -34,7 +34,13 @@
<groupId>io.dapr</groupId>
<artifactId>dapr-sdk</artifactId>
<version>${project.parent.version}</version>
<scope>test</scope>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.dapr.spring</groupId>
<artifactId>dapr-spring-core</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>

View File

@ -161,6 +161,10 @@ public class DaprContainer extends GenericContainer<DaprContainer> {
return getMappedPort(DAPRD_DEFAULT_GRPC_PORT);
}
public String getGrpcEndpoint() {
return ":" + getMappedPort(DAPRD_DEFAULT_GRPC_PORT);
}
public DaprContainer withAppChannelAddress(String appChannelAddress) {
this.appChannelAddress = appChannelAddress;
return this;

View File

@ -0,0 +1,40 @@
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 httpEndpoint;
private final String grpcEndpoint;
private final String daprHttpPort;
private final String daprGrpcPort;
/**
* Constructor for TestcontainersDaprClientCustomizer.
* @param httpEndpoint HTTP endpoint.
* @param grpcEndpoint GRPC endpoint.
* @param daprHttpPort Dapr HTTP port.
* @param daprGrpcPort Dapr GRPC port.
*/
public TestcontainersDaprClientCustomizer(
String httpEndpoint,
String grpcEndpoint,
String daprHttpPort,
String daprGrpcPort
) {
this.httpEndpoint = httpEndpoint;
this.grpcEndpoint = grpcEndpoint;
this.daprHttpPort = daprHttpPort;
this.daprGrpcPort = daprGrpcPort;
}
@Override
public void customize(DaprClientBuilder daprClientBuilder) {
daprClientBuilder.withPropertyOverride(Properties.HTTP_ENDPOINT, httpEndpoint);
daprClientBuilder.withPropertyOverride(Properties.GRPC_ENDPOINT, grpcEndpoint);
daprClientBuilder.withPropertyOverride(Properties.HTTP_PORT, daprHttpPort);
daprClientBuilder.withPropertyOverride(Properties.GRPC_PORT, daprGrpcPort);
}
}