-
Notifications
You must be signed in to change notification settings - Fork 245
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(java): utility to perform unsafe cast (#3730)
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
1 parent
283aa56
commit 4a52d4c
Showing
3 changed files
with
129 additions
and
5 deletions.
There are no files selected for viewing
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
63 changes: 63 additions & 0 deletions
63
packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/UnsafeCast.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,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(); | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
packages/@jsii/java-runtime/project/src/test/java/software/amazon/jsii/UnsafeCastTest.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,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"); | ||
} | ||
} | ||
} |