Skip to content

Commit 888ed12

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

11 files changed

+404
-46
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,7 @@ public CustomConversions customConversions(CryptoManager cryptoManager) {
402402
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
403403
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
404404
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
405+
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
405406
configurationAdapter.setPropertyValueConversions(valueConversions);
406407
configurationAdapter.registerConverters(newConverters);
407408
});

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

+2-4
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

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

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

+123-22
Original file line numberDiff line numberDiff line change
@@ -16,70 +16,171 @@
1616

1717
package org.springframework.data.couchbase.core.convert;
1818

19+
import java.lang.annotation.Annotation;
1920
import java.lang.reflect.Constructor;
2021
import java.lang.reflect.InvocationTargetException;
22+
import java.lang.reflect.Method;
2123
import java.util.HashMap;
2224
import java.util.Map;
25+
import java.util.Optional;
26+
import java.util.concurrent.ConcurrentHashMap;
2327

28+
import org.springframework.beans.BeanUtils;
2429
import org.springframework.data.convert.PropertyValueConverter;
2530
import org.springframework.data.convert.PropertyValueConverterFactory;
2631
import org.springframework.data.convert.ValueConversionContext;
2732
import org.springframework.data.mapping.PersistentProperty;
2833

2934
import com.couchbase.client.core.encryption.CryptoManager;
3035
import com.couchbase.client.java.encryption.annotation.Encrypted;
36+
import com.fasterxml.jackson.annotation.JsonCreator;
37+
import com.fasterxml.jackson.annotation.JsonValue;
3138

3239
/**
33-
* Accept the Couchbase @Encrypted annotation in addition to @ValueConverter
40+
* Accept the Couchbase @Encrypted and @JsonValue annotations in addition to @ValueConverter annotation.<br>
41+
* There can only be one propertyValueConverter for a property. Although there maybe be multiple annotations,
42+
* getConverter(property) only returns one converter (a ChainedPropertyValueConverter might be useful). Note that
43+
* valueConversions.afterPropertiesSet() encapsulates this in a CachingPropertyValueConverterFactory which caches by
44+
* 'property'. Although CachingPropertyValueConverterFactory does have the functionality to cache by a type, it only
45+
* caches by the type specified on an @ValueConverter annotation.To avoid having identical converter instances for each
46+
* instance of a class containing an @JsonValue annotation, converterCacheForType is used.
3447
*
3548
* @author Michael Reiche
3649
*/
3750
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
3851

3952
CryptoManager cryptoManager;
40-
Map<Class<? extends PropertyValueConverter<?, ?, ?>>, PropertyValueConverter<?, ?, ?>> converterCache = new HashMap<>();
53+
static protected Map<PersistentProperty<?>, Optional<PropertyValueConverter<?, ?, ?>>> converterCacheForType = new ConcurrentHashMap<>();
4154

4255
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager) {
4356
this.cryptoManager = cryptoManager;
4457
}
4558

