Skip to content

Commit 75cb544

Browse files
committed
Add support for property @ValueConverter.
We now support property-specific value converters to apply value conversion for individual properties instead being limited to the type level. Closes #1449
1 parent 1a1301d commit 75cb544

File tree

16 files changed

+838
-43
lines changed

16 files changed

+838
-43
lines changed

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/config/AbstractCassandraConfiguration.java

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package org.springframework.data.cassandra.config;
1717

1818
import java.util.Arrays;
19-
import java.util.Collections;
2019
import java.util.Optional;
2120
import java.util.Set;
2221

@@ -73,8 +72,6 @@ public CassandraConverter cassandraConverter() {
7372

7473
CqlSession cqlSession = getRequiredSession();
7574

76-
77-
7875
MappingCassandraConverter converter = new MappingCassandraConverter(
7976
requireBeanOfType(CassandraMappingContext.class));
8077

@@ -118,8 +115,6 @@ public CassandraMappingContext cassandraMappingContext(CassandraManagedTypes cas
118115

119116
CqlSession cqlSession = getRequiredSession();
120117

121-
122-
123118
CassandraMappingContext mappingContext = new CassandraMappingContext(userTypeResolver(cqlSession),
124119
SimpleTupleTypeFactory.DEFAULT);
125120

@@ -181,7 +176,7 @@ public CassandraAdminTemplate cassandraTemplate() {
181176
*/
182177
@Bean
183178
public CassandraCustomConversions customConversions() {
184-
return new CassandraCustomConversions(Collections.emptyList());
179+
return CassandraCustomConversions.create(config -> {});
185180
}
186181

187182
/**
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.cassandra.core.convert;
17+
18+
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
19+
import org.springframework.data.convert.ValueConversionContext;
20+
import org.springframework.data.mapping.model.PropertyValueProvider;
21+
import org.springframework.data.mapping.model.SpELContext;
22+
import org.springframework.data.util.TypeInformation;
23+
import org.springframework.lang.Nullable;
24+
25+
import com.datastax.oss.driver.api.core.cql.Row;
26+
27+
/**
28+
* {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link CassandraConverter}.
29+
*
30+
* @author Mark Paluch
31+
* @since 4.2
32+
*/
33+
public class CassandraConversionContext implements ValueConversionContext<CassandraPersistentProperty> {
34+
35+
private final PropertyValueProvider<CassandraPersistentProperty> accessor;
36+
private final CassandraPersistentProperty persistentProperty;
37+
private final CassandraConverter cassandraConverter;
38+
39+
@Nullable private final SpELContext spELContext;
40+
41+
public CassandraConversionContext(PropertyValueProvider<CassandraPersistentProperty> accessor,
42+
CassandraPersistentProperty persistentProperty, CassandraConverter CassandraConverter) {
43+
this(accessor, persistentProperty, CassandraConverter, null);
44+
}
45+
46+
public CassandraConversionContext(PropertyValueProvider<CassandraPersistentProperty> accessor,
47+
CassandraPersistentProperty persistentProperty, CassandraConverter CassandraConverter,
48+
@Nullable SpELContext spELContext) {
49+
50+
this.accessor = accessor;
51+
this.persistentProperty = persistentProperty;
52+
this.cassandraConverter = CassandraConverter;
53+
this.spELContext = spELContext;
54+
}
55+
56+
@Override
57+
public CassandraPersistentProperty getProperty() {
58+
return persistentProperty;
59+
}
60+
61+
@Nullable
62+
public Object getValue(String propertyPath) {
63+
return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath));
64+
}
65+
66+
@Override
67+
@SuppressWarnings("unchecked")
68+
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
69+
return (T) cassandraConverter.convertToColumnType(value, target);
70+
}
71+
72+
@Override
73+
public <T> T read(@Nullable Object value, TypeInformation<T> target) {
74+
return value instanceof Row row ? cassandraConverter.read(target.getType(), row)
75+
: ValueConversionContext.super.read(value, target);
76+
}
77+
78+
@Nullable
79+
public SpELContext getSpELContext() {
80+
return spELContext;
81+
}
82+
}

spring-data-cassandra/src/main/java/org/springframework/data/cassandra/core/convert/CassandraCustomConversions.java

Lines changed: 214 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,29 @@
1616
package org.springframework.data.cassandra.core.convert;
1717

1818
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.Collection;
1921
import java.util.Collections;
2022
import java.util.Date;
2123
import java.util.List;
24+
import java.util.function.Consumer;
2225
import java.util.function.Predicate;
2326

