Skip to content

Commit 0030214

Browse files
committed
Fix serialiation/deserialization for field level encryption. (#1622)
Closes #1621.
1 parent 761ed0d commit 0030214

File tree

10 files changed

+85
-68
lines changed

10 files changed

+85
-68
lines changed

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,6 @@
2626
import java.util.Map;
2727
import java.util.Set;
2828

29-
import com.couchbase.client.java.encryption.annotation.Encrypted;
30-
import com.fasterxml.jackson.annotation.JsonValue;
3129
import org.springframework.beans.factory.config.BeanDefinition;
3230
import org.springframework.context.annotation.Bean;
3331
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
@@ -77,11 +75,13 @@
7775
import com.couchbase.client.core.error.CouchbaseException;
7876
import com.couchbase.client.java.Cluster;
7977
import com.couchbase.client.java.codec.JacksonJsonSerializer;
78+
import com.couchbase.client.java.encryption.annotation.Encrypted;
8079
import com.couchbase.client.java.encryption.databind.jackson.EncryptionModule;
8180
import com.couchbase.client.java.env.ClusterEnvironment;
8281
import com.couchbase.client.java.json.JacksonTransformers;
8382
import com.couchbase.client.java.json.JsonValueModule;
8483
import com.couchbase.client.java.query.QueryScanConsistency;
84+
import com.fasterxml.jackson.annotation.JsonValue;
8585
import com.fasterxml.jackson.databind.DeserializationFeature;
8686
import com.fasterxml.jackson.databind.ObjectMapper;
8787

@@ -312,7 +312,7 @@ public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customC
312312
}
313313

