Skip to content

Commit 47a54c6

Browse files
committed
Add suport for JsonValue annotation on Enums and JsonValue/JsonCreator otherwise.
Closes #1617.
1 parent 7a05754 commit 47a54c6

19 files changed

+901
-64
lines changed

src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java

+24-5
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@
3939
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
4040
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
4141
import org.springframework.data.couchbase.core.convert.CouchbasePropertyValueConverterFactory;
42+
import org.springframework.data.couchbase.core.convert.IntegerToEnumConverterFactory;
4243
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
44+
import org.springframework.data.couchbase.core.convert.OtherConverters;
45+
import org.springframework.data.couchbase.core.convert.StringToEnumConverterFactory;
4346
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
4447
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
4548
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -60,7 +63,6 @@
6063
import org.springframework.util.ClassUtils;
6164
import org.springframework.util.StringUtils;
6265

63-
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature;
6466
import com.couchbase.client.core.encryption.CryptoManager;
6567
import com.couchbase.client.core.env.Authenticator;
6668
import com.couchbase.client.core.env.PasswordAuthenticator;
@@ -72,6 +74,7 @@
7274
import com.couchbase.client.java.json.JacksonTransformers;
7375
import com.couchbase.client.java.json.JsonValueModule;
7476
import com.couchbase.client.java.query.QueryScanConsistency;
77+
import com.fasterxml.jackson.databind.DeserializationFeature;
7578
import com.fasterxml.jackson.databind.ObjectMapper;
7679

7780
/**
@@ -280,7 +283,9 @@ public TranslationService couchbaseTranslationService() {
280283
jacksonTranslationService.setObjectMapper(getCouchbaseObjectMapper());
281284
jacksonTranslationService.afterPropertiesSet();
282285
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
283-
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
286+
JacksonTransformers.MAPPER.configure(
287+
com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
288+
false);
284289
return jacksonTranslationService;
285290
}
286291

@@ -306,20 +311,30 @@ private ObjectMapper getCouchbaseObjectMapper() {
306311
}
307312

308313
/**
309-
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
314+
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment and spring-data-couchbase
315+
* jacksonTranslationService and also some converters (EnumToObject, StringToEnum, IntegerToEnum)
310316
*
311317
* @return ObjectMapper
312318
*/
313319
public ObjectMapper couchbaseObjectMapper() {
314-
ObjectMapper om = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
315-
om.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
320+
ObjectMapper om = getObjectMapper();
321+
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
316322
om.registerModule(new JsonValueModule());
317323
if (getCryptoManager() != null) {
318324
om.registerModule(new EncryptionModule(getCryptoManager()));
319325
}
320326
return om;
321327
}
322328

329+
ObjectMapper objectMapper;
330+
331+
ObjectMapper getObjectMapper() {
332+
if (objectMapper == null) {
333+
objectMapper = new ObjectMapper();
334+
}
335+
return objectMapper;
336+
}
337+
323338
/**
324339
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
325340
* CallbackPreferringTransactionManagers do not play well with test-cases that rely
@@ -402,8 +417,12 @@ public CustomConversions customConversions(CryptoManager cryptoManager) {
402417
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
403418
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
404419
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
420+
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
405421
configurationAdapter.setPropertyValueConversions(valueConversions);
406422
configurationAdapter.registerConverters(newConverters);
423+
configurationAdapter.registerConverter(new OtherConverters.EnumToObject(getObjectMapper()));
424+
configurationAdapter.registerConverterFactory(new IntegerToEnumConverterFactory(getObjectMapper()));
425+
configurationAdapter.registerConverterFactory(new StringToEnumConverterFactory(getObjectMapper()));
407426
});
408427
return customConversions;
409428
}

src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java

+3-5
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,8 @@ public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, Converti
117117
return null;
118118
}
119119
if (processValueConverter && conversions.hasValueConverter(prop)) {
120-
CouchbaseDocument encrypted = (CouchbaseDocument) conversions.getPropertyValueConversions()
121-
.getValueConverter(prop)
122-
.write(value, new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
123-
return encrypted;
120+
return conversions.getPropertyValueConversions().getValueConverter(prop).write(value,
121+
new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
124122
}
125123
Class<?> targetClass = this.conversions.getCustomWriteTarget(value.getClass()).orElse(null);
126124

@@ -134,7 +132,7 @@ public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, Converti
134132
Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
135133
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
136134
TypeDescriptor.valueOf(it))) //
137-
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
135+
.orElse(value);
138136

139137
return result;
140138

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2012-2022 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+
17+
package org.springframework.data.couchbase.core.convert;
18+
19+
/**
20+
* PropertyValueConverter throws this when cannot convert the property. The caller should catch this and resort to other
21+
* means for creating the value.
22+
*
23+
* @author Michael Reiche
24+
*/
25+
public class ConverterHasNoConversion extends RuntimeException {}

src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java

