Skip to content

Commit

Permalink
BL-968
Browse files Browse the repository at this point in the history
  • Loading branch information
bdw429s committed Jan 22, 2025
1 parent 2bfc54b commit a12734b
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -324,9 +324,9 @@ public boolean canReturn() {
public int registerKey( BoxExpression key ) {
String name;
if ( key instanceof BoxStringLiteral str ) {
name = str.getValue();
name = "string___" + str.getValue();
} else if ( key instanceof BoxIntegerLiteral intr ) {
name = intr.getValue();
name = "integer___" + intr.getValue();
} else {
throw new IllegalStateException( "Key must be a string or integer literal" );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ public static Object cast( IBoxContext context, Object object, Object oType, Boo
}
}

if ( type.equals( "stream" ) ) {
if ( type.equals( "stream" ) && ! ( object instanceof IClassRunnable ) ) {
// No real "casting" to do, just return it if it is one
if ( object instanceof Stream ) {
return object;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2144,7 +2144,7 @@ private static boolean hasMatchingParameterTypes(
*
* @return True if the arguments were coerced, false otherwise
*/
private static Boolean coerceArguments(
public static Boolean coerceArguments(
IBoxContext context,
Class<?>[] methodParams,
Class<?>[] argumentsAsClasses,
Expand Down Expand Up @@ -2222,18 +2222,20 @@ private static Optional<?> coerceAttempt( IBoxContext context, Class<?> expected
// Use the expected caster to coerce the value to the actual type
if ( Number.class.isAssignableFrom( expected ) && Number.class.isAssignableFrom( actual ) ) {
// logger.debug( "Coerce attempt: Both numbers, using generic caster to " + expectedClass );
return Optional.of(
GenericCaster.cast( context, value, expectedClass, false )
);
CastAttempt<Object> numberAttempt = GenericCaster.attempt( context, value, expectedClass );
if ( numberAttempt.wasSuccessful() ) {
return numberAttempt.toOptional();
}
}

// EXPECTED: Key
// To help with interacting with core BL classes, if the target method requires a Key then cast simple values
if ( Key.class.isAssignableFrom( expected ) ) {
// logger.debug( "Coerce attempt: Both numbers, using generic caster to " + expectedClass );
return Optional.of(
KeyCaster.cast( value, false )
);
CastAttempt<Key> keyAttempt = KeyCaster.attempt( value );
if ( keyAttempt.wasSuccessful() ) {
return keyAttempt.toOptional();
}
}

// EXPECTED: BOOLEAN
Expand All @@ -2245,27 +2247,29 @@ private static Optional<?> coerceAttempt( IBoxContext context, Class<?> expected
Number.class.isAssignableFrom( actual ) ) {

// logger.debug( "Coerce attempt: Castable to boolean " + actualClass );

return Optional.of(
BooleanCaster.cast( value, false )
);
CastAttempt<Boolean> booleanAttempt = BooleanCaster.attempt( value );
if ( booleanAttempt.wasSuccessful() ) {
return booleanAttempt.toOptional();
}
}

// EXPECTED: STRING
if ( expectedClass.equals( "string" ) ) {
// logger.debug( "Coerce attempt: Castable to String " + actualClass );
return Optional.of(
StringCaster.cast( value, false )
);
CastAttempt<String> stringAttempt = StringCaster.attempt( value );
if ( stringAttempt.wasSuccessful() ) {
return stringAttempt.toOptional();
}
}

// Expected: Numeric and Actual: String
// If the expected type is a number and the actual is a string, we can TRY to coerce it
if ( Number.class.isAssignableFrom( expected ) && actualClass.equals( "string" ) ) {
// logger.debug( "Coerce attempt: Castable to Number from String " + actualClass );
return Optional.of(
GenericCaster.cast( context, value, expectedClass, false )
);
CastAttempt<Object> numberAttempt = GenericCaster.attempt( context, value, expectedClass );
if ( numberAttempt.wasSuccessful() ) {
return numberAttempt.toOptional();
}
}

// Allow Arrays to be coerced to native arrays
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@
import java.lang.reflect.Method;

import ortus.boxlang.runtime.context.IBoxContext;
import ortus.boxlang.runtime.interop.DynamicInteropService;
import ortus.boxlang.runtime.scopes.Key;
import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException;
import ortus.boxlang.runtime.types.util.BooleanRef;

/**
* A generic proxy allows you to wrap any object and call any method on it from Java/BoxLang
Expand All @@ -48,15 +50,42 @@ public Object invoke( Object proxy, Method method, Object[] args ) throws Throwa
// If we have a class and an incoming method proxy, run it
if ( isClassRunnableTarget() && method != null ) {
// Invoke the method
return invoke( Key.of( method.getName() ), args );
return coerceReturnValue( invoke( Key.of( method.getName() ), args ), method.getReturnType(), method.getName() );
}

// Use use the default invocations
return invoke( args );
if ( method != null ) {
return coerceReturnValue( invoke( Key.of( method.getName() ), args ), method.getReturnType(), method.getName() );
} else {
return invoke( args );
}
} catch ( Exception e ) {
getLogger().error( "Error invoking GenericProxy", e );
throw new BoxRuntimeException( "Error invoking GenericProxy", e );
}
}

/**
* Force return value to be what the interface requires
*
* @param returnValue The value being returned
* @param returnType The return type of the method
* @param methodName The name of the method (or error handling)
*
* @return The coerced return value
*/
private Object coerceReturnValue( Object returnValue, Class<?> returnType, String methodName ) {
if ( returnType == null ) {
return returnValue;
}
Object[] args = new Object[] { returnValue };
boolean success = DynamicInteropService.coerceArguments( context, new Class<?>[] { returnType }, new Class<?>[] { returnValue.getClass() }, args,
false, BooleanRef.of( true ) );
if ( !success ) {
throw new BoxRuntimeException( "Proxied method [ " + methodName + "() ] returned a value of type [ " + returnValue.getClass().getName()
+ " ] which could not be coerced to [ " + returnType.getName() + " ] in order to match the interface method signature." );
}
return args[ 0 ];
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package ortus.boxlang.runtime.bifs.global.system.java;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
Expand All @@ -29,6 +30,7 @@
import ortus.boxlang.runtime.scopes.Key;
import ortus.boxlang.runtime.scopes.ServerScope;
import ortus.boxlang.runtime.scopes.VariablesScope;
import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException;

public class CreateDynamicProxyTest {

Expand Down Expand Up @@ -99,30 +101,46 @@ public void testAllowsObjectMethods() {
assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( 42 );
}

@DisplayName( "It creates a proxy with multiple interfaces" )
@DisplayName( "It casts output to match interface method return type" )
@Test
public void testCreatesMultipleProxies() {
public void testCastsOutputToMatchInterfaceMethodReturnType() {
// @formatter:off
instance.executeSource(
"""
import java:java.lang.Thread;
import java.util.Arrays;
import java.util.stream.Collectors;
jStream = Arrays.stream( [ "foo", "bar" ] )
jRunnable = CreateDynamicProxy(
"src.test.java.ortus.boxlang.runtime.dynamic.javaproxy.BoxClassRunnable",
[ "java.lang.Runnable", "java.util.concurrent.Callable" ]
proxy = CreateDynamicProxy(
"src.test.java.ortus.boxlang.runtime.bifs.global.system.java.ToLongFunction",
[ "java.util.function.ToLongFunction" ]
);
result = jStream.collect( Collectors.summingLong( proxy ) );
println( result)
""",
context );
// @formatter:on
assertThat( variables.get( Key.of( "result" ) ) ).isEqualTo( 84 );

jthread = new java:Thread( jRunnable );
jthread.start();
sleep( 500 );
result = jRunnable.call();
BoxRuntimeException e = assertThrows( BoxRuntimeException.class,
// @formatter:off
()->instance.executeSource(
"""
import java.util.Arrays;
import java.util.stream.Collectors;
jStream = Arrays.stream( [ "foo", "bar" ] )
proxy = CreateDynamicProxy(
"src.test.java.ortus.boxlang.runtime.bifs.global.system.java.ToLongFunctionInvalidReturn",
[ "java.util.function.ToLongFunction" ]
);
result = jStream.collect( Collectors.summingLong( proxy ) );
println( result)
""",
context );
context ) );
// @formatter:on

assertThat( context.getScope( ServerScope.name ).get( "runnableProxyFired" ) ).isEqualTo( true );
assertThat( variables.get( result ) ).isEqualTo( "I was called!" );
assertThat( e.getCause().getMessage() ).contains( "could not be coerced" );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Functional interface that maps to java.util.function.ToLongFunction
* See https://docs.oracle.com/javase/8/docs/api/java/util/function/ToLongFunction.html
*/
class {


/**
* Functional interface for the apply functionional interface
*/
function applyAsLong( required value ){
// Return non-Long value, but castable as such
return "42"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* Functional interface that maps to java.util.function.ToLongFunction
* See https://docs.oracle.com/javase/8/docs/api/java/util/function/ToLongFunction.html
*/
class {


/**
* Functional interface for the apply functionional interface
*/
function applyAsLong( required value ){
// Return non-Long value, which cannot be coerced
return "sdf"
}

}

0 comments on commit a12734b

Please sign in to comment.