Skip to content

Commit

Permalink
Ensure that custom Jackson modules work in dev-mode
Browse files Browse the repository at this point in the history
  • Loading branch information
geoand committed Nov 8, 2024
1 parent 5be2257 commit ef112b8
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package io.quarkus.resteasy.reactive.jackson.deployment.test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.containsString;

import java.io.IOException;

import jakarta.inject.Singleton;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.test.QuarkusDevModeTest;
import io.vertx.core.json.JsonArray;

public class CustomModuleLiveReloadTest {

@RegisterExtension
static final QuarkusDevModeTest TEST = new QuarkusDevModeTest()
.withApplicationRoot((jar) -> jar
.addClasses(Resource.class, StringAndInt.class, StringAndIntSerializer.class,
StringAndIntDeserializer.class, Customizer.class)
.addAsResource(new StringAsset("index content"), "META-INF/resources/index.html"));

@Test
void test() {
assertResponse();

// force reload
TEST.addResourceFile("META-INF/resources/index.html", "html content");

assertResponse();
}

private static void assertResponse() {
given().accept("application/json").get("test/array")
.then()
.statusCode(200)
.body(containsString("first:1"), containsString("second:2"));
}

@Path("test")
public static class Resource {

@Path("array")
@GET
@Produces(MediaType.APPLICATION_JSON)
public JsonArray array() {
var array = new JsonArray();
array.add(new StringAndInt("first", 1));
array.add(new StringAndInt("second", 2));
return array;
}
}

public static class StringAndInt {
private final String stringValue;
private final int intValue;

public StringAndInt(String s, int i) {
this.stringValue = s;
this.intValue = i;
}

public static StringAndInt parse(String value) {
if (value == null) {
return null;
}
int dot = value.indexOf(':');
if (-1 == dot) {
throw new IllegalArgumentException(value);
}
try {
return new StringAndInt(value.substring(0, dot), Integer.parseInt(value.substring(dot + 1)));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(value, e);
}
}

public String format() {
return this.stringValue + ":" + intValue;
}
}

public static class StringAndIntSerializer extends StdSerializer<StringAndInt> {

public StringAndIntSerializer() {
super(StringAndInt.class);
}

@Override
public void serialize(StringAndInt value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (value == null)
gen.writeNull();
else {
gen.writeString(value.format());
}
}
}

public static class StringAndIntDeserializer extends StdDeserializer<StringAndInt> {

public StringAndIntDeserializer() {
super(StringAndInt.class);
}

@Override
public StringAndInt deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.currentToken() == JsonToken.VALUE_STRING) {
return StringAndInt.parse(p.getText());
} else if (p.currentToken() == JsonToken.VALUE_NULL) {
return null;
}
return null;
}
}