-5
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,6 @@
4444
import org.springframework.data.mapping.model.SimpleTypeHolder;
4545
import org.springframework.util.Assert;
4646

47-
import com.couchbase.client.java.encryption.annotation.Encrypted;
48-
4947
/**
5048
* Value object to capture custom conversion.
5149
* <p>
@@ -112,9 +110,6 @@ public static CouchbaseCustomConversions create(Consumer<CouchbaseConverterConfi
112110

113111
@Override
114112
public boolean hasValueConverter(PersistentProperty<?> property) {
115-
if (property.findAnnotation(Encrypted.class) != null) {
116-
return true;
117-
}
118113
return super.hasValueConverter(property);
119114
}
120115

Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2022 the original author or authors
2+
* Copyright 2022 the original author or authors
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -13,73 +13,154 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16-
1716
package org.springframework.data.couchbase.core.convert;
1817

18+
import java.lang.annotation.Annotation;
1919
import java.lang.reflect.Constructor;
20-
import java.lang.reflect.InvocationTargetException;
21-
import java.util.HashMap;
20+
import java.lang.reflect.Method;
2221
import java.util.Map;
22+
import java.util.Optional;
23+
import java.util.concurrent.ConcurrentHashMap;
2324

25+
import org.springframework.beans.BeanUtils;
2426
import org.springframework.data.convert.PropertyValueConverter;
2527
import org.springframework.data.convert.PropertyValueConverterFactory;
2628
import org.springframework.data.convert.ValueConversionContext;
2729
import org.springframework.data.mapping.PersistentProperty;
2830

2931
import com.couchbase.client.core.encryption.CryptoManager;
3032
import com.couchbase.client.java.encryption.annotation.Encrypted;
33+
import com.fasterxml.jackson.annotation.JsonCreator;
34+
import com.fasterxml.jackson.annotation.JsonValue;
3135

3236
/**
33-
* Accept the Couchbase @Encrypted annotation in addition to @ValueConverter
37+
* Accept the Couchbase @Encrypted and @JsonValue annotations in addition to @ValueConverter annotation.<br>
38+
* There can only be one propertyValueConverter for a property. Although there maybe be multiple annotations,
39+
* getConverter(property) only returns one converter (a ChainedPropertyValueConverter might be useful). Note that
40+
* valueConversions.afterPropertiesSet() (see
41+
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}
42+
* encapsulates this in a CachingPropertyValueConverterFactory which caches by 'property'. Although
43+
* CachingPropertyValueConverterFactory does have the functionality to cache by a type, it only caches by the type
44+
* specified on an @ValueConverter annotation.To avoid having identical converter instances for each instance of a class
45+
* containing an @JsonValue annotation, converterCacheForType is used.
3446
*
3547
* @author Michael Reiche
3648
*/
3749
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
3850

39-
CryptoManager cryptoManager;
40-
Map<Class<? extends PropertyValueConverter<?, ?, ?>>, PropertyValueConverter<?, ?, ?>> converterCache = new HashMap<>();
51+
final CryptoManager cryptoManager;
52+
static protected final Map<Class<?>, Optional<PropertyValueConverter<?, ?, ?>>> converterCacheForType = new ConcurrentHashMap<>();
4153

4254
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager) {
4355
this.cryptoManager = cryptoManager;
4456
}
4557

