FLE implementation with property value converter. (#1554)

* FLE Implemenation with Property Value Converter.
This commit is contained in:
Michael Reiche
2022-10-07 13:09:05 -10:00
committed by GitHub
parent d86e3b90cf
commit deca2465e0
18 changed files with 2081 additions and 112 deletions

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<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">
<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">
<modelVersion>4.0.0</modelVersion>
@@ -32,6 +33,8 @@
<com.couchbase.mock>1.5.25</com.couchbase.mock>
<com.squareup.okhttp3>4.8.1</com.squareup.okhttp3>
<org.awaitility>4.0.3</org.awaitility>
<jackson-joda>2.13.4</jackson-joda>
<couchbase.encryption>3.1.0</couchbase.encryption>
</properties>
<dependencyManagement>
@@ -115,6 +118,13 @@
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
<version>${jackson-joda}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
@@ -217,6 +227,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>couchbase-encryption</artifactId>
<version>${couchbase.encryption}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -279,7 +296,9 @@
</goals>
<configuration>
<outputDirectory>target/generated-test-sources</outputDirectory>
<processor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</processor>
<processor>
org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor
</processor>
</configuration>
</execution>
</executions>

View File

@@ -18,8 +18,9 @@ package org.springframework.data.couchbase.config;
import static com.couchbase.client.java.ClusterOptions.clusterOptions;
import java.util.Collections;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -27,13 +28,18 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.PropertyValueConverterRegistrar;
import org.springframework.data.convert.SimplePropertyValueConversions;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.SimpleCouchbaseClientFactory;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
import org.springframework.data.couchbase.core.convert.CouchbasePropertyValueConverterFactory;
import org.springframework.data.couchbase.core.convert.CryptoConverter;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
@@ -149,7 +155,9 @@ public abstract class AbstractCouchbaseConfiguration {
if (!nonShadowedJacksonPresent()) {
throw new CouchbaseException("non-shadowed Jackson not present");
}
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper()));
CryptoManager cryptoManager = cryptoManager();
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper(cryptoManager)));
builder.cryptoManager(cryptoManager);
configureEnvironment(builder);
return builder.build();
}
@@ -160,7 +168,6 @@ public abstract class AbstractCouchbaseConfiguration {
* @param builder the builder that can be customized.
*/
protected void configureEnvironment(final ClusterEnvironment.Builder builder) {
}
@Bean(name = BeanNames.COUCHBASE_TEMPLATE)
@@ -269,6 +276,7 @@ public abstract class AbstractCouchbaseConfiguration {
CouchbaseCustomConversions couchbaseCustomConversions) {
MappingCouchbaseConverter converter = new MappingCouchbaseConverter(couchbaseMappingContext, typeKey());
converter.setCustomConversions(couchbaseCustomConversions);
couchbaseMappingContext.setSimpleTypeHolder(couchbaseCustomConversions.getSimpleTypeHolder());
return converter;
}
@@ -280,8 +288,8 @@ public abstract class AbstractCouchbaseConfiguration {
@Bean
public TranslationService couchbaseTranslationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.setObjectMapper(couchbaseObjectMapper(cryptoManager()));
jacksonTranslationService.afterPropertiesSet();
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return jacksonTranslationService;
@@ -306,12 +314,26 @@ public abstract class AbstractCouchbaseConfiguration {
*
* @return ObjectMapper
*/
private ObjectMapper couchbaseObjectMapper() {
return couchbaseObjectMapper(cryptoManager());
}
public ObjectMapper couchbaseObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
/**
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
*
* @param cryptoManager
* @return ObjectMapper
*/
ObjectMapper mapper;
public ObjectMapper couchbaseObjectMapper(CryptoManager cryptoManager) {
if (mapper != null) {
return mapper;
}
mapper = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JsonValueModule());
CryptoManager cryptoManager = null;
if (cryptoManager != null) {
mapper.registerModule(new EncryptionModule(cryptoManager));
}
@@ -320,7 +342,7 @@ public abstract class AbstractCouchbaseConfiguration {
/**
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
* CallbackPreferrringTransactionmanagers do not play well with test-cases that rely
* CallbackPreferringTransactionManagers do not play well with test-cases that rely
* on @TestTransaction/@BeforeTransaction/@AfterTransaction
*
* @param clientFactory
@@ -341,6 +363,7 @@ public abstract class AbstractCouchbaseConfiguration {
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
return new TransactionTemplate(couchbaseTransactionManager);
}
/**
* The default TransactionalOperator.
*
@@ -376,14 +399,43 @@ public abstract class AbstractCouchbaseConfiguration {
/**
* Register custom Converters in a {@link CustomConversions} object if required. These {@link CustomConversions} will
* be registered with the {@link #mappingCouchbaseConverter(CouchbaseMappingContext, CouchbaseCustomConversions)} )}
* and {@link #couchbaseMappingContext(CustomConversions)}. Returns an empty {@link CustomConversions} instance by
* default.
* and {@link #couchbaseMappingContext(CustomConversions)}.
*
* @return must not be {@literal null}.
*/
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
public CustomConversions customConversions() {
return new CouchbaseCustomConversions(Collections.emptyList());
return customConversions(cryptoManager());
}
/**
* Register custom Converters in a {@link CustomConversions} object if required. These {@link CustomConversions} will
* be registered with the {@link #mappingCouchbaseConverter(CouchbaseMappingContext, CouchbaseCustomConversions)} )}
* and {@link #couchbaseMappingContext(CustomConversions)}.
*
* @param cryptoManager
* @return must not be {@literal null}.
*/
public CustomConversions customConversions(CryptoManager cryptoManager) {
List<GenericConverter> newConverters = new ArrayList();
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
configurationAdapter.setPropertyValueConversions(valueConversions);
configurationAdapter.registerConverters(newConverters);
});
return customConversions;
}
@Bean
protected CryptoManager cryptoManager() {
return null;
}
@Bean
protected CryptoConverter cryptoConverter(CryptoManager cryptoManager) {
return cryptoManager == null ? null : new CryptoConverter(cryptoManager);
}
/**

View File

@@ -18,10 +18,15 @@ package org.springframework.data.couchbase.core.convert;
import java.util.Collections;
import com.couchbase.client.java.query.QueryScanConsistency;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.EntityInstantiators;
/**
@@ -29,6 +34,7 @@ import org.springframework.data.mapping.model.EntityInstantiators;
*
* @author Michael Nitschinger
* @author Mark Paluch
* @author Michael Reiche
*/
public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean {
@@ -93,6 +99,51 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
conversions.registerConvertersIn(conversionService);
}
/**
* This convertForWriteIfNeeded takes a property and accessor so that the annotations can be accessed (ie. @Encrypted)
*
* @param prop the property to be converted to the class that would actually be stored.
* @param accessor the property accessor
* @return
*/
@Override
public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, ConvertingPropertyAccessor<Object> accessor,
boolean processValueConverter) {
Object value = accessor.getProperty(prop, prop.getType());
if (value == null) {
return null;
}
if (processValueConverter && conversions.hasValueConverter(prop)) {
CouchbaseDocument encrypted = (CouchbaseDocument) conversions.getPropertyValueConversions()
.getValueConverter(prop)
.write(value, new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
return encrypted;
}
Class<?> targetClass = this.conversions.getCustomWriteTarget(value.getClass()).orElse(null);
boolean canConvert = targetClass == null ? false
: this.conversionService.canConvert(new TypeDescriptor(prop.getField()), TypeDescriptor.valueOf(targetClass));
if (canConvert) {
return this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
TypeDescriptor.valueOf(targetClass));
}
Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
TypeDescriptor.valueOf(it))) //
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
return result;
}
/**
* This convertForWriteIfNeed takes only the value to convert. It cannot access the annotations of the Field being
* converted.
*
* @param value the value to be converted to the class that would actually be stored.
* @return
*/
@Override
public Object convertForWriteIfNeeded(Object value) {
if (value == null) {
@@ -105,27 +156,13 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
}
/* TODO needed later
@Override
public Object convertToCouchbaseType(Object value, TypeInformation<?> typeInformation) {
if (value == null) {
return null;
}
return this.conversions.getCustomWriteTarget(value.getClass()) //
.map(it -> (Object) this.conversionService.convert(value, it)) //
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
}
@Override
public Object convertToCouchbaseType(String source) {
return source;
}
*/
@Override
public Class<?> getWriteClassFor(Class<?> clazz) {
return this.conversions.getCustomWriteTarget(clazz).orElse(clazz);
}
@Override
public CustomConversions getConversions() {
return conversions;
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
/**
* {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link CouchbaseConverter}.
*
* @author Michael Reiche
* @since 5.0
*/
public class CouchbaseConversionContext implements ValueConversionContext<CouchbasePersistentProperty> {
private final CouchbasePersistentProperty persistentProperty;
private final MappingCouchbaseConverter couchbaseConverter;
private final ConvertingPropertyAccessor propertyAccessor;
public CouchbaseConversionContext(CouchbasePersistentProperty persistentProperty,
MappingCouchbaseConverter couchbaseConverter, ConvertingPropertyAccessor accessor) {
this.persistentProperty = persistentProperty;
this.couchbaseConverter = couchbaseConverter;
this.propertyAccessor = accessor;
}
@Override
public CouchbasePersistentProperty getProperty() {
return persistentProperty;
}
@Override
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
return (T) ValueConversionContext.super.write(value, target);
}
@Override
public <T> T read(@Nullable Object value, TypeInformation<T> target) {
return ValueConversionContext.super.read(value, target);
}
public MappingCouchbaseConverter getConverter() {
return couchbaseConverter;
}
public ConvertingPropertyAccessor getAccessor() {
return propertyAccessor;
}
}

View File

@@ -16,12 +16,14 @@
package org.springframework.data.couchbase.core.convert;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.EntityConverter;
import org.springframework.data.convert.EntityReader;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.util.TypeInformation;
/**
@@ -37,13 +39,24 @@ public interface CouchbaseConverter
/**
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
* needed.
* needed. This method cannot access the annotations of the field.
*
* @param value the value to be converted to the class that would actually be stored.
* @return the converted value (or the same value if no conversion necessary).
*/
Object convertForWriteIfNeeded(Object value);
/**
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
* needed. This method can access the annotations of the field.
*
* @param source the property to be converted to the class that would actually be stored.
* @param accessor the property accessor
* @return the converted value (or the same value if no conversion necessary).
*/
Object convertForWriteIfNeeded(final CouchbasePersistentProperty source,
final ConvertingPropertyAccessor<Object> accessor, boolean processValueConverter);
/**
* Return the Class that would actually be stored for a given Class.
*
@@ -63,9 +76,10 @@ public interface CouchbaseConverter
*/
Alias getTypeAlias(TypeInformation<?> info);
// TODO needed later
// CouchbaseTypeMapper getMapper();
// Object convertToCouchbaseType(Object source, TypeInformation<?> typeInformation);
//
// Object convertToCouchbaseType(String source);
/**
* return the conversions
*
* @return conversions
*/
CustomConversions getConversions();
}

