Skip to content

Commit 07e9462

Browse files
committed
Add suport for JsonValue annotation on Enums.
Closes #1617.
1 parent 7a05754 commit 07e9462

File tree

8 files changed

+206
-24
lines changed

8 files changed

+206
-24
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

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

+25-10
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,28 @@
1818

1919
import java.lang.reflect.Constructor;
2020
import java.lang.reflect.InvocationTargetException;
21-
import java.util.HashMap;
22-
import java.util.Map;
21+
import java.lang.reflect.Method;
2322

2423
import org.springframework.data.convert.PropertyValueConverter;
2524
import org.springframework.data.convert.PropertyValueConverterFactory;
2625
import org.springframework.data.convert.ValueConversionContext;
26+
import org.springframework.data.couchbase.core.convert.translation.JsonValueConverter;
2727
import org.springframework.data.mapping.PersistentProperty;
2828

2929
import com.couchbase.client.core.encryption.CryptoManager;
3030
import com.couchbase.client.java.encryption.annotation.Encrypted;
31+
import com.fasterxml.jackson.annotation.JsonValue;
3132

3233
/**
33-
* Accept the Couchbase @Encrypted annotation in addition to @ValueConverter
34+
* Accept the Couchbase @Encrypted annotation in addition to @ValueConverter.<br>
35+
* Note that valueConversions.afterPropertiesSet() encapsulates this in a CachingPropertyValueConverterFactory making
36+
* caching in this class unnecessary.
3437
*
3538
* @author Michael Reiche
3639
*/
3740
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
3841

3942
CryptoManager cryptoManager;
40-
Map<Class<? extends PropertyValueConverter<?, ?, ?>>, PropertyValueConverter<?, ?, ?>> converterCache = new HashMap<>();
4143

4244
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager) {
4345
this.cryptoManager = cryptoManager;
@@ -50,35 +52,48 @@ public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV,
5052
if (valueConverter != null) {
5153
return valueConverter;
5254
}
55+
5356
Encrypted encryptedAnn = property.findAnnotation(Encrypted.class);
5457
if (encryptedAnn != null) {
5558
Class cryptoConverterClass = CryptoConverter.class;
5659
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) cryptoConverterClass);
57-
} else {
60+
}
61+
62+
Class<?> type = property.getActualType();
63+
if (!Enum.class.isAssignableFrom(type)) {
5864
return null;
5965
}
66+
67+
// for Enums only
68+
69+
for (Method m : type.getDeclaredMethods()) {
70+
if (m.getAnnotation(JsonValue.class) != null) {
71+
Class jsonValueConverterClass = JsonValueConverter.class;
72+
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) jsonValueConverterClass);
73+
}
74+
}
75+
76+
return null;
6077
}
6178

