FLE implementation with property value converter. (#1554)
* FLE Implemenation with Property Value Converter.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user