Skip to content

Commit

Permalink
feat(java): utility to perform unsafe cast (#3730)
Browse files Browse the repository at this point in the history
Adds an `UnsafeCast.unsafeCast` method to the Jsii runtime for Java that
allows unsafely casting an instance to a managed interface of the user's
choice.

This can be useful when dealing with type unions composed of interfaces
or structs, as there is otherwise no way to convert the instance without
jumping through hoops.

Fixes #3726 (sort of)



---

By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
  • Loading branch information
RomainMuller authored Aug 30, 2022
1 parent 283aa56 commit 4a52d4c
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<String> interfaces) {
this(objId, interfaces, JsiiObjectRef.makeJson(objId, interfaces));
}

private JsiiObjectRef(final String objId, final Set<String> 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;
}

/**
Expand All @@ -81,6 +91,24 @@ public static JsiiObjectRef fromObjId(final String objId) {
return new JsiiObjectRef(objId, node);
}

JsiiObjectRef withInterface(final String fqn) {
final Set<String> interfaces = new HashSet<>(this.interfaces);
interfaces.add(fqn);

return new JsiiObjectRef(this.objId, interfaces);
}

private static JsonNode makeJson(final String objId, final Set<String> 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<String> parseInterfaces(final JsonNode node) {
if (!node.isArray()) {
throw new Error(String.format("Invalid value for %s. Expected array but received %s", TOKEN_INTERFACES, node));
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> 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 extends JsiiSerializable> T unsafeCast(final JsiiObject value, final Class<T> 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<? extends JsiiObject> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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");
}
}
}

0 comments on commit 4a52d4c

Please sign in to comment.