2427
import org.springframework.core.convert.converter.Converter;
28+
import org.springframework.core.convert.converter.ConverterFactory;
29+
import org.springframework.core.convert.converter.GenericConverter;
2530
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
31+
import org.springframework.data.cassandra.core.mapping.CassandraPersistentProperty;
2632
import org.springframework.data.cassandra.core.mapping.CassandraSimpleTypeHolder;
33+
import org.springframework.data.convert.ConverterBuilder;
2734
import org.springframework.data.convert.Jsr310Converters;
35+
import org.springframework.data.convert.PropertyValueConversions;
36+
import org.springframework.data.convert.PropertyValueConverter;
37+
import org.springframework.data.convert.PropertyValueConverterFactory;
38+
import org.springframework.data.convert.PropertyValueConverterRegistrar;
39+
import org.springframework.data.convert.SimplePropertyValueConversions;
2840
import org.springframework.data.mapping.model.SimpleTypeHolder;
41+
import org.springframework.util.Assert;
2942

3043
/**
3144
* Value object to capture custom conversion. {@link CassandraCustomConversions} also act as factory for
@@ -59,7 +72,33 @@ public class CassandraCustomConversions extends org.springframework.data.convert
5972
* @param converters must not be {@literal null}.
6073
*/
6174
public CassandraCustomConversions(List<?> converters) {
62-
super(new CassandraConverterConfiguration(STORE_CONVERSIONS, converters));
75+
super(new CassandraConverterConfiguration(converters));
76+
}
77+
78+
/**
79+
* Create a new {@link CassandraCustomConversions} given {@link CassandraConverterConfigurationAdapter}.
80+
*
81+
* @param conversionConfiguration must not be {@literal null}.
82+
* @since 4.2
83+
*/
84+
protected CassandraCustomConversions(CassandraConverterConfigurationAdapter conversionConfiguration) {
85+
super(conversionConfiguration.createConverterConfiguration());
86+
}
87+
88+
/**
89+
* Functional style {@link org.springframework.data.convert.CustomConversions} creation giving users a convenient way
90+
* of configuring store specific capabilities by providing deferred hooks to what will be configured when creating the
91+
* {@link org.springframework.data.convert.CustomConversions#CustomConversions(ConverterConfiguration) instance}.
92+
*
93+
* @param configurer must not be {@literal null}.
94+
* @since 4.2
95+
*/
96+
public static CassandraCustomConversions create(Consumer<CassandraConverterConfigurationAdapter> configurer) {
97+
98+
CassandraConverterConfigurationAdapter adapter = new CassandraConverterConfigurationAdapter();
99+
configurer.accept(adapter);
100+
101+
return new CassandraCustomConversions(adapter);
63102
}
64103

