-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow relying on Jackson-databind in Dev UI JsonRpc services regardle…
…ss of runtime dependencies
- Loading branch information
Showing
17 changed files
with
580 additions
and
111 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" 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 the deployment 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 runtime classloader. | ||
*/ | ||
Map<String, ?> createLinkData(T object); | ||
|
||
/** | ||
* @param linkData The result of calling {@link #createLinkData(Object)}. | ||
* @return An object implementing class {@code T} in the runtime classloader | ||
* and redirecting calls to the Functions, Suppliers, etc. from {@code linkData}, | ||
* thereby linking to the deployment implementation. | ||
*/ | ||
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
204 changes: 204 additions & 0 deletions
204
...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,204 @@ | ||
package io.quarkus.devui.deployment.jsonrpc; | ||
|
||
import java.io.Closeable; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
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.netty.buffer.ByteBufInputStream; | ||
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.buffer.Buffer; | ||
import io.vertx.core.json.DecodeException; | ||
import io.vertx.core.json.EncodeException; | ||
import io.vertx.core.spi.json.JsonCodec; | ||
|
||
public class DevUIDatabindCodec implements JsonCodec, 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); | ||
} | ||
|
||
@Override | ||
public <T> T fromBuffer(Buffer buf, Class<T> clazz) throws DecodeException { | ||
return fromParser(createParser(buf), clazz); | ||
} | ||
|
||
private JsonParser createParser(Buffer buf) { | ||
try { | ||
return mapper.getFactory() | ||
.createParser((InputStream) new ByteBufInputStream(buf.getByteBuf())); | ||
} catch (IOException e) { | ||
throw new DecodeException("Failed to decode:" + e.getMessage(), e); | ||
} | ||
} | ||
|
||
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); | ||
} | ||
} | ||
|
||
@Override | ||
public Buffer toBuffer(Object object, boolean pretty) throws EncodeException { | ||
try { | ||
ObjectMapper theMapper = pretty ? prettyMapper : mapper; | ||
return Buffer.buffer(theMapper.writeValueAsBytes(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.