Skip to content

Commit

Permalink
Polishing.
Browse files Browse the repository at this point in the history
Revise builder. Accept builder components in builder methods instead of the builder factory method. Enforce valid parameters instead of lenient, potentially null parameters.

Introduce configuration means to control default typing. Extend tests.

See #2878
Original pull request: #2905
  • Loading branch information
mp911de committed May 21, 2024
1 parent 6b56cfb commit fb140e3
Show file tree
Hide file tree
Showing 2 changed files with 213 additions and 125 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
* @author Mark Paluch
* @author Mao Shuai
* @author John Blum
* @author Anne Lee
* @see org.springframework.data.redis.serializer.JacksonObjectReader
* @see org.springframework.data.redis.serializer.JacksonObjectWriter
* @see com.fasterxml.jackson.databind.ObjectMapper
Expand Down Expand Up @@ -92,13 +93,13 @@ public GenericJackson2JsonRedisSerializer() {
* In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS} will be
* used.
*
* @param classPropertyTypeName {@link String name} of the JSON property holding type information; can be
* @param typeHintPropertyName {@link String name} of the JSON property holding type information; can be
* {@literal null}.
* @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
* @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
*/
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {
this(classPropertyTypeName, JacksonObjectReader.create(), JacksonObjectWriter.create());
public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName) {
this(typeHintPropertyName, JacksonObjectReader.create(), JacksonObjectWriter.create());
}

/**
Expand All @@ -109,40 +110,22 @@ public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName
* In case {@link String name} is {@literal empty} or {@literal null}, then {@link JsonTypeInfo.Id#CLASS} will be
* used.
*
* @param classPropertyTypeName {@link String name} of the JSON property holding type information; can be
* @param typeHintPropertyName {@link String name} of the JSON property holding type information; can be
* {@literal null}.
* @param reader {@link JacksonObjectReader} function to read objects using {@link ObjectMapper}.
* @param writer {@link JacksonObjectWriter} function to write objects using {@link ObjectMapper}.
* @see ObjectMapper#activateDefaultTypingAsProperty(PolymorphicTypeValidator, DefaultTyping, String)
* @see ObjectMapper#activateDefaultTyping(PolymorphicTypeValidator, DefaultTyping, As)
* @since 3.0
*/
public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName, JacksonObjectReader reader,
public GenericJackson2JsonRedisSerializer(@Nullable String typeHintPropertyName, JacksonObjectReader reader,
JacksonObjectWriter writer) {

this(new ObjectMapper(), reader, writer, classPropertyTypeName);
this(new ObjectMapper(), reader, writer, typeHintPropertyName);

registerNullValueSerializer(this.mapper, classPropertyTypeName);
registerNullValueSerializer(this.mapper, typeHintPropertyName);

StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(this.mapper).init(JsonTypeInfo.Id.CLASS, null)
.inclusion(JsonTypeInfo.As.PROPERTY);

if (StringUtils.hasText(classPropertyTypeName)) {
typer = typer.typeProperty(classPropertyTypeName);
}

this.mapper.setDefaultTyping(typer);
}

/**
* Factory method returning a {@literal Builder} used to construct and configure a {@link GenericJackson2JsonRedisSerializer}.
*
* @return new {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
* @since 3.3
*/
public static GenericJackson2JsonRedisSerializerBuilder builder(ObjectMapper objectMapper, JacksonObjectReader reader,
JacksonObjectWriter writer) {
return new GenericJackson2JsonRedisSerializerBuilder(objectMapper, reader, writer);
this.mapper.setDefaultTyping(createDefaultTypeResolverBuilder(getObjectMapper(), typeHintPropertyName));
}

