Skip to content

Commit 1b05530

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

11 files changed

+422
-49
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

+129-24
Original file line numberDiff line numberDiff line change
@@ -16,70 +16,175 @@
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;
23+
import java.util.Collections;
2124
import java.util.HashMap;
2225
import java.util.Map;
26+
import java.util.Optional;
27+
import java.util.concurrent.ConcurrentHashMap;
2328

29+
import org.springframework.beans.BeanUtils;
2430
import org.springframework.data.convert.PropertyValueConverter;
2531
import org.springframework.data.convert.PropertyValueConverterFactory;
2632
import org.springframework.data.convert.ValueConversionContext;
2733
import org.springframework.data.mapping.PersistentProperty;
2834

2935
import com.couchbase.client.core.encryption.CryptoManager;
3036
import com.couchbase.client.java.encryption.annotation.Encrypted;
37+
import com.fasterxml.jackson.annotation.JsonCreator;
38+
import com.fasterxml.jackson.annotation.JsonValue;
3139

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

39-
CryptoManager cryptoManager;
40-
Map<Class<? extends PropertyValueConverter<?, ?, ?>>, PropertyValueConverter<?, ?, ?>> converterCache = new HashMap<>();
41-
53+
final CryptoManager cryptoManager;
54+
static protected final Map<Class<?>, Optional<PropertyValueConverter<?, ?, ?>>> converterCacheForType = new ConcurrentHashMap<>();
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) {
71+
/* if using @ValueConverter, need process maybeTypePropertyConverter(property) for enum stuff
72+
Optional<PropertyValueConverter<?, ?, ?>> cachedConverterForType = converterCacheForType
73+
.get(property.getType());
74+
if (cachedConverterForType != null) {
75+
return valueConverter;
76+
}
77+
PropertyValueConverter<DV, SV, P> converterForType = maybeTypePropertyConverter(property);
78+
converterCacheForType.put(property, Optional.ofNullable(converterForType));
79+
*/
5180
return valueConverter;
5281
}
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;
82+
83+
// this will return the converter for the first annotation that requires a PropertyValueConverter
84+
for (Annotation ann : property.getField().getAnnotations()) {
85+
Class<?> converterClass = converterFromFieldAnnotation(ann);
86+
if (converterClass != null) {
87+
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) converterClass, property);
88+
}
89+
}
90+
return (PropertyValueConverter<DV, SV, P> )converterCacheForType
91+
.computeIfAbsent(property.getType(), p -> Optional.ofNullable((maybeTypePropertyConverter(property)))).orElse(null);
92+
}
93+
94+
/**
95+
* lookup the converter class from the annotation. Analogous to getting the converter class from the value() attribute
96+
* of the @ValueProperty annotation
97+
*
98+
* @param ann the annotation
99+
* @return the class of the converter
100+
*/
101+
private Class<?> converterFromFieldAnnotation(Annotation ann) {
102+
if (ann instanceof Encrypted) {
103+
return CryptoConverter.class;
59104
}
105+
return null;
106+
}
107+
108+
<DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> maybeTypePropertyConverter(
109+
PersistentProperty<?> property) {
110+
111+
Class<?> type = property.getType();
112+
113+
// since we need to find the annotated method to determine if a converter is required,
114+
// we may as well cache it. And also create a value -> enum-object map for Enums.
115+
Method jsonValueMethod = null;
116+
for (Method m : type.getDeclaredMethods()) {
117+
JsonValue jsonValueAnn = m.getAnnotation(JsonValue.class);
118+
if (jsonValueAnn != null && jsonValueAnn.value()) {
119+
jsonValueMethod = m;
120+
jsonValueMethod.setAccessible(true);
121+
JsonValueConverter.valueMethodCache.put(type, jsonValueMethod);
122+
// for Enums, we can compute the conversion on read()
123+
// otherwise, we rely on a constructor with an argument that is the output of the JsonValue method (inverse)
124+
if (type.isEnum()) {
125+
Map<Object, Enum<?>> enumConstants = new HashMap<>();
126+
Class<Enum<?>> enumType = (Class<Enum<?>>) type;
127+
for (Enum<?> e : enumType.getEnumConstants()) { // create a value -> enum-object map for Enums
128+
try {
129+
enumConstants.put(Optional.ofNullable(jsonValueMethod.invoke(e)), e);
130+
} catch (IllegalAccessException | InvocationTargetException ex) {
131+
throw new RuntimeException(ex);
132+
}
133+
}
134+
enumConstants = Collections.unmodifiableMap(enumConstants);
135+
JsonValueConverter.enumConstantsCache.put(type, enumConstants);
136+
}
137+
}
138+
}
139+
140+
Constructor<?> jsonCreatorMethod = null;
141+
for (Constructor<?> m : type.getConstructors()) {
142+
JsonCreator jsonCreatorAnn = m.getAnnotation(JsonCreator.class);
143+
if (jsonCreatorAnn != null && !jsonCreatorAnn.mode().equals(JsonCreator.Mode.DISABLED)) {
144+
jsonCreatorMethod = m;
145+
jsonCreatorMethod.setAccessible(true);
146+
JsonValueConverter.creatorMethodCache.put(type, jsonCreatorMethod);
147+
// hopefully the jsonValueConverter.read() will know what to do with this.
148+
}
149+
}
150+
if (jsonValueMethod != null || jsonCreatorMethod != null) {
151+
Class jsonValueConverterClass = JsonValueConverter.class;
152+
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) jsonValueConverterClass, property);
153+
}
154+
return null; // we didn't find a property value converter to use
60155
}
61156

