From 91f83589bd9bc70cfd56a4108b221120382e9d9d Mon Sep 17 00:00:00 2001 From: mikereiche Date: Wed, 30 Nov 2022 17:06:55 -0800 Subject: [PATCH] Add suport for JsonValue annotation on Enums and JsonValue/JsonCreator otherwise. Closes #1617. --- .../AbstractCouchbaseConfiguration.java | 67 ++++++--- .../convert/AbstractCouchbaseConverter.java | 12 +- .../BooleanToEnumConverterFactory.java | 83 ++++++++++ .../convert/ConverterHasNoConversion.java | 25 +++ .../convert/CouchbaseCustomConversions.java | 5 - ...ouchbasePropertyValueConverterFactory.java | 139 +++++++++++++---- .../core/convert/CryptoConverter.java | 16 +- .../IntegerToEnumConverterFactory.java | 84 +++++++++++ .../core/convert/JsonValueConverter.java | 75 +++++++++ .../convert/MappingCouchbaseConverter.java | 29 ++-- .../core/convert/OtherConverters.java | 55 ++++++- .../convert/StringToEnumConverterFactory.java | 77 ++++++++++ .../cache/CouchbaseCacheIntegrationTests.java | 1 + .../data/couchbase/domain/Airport.java | 14 +- .../couchbase/domain/AirportJsonValue.java | 142 ++++++++++++++++++ .../domain/AirportJsonValueRepository.java | 31 ++++ .../domain/AirportJsonValuedObject.java | 59 ++++++++ .../domain/EBTurbulenceCategory.java | 41 +++++ .../domain/EITurbulenceCategory.java | 38 +++++ .../EJsonCreatorTurbulenceCategory.java | 41 +++++ .../couchbase/domain/ETurbulenceCategory.java | 38 +++++ .../data/couchbase/domain/Person.java | 2 +- ...chbaseRepositoryQueryIntegrationTests.java | 133 +++++++++++++++- ...sitoryQueryCollectionIntegrationTests.java | 4 +- 24 files changed, 1126 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java create mode 100644 src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java create mode 100644 src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java diff --git a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java index 117c119c5..5b6b4a42f 100644 --- a/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java +++ b/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java @@ -18,11 +18,16 @@ 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.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.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.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 @@ @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 ClusterEnvironment couchbaseClusterEnvironment() { 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 MappingCouchbaseConverter mappingCouchbaseConverter(CouchbaseMappingConte @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 CouchbaseMappingContext couchbaseMappingContext(CustomConversions customC 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 CustomConversions customConversions(CryptoManager cryptoManager) { List 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> annotationToConverterMap(){ + Map,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; } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java index df362f11c..d419fc052 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java @@ -117,10 +117,8 @@ public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, Converti 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 Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, Converti 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 Object convertForWriteIfNeeded(Object inValue) { 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), diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java new file mode 100644 index 000000000..a86c3c2c6 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/BooleanToEnumConverterFactory.java @@ -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 { + + private final ObjectMapper objectMapper; + + public BooleanToEnumConverterFactory(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Converter getConverter(Class 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 implements Converter { + + private final Class enumType; + private final ObjectMapper objectMapper; + + BooleanToEnum(Class 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); + } + + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java new file mode 100644 index 000000000..6067a3e1a --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/ConverterHasNoConversion.java @@ -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 {} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java index b663bf9bd..fc25cd11a 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbaseCustomConversions.java @@ -44,8 +44,6 @@ 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. *

@@ -112,9 +110,6 @@ public static CouchbaseCustomConversions create(Consumer property) { - if (property.findAnnotation(Encrypted.class) != null) { - return true; - } return super.hasValueConverter(property); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java index ef6c70592..118eb8d92 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CouchbasePropertyValueConverterFactory.java @@ -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.
+ * 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>, PropertyValueConverter> converterCache = new HashMap<>(); + final CryptoManager cryptoManager; + final Map, Class> annotationToConverterMap; + static protected final Map, Optional>> converterCacheForType = new ConcurrentHashMap<>(); - public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager) { + public CouchbasePropertyValueConverterFactory(CryptoManager cryptoManager, + Map, Class> annotationToConverterMap) { this.cryptoManager = cryptoManager; + this.annotationToConverterMap = annotationToConverterMap; } + /** + * @param property must not be {@literal null}. + * @return + * @param destination value + * @param source value + * @param

context + */ @Override public > PropertyValueConverter getConverter( PersistentProperty property) { + PropertyValueConverter 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>) 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>) 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) 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()); + } + + > PropertyValueConverter 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>) jsonValueConverterClass, property); + } + } + } + + return null; // we didn't find a property value converter to use } @Override public > PropertyValueConverter getConverter( Class> converterType) { + return getConverter(converterType, null); + } - PropertyValueConverter converter = converterCache.get(converterType); - if (converter != null) { - return (PropertyValueConverter) converter; - } + /** + * @param converterType + * @param property + * @return + * @param + * @param + * @param