@Singleton
public static class Customizer implements ObjectMapperCustomizer {
@Override
public void customize(ObjectMapper objectMapper) {
var m = new SimpleModule("test");
m.addSerializer(StringAndInt.class, new StringAndIntSerializer());
m.addDeserializer(StringAndInt.class, new StringAndIntDeserializer());
objectMapper.registerModule(m);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.vertx.core.deployment;

import io.quarkus.deployment.IsDevelopment;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
Expand Down Expand Up @@ -50,6 +51,7 @@
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.logging.LogCleanupFilterBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import io.quarkus.gizmo.Gizmo;
import io.quarkus.netty.deployment.EventLoopSupplierBuildItem;
import io.quarkus.runtime.ThreadPoolConfig;
Expand Down Expand Up @@ -286,6 +288,12 @@ ContextHandlerBuildItem createVertxContextHandlers(VertxCoreRecorder recorder, V
return new ContextHandlerBuildItem(recorder.executionContextHandler(buildConfig.customizeArcContext()));
}

@BuildStep(onlyIf = IsDevelopment.class)
@Record(ExecutionTime.RUNTIME_INIT)
public void resetMapper(VertxCoreRecorder recorder, ShutdownContextBuildItem shutdown) {
recorder.resetMapper(shutdown);
}

private void handleBlockingWarningsInDevOrTestMode() {
try {
Filter debuggerFilter = createDebuggerFilter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import io.quarkus.vertx.core.runtime.context.VertxContextSafetyToggle;
import io.quarkus.vertx.mdc.provider.LateBoundMDCProvider;
import io.quarkus.vertx.runtime.VertxCurrentContextFactory;
import io.quarkus.vertx.runtime.jackson.QuarkusJacksonFactory;
import io.vertx.core.AsyncResult;
import io.vertx.core.Context;
import io.vertx.core.Handler;
Expand Down Expand Up @@ -583,6 +584,15 @@ public Thread newThread(Runnable runnable) {
};
}

public void resetMapper(ShutdownContext shutdown) {
shutdown.addShutdownTask(new Runnable() {
@Override
public void run() {
QuarkusJacksonFactory.reset();
}
});
}

private static void setNewThreadTccl(VertxThread thread) {
ClassLoader cl = VertxCoreRecorder.currentDevModeNewThreadCreationClassLoader;
if (cl == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.vertx.runtime.jackson;

import java.util.concurrent.atomic.AtomicInteger;

import io.vertx.core.json.jackson.DatabindCodec;
import io.vertx.core.json.jackson.JacksonCodec;
import io.vertx.core.spi.JsonFactory;
Expand All @@ -10,6 +12,8 @@
*/
public class QuarkusJacksonFactory implements JsonFactory {

private static final AtomicInteger COUNTER = new AtomicInteger();

@Override
public JsonCodec codec() {
JsonCodec codec;
Expand All @@ -25,7 +29,15 @@ public JsonCodec codec() {
codec = new JacksonCodec();
}
}
COUNTER.incrementAndGet();
return codec;
}

public static void reset() {
// if we blindly reset, we could get NCDFE because Jackson classes would not have been loaded
if (COUNTER.get() > 0) {
QuarkusJacksonJsonCodec.reset();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,31 @@
*/
class QuarkusJacksonJsonCodec implements JsonCodec {

private static final ObjectMapper mapper;
private static volatile ObjectMapper mapper;
// we don't want to create this unless it's absolutely necessary (and it rarely is)
private static volatile ObjectMapper prettyMapper;

static {
populateMapper();
}

public static void reset() {
mapper = null;
prettyMapper = null;
}

private static ObjectMapper mapper() {
if (mapper == null) {
synchronized (QuarkusJacksonJsonCodec.class) {
if (mapper == null) {
populateMapper();
}
}
}
return mapper;
}

private static void populateMapper() {
ArcContainer container = Arc.container();
if (container == null) {
// this can happen in QuarkusUnitTest
Expand Down Expand Up @@ -74,7 +94,7 @@ class QuarkusJacksonJsonCodec implements JsonCodec {

private ObjectMapper prettyMapper() {
if (prettyMapper == null) {
prettyMapper = mapper.copy();
prettyMapper = mapper().copy();
prettyMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
}
return prettyMapper;
Expand All @@ -83,7 +103,7 @@ private ObjectMapper prettyMapper() {
@SuppressWarnings("unchecked")
@Override
public <T> T fromValue(Object json, Class<T> clazz) {
T value = QuarkusJacksonJsonCodec.mapper.convertValue(json, clazz);
T value = QuarkusJacksonJsonCodec.mapper().convertValue(json, clazz);
if (clazz == Object.class) {
value = (T) adapt(value);
}
Expand All @@ -102,7 +122,7 @@ public <T> T fromBuffer(Buffer buf, Class<T> clazz) throws DecodeException {

public static JsonParser createParser(Buffer buf) {
try {
return QuarkusJacksonJsonCodec.mapper.getFactory()
return QuarkusJacksonJsonCodec.mapper().getFactory()
.createParser((InputStream) new ByteBufInputStream(buf.getByteBuf()));
} catch (IOException e) {
throw new DecodeException("Failed to decode:" + e.getMessage(), e);
Expand All @@ -111,7 +131,7 @@ public static JsonParser createParser(Buffer buf) {

public static JsonParser createParser(String str) {
try {
return QuarkusJacksonJsonCodec.mapper.getFactory().createParser(str);
return QuarkusJacksonJsonCodec.mapper().getFactory().createParser(str);
} catch (IOException e) {
throw new DecodeException("Failed to decode:" + e.getMessage(), e);
}
Expand All @@ -122,7 +142,7 @@ public static <T> T fromParser(JsonParser parser, Class<T> type) throws DecodeEx
T value;
JsonToken remaining;
try {
value = QuarkusJacksonJsonCodec.mapper.readValue(parser, type);
value = QuarkusJacksonJsonCodec.mapper().readValue(parser, type);
remaining = parser.nextToken();
} catch (Exception e) {
throw new DecodeException("Failed to decode:" + e.getMessage(), e);
Expand All @@ -141,7 +161,7 @@ public static <T> T fromParser(JsonParser parser, Class<T> type) throws DecodeEx
@Override
public String toString(Object object, boolean pretty) throws EncodeException {
try {
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper;
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper();
return mapper.writeValueAsString(object);
} catch (Exception e) {
throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e);
Expand All @@ -151,7 +171,7 @@ public String toString(Object object, boolean pretty) throws EncodeException {
@Override
public Buffer toBuffer(Object object, boolean pretty) throws EncodeException {
try {
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper;
ObjectMapper mapper = pretty ? prettyMapper() : QuarkusJacksonJsonCodec.mapper();
return Buffer.buffer(mapper.writeValueAsBytes(object));
} catch (Exception e) {
throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e);
Expand Down

0 comments on commit ef112b8

Please sign in to comment.