Add suport for JsonValue annotation on Enums and JsonValue/JsonCreator otherwise. (#1618)

Closes #1617.
This commit is contained in:
Michael Reiche
2022-12-15 13:31:19 -08:00
committed by GitHub
parent 7a05754161
commit 799b7dbd30
24 changed files with 1128 additions and 87 deletions

View File

@@ -18,11 +18,16 @@ package org.springframework.data.couchbase.config;
import static com.couchbase.client.java.ClusterOptions.clusterOptions;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import com.fasterxml.jackson.annotation.JsonValue;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
@@ -37,9 +42,15 @@ 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.BooleanToEnumConverterFactory;
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.IntegerToEnumConverterFactory;
import org.springframework.data.couchbase.core.convert.JsonValueConverter;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.OtherConverters;
import org.springframework.data.couchbase.core.convert.StringToEnumConverterFactory;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -60,7 +71,6 @@ import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.core.env.Authenticator;
import com.couchbase.client.core.env.PasswordAuthenticator;
@@ -72,6 +82,7 @@ import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JacksonTransformers;
import com.couchbase.client.java.json.JsonValueModule;
import com.couchbase.client.java.query.QueryScanConsistency;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
@@ -87,8 +98,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@Configuration
public abstract class AbstractCouchbaseConfiguration {
ObjectMapper mapper;
CryptoManager cryptoManager = null;
volatile ObjectMapper objectMapper;
volatile CryptoManager cryptoManager = null;
/**
* The connection string which allows the SDK to connect to the cluster.
@@ -157,7 +168,7 @@ public abstract class AbstractCouchbaseConfiguration {
if (!nonShadowedJacksonPresent()) {
throw new CouchbaseException("non-shadowed Jackson not present");
}
builder.jsonSerializer(JacksonJsonSerializer.create(getCouchbaseObjectMapper()));
builder.jsonSerializer(JacksonJsonSerializer.create(getObjectMapper()));
builder.cryptoManager(getCryptoManager());
configureEnvironment(builder);
return builder.build();
@@ -277,10 +288,12 @@ public abstract class AbstractCouchbaseConfiguration {
@Bean
public TranslationService couchbaseTranslationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.setObjectMapper(getCouchbaseObjectMapper());
jacksonTranslationService.setObjectMapper(getObjectMapper());
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);
JacksonTransformers.MAPPER.configure(
com.couchbase.client.core.deps.com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
false);
return jacksonTranslationService;
}
@@ -298,21 +311,26 @@ public abstract class AbstractCouchbaseConfiguration {
return mappingContext;
}
private ObjectMapper getCouchbaseObjectMapper() {
if (mapper != null) {
return mapper;
final public ObjectMapper getObjectMapper() {
if(objectMapper == null) {
synchronized (this) {
if (objectMapper == null) {
objectMapper = couchbaseObjectMapper();
}
}
}
return mapper = couchbaseObjectMapper();
return objectMapper;
}
/**
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment and spring-data-couchbase
* jacksonTranslationService and also some converters (EnumToObject, StringToEnum, IntegerToEnum)
*
* @return ObjectMapper
*/
public ObjectMapper couchbaseObjectMapper() {
ObjectMapper om = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
om.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
protected ObjectMapper couchbaseObjectMapper() {
ObjectMapper om = new ObjectMapper();
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
om.registerModule(new JsonValueModule());
if (getCryptoManager() != null) {
om.registerModule(new EncryptionModule(getCryptoManager()));
@@ -400,20 +418,35 @@ public abstract class AbstractCouchbaseConfiguration {
List<GenericConverter> newConverters = new ArrayList();
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager, annotationToConverterMap()));
valueConversions.setValueConverterRegistry(new PropertyValueConverterRegistrar().buildRegistry());
valueConversions.afterPropertiesSet(); // wraps the CouchbasePropertyValueConverterFactory with CachingPVCFactory
configurationAdapter.setPropertyValueConversions(valueConversions);
configurationAdapter.registerConverters(newConverters);
configurationAdapter.registerConverter(new OtherConverters.EnumToObject(getObjectMapper()));
configurationAdapter.registerConverterFactory(new IntegerToEnumConverterFactory(getObjectMapper()));
configurationAdapter.registerConverterFactory(new StringToEnumConverterFactory(getObjectMapper()));
configurationAdapter.registerConverterFactory(new BooleanToEnumConverterFactory(getObjectMapper()));
});
return customConversions;
}
Map<Class<? extends Annotation>,Class<?>> annotationToConverterMap(){
Map<Class<? extends Annotation>,Class<?>> map= new HashMap();
map.put(Encrypted.class, CryptoConverter.class);
map.put(JsonValue.class, JsonValueConverter.class);
return map;
}
/**
* cryptoManager can be null, so it cannot be a bean and then used as an arg for bean methods
*/
private CryptoManager getCryptoManager() {
if (cryptoManager == null) {
cryptoManager = cryptoManager();
if(cryptoManager == null) {
synchronized (this) {
if (cryptoManager == null) {
cryptoManager = cryptoManager();
}
}
}
return cryptoManager;
}

View File

@@ -117,10 +117,8 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
return null;
}
if (processValueConverter && conversions.hasValueConverter(prop)) {
CouchbaseDocument encrypted = (CouchbaseDocument) conversions.getPropertyValueConversions()
.getValueConverter(prop)
.write(value, new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
return encrypted;
return conversions.getPropertyValueConversions().getValueConverter(prop).write(value,
new CouchbaseConversionContext(prop, (MappingCouchbaseConverter) this, accessor));
}
Class<?> targetClass = this.conversions.getCustomWriteTarget(value.getClass()).orElse(null);
@@ -134,7 +132,9 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
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);
.orElse(value);
// superseded by Enum converters
// .orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
return result;
@@ -160,7 +160,7 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
Class<?> elementType = value.getClass();
if (elementType == null || conversions.isSimpleType(elementType)) {
value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
// superseded by EnumCvtrs value = Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
} else if (value instanceof Collection || elementType.isArray()) {
TypeInformation<?> type = ClassTypeInformation.from(value.getClass());
value = ((MappingCouchbaseConverter) this).writeCollectionInternal(MappingCouchbaseConverter.asCollection(value),

View File

@@ -0,0 +1,83 @@
package org.springframework.data.couchbase.core.convert;
/*
* 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.
*/
import java.io.IOException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.couchbase.client.core.encryption.CryptoManager;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Reading Converter factory for Enums. This differs from the one provided in org.springframework.core.convert.support
* by getting the result from the jackson objectmapper (which will process @JsonValue annotations) This is registered in
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
*
* @author Michael Reiche
*/
@ReadingConverter
public class BooleanToEnumConverterFactory implements ConverterFactory<Boolean, Enum> {
private final ObjectMapper objectMapper;
public BooleanToEnumConverterFactory(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public <T extends Enum> Converter<Boolean, T> getConverter(Class<T> targetType) {
return new BooleanToEnum(getEnumType(targetType), objectMapper);
}
public static Class<?> getEnumType(Class<?> targetType) {
Class<?> enumType = targetType;
while (enumType != null && !enumType.isEnum()) {
enumType = enumType.getSuperclass();
}
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
return enumType;
}
private static class BooleanToEnum<T extends Enum> implements Converter<Boolean, T> {
private final Class<T> enumType;
private final ObjectMapper objectMapper;
BooleanToEnum(Class<T> enumType, ObjectMapper objectMapper) {
this.enumType = enumType;
this.objectMapper = objectMapper;
}
@Override
@Nullable
public T convert(Boolean source) {
if (source == null) {
return null;
}
try {
return objectMapper.readValue("\"" + source + "\"", enumType);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* 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;
/**
* PropertyValueConverter throws this when cannot convert the property. The caller should catch this and resort to other
* means for creating the value.
*
* @author Michael Reiche
*/
public class ConverterHasNoConversion extends RuntimeException {}

View File

@@ -44,8 +44,6 @@ 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.
* <p>
@@ -112,9 +110,6 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
@Override
public boolean hasValueConverter(PersistentProperty<?> property) {
if (property.findAnnotation(Encrypted.class) != null) {
return true;
}
return super.hasValueConverter(property);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors
* 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.
@@ -13,73 +13,156 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.beans.BeanUtils;
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;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import org.springframework.util.Assert;
/**
* Accept the Couchbase @Encrypted annotation in addition to @ValueConverter
* Accept the Couchbase @Encrypted and @JsonValue annotations in addition to @ValueConverter annotation.<br>
* There can only be one propertyValueConverter for a property. Although there maybe be multiple annotations,
* getConverter(property) only returns one converter (a ChainedPropertyValueConverter might be useful). Note that
* valueConversions.afterPropertiesSet() (see
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}
* encapsulates this in a CachingPropertyValueConverterFactory which caches by 'property'. Although
* CachingPropertyValueConverterFactory does have the functionality to cache by a type, it only caches by the type
* specified on an @ValueConverter annotation.To avoid having identical converter instances for each instance of a class
* containing an @JsonValue annotation, converterCacheForType is used.
*
* @author Michael Reiche
*/
public class CouchbasePropertyValueConverterFactory implements PropertyValueConverterFactory {
CryptoManager cryptoManager;
Map<Class<? extends PropertyValueConverter<?, ?, ?>>, PropertyValueConverter<?, ?, ?>> converterCache = new HashMap<>();
final CryptoManager cryptoManager;
final Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap;
static protected final Map<Class<?>, Optional<PropertyValueConverter<?, ?, ?>>> converterCacheForType = new ConcurrentHashMap<>();
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager) {
public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager,
Map<Class<? extends Annotation>, Class<?>> annotationToConverterMap) {
this.cryptoManager = cryptoManager;
this.annotationToConverterMap = annotationToConverterMap;
}
/**
* @param property must not be {@literal null}.
* @return
* @param <DV> destination value
* @param <SV> source value
* @param <P> context
*/
@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 {
// this will return the converter for the first annotation that requires a PropertyValueConverter like @Encrypted
for (Annotation ann : property.getField().getAnnotations()) {
Class<?> converterClass = converterFromFieldAnnotation(ann);
if (converterClass != null) {
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) converterClass, property);
}
}
if (property.getType().isEnum()) { // Enums have type-based converters for JsonValue/Creator. see OtherConverters.
return null;
}
// Maybe the type of the property has annotations that indicate a converter (like a method with a @JsonValue)
return (PropertyValueConverter<DV, SV, P>) converterCacheForType
.computeIfAbsent(property.getType(), p -> Optional.ofNullable((maybeTypePropertyConverter(property))))
.orElse(null);
}
/**
* lookup the converter class from the annotation. Analogous to getting the converter class from the value() attribute
* of the @ValueProperty annotation
*
* @param ann the annotation
* @return the class of the converter
*/
private Class<?> converterFromFieldAnnotation(Annotation ann) {
return annotationToConverterMap.get(ann.annotationType());
}
<DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> maybeTypePropertyConverter(
PersistentProperty<?> property) {
Class<?> type = property.getType();
// find the annotated method to determine if a converter is required, and cache it.
Method jsonValueMethod = null;
for (Method m : type.getDeclaredMethods()) {
JsonValue jsonValueAnn = m.getAnnotation(JsonValue.class);
if (jsonValueAnn != null && jsonValueAnn.value()) {
Class jsonValueConverterClass = converterFromFieldAnnotation(jsonValueAnn);
if (jsonValueConverterClass != null) {
jsonValueMethod = m;
jsonValueMethod.setAccessible(true);
JsonValueConverter.valueMethodCache.put(type, jsonValueMethod);
Constructor<?> jsonCreatorMethod = null;
for (Constructor<?> c : type.getConstructors()) {
JsonCreator jsonCreatorAnn = c.getAnnotation(JsonCreator.class);
if (jsonCreatorAnn != null && !jsonCreatorAnn.mode().equals(JsonCreator.Mode.DISABLED)) {
jsonCreatorMethod = c;
jsonCreatorMethod.setAccessible(true);
JsonValueConverter.creatorMethodCache.put(type, jsonCreatorMethod);
break;
}
}
return getConverter((Class<PropertyValueConverter<DV, SV, P>>) jsonValueConverterClass, property);
}
}
}
return null; // we didn't find a property value converter to use
}
@Override
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
Class<? extends PropertyValueConverter<DV, SV, P>> converterType) {
return getConverter(converterType, null);
}
PropertyValueConverter<?, ?, ?> converter = converterCache.get(converterType);
if (converter != null) {
return (PropertyValueConverter<DV, SV, P>) converter;
}
/**
* @param converterType
* @param property
* @return
* @param <DV>
* @param <SV>
* @param <P>
*/
public <DV, SV, P extends ValueConversionContext<?>> PropertyValueConverter<DV, SV, P> getConverter(
Class<? extends PropertyValueConverter<DV, SV, P>> converterType, PersistentProperty<?> property) {
// CryptoConverter takes a cryptoManager argument
if (CryptoConverter.class.isAssignableFrom(converterType)) {
converter = new CryptoConverter(cryptoManager);
} else {
return (PropertyValueConverter<DV, SV, P>) new CryptoConverter(cryptoManager);
} else if (property != null) { // try constructor that takes PersistentProperty
try {
Constructor constructor = converterType.getConstructor();
converter = (PropertyValueConverter<?, ?, ?>) constructor.newInstance();
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
Constructor<?> constructor = converterType.getConstructor(PersistentProperty.class);
return (PropertyValueConverter<DV, SV, P>) BeanUtils.instantiateClass(constructor, property);
} catch (NoSuchMethodException e) {}
}
converterCache.put((Class<? extends PropertyValueConverter<DV, SV, P>>) converter.getClass(), converter);
return (PropertyValueConverter<DV, SV, P>) converter;
// there is no constructor that takes a property, fall-back to no-args constructor
return BeanUtils.instantiateClass(converterType);
}
}

View File

@@ -20,12 +20,15 @@ import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import com.fasterxml.jackson.databind.ObjectMapper;
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.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.PersistentProperty;
@@ -40,7 +43,8 @@ import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
/**
* Encrypt/Decrypted properties annotated with
* Encrypt/Decrypted properties annotated. This is registered in
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
*
* @author Michael Reiche
*/
@@ -68,7 +72,8 @@ public class CryptoConverter implements
CouchbaseConversionContext ctx = (CouchbaseConversionContext) context;
CouchbasePersistentProperty property = ctx.getProperty();
byte[] plainText = coerceToBytesWrite(property, ctx.getAccessor(), ctx);
Map<String, Object> encrypted = cryptoManager().encrypt(plainText, ctx.getProperty().findAnnotation(Encrypted.class).encrypter());
Map<String, Object> encrypted = cryptoManager().encrypt(plainText,
ctx.getProperty().findAnnotation(Encrypted.class).encrypter());
return new CouchbaseDocument().setContent(encrypted);
}
@@ -83,6 +88,9 @@ public class CryptoConverter implements
if ("null".equals(decryptedString)) {
return null;
}
// TODO - as-is, this never gets ran through ObjectMapper() -
// TODO - i.e. @JsonValue etc will not be processed by ObjectMapper
/* 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());
@@ -125,7 +133,7 @@ public class CryptoConverter implements
}
plainText = ja.toBytes();
} else if (cnvs.isSimpleType(sourceType)) { // simpleType
String plainString = value != null ? value.toString() : null;
String plainString = value != null ? value.toString() : null; // TODO - this will ignore @JsonValue
if ((sourceType == String.class || targetType == String.class) || sourceType == Character.class
|| sourceType == char.class || Enum.class.isAssignableFrom(sourceType)
|| Locale.class.isAssignableFrom(sourceType)) {
@@ -265,7 +273,7 @@ public class CryptoConverter implements
} else if (Character.class.isAssignableFrom(o.getClass())) {
o = ((Character) o).toString();
} else if (Enum.class.isAssignableFrom(o.getClass())) {
o = ((Enum) o).name();
o = ((Enum) o).name(); // TODO - this is will ignore @JsonValue
} else { // punt
o = o.toString();
}

View File

@@ -0,0 +1,84 @@
/*
* 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.io.IOException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.couchbase.client.core.encryption.CryptoManager;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Reading Converter factory for Enums. This differs from the one provided in org.springframework.core.convert.support
* by getting the result from the jackson objectmapper (which will process @JsonValue annotations) This is registered in
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
* This will take precedence over {@link org.springframework.core.convert.support.IntegerToEnumConverterFactory}
*
* @author Michael Reiche
*/
@ReadingConverter
public class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum> {
private final ObjectMapper objectMapper;
public IntegerToEnumConverterFactory(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public <T extends Enum> Converter<Integer, T> getConverter(Class<T> targetType) {
return new ObjectToEnum(getEnumType(targetType), objectMapper);
}
public static Class<?> getEnumType(Class<?> targetType) {
Class<?> enumType = targetType;
while (enumType != null && !enumType.isEnum()) {
enumType = enumType.getSuperclass();
}
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
return enumType;
}
private static class ObjectToEnum<T extends Enum> implements Converter<Integer, T> {
private final Class<T> enumType;
private final ObjectMapper objectMapper;
ObjectToEnum(Class<T> enumType, ObjectMapper objectMapper) {
this.enumType = enumType;
this.objectMapper = objectMapper;
}
@Override
@Nullable
public T convert(Integer source) {
if (source == null) {
return null;
}
try {
return objectMapper.readValue(source.toString(), enumType);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.data.convert.PropertyValueConverter;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.mapping.PersistentProperty;
/**
* Converter for non-Enum types that have @JsonValue and possibly an @JsonCreator annotated methods.
*
* @author Michael Reiche
*/
public class JsonValueConverter
implements PropertyValueConverter<Object, Object, ValueConversionContext<? extends PersistentProperty<?>>> {
static protected final Map<Class<?>, Method> valueMethodCache = new ConcurrentHashMap<>();
static protected final Map<Class<?>, Constructor<?>> creatorMethodCache = new ConcurrentHashMap<>();
static private final ConverterHasNoConversion CONVERTER_HAS_NO_CONVERSION = new ConverterHasNoConversion();
@Override
public Object read(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
Class<?> type = context.getProperty().getType();
// if there was a @JsonCreator method, use it
if (getJsonCreatorMethod(type) != null) {
try {
return getJsonCreatorMethod(type).newInstance(value);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
throw new RuntimeException(e);
}
}
// fall-through in MappingCouchbaseConverter.readValue(), maybe there is an @PersistenceCreator that takes the arg.
throw CONVERTER_HAS_NO_CONVERSION;
}
@Override
public Object write(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
Class<?> type = value.getClass();
try {
return getJsonValueMethod(type).invoke(value);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
private Method getJsonValueMethod(Class<?> type) {
return valueMethodCache.get(type);
}
private Constructor<?> getJsonCreatorMethod(Class<?> type) {
return creatorMethodCache.get(type);
}
}

View File

@@ -248,8 +248,6 @@ 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);
}
@@ -402,6 +400,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
if (Enum.class.isAssignableFrom(target)) {
// no longer needed with Enum converters
return Enum.valueOf((Class<Enum>) target, value.toString());
}
@@ -772,7 +771,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
type, prop, accessor));
} else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
writeInternalRoot(element, embeddedDoc, prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop);
writeInternalRoot(element, embeddedDoc,
prop != null ? prop.getTypeInformation() : TypeInformation.of(elementType), false, prop);
target.put(embeddedDoc);
}
@@ -854,7 +854,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
/**
* This does process PropertyValueConversions
*
*
* @param value
* @param accessor
* @return
@@ -954,18 +954,25 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
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)) {
try {
return (R) conversions.getPropertyValueConversions().getValueConverter(prop).read(value,
new CouchbaseConversionContext(prop, this, null));
} catch (ConverterHasNoConversion noConversion) {
; // ignore
}
}
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
}
if (value instanceof CouchbaseDocument) {
return (R) read(prop.getTypeInformation(), (CouchbaseDocument) value, parent);
}
if (value instanceof CouchbaseList) {
return (R) readCollection(prop.getTypeInformation(), (CouchbaseList) value, parent);
}
return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations
}
private ConvertingPropertyAccessor<Object> getPropertyAccessor(Object source) {

View File

@@ -16,6 +16,9 @@
package org.springframework.data.couchbase.core.convert;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
@@ -24,13 +27,19 @@ import java.util.Collection;
import java.util.List;
import java.util.UUID;
import com.fasterxml.jackson.databind.ObjectWriter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.util.Base64Utils;
import com.couchbase.client.core.encryption.CryptoManager;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Out of the box conversions for java dates and calendars.
* Out of the box conversions for Other types.
*
* @author Michael Reiche
*/
@@ -58,7 +67,10 @@ public final class OtherConverters {
converters.add(StringToCharArray.INSTANCE);
converters.add(ClassToString.INSTANCE);
converters.add(StringToClass.INSTANCE);
// EnumToObject, IntegerToEnumConverterFactory and StringToEnumConverterFactory are
// registered in
// {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(
// CryptoManager)} as they require an ObjectMapper
return converters;
}
@@ -148,7 +160,7 @@ public final class OtherConverters {
@Override
public String convert(char[] source) {
return source == null ? null : new String(source) ;
return source == null ? null : new String(source);
}
}
@@ -162,14 +174,13 @@ public final class OtherConverters {
}
}
@WritingConverter
public enum ClassToString implements Converter<Class<?>, String> {
INSTANCE;
@Override
public String convert(Class<?> source) {
return source == null ? null : source.getClass().getName() ;
return source == null ? null : source.getClass().getName();
}
}
@@ -187,4 +198,38 @@ public final class OtherConverters {
}
}
/**
* Writing converter for Enums. This is registered in
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions( CryptoManager)}.
* The corresponding reading converters are in {@link IntegerToEnumConverterFactory} and
* {@link StringToEnumConverterFactory}
*/
@WritingConverter
public static class EnumToObject implements Converter<Enum<?>, Object> {
private final ObjectMapper objectMapper;
private static final JsonFactory factory = new JsonFactory();
public EnumToObject(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public Object convert(Enum<?> source) {
try (Writer writer = new StringWriter(); JsonGenerator generator = factory.createGenerator(writer)) {
objectMapper.writeValue(generator, source);
String s = writer.toString();
if (s != null && s.startsWith("\"")) {
return objectMapper.readValue(s,String.class);
}
if ("true".equals(s) || "false".equals(s)) {
return objectMapper.readValue(s,Boolean.class);
}
return objectMapper.readValue(s,Number.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.couchbase.client.core.encryption.CryptoManager;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Reading Converter factory for Enums. This differs from the one provided in org.springframework.core.convert.support
* by getting the result from the jackson objectmapper (which will process @JsonValue annotations) This is registered in
* {@link org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration#customConversions(CryptoManager)}.
* This will take precedence over {@link org.springframework.core.convert.support.StringToEnumConverterFactory}
*
* @author Michael Reiche
*/
@ReadingConverter
public class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
private final ObjectMapper objectMapper;
public StringToEnumConverterFactory(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
return new StringToEnum(getEnumType(targetType), objectMapper);
}
public static Class<?> getEnumType(Class<?> targetType) {
Class<?> enumType = targetType;
while (enumType != null && !enumType.isEnum()) {
enumType = enumType.getSuperclass();
}
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
return enumType;
}
private static class StringToEnum<T extends Enum> implements Converter<String, T> {
private final Class<T> enumType;
private final ObjectMapper objectMapper;
StringToEnum(Class<T> enumType, ObjectMapper objectMapper) {
this.enumType = enumType;
this.objectMapper = objectMapper;
}
@Override
@Nullable
public T convert(String source) {
if (source == null) {
return null;
}
return objectMapper.convertValue(source, enumType);
}
}
}