6279
@Override
6380
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
6481
Class<? extends PropertyValueConverter<DV, SV, P>> converterType) {
6582

66-
PropertyValueConverter<?, ?, ?> converter = converterCache.get(converterType);
67-
if (converter != null) {
68-
return (PropertyValueConverter<DV, SV, P>) converter;
69-
}
83+
PropertyValueConverter<?, ?, ?> converter = null;
7084

85+
// CryptoConverter takes a cryptoManager argument
7186
if (CryptoConverter.class.isAssignableFrom(converterType)) {
7287
converter = new CryptoConverter(cryptoManager);
7388
} else {
89+
// everything else, try the no-args constructor.
7490
try {
7591
Constructor constructor = converterType.getConstructor();
7692
converter = (PropertyValueConverter<?, ?, ?>) constructor.newInstance();
7793
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
7894
throw new RuntimeException(e);
7995
}
8096
}
81-
converterCache.put((Class<? extends PropertyValueConverter<DV, SV, P>>) converter.getClass(), converter);
8297
return (PropertyValueConverter<DV, SV, P>) converter;
8398

8499
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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.translation;
17+
18+
import java.lang.reflect.InvocationTargetException;
19+
import java.lang.reflect.Method;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
import org.springframework.data.convert.PropertyValueConverter;
24+
import org.springframework.data.convert.ValueConversionContext;
25+
import org.springframework.data.mapping.PersistentProperty;
26+
27+
import com.fasterxml.jackson.annotation.JsonValue;
28+
29+
/**
30+
* Enum properties annotated with JsonValue
31+
*
32+
* @author Michael Reiche
33+
*/
34+
public class JsonValueConverter
35+
implements PropertyValueConverter<Enum<?>, Object, ValueConversionContext<? extends PersistentProperty<?>>> {
36+
37+
Map<Class<? extends Enum>, Method> methodCache = new HashMap();
38+
Map<Class<? extends Enum>, Map<Object, Enum<?>>> enumConstantsCache = new HashMap();
39+
40+
@Override
41+
public Enum<?> read(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
42+
Class<Enum> type = (Class<Enum>) context.getProperty().getActualType();
43+
return getEnumConstant(type, value);
44+
}
45+
46+
@Override
47+
public Object write(Enum<?> value, ValueConversionContext<? extends PersistentProperty<?>> context) {
48+
try {
49+
Class<? extends Enum> type = value.getClass();
50+
return getJsonValueMethod(type).invoke(value);
51+
} catch (IllegalAccessException | InvocationTargetException e) {
52+
throw new RuntimeException(e);
53+
}
54+
}
55+
56+
private Enum<?> getEnumConstant(Class<Enum> type, Object value) {
57+
Map<Object, Enum<?>> enumConstants = enumConstantsCache.get(type);
58+
if (enumConstants == null) {
59+
enumConstants = new HashMap<>();
60+
enumConstantsCache.put(type, enumConstants);
61+
Method jsonValueMethod = getJsonValueMethod(type);
62+
for (Enum<?> e : type.getEnumConstants()) { // cache all the constants while we're here
63+
try {
64+
enumConstants.put(jsonValueMethod.invoke(e), e);
65+
} catch (IllegalAccessException | InvocationTargetException ex) {
66+
throw new RuntimeException(ex);
67+
}
68+
}
69+
}
70+
71+
Enum<?> theEnum = enumConstants.get(value);
72+
if (theEnum == null) {
73+
throw new RuntimeException("could not deserialize " + value + " to " + type);
74+
}
75+
return theEnum;
76+
}
77+
78+
private Method getJsonValueMethod(Class<? extends Enum> type) {
79+
Method jsonValueMethod = methodCache.get(type);
80+
if (jsonValueMethod != null) {
81+
return jsonValueMethod;
82+
}
83+
for (Method m : type.getDeclaredMethods()) {
84+
JsonValue jsonValueAnn = m.getAnnotation(JsonValue.class);
85+
if (jsonValueAnn != null && jsonValueAnn.value()) {
86+
jsonValueMethod = m;
87+
break;
88+
}
89+
}
90+
if (jsonValueMethod == null) { // Is this necessary? Useful?
91+
for (Method m : type.getMethods()) {
92+
JsonValue jsonValueAnn = m.getAnnotation(JsonValue.class);
93+
if (jsonValueAnn != null && jsonValueAnn.value()) {
94+
jsonValueMethod = m;
95+
break;
96+
}
97+
}
98+
}
99+
// if nothing was found, use toString()
100+
if (jsonValueMethod == null) {
101+
try {
102+
jsonValueMethod = type.getMethod("toString");
103+
} catch (NoSuchMethodException e) {
104+
throw new RuntimeException(e);
105+
}
106+
}
107+
jsonValueMethod.setAccessible(true);
108+
methodCache.put(type, jsonValueMethod);
109+
return jsonValueMethod;
110+
}
111+
112+
}

src/test/java/org/springframework/data/couchbase/domain/Airport.java

+10
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ public class Airport extends ComparableEntity {
4141

4242
String icao;
4343

44+
ETurbulenceCategory turbulence;
45+
4446
@Version Number version;
4547

4648
@CreatedBy private String createdBy;
@@ -99,4 +101,12 @@ public long getSize() {
99101
public void setSize(long size) {
100102
this.size = size;
101103
}
104+
105+
public void setTurbulence(ETurbulenceCategory turbulence) {
106+
this.turbulence = turbulence;
107+
}
108+
109+
public ETurbulenceCategory getTurbulence() {
110+
return turbulence;
111+
}
102112
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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.domain;
17+
18+
public enum ETurbulenceCategory {
19+
T10("10%"), T20("20%"), T30("30%");
20+
21+
private final String code;
22+
23+
ETurbulenceCategory(String code) {
24+
this.code = code;
25+
}
26+
27+
@com.fasterxml.jackson.annotation.JsonValue
28+
public String getCode() {
29+
return code;
30+
}
31+
}

src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java

+25-5
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
import org.springframework.data.couchbase.domain.AirportRepository;
7474
import org.springframework.data.couchbase.domain.AirportRepositoryScanConsistencyTest;
7575
import org.springframework.data.couchbase.domain.Course;
76+
import org.springframework.data.couchbase.domain.ETurbulenceCategory;
7677
import org.springframework.data.couchbase.domain.Iata;
7778
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
7879
import org.springframework.data.couchbase.domain.Person;
@@ -108,6 +109,7 @@
108109
import com.couchbase.client.core.error.IndexFailureException;
109110
import com.couchbase.client.java.env.ClusterEnvironment;
110111
import com.couchbase.client.java.json.JsonArray;
112+
import com.couchbase.client.java.json.JsonObject;
111113
import com.couchbase.client.java.kv.GetResult;
112114
import com.couchbase.client.java.kv.InsertOptions;
113115
import com.couchbase.client.java.kv.MutationState;
@@ -316,6 +318,24 @@ void findBySimplePropertyReturnType() {
316318
}
317319
}
318320

321+
@Test
322+
void findBySimplePropertyReturnTypeJsonValue() {
323+
Airport vie = null;
324+
try {
325+
vie = new Airport("airports::vie", "vie", "low6");
326+
vie.setTurbulence(ETurbulenceCategory.T10);
327+
vie = airportRepository.save(vie);
328+
Optional<Airport> airports = airportRepository.findById(vie.getId());
329+
System.out.println(airports.get());
330+
GetResult result = couchbaseTemplate.getCouchbaseClientFactory().getCluster().bucket(bucketName())
331+
.defaultCollection().get(vie.getId());
332+
JsonObject jo = JsonObject.fromJson(result.contentAsBytes());
333+
assertEquals(ETurbulenceCategory.T10.getCode(), jo.get("turbulence"));
334+
} finally {
335+
airportRepository.delete(vie);
336+
}
337+
}
338+
319339
@Test
320340
public void saveNotBoundedRequestPlus() {
321341
airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll();
@@ -867,11 +887,11 @@ void namedParameterList() throws Exception {
867887
try {
868888
userSubmission.setUsername("updateObject");
869889
userSubmissionRepository.save(userSubmission);
870-
Course[] courses = new Course[]{ new Course("1", "2", "3"), new Course("4","5","6")};
890+
Course[] courses = new Course[] { new Course("1", "2", "3"), new Course("4", "5", "6") };
871891
userSubmissionRepository.setNamedCourses(userSubmission.getId(), courses);
872892
Optional<UserSubmission> fetched = userSubmissionRepository.findById(userSubmission.getId());
873893
assertEquals(courses.length, fetched.get().getCourses().size());
874-
for(int i=0; i< courses.length; i++){
894+
for (int i = 0; i < courses.length; i++) {
875895
assertEquals(courses[i], fetched.get().getCourses().get(i));
876896
}
877897
} finally {
@@ -886,13 +906,13 @@ void orderedParameterList() throws Exception {
886906
try {
887907
userSubmission.setUsername("updateObject");
888908
userSubmissionRepository.save(userSubmission);
889-
Course[] courses = new Course[]{ new Course("1", "2", "3"), new Course("4","5","6")};
909+
Course[] courses = new Course[] { new Course("1", "2", "3"), new Course("4", "5", "6") };
890910
userSubmissionRepository.setOrderedCourses(userSubmission.getId(), courses);
891911
Optional<UserSubmission> fetched = userSubmissionRepository.findById(userSubmission.getId());
892912
assertEquals(courses.length, fetched.get().getCourses().size());
893-
for(int i=0; i< courses.length; i++){
913+
for (int i = 0; i < courses.length; i++) {
894914
assertEquals(courses[i], fetched.get().getCourses().get(i));
895-
}
915+
}
896916
} finally {
897917
userSubmissionRepository.deleteById(userSubmission.getId());
898918
}

0 commit comments

Comments
 (0)