59+
/**
60+
* @param property must not be {@literal null}.
61+
* @return
62+
* @param <DV> destination value
63+
* @param <SV> source value
64+
* @param <P> context
65+
*/
4666
@Override
4767
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
4868
PersistentProperty<?> property) {
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 {
58-
return null;
73+
74+
// this will return the first annotation that requires a PropertyValueConverter
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+
Optional<PropertyValueConverter<?, ?, ?>> cachedConverterForType = converterCacheForType
82+
.get(property.getActualType());
83+
if (cachedConverterForType != null) {
84+
return (PropertyValueConverter<DV, SV, P>) cachedConverterForType.get();
85+
}
86+
PropertyValueConverter<DV, SV, P> converterForType = maybeTypePropertyConverter(property);
87+
converterCacheForType.put(property, Optional.ofNullable(converterForType));
88+
return converterForType;
89+
}
90+
91+
/**
92+
* lookup the converter class from the annotation. Analogous to getting the converter class from the value() attribute
93+
* of the @ValueProperty annotation
94+
*
95+
* @param ann the annotation
96+
* @return the class of the converter
97+
*/
98+
private Class<?> converterFromFieldAnnotation(Annotation ann) {
99+
if (ann instanceof Encrypted) {
100+
return CryptoConverter.class;
101+
}
102+
return null;
103+
}
104+
105+
<DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> maybeTypePropertyConverter(
106+
PersistentProperty<?> property) {
107+
108+
Class<?> type = property.getActualType();
109+
110+
// since we need to find the annotated method to determine if a converter is required,
111+
// we may as well cache it. And also create a value -> enum-object map for Enums.
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+
// for Enums, we can compute the conversion on read()
120+
// otherwise, we rely on a constructor with an argument that is the output of the JsonValue method (inverse)
121+
if (Enum.class.isAssignableFrom(property.getActualType())) {
122+
Map<Object, Enum<?>> enumConstants = new HashMap<>();
123+
JsonValueConverter.enumConstantsCache.put((Class<Enum<?>>) type, enumConstants);
124+
Class<Enum<?>> enumType = (Class<Enum<?>>) type;
125+
for (Enum<?> e : enumType.getEnumConstants()) { // create a value -> enum-object map for Enums
126+
try {
127+
enumConstants.put(jsonValueMethod.invoke(e), e);
128+
} catch (IllegalAccessException | InvocationTargetException ex) {
129+
throw new RuntimeException(ex);
130+
}
131+
}
132+
}
133+
}
134+
}
135+
136+
Constructor<?> jsonCreatorMethod = null;
137+
for (Constructor<?> m : type.getConstructors()) {
138+
JsonCreator jsonCreatorAnn = m.getAnnotation(JsonCreator.class);
139+
if (jsonCreatorAnn != null && !jsonCreatorAnn.mode().equals(JsonCreator.Mode.DISABLED)) {
140+
jsonCreatorMethod = m;
141+
jsonCreatorMethod.setAccessible(true);
142+
JsonValueConverter.creatorMethodCache.put(type, jsonCreatorMethod);
143+
// hopefully the JsonValueCOnverter will know what to do with this.
144+
}
145+
}
146+
if (jsonValueMethod != null || jsonCreatorMethod != null) {
147+
Class jsonValueConverterClass = JsonValueConverter.class;
148+
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) jsonValueConverterClass, property);
59149
}
150+
return null; // we didn't find a property value converter to use
60151
}
61152

62153
@Override
63154
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
64155
Class<? extends PropertyValueConverter<DV, SV, P>> converterType) {
156+
return getConverter(converterType, null);
157+
}
65158

66-
PropertyValueConverter<?, ?, ?> converter = converterCache.get(converterType);
67-
if (converter != null) {
68-
return (PropertyValueConverter<DV, SV, P>) converter;
69-
}
159+
/**
160+
* @param converterType
161+
* @param property
162+
* @return
163+
* @param <DV>
164+
* @param <SV>
165+
* @param <P>
166+
*/
167+
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
168+
Class<? extends PropertyValueConverter<DV, SV, P>> converterType, PersistentProperty<?> property) {
70169

170+
// CryptoConverter takes a cryptoManager argument
71171
if (CryptoConverter.class.isAssignableFrom(converterType)) {
72-
converter = new CryptoConverter(cryptoManager);
73-
} else {
172+
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager);
173+
} else if (property != null) { // try constructor that takes PersistentProperty, fall-back to no-args constructor
74174
try {
75-
Constructor constructor = converterType.getConstructor();
76-
converter = (PropertyValueConverter<?, ?, ?>) constructor.newInstance();
77-
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
78-
throw new RuntimeException(e);
79-
}
175+
Constructor<?> constructor = converterType.getConstructor(PersistentProperty.class);
176+
try {
177+
return (PropertyValueConverter<DV, SV, P>) constructor.newInstance(property);
178+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
179+
throw new RuntimeException(e); // the constructor that takes a property failed
180+
}
181+
} catch (NoSuchMethodException e) {}
80182
}
81-
converterCache.put((Class<? extends PropertyValueConverter<DV, SV, P>>) converter.getClass(), converter);
82-
return (PropertyValueConverter<DV, SV, P>) converter;
83-
183+
// there is no constructor that takes a property, fall-back to no-args constructor
184+
return BeanUtils.instantiateClass(converterType);
84185
}
85186
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright 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+
package org.springframework.data.couchbase.core.convert;
17+
18+
import java.lang.reflect.Constructor;
19+
import java.lang.reflect.InvocationTargetException;
20+
import java.lang.reflect.Method;
21+
import java.util.Map;
22+
import java.util.concurrent.ConcurrentHashMap;
23+
24+
import org.springframework.data.convert.PropertyValueConverter;
25+
import org.springframework.data.convert.ValueConversionContext;
26+
import org.springframework.data.mapping.PersistentProperty;
27+
28+
/**
29+
* Enum properties annotated with JsonValue
30+
*
31+
* @author Michael Reiche
32+
*/
33+
public class JsonValueConverter
34+
implements PropertyValueConverter<Object, Object, ValueConversionContext<? extends PersistentProperty<?>>> {
35+
36+
static protected Map<Class<?>, Method> valueMethodCache = new ConcurrentHashMap<>();
37+
static protected Map<Class<?>, Constructor<?>> creatorMethodCache = new ConcurrentHashMap<>();
38+
static protected Map<Class<?>, Map<Object, Enum<?>>> enumConstantsCache = new ConcurrentHashMap<>();
39+
40+
@Override
41+
public Object read(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
42+
Class<?> type = context.getProperty().getActualType();
43+
44+
// if there was a @JsonCreator method, use it
45+
if (getJsonCreatorMethod(type) != null) {
46+
try {
47+
return getJsonCreatorMethod(type).newInstance(value);
48+
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
49+
throw new RuntimeException(e);
50+
}
51+
}
52+
53+
// for Enums, we can use the inverse mapping we made from the @JsonValue method
54+
if (Enum.class.isAssignableFrom(type)) {
55+
return getEnumConstant((Class<Enum<?>>) type, value);
56+
}
57+
58+
// Otherwise fall-through in MappingCouchbaseConverter.readValue()
59+
throw new ConverterHasNoConversion();
60+
}
61+
62+
@Override
63+
public Object write(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
64+
try {
65+
Class<?> type = value.getClass();
66+
return getJsonValueMethod(type).invoke(value);
67+
} catch (IllegalAccessException | InvocationTargetException e) {
68+
throw new RuntimeException(e);
69+
}
70+
}
71+
72+
private Enum<?> getEnumConstant(Class<Enum<?>> type, Object value) {
73+
Map<Object, Enum<?>> enumConstants = enumConstantsCache.get(type);
74+
return enumConstants.get(value);
75+
}
76+
77+
private Method getJsonValueMethod(Class<?> type) {
78+
return valueMethodCache.get(type);
79+
}
80+
81+
private Constructor<?> getJsonCreatorMethod(Class<?> type) {
82+
return creatorMethodCache.get(type);
83+
}
84+
85+
}

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