65104
/**
@@ -68,14 +107,185 @@ public CassandraCustomConversions(List<?> converters) {
68107
*/
69108
static class CassandraConverterConfiguration extends ConverterConfiguration {
70109

71-
CassandraConverterConfiguration(StoreConversions storeConversions, List<?> userConverters) {
72-
super(storeConversions, userConverters, getConverterFilter());
110+
CassandraConverterConfiguration(List<?> converters) {
111+
super(STORE_CONVERSIONS, converters, getConverterFilter());
112+
113+
}
114+
115+
CassandraConverterConfiguration(List<?> userConverters, PropertyValueConversions propertyValueConversions) {
116+
super(STORE_CONVERSIONS, userConverters, getConverterFilter(), propertyValueConversions);
73117
}
74118

75119
static Predicate<ConvertiblePair> getConverterFilter() {
76120

77121
return convertiblePair -> !(Jsr310Converters.supports(convertiblePair.getSourceType())
78-
&& Date.class.isAssignableFrom(convertiblePair.getTargetType()));
122+
&& Date.class.isAssignableFrom(convertiblePair.getTargetType()));
123+
}
124+
}
125+
126+
/**
127+
* {@link CassandraConverterConfigurationAdapter} encapsulates creation of
128+
* {@link org.springframework.data.convert.CustomConversions.ConverterConfiguration} with Cassandra specifics.
129+
*
130+
* @author Mark Paluch
131+
* @since 4.2
132+
*/
133+
public static class CassandraConverterConfigurationAdapter {
134+
135+
private final List<Object> customConverters = new ArrayList<>();
136+
137+
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
138+
private PropertyValueConversions propertyValueConversions = internalValueConversion;
139+
140+
/**
141+
* Create a {@link CassandraConverterConfigurationAdapter} using the provided {@code converters} and our own codecs
142+
* for JSR-310 types.
143+
*
144+
* @param converters must not be {@literal null}.
145+
* @return
146+
*/
147+
public static CassandraConverterConfigurationAdapter from(List<?> converters) {
148+
149+
Assert.notNull(converters, "Converters must not be null");
150+
151+
CassandraConverterConfigurationAdapter adapter = new CassandraConverterConfigurationAdapter();
152+
adapter.registerConverters(converters);
153+
154+
return adapter;
155+
}
156+
157+
/**
158+
* Add a custom {@link Converter} implementation.
159+
*
160+
* @param converter must not be {@literal null}.
161+
* @return this.
162+
*/
163+
public CassandraConverterConfigurationAdapter registerConverter(Converter<?, ?> converter) {
164+
165+
Assert.notNull(converter, "Converter must not be null");
166+
167+
customConverters.add(converter);
168+
return this;
169+
}
170+
171+
/**
172+
* Add a custom {@link ConverterFactory} implementation.
173+
*
174+
* @param converterFactory must not be {@literal null}.
175+
* @return this.
176+
*/
177+
public CassandraConverterConfigurationAdapter registerConverterFactory(ConverterFactory<?, ?> converterFactory) {
178+
179+
Assert.notNull(converterFactory, "ConverterFactory must not be null");
180+
181+
customConverters.add(converterFactory);
182+
return this;
183+
}
184+
185+
/**
186+
* Add {@link Converter converters}, {@link ConverterFactory factories}, {@link ConverterBuilder.ConverterAware
187+
* converter-aware objects}, and {@link GenericConverter generic converters}.
188+
*
189+
* @param converters must not be {@literal null} nor contain {@literal null} values.
190+
* @return this.
191+
*/
192+
public CassandraConverterConfigurationAdapter registerConverters(Object... converters) {
193+
return registerConverters(Arrays.asList(converters));
194+
}
195+
196+
/**
197+
* Add {@link Converter converters}, {@link ConverterFactory factories}, {@link ConverterBuilder.ConverterAware
198+
* converter-aware objects}, and {@link GenericConverter generic converters}.
199+
*
200+
* @param converters must not be {@literal null} nor contain {@literal null} values.
201+
* @return this.
202+
*/
203+
public CassandraConverterConfigurationAdapter registerConverters(Collection<?> converters) {
204+
205+
Assert.notNull(converters, "Converters must not be null");
206+
Assert.noNullElements(converters, "Converters must not be null nor contain null values");
207+
208+
customConverters.addAll(converters);
209+
return this;
210+
}
211+
212+
/**
213+
* Add a custom/default {@link PropertyValueConverterFactory} implementation used to serve
214+
* {@link PropertyValueConverter}.
215+
*
216+
* @param converterFactory must not be {@literal null}.
217+
* @return this.
218+
*/
219+
public CassandraConverterConfigurationAdapter registerPropertyValueConverterFactory(
220+
PropertyValueConverterFactory converterFactory) {
221+
222+
Assert.state(valueConversions() instanceof SimplePropertyValueConversions,
223+
"Configured PropertyValueConversions does not allow setting custom ConverterRegistry");
224+
225+
((SimplePropertyValueConversions) valueConversions()).setConverterFactory(converterFactory);
226+
return this;
227+
}
228+
229+
/**
230+
* Gateway to register property specific converters.
231+
*
232+
* @param configurationAdapter must not be {@literal null}.
233+
* @return this.
234+
*/
235+
@SuppressWarnings({ "rawtypes", "unchecked" })
236+
public CassandraConverterConfigurationAdapter configurePropertyConversions(
237+
Consumer<PropertyValueConverterRegistrar<CassandraPersistentProperty>> configurationAdapter) {
238+
239+
Assert.state(valueConversions() instanceof SimplePropertyValueConversions,
240+
"Configured PropertyValueConversions does not allow setting custom ConverterRegistry");
241+
242+
PropertyValueConverterRegistrar propertyValueConverterRegistrar = new PropertyValueConverterRegistrar();
243+
configurationAdapter.accept(propertyValueConverterRegistrar);
244+
245+
((SimplePropertyValueConversions) valueConversions())
246+
.setValueConverterRegistry(propertyValueConverterRegistrar.buildRegistry());
247+
return this;
248+
}
249+
250+
/**
251+
* Optionally set the {@link PropertyValueConversions} to be applied during mapping.
252+
* <p>
253+
* Use this method if {@link #configurePropertyConversions(Consumer)} and
254+
* {@link #registerPropertyValueConverterFactory(PropertyValueConverterFactory)} are not sufficient.
255+
*
256+
* @param valueConversions must not be {@literal null}.
257+
* @return this.
258+
*/
259+
public CassandraConverterConfigurationAdapter withPropertyValueConversions(
260+
PropertyValueConversions valueConversions) {
261+
262+
Assert.notNull(valueConversions, "PropertyValueConversions must not be null");
263+
264+
this.propertyValueConversions = valueConversions;
265+
return this;
266+
}
267+
268+
PropertyValueConversions valueConversions() {
269+
270+
if (this.propertyValueConversions == null) {
271+
this.propertyValueConversions = internalValueConversion;
272+
}
273+
274+
return this.propertyValueConversions;
275+
}
276+
277+
CassandraConverterConfiguration createConverterConfiguration() {
278+
279+
if (hasDefaultPropertyValueConversions()
280+
&& propertyValueConversions instanceof SimplePropertyValueConversions svc) {
281+
svc.init();
282+
}
283+
284+
return new CassandraConverterConfiguration(this.customConverters, this.propertyValueConversions);
285+
}
286+
287+
private boolean hasDefaultPropertyValueConversions() {
288+
return propertyValueConversions == internalValueConversion;
79289
}
80290
}
81291
}

0 commit comments

Comments
 (0)