From 5866f002f0b3e96783f191942151a79f56f6822b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 5 Jul 2022 07:49:51 +0200 Subject: [PATCH] Support AOT processing of Cassandra repositories. We now ship runtime hints for AOT processing of the Spring Data infrastructure. Closes #1280 --- .../data/cassandra/CassandraManagedTypes.java | 83 ++ .../aot/CassandraRuntimeHintsRegistrar.java | 61 ++ .../data/cassandra/aot/package-info.java | 7 + .../AbstractCassandraConfiguration.java | 52 +- .../config/CassandraAuditingRegistrar.java | 56 +- .../cassandra/config/DefaultBeanNames.java | 2 +- .../ReactiveCassandraAuditingRegistrar.java | 20 +- .../convert/CassandraJsr310Converters.java | 14 + .../core/cql/keyspace/DefaultOption.java | 43 +- .../main/resources/META-INF/spring.schemas | 6 +- .../resources/META-INF/spring/aot.factories | 1 + .../cassandra/config/spring-cassandra-4.0.xsd | 814 ++++++++++++++++++ .../cassandra/aot/CodeContributionAssert.java | 84 ++ ...ndraAuditingRegistrarIntegrationTests.java | 10 +- .../CreateUserTypeIntegrationTests.java | 19 +- ...ssandraRuntimeHintsRegistrarUnitTests.java | 51 ++ .../config/spring-data-cassandra-basic.xml | 2 +- .../cassandra/config/multiple-converters.xml | 2 +- 18 files changed, 1259 insertions(+), 68 deletions(-) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/CassandraManagedTypes.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrar.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/package-info.java create mode 100644 spring-data-cassandra/src/main/resources/META-INF/spring/aot.factories create mode 100644 spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/aot/CodeContributionAssert.java create mode 100644 spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrarUnitTests.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/CassandraManagedTypes.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/CassandraManagedTypes.java new file mode 100644 index 000000000..a74d50fb6 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/CassandraManagedTypes.java @@ -0,0 +1,83 @@ +/* + * Copyright 2022 the original author or 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 + * + * https://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 org.springframework.data.cassandra; + +import java.util.Arrays; +import java.util.function.Consumer; + +import org.springframework.data.domain.ManagedTypes; + +/** + * Cassandra-specific extension to {@link ManagedTypes}. + * + * @author Mark Paluch + * @since 4.0 + */ +public final class CassandraManagedTypes implements ManagedTypes { + + private final ManagedTypes delegate; + + private CassandraManagedTypes(ManagedTypes types) { + this.delegate = types; + } + + /** + * Wraps an existing {@link ManagedTypes} object with {@link CassandraManagedTypes}. + * + * @param managedTypes + * @return + */ + public static CassandraManagedTypes from(ManagedTypes managedTypes) { + return new CassandraManagedTypes(managedTypes); + } + + /** + * Factory method used to construct {@link CassandraManagedTypes} from the given array of {@link Class types}. + * + * @param types array of {@link Class types} used to initialize the {@link ManagedTypes}; must not be {@literal null}. + * @return new instance of {@link CassandraManagedTypes} initialized from {@link Class types}. + */ + public static CassandraManagedTypes from(Class... types) { + return fromIterable(Arrays.asList(types)); + } + + /** + * Factory method used to construct {@link CassandraManagedTypes} from the given, required {@link Iterable} of + * {@link Class types}. + * + * @param types {@link Iterable} of {@link Class types} used to initialize the {@link ManagedTypes}; must not be + * {@literal null}. + * @return new instance of {@link CassandraManagedTypes} initialized the given, required {@link Iterable} of + * {@link Class types}. + */ + public static CassandraManagedTypes fromIterable(Iterable> types) { + return from(ManagedTypes.fromIterable(types)); + } + + /** + * Factory method to return an empty {@link CassandraManagedTypes} object. + * + * @return an empty {@link CassandraManagedTypes} object. + */ + public static CassandraManagedTypes empty() { + return from(ManagedTypes.empty()); + } + + @Override + public void forEach(Consumer> action) { + delegate.forEach(action); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrar.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrar.java new file mode 100644 index 000000000..acac96a24 --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrar.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 the original author or 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 + * + * https://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 org.springframework.data.cassandra.aot; + +import java.util.Arrays; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; +import org.springframework.data.cassandra.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.cassandra.core.mapping.event.BeforeSaveCallback; +import org.springframework.data.cassandra.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.cassandra.core.mapping.event.ReactiveBeforeSaveCallback; +import org.springframework.data.cassandra.repository.support.SimpleCassandraRepository; +import org.springframework.data.cassandra.repository.support.SimpleReactiveCassandraRepository; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.lang.Nullable; + +/** + * {@link RuntimeHintsRegistrar} for repository types and entity callbacks. + * + * @author Mark Paluch + * @since 4.0 + */ +class CassandraRuntimeHintsRegistrar implements RuntimeHintsRegistrar { + + private static final boolean PROJECT_REACTOR_PRESENT = ReactiveWrappers + .isAvailable(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR); + + @Override + public void registerHints(org.springframework.aot.hint.RuntimeHints hints, @Nullable ClassLoader classLoader) { + + hints.reflection().registerTypes(Arrays.asList(TypeReference.of(SimpleCassandraRepository.class), // + TypeReference.of(BeforeConvertCallback.class), // + TypeReference.of(BeforeSaveCallback.class)), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS)); + + if (PROJECT_REACTOR_PRESENT) { + + hints.reflection().registerTypes(Arrays.asList(TypeReference.of(SimpleReactiveCassandraRepository.class), // + TypeReference.of(ReactiveBeforeConvertCallback.class), // + TypeReference.of(ReactiveBeforeSaveCallback.class)), + builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS)); + } + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/package-info.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/package-info.java new file mode 100644 index 000000000..735b709bb --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/aot/package-info.java @@ -0,0 +1,7 @@ +/** + * Ahead of Time processing utilities for Spring Data Cassandra. + */ +@NonNullApi +package org.springframework.data.cassandra.aot; + +import org.springframework.lang.NonNullApi; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java index 5a78eb7f9..c26f3a039 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.core.io.ByteArrayResource; +import org.springframework.data.cassandra.CassandraManagedTypes; import org.springframework.data.cassandra.SessionFactory; import org.springframework.data.cassandra.core.CassandraAdminTemplate; import org.springframework.data.cassandra.core.convert.CassandraConverter; @@ -36,6 +37,7 @@ import org.springframework.data.cassandra.core.mapping.SimpleUserTypeResolver; import org.springframework.data.cassandra.core.mapping.Table; import org.springframework.data.cassandra.core.mapping.UserTypeResolver; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.domain.ManagedTypes; import org.springframework.data.mapping.context.MappingContext; import org.springframework.lang.Nullable; @@ -58,9 +60,8 @@ public abstract class AbstractCassandraConfiguration extends AbstractSessionConf private @Nullable ClassLoader beanClassLoader; /** - * Creates a {@link CassandraConverter} using the configured {@link #cassandraMapping()}. - * - * Will apply all specified {@link #customConversions()}. + * Creates a {@link CassandraConverter} using the configured {@link #cassandraMapping()}. Will apply all specified + * {@link #customConversions()}. * * @return {@link CassandraConverter} used to convert Java and Cassandra value types during the mapping process. * @see #cassandraMapping() @@ -71,11 +72,11 @@ public abstract class AbstractCassandraConfiguration extends AbstractSessionConf CqlSession cqlSession = getRequiredSession(); - UserTypeResolver userTypeResolver = - new SimpleUserTypeResolver(cqlSession, CqlIdentifier.fromCql(getKeyspaceName())); + UserTypeResolver userTypeResolver = new SimpleUserTypeResolver(cqlSession, + CqlIdentifier.fromCql(getKeyspaceName())); - MappingCassandraConverter converter = - new MappingCassandraConverter(requireBeanOfType(CassandraMappingContext.class)); + MappingCassandraConverter converter = new MappingCassandraConverter( + requireBeanOfType(CassandraMappingContext.class)); converter.setCodecRegistry(cqlSession.getContext().getCodecRegistry()); converter.setUserTypeResolver(userTypeResolver); @@ -84,23 +85,44 @@ public abstract class AbstractCassandraConfiguration extends AbstractSessionConf return converter; } + /** + * Returns the a {@link CassandraManagedTypes} object holding the initial entity set. + * + * @return new instance of {@link CassandraManagedTypes}. + * @throws ClassNotFoundException + * @since 4.0 + */ + @Bean + public CassandraManagedTypes cassandraManagedTypes() throws ClassNotFoundException { + return CassandraManagedTypes.fromIterable(getInitialEntitySet()); + } + + /** + * Return the {@link MappingContext} instance to map Entities to {@link Object Java Objects}. + * + * @see org.springframework.data.cassandra.core.mapping.CassandraMappingContext + * @deprecated since 4.0, use {@link #cassandraMappingContext(ManagedTypes)} instead. + */ + @Deprecated(since = "4.0", forRemoval = true) + public CassandraMappingContext cassandraMapping() throws ClassNotFoundException { + return cassandraMappingContext(cassandraManagedTypes()); + } + /** * Return the {@link MappingContext} instance to map Entities to {@link Object Java Objects}. * - * @throws ClassNotFoundException if the Cassandra Entity class type identified by name - * cannot be found during the scan. * @see org.springframework.data.cassandra.core.mapping.CassandraMappingContext */ @Bean - public CassandraMappingContext cassandraMapping() throws ClassNotFoundException { + public CassandraMappingContext cassandraMappingContext(CassandraManagedTypes cassandraManagedTypes) { CqlSession cqlSession = getRequiredSession(); - UserTypeResolver userTypeResolver = - new SimpleUserTypeResolver(cqlSession, CqlIdentifier.fromCql(getKeyspaceName())); + UserTypeResolver userTypeResolver = new SimpleUserTypeResolver(cqlSession, + CqlIdentifier.fromCql(getKeyspaceName())); - CassandraMappingContext mappingContext = - new CassandraMappingContext(userTypeResolver, SimpleTupleTypeFactory.DEFAULT); + CassandraMappingContext mappingContext = new CassandraMappingContext(userTypeResolver, + SimpleTupleTypeFactory.DEFAULT); CustomConversions customConversions = requireBeanOfType(CassandraCustomConversions.class); @@ -108,7 +130,7 @@ public abstract class AbstractCassandraConfiguration extends AbstractSessionConf mappingContext.setCodecRegistry(cqlSession.getContext().getCodecRegistry()); mappingContext.setCustomConversions(customConversions); - mappingContext.setInitialEntitySet(getInitialEntitySet()); + mappingContext.setManagedTypes(cassandraManagedTypes); mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); return mappingContext; diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java index 3157409e3..83ae5087b 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrar.java @@ -17,17 +17,19 @@ package org.springframework.data.cassandra.config; import java.lang.annotation.Annotation; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; import org.springframework.data.auditing.config.AuditingConfiguration; import org.springframework.data.cassandra.core.mapping.event.AuditingEntityCallback; import org.springframework.data.config.ParsingUtils; +import org.springframework.data.mapping.context.PersistentEntities; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -49,12 +51,10 @@ class CassandraAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport } @Override - public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { + protected void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration, + BeanDefinitionRegistry registry) { - Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null"); - Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); - - super.registerBeanDefinitions(annotationMetadata, registry); + potentiallyRegisterCassandraPersistentEntities(builder, registry); } @Override @@ -62,13 +62,8 @@ class CassandraAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport Assert.notNull(configuration, "AuditingConfiguration must not be null"); - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class); - - BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(PersistentEntitiesFactoryBean.class); - definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); - - builder.addConstructorArgValue(definition.getBeanDefinition()); - return configureDefaultAuditHandlerAttributes(configuration, builder); + return configureDefaultAuditHandlerAttributes(configuration, + BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class)); } @Override @@ -85,7 +80,40 @@ class CassandraAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(), AuditingEntityCallback.class.getName(), registry); + } + static void potentiallyRegisterCassandraPersistentEntities(BeanDefinitionBuilder builder, + BeanDefinitionRegistry registry) { + + String persistentEntitiesBeanName = detectPersistentEntitiesBeanName(registry); + + if (persistentEntitiesBeanName == null) { + + persistentEntitiesBeanName = BeanDefinitionReaderUtils.uniqueBeanName("cassandraPersistentEntities", registry); + + // TODO: https://github.com/spring-projects/spring-framework/issues/28728 + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(PersistentEntities.class) // + .setFactoryMethod("of") // + .addConstructorArgReference("cassandraMappingContext"); + + registry.registerBeanDefinition(persistentEntitiesBeanName, definition.getBeanDefinition()); + } + + builder.addConstructorArgReference(persistentEntitiesBeanName); + } + + @Nullable + private static String detectPersistentEntitiesBeanName(BeanDefinitionRegistry registry) { + + if (registry instanceof ListableBeanFactory beanFactory) { + for (String bn : beanFactory.getBeanNamesForType(PersistentEntities.class)) { + if (bn.startsWith("cassandra")) { + return bn; + } + } + } + + return null; } } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java index a6fb7280c..6e052e741 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/DefaultBeanNames.java @@ -23,6 +23,6 @@ public interface DefaultBeanNames extends DefaultCqlBeanNames { String DATA_TEMPLATE = "cassandraTemplate"; String CONVERTER = "cassandraConverter"; - String CONTEXT = "cassandraMapping"; + String CONTEXT = "cassandraMappingContext"; String USER_TYPE_RESOLVER = "userTypeResolver"; } diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/ReactiveCassandraAuditingRegistrar.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/ReactiveCassandraAuditingRegistrar.java index 35fa0894c..3939bc2ca 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/ReactiveCassandraAuditingRegistrar.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/ReactiveCassandraAuditingRegistrar.java @@ -18,11 +18,9 @@ package org.springframework.data.cassandra.config; import java.lang.annotation.Annotation; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport; import org.springframework.data.auditing.config.AuditingConfiguration; @@ -49,12 +47,9 @@ class ReactiveCassandraAuditingRegistrar extends AuditingBeanDefinitionRegistrar } @Override - public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) { - - Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null"); - Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); - - super.registerBeanDefinitions(annotationMetadata, registry); + protected void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration, + BeanDefinitionRegistry registry) { + CassandraAuditingRegistrar.potentiallyRegisterCassandraPersistentEntities(builder, registry); } @Override @@ -62,13 +57,8 @@ class ReactiveCassandraAuditingRegistrar extends AuditingBeanDefinitionRegistrar Assert.notNull(configuration, "AuditingConfiguration must not be null"); - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class); - - BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(PersistentEntitiesFactoryBean.class); - definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); - - builder.addConstructorArgValue(definition.getBeanDefinition()); - return configureDefaultAuditHandlerAttributes(configuration, builder); + return configureDefaultAuditHandlerAttributes(configuration, + BeanDefinitionBuilder.rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class)); } @Override diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java index 1c0729139..701ed06f7 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraJsr310Converters.java @@ -29,6 +29,7 @@ import java.util.List; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; +import org.springframework.lang.NonNull; /** * Helper class to register JodaTime specific {@link Converter} implementations in case the library is present on the @@ -55,6 +56,7 @@ public abstract class CassandraJsr310Converters { converters.add(LocalTimeToMillisOfDayConverter.INSTANCE); converters.add(DateToInstantConverter.INSTANCE); + converters.add(InstantToDateConverter.INSTANCE); converters.add(LocalDateTimeToInstantConverter.INSTANCE); return converters; @@ -111,6 +113,18 @@ public abstract class CassandraJsr310Converters { } } + @ReadingConverter + public enum InstantToDateConverter implements Converter { + + INSTANCE; + + @NonNull + @Override + public Date convert(Instant source) { + return Date.from(source); + } + } + /** * Converter from {@link LocalDateTime} to {@link Instant}. * diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/DefaultOption.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/DefaultOption.java index ff357400c..3fbaef6c1 100644 --- a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/DefaultOption.java +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/cql/keyspace/DefaultOption.java @@ -20,6 +20,10 @@ import static org.springframework.data.cassandra.core.cql.keyspace.CqlStringUtil import java.lang.reflect.Constructor; import java.util.Collection; import java.util.Map; +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -32,6 +36,8 @@ import org.springframework.util.Assert; */ public class DefaultOption implements Option { + protected final Log log = LogFactory.getLog(getClass()); + private final String name; private final Class type; @@ -86,18 +92,51 @@ public class DefaultOption implements Option { } } + if (type == Long.class) { + return tryParse(value, Long::parseLong); + } + if (type == Integer.class) { + return tryParse(value, Integer::parseInt); + } + if (type == Double.class) { + return tryParse(value, Double::parseDouble); + } + if (type == Float.class) { + return tryParse(value, Float::parseFloat); + } + if (type == Boolean.class) { + return tryParse(value, Boolean::valueOf); + } + // check class via String constructor try { Constructor ctor = type.getConstructor(String.class); - if (!ctor.isAccessible()) { + if (!ctor.canAccess(this)) { ctor.setAccessible(true); } ctor.newInstance(value.toString()); return true; - } catch (Exception e) {} + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.debug("Cannot parse option %s into %s".formatted(getName(), getType()), e); + } + + } return false; } + private boolean tryParse(Object value, Consumer parseFunction) { + try { + parseFunction.accept(value.toString()); + return true; + } catch (RuntimeException e) { + if (log.isDebugEnabled()) { + log.debug("Cannot parse option %s into %s".formatted(getName(), getType()), e); + } + return false; + } + } + public Class getType() { return type; } diff --git a/spring-data-cassandra/src/main/resources/META-INF/spring.schemas b/spring-data-cassandra/src/main/resources/META-INF/spring.schemas index e108fa191..ebbe9c885 100644 --- a/spring-data-cassandra/src/main/resources/META-INF/spring.schemas +++ b/spring-data-cassandra/src/main/resources/META-INF/spring.schemas @@ -8,7 +8,8 @@ http\://www.springframework.org/schema/data/cassandra/spring-cassandra-1.5.xsd=o http\://www.springframework.org/schema/data/cassandra/spring-cassandra-2.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.0.xsd http\://www.springframework.org/schema/data/cassandra/spring-cassandra-2.2.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd http\://www.springframework.org/schema/data/cassandra/spring-cassandra-3.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-3.0.xsd -http\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-3.0.xsd +http\://www.springframework.org/schema/data/cassandra/spring-cassandra-4.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd +http\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd https\://www.springframework.org/schema/cql/spring-cql-1.0.xsd=org/springframework/data/cassandra/config/spring-cql-1.0.xsd https\://www.springframework.org/schema/cql/spring-cql-1.5.xsd=org/springframework/data/cassandra/config/spring-cql-1.5.xsd https\://www.springframework.org/schema/cql/spring-cql-2.0.xsd=org/springframework/data/cassandra/config/spring-cql-2.0.xsd @@ -19,4 +20,5 @@ https\://www.springframework.org/schema/data/cassandra/spring-cassandra-1.5.xsd= https\://www.springframework.org/schema/data/cassandra/spring-cassandra-2.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.0.xsd https\://www.springframework.org/schema/data/cassandra/spring-cassandra-2.2.xsd=org/springframework/data/cassandra/config/spring-cassandra-2.2.xsd https\://www.springframework.org/schema/data/cassandra/spring-cassandra-3.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-3.0.xsd -https\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-3.0.xsd +https\://www.springframework.org/schema/data/cassandra/spring-cassandra-4.0.xsd=org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd +https\://www.springframework.org/schema/data/cassandra/spring-cassandra.xsd=org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd diff --git a/spring-data-cassandra/src/main/resources/META-INF/spring/aot.factories b/spring-data-cassandra/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000..d77a6be1f --- /dev/null +++ b/spring-data-cassandra/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.data.cassandra.aot.CassandraRuntimeHintsRegistrar diff --git a/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd new file mode 100644 index 000000000..db0fb2fbc --- /dev/null +++ b/spring-data-cassandra/src/main/resources/org/springframework/data/cassandra/config/spring-cassandra-4.0.xsd @@ -0,0 +1,814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + elements. + ]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Is this bean "enabled", meaning the scripts will be executed? + Defaults to true but can be used to switch on and off script execution + depending on the environment. + + + + + + + Should failed CQL statements be ignored during execution? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Should failed CQL statements be ignored during execution? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/aot/CodeContributionAssert.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/aot/CodeContributionAssert.java new file mode 100644 index 000000000..4e6e23d8e --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/aot/CodeContributionAssert.java @@ -0,0 +1,84 @@ +/* + * Copyright 2022 the original author or 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 + * + * https://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 org.springframework.data.cassandra.aot; + +import static org.assertj.core.api.Assertions.*; + +import java.util.stream.Stream; + +import org.assertj.core.api.AbstractAssert; + +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.hint.JdkProxyHint; +import org.springframework.aot.hint.RuntimeHintsPredicates; + +/** + * AssertJ {@link AbstractAssert Assertion} for code contributions originating from Spring Data Repository + * infrastructure AOT processing. + * + * @author Christoph Strobl + * @author John Blum + * @since 4.0 + */ +@SuppressWarnings("UnusedReturnValue") +public class CodeContributionAssert extends AbstractAssert { + + public CodeContributionAssert(GenerationContext contribution) { + super(contribution, CodeContributionAssert.class); + } + + public CodeContributionAssert contributesReflectionFor(Class... types) { + + for (Class type : types) { + assertThat(this.actual.getRuntimeHints()).describedAs("No reflection entry found for [%s]", type) + .matches(RuntimeHintsPredicates.reflection().onType(type)); + } + + return this; + } + + public CodeContributionAssert doesNotContributeReflectionFor(Class... types) { + + for (Class type : types) { + assertThat(this.actual.getRuntimeHints()).describedAs("Reflection entry found for [%s]", type) + .matches(RuntimeHintsPredicates.reflection().onType(type).negate()); + } + + return this; + } + + public CodeContributionAssert contributesJdkProxyFor(Class entryPoint) { + + assertThat(jdkProxiesFor(entryPoint).findFirst()).describedAs("No JDK proxy found for [%s]", entryPoint) + .isPresent(); + + return this; + } + + public CodeContributionAssert doesNotContributeJdkProxyFor(Class entryPoint) { + + assertThat(jdkProxiesFor(entryPoint).findFirst()) + .describedAs("Found JDK proxy matching [%s] though it should not be present", entryPoint).isNotPresent(); + + return this; + } + + private Stream jdkProxiesFor(Class entryPoint) { + + return this.actual.getRuntimeHints().proxies().jdkProxies().filter(jdkProxyHint -> jdkProxyHint + .getProxiedInterfaces().get(0).getCanonicalName().equals(entryPoint.getCanonicalName())); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarIntegrationTests.java index d140fbb6b..17fd3f749 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/config/CassandraAuditingRegistrarIntegrationTests.java @@ -25,6 +25,7 @@ import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler; import org.springframework.data.cassandra.core.convert.CassandraConverter; import org.springframework.data.cassandra.core.convert.MappingCassandraConverter; +import org.springframework.data.cassandra.core.mapping.CassandraMappingContext; /** * Integration tests for registering both, imperative and reactive auditing handlers. @@ -53,8 +54,13 @@ class CassandraAuditingRegistrarIntegrationTests { static class MyConfiguration { @Bean - CassandraConverter cassandraConverter() { - return new MappingCassandraConverter(); + CassandraConverter cassandraConverter(CassandraMappingContext context) { + return new MappingCassandraConverter(context); + } + + @Bean + CassandraMappingContext cassandraMappingContext() { + return new CassandraMappingContext(); } } diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CreateUserTypeIntegrationTests.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CreateUserTypeIntegrationTests.java index 839d3cbcd..b97404688 100644 --- a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CreateUserTypeIntegrationTests.java +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/core/mapping/CreateUserTypeIntegrationTests.java @@ -21,7 +21,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import java.util.Arrays; -import java.util.HashSet; import java.util.List; import org.junit.jupiter.api.Test; @@ -30,9 +29,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.annotation.Id; +import org.springframework.data.cassandra.CassandraManagedTypes; import org.springframework.data.cassandra.repository.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; import org.springframework.data.cassandra.repository.support.IntegrationTestConfig; -import org.springframework.data.convert.CustomConversions; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import com.datastax.oss.driver.api.core.CqlIdentifier; @@ -50,20 +49,10 @@ public class CreateUserTypeIntegrationTests extends AbstractSpringDataEmbeddedCa @Configuration public static class Config extends IntegrationTestConfig { + @Override @Bean - public CassandraMappingContext cassandraMapping() { - - CassandraMappingContext mappingContext = new CassandraMappingContext(); - - mappingContext.setInitialEntitySet(new HashSet<>(Arrays.asList(Car.class, Engine.class, Manufacturer.class))); - - CustomConversions customConversions = customConversions(); - - mappingContext.setCustomConversions(customConversions); - mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); - mappingContext.setUserTypeResolver(new SimpleUserTypeResolver(getRequiredSession())); - - return mappingContext; + public CassandraManagedTypes cassandraManagedTypes() { + return CassandraManagedTypes.fromIterable(Arrays.asList(Car.class, Engine.class, Manufacturer.class)); } } diff --git a/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrarUnitTests.java b/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrarUnitTests.java new file mode 100644 index 000000000..41a806cb4 --- /dev/null +++ b/spring-data-cassandra/src/test/kotlin/org/springframework/data/cassandra/aot/CassandraRuntimeHintsRegistrarUnitTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2022 the original author or 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 + * + * https://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 org.springframework.data.cassandra.aot; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.ClassNameGenerator; +import org.springframework.aot.generate.DefaultGenerationContext; +import org.springframework.aot.generate.InMemoryGeneratedFiles; +import org.springframework.data.cassandra.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.cassandra.core.mapping.event.BeforeSaveCallback; +import org.springframework.data.cassandra.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.cassandra.core.mapping.event.ReactiveBeforeSaveCallback; +import org.springframework.data.cassandra.repository.support.SimpleCassandraRepository; +import org.springframework.data.cassandra.repository.support.SimpleReactiveCassandraRepository; + +/** + * Unit tests for {@link CassandraRuntimeHintsRegistrar}. + * + * @author Mark Paluch + */ +class CassandraRuntimeHintsRegistrarUnitTests { + + @Test // GH-1280 + void shouldRegisterCassandraHints() { + + CassandraRuntimeHintsRegistrar registrar = new CassandraRuntimeHintsRegistrar(); + + DefaultGenerationContext context = new DefaultGenerationContext(new ClassNameGenerator(Object.class), + new InMemoryGeneratedFiles()); + registrar.registerHints(context.getRuntimeHints(), null); + + new CodeContributionAssert(context).contributesReflectionFor(SimpleCassandraRepository.class, + SimpleReactiveCassandraRepository.class); + new CodeContributionAssert(context).contributesReflectionFor(BeforeConvertCallback.class, BeforeSaveCallback.class, + ReactiveBeforeConvertCallback.class, ReactiveBeforeSaveCallback.class); + } +} diff --git a/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml b/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml index abc550bb0..046889915 100644 --- a/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml +++ b/spring-data-cassandra/src/test/resources/config/spring-data-cassandra-basic.xml @@ -17,7 +17,7 @@ - + diff --git a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/multiple-converters.xml b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/multiple-converters.xml index b92b4ce0b..5069c563e 100644 --- a/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/multiple-converters.xml +++ b/spring-data-cassandra/src/test/resources/org/springframework/data/cassandra/config/multiple-converters.xml @@ -13,7 +13,7 @@ - +