+17-9
Original file line numberDiff line numberDiff line change
@@ -772,7 +772,8 @@ public CouchbaseList writeCollectionInternal(final Collection<?> source, final C
772772
type, prop, accessor));
773773
} else {
774774
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
775-
writeInternalRoot(element, embeddedDoc, prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop);
775+
writeInternalRoot(element, embeddedDoc,
776+
prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop);
776777
target.put(embeddedDoc);
777778
}
778779

@@ -854,7 +855,7 @@ public Object getPotentiallyConvertedSimpleWrite(final Object value) {
854855

855856
/**
856857
* This does process PropertyValueConversions
857-
*
858+
*
858859
* @param value
859860
* @param accessor
860861
* @return
@@ -954,18 +955,25 @@ private <R> R readValue(Object value, TypeInformation type, Object parent) {
954955
public <R> R readValue(Object value, CouchbasePersistentProperty prop, Object parent, boolean noDecrypt) {
955956
Class<?> rawType = prop.getType();
956957
if (conversions.hasValueConverter(prop) && !noDecrypt) {
957-
return (R) conversions.getPropertyValueConversions().getValueConverter(prop).read(value,
958-
new CouchbaseConversionContext(prop, this, null));
959-
} else if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
958+
try {
959+
return (R) conversions.getPropertyValueConversions().getValueConverter(prop).read(value,
960+
new CouchbaseConversionContext(prop, this, null));
961+
} catch (ConverterHasNoConversion noConversion) {
962+
; // ignore
963+
}
964+
}
965+
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
960966
TypeInformation ti = ClassTypeInformation.from(value.getClass());
961967
return (R) conversionService.convert(value, ti.toTypeDescriptor(), new TypeDescriptor(prop.getField()));
962-
} else if (value instanceof CouchbaseDocument) {
968+
}
969+
if (value instanceof CouchbaseDocument) {
963970
return (R) read(prop.getTypeInformation(), (CouchbaseDocument) value, parent);
964-
} else if (value instanceof CouchbaseList) {
971+
}
972+
if (value instanceof CouchbaseList) {
965973
return (R) readCollection(prop.getTypeInformation(), (CouchbaseList) value, parent);
966-
} else {
967-
return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations
968974
}
975+
return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations
976+
969977
}
970978

971979
private ConvertingPropertyAccessor<Object> getPropertyAccessor(Object source) {

0 commit comments

Comments
 (0)