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);
}
}
}

View File

@@ -90,6 +90,7 @@ class CouchbaseCacheIntegrationTests extends JavaIntegrationTests {
t0 = System.currentTimeMillis();
users = userRepository.getByFirstname(user.getFirstname());
assert (System.currentTimeMillis() - t0 < 100);
userRepository.delete(user);
}
@Test

View File

@@ -26,6 +26,8 @@ import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
import org.springframework.data.couchbase.core.mapping.Expiration;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* Airport entity
*
@@ -39,7 +41,7 @@ public class Airport extends ComparableEntity {
String iata;
String icao;
@JsonFormat(pattern = "abc") String icao;
@Version Number version;
@@ -48,6 +50,15 @@ public class Airport extends ComparableEntity {
@Max(2) long size;
private long someNumber;
public AirportJsonValuedObject jvObject;
{
this.jvObject = new AirportJsonValuedObject();
this.jvObject.theValue = "first";
}
public Airport() {}
@PersistenceConstructor
public Airport(String key, String iata, String icao) {
this.key = key;
@@ -99,4 +110,5 @@ public class Airport extends ComparableEntity {
public void setSize(long size) {
this.size = size;
}
}

View File

@@ -0,0 +1,142 @@
/*
* 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.domain;
import jakarta.validation.constraints.Max;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.Id;
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 org.springframework.data.couchbase.core.mapping.Expiration;
import com.fasterxml.jackson.annotation.JsonFormat;
/**
* AirportJsonValue entity. From Airport.
*
* @author Michael Reiche
*/
@Document
@TypeAlias("airport")
public class AirportJsonValue extends ComparableEntity {
@Id String key;
String iata;
@JsonFormat(pattern = "abc") String icao;
ETurbulenceCategory turbulence1;
ETurbulenceCategory turbulence2;
EITurbulenceCategory turbulence3;
EJsonCreatorTurbulenceCategory turbulence4;
EBTurbulenceCategory turbulence5 ;
@Version Number version;
@CreatedBy private String createdBy;
@Expiration private long expiration;
@Max(2) long size;
public AirportJsonValuedObject jvObject;
{
this.jvObject = new AirportJsonValuedObject();
}
public AirportJsonValue() {}
@PersistenceConstructor
public AirportJsonValue(String key, String iata, String icao) {
this.key = key;
this.iata = iata;
this.icao = icao;
}
public String getId() {
return key;
}
public String getIata() {
return iata;
}
public String getIcao() {
return icao;
}
public long getExpiration() {
return expiration;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public void setTurbulence1(ETurbulenceCategory turbulence1) {
this.turbulence1 = turbulence1;
}
public ETurbulenceCategory getTurbulence1() {
return turbulence1;
}
public void setTurbulence2(ETurbulenceCategory turbulence) {
this.turbulence2 = turbulence;
}
public ETurbulenceCategory getTurbulence2() {
return turbulence2;
}
public void setTurbulence3(EITurbulenceCategory turbulence3) {
this.turbulence3 = turbulence3;
}
public EITurbulenceCategory getTurbulence3() {
return turbulence3;
}
public void setTurbulence4(EJsonCreatorTurbulenceCategory turbulence4) {
this.turbulence4 = turbulence4;
}
public EJsonCreatorTurbulenceCategory getTurbulence4() {
return turbulence4;
}
public void setTurbulence5(EBTurbulenceCategory turbulence5) {
this.turbulence5 = turbulence5;
}
public EBTurbulenceCategory getTurbulence5() {
return turbulence5;
}
public void setJvObject(AirportJsonValuedObject jvObject) {
this.jvObject = jvObject;
}
public AirportJsonValuedObject getJvObject() {
return jvObject;
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.domain;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.DynamicProxyable;
import org.springframework.stereotype.Repository;
/**
* AirportJsonValue repository for testing <br>
*
* @author Michael Reiche
*/
@Repository
public interface AirportJsonValueRepository
extends CouchbaseRepository<AirportJsonValue, String>, DynamicProxyable<AirportJsonValueRepository> {
}

View File

@@ -0,0 +1,59 @@
/*
* 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.domain;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.data.annotation.PersistenceCreator;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* A non-Enum object that has @JsonValue annotated method.
*
* @author Michael Reiche
*/
public class AirportJsonValuedObject {
public AirportJsonValuedObject() {}
@PersistenceCreator
@JsonCreator
public AirportJsonValuedObject(String theValue) {
this.theValue = reverseMap.get(theValue);
if (this.theValue == null) {
throw new RuntimeException(
"invalid value for " + this.getClass().getName() + " : " + theValue + ". Must be one of " + reverseMap);
}
}
public String theValue;
static Map<String, String> map = Map.of("first", "1st", "second", "2nd");
static Map<String, String> reverseMap = new HashMap<>();
static {
map.entrySet().stream().map((entry) -> reverseMap.put(entry.getValue(), entry.getKey()))
.collect(Collectors.toList());
}
@JsonValue
public String getMapped() {
return map.get(theValue);
}
}

View File

@@ -0,0 +1,41 @@
package org.springframework.data.couchbase.domain;
/*
* 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 com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* An enum that has an Boolean getCode() method.
*
* @author Michael Reiche
*/
public enum EBTurbulenceCategory {
T(true), F(false);
private final Boolean code;
@JsonCreator
EBTurbulenceCategory(Boolean code) {
this.code = code;
}
@JsonValue
public Boolean getCode() {
return code;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.domain;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* An enum that has an Integer getCode() method.
*
* @author Michael Reiche
*/
public enum EITurbulenceCategory {
T10(10), T20(20), T30(30), T40(40);
private final Integer code;
EITurbulenceCategory(Integer code) {
this.code = code;
}
@JsonValue
public Integer getCode() {
return code;
}
}

View File

@@ -0,0 +1,41 @@
/*
* 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.domain;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* An enum that has an @JsonCreator method.
*
* @author Michael Reiche
*/
public enum EJsonCreatorTurbulenceCategory {
T10("10%"), T20("20%"), T30("30%"), T40("40%");
private final String code;
@JsonCreator
EJsonCreatorTurbulenceCategory(String code) {
this.code = code;
}
@JsonValue
public String getCode() {
return code;
}
}

View File

@@ -0,0 +1,38 @@
/*
* 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.domain;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* An enum that has an String getCode() method.
*
* @author Michael Reiche
*/
public enum ETurbulenceCategory {
T10("10%"), T20("20%"), T30("30%");
private final String code;
ETurbulenceCategory(String code) {
this.code = code;
}
@JsonValue
public String getCode() {
return code;
}
}

View File

@@ -58,13 +58,13 @@ public class Person extends AbstractEntity implements Persistable<Object> {
public Person() {
setId(UUID.randomUUID());
setMiddlename("Nick");
}
public Person(String firstname, String lastname) {
this();
setFirstname(firstname);
setLastname(lastname);
setMiddlename("Nick");
isNew(true);
}

View File

@@ -69,10 +69,16 @@ import org.springframework.data.couchbase.core.query.QueryCriteria;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.AirlineRepository;
import org.springframework.data.couchbase.domain.Airport;
import org.springframework.data.couchbase.domain.AirportJsonValue;
import org.springframework.data.couchbase.domain.AirportJsonValueRepository;
import org.springframework.data.couchbase.domain.AirportJsonValuedObject;
import org.springframework.data.couchbase.domain.AirportMini;
import org.springframework.data.couchbase.domain.AirportRepository;
import org.springframework.data.couchbase.domain.AirportRepositoryScanConsistencyTest;
import org.springframework.data.couchbase.domain.Course;
import org.springframework.data.couchbase.domain.EITurbulenceCategory;
import org.springframework.data.couchbase.domain.EJsonCreatorTurbulenceCategory;
import org.springframework.data.couchbase.domain.ETurbulenceCategory;
import org.springframework.data.couchbase.domain.Iata;
import org.springframework.data.couchbase.domain.NaiveAuditorAware;
import org.springframework.data.couchbase.domain.Person;
@@ -108,6 +114,7 @@ import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.error.IndexFailureException;
import com.couchbase.client.java.env.ClusterEnvironment;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.kv.GetResult;
import com.couchbase.client.java.kv.InsertOptions;
import com.couchbase.client.java.kv.MutationState;
@@ -129,6 +136,8 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
@Autowired AirportRepository airportRepository;
@Autowired AirportJsonValueRepository airportJsonValueRepository;
@Autowired AirlineRepository airlineRepository;
@Autowired UserRepository userRepository;
@@ -316,6 +325,120 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
}
}
@Test
void sdcJsonValue() {
AirportJsonValue vie = null;
try {
vie = new AirportJsonValue("airports::vie", "vie", "low6");
vie.setTurbulence1(ETurbulenceCategory.T10);
vie.setTurbulence2(ETurbulenceCategory.T20);
vie.setTurbulence3(EITurbulenceCategory.T30);
vie.setTurbulence4(EJsonCreatorTurbulenceCategory.T40);
// sdk/jackson cannot handle vie.setTurbulence5(EBTurbulenceCategory.T);
vie.setJvObject(new AirportJsonValuedObject("1st"));
airportJsonValueRepository.save(vie);
// check that they were saved as expected
GetResult result = couchbaseTemplate.getCouchbaseClientFactory().getCluster().bucket(bucketName())
.defaultCollection().get(vie.getId());
JsonObject jo = JsonObject.fromJson(result.contentAsBytes());
assertEquals(ETurbulenceCategory.T10.getCode(), jo.get("turbulence1"));
assertEquals(ETurbulenceCategory.T20.getCode(), jo.get("turbulence2"));
assertEquals(EITurbulenceCategory.T30.getCode(), jo.get("turbulence3"));
assertEquals(EJsonCreatorTurbulenceCategory.T40.getCode(), jo.get("turbulence4"));
// sdk/jackson cannot handle assertEquals(EBTurbulenceCategory.T.getCode(), jo.get("turbulence5"));
assertEquals(new AirportJsonValuedObject("1st").getMapped(), jo.get("jvObject"));
// check that spring-data-couchbase deserializes as expected
AirportJsonValue airport = airportJsonValueRepository.findById(vie.getId()).get();
System.out.println(airport);
assertEquals(ETurbulenceCategory.T10, airport.getTurbulence1());
assertEquals(ETurbulenceCategory.T20, airport.getTurbulence2());
assertEquals(EITurbulenceCategory.T30, airport.getTurbulence3());
assertEquals(EJsonCreatorTurbulenceCategory.T40, airport.getTurbulence4());
// sdk/jackson cannot handle assertEquals(EBTurbulenceCategory.T, airport.getTurbulence5());
assertEquals(new AirportJsonValuedObject("1st").theValue, airport.getJvObject().theValue);
// check that java sdk deserializes as expected
airport = result.contentAs(AirportJsonValue.class);
assertEquals(ETurbulenceCategory.T10, airport.getTurbulence1());
assertEquals(ETurbulenceCategory.T20, airport.getTurbulence2());
assertEquals(EITurbulenceCategory.T30, airport.getTurbulence3());
assertEquals(EJsonCreatorTurbulenceCategory.T40, airport.getTurbulence4());
// sdk/jackson cannot handle assertEquals(EBTurbulenceCategory.T, airport.getTurbulence5());
assertEquals(new AirportJsonValuedObject("1st").theValue, airport.getJvObject().theValue);
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
try {
airportJsonValueRepository.delete(vie);
} catch (Exception e) {
System.err.println(e);
}
}
}
@Test
void sdkJsonValue() {
AirportJsonValue vie = null;
try {
vie = new AirportJsonValue("airports::vie", "vie", "low6");
vie.setTurbulence1(ETurbulenceCategory.T10);
vie.setTurbulence2(ETurbulenceCategory.T20);
vie.setTurbulence3(EITurbulenceCategory.T30);
vie.setTurbulence4(EJsonCreatorTurbulenceCategory.T40);
// sdk/jackson cannot handle vie.setTurbulence5(EBTurbulenceCategory.T);
vie.setJvObject(new AirportJsonValuedObject("1st"));
//airportJsonValueRepository.save(vie);
couchbaseTemplate.getCouchbaseClientFactory().getCluster().bucket(bucketName())
.defaultCollection().upsert(vie.getId(), vie);
// check that they were saved as expected
GetResult result = couchbaseTemplate.getCouchbaseClientFactory().getCluster().bucket(bucketName())
.defaultCollection().get(vie.getId());
JsonObject jo = JsonObject.fromJson(result.contentAsBytes());
assertEquals(ETurbulenceCategory.T10.getCode(), jo.get("turbulence1"));
assertEquals(ETurbulenceCategory.T20.getCode(), jo.get("turbulence2"));
assertEquals(EITurbulenceCategory.T30.getCode(), jo.get("turbulence3"));
assertEquals(EJsonCreatorTurbulenceCategory.T40.getCode(), jo.get("turbulence4"));
// sdk/jackson cannot handle assertEquals(EBTurbulenceCategory.T.getCode(), jo.get("turbulence5"));
assertEquals(new AirportJsonValuedObject("1st").getMapped(), jo.get("jvObject"));
// check that spring-data-couchbase deserializes as expected
AirportJsonValue airport = airportJsonValueRepository.findById(vie.getId()).get();
System.out.println(airport);
assertEquals(ETurbulenceCategory.T10, airport.getTurbulence1());
assertEquals(ETurbulenceCategory.T20, airport.getTurbulence2());
assertEquals(EITurbulenceCategory.T30, airport.getTurbulence3());
assertEquals(EJsonCreatorTurbulenceCategory.T40, airport.getTurbulence4());
// sdk/jackson cannot handle assertEquals(EBTurbulenceCategory.T, airport.getTurbulence5());
assertEquals(new AirportJsonValuedObject("1st").theValue, airport.getJvObject().theValue);
// check that java sdk deserializes as expected
airport = result.contentAs(AirportJsonValue.class);
assertEquals(ETurbulenceCategory.T10, airport.getTurbulence1());
assertEquals(ETurbulenceCategory.T20, airport.getTurbulence2());
assertEquals(EITurbulenceCategory.T30, airport.getTurbulence3());
assertEquals(EJsonCreatorTurbulenceCategory.T40, airport.getTurbulence4());
// sdk/jackson cannot handle assertEquals(EBTurbulenceCategory.T, airport.getTurbulence5());
assertEquals(new AirportJsonValuedObject("1st").theValue, airport.getJvObject().theValue);
} catch (Exception e) {
e.printStackTrace();
throw e;
} finally {
try {
airportJsonValueRepository.delete(vie);
} catch (Exception e) {
System.err.println(e);
}
}
}
@Test
public void saveNotBoundedRequestPlus() {
airportRepository.withOptions(QueryOptions.queryOptions().scanConsistency(REQUEST_PLUS)).deleteAll();
@@ -867,11 +990,11 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
try {
userSubmission.setUsername("updateObject");
userSubmissionRepository.save(userSubmission);
Course[] courses = new Course[]{ new Course("1", "2", "3"), new Course("4","5","6")};
Course[] courses = new Course[] { new Course("1", "2", "3"), new Course("4", "5", "6") };
userSubmissionRepository.setNamedCourses(userSubmission.getId(), courses);
Optional<UserSubmission> fetched = userSubmissionRepository.findById(userSubmission.getId());
assertEquals(courses.length, fetched.get().getCourses().size());
for(int i=0; i< courses.length; i++){
for (int i = 0; i < courses.length; i++) {
assertEquals(courses[i], fetched.get().getCourses().get(i));
}
} finally {
@@ -886,13 +1009,13 @@ public class CouchbaseRepositoryQueryIntegrationTests extends ClusterAwareIntegr
try {
userSubmission.setUsername("updateObject");
userSubmissionRepository.save(userSubmission);
Course[] courses = new Course[]{ new Course("1", "2", "3"), new Course("4","5","6")};
Course[] courses = new Course[] { new Course("1", "2", "3"), new Course("4", "5", "6") };
userSubmissionRepository.setOrderedCourses(userSubmission.getId(), courses);
Optional<UserSubmission> fetched = userSubmissionRepository.findById(userSubmission.getId());
assertEquals(courses.length, fetched.get().getCourses().size());
for(int i=0; i< courses.length; i++){
for (int i = 0; i < courses.length; i++) {
assertEquals(courses[i], fetched.get().getCourses().get(i));
}
}
} finally {
userSubmissionRepository.deleteById(userSubmission.getId());
}

View File

@@ -211,7 +211,7 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
assertEquals(saved, found.get(0), "should have found what was saved");
List<UserCol> notfound = userColRepository.withScope(DEFAULT_SCOPE)
.withCollection(CollectionIdentifier.DEFAULT_COLLECTION).findByFirstname(user.getFirstname());
assertEquals(0, notfound.size(), "should not have found what was saved");
assertEquals(0, notfound.size(), "should not have found what was saved "+notfound);
} finally {
try {
userColRepository.withScope(otherScope).withCollection(otherCollection).delete(user);
@@ -250,7 +250,7 @@ public class CouchbaseRepositoryQueryCollectionIntegrationTests extends Collecti
assertEquals(saved, found.get(0), "should have found what was saved");
List<UserCol> notfound = userColRepository.withScope(DEFAULT_SCOPE)
.withCollection(CollectionIdentifier.DEFAULT_COLLECTION).findByFirstname(user.getFirstname());
assertEquals(0, notfound.size(), "should not have found what was saved");
assertEquals(0, notfound.size(), "should not have found what was saved "+notfound);
userColRepository.withScope(scopeName).withCollection(collectionName).delete(user);
} finally {
try {