62157
@Override
63158
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
64159
Class<? extends PropertyValueConverter<DV, SV, P>> converterType) {
160+
return getConverter(converterType, null);
161+
}
65162

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

174+
// CryptoConverter takes a cryptoManager argument
71175
if (CryptoConverter.class.isAssignableFrom(converterType)) {
72-
converter = new CryptoConverter(cryptoManager);
73-
} else {
176+
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager);
177+
} else if (property != null) { // try constructor that takes PersistentProperty, fall-back to no-args constructor
74178
try {
75-
Constructor constructor = converterType.getConstructor();
76-
converter = (PropertyValueConverter<?, ?, ?>) constructor.newInstance();
77-
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
78-
throw new RuntimeException(e);
79-
}
179+
Constructor<?> constructor = converterType.getConstructor(PersistentProperty.class);
180+
try {
181+
return (PropertyValueConverter<DV, SV, P>) constructor.newInstance(property);
182+
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
183+
throw new RuntimeException(e); // the constructor that takes a property failed
184+
}
185+
} catch (NoSuchMethodException e) {}
80186
}
81-
converterCache.put((Class<? extends PropertyValueConverter<DV, SV, P>>) converter.getClass(), converter);
82-
return (PropertyValueConverter<DV, SV, P>) converter;
83-
187+
// there is no constructor that takes a property, fall-back to no-args constructor
188+
return BeanUtils.instantiateClass(converterType);
84189
}
85190
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 final Map<Class<?>, Method> valueMethodCache = new ConcurrentHashMap<>();
37+
static protected final Map<Class<?>, Constructor<?>> creatorMethodCache = new ConcurrentHashMap<>();
38+
static protected final Map<Class<?>, Map<Object, Enum<?>>> enumConstantsCache = new ConcurrentHashMap<>();
39+
static private final ConverterHasNoConversion CONVERTER_HAS_NO_CONVERSION = new ConverterHasNoConversion();
40+
41+
@Override
42+
public Object read(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
43+
Class<?> type = context.getProperty().getActualType();
44+
45+
// if there was a @JsonCreator method, use it
46+
if (getJsonCreatorMethod(type) != null) {
47+
try {
48+
return getJsonCreatorMethod(type).newInstance(value);
49+
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
50+
throw new RuntimeException(e);
51+
}
52+
}
53+
54+
// for Enums, we can use the inverse mapping we made from the @JsonValue method
55+
if (type.isEnum()) {
56+
return getEnumConstant((Class<Enum<?>>) type, value);
57+
}
58+
59+
// Otherwise fall-through in MappingCouchbaseConverter.readValue()
60+
throw CONVERTER_HAS_NO_CONVERSION;
61+
}
62+
63+
@Override
64+
public Object write(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
65+
try {
66+
Class<?> type = value.getClass();
67+
return getJsonValueMethod(type).invoke(value);
68+
} catch (IllegalAccessException | InvocationTargetException e) {
69+
throw new RuntimeException(e);
70+
}
71+
}
72+
73+
private Enum<?> getEnumConstant(Class<Enum<?>> type, Object value) {
74+
Map<Object, Enum<?>> enumConstants = enumConstantsCache.get(type);
75+
return enumConstants.get(value);
76+
}
77+
78+
private Method getJsonValueMethod(Class<?> type) {
79+
return valueMethodCache.get(type);
80+
}
81+
82+
private Constructor<?> getJsonCreatorMethod(Class<?> type) {
83+
return creatorMethodCache.get(type);
84+
}
85+
86+
}

0 commit comments

Comments
 (0)