Skip to content

Add suport for JsonValue annotation on Enums. #1618

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}

Expand All @@ -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()));
Expand Down Expand Up @@ -400,20 +418,35 @@ public CustomConversions customConversions(CryptoManager cryptoManager) {
List<GenericConverter> newConverters = new ArrayList();
CustomConversions customConversions = CouchbaseCustomConversions.create(configurationAdapter -> {
SimplePropertyValueConversions valueConversions = new SimplePropertyValueConversions();
valueConversions.setConverterFactory(new CouchbasePropertyValueConverterFactory(cryptoManager));
valueConversions.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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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;

Expand All @@ -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),
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
}
}
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
Expand Down Expand Up @@ -112,9 +110,6 @@ public static CouchbaseCustomConversions create(Consumer<CouchbaseConverterConfi

@Override
public boolean hasValueConverter(PersistentProperty<?> property) {
if (property.findAnnotation(Encrypted.class) != null) {
return true;
}
return super.hasValueConverter(property);
}

Expand Down
Loading