Skip to content

Commit 799b7db

Browse files
authored
Add suport for JsonValue annotation on Enums and JsonValue/JsonCreator otherwise. (#1618)
Closes #1617.
1 parent 7a05754 commit 799b7db

24 files changed

+1126
-85
lines changed

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

+50-17
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@
1818

1919
import static com.couchbase.client.java.ClusterOptions.clusterOptions;
2020

21+
import java.lang.annotation.Annotation;
2122
import java.util.ArrayList;
23+
import java.util.HashMap;
2224
import java.util.HashSet;
2325
import java.util.List;
26+
import java.util.Map;
2427
import java.util.Set;
2528

29+
import com.couchbase.client.java.encryption.annotation.Encrypted;
30+
import com.fasterxml.jackson.annotation.JsonValue;
2631
import org.springframework.beans.factory.config.BeanDefinition;
2732
import org.springframework.context.annotation.Bean;
2833
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
@@ -37,9 +42,15 @@
3742
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
3843
import org.springframework.data.couchbase.core.CouchbaseTemplate;
3944
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
45+
import org.springframework.data.couchbase.core.convert.BooleanToEnumConverterFactory;
4046
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
4147
import org.springframework.data.couchbase.core.convert.CouchbasePropertyValueConverterFactory;
48+
import org.springframework.data.couchbase.core.convert.CryptoConverter;
49+
import org.springframework.data.couchbase.core.convert.IntegerToEnumConverterFactory;
50+
import org.springframework.data.couchbase.core.convert.JsonValueConverter;
4251
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
52+
import org.springframework.data.couchbase.core.convert.OtherConverters;
53+
import org.springframework.data.couchbase.core.convert.StringToEnumConverterFactory;
4354
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
4455
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
4556
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -60,7 +71,6 @@
6071
import org.springframework.util.ClassUtils;
6172
import org.springframework.util.StringUtils;
6273

63-
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature;
6474
import com.couchbase.client.core.encryption.CryptoManager;
6575
import com.couchbase.client.core.env.Authenticator;
6676
import com.couchbase.client.core.env.PasswordAuthenticator;
@@ -72,6 +82,7 @@
7282
import com.couchbase.client.java.json.JacksonTransformers;
7383
import com.couchbase.client.java.json.JsonValueModule;
7484
import com.couchbase.client.java.query.QueryScanConsistency;
85+
import com.fasterxml.jackson.databind.DeserializationFeature;
7586
import com.fasterxml.jackson.databind.ObjectMapper;
7687

7788
/**
@@ -87,8 +98,8 @@
8798
@Configuration
8899
public abstract class AbstractCouchbaseConfiguration {
89100

90-
ObjectMapper mapper;
91-
CryptoManager cryptoManager = null;
101+
volatile ObjectMapper objectMapper;
102+
volatile CryptoManager cryptoManager = null;
92103

93104
/**
94105
* The connection string which allows the SDK to connect to the cluster.
@@ -157,7 +168,7 @@ public ClusterEnvironment couchbaseClusterEnvironment() {
157168
if (!nonShadowedJacksonPresent()) {
158169
throw new CouchbaseException("non-shadowed Jackson not present");
159170
}
160-
builder.jsonSerializer(JacksonJsonSerializer.create(getCouchbaseObjectMapper()));
171+
builder.jsonSerializer(JacksonJsonSerializer.create(getObjectMapper()));
161172
builder.cryptoManager(getCryptoManager());
162173
configureEnvironment(builder);
163174
return builder.build();
@@ -277,10 +288,12 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte
277288
@Bean
278289
public TranslationService couchbaseTranslationService() {
279290
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
280-
jacksonTranslationService.setObjectMapper(getCouchbaseObjectMapper());
291+
jacksonTranslationService.setObjectMapper(getObjectMapper());
281292
jacksonTranslationService.afterPropertiesSet();
282293
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
283-
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
294+
JacksonTransformers.MAPPER.configure(
295+
com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
296+
false);
284297
return jacksonTranslationService;
285298
}
286299

@@ -298,21 +311,26 @@ public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customC
298311
return mappingContext;
299312
}
300313

301-
private ObjectMapper getCouchbaseObjectMapper() {
302-
if (mapper != null) {
303-
return mapper;
314+
final public ObjectMapper getObjectMapper() {
315+
if(objectMapper == null) {
316+
synchronized (this) {
317+
if (objectMapper == null) {
318+
objectMapper = couchbaseObjectMapper();
319+
}
320+
}
304321
}
305-
return mapper = couchbaseObjectMapper();
322+
return objectMapper;
306323
}
307324

308325
/**
309-
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
326+
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment and spring-data-couchbase
327+
* jacksonTranslationService and also some converters (EnumToObject, StringToEnum, IntegerToEnum)
310328
*
311329
* @return ObjectMapper
312330
*/
313-
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);
331+
protected ObjectMapper couchbaseObjectMapper() {
332+
ObjectMapper om = new ObjectMapper();
333+
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
316334
om.registerModule(new JsonValueModule());
317335
if (getCryptoManager() != null) {
318336
om.registerModule(new EncryptionModule(getCryptoManager()));
@@ -400,20 +418,35 @@ public CustomConversions customConversions(CryptoManager cryptoManager) {
400418
List<GenericConverter> newConverters = new ArrayList();
401419
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
402420
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
403-
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
421+
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap()));
404422
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
423+
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
405424
configurationAdapter.setPropertyValueConversions(valueConversions);
406425
configurationAdapter.registerConverters(newConverters);
426+
configurationAdapter.registerConverter(new OtherConverters.EnumToObject(getObjectMapper()));
427+
configurationAdapter.registerConverterFactory(new IntegerToEnumConverterFactory(getObjectMapper()));
428+
configurationAdapter.registerConverterFactory(new StringToEnumConverterFactory(getObjectMapper()));
429+
configurationAdapter.registerConverterFactory(new BooleanToEnumConverterFactory(getObjectMapper()));
407430
});
408431
return customConversions;
409432
}
410433

