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> jsonObjectAdapter, + JsonTypeAdapter> jsonArrayAdapter, JsonTypeAdapter 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> jsonObjectAdapter, + JsonTypeAdapter> jsonArrayAdapter, + JsonTypeAdapter 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> jsonObjectAdapter, + JsonTypeAdapter> jsonArrayAdapter, + JsonTypeAdapter 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;