/**
Expand Down Expand Up @@ -188,7 +171,7 @@ private GenericJackson2JsonRedisSerializer(ObjectMapper mapper, JacksonObjectRea
this.typeResolver = newTypeResolver(mapper, typeHintPropertyName, this.defaultTypingEnabled);
}

private TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable String typeHintPropertyName,
private static TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable String typeHintPropertyName,
Lazy<Boolean> defaultTypingEnabled) {

Lazy<TypeFactory> lazyTypeFactory = Lazy.of(mapper::getTypeFactory);
Expand All @@ -199,19 +182,17 @@ private TypeResolver newTypeResolver(ObjectMapper mapper, @Nullable String typeH
return new TypeResolver(lazyTypeFactory, lazyTypeHintPropertyName);
}

private Lazy<String> newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy<Boolean> defaultTypingEnabled) {
private static Lazy<String> newLazyTypeHintPropertyName(ObjectMapper mapper, Lazy<Boolean> defaultTypingEnabled) {

Lazy<String> configuredTypeDeserializationPropertyName = getConfiguredTypeDeserializationPropertyName(mapper);

Lazy<String> resolvedLazyTypeHintPropertyName = Lazy.of(() -> defaultTypingEnabled.get() ? null
: configuredTypeDeserializationPropertyName.get());

resolvedLazyTypeHintPropertyName = resolvedLazyTypeHintPropertyName.or("@class");
Lazy<String> resolvedLazyTypeHintPropertyName = Lazy
.of(() -> defaultTypingEnabled.get() ? null : configuredTypeDeserializationPropertyName.get());

return resolvedLazyTypeHintPropertyName;
return resolvedLazyTypeHintPropertyName.or("@class");
}

private Lazy<String> getConfiguredTypeDeserializationPropertyName(ObjectMapper mapper) {
private static Lazy<String> getConfiguredTypeDeserializationPropertyName(ObjectMapper mapper) {

return Lazy.of(() -> {

Expand All @@ -226,20 +207,43 @@ private Lazy<String> getConfiguredTypeDeserializationPropertyName(ObjectMapper m
});
}

private static StdTypeResolverBuilder createDefaultTypeResolverBuilder(ObjectMapper objectMapper,
@Nullable String typeHintPropertyName) {

StdTypeResolverBuilder typer = TypeResolverBuilder.forEverything(objectMapper).init(JsonTypeInfo.Id.CLASS, null)
.inclusion(As.PROPERTY);

if (StringUtils.hasText(typeHintPropertyName)) {
typer = typer.typeProperty(typeHintPropertyName);
}
return typer;
}

/**
* Factory method returning a {@literal Builder} used to construct and configure a
* {@link GenericJackson2JsonRedisSerializer}.
*
* @return new {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
* @since 3.3.1
*/
public static GenericJackson2JsonRedisSerializerBuilder builder() {
return new GenericJackson2JsonRedisSerializerBuilder();
}

/**
* Register {@link NullValueSerializer} in the given {@link ObjectMapper} with an optional
* {@code classPropertyTypeName}. This method should be called by code that customizes
* {@code typeHintPropertyName}. This method should be called by code that customizes
* {@link GenericJackson2JsonRedisSerializer} by providing an external {@link ObjectMapper}.
*
* @param objectMapper the object mapper to customize.
* @param classPropertyTypeName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
* @param typeHintPropertyName name of the type property. Defaults to {@code @class} if {@literal null}/empty.
* @since 2.2
*/
public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String classPropertyTypeName) {
public static void registerNullValueSerializer(ObjectMapper objectMapper, @Nullable String typeHintPropertyName) {

// Simply setting {@code mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)} does not help here
// since we need the type hint embedded for deserialization using the default typing feature.
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));
objectMapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(typeHintPropertyName)));
}

