-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #32393 from yrodiere/jackson-json-rpc-service
Allow relying on Jackson-databind in Dev UI JsonRpc services regardless of runtime dependencies
- Loading branch information
Showing
17 changed files
with
554 additions
and
113 deletions.
There are no files selected for viewing
36 changes: 36 additions & 0 deletions
36
core/devmode-spi/src/main/java/io/quarkus/dev/console/DeploymentLinker.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
* <p> | ||
* 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. | ||
* <p> | ||
* 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<T> { | ||
|
||
/** | ||
* @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<String, ?> 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<String, ?> linkData); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
176 changes: 176 additions & 0 deletions
176
...http/deployment/src/main/java/io/quarkus/devui/deployment/jsonrpc/DevUIDatabindCodec.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Map<String, Object>, ?> runtimeObjectDeserializer; | ||
private final Function<List<?>, ?> runtimeArrayDeserializer; | ||
|
||
private DevUIDatabindCodec(ObjectMapper mapper, | ||
Function<Map<String, Object>, ?> runtimeObjectDeserializer, | ||
Function<List<?>, ?> runtimeArrayDeserializer) { | ||
this.mapper = mapper; | ||
prettyMapper = mapper.copy(); | ||
prettyMapper.configure(SerializationFeature.INDENT_OUTPUT, true); | ||
this.runtimeObjectDeserializer = runtimeObjectDeserializer; | ||
this.runtimeArrayDeserializer = runtimeArrayDeserializer; | ||
} | ||
|
||
@SuppressWarnings("unchecked") | ||
@Override | ||
public <T> T fromValue(Object json, Class<T> clazz) { | ||
T value = mapper.convertValue(json, clazz); | ||
if (clazz == Object.class) { | ||
value = (T) adapt(value); | ||
} | ||
return value; | ||
} | ||
|
||
@Override | ||
public <T> T fromString(String str, Class<T> 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> T fromParser(JsonParser parser, Class<T> 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<String, Object> map = (Map<String, Object>) 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<String, Object>> 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 <T, S> void addAdapterToObject(SimpleModule module, JsonTypeAdapter<T, S> 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 <T> void addAdapterToString(SimpleModule module, JsonTypeAdapter<T, String> 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<T>() { | ||
@Override | ||
public T deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException { | ||
return adapter.deserializer.apply(parser.getText()); | ||
} | ||
}); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.