diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java index 0a795b93a4..383ead102e 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiObjectRef.java @@ -1,8 +1,10 @@ package software.amazon.jsii; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.jetbrains.annotations.VisibleForTesting; import java.util.Collections; import java.util.HashSet; @@ -47,14 +49,22 @@ public final class JsiiObjectRef { * @param node The JSON node that includes the objref. */ private JsiiObjectRef(final String objId, final JsonNode node) { - this.objId = objId; - this.node = node; + this(objId, node.has(TOKEN_INTERFACES) + ? parseInterfaces(node.get(TOKEN_INTERFACES)) + : Collections.emptySet(), node); + } + + @VisibleForTesting + JsiiObjectRef(final String objId, final Set interfaces) { + this(objId, interfaces, JsiiObjectRef.makeJson(objId, interfaces)); + } + private JsiiObjectRef(final String objId, final Set interfaces, final JsonNode node) { + this.objId = objId; int fqnDelimiter = this.objId.lastIndexOf("@"); this.fqn = this.objId.substring(0, fqnDelimiter); - this.interfaces = node.has(TOKEN_INTERFACES) - ? parseInterfaces(node.get(TOKEN_INTERFACES)) - : Collections.emptySet(); + this.interfaces = interfaces; + this.node = node; } /** @@ -81,6 +91,24 @@ public static JsiiObjectRef fromObjId(final String objId) { return new JsiiObjectRef(objId, node); } + JsiiObjectRef withInterface(final String fqn) { + final Set interfaces = new HashSet<>(this.interfaces); + interfaces.add(fqn); + + return new JsiiObjectRef(this.objId, interfaces); + } + + private static JsonNode makeJson(final String objId, final Set interfaces) { + final ObjectNode node = JsonNodeFactory.instance.objectNode(); + node.put(TOKEN_REF, objId); + final ArrayNode jsonInterfaces = JsonNodeFactory.instance.arrayNode(); + for (final String iface : interfaces) { + jsonInterfaces.add(JsonNodeFactory.instance.textNode(iface)); + } + node.set(TOKEN_INTERFACES, jsonInterfaces); + return node; + } + private static Set parseInterfaces(final JsonNode node) { if (!node.isArray()) { throw new Error(String.format("Invalid value for %s. Expected array but received %s", TOKEN_INTERFACES, node)); diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java new file mode 100644 index 0000000000..c1b3a570ea --- /dev/null +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.java @@ -0,0 +1,63 @@ +package software.amazon.jsii; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +public final class UnsafeCast { + /** + * Unsafely obtains a view on a given value as an instance of an interface annotated with the {@link Jsii.Proxy} + * annotation. + * + * @param value the value to be converted. + * @param target the target type to obtain. This must be an interface with the {@link Jsii.Proxy} annotation. + * + * @return the converted value. Will only return {@code null} if {@code value} is {@code null}. + * + * @param the return type of the cast. + * + * @throws IllegalArgumentException if the provided {@code target} is not a {@link Jsii.Proxy} annotated interface. + */ + @SuppressWarnings("unchecked") + public static T unsafeCast(final JsiiObject value, final Class target) { + if (value == null) { + return null; + } + + if (target.isAssignableFrom(value.getClass())) { + return (T)value; + } + + if (!target.isAnnotationPresent(Jsii.class)) { + throw new IllegalArgumentException(String.format("Class %s does not have the @Jsii annotation!", target.getCanonicalName())); + } + + if (!target.isAnnotationPresent(Jsii.Proxy.class)) { + throw new IllegalArgumentException(String.format("Class %s does not have the @Jsii.Proxy annotation!", target.getCanonicalName())); + } + + final String fqn = target.getAnnotation(Jsii.class).fqn(); + final Jsii.Proxy annotation = target.getAnnotation(Jsii.Proxy.class); + try { + final Constructor constructor = annotation.value().getDeclaredConstructor(JsiiObjectRef.class); + @SuppressWarnings("deprecated") + final boolean oldAccessible = constructor.isAccessible(); + try { + constructor.setAccessible(true); + final JsiiObject proxyInstance = constructor.newInstance(value.jsii$objRef.withInterface(fqn)); + return (T) proxyInstance; + } finally { + constructor.setAccessible(oldAccessible); + } + } catch (final NoSuchMethodException nsme) { + throw new JsiiException(String.format("Unable to find interface proxy constructor on %s", annotation.value().getCanonicalName()), nsme); + } catch (final InvocationTargetException | InstantiationException e) { + throw new JsiiException(String.format("Unable to initialize interface proxy %s", annotation.value().getCanonicalName()), e); + } catch (final IllegalAccessException iae) { + throw new JsiiException(String.format("Unable to invoke constructor of %s", annotation.value().getCanonicalName()), iae); + } + } + + private UnsafeCast(){ + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/packages/@jsii/java-runtime/project/src/test/java/software/amazon/jsii/UnsafeCastTest.java b/packages/@jsii/java-runtime/project/src/test/java/software/amazon/jsii/UnsafeCastTest.java new file mode 100644 index 0000000000..338b01454f --- /dev/null +++ b/packages/@jsii/java-runtime/project/src/test/java/software/amazon/jsii/UnsafeCastTest.java @@ -0,0 +1,33 @@ +package software.amazon.jsii; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +public final class UnsafeCastTest { + @Test + public void canCastToAnyInterface() { + final JsiiObject subject = new JsiiObject(new JsiiObjectRef("Object@1000", Collections.emptySet())); + + final IManagedInterface result = UnsafeCast.unsafeCast(subject, IManagedInterface.class); + Assertions.assertInstanceOf(IManagedInterface.class, result); + } + + @Jsii(fqn = "test.IManagedInterface", module = JsiiModule.class) + @Jsii.Proxy(ManagedInterfaceProxy.class) + private interface IManagedInterface extends JsiiSerializable { + boolean getBooleanProperty(); + } + + private final static class ManagedInterfaceProxy extends JsiiObject implements IManagedInterface { + public ManagedInterfaceProxy(final JsiiObjectRef objRef) { + super(objRef); + } + + @Override + public boolean getBooleanProperty() { + throw new UnsupportedOperationException("Not Implemented"); + } + } +}