314314
final public ObjectMapper getObjectMapper() {
315-
if(objectMapper == null) {
315+
if (objectMapper == null) {
316316
synchronized (this) {
317317
if (objectMapper == null) {
318318
objectMapper = couchbaseObjectMapper();
@@ -403,7 +403,7 @@ protected boolean autoIndexCreation() {
403403
*/
404404
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
405405
public CustomConversions customConversions() {
406-
return customConversions(getCryptoManager());
406+
return customConversions(getCryptoManager(), getObjectMapper());
407407
}
408408

409409
/**
@@ -414,11 +414,12 @@ public CustomConversions customConversions() {
414414
* @param cryptoManager
415415
* @return must not be {@literal null}.
416416
*/
417-
public CustomConversions customConversions(CryptoManager cryptoManager) {
417+
public CustomConversions customConversions(CryptoManager cryptoManager, ObjectMapper objectMapper) {
418418
List<GenericConverter> newConverters = new ArrayList();
419419
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
420420
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
421-
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap()));
421+
valueConversions.setConverterFactory(
422+
new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap(), objectMapper));
422423
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
423424
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
424425
configurationAdapter.setPropertyValueConversions(valueConversions);
@@ -431,17 +432,18 @@ public CustomConversions customConversions(CryptoManager cryptoManager) {
431432
return customConversions;
432433
}
433434

434-
Map<Class<? extends Annotation>,Class<?>> annotationToConverterMap(){
435-
Map<Class<? extends Annotation>,Class<?>> map= new HashMap();
435+
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap() {
436+
Map<Class<? extends Annotation>, Class<?>> map = new HashMap();
436437
map.put(Encrypted.class, CryptoConverter.class);
437438
map.put(JsonValue.class, JsonValueConverter.class);
438439
return map;
439440
}
441+
440442
/**
441443
* cryptoManager can be null, so it cannot be a bean and then used as an arg for bean methods
442444
*/
443445
private CryptoManager getCryptoManager() {
444-
if(cryptoManager == null) {
446+
if (cryptoManager == null) {
445447
synchronized (this) {
446448
if (cryptoManager == null) {
447449
cryptoManager = cryptoManager();

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,16 @@ public Object convertForWriteIfNeeded(Object inValue) {
160160
Class<?> elementType = value.getClass();
161161

162162
if (elementType == null || conversions.isSimpleType(elementType)) {
163-
// superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
163+
// superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() :
164+
// value;
164165
} else if (value instanceof Collection || elementType.isArray()) {
165166
TypeInformation<?> type = ClassTypeInformation.from(value.getClass());
166167
value = ((MappingCouchbaseConverter) this).writeCollectionInternal(MappingCouchbaseConverter.asCollection(value),
167168
new CouchbaseList(conversions.getSimpleTypeHolder()), type, null, null);
168169
} else {
169170
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
170171
TypeInformation<?> type = ClassTypeInformation.from(value.getClass());
171-
((MappingCouchbaseConverter) this).writeInternalRoot(value, embeddedDoc, type, false, null);
172+
((MappingCouchbaseConverter) this).writeInternalRoot(value, embeddedDoc, type, false, null, true);
172173
value = embeddedDoc;
173174
}
174175
return value;

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import com.couchbase.client.core.encryption.CryptoManager;
3232
import com.fasterxml.jackson.annotation.JsonCreator;
3333
import com.fasterxml.jackson.annotation.JsonValue;
34-
import org.springframework.util.Assert;
34+
import com.fasterxml.jackson.databind.ObjectMapper;
3535

3636
/**
3737
* Accept the Couchbase @Encrypted and @JsonValue annotations in addition to @ValueConverter annotation.<br>
@@ -48,14 +48,16 @@
4848
*/
4949
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
5050

51-
final CryptoManager cryptoManager;
52-
final Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap;
51+
private final CryptoManager cryptoManager;
52+
private final ObjectMapper objectMapper;
53+
private final Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap;
5354
static protected final Map<Class<?>, Optional<PropertyValueConverter<?, ?, ?>>> converterCacheForType = new ConcurrentHashMap<>();
5455

5556
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager,
56-
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap) {
57+
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap, ObjectMapper objectMapper) {
5758
this.cryptoManager = cryptoManager;
5859
this.annotationToConverterMap = annotationToConverterMap;
60+
this.objectMapper = objectMapper;
5961
}
6062

6163
/**
@@ -155,7 +157,7 @@ public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV,
155157

156158
// CryptoConverter takes a cryptoManager argument
157159
if (CryptoConverter.class.isAssignableFrom(converterType)) {
158-
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager);
160+
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager, objectMapper);
159161
} else if (property != null) { // try constructor that takes PersistentProperty
160162
try {
161163
Constructor<?> constructor = converterType.getConstructor(PersistentProperty.class);

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

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,12 @@
2020
import java.util.Map;
2121
import java.util.Optional;
2222

23-
import com.fasterxml.jackson.databind.ObjectMapper;
2423
import org.springframework.core.convert.ConversionFailedException;
2524
import org.springframework.core.convert.ConversionService;
2625
import org.springframework.core.convert.ConverterNotFoundException;
2726
import org.springframework.data.convert.CustomConversions;
2827
import org.springframework.data.convert.PropertyValueConverter;
2928
import org.springframework.data.convert.ValueConversionContext;
30-
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
31-
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
3229
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
3330
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
3431
import org.springframework.data.mapping.PersistentProperty;
@@ -41,20 +38,24 @@
4138
import com.couchbase.client.java.json.JsonArray;
4239
import com.couchbase.client.java.json.JsonObject;
4340
import com.couchbase.client.java.json.JsonValue;
41+
import com.fasterxml.jackson.core.JsonProcessingException;
42+
import com.fasterxml.jackson.databind.ObjectMapper;
4443

4544
/**
4645
* Encrypt/Decrypted properties annotated. This is registered in
47-
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
46+
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager, ObjectMapper)}.
4847
*
4948
* @author Michael Reiche
5049
*/
5150
public class CryptoConverter implements
5251
PropertyValueConverter<Object, CouchbaseDocument, ValueConversionContext<? extends PersistentProperty<?>>> {
5352

54-
CryptoManager cryptoManager;
53+
private final CryptoManager cryptoManager;
54+
private final ObjectMapper objectMapper;
5555

56-
public CryptoConverter(CryptoManager cryptoManager) {
56+
public CryptoConverter(CryptoManager cryptoManager, ObjectMapper objectMapper) {
5757
this.cryptoManager = cryptoManager;
58+
this.objectMapper = objectMapper;
5859
}
5960

6061
@Override
@@ -81,22 +82,12 @@ private Object coerceToValueRead(byte[] decrypted, CouchbaseConversionContext co
8182
CouchbasePersistentProperty property = context.getProperty();
8283

8384
CustomConversions cnvs = context.getConverter().getConversions();
84-
ConversionService svc = context.getConverter().getConversionService();
8585
Class<?> type = property.getType();
8686

8787
String decryptedString = new String(decrypted);
8888
if ("null".equals(decryptedString)) {
8989
return null;
9090
}
91-
// TODO - as-is, this never gets ran through ObjectMapper() -
92-
// TODO - i.e. @JsonValue etc will not be processed by ObjectMapper
93-
94-
/* this what we would do if we could use a JsonParser with a beanPropertyTypeRef
95-
final JsonParser plaintextParser = p.getCodec().getFactory().createParser(plaintext);
96-
plaintextParser.setCodec(p.getCodec());
97-
98-
return plaintextParser.readValueAs(beanPropertyTypeRef);
99-
*/
10091

10192
if (!cnvs.isSimpleType(type) && !type.isArray()) {
10293
JsonObject jo = JsonObject.fromJson(decryptedString);
@@ -133,16 +124,20 @@ private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, Converti
133124
}
134125
plainText = ja.toBytes();
135126
} else if (cnvs.isSimpleType(sourceType)) { // simpleType
136-
String plainString = value != null ? value.toString() : null; // TODO - this will ignore @JsonValue
127+
String plainString = value != null ? value.toString() : null;
137128
if ((sourceType == String.class || targetType == String.class) || sourceType == Character.class
138-
|| sourceType == char.class || Enum.class.isAssignableFrom(sourceType)
139-
|| Locale.class.isAssignableFrom(sourceType)) {
140-
// TODO use jackson serializer here
141-
plainString = "\"" + plainString.replaceAll("\"", "\\\"") + "\"";
129+
|| sourceType == char.class || sourceType.isEnum() || Locale.class.isAssignableFrom(sourceType)) {
130+
try {
131+
plainString = objectMapper.writeValueAsString(plainString);// put quotes around strings
132+
} catch (JsonProcessingException e) {
133+
throw new RuntimeException(e);
134+
}
142135
}
143136
plainText = plainString.getBytes(StandardCharsets.UTF_8);
144137
} else { // an entity
145-
plainText = JsonObject.fromJson(context.read(value).toString().getBytes(StandardCharsets.UTF_8)).toBytes();
138+
CouchbaseDocument doc = new CouchbaseDocument();
139+
context.getConverter().writeInternalRoot(value, doc, property.getTypeInformation(), false, property, false);
140+
plainText = JsonObject.from(doc.export()).toBytes();
146141
}
147142
return plainText;
148143
}

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

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,7 @@ public void write(final Object source, final CouchbaseDocument target) {
443443
typeMapper.writeType(type, target);
444444
}
445445

446-
writeInternalRoot(source, target, type, true, null);
446+
writeInternalRoot(source, target, type, true, null, true);
447447
if (target.getId() == null) {
448448
throw new MappingException("An ID property is needed, but not found/could not be generated on this entity.");
449449
}
@@ -459,7 +459,7 @@ public void write(final Object source, final CouchbaseDocument target) {
459459
*/
460460
@SuppressWarnings("unchecked")
461461
public void writeInternalRoot(final Object source, CouchbaseDocument target, TypeInformation<?> typeHint,
462-
boolean withId, CouchbasePersistentProperty property) {
462+
boolean withId, CouchbasePersistentProperty property, boolean processValueConverter) {
463463
if (source == null) {
464464
return;
465465
}
@@ -480,7 +480,7 @@ public void writeInternalRoot(final Object source, CouchbaseDocument target, Typ
480480
}
481481

482482
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
483-
writeInternalEntity(source, target, entity, withId, property);
483+
writeInternalEntity(source, target, entity, withId, property, processValueConverter);
484484
addCustomTypeKeyIfNecessary(typeHint, source, target);
485485
}
486486

@@ -517,7 +517,8 @@ private String convertToString(Object propertyObj) {
517517
* @param withId one of the top-level properties is the id for the document
518518
*/
519519
protected void writeInternalEntity(final Object source, final CouchbaseDocument target,
520-
final CouchbasePersistentEntity<?> entity, boolean withId, CouchbasePersistentProperty prop) {
520+
final CouchbasePersistentEntity<?> entity, boolean withId, CouchbasePersistentProperty prop,
521+
boolean processValueConverter) {
521522
if (source == null) {
522523
return;
523524
}
@@ -566,7 +567,7 @@ public void doWithAssociation(final Association<CouchbasePersistentProperty> ass
566567
}
567568
});
568569

569-
if (prop != null && conversions.hasValueConverter(prop)) { // whole entity is encrypted
570+
if (prop != null && processValueConverter && conversions.hasValueConverter(prop)) { // whole entity is encrypted
570571
Map<String, Object> propertyConverted = (Map<String, Object>) conversions.getPropertyValueConversions()
571572
.getValueConverter(prop).write(source, new CouchbaseConversionContext(prop, this, accessor));
572573
target.setContent(JsonObject.from(propertyConverted));
@@ -677,7 +678,7 @@ protected void writePropertyInternal(final Object source, final CouchbaseDocumen
677678
CouchbasePersistentEntity<?> entity = isSubtype(prop.getType(), source.getClass())
678679
? mappingContext.getRequiredPersistentEntity(source.getClass())
679680
: mappingContext.getRequiredPersistentEntity(prop);
680-
writeInternalEntity(source, propertyDoc, entity, false, prop);
681+
writeInternalEntity(source, propertyDoc, entity, false, prop, true);
681682
target.put(maybeMangle(prop), propertyDoc);
682683
}
683684

@@ -728,7 +729,7 @@ private CouchbaseDocument writeMapInternal(final Map<? extends Object, Object> s
728729
prop.getTypeInformation(), prop, getPropertyAccessor(val)));
729730
} else {
730731
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
731-
writeInternalRoot(val, embeddedDoc, prop.getTypeInformation(), false, prop);
732+
writeInternalRoot(val, embeddedDoc, prop.getTypeInformation(), false, prop, true);
732733
target.put(simpleKey, embeddedDoc);
733734
}
734735
} else {
@@ -772,7 +773,7 @@ public CouchbaseList writeCollectionInternal(final Collection<?> source, final C
772773
} else {
773774
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
774775
writeInternalRoot(element, embeddedDoc,
775-
prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop);
776+
prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop, true);
776777
target.put(embeddedDoc);
777778
}
778779

@@ -873,9 +874,9 @@ public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProper
873874
* @param processValueConverter
874875
* @return
875876
*/
876-
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty value,
877+
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty property,
877878
ConvertingPropertyAccessor<Object> accessor, boolean processValueConverter) {
878-
return convertForWriteIfNeeded(value, accessor, processValueConverter); // can access annotations
879+
return convertForWriteIfNeeded(property, accessor, processValueConverter); // can access annotations
879880
}
880881

881882
/**

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.List;
2828
import java.util.UUID;
2929

30-
import com.fasterxml.jackson.databind.ObjectWriter;
3130
import org.springframework.core.convert.converter.Converter;
3231
import org.springframework.data.convert.ReadingConverter;
3332
import org.springframework.data.convert.WritingConverter;
@@ -200,7 +199,7 @@ public Class<?> convert(String source) {
200199

201200
/**
202201
* Writing converter for Enums. This is registered in
203-
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( CryptoManager)}.
202+
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( CryptoManager, ObjectMapper)}.
204203
* The corresponding reading converters are in {@link IntegerToEnumConverterFactory} and
205204
* {@link StringToEnumConverterFactory}
206205
*/
@@ -220,12 +219,12 @@ public Object convert(Enum<?> source) {
220219
objectMapper.writeValue(generator, source);
221220
String s = writer.toString();
222221
if (s != null && s.startsWith("\"")) {
223-
return objectMapper.readValue(s,String.class);
222+
return objectMapper.readValue(s, String.class);
224223
}
225224
if ("true".equals(s) || "false".equals(s)) {
226-
return objectMapper.readValue(s,Boolean.class);
225+
return objectMapper.readValue(s, Boolean.class);
227226
}
228-
return objectMapper.readValue(s,Number.class);
227+
return objectMapper.readValue(s, Number.class);
229228
} catch (IOException e) {
230229
throw new RuntimeException(e);
231230
}

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.springframework.data.couchbase.domain;
1717

18-
import com.couchbase.client.java.encryption.annotation.Encrypted;
1918
import org.springframework.data.couchbase.core.mapping.Document;
2019

2120
@Document
@@ -26,6 +25,7 @@ public class Address extends ComparableEntity {
2625
// for N1qlJoin
2726
private String id;
2827
private String parentId;
28+
private ETurbulenceCategory turbulence;
2929

3030
public Address() {}
3131

@@ -45,6 +45,13 @@ public void setCity(String city) {
4545
this.city = city;
4646
}
4747

48+
public ETurbulenceCategory getTurbulence() {
49+
return turbulence;
50+
}
51+
52+
public void setTurbulence(ETurbulenceCategory turbulence) {
53+
this.turbulence = turbulence;
54+
}
4855
public String getParentId() {
4956
return parentId;
5057
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* @author Michael Reiche
2424
*/
2525
public enum ETurbulenceCategory {
26-
T10("10%"), T20("20%"), T30("30%");
26+
T10("\"10%"), T20("\"20%"), T30("\"30%");
2727

2828
private final String code;
2929

0 commit comments

Comments
 (0)