View File

@@ -16,11 +16,35 @@
package org.springframework.data.couchbase.core.convert;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.convert.PropertyValueConversions;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.convert.PropertyValueConverterRegistrar;
import org.springframework.data.convert.SimplePropertyValueConversions;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.util.Assert;
import com.couchbase.client.java.encryption.annotation.Encrypted;
/**
* Value object to capture custom conversion.
@@ -32,6 +56,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
* @author Oliver Gierke
* @author Mark Paluch
* @author Subhashni Balakrishnan
* @Michael Reiche
* @see org.springframework.data.convert.CustomConversions
* @see SimpleTypeHolder
* @since 2.0
@@ -55,11 +80,249 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
}
/**
* Create a new instance with a given list of converters.
* Create a new {@link CouchbaseCustomConversions} instance registering the given converters.
*
* @param converters the list of custom converters.
* @param converters must not be {@literal null}.
*/
public CouchbaseCustomConversions(final List<?> converters) {
super(STORE_CONVERSIONS, converters);
public CouchbaseCustomConversions(List<?> converters) {
this(CouchbaseConverterConfigurationAdapter.from(converters));
}
/**
* Create a new {@link CouchbaseCustomConversions} given {@link CouchbaseConverterConfigurationAdapter}.
*
* @param conversionConfiguration must not be {@literal null}.
*/
protected CouchbaseCustomConversions(CouchbaseConverterConfigurationAdapter conversionConfiguration) {
super(conversionConfiguration.createConverterConfiguration());
}
/**
* Functional style {@link org.springframework.data.convert.CustomConversions} creation giving users a convenient way
* of configuring store specific capabilities by providing deferred hooks to what will be configured when creating the
* {@link org.springframework.data.convert.CustomConversions#CustomConversions(ConverterConfiguration) instance}.
*
* @param configurer must not be {@literal null}.
*/
public static CouchbaseCustomConversions create(Consumer<CouchbaseConverterConfigurationAdapter> configurer) {
CouchbaseConverterConfigurationAdapter adapter = new CouchbaseConverterConfigurationAdapter();
configurer.accept(adapter);
return new CouchbaseCustomConversions(adapter);
}
@Override
public boolean hasValueConverter(PersistentProperty<?> property) {
if (property.findAnnotation(Encrypted.class) != null) {
return true;
}
return super.hasValueConverter(property);
}
/**
* {@link CouchbaseConverterConfigurationAdapter} encapsulates creation of
* {@link org.springframework.data.convert.CustomConversions.ConverterConfiguration} with CouchbaseDB specifics.
*/
public static class CouchbaseConverterConfigurationAdapter {
/**
* List of {@literal java.time} types having different representation when rendered
*/
private static final Set<Class<?>> JAVA_DRIVER_TIME_SIMPLE_TYPES = new HashSet<>(
Arrays.asList(LocalDate.class, LocalTime.class, LocalDateTime.class));
private boolean useNativeDriverJavaTimeCodecs = false;
private final List<Object> customConverters = new ArrayList<>();
private final PropertyValueConversions internalValueConversion = PropertyValueConversions.simple(it -> {});
private PropertyValueConversions propertyValueConversions = internalValueConversion;
/**
* Create a {@link CouchbaseConverterConfigurationAdapter} using the provided {@code converters} and our own codecs
* for JSR-310 types.
*
* @param converters must not be {@literal null}.
* @return
*/
public static CouchbaseConverterConfigurationAdapter from(List<?> converters) {
Assert.notNull(converters, "Converters must not be null");
CouchbaseConverterConfigurationAdapter converterConfigurationAdapter = new CouchbaseConverterConfigurationAdapter();
converterConfigurationAdapter.registerConverters(converters);
return converterConfigurationAdapter;
}
/**
* Add a custom {@link Converter} implementation.
*
* @param converter must not be {@literal null}.
* @return this.
*/
public CouchbaseConverterConfigurationAdapter registerConverter(Converter<?, ?> converter) {
Assert.notNull(converter, "Converter must not be null!");
customConverters.add(converter);
return this;
}
/**
* Gateway to register property specific converters.
*
* @param configurationAdapter must not be {@literal null}.
* @return this.
*/
public CouchbaseConverterConfigurationAdapter configurePropertyConversions(
Consumer<PropertyValueConverterRegistrar<CouchbasePersistentProperty>> configurationAdapter) {
Assert.state(valueConversions() instanceof SimplePropertyValueConversions,
"Configured PropertyValueConversions does not allow setting custom ConverterRegistry");
PropertyValueConverterRegistrar propertyValueConverterRegistrar = new PropertyValueConverterRegistrar();
configurationAdapter.accept(propertyValueConverterRegistrar);
((SimplePropertyValueConversions) valueConversions())
.setValueConverterRegistry(propertyValueConverterRegistrar.buildRegistry());
return this;
}
/**
* Add a custom {@link ConverterFactory} implementation.
*
* @param converterFactory must not be {@literal null}.
* @return this.
*/
public CouchbaseConverterConfigurationAdapter registerConverterFactory(ConverterFactory<?, ?> converterFactory) {
Assert.notNull(converterFactory, "ConverterFactory must not be null");
customConverters.add(converterFactory);
return this;
}
/**
* Add {@link Converter converters}, {@link ConverterFactory factories}, {@link ConverterBuilder.ConverterAware
* converter-aware objects}, and {@link GenericConverter generic converters}.
*
* @param converters must not be {@literal null} nor contain {@literal null} values.
* @return this.
*/
public CouchbaseConverterConfigurationAdapter registerConverters(Collection<?> converters) {
Assert.notNull(converters, "Converters must not be null");
Assert.noNullElements(converters, "Converters must not be null nor contain null values");
customConverters.addAll(converters);
return this;
}
/**
* Add a custom/default {@link PropertyValueConverterFactory} implementation used to serve
* {@link PropertyValueConverter}.
*
* @param converterFactory must not be {@literal null}.
* @return this.
*/
public CouchbaseConverterConfigurationAdapter registerPropertyValueConverterFactory(
PropertyValueConverterFactory converterFactory) {
Assert.state(valueConversions() instanceof SimplePropertyValueConversions,
"Configured PropertyValueConversions does not allow setting custom ConverterRegistry");
((SimplePropertyValueConversions) valueConversions()).setConverterFactory(converterFactory);
return this;
}
/**
* Optionally set the {@link PropertyValueConversions} to be applied during mapping.
* <p>
* Use this method if {@link #configurePropertyConversions(Consumer)} and
* {@link #registerPropertyValueConverterFactory(PropertyValueConverterFactory)} are not sufficient.
*
* @param valueConversions must not be {@literal null}.
* @return this.
*/
public CouchbaseConverterConfigurationAdapter setPropertyValueConversions(
PropertyValueConversions valueConversions) {
Assert.notNull(valueConversions, "PropertyValueConversions must not be null");
this.propertyValueConversions = valueConversions;
return this;
}
PropertyValueConversions valueConversions() {
if (this.propertyValueConversions == null) {
this.propertyValueConversions = internalValueConversion;
}
return this.propertyValueConversions;
}
ConverterConfiguration createConverterConfiguration() {
if (hasDefaultPropertyValueConversions()
&& propertyValueConversions instanceof SimplePropertyValueConversions svc) {
svc.init();
}
if (!useNativeDriverJavaTimeCodecs) {
return new ConverterConfiguration(STORE_CONVERSIONS, this.customConverters, convertiblePair -> true,
this.propertyValueConversions);
}
/*
* We need to have those converters using UTC as the default ones would go on with the systemDefault.
*/
List<Object> converters = new ArrayList<>(STORE_CONVERTERS.size() + 3);
converters.add(DateToUtcLocalDateConverter.INSTANCE);
converters.add(DateToUtcLocalTimeConverter.INSTANCE);
converters.add(DateToUtcLocalDateTimeConverter.INSTANCE);
converters.addAll(STORE_CONVERTERS);
StoreConversions storeConversions = StoreConversions.of(new SimpleTypeHolder(JAVA_DRIVER_TIME_SIMPLE_TYPES,
SimpleTypeHolder.DEFAULT /* CouchbaseSimpleTypes.HOLDER */), converters);
return new ConverterConfiguration(storeConversions, this.customConverters, convertiblePair -> {
// Avoid default registrations
if (JAVA_DRIVER_TIME_SIMPLE_TYPES.contains(convertiblePair.getSourceType())
&& Date.class.isAssignableFrom(convertiblePair.getTargetType())) {
return false;
}
return true;
}, this.propertyValueConversions);
}
private enum DateToUtcLocalDateTimeConverter implements Converter<Date, LocalDateTime> {
INSTANCE;
@Override
public LocalDateTime convert(Date source) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getTime()), ZoneId.of("UTC"));
}
}
private enum DateToUtcLocalTimeConverter implements Converter<Date, LocalTime> {
INSTANCE;
@Override
public LocalTime convert(Date source) {
return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalTime();
}
}
private enum DateToUtcLocalDateConverter implements Converter<Date, LocalDate> {
INSTANCE;
@Override
public LocalDate convert(Date source) {
return DateToUtcLocalDateTimeConverter.INSTANCE.convert(source).toLocalDate();
}
}
private boolean hasDefaultPropertyValueConversions() {
return propertyValueConversions == internalValueConversion;
}
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.PropertyValueConverterFactory;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.mapping.PersistentProperty;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.java.encryption.annotation.Encrypted;
/**
* Accept the Couchbase @Encrypted annotation in addition to @ValueConverter
*
* @author Michael Reiche
*/
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
CryptoManager cryptoManager;
Map<Class<? extends PropertyValueConverter<?, ?, ?>>, PropertyValueConverter<?, ?, ?>> converterCache = new HashMap<>();
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager) {
this.cryptoManager = cryptoManager;
}
@Override
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
PersistentProperty<?> property) {
PropertyValueConverter<DV, SV, P> valueConverter = PropertyValueConverterFactory.super.getConverter(property);
if (valueConverter != null) {
return valueConverter;
}
Encrypted encryptedAnn = property.findAnnotation(Encrypted.class);
if (encryptedAnn != null) {
Class cryptoConverterClass = CryptoConverter.class;
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) cryptoConverterClass);
} else {
return null;
}
}
@Override
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
Class<? extends PropertyValueConverter<DV, SV, P>> converterType) {
PropertyValueConverter<?, ?, ?> converter = converterCache.get(converterType);
if (converter != null) {
return (PropertyValueConverter<DV, SV, P>) converter;
}
if (CryptoConverter.class.isAssignableFrom(converterType)) {
converter = new CryptoConverter(cryptoManager);
} else {
try {
Constructor constructor = converterType.getConstructor();
converter = (PropertyValueConverter<?, ?, ?>) constructor.newInstance();
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
converterCache.put((Class<? extends PropertyValueConverter<DV, SV, P>>) converter.getClass(), converter);
return (PropertyValueConverter<DV, SV, P>) converter;
}
}

View File

@@ -0,0 +1,276 @@
/*
* Copyright 2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import java.nio.charset.StandardCharsets;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.util.Assert;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.core.error.InvalidArgumentException;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
/**
* Encrypt/Decrypted properties annotated with
*
* @author Michael Reiche
*/
public class CryptoConverter implements
PropertyValueConverter<Object, CouchbaseDocument, ValueConversionContext<? extends PersistentProperty<?>>> {
CryptoManager cryptoManager;
public CryptoConverter(CryptoManager cryptoManager) {
this.cryptoManager = cryptoManager;
}
@Override
public Object read(CouchbaseDocument value, ValueConversionContext<? extends PersistentProperty<?>> context) {
byte[] decrypted = cryptoManager().decrypt(value.export());
if (decrypted == null) {
return null;
}
// it's decrypted to byte[]. Now figure out how to convert to the property type.
return coerceToValueRead(decrypted, (CouchbaseConversionContext) context);
}
@Override
public CouchbaseDocument write(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
CouchbaseConversionContext ctx = (CouchbaseConversionContext) context;
CouchbasePersistentProperty property = ctx.getProperty();
byte[] plainText = coerceToBytesWrite(property, ctx.getAccessor(), ctx);
Map<String, Object> encrypted = cryptoManager().encrypt(plainText, CryptoManager.DEFAULT_ENCRYPTER_ALIAS);
return new CouchbaseDocument().setContent(encrypted);
}
private Object coerceToValueRead(byte[] decrypted, CouchbaseConversionContext context) {
CouchbasePersistentProperty property = context.getProperty();
CustomConversions cnvs = context.getConverter().getConversions();
ConversionService svc = context.getConverter().getConversionService();
Class<?> type = property.getType();
String decryptedString = new String(decrypted);
if ("null".equals(decryptedString)) {
return null;
}
/* this what we would do if we could use a JsonParser with a beanPropertyTypeRef
final JsonParser plaintextParser = p.getCodec().getFactory().createParser(plaintext);
plaintextParser.setCodec(p.getCodec());
return plaintextParser.readValueAs(beanPropertyTypeRef);
*/
if (!cnvs.isSimpleType(type) && !type.isArray()) {
JsonObject jo = JsonObject.fromJson(decryptedString);
CouchbaseDocument source = new CouchbaseDocument().setContent(jo);
return context.getConverter().read(property.getTypeInformation(), source);
} else {
String jsonString = "{\"" + property.getFieldName() + "\":" + decryptedString + "}";
try {
CouchbaseDocument decryptedDoc = new CouchbaseDocument().setContent(JsonObject.fromJson(jsonString));
return context.getConverter().getPotentiallyConvertedSimpleRead(decryptedDoc.get(property.getFieldName()),
property);
} catch (InvalidArgumentException | ConverterNotFoundException | ConversionFailedException e) {
throw new RuntimeException(decryptedString, e);
}
}
}
private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, ConvertingPropertyAccessor accessor,
CouchbaseConversionContext context) {
byte[] plainText;
CustomConversions cnvs = context.getConverter().getConversions();
Class<?> sourceType = property.getType();
Class<?> targetType = cnvs.getCustomWriteTarget(property.getType()).orElse(null);
Object value = context.getConverter().getPotentiallyConvertedSimpleWrite(property, accessor, false);
if (value == null) { // null
plainText = "null".getBytes(StandardCharsets.UTF_8);
} else if (value.getClass().isArray()) { // array
JsonArray ja;
if (value.getClass().getComponentType().isPrimitive()) {
ja = jaFromPrimitiveArray(value);
} else {
ja = jaFromObjectArray(value, context.getConverter());
}
plainText = ja.toBytes();
} else if (cnvs.isSimpleType(sourceType)) { // simpleType
String plainString = value != null ? value.toString() : null;
if ((sourceType == String.class || targetType == String.class) || sourceType == Character.class
|| sourceType == char.class || Enum.class.isAssignableFrom(sourceType)
|| Locale.class.isAssignableFrom(sourceType)) {
// TODO use jackson serializer here
plainString = "\"" + plainString.replaceAll("\"", "\\\"") + "\"";
}
plainText = plainString.getBytes(StandardCharsets.UTF_8);
} else { // an entity
plainText = JsonObject.fromJson(context.read(value).toString().getBytes(StandardCharsets.UTF_8)).toBytes();
}
return plainText;
}
CryptoManager cryptoManager() {
Assert.notNull(cryptoManager,
"cryptoManager needed to encrypt/decrypt but it is null. Override needed for cryptoManager() method of "
+ AbstractCouchbaseConverter.class.getName());
return cryptoManager;
}
JsonArray jaFromObjectArray(Object value, MappingCouchbaseConverter converter) {
CustomConversions cnvs = converter.getConversions();
ConversionService svc = converter.getConversionService();
JsonArray ja = JsonArray.ja();
for (Object o : (Object[]) value) {
ja.add(coerceToJson(o, cnvs, svc));
}
return ja;
}
JsonArray jaFromPrimitiveArray(Object value) {
Class<?> component = value.getClass().getComponentType();
JsonArray jArray;
if (Long.TYPE.isAssignableFrom(component)) {
jArray = ja_long((long[]) value);
} else if (Integer.TYPE.isAssignableFrom(component)) {
jArray = ja_int((int[]) value);
} else if (Double.TYPE.isAssignableFrom(component)) {
jArray = ja_double((double[]) value);
} else if (Float.TYPE.isAssignableFrom(component)) {
jArray = ja_float((float[]) value);
} else if (Boolean.TYPE.isAssignableFrom(component)) {
jArray = ja_boolean((boolean[]) value);
} else if (Short.TYPE.isAssignableFrom(component)) {
jArray = ja_short((short[]) value);
} else if (Byte.TYPE.isAssignableFrom(component)) {
jArray = ja_byte((byte[]) value);
} else if (Character.TYPE.isAssignableFrom(component)) {
jArray = ja_char((char[]) value);
} else {
throw new RuntimeException("unhandled primitive array: " + component.getName());
}
return jArray;
}
JsonArray ja_long(long[] array) {
JsonArray ja = JsonArray.ja();
for (long t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_int(int[] array) {
JsonArray ja = JsonArray.ja();
for (int t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_double(double[] array) {
JsonArray ja = JsonArray.ja();
for (double t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_float(float[] array) {
JsonArray ja = JsonArray.ja();
for (float t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_boolean(boolean[] array) {
JsonArray ja = JsonArray.ja();
for (boolean t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_short(short[] array) {
JsonArray ja = JsonArray.ja();
for (short t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_byte(byte[] array) {
JsonArray ja = JsonArray.ja();
for (byte t : array) {
ja.add(t);
}
return ja;
}
JsonArray ja_char(char[] array) {
JsonArray ja = JsonArray.ja();
for (char t : array) {
ja.add(String.valueOf(t));
}
return ja;
}
Object coerceToJson(Object o, CustomConversions cnvs, ConversionService svc) {
if (o != null && o.getClass() == Optional.class) {
o = ((Optional<?>) o).isEmpty() ? null : ((Optional) o).get();
}
Optional<Class<?>> clazz;
if (o == null) {
o = JsonValue.NULL;
} else if ((clazz = cnvs.getCustomWriteTarget(o.getClass())).isPresent()) {
o = svc.convert(o, clazz.get());
} else if (JsonObject.checkType(o)) {
// The object is of an acceptable type
} else if (Number.class.isAssignableFrom(o.getClass())) {
if (o.toString().contains(".")) {
o = ((Number) o).doubleValue();
} else {
o = ((Number) o).longValue();
}
} else if (Character.class.isAssignableFrom(o.getClass())) {
o = ((Character) o).toString();
} else if (Enum.class.isAssignableFrom(o.getClass())) {
o = ((Enum) o).name();
} else { // punt
o = o.toString();
}
return o;
}
}

View File

@@ -16,7 +16,7 @@
package org.springframework.data.couchbase.core.convert;
import static java.time.ZoneId.*;
import static java.time.ZoneId.systemDefault;
import java.time.Instant;
import java.util.ArrayList;
@@ -26,6 +26,7 @@ import java.util.Date;
import java.util.List;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;
import org.springframework.core.convert.converter.Converter;
@@ -204,7 +205,7 @@ public final class DateConverters {
@Override
public DateTime convert(Number source) {
return source == null ? null : new DateTime(source.longValue());
return source == null ? null : new DateTime(source.longValue(), DateTimeZone.UTC);
}
}

View File

@@ -19,6 +19,7 @@ package org.springframework.data.couchbase.core.convert;
import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.UNIQUE;
import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.USE_ATTRIBUTES;
import java.beans.Transient;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -33,9 +34,10 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbaseList;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -53,6 +55,7 @@ import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.Parameter;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.callback.EntityCallbacks;
@@ -72,6 +75,10 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import com.couchbase.client.java.json.JsonObject;
/**
* A mapping converter for Couchbase. The converter is responsible for reading from and writing to entities and
* converting it into a consumable database representation.
@@ -91,7 +98,6 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* @see #TYPEKEY_SYNCGATEWAY_COMPATIBLE
*/
public static final String TYPEKEY_DEFAULT = DefaultCouchbaseTypeMapper.DEFAULT_TYPE_KEY;
/**
* A "type key" (the name of the field that will hold type information) that is compatible with Sync Gateway (which
* doesn't allows underscores).
@@ -149,6 +155,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
// the conversions Service gets them in afterPropertiesSet()
CustomConversions customConversions = new CouchbaseCustomConversions(Collections.emptyList());
this.setCustomConversions(customConversions);
// Don't rely on setSimpleTypeHolder being called in afterPropertiesSet() - some integration tests do not use it
// if the mappingContext does not have the SimpleTypes, it will not know that they have converters, then it will
// try to access the fields of the type and (maybe) fail with InaccessibleObjectException
((CouchbaseMappingContext) mappingContext).setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
@@ -241,13 +248,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
CouchbasePersistentEntity<R> entity = (CouchbasePersistentEntity<R>) mappingContext
.getRequiredPersistentEntity(typeToUse);
if (source.containsKey("encbooleans"))
System.err.println(source);
return read(entity, source, parent);
}
private boolean isIdConstructionProperty(final CouchbasePersistentProperty property) {
return property.isAnnotationPresent(IdPrefix.class) || property.isAnnotationPresent(IdSuffix.class);
}
/**
* Read an incoming {@link CouchbaseDocument} into the target entity.
*
@@ -266,20 +271,29 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
final R instance = instantiator.createInstance(entity, provider);
final ConvertingPropertyAccessor accessor = getPropertyAccessor(instance);
entity.doWithProperties(new PropertyHandler<CouchbasePersistentProperty>() {
entity.doWithProperties(new PropertyHandler<>() {
@Override
public void doWithPersistentProperty(final CouchbasePersistentProperty prop) {
if (!doesPropertyExistInSource(prop) || entity.isConstructorArgument(prop) || isIdConstructionProperty(prop)
|| prop.isAnnotationPresent(N1qlJoin.class)) {
return;
}
Object obj = prop == entity.getIdProperty() && parent == null ? source.getId()
: getValueInternal(prop, source, instance, entity);
accessor.setProperty(prop, obj);
}
/**
* doesPropertyExistInSource. It could have getFieldName() or mangled(getFieldName())
*
* @param property
* @return
*/
private boolean doesPropertyExistInSource(final CouchbasePersistentProperty property) {
return property.isIdProperty() || source.containsKey(property.getFieldName());
return property.isIdProperty() || source.containsKey(property.getFieldName())
|| source.containsKey(maybeMangle(property));
}
private boolean isIdConstructionProperty(final CouchbasePersistentProperty property) {
@@ -398,10 +412,25 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
throw new MappingException("Unable to create class from " + value.toString());
}
}
return target.isAssignableFrom(value.getClass()) ? value : conversionService.convert(value, target);
}
/**
* Potentially convert simple values like ENUMs.
*
* @param value the value to convert.
* @param target the target persistent property which may have an Encrypt annotation
* @return the potentially converted object.
*/
@SuppressWarnings("unchecked")
protected Object getPotentiallyConvertedSimpleRead(Object value, final CouchbasePersistentProperty target) {
if (value == null || target == null) {
return value;
}
// this call to convert takes TypeDescriptors - the target type descriptor may have an Encrypt annotation.
return conversionService.convert(value, TypeDescriptor.forObject(value), new TypeDescriptor(target.getField()));
}
@Override
public void write(final Object source, final CouchbaseDocument target) {
if (source == null) {
@@ -415,7 +444,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
typeMapper.writeType(type, target);
}
writeInternal(source, target, type, true);
writeInternalRoot(source, target, type, true, null);
if (target.getId() == null) {
throw new MappingException("An ID property is needed, but not found/could not be generated on this entity.");
}
@@ -426,11 +455,12 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*
* @param source the source object.
* @param target the target document.
* @param typeHint the type information for the source.
* @param withId write out with the id.
* @param property will be null for the root
*/
@SuppressWarnings("unchecked")
protected void writeInternal(final Object source, CouchbaseDocument target, final TypeInformation<?> typeHint,
boolean withId) {
protected void writeInternalRoot(final Object source, CouchbaseDocument target, TypeInformation<?> typeHint,
boolean withId, CouchbasePersistentProperty property) {
if (source == null) {
return;
}
@@ -442,7 +472,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
if (Map.class.isAssignableFrom(source.getClass())) {
writeMapInternal((Map<Object, Object>) source, target, ClassTypeInformation.MAP);
writeMapInternal((Map<Object, Object>) source, target, ClassTypeInformation.MAP, property);
return;
}
@@ -451,7 +481,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
writeInternal(source, target, entity, withId);
writeInternalEntity(source, target, entity, withId, property);
addCustomTypeKeyIfNecessary(typeHint, source, target);
}
@@ -487,8 +517,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* @param entity the persistent entity to convert from.
* @param withId one of the top-level properties is the id for the document
*/
protected void writeInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentEntity<?> entity, boolean withId) {
protected void writeInternalEntity(final Object source, final CouchbaseDocument target,
final CouchbasePersistentEntity<?> entity, boolean withId, CouchbasePersistentProperty prop) {
if (source == null) {
return;
}
@@ -508,6 +538,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
target.setExpiration((int) (entity.getExpiryDuration().getSeconds()));
// write all the entity.properties to the target. Does not write the id or version.
writeToTargetDocument(target, entity, accessor, idProperty, versionProperty, prefixes, suffixes, idAttributes);
if (idProperty != null && target.getId() == null) {
@@ -521,6 +552,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
} else {
target.setId(id);
}
}
entity.doWithAssociations(new AssociationHandler<CouchbasePersistentProperty>() {
@@ -530,11 +562,16 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
Class<?> type = inverseProp.getType();
Object propertyObj = accessor.getProperty(inverseProp, type);
if (null != propertyObj) {
writePropertyInternal(propertyObj, target, inverseProp, false);
writePropertyInternal(propertyObj, target, inverseProp, accessor);
}
}
});
if (prop != null && conversions.hasValueConverter(prop)) { // whole entity is encrypted
Map<String, Object> propertyConverted = (Map<String, Object>) conversions.getPropertyValueConversions()
.getValueConverter(prop).write(source, new CouchbaseConversionContext(prop, this, accessor));
target.setContent(JsonObject.from(propertyConverted));
}
}
private void writeToTargetDocument(final CouchbaseDocument target, final CouchbasePersistentEntity<?> entity,
@@ -576,10 +613,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
return;
}
if (!conversions.isSimpleType(propertyObj.getClass())) {
writePropertyInternal(propertyObj, target, prop, false);
if (!conversions.isSimpleType(prop.getType())) {
writePropertyInternal(propertyObj, target, prop, accessor);
} else {
writeSimpleInternal(propertyObj, target, prop.getFieldName());
writeSimpleInternal(prop, accessor, target, prop.getFieldName());
}
}
}
@@ -587,15 +624,15 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
/**
* Helper method to write a property into the target document.
* Helper method to write a non-simple property into the target document.
*
* @param source the source object.
* @param target the target document.
* @param prop the property information.
*/
@SuppressWarnings("unchecked")
private void writePropertyInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentProperty prop, boolean withId) {
protected void writePropertyInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentProperty prop, final ConvertingPropertyAccessor accessor) {
if (source == null) {
return;
}
@@ -603,34 +640,31 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
String name = prop.getFieldName();
TypeInformation<?> valueType = ClassTypeInformation.from(source.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if (valueType.isCollectionLike()) {
CouchbaseList collectionDoc = createCollection(asCollection(source), prop);
target.put(name, collectionDoc);
CouchbaseList collectionDoc = createCollection(asCollection(source), valueType, prop, accessor);
putMaybeEncrypted(target, prop, collectionDoc, accessor);
return;
}
if (valueType.isMap()) {
CouchbaseDocument mapDoc = createMap((Map<Object, Object>) source, prop);
target.put(name, mapDoc);
putMaybeEncrypted(target, prop, mapDoc, accessor);
return;
}
if (conversions.hasValueConverter(prop)) { // property is encrypted
putMaybeEncrypted(target, prop, source, accessor);
return;
}
if (valueType.getType().equals(java.util.Optional.class)) {
if (source == null)
return;
Optional<String> o = (Optional<String>) source;
if (o.isPresent()) {
writeSimpleInternal(o.get(), target, prop.getFieldName());
} else {
writeSimpleInternal(null, target, prop.getFieldName());
}
Optional<?> o = (Optional<?>) source;
writeSimpleInternal(o.map(s -> prop).orElse(null), accessor, target, prop.getFieldName());
return;
}
Optional<Class<?>> basicTargetType = conversions.getCustomWriteTarget(source.getClass());
if (basicTargetType.isPresent()) {
basicTargetType.ifPresent(it -> {
target.put(name, conversionService.convert(source, it));
});
@@ -643,9 +677,18 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
CouchbasePersistentEntity<?> entity = isSubtype(prop.getType(), source.getClass())
? mappingContext.getRequiredPersistentEntity(source.getClass())
: mappingContext.getRequiredPersistentEntity(type);
writeInternal(source, propertyDoc, entity, false);
target.put(name, propertyDoc);
: mappingContext.getRequiredPersistentEntity(prop);
writeInternalEntity(source, propertyDoc, entity, false, prop);
target.put(maybeMangle(prop), propertyDoc);
}
private void putMaybeEncrypted(CouchbaseDocument target, CouchbasePersistentProperty prop, Object value,
ConvertingPropertyAccessor accessor) {
if (conversions.hasValueConverter(prop)) { // property is encrypted
value = conversions.getPropertyValueConversions().getValueConverter(prop).write(value,
new CouchbaseConversionContext(prop, this, accessor));
}
target.put(maybeMangle(prop), value);
}
/**
@@ -659,7 +702,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(prop, "PersistentProperty must not be null!");
return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation());
return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation(), prop);
}
/**
@@ -667,12 +710,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*
* @param source the source object.
* @param target the target document.
* @param type the type information for the document.
* @return the written couchbase document.
*/
private CouchbaseDocument writeMapInternal(final Map<Object, Object> source, final CouchbaseDocument target,
final TypeInformation<?> type) {
for (Map.Entry<Object, Object> entry : source.entrySet()) {
private CouchbaseDocument writeMapInternal(final Map<? extends Object, Object> source, final CouchbaseDocument target,
TypeInformation<?> type, CouchbasePersistentProperty prop) {
for (Map.Entry<? extends Object, Object> entry : source.entrySet()) {
Object key = entry.getKey();
Object val = entry.getValue();
@@ -680,14 +722,14 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
String simpleKey = key.toString();
if (val == null || conversions.isSimpleType(val.getClass())) {
writeSimpleInternal(val, target, simpleKey);
writeSimpleInternal(val, target, simpleKey); // this is an entry in a map, cannot have an annotation
} else if (val instanceof Collection || val.getClass().isArray()) {
target.put(simpleKey, writeCollectionInternal(asCollection(val),
new CouchbaseList(conversions.getSimpleTypeHolder()), type.getMapValueType()));
target.put(simpleKey,
writeCollectionInternal(asCollection(val), new CouchbaseList(conversions.getSimpleTypeHolder()),
prop.getTypeInformation(), prop, getPropertyAccessor(val)));
} else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
TypeInformation<?> valueTypeInfo = type.isMap() ? type.getMapValueType() : ClassTypeInformation.OBJECT;
writeInternal(val, embeddedDoc, valueTypeInfo, false);
writeInternalRoot(val, embeddedDoc, prop.getTypeInformation(), false, prop);
target.put(simpleKey, embeddedDoc);
}
} else {
@@ -702,12 +744,12 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* Helper method to create the underlying collection/list.
*
* @param collection the collection to write.
* @param prop the property information.
* @return the created couchbase list.
*/
private CouchbaseList createCollection(final Collection<?> collection, final CouchbasePersistentProperty prop) {
return writeCollectionInternal(collection, new CouchbaseList(conversions.getSimpleTypeHolder()),
prop.getTypeInformation());
private CouchbaseList createCollection(final Collection<?> collection, final TypeInformation<?> type,
CouchbasePersistentProperty prop, ConvertingPropertyAccessor accessor) {
return writeCollectionInternal(collection, new CouchbaseList(conversions.getSimpleTypeHolder()), type, prop,
accessor);
}
/**
@@ -715,12 +757,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*
* @param source the source object.
* @param target the target document.
* @param type the type information for the document.
* @return the created couchbase list.
*/
private CouchbaseList writeCollectionInternal(final Collection<?> source, final CouchbaseList target,
final TypeInformation<?> type) {
TypeInformation<?> componentType = type == null ? null : type.getComponentType();
final TypeInformation<?> type, CouchbasePersistentProperty prop, ConvertingPropertyAccessor accessor) {
for (Object element : source) {
Class<?> elementType = element == null ? null : element.getClass();
@@ -729,11 +769,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
target.put(getPotentiallyConvertedSimpleWrite(element));
} else if (element instanceof Collection || elementType.isArray()) {
target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()),
componentType));
type, prop, accessor));
} else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
writeInternal(element, embeddedDoc, componentType, false);
writeInternalRoot(element, embeddedDoc, prop.getTypeInformation(), false, prop);
target.put(embeddedDoc);
}
@@ -784,7 +823,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
/**
* Write the given source into the couchbase document target.
*
* @param source the source object.
* @param source the source object. This does not have access to annotaions.
* @param target the target document.
* @param key the key of the object.
*/
@@ -792,8 +831,51 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
target.put(key, getPotentiallyConvertedSimpleWrite(source));
}
/**
* Write the given source into the couchbase document target.
*
* @param source the source persistent property. This has access to annotations.
* @param target the target document.
* @param key the key of the object.
*/
private void writeSimpleInternal(final CouchbasePersistentProperty source,
final ConvertingPropertyAccessor<Object> accessor, final CouchbaseDocument target, final String key) {
Object result = getPotentiallyConvertedSimpleWrite(source, accessor);
if (result instanceof Optional) {
Optional<Object> optional = (Optional) result;
result = optional.orElse(null);
}
target.put(maybeMangle(source), result);
}
public Object getPotentiallyConvertedSimpleWrite(final Object value) {
return convertForWriteIfNeeded(value);
return convertForWriteIfNeeded(value); // cannot access annotations
}
/**
* This does process PropertyValueConversions
*
* @param value
* @param accessor
* @return
*/
@Deprecated
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty value,
ConvertingPropertyAccessor<Object> accessor) {
return convertForWriteIfNeeded(value, accessor, true); // can access annotations
}
/**
* This does process PropertyValueConversions
*
* @param value
* @param accessor
* @param processValueConverter
* @return
*/
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty value,
ConvertingPropertyAccessor<Object> accessor, boolean processValueConverter) {
return convertForWriteIfNeeded(value, accessor, processValueConverter); // can access annotations
}
/**
@@ -845,7 +927,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* @return the converted object.
*/
@SuppressWarnings("unchecked")
private <R> R readValue(Object value, TypeInformation<?> type, Object parent) {
private <R> R readValue(Object value, TypeInformation type, Object parent) {
Class<?> rawType = type.getType();
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
@@ -855,7 +937,34 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
} else if (value instanceof CouchbaseList) {
return (R) readCollection(type, (CouchbaseList) value, parent);
} else {
return (R) getPotentiallyConvertedSimpleRead(value, rawType);
return (R) getPotentiallyConvertedSimpleRead(value, type.getType()); // type does not have annotations
}
}
/**
* Helper method to read the value based on the PersistentProperty
*
* @param value the value to convert.
* @param prop the persistent property - will have annotations (i.e. Encrypt for FLE)
* @param parent the optional parent.
* @param <R> the target type.
* @return the converted object.
*/
@SuppressWarnings("unchecked")
public <R> R readValue(Object value, CouchbasePersistentProperty prop, Object parent, boolean noDecrypt) {
Class<?> rawType = prop.getType();
if (conversions.hasValueConverter(prop) && !noDecrypt) {
return (R) conversions.getPropertyValueConversions().getValueConverter(prop).read(value,
new CouchbaseConversionContext(prop, this, null));
} else if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
TypeInformation ti = ClassTypeInformation.from(value.getClass());
return (R) conversionService.convert(value, ti.toTypeDescriptor(), new TypeDescriptor(prop.getField()));
} else if (value instanceof CouchbaseDocument) {
return (R) read(prop.getTypeInformation(), (CouchbaseDocument) value, parent);
} else if (value instanceof CouchbaseList) {
return (R) readCollection(prop.getTypeInformation(), (CouchbaseList) value, parent);
} else {
return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations
}
}
@@ -960,7 +1069,23 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
@SuppressWarnings("unchecked")
public <R> R getPropertyValue(final CouchbasePersistentProperty property) {
String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
String maybeFieldName = maybeMangle(property);
Object value = expression != null ? evaluator.evaluate(expression) : source.get(maybeFieldName);
boolean noDecrypt = false;
// handle @Encrypted FROM_UNENCRYPTED. Just accept them as-is.
if (property.findAnnotation(Encrypted.class) != null) {
if (value == null && !maybeFieldName.equals(property.getFieldName())
&& property.findAnnotation(Encrypted.class).migration().equals(Encrypted.Migration.FROM_UNENCRYPTED)) {
value = source.get(property.getFieldName());
noDecrypt = true;
} else if (value != null
&& !((value instanceof CouchbaseDocument) && (((CouchbaseDocument) value)).containsKey("kid"))) {
noDecrypt = true;
// TODO - should we throw an exception, or just ignore the problem of not being encrypted with noDecrypt=true?
throw new RuntimeException("should have been encrypted, but is not " + maybeFieldName);
}
}
if (property == entity.getIdProperty() && parent == null) {
return readValue(source.getId(), property.getTypeInformation(), source);
@@ -968,11 +1093,24 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
if (value == null) {
return null;
}
return readValue(value, property.getTypeInformation(), source);
return readValue(value, property, source, noDecrypt);
}
}
String maybeMangle(PersistentProperty<?> property) {
Assert.notNull(property, "property");
if (!conversions.hasValueConverter(property)) {
return ((CouchbasePersistentProperty) property).getFieldName();
}
PropertyValueConverter<?, ?, ?> propertyValueConverter = conversions.getPropertyValueConversions()
.getValueConverter((CouchbasePersistentProperty) property);
CryptoManager cryptoManager = propertyValueConverter != null && propertyValueConverter instanceof CryptoConverter
? ((CryptoConverter) propertyValueConverter).cryptoManager()
: null;
String fname = ((CouchbasePersistentProperty) property).getFieldName();
return cryptoManager != null ? cryptoManager.mangle(fname) : fname;
}
/**
* A expression parameter value provider.
*/

View File

@@ -18,6 +18,7 @@ package org.springframework.data.couchbase.core.convert;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@@ -26,6 +27,7 @@ import java.util.UUID;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.util.Base64Utils;
/**
* Out of the box conversions for java dates and calendars.
@@ -50,6 +52,12 @@ public final class OtherConverters {
converters.add(StringToBigInteger.INSTANCE);
converters.add(BigDecimalToString.INSTANCE);
converters.add(StringToBigDecimal.INSTANCE);
converters.add(ByteArrayToString.INSTANCE);
converters.add(StringToByteArray.INSTANCE);
converters.add(CharArrayToString.INSTANCE);
converters.add(StringToCharArray.INSTANCE);
converters.add(ClassToString.INSTANCE);
converters.add(StringToClass.INSTANCE);
return converters;
}
@@ -113,4 +121,70 @@ public final class OtherConverters {
return source == null ? null : new BigDecimal(source);
}
}
@WritingConverter
public enum ByteArrayToString implements Converter<byte[], String> {
INSTANCE;
@Override
public String convert(byte[] source) {
return source == null ? null : Base64Utils.encodeToString(source);
}
}
@ReadingConverter
public enum StringToByteArray implements Converter<String, byte[]> {
INSTANCE;
@Override
public byte[] convert(String source) {
return source == null ? null : Base64Utils.decode(source.getBytes(StandardCharsets.UTF_8));
}
}
@WritingConverter
public enum CharArrayToString implements Converter<char[], String> {
INSTANCE;
@Override
public String convert(char[] source) {
return source == null ? null : new String(source) ;
}
}
@ReadingConverter
public enum StringToCharArray implements Converter<String, char[]> {
INSTANCE;
@Override
public char[] convert(String source) {
return source == null ? null : source.toCharArray();
}
}
@WritingConverter
public enum ClassToString implements Converter<Class<?>, String> {
INSTANCE;
@Override
public String convert(Class<?> source) {
return source == null ? null : source.getClass().getName() ;
}
}
@ReadingConverter
public enum StringToClass implements Converter<String, Class<?>> {
INSTANCE;
@Override
public Class<?> convert(String source) {
try {
return source == null ? null : Class.forName(source);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -16,8 +16,15 @@
package org.springframework.data.couchbase.core.mapping;
import static java.util.stream.Collectors.*;
import static java.util.stream.Collectors.toSet;
import java.math.BigInteger;
import java.time.Instant;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.springframework.data.mapping.model.SimpleTypeHolder;
@@ -35,4 +42,39 @@ public abstract class CouchbaseSimpleTypes {
private CouchbaseSimpleTypes() {}
public static final Set<Class<?>> AUTOGENERATED_ID_TYPES;
static {
Set<Class<?>> classes = new HashSet<>();
classes.add(String.class);
classes.add(BigInteger.class);
AUTOGENERATED_ID_TYPES = Collections.unmodifiableSet(classes);
Set<Class<?>> simpleTypes = new HashSet<>();
simpleTypes.add(Pattern.class);
simpleTypes.add(UUID.class);
simpleTypes.add(Instant.class);
COUCHBASE_SIMPLE_TYPES = Collections.unmodifiableSet(simpleTypes);
}
private static final Set<Class<?>> COUCHBASE_SIMPLE_TYPES;
public static final SimpleTypeHolder HOLDER = new SimpleTypeHolder(COUCHBASE_SIMPLE_TYPES, true) {
@Override
public boolean isSimpleType(Class<?> type) {
if (type.isEnum()) {
return true;
}
if (type.getName().startsWith("java.time")) {
return false;
}
return super.isSimpleType(type);
}
};
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.data.couchbase.domain;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import org.springframework.data.couchbase.core.mapping.Document;
@Document

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2020-2021 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import org.springframework.data.couchbase.core.mapping.Document;
import com.couchbase.client.java.encryption.annotation.Encrypted;
@Document
public class AddressWithEncStreet extends Address {
private @Encrypted String encStreet;
public AddressWithEncStreet() {}
public String getEncStreet() {
return encStreet;
}
public void setEncStreet(String encStreet) {
this.encStreet = encStreet;
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* UserEncrypted entity for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Document
public class TestEncrypted implements Serializable {
public String id;
@Encrypted
public byte[] encString={1,2,3,4};
public TestEncrypted() {
}
public TestEncrypted(final String id) {
this();
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String toString(){
StringBuffer sb=new StringBuffer();
sb.append("encString: "+encToString());
return sb.toString();
}
@Override
public int hashCode() {
return Objects.hash(id);
}
public void initSimpleTypes(){
}
@Override public boolean equals(Object o){
if(o == null || o.getClass() != getClass()){
return false;
}
TestEncrypted other = (TestEncrypted) o;
//return this.encString == other.encString;
if(other.encString == null && this.encString != null)
return false;
return other.encString.equals(this.encString);
}
public String encToString(){
StringBuffer sb = new StringBuffer();
for(byte c:encString){
if(!sb.isEmpty())
sb.append(",");
sb.append(c);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,260 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* UserEncrypted entity for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Document
@TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER)
public class UserEncrypted extends AbstractUser implements Serializable {
public UserEncrypted() {
this._class = "abstractuser";
this.subtype = AbstractingTypeMapper.Type.USER;
}
public String _class; // cheat a little so that will work with Java SDK
@PersistenceConstructor
public UserEncrypted(final String id, final String firstname, final String lastname) {
this();
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
}
public UserEncrypted(final String id, final String firstname, final String lastname, final String encryptedField) {
this();
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.encryptedField = encryptedField;
}
static DateTime NOW_DateTime = DateTime.now(DateTimeZone.UTC);
static Date NOW_Date = Date.from(Instant.now());
@Version protected long version;
@Encrypted(migration = Encrypted.Migration.FROM_UNENCRYPTED) public String encryptedField;
@Encrypted public boolean encboolean;
@Encrypted public boolean[] encbooleans;
@Encrypted public Boolean encBoolean;
@Encrypted public Boolean[] encBooleans;
@Encrypted public long enclong;
@Encrypted public long[] enclongs;
@Encrypted public Long encLong;
@Encrypted public Long[] encLongs;
@Encrypted public short encshort;
@Encrypted public short[] encshorts;
@Encrypted public Short encShort;
@Encrypted public Short[] encShorts;
@Encrypted public int encinteger;
@Encrypted public int[] encintegers;
@Encrypted public Integer encInteger;
@Encrypted public Integer[] encIntegers;
@Encrypted public byte encbyte;
public byte[] plainbytes;
@Encrypted public byte[] encbytes;
@Encrypted public Byte encByte;
@Encrypted public Byte[] encBytes;
@Encrypted public float encfloat;
@Encrypted public float[] encfloats;
@Encrypted public Float encFloat;
@Encrypted public Float[] encFloats;
@Encrypted public double encdouble;
@Encrypted public double[] encdoubles;
@Encrypted public Double encDouble;
@Encrypted public Double[] encDoubles;
@Encrypted public char encchar='x'; // need to initialize as char(0) is not legal
@Encrypted public char[] encchars;
@Encrypted public Character encCharacter;
@Encrypted public Character[] encCharacters;
@Encrypted public String encString;
@Encrypted public String[] encStrings;
@Encrypted public Date encDate;
@Encrypted public Date[] encDates;
@Encrypted public Locale encLocal;
@Encrypted public Locale[] encLocales;
@Encrypted public QueryScanConsistency encEnum;
@Encrypted public QueryScanConsistency[] encEnums;
@Encrypted public Class<?> clazz;
@Encrypted public BigInteger encBigInteger;
@Encrypted public BigDecimal encBigDecimal;
@Encrypted public UUID encUUID;
@Encrypted public DateTime encDateTime;
@Encrypted public Address encAddress = new Address();
public Date plainDate;
public DateTime plainDateTime;
public List nicknames;
public Address homeAddress = null;
public List<AddressWithEncStreet> addresses = new ArrayList<>();
public String getLastname() {
return lastname;
}
public long getVersion() {
return version;
}
public void setId(String id) {
this.id = id;
}
public void setVersion(long version) {
this.version = version;
}
public void setHomeAddress(Address address) {
this.homeAddress = address;
}
public void setEncAddress(Address address) {
this.encAddress = address;
}
public void addAddress(AddressWithEncStreet address) {
this.addresses.add(address);
}
public UserEncrypted withClass(String _class) {
this._class = _class;
return this;
}
@Override
public int hashCode() {
return Objects.hash(getId(), firstname, lastname);
}
public void initSimpleTypes() {
encboolean = false;
encbooleans = new boolean[] { true, false };
encBoolean = true;
encBooleans = new Boolean[] { true, false };
enclong = 1;
enclongs = new long[] { 1, 2 };
encLong = Long.valueOf(1);
encLongs = new Long[] { Long.valueOf(1), Long.valueOf(2) };
encshort = 1;
encshorts = new short[] { 3, 4 };
encShort = 5;
encShorts = new Short[] { 6, 7 };
encinteger = 1;
encintegers = new int[] { 2, 3 };
encInteger = 4;
encIntegers = new Integer[] { 5, 6 };
encbyte = 32;
encbytes = new byte[] { 1, 2, 3, 4 };
plainbytes = new byte[] { 1, 2, 3, 4 };
encByte = 48;
encBytes = new Byte[] { 4, 5, 6, 7 };
encfloat = 1;
encfloats = new float[] { 1, 2 };
encFloat = Float.valueOf("1.1");
encFloats = new Float[] { encFloat };
encdouble = 1.2;
encdoubles = new double[] { 3.4, 5.6 };
encDouble = 7.8;
encDoubles = new Double[] { 9.10, 11.12 };
encchar = 'a';
encchars = new char[] { 'b', 'c', 'd' };
encCharacter = 'a';
encCharacters = new Character[] { 'a', 'b' };
encString = "myString";
encStrings = new String[] { "myString" };
encDate = NOW_Date;
encDates = new Date[] { NOW_Date };
encLocal = Locale.US;
encLocales = new Locale[] { Locale.US };
encEnum = QueryScanConsistency.NOT_BOUNDED;
encEnums = new QueryScanConsistency[] { QueryScanConsistency.NOT_BOUNDED };
encBigInteger = new BigInteger("123");
encBigDecimal = new BigDecimal("456");
encUUID = UUID.fromString("00000000-0000-0000-0000-000000000000");
//clazz = String.class;
encDateTime = NOW_DateTime;
encAddress = new Address();
plainDate = NOW_Date;
plainDateTime = NOW_DateTime;
nicknames = List.of("Happy", "Sleepy");
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.couchbase.repository.ScanConsistency;
import org.springframework.data.couchbase.repository.Scope;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* User Repository for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Repository
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
public interface UserEncryptedRepository extends CouchbaseRepository<UserEncrypted, String> {
List<User> findByFirstname(String firstname);
List<User> findByFirstnameIgnoreCase(String firstname);
Stream<User> findByLastname(String lastname);
List<User> findByFirstnameIn(String... firstnames);
List<User> findByFirstnameIn(JsonArray firstnames);
List<User> findByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2")
List<User> getByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)")
List<User> getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname);
List<User> findByIdIsNotNullAndFirstnameEquals(String firstname);
List<User> findByVersionEqualsAndFirstnameEquals(Long version, String firstname);
@Query("#{#n1ql.selectEntity}|#{#n1ql.filter}|#{#n1ql.bucket}|#{#n1ql.scope}|#{#n1ql.collection}")
@Scope("thisScope")
@Collection("thisCollection")
List<User> spelTests();
// simulate a slow operation
@Cacheable("mySpringCache")
default List<User> getByFirstname(String firstname) {
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException ie) {}
return findByFirstname(firstname);
}
@Override
UserEncrypted save(UserEncrypted user);
}

View File

@@ -0,0 +1,417 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.junit.Assert.assertNull;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.data.couchbase.CouchbaseClientFactory;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.AddressWithEncStreet;
import org.springframework.data.couchbase.domain.TestEncrypted;
import org.springframework.data.couchbase.domain.UserEncrypted;
import org.springframework.data.couchbase.domain.UserEncryptedRepository;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.core.env.SecurityConfig;
import com.couchbase.client.encryption.AeadAes256CbcHmacSha512Provider;
import com.couchbase.client.encryption.DefaultCryptoManager;
import com.couchbase.client.encryption.Keyring;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JsonObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.joda.JodaModule;
/**
* Repository KV tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@SpringJUnitConfig(CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.Config.class)
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
public class CouchbaseRepositoryFieldLevelEncryptionIntegrationTests extends ClusterAwareIntegrationTests {
@Autowired UserEncryptedRepository userEncryptedRepository;
@Autowired CouchbaseClientFactory clientFactory;
@Autowired CouchbaseTemplate couchbaseTemplate;
@BeforeEach
public void beforeEach() {
super.beforeEach();
List<UserEncrypted> users = couchbaseTemplate.findByQuery(UserEncrypted.class).withConsistency(REQUEST_PLUS).all();
for (UserEncrypted user : users) {
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId());
try { // may have also used upperCased-id
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId().toUpperCase());
} catch (DataRetrievalFailureException iae) {
// ignore
}
}
couchbaseTemplate.removeByQuery(UserEncrypted.class).all();
couchbaseTemplate.findByQuery(UserEncrypted.class).withConsistency(REQUEST_PLUS).all();
}
@Test
void javaSDKEncryption() {
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void saveAndFindByTestId() {
TestEncrypted user = new TestEncrypted(UUID.randomUUID().toString());
user.initSimpleTypes();
couchbaseTemplate.save(user);
TestEncrypted writeSpringReadSpring = couchbaseTemplate.findById(TestEncrypted.class).one(user.id);
System.err.println(user);
System.err.println(writeSpringReadSpring);
assertEquals(user.toString(), writeSpringReadSpring.toString());
TestEncrypted writeSpringReadSDK = clientFactory.getCluster().bucket(config().bucketname()).defaultCollection()
.get(user.id).contentAs(TestEncrypted.class);
writeSpringReadSDK.setId(user.id);
assertEquals(user.toString(), writeSpringReadSDK.toString());
clientFactory.getCluster().bucket(config().bucketname()).defaultCollection().insert(user.getId().toUpperCase(),
user);
TestEncrypted writeSDKReadSDK = clientFactory.getCluster().bucket(config().bucketname()).defaultCollection()
.get(user.getId().toUpperCase()).contentAs(TestEncrypted.class);
writeSDKReadSDK.setId(user.getId());
assertEquals(user.toString(), writeSDKReadSDK.toString());
TestEncrypted writeSDKReadSpring = couchbaseTemplate.findById(TestEncrypted.class).one(user.getId().toUpperCase());
writeSDKReadSpring.setId(user.getId());
assertEquals(user.toString(), writeSDKReadSpring.toString());
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void writeSpring_readSpring() {
boolean cleanAfter = false;
UserEncrypted user = new UserEncrypted(UUID.randomUUID().toString(), "writeSpring_readSpring", "l", "hello");
AddressWithEncStreet address = new AddressWithEncStreet(); // plaintext address with encrypted street
address.setEncStreet("Olcott Street");
address.setCity("Santa Clara");
user.addAddress(address);
user.setHomeAddress(null);
Address encAddress = new Address(); // encrypted address with plaintext street.
encAddress.setStreet("Castro St");
encAddress.setCity("Mountain View");
user.setEncAddress(encAddress);
user.initSimpleTypes();
// save the user with spring
assertFalse(userEncryptedRepository.existsById(user.getId()));
userEncryptedRepository.save(user);
// read the user with Spring
Optional<UserEncrypted> writeSpringReadSpring = userEncryptedRepository.findById(user.getId());
assertTrue(writeSpringReadSpring.isPresent());
writeSpringReadSpring.ifPresent(u -> assertEquals(user, u));
if (cleanAfter) {
try {
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId());
} catch (DataRetrievalFailureException iae) {
// ignore
}
}
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void writeSpring_readSDK() {
boolean cleanAfter = false;
UserEncrypted user = new UserEncrypted(UUID.randomUUID().toString(), "writeSpring_readSDK", "l", "hello");
AddressWithEncStreet address = new AddressWithEncStreet(); // plaintext address with encrypted street
address.setEncStreet("Olcott Street");
address.setCity("Santa Clara");
user.addAddress(address);
user.setHomeAddress(null);
Address encAddress = new Address(); // encrypted address with plaintext street.
encAddress.setStreet("Castro St");
encAddress.setCity("Mountain View");
user.setEncAddress(encAddress);
user.initSimpleTypes();
// save the user with spring
assertFalse(userEncryptedRepository.existsById(user.getId()));
userEncryptedRepository.save(user);
// read user with SDK
UserEncrypted writeSpringReadSDK = clientFactory.getCluster().bucket(config().bucketname()).defaultCollection()
.get(user.getId()).contentAs(UserEncrypted.class);
writeSpringReadSDK.setId(user.getId());
writeSpringReadSDK.setVersion(user.getVersion());
assertEquals(user, writeSpringReadSDK);
if (cleanAfter) {
try {
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId());
} catch (DataRetrievalFailureException iae) {
// ignore
}
}
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void writeSDK_readSpring() {
boolean cleanAfter = false;
UserEncrypted user = new UserEncrypted(UUID.randomUUID().toString(), "writeSDK_readSpring", "l", "hello");
AddressWithEncStreet address = new AddressWithEncStreet(); // plaintext address with encrypted street
address.setEncStreet("Olcott Street");
address.setCity("Santa Clara");
user.addAddress(address);
user.setHomeAddress(null);
Address encAddress = new Address(); // encrypted address with plaintext street.
encAddress.setStreet("Castro St");
encAddress.setCity("Mountain View");
user.setEncAddress(encAddress);
user.initSimpleTypes();
// save the user with the SDK
assertFalse(userEncryptedRepository.existsById(user.getId().toUpperCase()));
clientFactory.getCluster().bucket(config().bucketname()).defaultCollection().insert(user.getId().toUpperCase(),
user);
Optional<UserEncrypted> writeSDKReadSpring = userEncryptedRepository.findById(user.getId().toUpperCase());
assertTrue(writeSDKReadSpring.isPresent());
writeSDKReadSpring.get().setId(user.getId());
writeSDKReadSpring.get().setVersion(user.getVersion());
writeSDKReadSpring.ifPresent(u -> assertEquals(user, u));
if (cleanAfter) {
try {
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId().toUpperCase());
} catch (DataRetrievalFailureException iae) {
// ignore
}
}
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void writeSDK_readSDK() {
boolean cleanAfter = false;
UserEncrypted user = new UserEncrypted(UUID.randomUUID().toString(), "writeSDK_readSDK", "l", "hello");
AddressWithEncStreet address = new AddressWithEncStreet(); // plaintext address with encrypted street
address.setEncStreet("Olcott Street");
address.setCity("Santa Clara");
user.addAddress(address);
user.setHomeAddress(null);
Address encAddress = new Address(); // encrypted address with plaintext street.
encAddress.setStreet("Castro St");
encAddress.setCity("Mountain View");
user.setEncAddress(encAddress);
user.clazz = String.class; // not supported by SDK, but not support by UserEncrypted.toString() either.
user.initSimpleTypes();
// write the user with the SDK
assertFalse(userEncryptedRepository.existsById(user.getId().toUpperCase()));
clientFactory.getCluster().bucket(config().bucketname()).defaultCollection().insert(user.getId().toUpperCase(),
user);
// read the user with the SDK
UserEncrypted writeSDKReadSDK = clientFactory.getCluster().bucket(config().bucketname()).defaultCollection()
.get(user.getId().toUpperCase()).contentAs(UserEncrypted.class);
writeSDKReadSDK.setId(user.getId());
writeSDKReadSDK.setVersion(user.getVersion());
assertEquals(user.clazz, writeSDKReadSDK.clazz);
writeSDKReadSDK.clazz = null; // null these out as UserEncrypted.toString() doesn't support them.
user.clazz = null;
assertEquals(user, writeSDKReadSDK);
if (cleanAfter) {
try {
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId().toUpperCase());
} catch (DataRetrievalFailureException iae) {
// ignore
}
}
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void testFromMigration() {
boolean cleanAfter = true;
UserEncrypted user = new UserEncrypted(UUID.randomUUID().toString(), "testFromMigration", "l",
"migrating from unencrypted");
JsonObject jo = JsonObject.jo();
jo.put("firstname", user.getFirstname());
jo.put("lastname", user.getLastname());
jo.put("encryptedField", user.encryptedField);
jo.put("_class", user._class);
// save it unencrypted
clientFactory.getCluster().bucket(config().bucketname()).defaultCollection().insert(user.getId(), jo);
JsonObject migration = clientFactory.getCluster().bucket(config().bucketname()).defaultCollection()
.get(user.getId()).contentAsObject();
assertEquals("migrating from unencrypted", migration.get("encryptedField"));
assertNull(migration.get(CryptoManager.DEFAULT_ENCRYPTER_ALIAS + "encryptedField"));
// it will be retrieved successfully
Optional<UserEncrypted> found = userEncryptedRepository.findById(user.getId());
assertTrue(found.isPresent());
user.setVersion(found.get().getVersion());
found.ifPresent(u -> assertEquals(user, u));
// save it encrypted
UserEncrypted saved = userEncryptedRepository.save(user);
// it will be retrieved successfully
Optional<UserEncrypted> foundEnc = userEncryptedRepository.findById(user.getId());
assertTrue(foundEnc.isPresent());
user.setVersion(foundEnc.get().getVersion());
foundEnc.ifPresent(u -> assertEquals(user, u));
// retrieve it without decrypting
JsonObject encrypted = clientFactory.getCluster().bucket(config().bucketname()).defaultCollection()
.get(user.getId()).contentAsObject();
assertEquals("myKey",
((JsonObject) encrypted.get(CryptoManager.DEFAULT_ENCRYPTED_FIELD_NAME_PREFIX + "encryptedField")).get("kid"));
assertNull(encrypted.get("encryptedField"));
if (cleanAfter) {
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId());
try { // may have also used upperCased-id
couchbaseTemplate.removeById(UserEncrypted.class).one(user.getId().toUpperCase());
} catch (DataRetrievalFailureException iae) {
// ignore
}
}
}
@Configuration
@EnableCouchbaseRepositories("org.springframework.data.couchbase")
static class Config extends AbstractCouchbaseConfiguration {
@Override
public String getConnectionString() {
return connectionString();
}
@Override
public String getUserName() {
return config().adminUsername();
}
@Override
public String getPassword() {
return config().adminPassword();
}
@Override
public String getBucketName() {
return bucketName();
}
@Override
public ObjectMapper couchbaseObjectMapper(CryptoManager cryptoManager) {
ObjectMapper om = super.couchbaseObjectMapper(cryptoManager);
om.registerModule(new JodaModule());
return om;
}
@Override
protected void configureEnvironment(ClusterEnvironment.Builder builder) {
if (config().isUsingCloud()) {
builder.securityConfig(
SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true));
}
CryptoManager cryptoManager = cryptoManager();
builder.cryptoManager(cryptoManager).build();
}
@Override
protected CryptoManager cryptoManager() {
Map<String, byte[]> keyMap = new HashMap();
keyMap.put("myKey", new byte[64] /* all zeroes */);
Keyring keyring = Keyring.fromMap(keyMap);
AeadAes256CbcHmacSha512Provider provider = AeadAes256CbcHmacSha512Provider.builder().keyring(keyring)
/*.securityProvider(secProvider)*/.build();
return new WrappingCryptoManager(DefaultCryptoManager.builder().decrypter(provider.decrypter())
.defaultEncrypter(provider.encrypterForKey("myKey")).build());
}
public class WrappingCryptoManager implements CryptoManager {
CryptoManager cryptoManager;
public WrappingCryptoManager(CryptoManager cryptoManager) {
this.cryptoManager = cryptoManager;
}
@Override
public Map<String, Object> encrypt(byte[] plaintext, String encrypterAlias) {
Map<String, Object> encryptedNode = cryptoManager.encrypt(plaintext, encrypterAlias);
return encryptedNode;
}
@Override
public byte[] decrypt(Map<String, Object> encryptedNode) {
byte[] result = cryptoManager.decrypt(encryptedNode);
return result;
}
private String toBytes(byte[] plaintext) {
StringBuffer sb = new StringBuffer();
for (byte b : plaintext) {
sb.append(b);
sb.append(" ");
}
return sb.toString();
}
private boolean cmp(byte[] a, byte[] b) {
if (a.length != b.length)
return false;
for (int i = 0; i < a.length; i++) {
if (a[i] != b[i])
return false;
}
return true;
}
byte[] canned_sdk_encbytes = { 34, 65, 81, 73, 68, 66, 65, 61, 61, 34 };
byte[] canned_spring_encbytes = { 91, 49, 44, 50, 44, 51, 44, 52, 93 };
}
}
}