434+
Map<Class<? extends Annotation>,Class<?>> annotationToConverterMap(){
435+
Map<Class<? extends Annotation>,Class<?>> map= new HashMap();
436+
map.put(Encrypted.class, CryptoConverter.class);
437+
map.put(JsonValue.class, JsonValueConverter.class);
438+
return map;
439+
}
411440
/**
412441
* cryptoManager can be null, so it cannot be a bean and then used as an arg for bean methods
413442
*/
414443
private CryptoManager getCryptoManager() {
415-
if (cryptoManager == null) {
416-
cryptoManager = cryptoManager();
444+
if(cryptoManager == null) {
445+
synchronized (this) {
446+
if (cryptoManager == null) {
447+
cryptoManager = cryptoManager();
448+
}
449+
}
417450
}
418451
return cryptoManager;
419452
}

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

+6-6
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,9 @@ 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);
136+
// superseded by Enum converters
137+
// .orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
138138

139139
return result;
140140

@@ -160,7 +160,7 @@ public Object convertForWriteIfNeeded(Object inValue) {
160160
Class<?> elementType = value.getClass();
161161

162162
if (elementType == null || conversions.isSimpleType(elementType)) {
163-
value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
163+
// superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
164164
} else if (value instanceof Collection || elementType.isArray()) {
165165
TypeInformation<?> type = ClassTypeInformation.from(value.getClass());
166166
value = ((MappingCouchbaseConverter) this).writeCollectionInternal(MappingCouchbaseConverter.asCollection(value),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.springframework.data.couchbase.core.convert;
2+
/*
3+
* Copyright 2022 the original author or authors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import java.io.IOException;
19+
20+
import org.springframework.core.convert.converter.Converter;
21+
import org.springframework.core.convert.converter.ConverterFactory;
22+
import org.springframework.data.convert.ReadingConverter;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
25+
26+
import com.couchbase.client.core.encryption.CryptoManager;
27+
import com.fasterxml.jackson.databind.ObjectMapper;
28+
29+
/**
30+
* Reading Converter factory for Enums. This differs from the one provided in org.springframework.core.convert.support
31+
* by getting the result from the jackson objectmapper (which will process @JsonValue annotations) This is registered in
32+
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
33+
*
34+
* @author Michael Reiche
35+
*/
36+
@ReadingConverter
37+
public class BooleanToEnumConverterFactory implements ConverterFactory<Boolean, Enum> {
38+
39+
private final ObjectMapper objectMapper;
40+
41+
public BooleanToEnumConverterFactory(ObjectMapper objectMapper) {
42+
this.objectMapper = objectMapper;
43+
}
44+
45+
@Override
46+
public <T extends Enum> Converter<Boolean, T> getConverter(Class<T> targetType) {
47+
return new BooleanToEnum(getEnumType(targetType), objectMapper);
48+
}
49+
50+
public static Class<?> getEnumType(Class<?> targetType) {
51+
Class<?> enumType = targetType;
52+
while (enumType != null && !enumType.isEnum()) {
53+
enumType = enumType.getSuperclass();
54+
}
55+
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
56+
return enumType;
57+
}
58+
59+
private static class BooleanToEnum<T extends Enum> implements Converter<Boolean, T> {
60+
61+
private final Class<T> enumType;
62+
private final ObjectMapper objectMapper;
63+
64+
BooleanToEnum(Class<T> enumType, ObjectMapper objectMapper) {
65+
this.enumType = enumType;
66+
this.objectMapper = objectMapper;
67+
}
68+
69+
@Override
70+
@Nullable
71+
public T convert(Boolean source) {
72+
if (source == null) {
73+
return null;
74+
}
75+
try {
76+
return objectMapper.readValue("\"" + source + "\"", enumType);
77+
} catch (IOException e) {
78+
throw new RuntimeException(e);
79+
}
80+
81+
}
82+
}
83+
}
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

0 commit comments

Comments
 (0)