/**
Expand Down Expand Up @@ -376,8 +380,7 @@ protected JavaType resolveType(byte[] source, Class<?> type) throws IOException
*/
private static class NullValueSerializer extends StdSerializer<NullValue> {

@Serial
private static final long serialVersionUID = 1999052150548658808L;
@Serial private static final long serialVersionUID = 1999052150548658808L;

private final String classIdentifier;

Expand Down Expand Up @@ -408,65 +411,155 @@ public void serializeWithType(NullValue value, JsonGenerator jsonGenerator, Seri
}

/**
* {@literal Builder} for creating a {@link GenericJackson2JsonRedisSerializer}.
* Builder for configuring and creating a {@link GenericJackson2JsonRedisSerializer}.
*
* @author Anne Lee
* @since 3.3
* @author Mark Paluch
* @since 3.3.1
*/
public static class GenericJackson2JsonRedisSerializerBuilder {
@Nullable
private String classPropertyTypeName;
private JacksonObjectReader reader;
private JacksonObjectWriter writer;
private ObjectMapper mapper;
@Nullable
private StdSerializer<NullValue> nullValueSerializer;

private GenericJackson2JsonRedisSerializerBuilder(
ObjectMapper objectMapper,
JacksonObjectReader reader,
JacksonObjectWriter writer
) {
this.mapper = objectMapper;

private @Nullable String typeHintPropertyName;

private JacksonObjectReader reader = JacksonObjectReader.create();

private JacksonObjectWriter writer = JacksonObjectWriter.create();

private @Nullable ObjectMapper objectMapper;

private @Nullable Boolean defaultTyping;

private boolean registerNullValueSerializer = true;

private @Nullable StdSerializer<NullValue> nullValueSerializer;

private GenericJackson2JsonRedisSerializerBuilder() {}

/**
* Enable or disable default typing. Enabling default typing will override
* {@link ObjectMapper#setDefaultTyping(com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder)} for a given
* {@link ObjectMapper}. Default typing is enabled by default if no {@link ObjectMapper} is provided.
*
* @param defaultTyping whether to enable/disable default typing. Enabled by default if the {@link ObjectMapper} is
* not provided.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder defaultTyping(boolean defaultTyping) {
this.defaultTyping = defaultTyping;
return this;
}

/**
* Configure a property name to that represents the type hint.
*
* @param typeHintPropertyName {@link String name} of the JSON property holding type information.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder typeHintPropertyName(String typeHintPropertyName) {

Assert.hasText(typeHintPropertyName, "Type hint property name must bot be null or empty");

this.typeHintPropertyName = typeHintPropertyName;
return this;
}

/**
* Configure a provided {@link ObjectMapper}. Note that the provided {@link ObjectMapper} can be reconfigured with a
* {@link #nullValueSerializer} or default typing depending on the builder configuration.
*
* @param objectMapper must not be {@literal null}.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder objectMapper(ObjectMapper objectMapper) {

Assert.notNull(objectMapper, "ObjectMapper must not be null");

this.objectMapper = objectMapper;
return this;
}

/**
* Configure {@link JacksonObjectReader}.
*
* @param reader must not be {@literal null}.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder reader(JacksonObjectReader reader) {

Assert.notNull(reader, "JacksonObjectReader must not be null");

this.reader = reader;
this.writer = writer;
return this;
}

/**
* Configure a classPropertyName.
* Configure {@link JacksonObjectWriter}.
*
* @param classPropertyTypeName can be {@literal null}.
* @param writer must not be {@literal null}.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
* @since 3.3
*/
public GenericJackson2JsonRedisSerializerBuilder classPropertyTypeName(@Nullable String classPropertyTypeName) {
this.classPropertyTypeName = classPropertyTypeName;
public GenericJackson2JsonRedisSerializerBuilder writer(JacksonObjectWriter writer) {

Assert.notNull(writer, "JacksonObjectWriter must not be null");

this.writer = writer;
return this;
}

/**
* Register a nullValueSerializer.
* Register a {@link StdSerializer serializer} for {@link NullValue}.
*
* @param nullValueSerializer the {@link StdSerializer} to use for {@link NullValue} serialization. Can be {@literal null}.
* @param nullValueSerializer the {@link StdSerializer} to use for {@link NullValue} serialization, must not be
* {@literal null}.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder registerNullValueSerializer(@Nullable StdSerializer<NullValue> nullValueSerializer) {
public GenericJackson2JsonRedisSerializerBuilder nullValueSerializer(StdSerializer<NullValue> nullValueSerializer) {

Assert.notNull(nullValueSerializer, "Null value serializer must not be null");

this.nullValueSerializer = nullValueSerializer;
return this;
}

/**
* Create new instance of {@link GenericJackson2JsonRedisSerializer} with configuration options applied.
* Configure whether to register a {@link StdSerializer serializer} for {@link NullValue} serialization. The default
* serializer considers {@link #typeHintPropertyName(String)}.
*
* @return new instance of {@link GenericJackson2JsonRedisSerializer}.
* @param registerNullValueSerializer {@code true} to register the default serializer; {@code false} otherwise.
* @return this {@link GenericJackson2JsonRedisSerializer.GenericJackson2JsonRedisSerializerBuilder}.
*/
public GenericJackson2JsonRedisSerializerBuilder registerNullValueSerializer(boolean registerNullValueSerializer) {
this.registerNullValueSerializer = registerNullValueSerializer;
return this;
}

/**
* Creates a new instance of {@link GenericJackson2JsonRedisSerializer} with configuration options applied. Creates
* also a new {@link ObjectMapper} if none was provided.
*
* @return a new instance of {@link GenericJackson2JsonRedisSerializer}.
*/
public GenericJackson2JsonRedisSerializer build() {
Assert.notNull(this.mapper, "ObjectMapper must not be null");
Assert.notNull(this.reader, "Reader must not be null");
Assert.notNull(this.writer, "Writer must not be null");

this.mapper.registerModule(new SimpleModule().addSerializer(this.nullValueSerializer != null ? this.nullValueSerializer : new NullValueSerializer(this.classPropertyTypeName)));
return new GenericJackson2JsonRedisSerializer(this.mapper, this.reader, this.writer, this.classPropertyTypeName);
ObjectMapper objectMapper = this.objectMapper;
boolean providedObjectMapper = objectMapper != null;

if (objectMapper == null) {
objectMapper = new ObjectMapper();
}

if (registerNullValueSerializer) {
objectMapper.registerModule(new SimpleModule("GenericJackson2JsonRedisSerializerBuilder")
.addSerializer(this.nullValueSerializer != null ? this.nullValueSerializer
: new NullValueSerializer(this.typeHintPropertyName)));
}

if ((!providedObjectMapper && (defaultTyping == null || defaultTyping))
|| (defaultTyping != null && defaultTyping)) {
objectMapper.setDefaultTyping(createDefaultTypeResolverBuilder(objectMapper, typeHintPropertyName));
}

return new GenericJackson2JsonRedisSerializer(objectMapper, this.reader, this.writer, this.typeHintPropertyName);
}
}

Expand Down
Loading

0 comments on commit fb140e3

Please sign in to comment.