Skip to content

Commit deca246

Browse files
authored
FLE implementation with property value converter. (#1554)
* FLE Implemenation with Property Value Converter.
1 parent d86e3b9 commit deca246

18 files changed

+2074
-105
lines changed

spring-data-couchbase/pom.xml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
34

45
<modelVersion>4.0.0</modelVersion>
56

@@ -32,6 +33,8 @@
3233
<com.couchbase.mock>1.5.25</com.couchbase.mock>
3334
<com.squareup.okhttp3>4.8.1</com.squareup.okhttp3>
3435
<org.awaitility>4.0.3</org.awaitility>
36+
<jackson-joda>2.13.4</jackson-joda>
37+
<couchbase.encryption>3.1.0</couchbase.encryption>
3538
</properties>
3639

3740
<dependencyManagement>
@@ -115,6 +118,13 @@
115118
<optional>true</optional>
116119
</dependency>
117120

121+
<dependency>
122+
<groupId>com.fasterxml.jackson.datatype</groupId>
123+
<artifactId>jackson-datatype-joda</artifactId>
124+
<version>${jackson-joda}</version>
125+
<scope>test</scope>
126+
</dependency>
127+
118128
<dependency>
119129
<groupId>org.testcontainers</groupId>
120130
<artifactId>testcontainers</artifactId>
@@ -217,6 +227,13 @@
217227
<scope>test</scope>
218228
</dependency>
219229

230+
<dependency>
231+
<groupId>com.couchbase.client</groupId>
232+
<artifactId>couchbase-encryption</artifactId>
233+
<version>${couchbase.encryption}</version>
234+
<scope>test</scope>
235+
</dependency>
236+
220237
</dependencies>
221238

222239
<build>
@@ -279,7 +296,9 @@
279296
</goals>
280297
<configuration>
281298
<outputDirectory>target/generated-test-sources</outputDirectory>
282-
<processor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</processor>
299+
<processor>
300+
org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor
301+
</processor>
283302
</configuration>
284303
</execution>
285304
</executions>

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

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,28 @@
1818

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

21-
import java.util.Collections;
21+
import java.util.ArrayList;
2222
import java.util.HashSet;
23+
import java.util.List;
2324
import java.util.Set;
2425

2526
import org.springframework.beans.factory.config.BeanDefinition;
2627
import org.springframework.context.annotation.Bean;
2728
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
2829
import org.springframework.context.annotation.Configuration;
2930
import org.springframework.context.annotation.Role;
31+
import org.springframework.core.convert.converter.GenericConverter;
3032
import org.springframework.core.type.filter.AnnotationTypeFilter;
3133
import org.springframework.data.convert.CustomConversions;
34+
import org.springframework.data.convert.PropertyValueConverterRegistrar;
35+
import org.springframework.data.convert.SimplePropertyValueConversions;
3236
import org.springframework.data.couchbase.CouchbaseClientFactory;
3337
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
3438
import org.springframework.data.couchbase.core.CouchbaseTemplate;
3539
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
3640
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
41+
import org.springframework.data.couchbase.core.convert.CouchbasePropertyValueConverterFactory;
42+
import org.springframework.data.couchbase.core.convert.CryptoConverter;
3743
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
3844
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
3945
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
@@ -149,7 +155,9 @@ public ClusterEnvironment couchbaseClusterEnvironment() {
149155
if (!nonShadowedJacksonPresent()) {
150156
throw new CouchbaseException("non-shadowed Jackson not present");
151157
}
152-
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper()));
158+
CryptoManager cryptoManager = cryptoManager();
159+
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper(cryptoManager)));
160+
builder.cryptoManager(cryptoManager);
153161
configureEnvironment(builder);
154162
return builder.build();
155163
}
@@ -160,7 +168,6 @@ public ClusterEnvironment couchbaseClusterEnvironment() {
160168
* @param builder the builder that can be customized.
161169
*/
162170
protected void configureEnvironment(final ClusterEnvironment.Builder builder) {
163-
164171
}
165172

166173
@Bean(name = BeanNames.COUCHBASE_TEMPLATE)
@@ -269,6 +276,7 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte
269276
CouchbaseCustomConversions couchbaseCustomConversions) {
270277
MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey());
271278
converter.setCustomConversions(couchbaseCustomConversions);
279+
couchbaseMappingContext.setSimpleTypeHolder(couchbaseCustomConversions.getSimpleTypeHolder());
272280
return converter;
273281
}
274282