+ */ + public > PropertyValueConverter getConverter( + Class> converterType, PersistentProperty property) { + // CryptoConverter takes a cryptoManager argument if (CryptoConverter.class.isAssignableFrom(converterType)) { - converter = new CryptoConverter(cryptoManager); - } else { + return (PropertyValueConverter) 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) BeanUtils.instantiateClass(constructor, property); + } catch (NoSuchMethodException e) {} } - converterCache.put((Class>) converter.getClass(), converter); - return (PropertyValueConverter) converter; - + // there is no constructor that takes a property, fall-back to no-args constructor + return BeanUtils.instantiateClass(converterType); } } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java index 700122465..31558f6f2 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java @@ -20,12 +20,15 @@ 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.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 CouchbaseDocument write(Object value, ValueConversionContext encrypted = cryptoManager().encrypt(plainText, ctx.getProperty().findAnnotation(Encrypted.class).encrypter()); + Map encrypted = cryptoManager().encrypt(plainText, + ctx.getProperty().findAnnotation(Encrypted.class).encrypter()); return new CouchbaseDocument().setContent(encrypted); } @@ -83,6 +88,9 @@ private Object coerceToValueRead(byte[] decrypted, CouchbaseConversionContext co 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 @@ private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, Converti } 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 @@ Object coerceToJson(Object o, CustomConversions cnvs, ConversionService svc) { } 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(); } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java new file mode 100644 index 000000000..92373ccd5 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/IntegerToEnumConverterFactory.java @@ -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 { + + private final ObjectMapper objectMapper; + + public IntegerToEnumConverterFactory(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Converter getConverter(Class 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 implements Converter { + + private final Class enumType; + private final ObjectMapper objectMapper; + + ObjectToEnum(Class 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); + } + + } + } +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java new file mode 100644 index 000000000..c91079b7b --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/JsonValueConverter.java @@ -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>> { + + static protected final Map, Method> valueMethodCache = new ConcurrentHashMap<>(); + static protected final Map, Constructor> creatorMethodCache = new ConcurrentHashMap<>(); + static private final ConverterHasNoConversion CONVERTER_HAS_NO_CONVERSION = new ConverterHasNoConversion(); + + @Override + public Object read(Object value, ValueConversionContext> 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> 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); + } + +} diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java index e9f684d24..468eb6988 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/MappingCouchbaseConverter.java @@ -248,8 +248,6 @@ protected R read(final TypeInformation type, final CouchbaseDocument sour CouchbasePersistentEntity entity = (CouchbasePersistentEntity) mappingContext .getRequiredPersistentEntity(typeToUse); - if (source.containsKey("encbooleans")) - System.err.println(source); return read(entity, source, parent); } @@ -402,6 +400,7 @@ private Object getPotentiallyConvertedSimpleRead(final Object value, final Class } if (Enum.class.isAssignableFrom(target)) { + // no longer needed with Enum converters return Enum.valueOf((Class) target, value.toString()); } @@ -772,7 +771,8 @@ public CouchbaseList writeCollectionInternal(final Collection source, final C 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 Object getPotentiallyConvertedSimpleWrite(final Object value) { /** * This does process PropertyValueConversions - * + * * @param value * @param accessor * @return @@ -954,18 +954,25 @@ private R readValue(Object value, TypeInformation type, Object parent) { public 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) { + } + if (value instanceof CouchbaseDocument) { return (R) read(prop.getTypeInformation(), (CouchbaseDocument) value, parent); - } else if (value instanceof CouchbaseList) { + } + if (value instanceof CouchbaseList) { return (R) readCollection(prop.getTypeInformation(), (CouchbaseList) value, parent); - } else { - return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations } + return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations + } private ConvertingPropertyAccessor getPropertyAccessor(Object source) { diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java b/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java index b154366a9..b22c20a1c 100644 --- a/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java +++ b/src/main/java/org/springframework/data/couchbase/core/convert/OtherConverters.java @@ -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.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 @@ private 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 enum CharArrayToString implements Converter { @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 char[] convert(String source) { } } - @WritingConverter public enum ClassToString implements Converter, 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 Class convert(String source) { } } + /** + * 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, 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); + } + } + } + } diff --git a/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java b/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java new file mode 100644 index 000000000..756e80ec1 --- /dev/null +++ b/src/main/java/org/springframework/data/couchbase/core/convert/StringToEnumConverterFactory.java @@ -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 { + + private final ObjectMapper objectMapper; + + public StringToEnumConverterFactory(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public Converter getConverter(Class 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 implements Converter { + + private final Class enumType; + private final ObjectMapper objectMapper; + + StringToEnum(Class 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); + } + } +} diff --git a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java index e9fa46ceb..819778ff1 100644 --- a/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/cache/CouchbaseCacheIntegrationTests.java @@ -90,6 +90,7 @@ void cacheable() { t0 = System.currentTimeMillis(); users = userRepository.getByFirstname(user.getFirstname()); assert (System.currentTimeMillis() - t0 < 100); + userRepository.delete(user); } @Test diff --git a/src/test/java/org/springframework/data/couchbase/domain/Airport.java b/src/test/java/org/springframework/data/couchbase/domain/Airport.java index cda300bf6..771d25171 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Airport.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Airport.java @@ -26,6 +26,8 @@ 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 long getSize() { public void setSize(long size) { this.size = size; } + } diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java new file mode 100644 index 000000000..27d123b03 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValue.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java new file mode 100644 index 000000000..fb4390cbc --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValueRepository.java @@ -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
+ * + * @author Michael Reiche + */ +@Repository +public interface AirportJsonValueRepository + extends CouchbaseRepository, DynamicProxyable { + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java new file mode 100644 index 000000000..41a411c4c --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/AirportJsonValuedObject.java @@ -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 map = Map.of("first", "1st", "second", "2nd"); + static Map 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); + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java new file mode 100644 index 000000000..b7b16924a --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/EBTurbulenceCategory.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java new file mode 100644 index 000000000..897307fc1 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/EITurbulenceCategory.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java new file mode 100644 index 000000000..619a591d1 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/EJsonCreatorTurbulenceCategory.java @@ -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; + } + +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java b/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java new file mode 100644 index 000000000..854374e07 --- /dev/null +++ b/src/test/java/org/springframework/data/couchbase/domain/ETurbulenceCategory.java @@ -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; + } +} diff --git a/src/test/java/org/springframework/data/couchbase/domain/Person.java b/src/test/java/org/springframework/data/couchbase/domain/Person.java index 639ae4b5e..4e8f567c5 100644 --- a/src/test/java/org/springframework/data/couchbase/domain/Person.java +++ b/src/test/java/org/springframework/data/couchbase/domain/Person.java @@ -58,13 +58,13 @@ public class Person extends AbstractEntity implements Persistable { public Person() { setId(UUID.randomUUID()); + setMiddlename("Nick"); } public Person(String firstname, String lastname) { this(); setFirstname(firstname); setLastname(lastname); - setMiddlename("Nick"); isNew(true); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java index 79345c2a5..959b179c7 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/CouchbaseRepositoryQueryIntegrationTests.java @@ -69,10 +69,16 @@ 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.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 @@ void findBySimplePropertyReturnType() { } } + @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 @@ void namedParameterList() throws Exception { 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 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 @@ void orderedParameterList() throws Exception { 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 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()); } diff --git a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java index 027eb33c1..46f816d13 100644 --- a/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/couchbase/repository/query/CouchbaseRepositoryQueryCollectionIntegrationTests.java @@ -211,7 +211,7 @@ public void testScopeCollectionAnnotation() { assertEquals(saved, found.get(0), "should have found what was saved"); List 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 void testScopeCollectionRepoWith() { assertEquals(saved, found.get(0), "should have found what was saved"); List 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 {