diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/console/DeploymentLinker.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/DeploymentLinker.java
new file mode 100644
index 0000000000000..bad32c12250e6
--- /dev/null
+++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/DeploymentLinker.java
@@ -0,0 +1,36 @@
+package io.quarkus.dev.console;
+
+import java.util.Map;
+
+/**
+ * Creates "links" to objects between deployment and runtime,
+ * essentially exposing the same interface but on a different classloader.
+ *
+ * This implies all communication must go through JDK classes, so the transfer involves Maps, Functions, ...
+ * Yes this is awful. No there's no better solution ATM.
+ * Ideally we'd automate this through bytecode generation,
+ * but feasibility is uncertain, and we'd need a volunteer who has time for that.
+ *
+ * Implementations should live in the runtime module.
+ * To transfer {@link #createLinkData(Object) link data} between deployment and runtime,
+ * see {@link DevConsoleManager#setGlobal(String, Object)} and {@link DevConsoleManager#getGlobal(String)}.
+ */
+public interface DeploymentLinker {
+
+ /**
+ * @param object An object implementing class {@code T} in either the current classloader.
+ * @return A classloader-independent map containing Functions, Suppliers, etc.
+ * giving access to the object's methods,
+ * which will be passed to {@link #createLink(Map)} from the other classloader.
+ */
+ Map createLinkData(T object);
+
+ /**
+ * @param linkData The result of calling {@link #createLinkData(Object)}.
+ * @return An object implementing class {@code T} in the current classloader
+ * and redirecting calls to the Functions, Suppliers, etc. from {@code linkData},
+ * thereby linking to the implementation in its original classloader.
+ */
+ T createLink(Map linkData);
+
+}
diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java
index b56db0e82332e..53edd36dc8eff 100644
--- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java
+++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/DevUIProcessor.java
@@ -39,12 +39,15 @@
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem;
+import io.quarkus.dev.console.DevConsoleManager;
import io.quarkus.devui.deployment.extension.Codestart;
import io.quarkus.devui.deployment.extension.Extension;
+import io.quarkus.devui.deployment.jsonrpc.DevUIDatabindCodec;
import io.quarkus.devui.runtime.DevUIRecorder;
import io.quarkus.devui.runtime.comms.JsonRpcRouter;
import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod;
import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName;
+import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
import io.quarkus.devui.spi.DevUIContent;
import io.quarkus.devui.spi.JsonRPCProvidersBuildItem;
import io.quarkus.devui.spi.buildtime.StaticContentBuildItem;
@@ -334,6 +337,8 @@ void createJsonRpcRouter(DevUIRecorder recorder,
Map> extensionMethodsMap = jsonRPCMethodsBuildItem
.getExtensionMethodsMap();
+ DevConsoleManager.setGlobal(DevUIRecorder.DEV_MANAGER_GLOBALS_JSON_MAPPER_FACTORY,
+ JsonMapper.Factory.deploymentLinker().createLinkData(new DevUIDatabindCodec.Factory()));
recorder.createJsonRpcRouter(beanContainer.getValue(), extensionMethodsMap);
}
}
diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java
new file mode 100644
index 0000000000000..b9b8af66a9d74
--- /dev/null
+++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java
@@ -0,0 +1,176 @@
+package io.quarkus.devui.deployment.jsonrpc;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.time.Instant;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+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.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+
+import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
+import io.quarkus.devui.runtime.jsonrpc.json.JsonTypeAdapter;
+import io.quarkus.vertx.runtime.jackson.ByteArrayDeserializer;
+import io.quarkus.vertx.runtime.jackson.ByteArraySerializer;
+import io.quarkus.vertx.runtime.jackson.InstantDeserializer;
+import io.quarkus.vertx.runtime.jackson.InstantSerializer;
+import io.vertx.core.json.DecodeException;
+import io.vertx.core.json.EncodeException;
+
+public class DevUIDatabindCodec implements JsonMapper {
+ private final ObjectMapper mapper;
+ private final ObjectMapper prettyMapper;
+ private final Function, ?> runtimeObjectDeserializer;
+ private final Function, ?> runtimeArrayDeserializer;
+
+ private DevUIDatabindCodec(ObjectMapper mapper,
+ Function, ?> runtimeObjectDeserializer,
+ Function, ?> runtimeArrayDeserializer) {
+ this.mapper = mapper;
+ prettyMapper = mapper.copy();
+ prettyMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
+ this.runtimeObjectDeserializer = runtimeObjectDeserializer;
+ this.runtimeArrayDeserializer = runtimeArrayDeserializer;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T fromValue(Object json, Class clazz) {
+ T value = mapper.convertValue(json, clazz);
+ if (clazz == Object.class) {
+ value = (T) adapt(value);
+ }
+ return value;
+ }
+
+ @Override
+ public T fromString(String str, Class clazz) throws DecodeException {
+ return fromParser(createParser(str), clazz);
+ }
+
+ private JsonParser createParser(String str) {
+ try {
+ return mapper.getFactory().createParser(str);
+ } catch (IOException e) {
+ throw new DecodeException("Failed to decode:" + e.getMessage(), e);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private T fromParser(JsonParser parser, Class type) throws DecodeException {
+ T value;
+ JsonToken remaining;
+ try {
+ value = mapper.readValue(parser, type);
+ remaining = parser.nextToken();
+ } catch (Exception e) {
+ throw new DecodeException("Failed to decode:" + e.getMessage(), e);
+ } finally {
+ close(parser);
+ }
+ if (remaining != null) {
+ throw new DecodeException("Unexpected trailing token");
+ }
+ if (type == Object.class) {
+ value = (T) adapt(value);
+ }
+ return value;
+ }
+
+ @Override
+ public String toString(Object object, boolean pretty) throws EncodeException {
+ try {
+ ObjectMapper theMapper = pretty ? prettyMapper : mapper;
+ return theMapper.writeValueAsString(object);
+ } catch (Exception e) {
+ throw new EncodeException("Failed to encode as JSON: " + e.getMessage(), e);
+ }
+ }
+
+ private static void close(Closeable parser) {
+ try {
+ parser.close();
+ } catch (IOException ignore) {
+ }
+ }
+
+ private Object adapt(Object o) {
+ try {
+ if (o instanceof List) {
+ List> list = (List>) o;
+ return runtimeArrayDeserializer.apply(list);
+ } else if (o instanceof Map) {
+ @SuppressWarnings("unchecked")
+ Map map = (Map) o;
+ return runtimeObjectDeserializer.apply(map);
+ }
+ return o;
+ } catch (Exception e) {
+ throw new DecodeException("Failed to decode: " + e.getMessage());
+ }
+ }
+
+ public static final class Factory implements JsonMapper.Factory {
+ @Override
+ public JsonMapper create(JsonTypeAdapter, Map> jsonObjectAdapter,
+ JsonTypeAdapter, List>> jsonArrayAdapter, JsonTypeAdapter, String> bufferAdapter) {
+ // We want our own mapper, separate from the user-configured one.
+ ObjectMapper mapper = new ObjectMapper();
+
+ // Non-standard JSON but we allow C style comments in our JSON
+ mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+
+ SimpleModule module = new SimpleModule("vertx-module-common");
+ module.addSerializer(Instant.class, new InstantSerializer());
+ module.addDeserializer(Instant.class, new InstantDeserializer());
+ module.addSerializer(byte[].class, new ByteArraySerializer());
+ module.addDeserializer(byte[].class, new ByteArrayDeserializer());
+ mapper.registerModule(module);
+
+ SimpleModule runtimeModule = new SimpleModule("vertx-module-runtime");
+ addAdapterToObject(runtimeModule, jsonObjectAdapter);
+ addAdapterToObject(runtimeModule, jsonArrayAdapter);
+ addAdapterToString(runtimeModule, bufferAdapter);
+ mapper.registerModule(runtimeModule);
+
+ return new DevUIDatabindCodec(mapper, jsonObjectAdapter.deserializer, jsonArrayAdapter.deserializer);
+ }
+
+ private static void addAdapterToObject(SimpleModule module, JsonTypeAdapter adapter) {
+ module.addSerializer(adapter.type, new JsonSerializer<>() {
+ @Override
+ public void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+ jgen.writeObject(adapter.serializer.apply(value));
+ }
+ });
+ }
+
+ private static void addAdapterToString(SimpleModule module, JsonTypeAdapter adapter) {
+ module.addSerializer(adapter.type, new JsonSerializer<>() {
+ @Override
+ public void serialize(T value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+ jgen.writeString(adapter.serializer.apply(value));
+ }
+ });
+ module.addDeserializer(adapter.type, new JsonDeserializer() {
+ @Override
+ public T deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
+ return adapter.deserializer.apply(parser.getText());
+ }
+ });
+ }
+ }
+
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java
index 086b5fb15ed68..6702ee7c3ce43 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/DevUIRecorder.java
@@ -1,5 +1,8 @@
package io.quarkus.devui.runtime;
+import static io.quarkus.vertx.runtime.jackson.JsonUtil.BASE64_DECODER;
+import static io.quarkus.vertx.runtime.jackson.JsonUtil.BASE64_ENCODER;
+
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileVisitResult;
@@ -15,19 +18,26 @@
import org.jboss.logging.Logger;
import io.quarkus.arc.runtime.BeanContainer;
+import io.quarkus.dev.console.DevConsoleManager;
import io.quarkus.devui.runtime.comms.JsonRpcRouter;
import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod;
import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName;
+import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
+import io.quarkus.devui.runtime.jsonrpc.json.JsonTypeAdapter;
import io.quarkus.runtime.ShutdownContext;
import io.quarkus.runtime.annotations.Recorder;
import io.quarkus.vertx.http.runtime.devmode.FileSystemStaticHandler;
import io.quarkus.vertx.http.runtime.webjar.WebJarStaticHandler;
import io.vertx.core.Handler;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
@Recorder
public class DevUIRecorder {
private static final Logger LOG = Logger.getLogger(DevUIRecorder.class);
+ public static final String DEV_MANAGER_GLOBALS_JSON_MAPPER_FACTORY = "dev-ui-databind-codec-builder";
public void shutdownTask(ShutdownContext shutdownContext, String devUIBasePath) {
shutdownContext.addShutdownTask(new DeleteDirectoryRunnable(devUIBasePath));
@@ -37,6 +47,25 @@ public void createJsonRpcRouter(BeanContainer beanContainer,
Map> extensionMethodsMap) {
JsonRpcRouter jsonRpcRouter = beanContainer.beanInstance(JsonRpcRouter.class);
jsonRpcRouter.populateJsonRPCMethods(extensionMethodsMap);
+ jsonRpcRouter.initializeCodec(createJsonMapper());
+ }
+
+ private JsonMapper createJsonMapper() {
+ // We use a codec defined in the deployment module
+ // because that module always has access to Jackson-Databind regardless of the application dependencies.
+ JsonMapper.Factory factory = JsonMapper.Factory.deploymentLinker().createLink(
+ DevConsoleManager.getGlobal(DEV_MANAGER_GLOBALS_JSON_MAPPER_FACTORY));
+ // We need to pass some information so that the mapper, who lives in the deployment classloader,
+ // knows how to deal with JsonObject/JsonArray/JsonBuffer, who live in the runtime classloader.
+ return factory.create(new JsonTypeAdapter<>(JsonObject.class, JsonObject::getMap, JsonObject::new),
+ new JsonTypeAdapter>(JsonArray.class, JsonArray::getList, JsonArray::new),
+ new JsonTypeAdapter<>(Buffer.class, buffer -> BASE64_ENCODER.encodeToString(buffer.getBytes()), text -> {
+ try {
+ return Buffer.buffer(BASE64_DECODER.decode(text));
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Expected a base64 encoded byte array, got: " + text, e);
+ }
+ }));
}
public Handler communicationHandler() {
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java
index 0a1a173c1f53f..b55dc0e71fe39 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/comms/JsonRpcRouter.java
@@ -17,10 +17,11 @@
import org.jboss.logging.Logger;
import io.quarkus.arc.Arc;
+import io.quarkus.devui.runtime.jsonrpc.JsonRpcCodec;
import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethod;
import io.quarkus.devui.runtime.jsonrpc.JsonRpcMethodName;
-import io.quarkus.devui.runtime.jsonrpc.JsonRpcReader;
-import io.quarkus.devui.runtime.jsonrpc.JsonRpcWriter;
+import io.quarkus.devui.runtime.jsonrpc.JsonRpcRequest;
+import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
@@ -28,7 +29,6 @@
import io.smallrye.mutiny.subscription.Cancellable;
import io.smallrye.mutiny.unchecked.Unchecked;
import io.vertx.core.http.ServerWebSocket;
-import io.vertx.core.json.JsonObject;
/**
* Route JsonRPC message to the correct method
@@ -41,6 +41,7 @@ public class JsonRpcRouter {
private final Map jsonRpcToJava = new HashMap<>();
private static final List SESSIONS = Collections.synchronizedList(new ArrayList<>());
+ private JsonRpcCodec codec;
/**
* This gets called on build to build into of the classes we are going to call in runtime
@@ -79,6 +80,10 @@ public void populateJsonRPCMethods(Map invoke(ReflectionInfo info, Object target, Object[] args) {
if (info.isReturningUni()) {
try {
@@ -104,7 +109,7 @@ private Uni> invoke(ReflectionInfo info, Object target, Object[] args) {
public void addSocket(ServerWebSocket socket) {
SESSIONS.add(socket);
socket.textMessageHandler((e) -> {
- JsonRpcReader jsonRpcRequest = JsonRpcReader.read(e);
+ JsonRpcRequest jsonRpcRequest = codec.readRequest(e);
route(jsonRpcRequest, socket);
}).closeHandler((e) -> {
purge();
@@ -116,8 +121,7 @@ void onStart(@Observes StartupEvent ev) {
purge();
for (ServerWebSocket s : new ArrayList<>(SESSIONS)) {
if (!s.isClosed()) {
- s.writeTextMessage(
- JsonRpcWriter.writeResponse(-1, LocalDateTime.now().toString(), MessageType.HotReload).encode());
+ codec.writeResponse(s, -1, LocalDateTime.now().toString(), MessageType.HotReload);
}
}
}
@@ -134,18 +138,16 @@ private void purge() {
Logger logger;
@SuppressWarnings("unchecked")
- private void route(JsonRpcReader jsonRpcRequest, ServerWebSocket s) {
+ private void route(JsonRpcRequest jsonRpcRequest, ServerWebSocket s) {
String jsonRpcMethodName = jsonRpcRequest.getMethod();
// First check some internal methods
if (jsonRpcMethodName.equalsIgnoreCase(UNSUBSCRIBE)) {
- JsonObject jsonRpcResponse = JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), null, MessageType.Void);
-
if (this.subscriptions.containsKey(jsonRpcRequest.getId())) {
Cancellable cancellable = this.subscriptions.remove(jsonRpcRequest.getId());
cancellable.cancel();
}
- s.writeTextMessage(jsonRpcResponse.encode());
+ codec.writeResponse(s, jsonRpcRequest.getId(), null, MessageType.Void);
} else if (this.jsonRpcToJava.containsKey(jsonRpcMethodName)) { // Route to extension
ReflectionInfo reflectionInfo = this.jsonRpcToJava.get(jsonRpcMethodName);
Object target = Arc.container().select(reflectionInfo.bean).get();
@@ -162,26 +164,23 @@ private void route(JsonRpcReader jsonRpcRequest, ServerWebSocket s) {
} catch (Exception e) {
logger.errorf(e, "Unable to invoke method %s using JSON-RPC, request was: %s", jsonRpcMethodName,
jsonRpcRequest);
- s.writeTextMessage(JsonRpcWriter.writeErrorResponse(jsonRpcRequest.getId(), jsonRpcMethodName, e).encode());
+ codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, e);
return;
}
Cancellable cancellable = multi.subscribe()
.with(
item -> {
- JsonObject jsonResponse = JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), item,
- MessageType.SubscriptionMessage);
- s.writeTextMessage(jsonResponse.encodePrettily());
+ codec.writeResponse(s, jsonRpcRequest.getId(), item, MessageType.SubscriptionMessage);
},
failure -> {
- s.writeTextMessage(JsonRpcWriter
- .writeErrorResponse(jsonRpcRequest.getId(), jsonRpcMethodName, failure).encode());
+ codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, failure);
this.subscriptions.remove(jsonRpcRequest.getId());
},
() -> this.subscriptions.remove(jsonRpcRequest.getId()));
this.subscriptions.put(jsonRpcRequest.getId(), cancellable);
- s.writeTextMessage(JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), null, MessageType.Void).encode());
+ codec.writeResponse(s, jsonRpcRequest.getId(), null, MessageType.Void);
} else {
// The invocation will return a Uni
Uni> uni;
@@ -195,32 +194,30 @@ private void route(JsonRpcReader jsonRpcRequest, ServerWebSocket s) {
} catch (Exception e) {
logger.errorf(e, "Unable to invoke method %s using JSON-RPC, request was: %s", jsonRpcMethodName,
jsonRpcRequest);
- s.writeTextMessage(JsonRpcWriter.writeErrorResponse(jsonRpcRequest.getId(), jsonRpcMethodName, e).encode());
+ codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, e);
return;
}
uni.subscribe()
.with(item -> {
- s.writeTextMessage(JsonRpcWriter.writeResponse(jsonRpcRequest.getId(), item,
- MessageType.Response).encode());
+ codec.writeResponse(s, jsonRpcRequest.getId(), item,
+ MessageType.Response);
}, failure -> {
- s.writeTextMessage(JsonRpcWriter
- .writeErrorResponse(jsonRpcRequest.getId(), jsonRpcMethodName, failure).encode());
+ codec.writeErrorResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName, failure);
});
}
} else {
// Method not found
- s.writeTextMessage(JsonRpcWriter.writeMethodNotFoundResponse(jsonRpcRequest.getId(), jsonRpcMethodName).encode());
+ codec.writeMethodNotFoundResponse(s, jsonRpcRequest.getId(), jsonRpcMethodName);
}
}
- private Object[] getArgsAsObjects(Map params, JsonRpcReader jsonRpcRequest) {
+ private Object[] getArgsAsObjects(Map params, JsonRpcRequest jsonRpcRequest) {
List objects = new ArrayList<>();
for (Map.Entry expectedParams : params.entrySet()) {
String paramName = expectedParams.getKey();
Class paramType = expectedParams.getValue();
- Object param = jsonRpcRequest.getParam(paramName);
- Object casted = paramType.cast(param);
- objects.add(casted);
+ Object param = jsonRpcRequest.getParam(paramName, paramType);
+ objects.add(param);
}
return objects.toArray(Object[]::new);
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java
new file mode 100644
index 0000000000000..d7c1c601233ef
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcCodec.java
@@ -0,0 +1,43 @@
+package io.quarkus.devui.runtime.jsonrpc;
+
+import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.INTERNAL_ERROR;
+import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.METHOD_NOT_FOUND;
+import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MessageType;
+
+import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
+import io.vertx.core.http.ServerWebSocket;
+import io.vertx.core.json.JsonObject;
+
+public final class JsonRpcCodec {
+
+ private final JsonMapper jsonMapper;
+
+ public JsonRpcCodec(JsonMapper jsonMapper) {
+ this.jsonMapper = jsonMapper;
+ }
+
+ public JsonRpcRequest readRequest(String json) {
+ return new JsonRpcRequest(jsonMapper, (JsonObject) jsonMapper.fromString(json, Object.class));
+ }
+
+ public void writeResponse(ServerWebSocket socket, int id, Object object, MessageType messageType) {
+ writeResponse(socket, new JsonRpcResponse(id,
+ new JsonRpcResponse.Result(messageType.name(), object)));
+ }
+
+ public void writeMethodNotFoundResponse(ServerWebSocket socket, int id, String jsonRpcMethodName) {
+ writeResponse(socket, new JsonRpcResponse(id,
+ new JsonRpcResponse.Error(METHOD_NOT_FOUND, "Method [" + jsonRpcMethodName + "] not found")));
+ }
+
+ public void writeErrorResponse(ServerWebSocket socket, int id, String jsonRpcMethodName, Throwable exception) {
+ writeResponse(socket, new JsonRpcResponse(id,
+ new JsonRpcResponse.Error(INTERNAL_ERROR,
+ "Method [" + jsonRpcMethodName + "] failed: " + exception.getMessage())));
+ }
+
+ private void writeResponse(ServerWebSocket socker, JsonRpcResponse response) {
+ socker.writeTextMessage(jsonMapper.toString(response, true));
+ }
+
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java
similarity index 64%
rename from extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java
rename to extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java
index 4e59f83ab9100..bfcaf635d57bd 100644
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcReader.java
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcRequest.java
@@ -8,42 +8,40 @@
import java.util.Map;
-import io.vertx.core.json.Json;
+import io.quarkus.devui.runtime.jsonrpc.json.JsonMapper;
import io.vertx.core.json.JsonObject;
-public class JsonRpcReader {
+public class JsonRpcRequest {
+ private final JsonMapper jsonMapper;
private final JsonObject jsonObject;
- private JsonRpcReader(JsonObject jsonObject) {
+ JsonRpcRequest(JsonMapper jsonMapper, JsonObject jsonObject) {
+ this.jsonMapper = jsonMapper;
this.jsonObject = jsonObject;
}
- public static JsonRpcReader read(String json) {
- return new JsonRpcReader((JsonObject) Json.decodeValue(json));
- }
-
public int getId() {
return jsonObject.getInteger(ID);
}
public String getJsonrpc() {
- return jsonObject.getString(JSONRPC, VERSION);
+ String value = jsonObject.getString(JSONRPC);
+ if (value != null) {
+ return value;
+ }
+ return VERSION;
}
public String getMethod() {
return jsonObject.getString(METHOD);
}
- public boolean isMethod(String m) {
- return this.getMethod().equalsIgnoreCase(m);
- }
-
public boolean hasParams() {
return this.getParams() != null;
}
- public Map getParams() {
+ private Map, ?> getParams() {
JsonObject paramsObject = jsonObject.getJsonObject(PARAMS);
if (paramsObject != null && paramsObject.getMap() != null && !paramsObject.getMap().isEmpty()) {
return paramsObject.getMap();
@@ -51,18 +49,16 @@ public boolean hasParams() {
return null;
}
- @SuppressWarnings({ "unchecked", "unchecked" })
- public T getParam(String key) {
- Map params = getParams();
+ public T getParam(String key, Class paramType) {
+ Map, ?> params = getParams();
if (params == null || !params.containsKey(key)) {
return null;
}
- return (T) params.get(key);
+ return jsonMapper.fromValue(params.get(key), paramType);
}
@Override
public String toString() {
- return jsonObject.encodePrettily();
+ return jsonMapper.toString(jsonObject, true);
}
-
}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java
new file mode 100644
index 0000000000000..fe7687c638e52
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcResponse.java
@@ -0,0 +1,72 @@
+package io.quarkus.devui.runtime.jsonrpc;
+
+import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.VERSION;
+
+public final class JsonRpcResponse {
+
+ // Public for serialization
+ public final int id;
+ public final Result result;
+ public final Error error;
+
+ public JsonRpcResponse(int id, Result result) {
+ this.id = id;
+ this.result = result;
+ this.error = null;
+ }
+
+ public JsonRpcResponse(int id, Error error) {
+ this.id = id;
+ this.result = null;
+ this.error = error;
+ }
+
+ public String getJsonrpc() {
+ return VERSION;
+ }
+
+ @Override
+ public String toString() {
+ return "JsonRpcResponse{" +
+ "id=" + id +
+ ", result=" + result +
+ ", error=" + error +
+ '}';
+ }
+
+ public static final class Result {
+ public final String messageType;
+ public final Object object;
+
+ public Result(String messageType, Object object) {
+ this.messageType = messageType;
+ this.object = object;
+ }
+
+ @Override
+ public String toString() {
+ return "Result{" +
+ "messageType='" + messageType + '\'' +
+ ", object=" + object +
+ '}';
+ }
+ }
+
+ public static final class Error {
+ public final int code;
+ public final String message;
+
+ public Error(int code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return "Error{" +
+ "code=" + code +
+ ", message='" + message + '\'' +
+ '}';
+ }
+ }
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java
deleted file mode 100644
index b85680305903c..0000000000000
--- a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/JsonRpcWriter.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package io.quarkus.devui.runtime.jsonrpc;
-
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.CODE;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ERROR;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.ID;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.INTERNAL_ERROR;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.JSONRPC;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MESSAGE;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MESSAGE_TYPE;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.METHOD_NOT_FOUND;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.MessageType;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.OBJECT;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.RESULT;
-import static io.quarkus.devui.runtime.jsonrpc.JsonRpcKeys.VERSION;
-
-import io.vertx.core.json.JsonObject;
-
-public class JsonRpcWriter {
-
- private JsonRpcWriter() {
- }
-
- public static JsonObject writeResponse(int id, Object object, MessageType messageType) {
- JsonObject result = JsonObject.of();
- if (object != null) {
- result.put(OBJECT, object);
- }
- result.put(MESSAGE_TYPE, messageType.name());
-
- return JsonObject.of(
- ID, id,
- JSONRPC, VERSION,
- RESULT, result);
- }
-
- public static JsonObject writeMethodNotFoundResponse(int id, String jsonRpcMethodName) {
- JsonObject jsonRpcError = JsonObject.of(
- CODE, METHOD_NOT_FOUND,
- MESSAGE, "Method [" + jsonRpcMethodName + "] not found");
-
- return JsonObject.of(
- ID, id,
- JSONRPC, VERSION,
- ERROR, jsonRpcError);
- }
-
- public static JsonObject writeErrorResponse(int id, String jsonRpcMethodName, Throwable exception) {
- JsonObject jsonRpcError = JsonObject.of(
- CODE, INTERNAL_ERROR,
- MESSAGE, "Method [" + jsonRpcMethodName + "] failed: " + exception.getMessage());
-
- return JsonObject.of(
- ID, id,
- JSONRPC, VERSION,
- ERROR, jsonRpcError);
- }
-
-}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/json/JsonMapper.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/json/JsonMapper.java
new file mode 100644
index 0000000000000..cebd14786a363
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/json/JsonMapper.java
@@ -0,0 +1,131 @@
+package io.quarkus.devui.runtime.jsonrpc.json;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+import io.quarkus.dev.console.DeploymentLinker;
+
+public interface JsonMapper {
+
+ String toString(Object object, boolean pretty);
+
+ T fromString(String json, Class target);
+
+ T fromValue(Object json, Class target);
+
+ static DeploymentLinker deploymentLinker() {
+ return new DeploymentLinker<>() {
+ @Override
+ public Map createLinkData(JsonMapper object) {
+ return Map.of("delegate", object,
+ "toString", (BiFunction) object::toString,
+ "fromString", (BiFunction, Object>) object::fromString,
+ "fromValue", (BiFunction, Object>) object::fromValue);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public JsonMapper createLink(Map linkData) {
+ Object delegate = linkData.get("delegate");
+ BiFunction toString = (BiFunction) linkData.get("toString");
+ BiFunction, Object> fromString = (BiFunction, Object>) linkData
+ .get("fromString");
+ BiFunction, Object> fromValue = (BiFunction, Object>) linkData
+ .get("fromValue");
+ return new JsonMapper() {
+ @Override
+ public String toString() {
+ return "JsonMapper[delegate=" + delegate + "]";
+ }
+
+ @Override
+ public String toString(Object object, boolean pretty) {
+ return toString.apply(object, pretty);
+ }
+
+ @Override
+ public T fromString(String json, Class target) {
+ return target.cast(fromString.apply(json, target));
+ }
+
+ @Override
+ public T fromValue(Object json, Class target) {
+ return target.cast(fromValue.apply(json, target));
+ }
+ };
+ }
+ };
+ }
+
+ interface Factory {
+
+ /**
+ * Creates the mapper, delegating to the deployment to configure and implement it.
+ *
+ * We can't implement it in the runtime because we don't have a dependency to Jackson in the runtime.
+ *
+ * @return A JSON mapper implemented in the deployment module.
+ */
+ JsonMapper create(JsonTypeAdapter, Map> jsonObjectAdapter,
+ JsonTypeAdapter, List>> jsonArrayAdapter,
+ JsonTypeAdapter, String> bufferAdapter);
+
+ static DeploymentLinker deploymentLinker() {
+ return new DeploymentLinker<>() {
+ @Override
+ public Map createLinkData(Factory object) {
+ return Map.of(
+ "delegate", object,
+ "create",
+ (Function, Map>) args -> {
+ var created = object.create(typeAdapterFromLinkData(args.get("jsonObjectAdapter")),
+ typeAdapterFromLinkData(args.get("jsonArrayAdapter")),
+ typeAdapterFromLinkData(args.get("bufferAdapter")));
+ return JsonMapper.deploymentLinker().createLinkData(created);
+ });
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Factory createLink(Map linkData) {
+ Object delegate = linkData.get("delegate");
+ Function, Map> create = (Function, Map>) linkData
+ .get("create");
+ return new Factory() {
+ @Override
+ public String toString() {
+ return "JsonMapper[delegate=" + delegate + "]";
+ }
+
+ @Override
+ public JsonMapper create(JsonTypeAdapter, Map> jsonObjectAdapter,
+ JsonTypeAdapter, List>> jsonArrayAdapter,
+ JsonTypeAdapter, String> bufferAdapter) {
+ var linkData = create.apply(Map.of("jsonObjectAdapter", typeAdapterToLinkData(jsonObjectAdapter),
+ "jsonArrayAdapter", typeAdapterToLinkData(jsonArrayAdapter),
+ "bufferAdapter", typeAdapterToLinkData(bufferAdapter)));
+ return JsonMapper.deploymentLinker().createLink(linkData);
+ }
+ };
+ }
+
+ private Map typeAdapterToLinkData(JsonTypeAdapter, ?> object) {
+ return Map.of("type", object.type,
+ "serializer", object.serializer,
+ "deserializer", object.deserializer);
+ }
+
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ private JsonTypeAdapter typeAdapterFromLinkData(Object linkData) {
+ Map map = (Map) linkData;
+ return new JsonTypeAdapter<>((Class) map.get("type"),
+ (Function) map.get("serializer"),
+ (Function) map.get("deserializer"));
+ }
+ };
+ }
+ }
+
+}
diff --git a/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/json/JsonTypeAdapter.java b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/json/JsonTypeAdapter.java
new file mode 100644
index 0000000000000..20ecbd95666cc
--- /dev/null
+++ b/extensions/vertx-http/runtime/src/main/java/io/quarkus/devui/runtime/jsonrpc/json/JsonTypeAdapter.java
@@ -0,0 +1,16 @@
+package io.quarkus.devui.runtime.jsonrpc.json;
+
+import java.util.Objects;
+import java.util.function.Function;
+
+public final class JsonTypeAdapter {
+ public final Class type;
+ public final Function serializer;
+ public final Function deserializer;
+
+ public JsonTypeAdapter(Class type, Function serializer, Function deserializer) {
+ this.type = Objects.requireNonNull(type, "type");
+ this.serializer = Objects.requireNonNull(serializer, "serializer");
+ this.deserializer = Objects.requireNonNull(deserializer, "deserializer");
+ }
+}
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/BufferDeserializer.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/BufferDeserializer.java
index c06dd947fe679..696f7182feeab 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/BufferDeserializer.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/BufferDeserializer.java
@@ -13,7 +13,6 @@
import static io.quarkus.vertx.runtime.jackson.JsonUtil.BASE64_DECODER;
import java.io.IOException;
-import java.time.Instant;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -34,7 +33,7 @@ public Buffer deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
try {
return Buffer.buffer(BASE64_DECODER.decode(text));
} catch (IllegalArgumentException e) {
- throw new InvalidFormatException(p, "Expected a base64 encoded byte array", text, Instant.class);
+ throw new InvalidFormatException(p, "Expected a base64 encoded byte array", text, Buffer.class);
}
}
}
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArrayDeserializer.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArrayDeserializer.java
index 7fb69868dffb4..5af275a328016 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArrayDeserializer.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArrayDeserializer.java
@@ -13,7 +13,6 @@
import static io.quarkus.vertx.runtime.jackson.JsonUtil.BASE64_DECODER;
import java.io.IOException;
-import java.time.Instant;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -24,7 +23,7 @@
/**
* Copied from {@code io.vertx.core.json.jackson.ByteArrayDeserializer} as that class is package private
*/
-class ByteArrayDeserializer extends JsonDeserializer {
+public class ByteArrayDeserializer extends JsonDeserializer {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
@@ -32,7 +31,7 @@ public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
try {
return BASE64_DECODER.decode(text);
} catch (IllegalArgumentException e) {
- throw new InvalidFormatException(p, "Expected a base64 encoded byte array", text, Instant.class);
+ throw new InvalidFormatException(p, "Expected a base64 encoded byte array", text, byte[].class);
}
}
}
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArraySerializer.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArraySerializer.java
index d2a3a757950c3..95c1d5f73a070 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArraySerializer.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/ByteArraySerializer.java
@@ -21,7 +21,7 @@
/**
* Copied from {@code io.vertx.core.json.jackson.ByteArraySerializer} as that class is package private
*/
-class ByteArraySerializer extends JsonSerializer {
+public class ByteArraySerializer extends JsonSerializer {
@Override
public void serialize(byte[] value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantDeserializer.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantDeserializer.java
index b3607ff49adf8..8b5d89b1c77d1 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantDeserializer.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantDeserializer.java
@@ -25,7 +25,7 @@
/**
* Copied from {@code io.vertx.core.json.jackson.InstantDeserializer} as that class is package private
*/
-class InstantDeserializer extends JsonDeserializer {
+public class InstantDeserializer extends JsonDeserializer {
@Override
public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String text = p.getText();
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantSerializer.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantSerializer.java
index 66e2e608895a7..8092a6f3bfb48 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantSerializer.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/InstantSerializer.java
@@ -22,7 +22,7 @@
/**
* Copied from {@code io.vertx.core.json.jackson.InstantSerializer} as that class is package private
*/
-class InstantSerializer extends JsonSerializer {
+public class InstantSerializer extends JsonSerializer {
@Override
public void serialize(Instant value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(ISO_INSTANT.format(value));
diff --git a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/JsonUtil.java b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/JsonUtil.java
index 7b15bdc1c834a..1df71e91f44fc 100644
--- a/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/JsonUtil.java
+++ b/extensions/vertx/runtime/src/main/java/io/quarkus/vertx/runtime/jackson/JsonUtil.java
@@ -17,7 +17,7 @@
*
* This class is copied from {@code io.vertx.core.json.impl.JsonUtil} as it is internal to Vert.x
*/
-final class JsonUtil {
+public final class JsonUtil {
public static final Base64.Encoder BASE64_ENCODER;
public static final Base64.Decoder BASE64_DECODER;