@@ -280,8 +288,8 @@ public MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte
280288
@Bean
281289
public TranslationService couchbaseTranslationService() {
282290
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
291+
jacksonTranslationService.setObjectMapper(couchbaseObjectMapper(cryptoManager()));
283292
jacksonTranslationService.afterPropertiesSet();
284-
285293
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
286294
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
287295
return jacksonTranslationService;
@@ -306,12 +314,26 @@ public CouchbaseMappingContext couchbaseMappingContext(CustomConversions customC
306314
*
307315
* @return ObjectMapper
308316
*/
317+
private ObjectMapper couchbaseObjectMapper() {
318+
return couchbaseObjectMapper(cryptoManager());
319+
}
320+
321+
/**
322+
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
323+
*
324+
* @param cryptoManager
325+
* @return ObjectMapper
326+
*/
309327

310-
public ObjectMapper couchbaseObjectMapper() {
311-
ObjectMapper mapper = new ObjectMapper();
328+
ObjectMapper mapper;
329+
330+
public ObjectMapper couchbaseObjectMapper(CryptoManager cryptoManager) {
331+
if (mapper != null) {
332+
return mapper;
333+
}
334+
mapper = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
312335
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
313336
mapper.registerModule(new JsonValueModule());
314-
CryptoManager cryptoManager = null;
315337
if (cryptoManager != null) {
316338
mapper.registerModule(new EncryptionModule(cryptoManager));
317339
}
@@ -320,7 +342,7 @@ public ObjectMapper couchbaseObjectMapper() {
320342

321343
/**
322344
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
323-
* CallbackPreferrringTransactionmanagers do not play well with test-cases that rely
345+
* CallbackPreferringTransactionManagers do not play well with test-cases that rely
324346
* on @TestTransaction/@BeforeTransaction/@AfterTransaction
325347
*
326348
* @param clientFactory
@@ -341,6 +363,7 @@ CouchbaseCallbackTransactionManager couchbaseTransactionManager(CouchbaseClientF
341363
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
342364
return new TransactionTemplate(couchbaseTransactionManager);
343365
}
366+
344367
/**
345368
* The default TransactionalOperator.
346369
*
@@ -376,14 +399,43 @@ protected boolean autoIndexCreation() {
376399
/**
377400
* Register custom Converters in a {@link CustomConversions} object if required. These {@link CustomConversions} will
378401
* be registered with the {@link #mappingCouchbaseConverter(CouchbaseMappingContext, CouchbaseCustomConversions)} )}
379-
* and {@link #couchbaseMappingContext(CustomConversions)}. Returns an empty {@link CustomConversions} instance by
380-
* default.
402+
* and {@link #couchbaseMappingContext(CustomConversions)}.
381403
*
382404
* @return must not be {@literal null}.
383405
*/
384406
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
385407
public CustomConversions customConversions() {
386-
return new CouchbaseCustomConversions(Collections.emptyList());
408+
return customConversions(cryptoManager());
409+
}
410+
411+
/**
412+
* Register custom Converters in a {@link CustomConversions} object if required. These {@link CustomConversions} will
413+
* be registered with the {@link #mappingCouchbaseConverter(CouchbaseMappingContext, CouchbaseCustomConversions)} )}
414+
* and {@link #couchbaseMappingContext(CustomConversions)}.
415+
*
416+
* @param cryptoManager
417+
* @return must not be {@literal null}.
418+
*/
419+
public CustomConversions customConversions(CryptoManager cryptoManager) {
420+
List<GenericConverter> newConverters = new ArrayList();
421+
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
422+
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
423+
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
424+
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
425+
configurationAdapter.setPropertyValueConversions(valueConversions);
426+
configurationAdapter.registerConverters(newConverters);
427+
});
428+
return customConversions;
429+
}
430+
431+
@Bean
432+
protected CryptoManager cryptoManager() {
433+
return null;
434+
}
435+
436+
@Bean
437+
protected CryptoConverter cryptoConverter(CryptoManager cryptoManager) {
438+
return cryptoManager == null ? null : new CryptoConverter(cryptoManager);
387439
}
388440

389441
/**

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

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@
1818

1919
import java.util.Collections;
2020

21+
import com.couchbase.client.java.query.QueryScanConsistency;
2122
import org.springframework.beans.factory.InitializingBean;
2223
import org.springframework.core.convert.ConversionService;
24+
import org.springframework.core.convert.TypeDescriptor;
2325
import org.springframework.core.convert.support.GenericConversionService;
2426
import org.springframework.data.convert.CustomConversions;
27+
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
28+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
29+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
2530
import org.springframework.data.mapping.model.EntityInstantiators;
2631

2732
/**
2833
* An abstract {@link CouchbaseConverter} that provides the basics for the {@link MappingCouchbaseConverter}.
2934
*
3035
* @author Michael Nitschinger
3136
* @author Mark Paluch
37+
* @author Michael Reiche
3238
*/
3339
public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean {
3440

@@ -93,39 +99,70 @@ public void afterPropertiesSet() {
9399
conversions.registerConvertersIn(conversionService);
94100
}
95101

102+
/**
103+
* This convertForWriteIfNeeded takes a property and accessor so that the annotations can be accessed (ie. @Encrypted)
104+
*
105+
* @param prop the property to be converted to the class that would actually be stored.
106+
* @param accessor the property accessor
107+
* @return
108+
*/
96109
@Override
97-
public Object convertForWriteIfNeeded(Object value) {
110+
public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, ConvertingPropertyAccessor<Object> accessor,
111+
boolean processValueConverter) {
112+
Object value = accessor.getProperty(prop, prop.getType());
98113
if (value == null) {
99114
return null;
100115
}
116+
if (processValueConverter && conversions.hasValueConverter(prop)) {
117+
CouchbaseDocument encrypted = (CouchbaseDocument) conversions.getPropertyValueConversions()
118+
.getValueConverter(prop)
119+
.write(value, new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
120+
return encrypted;
121+
}
122+
Class<?> targetClass = this.conversions.getCustomWriteTarget(value.getClass()).orElse(null);
101123

102-
return this.conversions.getCustomWriteTarget(value.getClass()) //
103-
.map(it -> (Object) this.conversionService.convert(value, it)) //
124+
boolean canConvert = targetClass == null ? false
125+
: this.conversionService.canConvert(new TypeDescriptor(prop.getField()), TypeDescriptor.valueOf(targetClass));
126+
if (canConvert) {
127+
return this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
128+
TypeDescriptor.valueOf(targetClass));
129+
}
130+
131+
Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
132+
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
133+
TypeDescriptor.valueOf(it))) //
104134
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
105135

136+
return result;
137+
106138
}
107139

108-
/* TODO needed later
140+
/**
141+
* This convertForWriteIfNeed takes only the value to convert. It cannot access the annotations of the Field being
142+
* converted.
143+
*
144+
* @param value the value to be converted to the class that would actually be stored.
145+
* @return
146+
*/
109147
@Override
110-
public Object convertToCouchbaseType(Object value, TypeInformation<?> typeInformation) {
148+
public Object convertForWriteIfNeeded(Object value) {
111149
if (value == null) {
112150
return null;
113151
}
114-
152+
115153
return this.conversions.getCustomWriteTarget(value.getClass()) //
116154
.map(it -> (Object) this.conversionService.convert(value, it)) //
117155
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
118-
119-
}
120-
121-
@Override
122-
public Object convertToCouchbaseType(String source) {
123-
return source;
156+
124157
}
125-
*/
126158

127159
@Override
128160
public Class<?> getWriteClassFor(Class<?> clazz) {
129161
return this.conversions.getCustomWriteTarget(clazz).orElse(clazz);
130162
}
163+
164+
@Override
165+
public CustomConversions getConversions() {
166+
return conversions;
167+
}
131168
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 org.springframework.data.convert.ValueConversionContext;
19+
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
20+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
21+
import org.springframework.data.util.TypeInformation;
22+
import org.springframework.lang.Nullable;
23+
24+
/**
25+
* {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link CouchbaseConverter}.
26+
*
27+
* @author Michael Reiche
28+
* @since 5.0
29+
*/
30+
public class CouchbaseConversionContext implements ValueConversionContext<CouchbasePersistentProperty> {
31+
32+
private final CouchbasePersistentProperty persistentProperty;
33+
private final MappingCouchbaseConverter couchbaseConverter;
34+
private final ConvertingPropertyAccessor propertyAccessor;
35+
36+
public CouchbaseConversionContext(CouchbasePersistentProperty persistentProperty,
37+
MappingCouchbaseConverter couchbaseConverter, ConvertingPropertyAccessor accessor) {
38+
39+
this.persistentProperty = persistentProperty;
40+
this.couchbaseConverter = couchbaseConverter;
41+
this.propertyAccessor = accessor;
42+
}
43+
44+
@Override
45+
public CouchbasePersistentProperty getProperty() {
46+
return persistentProperty;
47+
}
48+
49+
@Override
50+
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
51+
return (T) ValueConversionContext.super.write(value, target);
52+
}
53+
54+
@Override
55+
public <T> T read(@Nullable Object value, TypeInformation<T> target) {
56+
return ValueConversionContext.super.read(value, target);
57+
}
58+
59+
public MappingCouchbaseConverter getConverter() {
60+
return couchbaseConverter;
61+
}
62+
63+
public ConvertingPropertyAccessor getAccessor() {
64+
return propertyAccessor;
65+
}
66+
}

0 commit comments

Comments
 (0)