58+
/**
59+
* @param property must not be {@literal null}.
60+
* @return
61+
* @param <DV> destination value
62+
* @param <SV> source value
63+
* @param <P> context
64+
*/
4665
@Override
4766
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
4867
PersistentProperty<?> property) {
68+
4969
PropertyValueConverter<DV, SV, P> valueConverter = PropertyValueConverterFactory.super.getConverter(property);
5070
if (valueConverter != null) {
5171
return valueConverter;
5272
}
53-
Encrypted encryptedAnn = property.findAnnotation(Encrypted.class);
54-
if (encryptedAnn != null) {
55-
Class cryptoConverterClass = CryptoConverter.class;
56-
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) cryptoConverterClass);
57-
} else {
73+
74+
// this will return the converter for the first annotation that requires a PropertyValueConverter like @Encrypted
75+
for (Annotation ann : property.getField().getAnnotations()) {
76+
Class<?> converterClass = converterFromFieldAnnotation(ann);
77+
if (converterClass != null) {
78+
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) converterClass, property);
79+
}
80+
}
81+
82+
if (property.getType().isEnum()) { // Enums have type-based converters for JsonValue/Creator. see OtherConverters.
5883
return null;
5984
}
85+
86+
// Maybe the type of the property has annotations that indicate a converter (like a method with a @JsonValue)
87+
return (PropertyValueConverter<DV, SV, P>) converterCacheForType
88+
.computeIfAbsent(property.getType(), p -> Optional.ofNullable((maybeTypePropertyConverter(property))))
89+
.orElse(null);
90+
}
91+
92+
/**
93+
* lookup the converter class from the annotation. Analogous to getting the converter class from the value() attribute
94+
* of the @ValueProperty annotation
95+
*
96+
* @param ann the annotation
97+
* @return the class of the converter
98+
*/
99+
private Class<?> converterFromFieldAnnotation(Annotation ann) {
100+
if (ann instanceof Encrypted) {
101+
return CryptoConverter.class;
102+
}
103+
return null;
104+
}
105+
106+
<DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> maybeTypePropertyConverter(
107+
PersistentProperty<?> property) {
108+
109+
Class<?> type = property.getType();
110+
111+
// find the annotated method to determine if a converter is required, and cache it.
112+
Method jsonValueMethod = null;
113+
for (Method m : type.getDeclaredMethods()) {
114+
JsonValue jsonValueAnn = m.getAnnotation(JsonValue.class);
115+
if (jsonValueAnn != null && jsonValueAnn.value()) {
116+
jsonValueMethod = m;
117+
jsonValueMethod.setAccessible(true);
118+
JsonValueConverter.valueMethodCache.put(type, jsonValueMethod);
119+
Constructor<?> jsonCreatorMethod = null;
120+
for (Constructor<?> c : type.getConstructors()) {
121+
JsonCreator jsonCreatorAnn = c.getAnnotation(JsonCreator.class);
122+
if (jsonCreatorAnn != null && !jsonCreatorAnn.mode().equals(JsonCreator.Mode.DISABLED)) {
123+
jsonCreatorMethod = c;
124+
jsonCreatorMethod.setAccessible(true);
125+
JsonValueConverter.creatorMethodCache.put(type, jsonCreatorMethod);
126+
break;
127+
}
128+
}
129+
Class jsonValueConverterClass = JsonValueConverter.class;
130+
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) jsonValueConverterClass, property);
131+
}
132+
}
133+
134+
return null; // we didn't find a property value converter to use
60135
}
61136

62137
@Override
63138
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
64139
Class<? extends PropertyValueConverter<DV, SV, P>> converterType) {
140+
return getConverter(converterType, null);
141+
}
65142

66-
PropertyValueConverter<?, ?, ?> converter = converterCache.get(converterType);
67-
if (converter != null) {
68-
return (PropertyValueConverter<DV, SV, P>) converter;
69-
}
143+
/**
144+
* @param converterType
145+
* @param property
146+
* @return
147+
* @param <DV>
148+
* @param <SV>
149+
* @param <P>
150+
*/
151+
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
152+
Class<? extends PropertyValueConverter<DV, SV, P>> converterType, PersistentProperty<?> property) {
70153

154+
// CryptoConverter takes a cryptoManager argument
71155
if (CryptoConverter.class.isAssignableFrom(converterType)) {
72-
converter = new CryptoConverter(cryptoManager);
73-
} else {
156+
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager);
157+
} else if (property != null) { // try constructor that takes PersistentProperty
74158
try {
75-
Constructor constructor = converterType.getConstructor();
76-
converter = (PropertyValueConverter<?, ?, ?>) constructor.newInstance();
77-
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
78-
throw new RuntimeException(e);
79-
}
159+
Constructor<?> constructor = converterType.getConstructor(PersistentProperty.class);
160+
return (PropertyValueConverter<DV, SV, P>) BeanUtils.instantiateClass(constructor, property);
161+
} catch (NoSuchMethodException e) {}
80162
}
81-
converterCache.put((Class<? extends PropertyValueConverter<DV, SV, P>>) converter.getClass(), converter);
82-
return (PropertyValueConverter<DV, SV, P>) converter;
83-
163+
// there is no constructor that takes a property, fall-back to no-args constructor
164+
return BeanUtils.instantiateClass(converterType);
84165
}
85166
}

src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
import com.couchbase.client.java.json.JsonValue;
4141

4242
/**
43-
* Encrypt/Decrypted properties annotated with
43+
* Encrypt/Decrypted properties annotated. This is registered in
44+
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
4445
*
4546
* @author Michael Reiche
4647
*/
@@ -68,7 +69,8 @@ public CouchbaseDocument write(Object value, ValueConversionContext<? extends Pe
6869
CouchbaseConversionContext ctx = (CouchbaseConversionContext) context;
6970
CouchbasePersistentProperty property = ctx.getProperty();
7071
byte[] plainText = coerceToBytesWrite(property, ctx.getAccessor(), ctx);
71-
Map<String, Object> encrypted = cryptoManager().encrypt(plainText, ctx.getProperty().findAnnotation(Encrypted.class).encrypter());
72+
Map<String, Object> encrypted = cryptoManager().encrypt(plainText,
73+
ctx.getProperty().findAnnotation(Encrypted.class).encrypter());
7274
return new CouchbaseDocument().setContent(encrypted);
7375
}
7476

0 commit comments

Comments
 (0)