From b6dadf58aa31f809fd0f8f762e640d356f0d960e Mon Sep 17 00:00:00 2001 From: Greg Gibeling Date: Wed, 17 Dec 2025 14:04:23 -0800 Subject: [PATCH] G2-1794 Jackson injection --- .../serdes/injection/HJackonInjection.java | 20 ++++++ .../injection/JacksonInjectedValue.java | 67 +++++++++++++++++++ .../serdes/injection/ExampleObject.java | 21 ++++++ .../injection/ExampleObjectDeserializer.java | 24 +++++++ .../injection/ExampleObjectSerializer.java | 23 +++++++ .../serdes/injection/TestInjection.java | 23 +++++++ 6 files changed, 178 insertions(+) create mode 100644 gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/HJackonInjection.java create mode 100644 gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/JacksonInjectedValue.java create mode 100644 gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObject.java create mode 100644 gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectDeserializer.java create mode 100644 gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectSerializer.java create mode 100644 gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/TestInjection.java diff --git a/gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/HJackonInjection.java b/gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/HJackonInjection.java new file mode 100644 index 0000000..08029f2 --- /dev/null +++ b/gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/HJackonInjection.java @@ -0,0 +1,20 @@ +package com.g2forge.gearbox.serdes.injection; + +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.cfg.ContextAttributes; + +public class HJackonInjection { + public ObjectMapper with(ObjectMapper mapper, Map injectables) { + final SerializationConfig initialSerializationConfig = mapper.getSerializationConfig(); + final ContextAttributes attributes = initialSerializationConfig.getAttributes(); + final ContextAttributes attributesWithInjectedValue = attributes.withSharedAttributes(injectables); + final SerializationConfig serializationConfigWithInjectedValue = initialSerializationConfig.with(attributesWithInjectedValue); + + mapper.setInjectableValues(null); + + return mapper.setConfig(serializationConfigWithInjectedValue); + } +} diff --git a/gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/JacksonInjectedValue.java b/gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/JacksonInjectedValue.java new file mode 100644 index 0000000..2bd1b3e --- /dev/null +++ b/gb-serdes/src/main/java/com/g2forge/gearbox/serdes/injection/JacksonInjectedValue.java @@ -0,0 +1,67 @@ +package com.g2forge.gearbox.serdes.injection; + +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.cfg.ContextAttributes; +import com.fasterxml.jackson.databind.exc.MissingInjectableValueExcepion; +import com.g2forge.alexandria.java.type.ref.ITypeRef; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@Builder(toBuilder = true) +@RequiredArgsConstructor +public class JacksonInjectedValue { + protected final String id; + + protected final ITypeRef type; + + protected final T fallback; + + public JacksonInjectedValue(Class owner, String name, ITypeRef type, T fallback) { + this(owner.getName() + "." + name, type, fallback); + } + + public JacksonInjectedValue(Class type, T fallback) { + this(type.getName(), ITypeRef.of(type), fallback); + } + + public ObjectMapper inject(ObjectMapper mapper, T value) { + final SerializationConfig initialSerializationConfig = mapper.getSerializationConfig(); + final ContextAttributes attributes = initialSerializationConfig.getAttributes(); + final ContextAttributes attributesWithInjectedValue = attributes.withSharedAttribute(getId(), value); + final SerializationConfig serializationConfigWithInjectedValue = initialSerializationConfig.with(attributesWithInjectedValue); + + final InjectableValues existingInjectableValues = mapper.getInjectableValues(); + if (existingInjectableValues == null) mapper.setInjectableValues(new InjectableValues.Std().addValue(getId(), value)); + else if (!(existingInjectableValues instanceof InjectableValues.Std)) throw new IllegalArgumentException(); + else((InjectableValues.Std) existingInjectableValues).addValue(getId(), value); + + return mapper.setConfig(serializationConfigWithInjectedValue); + } + + public T get(SerializerProvider provider) { + final Object value = provider.getAttribute(getId()); + if (value == null) return getFallback(); + return getType().cast(value); + } + + public T get(DeserializationContext context) { + final Object value; + try { + value = context.findInjectableValue(getId(), null, null, null, null); + } catch (MissingInjectableValueExcepion e) { + return getFallback(); + } catch (JsonMappingException e) { + throw new IllegalStateException(e); + } + if (value == null) return getFallback(); + return getType().cast(value); + } +} diff --git a/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObject.java b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObject.java new file mode 100644 index 0000000..d399cba --- /dev/null +++ b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObject.java @@ -0,0 +1,21 @@ +package com.g2forge.gearbox.serdes.injection; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.g2forge.alexandria.java.function.IFunction1; +import com.g2forge.alexandria.java.type.ref.ATypeRef; + +import lombok.Builder; +import lombok.Data; +import lombok.RequiredArgsConstructor; + +@Data +@Builder(toBuilder = true) +@RequiredArgsConstructor +@JsonSerialize(using = ExampleObjectSerializer.class) +@JsonDeserialize(using = ExampleObjectDeserializer.class) +public class ExampleObject { + public static final JacksonInjectedValue> PREPROCESSOR = new JacksonInjectedValue<>(ExampleObject.class, "preprocessor", new ATypeRef>() {}, IFunction1.identity()); + + protected final Integer value; +} diff --git a/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectDeserializer.java b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectDeserializer.java new file mode 100644 index 0000000..6e6cd96 --- /dev/null +++ b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectDeserializer.java @@ -0,0 +1,24 @@ +package com.g2forge.gearbox.serdes.injection; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; + +public class ExampleObjectDeserializer extends StdDeserializer { + private static final long serialVersionUID = 2867578675754843411L; + + public ExampleObjectDeserializer() { + super(ExampleObject.class); + } + + @Override + public ExampleObject deserialize(JsonParser parser, DeserializationContext context) throws IOException, JacksonException { + final JsonNode node = parser.getCodec().readTree(parser); + final int value = node.get("value").asInt(); + return new ExampleObject(ExampleObject.PREPROCESSOR.get(context).apply(value)); + } +} diff --git a/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectSerializer.java b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectSerializer.java new file mode 100644 index 0000000..5bffa3d --- /dev/null +++ b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/ExampleObjectSerializer.java @@ -0,0 +1,23 @@ +package com.g2forge.gearbox.serdes.injection; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +public class ExampleObjectSerializer extends StdSerializer { + private static final long serialVersionUID = 7620126831613439913L; + + public ExampleObjectSerializer() { + super(ExampleObject.class); + } + + @Override + public void serialize(ExampleObject value, JsonGenerator jsonGenerator, SerializerProvider provider) throws IOException, JsonProcessingException { + jsonGenerator.writeStartObject(); + jsonGenerator.writeNumberField("value", ExampleObject.PREPROCESSOR.get(provider).apply(value.getValue())); + jsonGenerator.writeEndObject(); + } +} diff --git a/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/TestInjection.java b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/TestInjection.java new file mode 100644 index 0000000..b49f66a --- /dev/null +++ b/gb-serdes/src/test/java/com/g2forge/gearbox/serdes/injection/TestInjection.java @@ -0,0 +1,23 @@ +package com.g2forge.gearbox.serdes.injection; + +import org.junit.Test; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.g2forge.alexandria.test.HAssert; + +public class TestInjection { + @Test + public void noinjection() throws JsonMappingException, JsonProcessingException { + final ExampleObject actual = new ExampleObject(0); + final ObjectMapper mapper = new ObjectMapper(); + HAssert.assertEquals(actual, mapper.readValue(mapper.writeValueAsString(actual), ExampleObject.class)); + } + + @Test + public void increment() throws JsonMappingException, JsonProcessingException { + final ObjectMapper mapper = ExampleObject.PREPROCESSOR.inject(new ObjectMapper(), x -> x + 1);; + HAssert.assertEquals(new ExampleObject(2), mapper.readValue(mapper.writeValueAsString(new ExampleObject(0)), ExampleObject.class)); + } +}