From 14f5659c0121b7025d33a3b42991e7915cdaba4b Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 4 Oct 2024 18:10:24 +0000 Subject: [PATCH 01/38] Version bump --- changelog.md | 6 +++++- gradle.properties | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 8113f1c57..ca542ba25 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.0.0-beta17] - 2024-10-04 + ## [1.0.0-beta16] - 2024-09-27 ## [1.0.0-beta15] - 2024-09-20 @@ -45,7 +47,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0-beta1] - 2024-06-14 -[Unreleased]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta16...HEAD +[Unreleased]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta17...HEAD + +[1.0.0-beta17]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta16...v1.0.0-beta17 [1.0.0-beta16]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta15...v1.0.0-beta16 diff --git a/gradle.properties b/gradle.properties index a1d246936..c94a19d18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -#Fri Sep 27 20:09:04 UTC 2024 +#Fri Oct 04 18:10:20 UTC 2024 antlrVersion=4.13.1 jdkVersion=21 -version=1.0.0-beta17 +version=1.0.0-beta18 From 0adb9befbb64f68685660a957b90df028741b823 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Sat, 5 Oct 2024 19:05:53 +0200 Subject: [PATCH 02/38] BL-616 #resolve array.find does not use cf rules to convert result of predicate to boolean BL-617 #resolve arrayFind, arrayFindNoCase value closures, accept the value and now the index of the item as the second param --- .../runtime/bifs/global/array/ArrayFind.java | 19 +++++++++++++++++++ .../ortus/boxlang/runtime/types/Array.java | 3 ++- .../bifs/global/array/ArrayFindTest.java | 13 +++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java b/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java index fec512f22..c756a83c4 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java @@ -52,6 +52,25 @@ public ArrayFind() { /** * Array finders and contains functions with and without case sensitivity. * Please note that "contain" methods return a boolean, while "find" methods return an index. + * If you use a function as the value, it will be used as a search closure or lambda. The signature of the function should be: + * + *
+	 *    ( value, index ) => {
+	 * 	  	return true; // if the value is found, else false
+	 *   }
+	 * 
+ * + * Example: + * + *
+	 *   array = [ 1, 2, 3, 4, 5 ];
+	 *  index = array.find( ( value, index ) -> {
+	 * 		return value == 3;
+	 * } );
+	 * 
+ * + * We recommend you use BoxLang lambdas ({@code ->}) for this purpose, so they only act upon the value and index without any side effects. + * They will be faster and more efficient. * * @function.arrayFind This function searches the array for the specified value. Returns the index in the array of the first match, or 0 if there is * no match. diff --git a/src/main/java/ortus/boxlang/runtime/types/Array.java b/src/main/java/ortus/boxlang/runtime/types/Array.java index 0fe924283..0b77a07be 100644 --- a/src/main/java/ortus/boxlang/runtime/types/Array.java +++ b/src/main/java/ortus/boxlang/runtime/types/Array.java @@ -40,6 +40,7 @@ import ortus.boxlang.runtime.bifs.MemberDescriptor; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.dynamic.IReferenceable; +import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.dynamic.casters.CastAttempt; import ortus.boxlang.runtime.dynamic.casters.NumberCaster; import ortus.boxlang.runtime.dynamic.casters.StringCaster; @@ -704,7 +705,7 @@ public int findIndex( Object value ) { */ public int findIndex( Function test, IBoxContext context ) { return intStream() - .filter( i -> ( boolean ) context.invokeFunction( test, new Object[] { get( i ) } ) ) + .filter( i -> BooleanCaster.cast( context.invokeFunction( test, new Object[] { get( i ), i } ) ) ) .findFirst() .orElse( -1 ) + 1; } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java index 041801397..14aa75779 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java @@ -155,4 +155,17 @@ public void testMatchStringLambda() { int found = ( int ) variables.get( result ); assertThat( found ).isEqualTo( 2 ); } + + @DisplayName( "It should find using a closure returning a boolean" ) + @Test + public void testMatchClosure() { + instance.executeSource( + """ + result = ["a","b","c"].find( (v) => v == "b" ? 1 : 0 ); + """, + context ); + + int found = ( int ) variables.get( result ); + assertThat( found ).isEqualTo( 2 ); + } } From 35620b9d960a8faa82f4f7e243eb4ea849bd9e5d Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Sat, 5 Oct 2024 19:12:02 +0200 Subject: [PATCH 03/38] docs --- .../java/ortus/boxlang/runtime/types/JavaMethod.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java b/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java index fac557bfb..e565c92f3 100644 --- a/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java +++ b/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java @@ -36,11 +36,11 @@ public class JavaMethod extends Function { private static final Argument[] EMPTY_ARGUMENTS = new Argument[ 0 ]; - private static final IStruct documentation = Struct.of( "hint", + private static final IStruct DOCUMENTATION = Struct.of( "hint", "I am a wrapped Java method. Since I may be overloaded, my return type and arguments will be determined when I am invoked" ); + private static final String RETURN_TYPE = "any"; private final Key name; - private final String returnType = "any"; private DynamicObject dynamicObject; /** @@ -78,7 +78,7 @@ public Argument[] getArguments() { * @return return type */ public String getReturnType() { - return returnType; + return RETURN_TYPE; } /** @@ -96,7 +96,7 @@ public IStruct getAnnotations() { * @return function metadata */ public IStruct getDocumentation() { - return documentation; + return DOCUMENTATION; } /** @@ -163,9 +163,10 @@ public BoxSourceType getSourceType() { /** * True if the function requires strict arguments (basically a java method) * or false if this is a Boxlang method which can accept additional arbitrary arguments - * + * * @return true if strict arguments are required */ + @Override public boolean requiresStrictArguments() { return true; } From 9f9bd4d934be68b635d5e659ff672ca83c9480fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:19:15 +0000 Subject: [PATCH 04/38] Bump org.ow2.asm:asm-tree from 9.7 to 9.7.1 Bumps org.ow2.asm:asm-tree from 9.7 to 9.7.1. --- updated-dependencies: - dependency-name: org.ow2.asm:asm-tree dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f95724422..be062d459 100644 --- a/build.gradle +++ b/build.gradle @@ -132,7 +132,7 @@ dependencies { // https://mvnrepository.com/artifact/com.zaxxer/HikariCP implementation 'com.zaxxer:HikariCP:6.0.0' // https://mvnrepository.com/artifact/org.ow2.asm/asm-tree - implementation 'org.ow2.asm:asm-tree:9.7' + implementation 'org.ow2.asm:asm-tree:9.7.1' // https://mvnrepository.com/artifact/org.ow2.asm/asm-util implementation 'org.ow2.asm:asm-util:9.7' } From c3ca1a7ee2be6d89d4317ec3946bb7ae3fe97480 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 15:19:17 +0000 Subject: [PATCH 05/38] Bump org.ow2.asm:asm-util from 9.7 to 9.7.1 Bumps org.ow2.asm:asm-util from 9.7 to 9.7.1. --- updated-dependencies: - dependency-name: org.ow2.asm:asm-util dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index f95724422..7a445711d 100644 --- a/build.gradle +++ b/build.gradle @@ -134,7 +134,7 @@ dependencies { // https://mvnrepository.com/artifact/org.ow2.asm/asm-tree implementation 'org.ow2.asm:asm-tree:9.7' // https://mvnrepository.com/artifact/org.ow2.asm/asm-util - implementation 'org.ow2.asm:asm-util:9.7' + implementation 'org.ow2.asm:asm-util:9.7.1' } /** From 565146d434c5a35438f6a7a3ec4f4647c7eccdad Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 7 Oct 2024 14:39:50 -0500 Subject: [PATCH 06/38] BL-617 --- .../bifs/global/array/ArrayFindTest.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java index 14aa75779..746f6c283 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java @@ -20,9 +20,12 @@ import static com.google.common.truth.Truth.assertThat; +import java.util.function.Predicate; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -168,4 +171,21 @@ public void testMatchClosure() { int found = ( int ) variables.get( result ); assertThat( found ).isEqualTo( 2 ); } + + @DisplayName( "Function can be Java functional interface" ) + @Test + @Disabled( "See comments on https://ortussolutions.atlassian.net/browse/BL-617" ) + public void testJavaFunctionalInterface() { + Predicate javaPredicate = ( s ) -> s.equals( "b" ); + variables.put( "javaPredicate", javaPredicate ); + instance.executeSource( + """ + import java.util.function.Predicate; + result = ["a","b","c"].find( javaPredicate ); + """, + context ); + + int found = ( int ) variables.get( result ); + assertThat( found ).isEqualTo( 2 ); + } } From f04723956552fc29a3a88531dfa21ce23afd5b30 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 7 Oct 2024 16:57:40 -0500 Subject: [PATCH 07/38] BL-614 --- .../runtime/loader/ImportDefinition.java | 4 +++ .../java/TestCases/phase1/CoreLangTest.java | 34 +++++++++++++------ .../runtime/loader/ImportDefinitionTest.java | 10 +++--- 3 files changed, 33 insertions(+), 15 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java b/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java index 6c2a1ece0..a3356ee3b 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java +++ b/src/main/java/ortus/boxlang/runtime/loader/ImportDefinition.java @@ -96,6 +96,10 @@ public static ImportDefinition parse( String importStr ) { // If there is no alias, use the last part of the class name as the alias String[] parts = className.split( "\\." ); alias = parts[ parts.length - 1 ]; + // If there is one or more $ chars, take the last segment (nested class) + if ( alias.contains( "$" ) ) { + alias = alias.substring( alias.lastIndexOf( "$" ) + 1 ); + } } int resolverDelimiterPos = className.indexOf( ":" ); diff --git a/src/test/java/TestCases/phase1/CoreLangTest.java b/src/test/java/TestCases/phase1/CoreLangTest.java index 7418f247a..a8fbbfdf4 100644 --- a/src/test/java/TestCases/phase1/CoreLangTest.java +++ b/src/test/java/TestCases/phase1/CoreLangTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.math.BigInteger; +import java.net.http.HttpRequest.BodyPublisher; import java.util.Comparator; import java.util.concurrent.TimeUnit; @@ -2665,18 +2666,18 @@ public void testJavaMethodReference() { instance.executeSource( """ - import java:java.lang.String; - javaStatic = java.lang.String::valueOf; - result = javaStatic( "test" ) + import java:java.lang.String; + javaStatic = java.lang.String::valueOf; + result = javaStatic( "test" ) - javaInstance = result.toUpperCase - result2 = javaInstance() + javaInstance = result.toUpperCase + result2 = javaInstance() - import java.util.Collections; - result3 = [ 1, 7, 3, 99, 0 ].sort( Collections.reverseOrder().compare ) + import java.util.Collections; + result3 = [ 1, 7, 3, 99, 0 ].sort( Collections.reverseOrder().compare ) - import java:java.lang.Math; - result4 = [ 1, 2.4, 3.9, 4.5 ].map( Math::floor ) + import java:java.lang.Math; + result4 = [ 1, 2.4, 3.9, 4.5 ].map( Math::floor ) // Use the compare method from the Java reverse order comparator to sort a BL array [ 1, 7, 3, 99, 0 ].sort( Collections.reverseOrder() ) @@ -2687,7 +2688,7 @@ public void testJavaMethodReference() { result6 = isBrad( "luis" ) result7 = [ "brad", "luis", "jon" ].filter( isBrad ) - """, + """, context ); assertThat( variables.get( result ) ).isEqualTo( "test" ); @@ -2704,6 +2705,19 @@ public void testJavaMethodReference() { assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( Array.of( "brad" ) ); } + @Test + public void testJavaNestedClassImport() { + + instance.executeSource( + """ + import java.net.http.HttpRequest$BodyPublishers; + result = BodyPublishers.noBody(); + """, + context ); + assertThat( variables.get( result ) ).isNotNull(); + assertThat( BodyPublisher.class.isAssignableFrom( variables.get( result ).getClass() ) ).isTrue(); + } + @Test public void testFunctionalBIFAccess() { diff --git a/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java b/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java index 27c96451e..6080ce7fe 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/ImportDefinitionTest.java @@ -66,12 +66,12 @@ public void testCanUseStaticConstructorParser() { assertThat( importDef.resolverPrefix() ).isEqualTo( null ); assertThat( importDef.alias() ).isEqualTo( "String" ); - importDef = ImportDefinition.parse( "java.util.*" ); - assertThat( importDef.isMultiImport() ).isEqualTo( true ); - assertThat( importDef.className() ).isEqualTo( "java.util.*" ); + importDef = ImportDefinition.parse( "java.net.http.HttpRequest$BodyPublishers" ); + assertThat( importDef.isMultiImport() ).isEqualTo( false ); + assertThat( importDef.className() ).isEqualTo( "java.net.http.HttpRequest$BodyPublishers" ); assertThat( importDef.resolverPrefix() ).isEqualTo( null ); - assertThat( importDef.alias() ).isEqualTo( "*" ); - assertThat( importDef.getPackageName() ).isEqualTo( "java.util" ); + assertThat( importDef.alias() ).isEqualTo( "BodyPublishers" ); + assertThat( importDef.getPackageName() ).isEqualTo( "java.net.http" ); } } From 1fcf47e9758e8fe9a38764263685aa0c19dd4244 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 7 Oct 2024 17:05:46 -0500 Subject: [PATCH 08/38] BL-620 --- src/test/java/TestCases/phase3/ClassTest.java | 28 ++++++++++--------- src/test/java/TestCases/phase3/StaticTest.bx | 9 +++++- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/src/test/java/TestCases/phase3/ClassTest.java b/src/test/java/TestCases/phase3/ClassTest.java index c5ed44f73..0b9cd8e3d 100644 --- a/src/test/java/TestCases/phase3/ClassTest.java +++ b/src/test/java/TestCases/phase3/ClassTest.java @@ -1151,19 +1151,20 @@ public void testStaticImport() { public void testStaticImportDot() { instance.executeSource( """ - import src.test.java.TestCases.phase3.StaticTest; - - result1 = StaticTest.foo; - result2 = StaticTest.myStaticFunc(); - result4 = StaticTest.scoped; - result5 = StaticTest.unscoped; - result6 = StaticTest.again; - // instance - myInstance = new StaticTest(); - result7 = myInstance.foo; - result8 = StaticTest.foo; - result9 = myInstance.myInstanceFunc2() - """, context, BoxSourceType.BOXSCRIPT ); + import src.test.java.TestCases.phase3.StaticTest; + + result1 = StaticTest.foo; + result2 = StaticTest.myStaticFunc(); + result4 = StaticTest.scoped; + result5 = StaticTest.unscoped; + result6 = StaticTest.again; + // instance + myInstance = new StaticTest(); + result7 = myInstance.foo; + result8 = StaticTest.foo; + result9 = myInstance.myInstanceFunc2() + result10 = myInstance.getStaticBrad() + """, context, BoxSourceType.BOXSCRIPT ); assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); @@ -1177,6 +1178,7 @@ public void testStaticImportDot() { assertThat( result9.get( 0 ) ).isEqualTo( "brad" ); assertThat( result9.get( 1 ) ).isEqualTo( "wood" ); assertThat( result9.get( 2 ) ).isEqualTo( 42 ); + assertThat( variables.get( Key.of( "result10" ) ) ).isEqualTo( "wood" ); } @Test diff --git a/src/test/java/TestCases/phase3/StaticTest.bx b/src/test/java/TestCases/phase3/StaticTest.bx index 51240db75..dd2767ec3 100644 --- a/src/test/java/TestCases/phase3/StaticTest.bx +++ b/src/test/java/TestCases/phase3/StaticTest.bx @@ -4,9 +4,11 @@ unscoped = "wood" static foo = 9000; '123' = 456; + final brad = "wood" } - static.foo = 42; + static.foo = 42; + variables.staticBrad = static.brad; static { static.again = "luis" @@ -30,4 +32,9 @@ return "Hello"; } + function getStaticBrad() { + return staticBrad; + } + + } \ No newline at end of file From 819f984c59a42a03297d772d4657cdb9e17b5a39 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 7 Oct 2024 17:10:45 -0500 Subject: [PATCH 09/38] BL-620 --- src/test/java/TestCases/phase3/ClassTest.java | 41 +++++++++++-------- .../java/TestCases/phase3/StaticTestCF.cfc | 6 +++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/test/java/TestCases/phase3/ClassTest.java b/src/test/java/TestCases/phase3/ClassTest.java index 0b9cd8e3d..02e31e742 100644 --- a/src/test/java/TestCases/phase3/ClassTest.java +++ b/src/test/java/TestCases/phase3/ClassTest.java @@ -1093,20 +1093,25 @@ public void testStaticStatic() { public void testStaticInstanceCF() { instance.executeSource( """ - clazz = new src.test.java.TestCases.phase3.StaticTestCF(); + clazz = new src.test.java.TestCases.phase3.StaticTestCF(); result1 = clazz.foo; result2 = clazz.myStaticFunc(); result3 = clazz.myInstanceFunc(); result4 = clazz.scoped; result5 = clazz.unscoped; result6 = clazz.again; - """, context, BoxSourceType.CFSCRIPT ); + result7 = clazz.getStaticBrad(); + clazz2 = new src.test.java.TestCases.phase3.StaticTestCF(); + result8 = clazz2.getStaticBrad(); + """, context, BoxSourceType.CFSCRIPT ); assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 42 ); assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static42" ); assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( "instancestatic42" ); assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( "wood" ); assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result8" ) ) ).isEqualTo( "wood" ); } @Test @@ -1151,20 +1156,23 @@ public void testStaticImport() { public void testStaticImportDot() { instance.executeSource( """ - import src.test.java.TestCases.phase3.StaticTest; - - result1 = StaticTest.foo; - result2 = StaticTest.myStaticFunc(); - result4 = StaticTest.scoped; - result5 = StaticTest.unscoped; - result6 = StaticTest.again; - // instance - myInstance = new StaticTest(); - result7 = myInstance.foo; - result8 = StaticTest.foo; - result9 = myInstance.myInstanceFunc2() - result10 = myInstance.getStaticBrad() - """, context, BoxSourceType.BOXSCRIPT ); + import src.test.java.TestCases.phase3.StaticTest; + + result1 = StaticTest.foo; + result2 = StaticTest.myStaticFunc(); + result4 = StaticTest.scoped; + result5 = StaticTest.unscoped; + result6 = StaticTest.again; + // instance + myInstance = new StaticTest(); + result7 = myInstance.foo; + result8 = StaticTest.foo; + result9 = myInstance.myInstanceFunc2() + result10 = myInstance.getStaticBrad() + myInstance2 = new StaticTest(); + result11 = myInstance2.getStaticBrad() + + """, context, BoxSourceType.BOXSCRIPT ); assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "brad" ); @@ -1179,6 +1187,7 @@ public void testStaticImportDot() { assertThat( result9.get( 1 ) ).isEqualTo( "wood" ); assertThat( result9.get( 2 ) ).isEqualTo( 42 ); assertThat( variables.get( Key.of( "result10" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result11" ) ) ).isEqualTo( "wood" ); } @Test diff --git a/src/test/java/TestCases/phase3/StaticTestCF.cfc b/src/test/java/TestCases/phase3/StaticTestCF.cfc index ebfad9dc3..27bcb8cf8 100644 --- a/src/test/java/TestCases/phase3/StaticTestCF.cfc +++ b/src/test/java/TestCases/phase3/StaticTestCF.cfc @@ -3,9 +3,11 @@ component { static.scoped = "brad"; unscoped = "wood" static foo = 9000; + final brad = "wood" } static.foo = 42; + variables.staticBrad = static.brad; static { static.again = "luis" @@ -19,4 +21,8 @@ component { return "instance" & myStaticFunc(); } + function getStaticBrad() { + return staticBrad; + } + } \ No newline at end of file From accf4b97aa24414c6abb98d0c0a5a581e95a41da Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 7 Oct 2024 17:13:58 -0500 Subject: [PATCH 10/38] BL-614 --- src/test/java/TestCases/asm/phase1/DereferenceTest.java | 2 +- src/test/java/TestCases/phase1/DereferenceTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/TestCases/asm/phase1/DereferenceTest.java b/src/test/java/TestCases/asm/phase1/DereferenceTest.java index 2cea9439b..f0188d5c4 100644 --- a/src/test/java/TestCases/asm/phase1/DereferenceTest.java +++ b/src/test/java/TestCases/asm/phase1/DereferenceTest.java @@ -246,7 +246,7 @@ public void testDereferenceNestedClass() { instance.executeSource( """ import java.util.Map$Entry; - result = Map$Entry; + result = Entry; """, context ); diff --git a/src/test/java/TestCases/phase1/DereferenceTest.java b/src/test/java/TestCases/phase1/DereferenceTest.java index 8b644666c..e3cae8ba9 100644 --- a/src/test/java/TestCases/phase1/DereferenceTest.java +++ b/src/test/java/TestCases/phase1/DereferenceTest.java @@ -239,7 +239,7 @@ public void testDereferenceNestedClass() { instance.executeSource( """ import java.util.Map$Entry; - result = Map$Entry; + result = Entry; """, context ); From ef3d015abdb64620a17234e6f5449e2e923ee572 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 8 Oct 2024 00:08:14 -0500 Subject: [PATCH 11/38] BL-615 --- .../ortus/boxlang/runtime/interop/DynamicInteropService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java index 8250c4131..2c7ec2eae 100644 --- a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java +++ b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java @@ -1711,7 +1711,7 @@ public static Object dereference( IBoxContext context, Class targetClass, Obj // If we have the field, return its value, even if it's null return getField( targetClass, targetInstance, name.getName() ).orElse( null ); } else if ( hasClassNoCase( targetClass, name.getName() ) ) { - return findClass( targetClass, name.getName() ); + return DynamicObject.of( findClass( targetClass, name.getName() ) ); } else if ( targetClass.isEnum() ) { return Enum.valueOf( ( Class ) targetClass, name.getName() ); } else if ( getMethodNamesNoCase( targetClass, true ).contains( name.getName().toUpperCase() ) ) { From 5f29069a6fa62acd568934f5ed864f8eb97b12a3 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 8 Oct 2024 00:22:19 -0500 Subject: [PATCH 12/38] BL-620 --- .../boxlang/runtime/loader/ClassLocator.java | 31 ++----------------- .../runtime/runnables/RunnableLoader.java | 31 +++++++++++++++++-- .../TestCases/phase1/DereferenceTest.java | 16 +++++++++- .../bifs/global/system/testApp/Application.bx | 8 +++++ 4 files changed, 54 insertions(+), 32 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java b/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java index 8f20cd07f..aaa95446a 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java +++ b/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java @@ -24,13 +24,10 @@ import java.util.concurrent.ConcurrentMap; import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.context.StaticClassBoxContext; import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.loader.resolvers.BoxResolver; import ortus.boxlang.runtime.loader.resolvers.IClassResolver; import ortus.boxlang.runtime.loader.resolvers.JavaResolver; -import ortus.boxlang.runtime.runnables.BoxClassSupport; -import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.types.exceptions.ClassNotFoundBoxLangException; import ortus.boxlang.runtime.types.exceptions.KeyNotFoundException; @@ -348,7 +345,7 @@ public DynamicObject load( IBoxContext context, String name, List loadClass( String source, IBoxContext context, BoxSourceType type ) { - return this.boxpiler.compileClass( source, type ); + Class clazz = this.boxpiler.compileClass( source, type ); + runStaticInitializer( clazz, context ); + return clazz; } /** @@ -238,7 +241,31 @@ public Class loadClass( String source, IBoxContext context, BoxSou * @return The BoxLang class */ public Class loadClass( ResolvedFilePath resolvedFilePath, IBoxContext context ) { - return this.boxpiler.compileClass( resolvedFilePath ); + Class clazz = this.boxpiler.compileClass( resolvedFilePath ); + runStaticInitializer( clazz, context ); + return clazz; + } + + /** + * Run static initializers for a Box class + * + * @param clazz The class to run the static initializer for + * @param context The context to use + */ + private void runStaticInitializer( Class clazz, IBoxContext context ) { + // Static initializers for Box Classes. We need to manually fire these so we can control the context + if ( !clazz.isInterface() && IClassRunnable.class.isAssignableFrom( clazz ) ) { + DynamicObject boxClass = DynamicObject.of( clazz ); + if ( !( Boolean ) boxClass.getField( "staticInitialized" ).get() ) { + synchronized ( clazz ) { + if ( !( Boolean ) boxClass.getField( "staticInitialized" ).get() ) { + boxClass.invokeStatic( context, "staticInitializer", + new StaticClassBoxContext( context, boxClass, BoxClassSupport.getStaticScope( context, boxClass ) ) ); + boxClass.setField( "staticInitialized", true ); + } + } + } + } } } diff --git a/src/test/java/TestCases/phase1/DereferenceTest.java b/src/test/java/TestCases/phase1/DereferenceTest.java index e3cae8ba9..4be58f405 100644 --- a/src/test/java/TestCases/phase1/DereferenceTest.java +++ b/src/test/java/TestCases/phase1/DereferenceTest.java @@ -234,7 +234,7 @@ public void testDereferenceNestedClass() { """, context ); - assertThat( variables.get( result ) ).isEqualTo( Map.Entry.class ); + assertThat( DynamicObject.unWrap( variables.get( result ) ) ).isEqualTo( Map.Entry.class ); instance.executeSource( """ @@ -246,4 +246,18 @@ public void testDereferenceNestedClass() { assertThat( DynamicObject.unWrap( variables.get( result ) ) ).isEqualTo( Map.Entry.class ); } + @DisplayName( "dereference a method on anested class" ) + @Test + public void testDereferenceAMethodOnANestedClass() { + instance.executeSource( + """ + import java.net.http.HttpRequest; + + result = HttpRequest.BodyPublishers.ofString("test") + """, + context ); + + // assertThat( DynamicObject.unWrap( variables.get( result ) ) ).isEqualTo( Map.Entry.class ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/testApp/Application.bx b/src/test/java/ortus/boxlang/runtime/bifs/global/system/testApp/Application.bx index e1ff12167..ff540604d 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/testApp/Application.bx +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/testApp/Application.bx @@ -1,4 +1,12 @@ class { + + static { + final brad = "wood" + } + + variables.staticBrad = static.brad; + + this.name = "testApp"; this.sessionManagement = true; this.mappings["/foobar"] = getDirectoryFromPath( getCurrentTemplatePath() ); From 2faaa798ed691258155c8e5c8239519e9b62d6ff Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 12:56:40 +0200 Subject: [PATCH 13/38] docs --- src/main/java/ortus/boxlang/runtime/types/JavaMethod.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java b/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java index e565c92f3..b9e76c967 100644 --- a/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java +++ b/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java @@ -67,7 +67,6 @@ public Key getName() { * * @return array of arguments */ - public Argument[] getArguments() { return EMPTY_ARGUMENTS; } From 098a4a6c23cc117e33f471764239a3cce45d087a Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 18:32:26 +0200 Subject: [PATCH 14/38] BL-622 #resolve Consolidate CastAttempt and Attempt into a hierarchy --- .../boxlang/runtime/dynamic/Attempt.java | 36 ++++- .../runtime/dynamic/casters/CastAttempt.java | 147 ++++-------------- 2 files changed, 59 insertions(+), 124 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java b/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java index 26823aa8f..dfcc886f5 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java @@ -52,18 +52,18 @@ public class Attempt { * |-------------------------------------------------------------------------- */ - private static final Attempt EMPTY = new Attempt<>(); + protected static final Attempt EMPTY = new Attempt<>(); /** * The target value to evaluate * This can be a truthy or falsey value */ - private final T value; + protected final T value; /** * Validation Record */ - private ValidationRecord validationRecord; + protected ValidationRecord validationRecord; /** * |-------------------------------------------------------------------------- @@ -74,7 +74,7 @@ public class Attempt { /** * Constructor for an empty attempt */ - private Attempt() { + protected Attempt() { this( null ); } @@ -82,7 +82,7 @@ private Attempt() { * Constructor for an attempt with the incoming value * This can be anything, a truthy or falsey or null */ - private Attempt( T value ) { + protected Attempt( T value ) { this.value = value; this.validationRecord = new ValidationRecord(); } @@ -374,6 +374,17 @@ public T get() { throw new NoElementException( "Attempt is empty" ); } + /** + * Alias to get() so it's more functional on what it does + * + * @return The value of the attempt + * + * @throws NoElementException If the attempt is empty + */ + public T getOrFail() { + return get(); + } + /** * Verifies if the attempt is empty or not using the following rules: * - If the value is null, it is empty @@ -534,7 +545,7 @@ public T getOrDefault( T other ) { * * @return The value of the attempt or the value of the supplier */ - public T orElseGet( Supplier supplier ) { + public T orElseGet( Supplier supplier ) { Objects.requireNonNull( supplier ); if ( this.isEmpty() ) { return supplier.get(); @@ -542,6 +553,17 @@ public T orElseGet( Supplier supplier ) { return this.value; } + /** + * Alias to `orElseGet` but more fluent + * + * @param other The value to return if the attempt is empty + * + * @return The value of the attempt or the value passed in + */ + public T getOrSupply( Supplier other ) { + return orElseGet( other ); + } + /** * Map the attempt to a new value with a supplier * @@ -675,7 +697,7 @@ public Stream stream() { * @return The string representation of the value if any, else empty string */ public String toString() { - return isEmpty() ? "Attempt.empty" : "Attempt[" + this.value.toString() + "]"; + return isEmpty() ? "Attempt.empty" : String.format( "Attempt[%s]", this.value.toString() ); } /** diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java index c8246fc0b..db339fae4 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java @@ -18,9 +18,8 @@ package ortus.boxlang.runtime.dynamic.casters; import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Supplier; +import ortus.boxlang.runtime.dynamic.Attempt; import ortus.boxlang.runtime.types.exceptions.BoxCastException; /** @@ -30,43 +29,37 @@ * to check if a value is castable and then actually cast it. Instead, you can combine those * steps into one and still do something else if the casting was not possible. */ -public final class CastAttempt { - - private static final CastAttempt EMPTY = new CastAttempt<>(); +public final class CastAttempt extends Attempt { /** - * If non-null, the value; if null, indicates no value is present + * |-------------------------------------------------------------------------- + * | Constructors + * |-------------------------------------------------------------------------- */ - private final T value; /** - * Constructs an empty instance. + * Constructs an instance with the value present. + * + * @param value the non-null value to be present */ - private CastAttempt() { - this.value = null; + private CastAttempt( T value ) { + super( Objects.requireNonNull( value ) ); } /** - * Returns an empty instance. No value is present for this - * CastAttempt. - * - * @param Type of the non-existent value - * - * @return an empty {@code CastAttempt} + * |-------------------------------------------------------------------------- + * | Static Builders + * |-------------------------------------------------------------------------- */ - public static CastAttempt empty() { - @SuppressWarnings( "unchecked" ) - CastAttempt t = ( CastAttempt ) EMPTY; - return t; - } /** - * Constructs an instance with the value present. + * Create an empty attempt * - * @param value the non-null value to be present + * @return An empty attempt */ - private CastAttempt( T value ) { - this.value = Objects.requireNonNull( value ); + @SuppressWarnings( "unchecked" ) + public static CastAttempt empty() { + return ( CastAttempt ) EMPTY; } /** @@ -96,105 +89,25 @@ public static CastAttempt ofNullable( T value ) { } /** - * If a value is present in this {@code CastAttempt}, returns the value, - * otherwise throws Exception - * - * @return the non-null value held by this {@code CastAttempt} - * - * @see CastAttempt#wasSuccessful() - */ - public T get() { - if ( value == null ) { - throw new BoxCastException( "The cast was not successful. You cannot get the value." ); - } - return value; - } - - /** - * Return {@code true} if there is a value present, otherwise {@code false}. - * - * @return {@code true} if there is a value present, otherwise {@code false} + * |-------------------------------------------------------------------------- + * | Overrides + * |-------------------------------------------------------------------------- */ - public boolean wasSuccessful() { - return value != null; - } - - /** - * The opposite of {@link #wasSuccessful()}. Returns {@code true} if there is no - * value present, otherwise {@code false}. - */ - public boolean ifFailed() { - return !wasSuccessful(); - } /** - * If a value is present, invoke the specified consumer with the value, - * otherwise do nothing. - * - * @param consumer block to be executed if a value is present - * - * @return this {@code CastAttempt} - * - * @throws NullPointerException if value is present and {@code consumer} is - * null - */ - public CastAttempt ifSuccessful( Consumer consumer ) { - if ( value != null ) - consumer.accept( value ); - - return this; - } - - /** - * Return the value if present, otherwise return {@code other}. - * - * @param other the value to be returned if there is no value present, may - * be null - * - * @return the value, if present, otherwise {@code other} - */ - public T getOrDefault( T other ) { - return value != null ? value : other; - } - - /** - * Return the value if present, otherwise invoke {@code other} and return - * the result of that invocation. - * - * @param other a {@code Supplier} whose result is returned if no value - * is present + * If a value is present in this {@code CastAttempt}, returns the value, + * otherwise throws BoxCastException * - * @return the value if present otherwise the result of {@code other.get()} + * @return the non-null value held by this {@code CastAttempt} * - * @throws NullPointerException if value is not present and {@code other} is - * null - */ - public T getOrSupply( Supplier other ) { - return value != null ? value : other.get(); - } - - /** - * @return The contained value, if present, otherwise throw an exception + * @throws BoxCastException if there is no value present */ - public T getOrFail() { - if ( value != null ) { - return value; - } else { - throw new BoxCastException( "Value could not be cast." ); + @Override + public T get() { + if ( isPresent() ) { + return this.value; } + throw new BoxCastException( "The cast was not successful. You cannot get the value." ); } - /** - * Returns a non-empty string representation of this CastAttempt suitable for - * debugging. The exact presentation format is unspecified and may vary - * between implementations and versions. - * - * @return the string representation of this instance - */ - @Override - public String toString() { - return value != null - ? String.format( "CastAttempt[%s]", value ) - : "CastAttempt.empty"; - } } From 424f01c6cb0c1a64ae7ccd65d79193aeb934b27b Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 18:51:31 +0200 Subject: [PATCH 15/38] BL-622 --- .../boxlang/runtime/dynamic/Attempt.java | 2 +- .../runtime/dynamic/casters/CastAttempt.java | 39 ++++++++++++++----- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java b/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java index dfcc886f5..557e0c35a 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java @@ -465,7 +465,7 @@ public Attempt ifPresent( Consumer action ) { * * @param action The action to perform */ - public Attempt ifSuccesful( Consumer action ) { + public Attempt ifSuccessful( Consumer action ) { return ifPresent( action ); } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java index db339fae4..494327ccb 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java @@ -31,12 +31,21 @@ */ public final class CastAttempt extends Attempt { + private static final CastAttempt EMPTY = new CastAttempt<>(); + /** * |-------------------------------------------------------------------------- * | Constructors * |-------------------------------------------------------------------------- */ + /** + * Constructs an empty instance. + */ + private CastAttempt() { + super(); + } + /** * Constructs an instance with the value present. * @@ -52,16 +61,6 @@ private CastAttempt( T value ) { * |-------------------------------------------------------------------------- */ - /** - * Create an empty attempt - * - * @return An empty attempt - */ - @SuppressWarnings( "unchecked" ) - public static CastAttempt empty() { - return ( CastAttempt ) EMPTY; - } - /** * Returns an {@code CastAttempt} with the specified present non-null value. * @@ -88,6 +87,16 @@ public static CastAttempt ofNullable( T value ) { return value == null ? empty() : of( value ); } + /** + * Create an empty attempt + * + * @return An empty attempt + */ + @SuppressWarnings( "unchecked" ) + public static CastAttempt empty() { + return ( CastAttempt ) EMPTY; + } + /** * |-------------------------------------------------------------------------- * | Overrides @@ -110,4 +119,14 @@ public T get() { throw new BoxCastException( "The cast was not successful. You cannot get the value." ); } + /** + * Verifies if the attempt is empty + * + * @return True if the attempt is empty, false otherwise + */ + @Override + public boolean isPresent() { + return this.value != null; + } + } From 2fddf177d275048af2322247f2bd456c0847e21e Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 18:54:46 +0200 Subject: [PATCH 16/38] BL-617 #resolve arrayFind, arrayFindNoCase value closures, accept the value and now the index of the item as the second param --- .../runtime/bifs/global/array/ArrayFind.java | 22 +++++++++---------- .../bifs/global/array/ArrayFindTest.java | 13 ++++++----- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java b/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java index c756a83c4..f958d710b 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/array/ArrayFind.java @@ -14,18 +14,21 @@ */ package ortus.boxlang.runtime.bifs.global.array; +import java.util.Set; + import org.apache.commons.lang3.StringUtils; import ortus.boxlang.runtime.bifs.BIF; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.bifs.BoxMember; import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.dynamic.casters.FunctionCaster; import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; import ortus.boxlang.runtime.types.Array; import ortus.boxlang.runtime.types.BoxLangType; -import ortus.boxlang.runtime.types.Function; +import ortus.boxlang.runtime.validation.Validator; @BoxBIF @BoxBIF( alias = "ArrayFindNoCase" ) @@ -45,7 +48,7 @@ public ArrayFind() { declaredArguments = new Argument[] { new Argument( true, Argument.ARRAY, Key.array ), new Argument( true, Argument.ANY, Key.value ), - new Argument( false, Argument.BOOLEAN, Key.substringMatch, false ) + new Argument( false, Argument.BOOLEAN, Key.substringMatch, false, Set.of( Validator.NON_EMPTY ) ) }; } @@ -95,18 +98,15 @@ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { Object value = arguments.get( Key.value ); Boolean substringMatch = arguments.getAsBoolean( Key.substringMatch ); - // This case might exist. If it does, we need to set it to false - if ( substringMatch == null ) { - substringMatch = false; - } - // Go search by function or by value - int indexFound = value instanceof Function castedValueFunction + int indexFound = FunctionCaster.attempt( value, "Predicate" ) // Search by function - ? actualArray.findIndex( castedValueFunction, context ) + .map( targetFunction -> actualArray.findIndex( targetFunction, context ) ) // Search by value or by substring - : ( substringMatch ? actualArray.findIndexWithSubstring( value, isCaseSensitive( bifMethodKey ) ) - : actualArray.findIndex( value, isCaseSensitive( bifMethodKey ) ) ); + .orElseGet( () -> substringMatch + ? actualArray.findIndexWithSubstring( value, isCaseSensitive( bifMethodKey ) ) + : actualArray.findIndex( value, isCaseSensitive( bifMethodKey ) ) + ); // If the function is a boolean return function, return a boolean return isBooleanReturn( bifMethodKey ) ? indexFound > 0 : indexFound; diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java index 746f6c283..7036b8abf 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java @@ -25,7 +25,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -174,16 +173,18 @@ public void testMatchClosure() { @DisplayName( "Function can be Java functional interface" ) @Test - @Disabled( "See comments on https://ortussolutions.atlassian.net/browse/BL-617" ) + // @Disabled( "See comments on https://ortussolutions.atlassian.net/browse/BL-617" ) public void testJavaFunctionalInterface() { Predicate javaPredicate = ( s ) -> s.equals( "b" ); variables.put( "javaPredicate", javaPredicate ); + // @formatter:off instance.executeSource( """ - import java.util.function.Predicate; - result = ["a","b","c"].find( javaPredicate ); - """, - context ); + import java.util.function.Predicate; + result = ["a","b","c"].find( javaPredicate ); + """, + context ); + // @formatter:on int found = ( int ) variables.get( result ); assertThat( found ).isEqualTo( 2 ); From 34bfe57b2cbfe5fdab0c3ad0081dd005c177d4c1 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 18:54:56 +0200 Subject: [PATCH 17/38] BL-617 arrayFind, arrayFindNoCase value closures, accept the value and now the index of the item as the second param --- src/main/java/ortus/boxlang/runtime/types/Array.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/types/Array.java b/src/main/java/ortus/boxlang/runtime/types/Array.java index 0b77a07be..5393bd1ca 100644 --- a/src/main/java/ortus/boxlang/runtime/types/Array.java +++ b/src/main/java/ortus/boxlang/runtime/types/Array.java @@ -705,7 +705,13 @@ public int findIndex( Object value ) { */ public int findIndex( Function test, IBoxContext context ) { return intStream() - .filter( i -> BooleanCaster.cast( context.invokeFunction( test, new Object[] { get( i ), i } ) ) ) + .filter( i -> BooleanCaster.cast( + test.requiresStrictArguments() + // Java Lambdas + ? context.invokeFunction( test, new Object[] { get( i ) } ) + // BoxLang Functions, more args!!= + : context.invokeFunction( test, new Object[] { get( i ), i, this } ) + ) ) .findFirst() .orElse( -1 ) + 1; } From d342938c3f769dd91b4a9ebfb6179df0bcde5cc3 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 20:07:08 +0200 Subject: [PATCH 18/38] BL-623 #resolve New DynamicFunction type that can be used to generate dynamic BoxLang functions using Java Lambda proxies. Great for code generation --- .../runtime/types/DynamicFunction.java | 265 ++++++++++++++++++ .../boxlang/runtime/types/JavaMethod.java | 1 + .../runtime/types/DynamicFunctionTest.java | 81 ++++++ 3 files changed, 347 insertions(+) create mode 100644 src/main/java/ortus/boxlang/runtime/types/DynamicFunction.java create mode 100644 src/test/java/ortus/boxlang/runtime/types/DynamicFunctionTest.java diff --git a/src/main/java/ortus/boxlang/runtime/types/DynamicFunction.java b/src/main/java/ortus/boxlang/runtime/types/DynamicFunction.java new file mode 100644 index 000000000..e628fc5aa --- /dev/null +++ b/src/main/java/ortus/boxlang/runtime/types/DynamicFunction.java @@ -0,0 +1,265 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License.8 + */ +package ortus.boxlang.runtime.types; + +import java.nio.file.Path; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.List; +import java.util.function.BiFunction; + +import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.context.FunctionBoxContext; +import ortus.boxlang.runtime.loader.ImportDefinition; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.util.ResolvedFilePath; + +/** + * This represents a BoxLang Function that will be created dynamically either by Java or BoxLang. + * It can be used to generate using a Java Lambda, or subclass with no compiling or parsing. + * The impetus of this class is to be able to have an injected mixin that can pivot according to + * the name of the function as it's injected into a BoxLang class. + */ +public class DynamicFunction extends UDF { + + private static final Argument[] EMPTY_ARGUMENTS = new Argument[ 0 ]; + + /** + * |-------------------------------------------------------------------------- + * | Properties + * |-------------------------------------------------------------------------- + */ + + /** + * The injectable name of the function + */ + private Key name; + + /** + * The hint of the function + */ + private String hint = "Dynamic BoxLang Function"; + + /** + * The return type of the function, default is 'any' + */ + private String returnType = "any"; + + /** + * The arguments of the function + */ + private Argument[] arguments = EMPTY_ARGUMENTS; + + /** + * The annotations of the function + */ + private IStruct annotations = Struct.EMPTY; + + /** + * The documentation of the function + */ + private IStruct documentation = Struct.of( + "hint", + "I am a dynamic BoxLang function. I can be used to generate a function dynamically using Java or BoxLang." + ); + + /** + * The created on date of the function + */ + private Instant createdOn = Instant.now(); + + /** + * A Java BiFunction lambda that will proxy the function + * Receives the context and this class as arguments, can return anything + */ + private BiFunction target; + + /** + * |-------------------------------------------------------------------------- + * | Constructors + * |-------------------------------------------------------------------------- + */ + + /** + * Full Constructor + * + * @param name The name of the function that will be used to register it + * @param hint The hint of the function + * @param returnType The return type of the function + * @param arguments The arguments of the function + * @param annotations The annotations of the function + */ + public DynamicFunction( + Key name, + BiFunction target, + Argument[] arguments, + String returnType, + String hint, + IStruct annotations ) { + this.name = name; + this.target = target; + this.returnType = returnType; + this.arguments = arguments; + this.annotations = annotations; + this.documentation.put( "hint", hint ); + } + + /** + * Simple Constructor of just the name and the target + * + * @param name The name of the function that will be used to register it + * @param target The target lambda that will be executed when the function is called + */ + public DynamicFunction( + Key name, + BiFunction target ) { + this.name = name; + this.target = target; + } + + /** + * Simple Constructor of just the name, target, and return type + * + * @param name The name of the function that will be used to register it + * @param target The target lambda that will be executed when the function is called + * @param returnType The return type of the function + */ + public DynamicFunction( + Key name, + BiFunction target, String returnType ) { + this.name = name; + this.target = target; + this.returnType = returnType; + } + + /** + * |-------------------------------------------------------------------------- + * | Abstract Methods + * |-------------------------------------------------------------------------- + */ + + /** + * This is our proxy to the lambda we generate the function with + * + * @inheritDoc + */ + @Override + public Object _invoke( FunctionBoxContext context ) { + return this.target.apply( context, this ); + } + + /** + * @inheritDoc + */ + @Override + public Key getName() { + return this.name; + } + + /** + * @inheritDoc + */ + @Override + public Argument[] getArguments() { + return this.arguments; + } + + /** + * @inheritDoc + */ + @Override + public List getImports() { + return List.of(); + } + + /** + * @inheritDoc + */ + @Override + public ResolvedFilePath getRunnablePath() { + return ResolvedFilePath.of( Path.of( "dynamic-boxlang-function" ) ); + } + + /** + * @inheritDoc + */ + @Override + public BoxSourceType getSourceType() { + return BoxSourceType.BOXSCRIPT; + } + + /** + * @inheritDoc + */ + @Override + public String getReturnType() { + return this.returnType; + } + + /** + * @inheritDoc + */ + @Override + public IStruct getAnnotations() { + return this.annotations; + } + + /** + * @inheritDoc + */ + @Override + public IStruct getDocumentation() { + return this.documentation; + } + + /** + * All dynamic functions are usually public + * + * @inheritDoc + */ + @Override + public Access getAccess() { + return Access.PUBLIC; + } + + /** + * @inheritDoc + */ + @Override + public long getRunnableCompileVersion() { + return 0; + } + + /** + * @inheritDoc + */ + @Override + public LocalDateTime getRunnableCompiledOn() { + return LocalDateTime.ofInstant( this.createdOn, ZoneId.systemDefault() ); + } + + /** + * @inheritDoc + */ + @Override + public Object getRunnableAST() { + return null; + } + +} diff --git a/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java b/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java index b9e76c967..01d39cf3a 100644 --- a/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java +++ b/src/main/java/ortus/boxlang/runtime/types/JavaMethod.java @@ -57,6 +57,7 @@ public JavaMethod( Key name, DynamicObject dynamicObject ) { * * @return function name */ + @Override public Key getName() { return this.name; } diff --git a/src/test/java/ortus/boxlang/runtime/types/DynamicFunctionTest.java b/src/test/java/ortus/boxlang/runtime/types/DynamicFunctionTest.java new file mode 100644 index 000000000..a7ea4b5fb --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/types/DynamicFunctionTest.java @@ -0,0 +1,81 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License.8 + */ +package ortus.boxlang.runtime.types; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.IBoxContext; +import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; +import ortus.boxlang.runtime.scopes.IScope; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.scopes.VariablesScope; + +public class DynamicFunctionTest { + + static BoxRuntime instance; + IBoxContext context; + IScope variables; + static Key result = new Key( "result" ); + + @BeforeAll + public static void setUp() { + instance = BoxRuntime.getInstance( true ); + } + + @AfterAll + public static void teardown() { + } + + @BeforeEach + public void setupEach() { + context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); + variables = context.getScopeNearby( VariablesScope.name ); + } + + @DisplayName( "It can create dynamic function and execute it" ) + @Test + public void testDynamicFunction() { + DynamicFunction target = new DynamicFunction( + Key.of( "SayHelloBaby" ), + ( context, function ) -> { + System.out.println( context.getArgumentsScope().toString() ); + return "Hello World"; + } + ); + context.registerUDF( target ); + + // @formatter:off + instance.executeSource( + """ + println( "Executing source") + result = SayHelloBaby( "Luis Majano" ) + """, + context + ); + // @formatter:on + assertThat( variables.get( result ) ).isEqualTo( "Hello World" ); + } + +} From 066dc7ebba2be24249d5ac866f72d707b6ed7db9 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 8 Oct 2024 13:35:29 -0500 Subject: [PATCH 19/38] BL-624 --- .../ortus/boxlang/runtime/types/util/RegexUtil.java | 13 ++++++++++--- .../runtime/bifs/global/string/ReFindTest.java | 9 +++++++++ .../runtime/bifs/global/string/ReMatchTest.java | 10 ++++++++++ .../runtime/bifs/global/string/ReReplaceTest.java | 9 +++++++++ 4 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/types/util/RegexUtil.java b/src/main/java/ortus/boxlang/runtime/types/util/RegexUtil.java index 6e8c74e13..6aa6c236b 100644 --- a/src/main/java/ortus/boxlang/runtime/types/util/RegexUtil.java +++ b/src/main/java/ortus/boxlang/runtime/types/util/RegexUtil.java @@ -94,6 +94,11 @@ public static String posixReplace( String expression, Boolean noCase ) { * * @return The escaped regular expression string */ + /** + * @param input The regular expression string + * + * @return The escaped regular expression string + */ public static String replaceNonQuantiferCurlyBraces( String input ) { // Regular expression to match valid quantifiers like {2,4}, {3}, {,5}, etc. String quantifierRegex = "\\{\\d*,?\\d*\\}"; @@ -113,8 +118,8 @@ public static String replaceNonQuantiferCurlyBraces( String input ) { while ( matcher.find() ) { // Append text between matches and escape curly braces in that portion String betweenMatches = input.substring( lastIndex, matcher.start() ) - .replace( "{", "\\{" ) - .replace( "}", "\\}" ); + .replaceAll( "(? Date: Tue, 8 Oct 2024 14:25:30 -0500 Subject: [PATCH 20/38] BL-625 --- src/main/java/ortus/boxlang/runtime/util/FQN.java | 4 ++-- src/test/java/TestCases/phase3/ClassTest.java | 8 ++++++++ src/test/java/TestCases/phase3/MyRenderer.cfc | 7 +++++++ src/test/java/TestCases/phase3/Renderer.cfc | 6 ++++++ src/test/java/TestCases/phase3/_Renderer.cfc | 7 +++++++ 5 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/test/java/TestCases/phase3/MyRenderer.cfc create mode 100644 src/test/java/TestCases/phase3/Renderer.cfc create mode 100644 src/test/java/TestCases/phase3/_Renderer.cfc diff --git a/src/main/java/ortus/boxlang/runtime/util/FQN.java b/src/main/java/ortus/boxlang/runtime/util/FQN.java index 19687608b..434270319 100644 --- a/src/main/java/ortus/boxlang/runtime/util/FQN.java +++ b/src/main/java/ortus/boxlang/runtime/util/FQN.java @@ -186,7 +186,7 @@ protected String[] parseParts( String fqn, boolean allPackage ) { fqn = normalizeDots( fqn ); // Remove any non alpha-numeric chars. - fqn = fqn.replaceAll( "[^a-zA-Z0-9$\\.]", "" ); + fqn = fqn.replaceAll( "[^a-zA-Z0-9$\\.]", "__" ); if ( fqn.isEmpty() ) { return new String[] {}; @@ -281,7 +281,7 @@ protected String parseFromFile( Path file ) { // Replace / with . fqn = fqn.replaceAll( "/", "." ); // Remove any : from Windows drives - fqn = fqn.replaceAll( ":", "" ); + fqn = fqn.replaceAll( ":", "_" ); // Replace \ with . fqn = fqn.replaceAll( "\\\\", "." ); diff --git a/src/test/java/TestCases/phase3/ClassTest.java b/src/test/java/TestCases/phase3/ClassTest.java index 02e31e742..e05f1347c 100644 --- a/src/test/java/TestCases/phase3/ClassTest.java +++ b/src/test/java/TestCases/phase3/ClassTest.java @@ -1360,4 +1360,12 @@ public void testHypenInPath() { .isEqualTo( "src.test.java.TestCases.phase3.sub-folder.Funky-Class" ); } + @Test + public void testColdBoxRenderer() { + instance.executeSource( + """ + cfc = new src.test.java.TestCases.phase3.MyRenderer(); + """, context ); + } + } diff --git a/src/test/java/TestCases/phase3/MyRenderer.cfc b/src/test/java/TestCases/phase3/MyRenderer.cfc new file mode 100644 index 000000000..dbdcf1ed2 --- /dev/null +++ b/src/test/java/TestCases/phase3/MyRenderer.cfc @@ -0,0 +1,7 @@ +component extends="src.test.java.TestCases.phase3._Renderer" { + + function init() { + super.init(); + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/phase3/Renderer.cfc b/src/test/java/TestCases/phase3/Renderer.cfc new file mode 100644 index 000000000..5b4b31fc7 --- /dev/null +++ b/src/test/java/TestCases/phase3/Renderer.cfc @@ -0,0 +1,6 @@ +component { + + function init() { + } + +} \ No newline at end of file diff --git a/src/test/java/TestCases/phase3/_Renderer.cfc b/src/test/java/TestCases/phase3/_Renderer.cfc new file mode 100644 index 000000000..3b3002770 --- /dev/null +++ b/src/test/java/TestCases/phase3/_Renderer.cfc @@ -0,0 +1,7 @@ +component extends="src.test.java.TestCases.phase3.Renderer" { + + function init() { + super.init(); + } + +} \ No newline at end of file From d00b73a5bc2719a4bd81272b7a71123698c7374e Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 22:23:13 +0200 Subject: [PATCH 21/38] BL-626 #resolve New configuration: validBoxLangTemplates to determine which templates the Runnable Loader can process BL-627 #resolve New configuration: validClassExtensions to determine which class extensions to work with --- .../boxlang/runtime/config/Configuration.java | 51 ++++++++++++++++++- .../runtime/runnables/RunnableLoader.java | 17 +++++-- .../ortus/boxlang/runtime/scopes/Key.java | 3 ++ .../scripting/BoxScriptingFactory.java | 4 +- .../runtime/services/ApplicationService.java | 19 +++---- .../ortus/boxlang/runtime/types/Array.java | 11 ++++ src/main/resources/config/boxlang.json | 17 +++++++ 7 files changed, 105 insertions(+), 17 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/config/Configuration.java b/src/main/java/ortus/boxlang/runtime/config/Configuration.java index f04ea81e9..ce5d12162 100644 --- a/src/main/java/ortus/boxlang/runtime/config/Configuration.java +++ b/src/main/java/ortus/boxlang/runtime/config/Configuration.java @@ -26,9 +26,11 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.TimeZone; import org.slf4j.Logger; @@ -234,6 +236,16 @@ public class Configuration implements IConfigSegment { public List allowedFileOperationExtensions = new ArrayList<>(); public List disallowedFileOperationExtensions = new ArrayList<>(); + /** + * Valid BoxLang class extensions + */ + public Set validClassExtensions = new HashSet<>(); + + /** + * Valid BoxLang template extensions + */ + public Set validTemplateExtensions = new HashSet<>(); + /** * Experimental Features */ @@ -397,7 +409,7 @@ public Configuration process( IStruct config ) { } } ); } else { - logger.warn( "The [runtime.javaLibraryPaths] configuration is not a JSON Object, ignoring it." ); + logger.warn( "The [javaLibraryPaths] configuration is not a JSON Array, ignoring it." ); } } @@ -453,6 +465,26 @@ public Configuration process( IStruct config ) { } } + // Process validClassExtensions + if ( config.containsKey( Key.validClassExtensions ) ) { + if ( config.get( Key.validClassExtensions ) instanceof List castedList ) { + // iterate and add to the original list if it doesn't exist + castedList.forEach( item -> this.validClassExtensions.add( PlaceholderHelper.resolve( item ).toLowerCase() ) ); + } else { + logger.warn( "The [validClassExtensions] configuration is not a JSON Array, ignoring it." ); + } + } + + // Process validtemplateExtensions + if ( config.containsKey( Key.validTemplateExtensions ) ) { + if ( config.get( Key.validTemplateExtensions ) instanceof List castedList ) { + // iterate and add to the original list if it doesn't exist + castedList.forEach( item -> this.validTemplateExtensions.add( PlaceholderHelper.resolve( item ).toLowerCase() ) ); + } else { + logger.warn( "The [validTemplateExtensions] configuration is not a JSON Array, ignoring it." ); + } + } + // Process experimentals map if ( config.containsKey( Key.experimental ) ) { if ( config.get( Key.experimental ) instanceof Map castedMap ) { @@ -725,6 +757,18 @@ public Navigator navigate( String... path ) { return DataNavigator.of( asStruct() ).from( path ); } + /** + * This returns all valid BoxLang extensions for classes and templates. + * + * @return A set of all valid class extensions + */ + public Set getValidExtensions() { + Set extensions = new HashSet<>(); + extensions.addAll( this.validClassExtensions ); + extensions.addAll( this.validTemplateExtensions ); + return extensions; + } + /** * -------------------------------------------------------------------------- * Conversion @@ -781,7 +825,10 @@ public IStruct asStruct() { Key.setClientCookies, this.setClientCookies, Key.setDomainCookies, this.setDomainCookies, Key.timezone, this.timezone, - Key.useHighPrecisionMath, this.useHighPrecisionMath + Key.useHighPrecisionMath, this.useHighPrecisionMath, + Key.validExtensions, Array.fromSet( getValidExtensions() ), + Key.validClassExtensions, Array.fromSet( this.validClassExtensions ), + Key.validTemplateExtensions, Array.fromSet( this.validTemplateExtensions ) ); } } diff --git a/src/main/java/ortus/boxlang/runtime/runnables/RunnableLoader.java b/src/main/java/ortus/boxlang/runtime/runnables/RunnableLoader.java index fa3524f5d..24d5aa941 100644 --- a/src/main/java/ortus/boxlang/runtime/runnables/RunnableLoader.java +++ b/src/main/java/ortus/boxlang/runtime/runnables/RunnableLoader.java @@ -24,6 +24,8 @@ import ortus.boxlang.compiler.asmboxpiler.ASMBoxpiler; import ortus.boxlang.compiler.javaboxpiler.JavaBoxpiler; import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.config.Configuration; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.StaticClassBoxContext; import ortus.boxlang.runtime.interop.DynamicObject; @@ -48,9 +50,18 @@ public class RunnableLoader { * Singleton instance */ private static RunnableLoader instance; + + /** + * The Boxpiler to use + */ private IBoxpiler boxpiler; - // TODO: make this configurable and move cf extensions to compat - private static final Set VALID_TEMPLATE_EXTENSIONS = Set.of( "cfm", "cfml", "cfs", "bxs", "bxm", "bxml" ); + + /** + * Valid template extensions + * + * @see Configuration#validTemplateExtensions + */ + private static final Set VALID_TEMPLATE_EXTENSIONS = BoxRuntime.getInstance().getConfiguration().validTemplateExtensions; /** * -------------------------------------------------------------------------- @@ -248,7 +259,7 @@ public Class loadClass( ResolvedFilePath resolvedFilePath, IBoxCon /** * Run static initializers for a Box class - * + * * @param clazz The class to run the static initializer for * @param context The context to use */ diff --git a/src/main/java/ortus/boxlang/runtime/scopes/Key.java b/src/main/java/ortus/boxlang/runtime/scopes/Key.java index cca2fdb55..31a191a5b 100644 --- a/src/main/java/ortus/boxlang/runtime/scopes/Key.java +++ b/src/main/java/ortus/boxlang/runtime/scopes/Key.java @@ -707,6 +707,9 @@ public class Key implements Comparable, Serializable { public static final Key value = Key.of( "value" ); public static final Key var = Key.of( "var" ); public static final Key variable = Key.of( "variable" ); + public static final Key validExtensions = Key.of( "validExtensions" ); + public static final Key validClassExtensions = Key.of( "validClassExtensions" ); + public static final Key validTemplateExtensions = Key.of( "validTemplateExtensions" ); public static final Key variables = Key.of( "variables" ); public static final Key variant = Key.of( "variant" ); public static final Key version = Key.of( "version" ); diff --git a/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingFactory.java b/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingFactory.java index 057465908..46d586f3b 100644 --- a/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingFactory.java +++ b/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingFactory.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -26,6 +27,7 @@ import javax.script.ScriptEngineFactory; import javax.script.ScriptEngineManager; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.IStruct; import ortus.boxlang.runtime.types.Struct; @@ -177,7 +179,7 @@ public BoxScriptingEngine getScriptEngine( Boolean debug ) { */ @Override public List getExtensions() { - return List.of( "bx", "cfm", "cfc", "cfs", "bxs", "bxm" ); + return new ArrayList<>( BoxRuntime.getInstance().getConfiguration().getValidExtensions() ); } /** diff --git a/src/main/java/ortus/boxlang/runtime/services/ApplicationService.java b/src/main/java/ortus/boxlang/runtime/services/ApplicationService.java index 11dbd67bf..7946b54be 100644 --- a/src/main/java/ortus/boxlang/runtime/services/ApplicationService.java +++ b/src/main/java/ortus/boxlang/runtime/services/ApplicationService.java @@ -21,8 +21,6 @@ import java.net.URI; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -58,21 +56,18 @@ public class ApplicationService extends BaseService { /** * The applications for this runtime - * TODO: timeout applications */ - private Map applications = new ConcurrentHashMap<>(); + private Map applications = new ConcurrentHashMap<>(); /** - * Extensions to search for application descriptor templates + * Extensions to search for application descriptor classes: Application.bx, Application.cfc, etc */ - // TODO: contribute cfc from compat extension - private Set applicationDescriptorClassExtensions = new HashSet<>( Arrays.asList( "bx", "cfc" ) ); + private Set applicationDescriptorClassExtensions; /** - * Extensions to search for application descriptor classes + * Extensions to search for application descriptor templates: Application.bxm, Application.bxml, etc */ - // TODO: contribute cfc from compat extension - private Set applicationDescriptorExtensions = new HashSet<>( Arrays.asList( "bxm", "bxs", "cfm", "cfs" ) ); + private Set applicationDescriptorExtensions; /** * The types of application listeners we support: Application classes and @@ -188,7 +183,9 @@ public String[] getApplicationNames() { */ @Override public void onStartup() { - // logger.info( "ApplicationService.onStartup()" ); + // Setup the application descriptor extensions from the runtime configuration + this.applicationDescriptorClassExtensions = BoxRuntime.getInstance().getConfiguration().validClassExtensions; + this.applicationDescriptorExtensions = BoxRuntime.getInstance().getConfiguration().validTemplateExtensions; } /** diff --git a/src/main/java/ortus/boxlang/runtime/types/Array.java b/src/main/java/ortus/boxlang/runtime/types/Array.java index 5393bd1ca..f2577cb8a 100644 --- a/src/main/java/ortus/boxlang/runtime/types/Array.java +++ b/src/main/java/ortus/boxlang/runtime/types/Array.java @@ -204,6 +204,17 @@ public static Array fromList( List list ) { return new Array( list ); } + /** + * Create an array from a Set + * + * @param set The set to create the Array from + * + * @return The array + */ + public static Array fromSet( Set set ) { + return new Array( new ArrayList<>( set ) ); + } + /** * Create an Array from a Java array * diff --git a/src/main/resources/config/boxlang.json b/src/main/resources/config/boxlang.json index 32de963eb..720debb4a 100644 --- a/src/main/resources/config/boxlang.json +++ b/src/main/resources/config/boxlang.json @@ -9,6 +9,23 @@ * ${env.variablename:defaultValue} - The value of a valid environment variable or the default value. Example: ${env.CFCONFIG_HOME:/etc/cfconfig} */ { + // Extensions BoxLang will process as classes + "validClassExtensions": [ + "bx", + // Moving to compat at final release + "cfc" + ], + // Extensions BoxLang will process as templates. + // This is used by the RunnableLoader + "validTemplateExtensions": [ + "bxs", + "bxm", + "bxml", + // Moving to compat at final release + "cfm", + "cfml", + "cfs" + ], // Where all generated classes will be placed "classGenerationDirectory": "${boxlang-home}/classes", // This puts the entire runtime in debug mode From 59f74a0a662f8058e49cc268131eb64d6bae4326 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 22:32:44 +0200 Subject: [PATCH 22/38] fixing my test --- .../boxlang/runtime/scripting/BoxScriptingFactoryTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/ortus/boxlang/runtime/scripting/BoxScriptingFactoryTest.java b/src/test/java/ortus/boxlang/runtime/scripting/BoxScriptingFactoryTest.java index 86889325e..cdbe72f49 100644 --- a/src/test/java/ortus/boxlang/runtime/scripting/BoxScriptingFactoryTest.java +++ b/src/test/java/ortus/boxlang/runtime/scripting/BoxScriptingFactoryTest.java @@ -59,7 +59,7 @@ public void testGetScriptEngine() { @Test public void testGetExtensions() { BoxScriptingFactory factory = new BoxScriptingFactory(); - assertThat( factory.getExtensions() ).containsExactly( "bx", "cfm", "cfc", "cfs", "bxs", "bxm" ); + assertThat( factory.getExtensions() ).containsAnyOf( "bx", "bxm", "bxs" ); } @Test From 4a8edae592db0371988c286f328cbc0af82ff916 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 8 Oct 2024 16:07:53 -0500 Subject: [PATCH 23/38] BL-625 --- src/test/java/ortus/boxlang/runtime/util/FQNTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/ortus/boxlang/runtime/util/FQNTest.java b/src/test/java/ortus/boxlang/runtime/util/FQNTest.java index 55c586e07..0a0a3415e 100644 --- a/src/test/java/ortus/boxlang/runtime/util/FQNTest.java +++ b/src/test/java/ortus/boxlang/runtime/util/FQNTest.java @@ -72,6 +72,6 @@ void testItCanParseFQNWithPrefix() { @Test void testItCanCleanInvalidParts() { FQN fqn = new FQN( Paths.get( "/src\\\\test\\class/ORTUS//switch/45/2run-time/util/fqntest.java" ) ); - assertEquals( "src.test._class.ortus._switch._45._2runtime.util.Fqntest$java", fqn.toString() ); + assertEquals( "src.test._class.ortus._switch._45._2run__time.util.Fqntest$java", fqn.toString() ); } } From fd04e0d0df83dab359790814f88fce7b593a44fe Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 8 Oct 2024 19:29:15 -0500 Subject: [PATCH 24/38] BL-628 --- src/main/antlr/CFLexer.g4 | 18 ++++++++++----- .../TestCases/components/BoxTemplateTest.java | 22 +++++++++++++++++++ .../TestCases/components/CFTemplateTest.java | 22 +++++++++++++++++++ 3 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/main/antlr/CFLexer.g4 b/src/main/antlr/CFLexer.g4 index 4e1b612b9..abf4a7ecb 100644 --- a/src/main/antlr/CFLexer.g4 +++ b/src/main/antlr/CFLexer.g4 @@ -81,10 +81,14 @@ options { } public void reset() { + resetCounters(); + super.reset(); + } + + public void resetCounters() { parenCount = 0; braceCount = 0; bracketCount = 0; - super.reset(); } private boolean isExpressionComplete() { @@ -480,19 +484,23 @@ TEMPLATE_ARGUMENT : 'argument' -> pushMode( TEMPLATE_COMPONENT_MODE ); // return may or may not have an expression, so eat any leading whitespace now so it doesn't give us an expression part that's just a space TEMPLATE_RETURN: - 'return' [ \t\r\n]* -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode( TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( DEFAULT_SCRIPT_MODE) + 'return' [ \t\r\n]* {resetCounters();} -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode( TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( + DEFAULT_SCRIPT_MODE) ; TEMPLATE_IF: - 'if' [ \t\r\n]+ -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode(TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( DEFAULT_SCRIPT_MODE) + 'if' [ \t\r\n]+ {resetCounters();} -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode(TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( + DEFAULT_SCRIPT_MODE) ; TEMPLATE_ELSE: 'else' -> pushMode( TEMPLATE_COMPONENT_MODE ); TEMPLATE_ELSEIF: - 'elseif' [ \t\r\n]+ -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode(TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( DEFAULT_SCRIPT_MODE) + 'elseif' [ \t\r\n]+ {resetCounters();} -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode(TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( + DEFAULT_SCRIPT_MODE) ; TEMPLATE_SET: - 'set' [ \t\r\n]+ -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode( TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( DEFAULT_SCRIPT_MODE) + 'set' [ \t\r\n]+ {resetCounters();} -> pushMode( TEMPLATE_COMPONENT_MODE ), pushMode( TEMPLATE_EXPRESSION_MODE_COMPONENT), pushMode( + DEFAULT_SCRIPT_MODE) ; TEMPLATE_TRY : 'try' -> pushMode( TEMPLATE_COMPONENT_MODE ); diff --git a/src/test/java/TestCases/components/BoxTemplateTest.java b/src/test/java/TestCases/components/BoxTemplateTest.java index 8cd11a013..d651594c5 100644 --- a/src/test/java/TestCases/components/BoxTemplateTest.java +++ b/src/test/java/TestCases/components/BoxTemplateTest.java @@ -20,6 +20,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -28,6 +30,7 @@ import org.junit.jupiter.api.Test; import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.compiler.parser.Parser; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; @@ -1266,4 +1269,23 @@ public void testPoundInOutput() { assertThat( variables.getAsString( result ).replaceAll( "\\s", "" ) ).isEqualTo( "foo##barbaz#bum" ); } + @Test + public void testQueryInTemplateIsland() { + try { + var result = new Parser().parse( + """ + { + ``` + + ``` + } + """, BoxSourceType.BOXSCRIPT ); + if ( !result.isCorrect() ) { + throw new ParseException( result.getIssues(), "" ); + } + } catch ( IOException e ) { + throw new RuntimeException( e ); + } + } + } diff --git a/src/test/java/TestCases/components/CFTemplateTest.java b/src/test/java/TestCases/components/CFTemplateTest.java index 66382fe3b..80b6a358a 100644 --- a/src/test/java/TestCases/components/CFTemplateTest.java +++ b/src/test/java/TestCases/components/CFTemplateTest.java @@ -20,6 +20,8 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; + import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -28,6 +30,7 @@ import org.junit.jupiter.api.Test; import ortus.boxlang.compiler.parser.BoxSourceType; +import ortus.boxlang.compiler.parser.Parser; import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; @@ -1506,4 +1509,23 @@ public void testAttributeUnquotedHashed() { context, BoxSourceType.CFTEMPLATE ); } + @Test + public void testQueryInTemplateIsland() { + try { + var result = new Parser().parse( + """ + { + ``` + + ``` + } + """, BoxSourceType.CFSCRIPT ); + if ( !result.isCorrect() ) { + throw new ParseException( result.getIssues(), "" ); + } + } catch ( IOException e ) { + throw new RuntimeException( e ); + } + } + } From efa0f4c584040f74193ab515b29a787e226a39ee Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 8 Oct 2024 22:38:59 +0200 Subject: [PATCH 25/38] start of security settings --- src/main/resources/config/boxlang.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/resources/config/boxlang.json b/src/main/resources/config/boxlang.json index 720debb4a..91dffdb73 100644 --- a/src/main/resources/config/boxlang.json +++ b/src/main/resources/config/boxlang.json @@ -212,6 +212,23 @@ } } }, + // These are the security settings for the runtime + "security": { + // All regex patterns are case-insensitive + // A list of regex patterns that will match import statements, and if matched, execution will be disallowed + // Ex: "disallowedImports": ["java\\.lang\\.(ProcessBuilder|Reflect", "java\\.io\\.(File|FileWriter)"] + "disallowedImports": [], + // A list of BIF names that will be disallowed from execution + // Ex: "disallowedBifs": ["createObject", "systemExecute"] + "disallowedBifs": [], + // A list of Component names that will be disallowed from execution + // Ex: "disallowedComponents": [ "execute", "http" ] + "disallowedComponents": [], + // A list of regex patterns that will match expressions in any type of source, and if matched, execution will be disallowed + // Careful here, you can disallow a lot of things + // Ex: "disallowedExpressions": [ "(\s)Runtime\\.(.*)" ] + "disallowedExpressions": [] + }, /** * The BoxLang module settings * The key is the module name and the value is a struct of settings for that specific module From 2507e14cbd6060b4168ff89356a815e4e2b678f9 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 9 Oct 2024 15:59:13 +0200 Subject: [PATCH 26/38] hmm, seems some straglers for testing where left here --- test.bxs | 2 -- test.cfs | 2 -- 2 files changed, 4 deletions(-) delete mode 100644 test.bxs delete mode 100644 test.cfs diff --git a/test.bxs b/test.bxs deleted file mode 100644 index 9bba71eed..000000000 --- a/test.bxs +++ /dev/null @@ -1,2 +0,0 @@ -foo.BAR = "test"; -echo( foo.BAR ); diff --git a/test.cfs b/test.cfs deleted file mode 100644 index b2373d500..000000000 --- a/test.cfs +++ /dev/null @@ -1,2 +0,0 @@ -foo.bar = "test" -echo( foo.bar ) \ No newline at end of file From 6f5bf049b565bf38e6bd655b33342de1c720f59a Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 9 Oct 2024 18:12:20 +0200 Subject: [PATCH 27/38] BL-629 #resolve New security configuration section for disallowing: BIFS, Components, Imports and Expressions --- .../boxlang/runtime/config/Configuration.java | 7 + .../config/segments/SecurityConfig.java | 140 ++++++++++++++++++ .../runtime/config/util/PropertyHelper.java | 58 ++++++++ .../runtime/context/BaseBoxContext.java | 2 + .../ortus/boxlang/runtime/scopes/Key.java | 5 + .../runtime/services/FunctionService.java | 2 +- 6 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java create mode 100644 src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java diff --git a/src/main/java/ortus/boxlang/runtime/config/Configuration.java b/src/main/java/ortus/boxlang/runtime/config/Configuration.java index ce5d12162..b76812bc6 100644 --- a/src/main/java/ortus/boxlang/runtime/config/Configuration.java +++ b/src/main/java/ortus/boxlang/runtime/config/Configuration.java @@ -42,6 +42,7 @@ import ortus.boxlang.runtime.config.segments.ExecutorConfig; import ortus.boxlang.runtime.config.segments.IConfigSegment; import ortus.boxlang.runtime.config.segments.ModuleConfig; +import ortus.boxlang.runtime.config.segments.SecurityConfig; import ortus.boxlang.runtime.config.util.PlaceholderHelper; import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; @@ -251,6 +252,11 @@ public class Configuration implements IConfigSegment { */ public IStruct experimental = new Struct(); + /** + * The security configuration + */ + public SecurityConfig security = new SecurityConfig(); + /** * -------------------------------------------------------------------------- * Private Properties @@ -824,6 +830,7 @@ public IStruct asStruct() { Key.sessionTimeout, this.sessionTimeout, Key.setClientCookies, this.setClientCookies, Key.setDomainCookies, this.setDomainCookies, + Key.security, this.security.asStruct(), Key.timezone, this.timezone, Key.useHighPrecisionMath, this.useHighPrecisionMath, Key.validExtensions, Array.fromSet( getValidExtensions() ), diff --git a/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java b/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java new file mode 100644 index 000000000..2dcceae86 --- /dev/null +++ b/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java @@ -0,0 +1,140 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.runtime.config.segments; + +import java.util.HashSet; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ortus.boxlang.runtime.config.util.PropertyHelper; +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.Struct; + +/** + * The SecurityConfig class is a configuration segment that is used to define the security settings for the BoxLang runtime. + */ +public class SecurityConfig implements IConfigSegment { + + /** + * A list of disallowed imports for the runtime + * These are a list of regular expressions that are used to match against the import statements in the code + * Ex: "disallowedImports": ["java\\.lang\\.(ProcessBuilder|Reflect", "java\\.io\\.(File|FileWriter)"] + */ + public Set disallowedImports = new HashSet<>(); + + /** + * A list of regex patterns that will match expressions in any type of source, and if matched, execution will be disallowed + * Careful here, you can disallow a lot of things + * Ex: "disallowedExpressions": [ "(\s)Runtime\\.(.*)" ] + */ + public Set disallowedExpressions = new HashSet<>(); + + /** + * Disallowed BIFs in the runtime + * Ex: "disallowedBifs": ["createObject", "systemExecute"] + */ + public Set disallowedBIFs = new HashSet<>(); + + /** + * Disallowed Components in the runtime + * Ex: "disallowedComponents": [ "execute", "http" ] + */ + public Set disallowedComponents = new HashSet<>(); + + /** + * -------------------------------------------------------------------------- + * Private Properties + * -------------------------------------------------------------------------- + */ + + /** + * Logger + */ + private static final Logger logger = LoggerFactory.getLogger( SecurityConfig.class ); + + /** + * -------------------------------------------------------------------------- + * Methods + * -------------------------------------------------------------------------- + */ + + /** + * Default empty constructor + */ + public SecurityConfig() { + // Default all things + } + + /** + * This function takes in the name of a BIF to test if it is disallowed. + * The search is case-insensitive. + * If it's disallowed, it will throw a SecurityException, else it will return true + */ + public boolean isBIFDisallowed( String name ) { + if ( this.disallowedBIFs.stream().anyMatch( name::equalsIgnoreCase ) ) { + throw new SecurityException( "The BIF '" + name + "' is disallowed, please check your security configuration in the language configuration file." ); + } + return true; + } + + /** + * This function takes in the name of a Component to test if it is disallowed. + * The search is case-insensitive. + * If it's disallowed, it will throw a SecurityException, else it will return true + */ + public boolean isComponentDisallowed( String name ) { + if ( this.disallowedComponents.stream().anyMatch( name::equalsIgnoreCase ) ) { + throw new SecurityException( + "The Component '" + name + "' is disallowed, please check your security configuration in the language configuration file." ); + } + return true; + } + + /** + * Processes the configuration struct. Each segment is processed individually from the initial configuration struct. + * + * @param config the configuration struct + * + * @return the configuration + */ + @Override + public IConfigSegment process( IStruct config ) { + PropertyHelper.processListToSet( config, Key.disallowedImports, this.disallowedImports ); + PropertyHelper.processListToSet( config, Key.disallowedExpressions, this.disallowedExpressions ); + PropertyHelper.processListToSet( config, Key.disallowedBIFs, this.disallowedBIFs ); + PropertyHelper.processListToSet( config, Key.disallowedComponents, this.disallowedComponents ); + return this; + } + + /** + * @inheritDoc + */ + @Override + public IStruct asStruct() { + return Struct.of( + Key.disallowedImports, this.disallowedImports, + Key.disallowedExpressions, this.disallowedExpressions, + Key.disallowedBIFs, this.disallowedBIFs, + Key.disallowedComponents, this.disallowedComponents + ); + } + +} diff --git a/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java b/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java new file mode 100644 index 000000000..a7734f467 --- /dev/null +++ b/src/main/java/ortus/boxlang/runtime/config/util/PropertyHelper.java @@ -0,0 +1,58 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.runtime.config.util; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ortus.boxlang.runtime.scopes.Key; +import ortus.boxlang.runtime.types.IStruct; + +/** + * Helps convert from JSON to native Java/BoxLang Types + */ +public class PropertyHelper { + + /** + * Logger + */ + private static final Logger logger = LoggerFactory.getLogger( PropertyHelper.class ); + + /** + * Process the target key + * + * @param config The configuration object + * @param key The target key to look and process + * @param target The target set to populate + */ + @SuppressWarnings( "unchecked" ) + public static void processListToSet( IStruct config, Key key, Set target ) { + if ( config.containsKey( key ) ) { + if ( config.get( key ) instanceof List castedList ) { + target.addAll( ( Collection ) castedList ); + } else { + logger.warn( "The property [{}] must be a JSON Array", key ); + } + } + } + +} diff --git a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java index 5ede0bc7d..6b37f0e95 100644 --- a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java @@ -476,6 +476,7 @@ public Object invokeFunction( Key name ) { * */ public Component.BodyResult invokeComponent( Key name, IStruct attributes, Component.ComponentBody componentBody ) { + getRuntime().getConfiguration().security.isComponentDisallowed( name.getName() ); ComponentDescriptor comp = componentService.getComponent( name ); if ( comp != null ) { return comp.invoke( this, attributes, componentBody ); @@ -506,6 +507,7 @@ public Object invokeFunction( Object function, Object[] positionalArguments ) { * @return The BIFDescriptor if found, else null */ protected BIFDescriptor findBIF( Key name ) { + getRuntime().getConfiguration().security.isBIFDisallowed( name.getName() ); return this.functionService.getGlobalFunction( name ); } diff --git a/src/main/java/ortus/boxlang/runtime/scopes/Key.java b/src/main/java/ortus/boxlang/runtime/scopes/Key.java index 31a191a5b..067df8c95 100644 --- a/src/main/java/ortus/boxlang/runtime/scopes/Key.java +++ b/src/main/java/ortus/boxlang/runtime/scopes/Key.java @@ -243,6 +243,10 @@ public class Key implements Comparable, Serializable { public static final Key directoryMove = Key.of( "directoryMove" ); public static final Key disabled = Key.of( "disabled" ); public static final Key display = Key.of( "display" ); + public static final Key disallowedImports = Key.of( "disallowedImports" ); + public static final Key disallowedBIFs = Key.of( "disallowedBIFs" ); + public static final Key disallowedComponents = Key.of( "disallowedComponents" ); + public static final Key disallowedExpressions = Key.of( "disallowedExpressions" ); public static final Key doAll = Key.of( "doAll" ); public static final Key documentation = Key.of( "documentation" ); public static final Key dollarFormat = Key.of( "dollarFormat" ); @@ -586,6 +590,7 @@ public class Key implements Comparable, Serializable { public static final Key schedulerService = Key.of( "schedulerService" ); public static final Key scope = Key.of( "scope" ); public static final Key script_name = Key.of( "script_name" ); + public static final Key security = Key.of( "security" ); public static final Key second = Key.of( "second" ); public static final Key seconds = Key.of( "seconds" ); public static final Key secure = Key.of( "secure" ); diff --git a/src/main/java/ortus/boxlang/runtime/services/FunctionService.java b/src/main/java/ortus/boxlang/runtime/services/FunctionService.java index fc55037a9..a098e777b 100644 --- a/src/main/java/ortus/boxlang/runtime/services/FunctionService.java +++ b/src/main/java/ortus/boxlang/runtime/services/FunctionService.java @@ -195,7 +195,7 @@ public BIFDescriptor getGlobalFunction( String name ) { } /** - * Returns the global function with the given name + * Returns the global function with the given name. * * @param name The name of the global function * From 9f3559f220efad69ea1e06da111000f61332ecfd Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 9 Oct 2024 22:21:04 +0200 Subject: [PATCH 28/38] BL-629 #resolve New security configuration section for disallowing: BIFS, Components, Imports --- .../config/segments/SecurityConfig.java | 58 ++++++++++--- .../runtime/context/BaseBoxContext.java | 4 +- .../loader/resolvers/BaseResolver.java | 9 +- .../ortus/boxlang/runtime/scopes/Key.java | 1 - src/main/resources/config/boxlang.json | 7 +- .../config/segments/SecurityConfigTest.java | 83 +++++++++++++++++++ .../loader/resolvers/JavaResolverTest.java | 4 +- 7 files changed, 144 insertions(+), 22 deletions(-) create mode 100644 src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java diff --git a/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java b/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java index 2dcceae86..3375ca683 100644 --- a/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java +++ b/src/main/java/ortus/boxlang/runtime/config/segments/SecurityConfig.java @@ -18,7 +18,9 @@ package ortus.boxlang.runtime.config.segments; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,13 +42,6 @@ public class SecurityConfig implements IConfigSegment { */ public Set disallowedImports = new HashSet<>(); - /** - * A list of regex patterns that will match expressions in any type of source, and if matched, execution will be disallowed - * Careful here, you can disallow a lot of things - * Ex: "disallowedExpressions": [ "(\s)Runtime\\.(.*)" ] - */ - public Set disallowedExpressions = new HashSet<>(); - /** * Disallowed BIFs in the runtime * Ex: "disallowedBifs": ["createObject", "systemExecute"] @@ -70,6 +65,13 @@ public class SecurityConfig implements IConfigSegment { */ private static final Logger logger = LoggerFactory.getLogger( SecurityConfig.class ); + /** + * Maps of allowed BIFs so lookups get faster as we go + */ + public Map allowedBIFsLookup = new ConcurrentHashMap<>(); + public Map allowedComponentsLookup = new ConcurrentHashMap<>(); + public Map allowedImportsLookup = new ConcurrentHashMap<>(); + /** * -------------------------------------------------------------------------- * Methods @@ -88,10 +90,17 @@ public SecurityConfig() { * The search is case-insensitive. * If it's disallowed, it will throw a SecurityException, else it will return true */ - public boolean isBIFDisallowed( String name ) { + public boolean isBIFAllowed( String name ) { + // Is it allowed already? + if ( this.allowedBIFsLookup.containsKey( name ) ) { + return true; + } + // Check if it's disallowed if ( this.disallowedBIFs.stream().anyMatch( name::equalsIgnoreCase ) ) { throw new SecurityException( "The BIF '" + name + "' is disallowed, please check your security configuration in the language configuration file." ); } + // Add it + this.allowedBIFsLookup.put( name, true ); return true; } @@ -100,11 +109,40 @@ public boolean isBIFDisallowed( String name ) { * The search is case-insensitive. * If it's disallowed, it will throw a SecurityException, else it will return true */ - public boolean isComponentDisallowed( String name ) { + public boolean isComponentAllowed( String name ) { + // Is it allowed already? + if ( this.allowedComponentsLookup.containsKey( name ) ) { + return true; + } + // Check if it's disallowed if ( this.disallowedComponents.stream().anyMatch( name::equalsIgnoreCase ) ) { throw new SecurityException( "The Component '" + name + "' is disallowed, please check your security configuration in the language configuration file." ); } + // Add it + this.allowedComponentsLookup.put( name, true ); + return true; + } + + /** + * This function takes in a fully qualified class name and tests if it is disallowed. + * The search is case-insensitive. + * If it's disallowed, it will throw a SecurityException, else it will return true + */ + public boolean isClassAllowed( String name ) { + // Is it allowed already? + if ( this.allowedImportsLookup.containsKey( name ) ) { + return true; + } + // Check if it's disallowed + if ( this.disallowedImports + .stream() + .anyMatch( name::matches ) ) { + throw new SecurityException( + "The class '" + name + "' is disallowed, please check your security configuration in the language configuration file." ); + } + // Add it + this.allowedImportsLookup.put( name, true ); return true; } @@ -118,7 +156,6 @@ public boolean isComponentDisallowed( String name ) { @Override public IConfigSegment process( IStruct config ) { PropertyHelper.processListToSet( config, Key.disallowedImports, this.disallowedImports ); - PropertyHelper.processListToSet( config, Key.disallowedExpressions, this.disallowedExpressions ); PropertyHelper.processListToSet( config, Key.disallowedBIFs, this.disallowedBIFs ); PropertyHelper.processListToSet( config, Key.disallowedComponents, this.disallowedComponents ); return this; @@ -131,7 +168,6 @@ public IConfigSegment process( IStruct config ) { public IStruct asStruct() { return Struct.of( Key.disallowedImports, this.disallowedImports, - Key.disallowedExpressions, this.disallowedExpressions, Key.disallowedBIFs, this.disallowedBIFs, Key.disallowedComponents, this.disallowedComponents ); diff --git a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java index 6b37f0e95..ed472e040 100644 --- a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java @@ -476,7 +476,7 @@ public Object invokeFunction( Key name ) { * */ public Component.BodyResult invokeComponent( Key name, IStruct attributes, Component.ComponentBody componentBody ) { - getRuntime().getConfiguration().security.isComponentDisallowed( name.getName() ); + getRuntime().getConfiguration().security.isComponentAllowed( name.getName() ); ComponentDescriptor comp = componentService.getComponent( name ); if ( comp != null ) { return comp.invoke( this, attributes, componentBody ); @@ -507,7 +507,7 @@ public Object invokeFunction( Object function, Object[] positionalArguments ) { * @return The BIFDescriptor if found, else null */ protected BIFDescriptor findBIF( Key name ) { - getRuntime().getConfiguration().security.isBIFDisallowed( name.getName() ); + getRuntime().getConfiguration().security.isBIFAllowed( name.getName() ); return this.functionService.getGlobalFunction( name ); } diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java index aa9841086..eea5ef3ac 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java @@ -174,11 +174,12 @@ public Optional resolve( IBoxContext context, String name, List imports ) { - return imports.stream() + var fullyQualifiedName = imports.stream() // Discover import by matching the resolver prefix and the class name or alias or multi-import .filter( thisImport -> importApplies( thisImport ) && importHas( thisImport, className ) ) // Return the first one, the first one wins .findFirst() + // Convert the import to a fully qualified class name .map( targetImport -> { String fqn = targetImport.getFullyQualifiedClass( className ); importCache.add( className + ":" + fqn ); @@ -186,6 +187,12 @@ public String expandFromImport( IBoxContext context, String className, List, Serializable { public static final Key disallowedImports = Key.of( "disallowedImports" ); public static final Key disallowedBIFs = Key.of( "disallowedBIFs" ); public static final Key disallowedComponents = Key.of( "disallowedComponents" ); - public static final Key disallowedExpressions = Key.of( "disallowedExpressions" ); public static final Key doAll = Key.of( "doAll" ); public static final Key documentation = Key.of( "documentation" ); public static final Key dollarFormat = Key.of( "dollarFormat" ); diff --git a/src/main/resources/config/boxlang.json b/src/main/resources/config/boxlang.json index 91dffdb73..487848994 100644 --- a/src/main/resources/config/boxlang.json +++ b/src/main/resources/config/boxlang.json @@ -216,6 +216,7 @@ "security": { // All regex patterns are case-insensitive // A list of regex patterns that will match import statements, and if matched, execution will be disallowed + // This also applies to the `createObject` and `new` operators // Ex: "disallowedImports": ["java\\.lang\\.(ProcessBuilder|Reflect", "java\\.io\\.(File|FileWriter)"] "disallowedImports": [], // A list of BIF names that will be disallowed from execution @@ -223,11 +224,7 @@ "disallowedBifs": [], // A list of Component names that will be disallowed from execution // Ex: "disallowedComponents": [ "execute", "http" ] - "disallowedComponents": [], - // A list of regex patterns that will match expressions in any type of source, and if matched, execution will be disallowed - // Careful here, you can disallow a lot of things - // Ex: "disallowedExpressions": [ "(\s)Runtime\\.(.*)" ] - "disallowedExpressions": [] + "disallowedComponents": [] }, /** * The BoxLang module settings diff --git a/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java b/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java new file mode 100644 index 000000000..e5851fff9 --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/config/segments/SecurityConfigTest.java @@ -0,0 +1,83 @@ +/** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.runtime.config.segments; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class SecurityConfigTest { + + private SecurityConfig securityConfig; + + @BeforeEach + public void setUp() { + securityConfig = new SecurityConfig(); + } + + @Test + public void testIsBIFDisallowed_AllowedBIF() { + String bifName = "allowedBIF"; + assertTrue( securityConfig.isBIFAllowed( bifName ) ); + // Verify it's in the allowedBIFSLookup + assertTrue( securityConfig.allowedBIFsLookup.containsKey( bifName ) ); + } + + @Test + public void testisBIFAllowed_DisallowedBIF() { + String bifName = "disallowedBIF"; + securityConfig.disallowedBIFs.add( bifName ); + assertThrows( SecurityException.class, () -> securityConfig.isBIFAllowed( bifName ) ); + } + + @Test + public void testisComponentAllowed_AllowedComponent() { + String componentName = "allowedComponent"; + assertTrue( securityConfig.isComponentAllowed( componentName ) ); + // Verify it's in the allowedComponentsLookup + assertTrue( securityConfig.allowedComponentsLookup.containsKey( componentName ) ); + } + + @Test + public void testisComponentAllowed_DisallowedComponent() { + String componentName = "disallowedComponent"; + securityConfig.disallowedComponents.add( componentName ); + assertThrows( SecurityException.class, () -> securityConfig.isComponentAllowed( componentName ) ); + } + + @DisplayName( "Check if a class import is allowed" ) + @Test + public void testIsImportDisallowed_AllowedImport() { + String importName = "java.lang.Runnable"; + assertTrue( securityConfig.isClassAllowed( importName ) ); + // Verify it's in the allowedImportsLookup + assertTrue( securityConfig.allowedImportsLookup.containsKey( importName ) ); + } + + @DisplayName( "Check if a class import is disallowed" ) + @Test + public void testIsImportDisallowed_DisallowedImport() { + String importName = "java\\.lang\\.String"; + securityConfig.disallowedImports.add( importName ); + assertThrows( SecurityException.class, () -> securityConfig.isClassAllowed( "java.lang.String" ) ); + } + +} diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java index 0bdb54daa..03421d8f8 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java @@ -173,7 +173,7 @@ void testItCanResolveWildcardImports() throws Exception { @DisplayName( "It can load libs from the 'home/libs' convention" ) @Test void testItCanLoadLibsFromHomeLibs() throws IOException { - IBoxContext context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); + IBoxContext ctx = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); Path homeLibs = Path.of( "src/test/resources/libs" ).toAbsolutePath(); runtime.getRuntimeLoader().addURLs( @@ -184,7 +184,7 @@ void testItCanLoadLibsFromHomeLibs() throws IOException { JavaResolver javaResolver = JavaResolver.getInstance(); String targetClass = "com.github.benmanes.caffeine.cache.Caffeine"; - Optional location = javaResolver.resolve( context, targetClass ); + Optional location = javaResolver.resolve( ctx, targetClass ); assertThat( location.isPresent() ).isTrue(); assertThat( location.get().clazz().getName() ).isEqualTo( targetClass ); From d2afd8c8ae5ff13d8c0eaf3de732e070d2b17d44 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 10 Oct 2024 15:06:42 -0500 Subject: [PATCH 29/38] BL-631 --- .../boxlang/runtime/util/DuplicationUtil.java | 15 ++++++--------- .../java/TestCases/phase2/UDFFunctionTest.java | 18 ++++++++++++++++++ .../bifs/global/system/DuplicateTest.java | 13 +++++++++++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java b/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java index 928aaf5dd..f770ccf98 100644 --- a/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java +++ b/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java @@ -28,7 +28,6 @@ import ortus.boxlang.runtime.bifs.global.type.NullValue; import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; -import ortus.boxlang.runtime.dynamic.casters.QueryCaster; import ortus.boxlang.runtime.dynamic.casters.StructCaster; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Array; @@ -62,14 +61,12 @@ public static Object duplicate( Object target, Boolean deep ) { return target; } else if ( target instanceof Enum || target instanceof Class ) { return target; - } else if ( target instanceof IStruct && ( ( IStruct ) target ).isEmpty() ) { - return target; - } else if ( target instanceof IStruct ) { - return duplicateStruct( StructCaster.cast( target ), deep ); - } else if ( target instanceof Array ) { - return duplicateArray( ArrayCaster.cast( target ), deep ); - } else if ( target instanceof Query ) { - return duplicateQuery( QueryCaster.cast( target ), deep ); + } else if ( target instanceof IStruct str ) { + return duplicateStruct( str, deep ); + } else if ( target instanceof Array arr ) { + return duplicateArray( arr, deep ); + } else if ( target instanceof Query arr ) { + return duplicateQuery( arr, deep ); } else if ( target instanceof DateTime dateTimeInstance ) { return dateTimeInstance.clone(); } else if ( target instanceof Function ) { diff --git a/src/test/java/TestCases/phase2/UDFFunctionTest.java b/src/test/java/TestCases/phase2/UDFFunctionTest.java index 6cb3efe37..cb817c67b 100644 --- a/src/test/java/TestCases/phase2/UDFFunctionTest.java +++ b/src/test/java/TestCases/phase2/UDFFunctionTest.java @@ -954,4 +954,22 @@ function foo( arg=new src.test.java.TestCases.phase3.MyClass() ) { assertThat( variables.get( result ) ).isEqualTo( true ); } + @Test + public void testDefaultValueReference() { + instance.executeSource( + """ + function foo( headers={}, flag=false ) { + if( flag ) { + headers["brad"] = "wood"; + } + return headers; + } + foo(flag=true); + result = foo(); + """, + context ); + assertThat( variables.getAsStruct( result ) ).isEmpty(); + + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/DuplicateTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/system/DuplicateTest.java index 5f699d3ac..c1bd0f5d1 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/DuplicateTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/DuplicateTest.java @@ -79,6 +79,19 @@ public void setupEach() { @DisplayName( "It tests the BIF Duplicate can duplicate a struct" ) @Test public void testDuplicateStruct() { + + // @formatter:off + instance.executeSource( + """ + ref = {}; + result = duplicate( ref ); + result.foo = "bar"; + """, + context + ); + // @formatter:on + assertTrue( variables.getAsStruct( refKey ).isEmpty() ); + // @formatter:off instance.executeSource( """ From ffe0b04837b8f93962890f56ee2736083f820462 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 10 Oct 2024 17:37:05 +0200 Subject: [PATCH 30/38] docs --- src/main/resources/config/boxlang.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/config/boxlang.json b/src/main/resources/config/boxlang.json index 487848994..932598757 100644 --- a/src/main/resources/config/boxlang.json +++ b/src/main/resources/config/boxlang.json @@ -215,8 +215,8 @@ // These are the security settings for the runtime "security": { // All regex patterns are case-insensitive - // A list of regex patterns that will match import statements, and if matched, execution will be disallowed - // This also applies to the `createObject` and `new` operators + // A list of regex patterns that will match class paths, and if matched, execution will be disallowed + // This applies to import statements, createObject, new, and class creation // Ex: "disallowedImports": ["java\\.lang\\.(ProcessBuilder|Reflect", "java\\.io\\.(File|FileWriter)"] "disallowedImports": [], // A list of BIF names that will be disallowed from execution From 99659261fd77e0ca976690e23448d9f630c61fa9 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 10 Oct 2024 17:37:14 +0200 Subject: [PATCH 31/38] prep work for tests --- src/test/bx/models/TestClass.bx | 3 +++ src/test/bx/models/Validation.bx | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 src/test/bx/models/TestClass.bx create mode 100644 src/test/bx/models/Validation.bx diff --git a/src/test/bx/models/TestClass.bx b/src/test/bx/models/TestClass.bx new file mode 100644 index 000000000..faff6c421 --- /dev/null +++ b/src/test/bx/models/TestClass.bx @@ -0,0 +1,3 @@ +class{ + +} diff --git a/src/test/bx/models/Validation.bx b/src/test/bx/models/Validation.bx new file mode 100644 index 000000000..faff6c421 --- /dev/null +++ b/src/test/bx/models/Validation.bx @@ -0,0 +1,3 @@ +class{ + +} From 1fe47bdd43be8eee18bd935e5358aa996c51fb29 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 10 Oct 2024 21:59:04 +0200 Subject: [PATCH 32/38] the BIF only takes one arg not two --- .../ortus/boxlang/runtime/bifs/global/struct/StructGet.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructGet.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructGet.java index 67dad3ad0..803ded0f6 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructGet.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructGet.java @@ -29,7 +29,6 @@ @BoxBIF @BoxMember( type = BoxLangType.STRUCT, name = "getFromPath", objectArgument = "object" ) - public class StructGet extends BIF { /** @@ -38,8 +37,7 @@ public class StructGet extends BIF { public StructGet() { super(); declaredArguments = new Argument[] { - new Argument( true, "string", Key.path ), - new Argument( false, "structloose", Key.object ) + new Argument( true, "string", Key.path ) }; } From 90cfed783bfc91549b0f389e9972c9c7cfaddc48 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Thu, 10 Oct 2024 22:47:47 +0200 Subject: [PATCH 33/38] BL-630 #resolve Internal refactor to make the class locator and resolvers have a life-cycle based on the runtime and not alone --- .../ast/visitor/ClassMetadataVisitor.java | 215 +++++++++++++----- .../ortus/boxlang/runtime/BoxRuntime.java | 16 ++ .../application/BaseApplicationListener.java | 3 +- .../bifs/global/java/CreateDynamicProxy.java | 3 +- .../bifs/global/system/CreateObject.java | 3 +- .../bifs/global/system/GetClassMetadata.java | 3 +- .../runtime/bifs/global/system/Invoke.java | 3 +- .../runtime/components/system/Invoke.java | 13 +- .../dynamic/casters/GenericCaster.java | 4 +- .../javaproxy/InterfaceProxyService.java | 3 +- .../runtime/events/InterceptorPool.java | 3 +- .../interop/DynamicInteropService.java | 2 +- .../boxlang/runtime/loader/ClassLocator.java | 48 +++- .../loader/resolvers/BaseResolver.java | 46 +++- .../runtime/loader/resolvers/BoxResolver.java | 75 +++--- .../loader/resolvers/IClassResolver.java | 2 +- .../loader/resolvers/JavaResolver.java | 30 +-- .../runtime/runnables/BoxClassSupport.java | 3 +- .../runtime/runnables/BoxInterface.java | 4 +- .../ortus/boxlang/runtime/BoxRuntimeTest.java | 1 - .../bifs/global/struct/StructGetTest.java | 1 - .../javaproxy/InterfaceProxyServiceTest.java | 7 +- .../runtime/loader/ClassLocatorTest.java | 23 +- .../loader/resolvers/BaseResolverTest.java | 35 +-- .../loader/resolvers/BoxResolverTest.java | 13 +- .../loader/resolvers/JavaResolverTest.java | 30 +-- .../runtime/modules/ModuleRecordTest.java | 5 +- 27 files changed, 398 insertions(+), 196 deletions(-) diff --git a/src/main/java/ortus/boxlang/compiler/ast/visitor/ClassMetadataVisitor.java b/src/main/java/ortus/boxlang/compiler/ast/visitor/ClassMetadataVisitor.java index 6f6699b49..b929311c8 100644 --- a/src/main/java/ortus/boxlang/compiler/ast/visitor/ClassMetadataVisitor.java +++ b/src/main/java/ortus/boxlang/compiler/ast/visitor/ClassMetadataVisitor.java @@ -44,6 +44,7 @@ import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; +import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.loader.ClassLocator.ClassLocation; import ortus.boxlang.runtime.loader.resolvers.BoxResolver; import ortus.boxlang.runtime.scopes.Key; @@ -62,6 +63,9 @@ */ public class ClassMetadataVisitor extends VoidBoxVisitor { + /** + * Base metadata for the discovered class + */ private IStruct meta = Struct.of( Key._NAME, null, Key.nameAsKey, null, @@ -75,45 +79,62 @@ public class ClassMetadataVisitor extends VoidBoxVisitor { Key.path, null ); + /** + * Should we generate accessors for properties + */ private boolean accessors = false; + /** + * The path to the source file + */ private Path sourcePath; + /** + * The context of the runtime + */ private IBoxContext context; - private BoxResolver BXResolver = BoxResolver.getInstance(); + /** + * The Box Resolver so we can resolve super classes + */ + private BoxResolver boxResolver; /** * Constructor */ public ClassMetadataVisitor( IBoxContext context ) { - this.context = context; + this.context = context; + this.boxResolver = ClassLocator.getInstance().getBoxResolver(); } /** * Constructor */ public ClassMetadataVisitor() { - this.context = BoxRuntime.getInstance().getRuntimeContext(); + this( BoxRuntime.getInstance().getRuntimeContext() ); } /** * Get the metadata generated by this visitor - * + * * @return The metadata */ public IStruct getMetadata() { - return meta; + return this.meta; } + /** + * @inheritDoc + */ + @Override public void visit( BoxClass node ) { - meta.put( Key.type, "class" ); - meta.put( Key._NAME, "" ); - meta.put( Key.fullname, "" ); - meta.put( Key.nameAsKey, Key.of( "" ) ); - meta.put( Key.annotations, processAnnotations( node.getAnnotations() ) ); - meta.put( Key.documentation, processDocumentation( node.getDocumentation() ) ); - Object accessorsAnnotation = meta.getAsStruct( Key.annotations ).get( Key.accessors ); + this.meta.put( Key.type, "class" ); + this.meta.put( Key._NAME, "" ); + this.meta.put( Key.fullname, "" ); + this.meta.put( Key.nameAsKey, Key.of( "" ) ); + this.meta.put( Key.annotations, processAnnotations( node.getAnnotations() ) ); + this.meta.put( Key.documentation, processDocumentation( node.getDocumentation() ) ); + Object accessorsAnnotation = this.meta.getAsStruct( Key.annotations ).get( Key.accessors ); if ( accessorsAnnotation != null ) { var boolAccessors = BooleanCaster.attempt( accessorsAnnotation ); accessors = boolAccessors.wasSuccessful() && boolAccessors.get(); @@ -121,62 +142,31 @@ public void visit( BoxClass node ) { accessors = true; } processName( node ); - if ( meta.getAsStruct( Key.annotations ).containsKey( Key._EXTENDS ) ) { - meta.put( Key._EXTENDS, processSuper( meta.getAsStruct( Key.annotations ).getAsString( Key._EXTENDS ) ) ); + if ( this.meta.getAsStruct( Key.annotations ).containsKey( Key._EXTENDS ) ) { + this.meta.put( Key._EXTENDS, processSuper( this.meta.getAsStruct( Key.annotations ).getAsString( Key._EXTENDS ) ) ); } super.visit( node ); } - private IStruct processSuper( String superName ) { - if ( this.sourcePath != null ) { - context.pushTemplate( ResolvedFilePath.of( this.sourcePath ) ); - } - Optional superLookup = BXResolver.resolve( context, superName, false ); - if ( this.sourcePath != null ) { - context.popTemplate(); - } - if ( !superLookup.isPresent() ) { - throw new BoxRuntimeException( "Super class [" + superName + "] not found" ); - } - String superDiskPath = superLookup.get().path(); - - ParsingResult result = new Parser().parse( Paths.get( superDiskPath ).toAbsolutePath().toFile() ); - if ( !result.isCorrect() ) { - throw new ParseException( result.getIssues(), "" ); - } - ClassMetadataVisitor visitor = new ClassMetadataVisitor(); - result.getRoot().accept( visitor ); - - return visitor.getMetadata(); - } - - private void processName( BoxNode node ) { - if ( node.getPosition() != null && node.getPosition().getSource() != null && node.getPosition().getSource() instanceof SourceFile sf ) { - File sourceFile = sf.getFile(); - this.sourcePath = sourceFile.toPath(); - var contractedPath = FileSystemUtil.contractPath( context, sourceFile.toString() ); - String name = sourceFile.getName().replaceFirst( "[.][^.]+$", "" ); - String packageName = FQN.of( Paths.get( contractedPath.relativePath() ) ).getPackageString(); - String fullName = packageName.length() > 0 ? packageName + "." + name : name; - meta.put( Key._NAME, name ); - meta.put( Key.fullname, fullName ); - meta.put( Key.nameAsKey, Key.of( fullName ) ); - meta.put( Key.path, sourceFile.getAbsolutePath() ); - } - } - + /** + * @inheritDoc + */ + @Override public void visit( BoxInterface node ) { - meta.put( Key.type, "interface" ); - meta.put( Key._NAME, "" ); - meta.put( Key.fullname, "" ); - meta.put( Key.nameAsKey, Key.of( "" ) ); + this.meta.put( Key.type, "interface" ); + this.meta.put( Key._NAME, "" ); + this.meta.put( Key.fullname, "" ); + this.meta.put( Key.nameAsKey, Key.of( "" ) ); processName( node ); super.visit( node ); } + /** + * @inheritDoc + */ + @Override public void visit( BoxProperty prop ) { List finalAnnotations = BoxClassTransformer.normlizePropertyAnnotations( prop ); - BoxAnnotation nameAnnotation = finalAnnotations.stream().filter( it -> it.getKey().getValue().equalsIgnoreCase( "name" ) ).findFirst() .orElseThrow( () -> new ExpressionException( "Property [" + prop.getSourceText() + "] missing name annotation", prop ) ); BoxAnnotation typeAnnotation = finalAnnotations.stream().filter( it -> it.getKey().getValue().equalsIgnoreCase( "type" ) ).findFirst() @@ -186,7 +176,7 @@ public void visit( BoxProperty prop ) { String name = getBoxExprAsSimpleValue( nameAnnotation.getValue() ).toString(); String type = getBoxExprAsSimpleValue( typeAnnotation.getValue() ).toString(); - meta.getAsArray( Key.properties ).add( + this.meta.getAsArray( Key.properties ).add( Struct.of( Key._NAME, name, Key.nameAsKey, Key.of( name ), @@ -198,7 +188,7 @@ Key.documentation, processDocumentation( prop.getDocumentation() ) ); if ( accessors ) { - meta.getAsArray( Key.functions ).add( + this.meta.getAsArray( Key.functions ).add( Struct.of( Key._NAME, "get" + name, Key.nameAsKey, Key.of( "get" + name ), @@ -211,7 +201,7 @@ Key.documentation, processDocumentation( prop.getDocumentation() ) Key.lambda, false ) ); - meta.getAsArray( Key.functions ).add( + this.meta.getAsArray( Key.functions ).add( Struct.of( Key._NAME, "set" + name, Key.nameAsKey, Key.of( "set" + name ), @@ -228,6 +218,10 @@ Key.parameters, processArguments( List.of( new BoxArgumentDeclaration( true, typ } } + /** + * @inheritDoc + */ + @Override public void visit( BoxFunctionDeclaration func ) { BoxReturnType boxReturnType = func.getType(); BoxType returnType = BoxType.Any; @@ -238,7 +232,7 @@ public void visit( BoxFunctionDeclaration func ) { fqn = boxReturnType.getFqn(); } } - meta.getAsArray( Key.functions ).add( + this.meta.getAsArray( Key.functions ).add( Struct.of( Key._NAME, func.getName(), Key.nameAsKey, func.getName(), @@ -253,6 +247,69 @@ Key.parameters, processArguments( func.getArgs() ), ); } + /** + * -------------------------------------------------------------------------- + * Private Helpers + * -------------------------------------------------------------------------- + */ + + /** + * Process the super class details + * + * @param superName The name of the super class + * + * @return The metadata for the super class + */ + private IStruct processSuper( String superName ) { + if ( this.sourcePath != null ) { + context.pushTemplate( ResolvedFilePath.of( this.sourcePath ) ); + } + Optional superLookup = this.boxResolver.resolve( context, superName, false ); + if ( this.sourcePath != null ) { + context.popTemplate(); + } + if ( !superLookup.isPresent() ) { + throw new BoxRuntimeException( "Super class [" + superName + "] not found" ); + } + String superDiskPath = superLookup.get().path(); + + ParsingResult result = new Parser().parse( Paths.get( superDiskPath ).toAbsolutePath().toFile() ); + if ( !result.isCorrect() ) { + throw new ParseException( result.getIssues(), "" ); + } + ClassMetadataVisitor visitor = new ClassMetadataVisitor(); + result.getRoot().accept( visitor ); + + return visitor.getMetadata(); + } + + /** + * Process the name of the class + * + * @param node The node to process + */ + private void processName( BoxNode node ) { + if ( node.getPosition() != null && node.getPosition().getSource() != null && node.getPosition().getSource() instanceof SourceFile sf ) { + File sourceFile = sf.getFile(); + this.sourcePath = sourceFile.toPath(); + var contractedPath = FileSystemUtil.contractPath( context, sourceFile.toString() ); + String name = sourceFile.getName().replaceFirst( "[.][^.]+$", "" ); + String packageName = FQN.of( Paths.get( contractedPath.relativePath() ) ).getPackageString(); + String fullName = packageName.length() > 0 ? packageName + "." + name : name; + this.meta.put( Key._NAME, name ); + this.meta.put( Key.fullname, fullName ); + this.meta.put( Key.nameAsKey, Key.of( fullName ) ); + this.meta.put( Key.path, sourceFile.getAbsolutePath() ); + } + } + + /** + * Process the arguments + * + * @param arguments The arguments to process + * + * @return The processed arguments + */ private Array processArguments( List arguments ) { Array arr = Array.of(); arguments.forEach( argument -> { @@ -269,6 +326,13 @@ Key.annotations, processAnnotations( argument.getAnnotations() ) return arr; } + /** + * Process the annotations + * + * @param annotations The annotations to process + * + * @return The processed annotations + */ private IStruct processAnnotations( List annotations ) { IStruct struct = Struct.of(); annotations.forEach( annotation -> { @@ -279,6 +343,13 @@ private IStruct processAnnotations( List annotations ) { return struct; } + /** + * Process the documentation + * + * @param documentation The documentation to process + * + * @return The processed documentation + */ private IStruct processDocumentation( List documentation ) { IStruct struct = Struct.of(); documentation.forEach( annotation -> { @@ -289,10 +360,25 @@ private IStruct processDocumentation( List documenta return struct; } + /** + * Get the value of a BoxExpr as a simple value + * + * @param expr The expression to process + * + * @return The value + */ private Object getBoxExprAsSimpleValue( BoxExpression expr ) { return getBoxExprAsSimpleValue( expr, null, false ); } + /** + * Get the value of a BoxExpr as a simple value + * + * @param expr The expression to process + * @param defaultValue The default value to return if the expression is null + * + * @return The value + */ private Object getBoxExprAsSimpleValue( BoxExpression expr, Object defaultValue, boolean identifierAsText ) { if ( expr == null ) { return ""; @@ -313,6 +399,13 @@ private Object getBoxExprAsSimpleValue( BoxExpression expr, Object defaultValue, } } + /** + * Get the value of a BoxExpr as a literal value + * + * @param expr The expression to process + * + * @return The value + */ private Object getBoxExprAsLiteralValue( BoxExpression expr ) { if ( expr == null ) { return ""; diff --git a/src/main/java/ortus/boxlang/runtime/BoxRuntime.java b/src/main/java/ortus/boxlang/runtime/BoxRuntime.java index f35d16e5b..64242967c 100644 --- a/src/main/java/ortus/boxlang/runtime/BoxRuntime.java +++ b/src/main/java/ortus/boxlang/runtime/BoxRuntime.java @@ -59,6 +59,7 @@ import ortus.boxlang.runtime.interceptors.ASTCapture; import ortus.boxlang.runtime.interceptors.Logging; import ortus.boxlang.runtime.interop.DynamicObject; +import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.loader.DynamicClassLoader; import ortus.boxlang.runtime.logging.LoggingConfigurator; import ortus.boxlang.runtime.runnables.BoxScript; @@ -237,6 +238,11 @@ public class BoxRuntime implements java.io.Closeable { */ private DatasourceService dataSourceService; + /** + * The global class locator service + */ + private ClassLocator classLocator; + /** * -------------------------------------------------------------------------- * Public Fields @@ -434,6 +440,9 @@ private void startup() { this.schedulerService = new SchedulerService( this ); this.dataSourceService = new DatasourceService( this ); + // Initiate the Class Locator Service in charge of doing all the class resolutions + this.classLocator = ClassLocator.getInstance( this ); + // Load the configurations and overrides loadConfiguration( this.debugMode, this.configPath ); @@ -631,6 +640,13 @@ public static Boolean hasInstance() { * -------------------------------------------------------------------------- */ + /** + * Get the global class locator service + */ + public ClassLocator getClassLocator() { + return this.classLocator; + } + /** * Get the async service * diff --git a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java index b4ba3c10b..a4a43dfd7 100644 --- a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java +++ b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java @@ -634,7 +634,8 @@ protected void invokeClassRequest( case "wddx" : case "xml" : if ( context.getRuntime().getModuleService().hasModule( Key.wddx ) ) { - DynamicObject WDDXUtil = ClassLocator.getInstance() + DynamicObject WDDXUtil = context.getRuntime() + .getClassLocator() .load( context, "ortus.boxlang.modules.wddx.util.WDDXUtil@wddx", diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/java/CreateDynamicProxy.java b/src/main/java/ortus/boxlang/runtime/bifs/global/java/CreateDynamicProxy.java index 64630129f..f917e5d93 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/java/CreateDynamicProxy.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/java/CreateDynamicProxy.java @@ -14,6 +14,7 @@ */ package ortus.boxlang.runtime.bifs.global.java; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.bifs.BIF; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; @@ -33,7 +34,7 @@ @BoxBIF public class CreateDynamicProxy extends BIF { - ClassLocator classLocator = ClassLocator.getInstance(); + ClassLocator classLocator = BoxRuntime.getInstance().getClassLocator(); /** * Constructor diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/system/CreateObject.java b/src/main/java/ortus/boxlang/runtime/bifs/global/system/CreateObject.java index debab2e8e..73fe9776d 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/system/CreateObject.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/system/CreateObject.java @@ -17,6 +17,7 @@ */ package ortus.boxlang.runtime.bifs.global.system; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.bifs.BIF; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; @@ -37,7 +38,7 @@ public class CreateObject extends BIF { /** * How we find classes */ - private static final ClassLocator CLASS_LOCATOR = ClassLocator.getInstance(); + private static final ClassLocator CLASS_LOCATOR = BoxRuntime.getInstance().getClassLocator(); private static final String CLASS_TYPE = "class"; private static final String COMPONENT_TYPE = "component"; diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetClassMetadata.java b/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetClassMetadata.java index 17b17ff17..c0fa0e509 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetClassMetadata.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetClassMetadata.java @@ -14,6 +14,7 @@ */ package ortus.boxlang.runtime.bifs.global.system; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.bifs.BIF; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; @@ -29,7 +30,7 @@ @BoxBIF public class GetClassMetadata extends BIF { - private static final ClassLocator CLASS_LOCATOR = ClassLocator.getInstance(); + private static final ClassLocator CLASS_LOCATOR = BoxRuntime.getInstance().getClassLocator(); /** * Constructor diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/system/Invoke.java b/src/main/java/ortus/boxlang/runtime/bifs/global/system/Invoke.java index aa66d4b63..ba88f0445 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/system/Invoke.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/system/Invoke.java @@ -14,6 +14,7 @@ */ package ortus.boxlang.runtime.bifs.global.system; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.bifs.BIF; import ortus.boxlang.runtime.bifs.BoxBIF; import ortus.boxlang.runtime.context.IBoxContext; @@ -32,7 +33,7 @@ @BoxBIF public class Invoke extends BIF { - ClassLocator classLocator = ClassLocator.getInstance(); + ClassLocator classLocator = BoxRuntime.getInstance().getClassLocator(); /** * Constructor diff --git a/src/main/java/ortus/boxlang/runtime/components/system/Invoke.java b/src/main/java/ortus/boxlang/runtime/components/system/Invoke.java index db890dbab..6b45bf47f 100644 --- a/src/main/java/ortus/boxlang/runtime/components/system/Invoke.java +++ b/src/main/java/ortus/boxlang/runtime/components/system/Invoke.java @@ -19,6 +19,7 @@ import java.util.Set; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.components.Attribute; import ortus.boxlang.runtime.components.BoxComponent; import ortus.boxlang.runtime.components.Component; @@ -38,7 +39,7 @@ @BoxComponent( allowsBody = true ) public class Invoke extends Component { - ClassLocator classLocator = ClassLocator.getInstance(); + ClassLocator classLocator = BoxRuntime.getInstance().getClassLocator(); static final Set reservedAttributeNames = Set.of( Key._CLASS, Key.method, Key.returnVariable, Key.argumentCollection ); /** @@ -61,15 +62,15 @@ public Invoke() { * @param attributes The attributes to the Component * @param body The body of the Component * @param executionState The execution state of the Component - * + * * @attribute.class The Box Class instance or the name of the Box Class to instantiate. - * + * * @attribute.method The name of the method to invoke. - * + * * @attribute.returnVariable The variable to store the result of the method invocation. - * + * * @attribute.argumentCollection An array or struct of arguments to pass to the method. - * + * */ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBody body, IStruct executionState ) { String returnVariable = attributes.getAsString( Key.returnVariable ); diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java index 081de09a9..a6ed5726b 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java @@ -26,9 +26,9 @@ import java.util.stream.LongStream; import java.util.stream.Stream; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.interop.DynamicObject; -import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.operators.InstanceOf; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.types.BoxLangType; @@ -405,7 +405,7 @@ public static Class getClassFromType( IBoxContext context, String type, Strin // If we got here, then we have a full class name like java.lang.String // Let's see if we can load it - Optional loadResult = ClassLocator.getInstance().safeLoad( context, originalCaseType, "java" ); + Optional loadResult = BoxRuntime.getInstance().getClassLocator().safeLoad( context, originalCaseType, "java" ); if ( loadResult.isPresent() ) { return loadResult.get().getTargetClass(); } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/javaproxy/InterfaceProxyService.java b/src/main/java/ortus/boxlang/runtime/dynamic/javaproxy/InterfaceProxyService.java index 093fe7558..1fac44f4b 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/javaproxy/InterfaceProxyService.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/javaproxy/InterfaceProxyService.java @@ -29,6 +29,7 @@ import ortus.boxlang.compiler.IBoxpiler; import ortus.boxlang.compiler.javaboxpiler.JavaBoxpiler; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.interop.proxies.BaseProxy; @@ -46,7 +47,7 @@ public class InterfaceProxyService { /** * Our class locator */ - private static final ClassLocator classLocator = ClassLocator.getInstance(); + private static final ClassLocator classLocator = BoxRuntime.getInstance().getClassLocator(); /** * BoxPiler diff --git a/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java b/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java index 616d0a438..d4cd937c2 100644 --- a/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java +++ b/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java @@ -32,7 +32,6 @@ import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.loader.ClassLocator.ClassLocation; -import ortus.boxlang.runtime.loader.resolvers.BoxResolver; import ortus.boxlang.runtime.modules.ModuleRecord; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.Key; @@ -306,7 +305,7 @@ public IClassRunnable newAndRegister( ModuleRecord moduleRecord ) { // Get the class location IBoxContext context = this.runtime.getRuntimeContext(); - Optional classLocation = BoxResolver.getInstance().resolve( + Optional classLocation = this.runtime.getClassLocator().getBoxResolver().resolve( context, clazz ); diff --git a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java index 2c7ec2eae..8e72ac6c5 100644 --- a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java +++ b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java @@ -175,7 +175,7 @@ public class DynamicInteropService { /** * This is the class locator */ - private static ClassLocator classLocator = ClassLocator.getInstance(); + private static ClassLocator classLocator = BoxRuntime.getInstance().getClassLocator(); /** * Coercion maps diff --git a/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java b/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java index aaa95446a..f1aae2702 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java +++ b/src/main/java/ortus/boxlang/runtime/loader/ClassLocator.java @@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.interop.DynamicObject; import ortus.boxlang.runtime.loader.resolvers.BoxResolver; @@ -100,6 +101,11 @@ public class ClassLocator extends ClassLoader { BX_PREFIX, JAVA_PREFIX ); + /** + * The Runtime + */ + private BoxRuntime runtime; + /** * -------------------------------------------------------------------------- * Constructors @@ -120,20 +126,56 @@ private ClassLocator() { */ public static synchronized ClassLocator getInstance() { if ( instance == null ) { - instance = new ClassLocator(); + throw new BoxRuntimeException( "The ClassLocator instance has not been initialized." ); + } + return instance; + } + + /** + * Get the singleton instance + * + * @param runtime The current runtime + * + * @return ClassLocator + */ + public static synchronized ClassLocator getInstance( BoxRuntime runtime ) { + if ( instance == null ) { + instance = new ClassLocator(); + instance.runtime = runtime; // Register core box and java resolvers - instance.registerResolver( BoxResolver.getInstance() ); - instance.registerResolver( JavaResolver.getInstance() ); + instance.registerResolver( new BoxResolver( instance ) ); + instance.registerResolver( new JavaResolver( instance ) ); } return instance; } + /** + * Get the runtime associated with this locator + */ + public BoxRuntime getRuntime() { + return this.runtime; + } + /** * -------------------------------------------------------------------------- * Resolvers Registration * -------------------------------------------------------------------------- */ + /** + * Shortcut to get the Java Resolver + */ + public JavaResolver getJavaResolver() { + return ( JavaResolver ) getResolver( JAVA_PREFIX ); + } + + /** + * Shortcut to get the Box Resolver + */ + public BoxResolver getBoxResolver() { + return ( BoxResolver ) getResolver( BX_PREFIX ); + } + /** * Get the cache of resolved classes * diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java index eea5ef3ac..5297cac40 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BaseResolver.java @@ -26,6 +26,7 @@ import org.apache.commons.lang3.ClassUtils; import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.config.Configuration; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.loader.ClassLocator.ClassLocation; @@ -42,17 +43,27 @@ public class BaseResolver implements IClassResolver { /** * The name of a resolver */ - protected String name = ""; + protected String name = ""; /** * The prefix of a resolver */ - protected String prefix = ""; + protected String prefix = ""; + + /** + * The runtime that this resolver is associated with + */ + protected BoxRuntime runtime; + + /** + * The class locator that this resolver is associated with + */ + protected ClassLocator classLocator; /** * The import cache */ - private Set importCache = ConcurrentHashMap.newKeySet(); + private Set importCache = ConcurrentHashMap.newKeySet(); /** * -------------------------------------------------------------------------- @@ -66,9 +77,11 @@ public class BaseResolver implements IClassResolver { * @param name The name of the resolver * @param prefix The prefix of the resolver */ - protected BaseResolver( String name, String prefix ) { - this.name = name; - this.prefix = prefix.toLowerCase(); + protected BaseResolver( String name, String prefix, ClassLocator classLocator ) { + this.name = name; + this.prefix = prefix.toLowerCase(); + this.classLocator = classLocator; + this.runtime = classLocator.getRuntime(); } /** @@ -122,6 +135,27 @@ public void clearImportCache() { this.importCache.clear(); } + /** + * Get the runtime + */ + public BoxRuntime getRuntime() { + return this.runtime; + } + + /** + * Get the class locator + */ + public ClassLocator getClassLocator() { + return this.classLocator; + } + + /** + * Get the Runtime Configuration + */ + public Configuration getConfiguration() { + return getRuntime().getConfiguration(); + } + /** * -------------------------------------------------------------------------- * Resolvers diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BoxResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BoxResolver.java index c2cf5a1ef..4cacdc3de 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/BoxResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/BoxResolver.java @@ -23,9 +23,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import org.apache.commons.lang3.StringUtils; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.loader.ClassLocator.ClassLocation; @@ -37,26 +39,27 @@ import ortus.boxlang.runtime.util.ResolvedFilePath; /** - * This resolver deals with BoxLang classes only. + * This resolver is in charge of resolving and returning BoxLang classes. It will follow the + * rules of checking runtime mappings and the current template directory for the class requesting + * resolution. + *

+ * In order to access it you must go via the @{link ClassLocator} class, as the ClassLocator + * controls all the resolvers in the runtime. + *

+ * Example: + * + *

+ * ClassLocator.getJavaResolver();
+ * or
+ * ClassLocator.getResolver( ClassLocator.JAVA_PREFIX );
+ * 
*/ public class BoxResolver extends BaseResolver { - /** - * Singleton instance - */ - protected static BoxResolver instance; - - /** - * List of valid class extensions - */ - // TODO: Move .cfc extension into CF compat module and contribute it at startup. - // Need to add a setter or other similar mechanism to allow for dynamic extension - private static List VALID_EXTENSIONS = List.of( ".bx", ".cfc" ); - /** * Empty list of imports */ - private static final List EMPTY_IMPORTS = List.of(); + private static final List EMPTY_IMPORTS = List.of(); /** * -------------------------------------------------------------------------- @@ -65,23 +68,12 @@ public class BoxResolver extends BaseResolver { */ /** - * Private constructor - */ - private BoxResolver() { - super( "BoxResolver", "bx" ); - } - - /** - * Singleton instance + * Constructor * - * @return The instance + * @param classLocator The class locator to use */ - public static synchronized BoxResolver getInstance() { - if ( instance == null ) { - instance = new BoxResolver(); - } - - return instance; + public BoxResolver( ClassLocator classLocator ) { + super( "BoxResolver", "bx", classLocator ); } /** @@ -90,6 +82,14 @@ public static synchronized BoxResolver getInstance() { * -------------------------------------------------------------------------- */ + /** + * Get all the valid extensions we can process. + * This list does NOT include the dot. + */ + public Set getValidExtensions() { + return BoxRuntime.getInstance().getConfiguration().validClassExtensions; + } + /** * Each resolver has a way to resolve the class it represents. * This method will be called by the {@link ClassLocator} class @@ -266,8 +266,9 @@ private Optional findByMapping( .flatMap( entry -> { // Generate multiple paths here List paths = new ArrayList(); - for ( String extension : VALID_EXTENSIONS ) { - Path absolutePath = Path.of( StringUtils.replaceOnceIgnoreCase( slashName, entry.getKey().getName(), entry.getValue() + "/" ) + extension ) + for ( String extension : getValidExtensions() ) { + Path absolutePath = Path + .of( StringUtils.replaceOnceIgnoreCase( slashName, entry.getKey().getName(), entry.getValue() + "/" ) + "." + extension ) .normalize(); // Verify that the file exists absolutePath = FileSystemUtil.pathExistsCaseInsensitive( absolutePath ); @@ -372,9 +373,17 @@ private Optional findByRelativeLocation( return Optional.empty(); } + /** + * Find an existing path with a valid extension + * + * @param parentPath The parent path to search in + * @param slashName The name of the class to find using slahes instead of dots + * + * @return The path if found, null otherwise + */ private Path findExistingPathWithValidExtension( Path parentPath, String slashName ) { - for ( String extension : VALID_EXTENSIONS ) { - Path targetPath = parentPath.resolve( slashName.substring( 1 ) + extension ).normalize(); + for ( String extension : getValidExtensions() ) { + Path targetPath = parentPath.resolve( slashName.substring( 1 ) + "." + extension ).normalize(); Path result = FileSystemUtil.pathExistsCaseInsensitive( targetPath ); if ( result != null ) { diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/IClassResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/IClassResolver.java index 79ef09d76..e1e0491c5 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/IClassResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/IClassResolver.java @@ -22,8 +22,8 @@ import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.loader.ClassLocator; -import ortus.boxlang.runtime.loader.ImportDefinition; import ortus.boxlang.runtime.loader.ClassLocator.ClassLocation; +import ortus.boxlang.runtime.loader.ImportDefinition; /** * This interface is to implement ways to resolve classes in BoxLang. diff --git a/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java b/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java index dd61c5584..614e03e8e 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java +++ b/src/main/java/ortus/boxlang/runtime/loader/resolvers/JavaResolver.java @@ -40,6 +40,17 @@ /** * This resolver deals with Java classes only. * It has two import caches as well to deal with JDK classes and non-JDK classes. + *

+ * In order to access it you must go via the @{link ClassLocator} class, as the ClassLocator + * controls all the resolvers in the runtime. + *

+ * Example: + * + *

+ * ClassLocator.getJavaResolver();
+ * or
+ * ClassLocator.getResolver( ClassLocator.JAVA_PREFIX );
+ * 
*/ public class JavaResolver extends BaseResolver { @@ -65,23 +76,12 @@ public class JavaResolver extends BaseResolver { */ /** - * Private constructor - */ - private JavaResolver() { - super( "JavaResolver", "java" ); - } - - /** - * Singleton instance + * Constructor * - * @return The instance + * @param classLocator The class locator to use */ - public static synchronized JavaResolver getInstance() { - if ( instance == null ) { - instance = new JavaResolver(); - } - - return instance; + public JavaResolver( ClassLocator classLocator ) { + super( "JavaResolver", "java", classLocator ); } /** diff --git a/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java b/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java index a9ea84bbd..e862e9cd6 100644 --- a/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java +++ b/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java @@ -25,7 +25,6 @@ import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; import ortus.boxlang.runtime.interop.DynamicObject; -import ortus.boxlang.runtime.loader.ClassLocator; import ortus.boxlang.runtime.loader.ImportDefinition; import ortus.boxlang.runtime.scopes.BaseScope; import ortus.boxlang.runtime.scopes.Key; @@ -723,7 +722,7 @@ public static DynamicObject ensureClass( IBoxContext context, Object obj, List { locator.removeResolver( "java" ); } ); @@ -59,7 +68,6 @@ public void testItCannotRemoveACoreResolver() { @DisplayName( "It can load classes with resolver prefix part of name" ) @Test public void testCanLoadClassWithResolverInName() { - ClassLocator locator = ClassLocator.getInstance(); String targetClass = "java:java.lang.String"; DynamicObject target = locator.load( new ScriptingRequestBoxContext(), targetClass ); @@ -71,7 +79,6 @@ public void testCanLoadClassWithResolverInName() { @DisplayName( "It can load classes with system resolver lookup" ) @Test public void testCanLoadClassWithSystemResolver() { - ClassLocator locator = ClassLocator.getInstance(); String targetClass = "java.lang.String"; DynamicObject target = locator.load( new ScriptingRequestBoxContext(), targetClass ); @@ -83,7 +90,6 @@ public void testCanLoadClassWithSystemResolver() { @DisplayName( "It can find appropriate imports based on resolver type" ) @Test public void testCanFindAppropriateImports() { - ClassLocator locator = ClassLocator.getInstance(); String targetClass = "java:String"; List imports = List.of( ImportDefinition.parse( "java:java.lang.String as String" ) ); @@ -100,8 +106,7 @@ public void testCanFindAppropriateImports() { @DisplayName( "It can load native Java classes and add to the resolver cache" ) @Test public void testCanLoadJavaClassesWithCaching() { - ClassLocator locator = ClassLocator.getInstance(); - String targetClass = "java.lang.String"; + String targetClass = "java.lang.String"; locator.clear(); assertThat( locator.size() ).isEqualTo( 0 ); @@ -120,7 +125,6 @@ public void testCanLoadJavaClassesWithCaching() { @DisplayName( "It can safe load non-existent classes without throwing an exception" ) @Test public void testCanSafeLoad() throws ClassNotFoundException { - ClassLocator locator = ClassLocator.getInstance(); String targetClass = "java.lang.Bogus"; Optional target = locator.safeLoad( new ScriptingRequestBoxContext(), targetClass, "java" ); @@ -133,8 +137,7 @@ public void testCanSafeLoad() throws ClassNotFoundException { @DisplayName( "Resolver cache methods work" ) @Test public void testResolverCacheMethods() throws ClassNotFoundException { - ClassLocator locator = ClassLocator.getInstance(); - String targetClass = "java.lang.String"; + String targetClass = "java.lang.String"; locator.getResolverCache().clear(); assertThat( locator.isEmpty() ).isTrue(); diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/BaseResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/BaseResolverTest.java index 09366dc26..f1d690455 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/resolvers/BaseResolverTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/BaseResolverTest.java @@ -23,18 +23,27 @@ import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; import ortus.boxlang.runtime.loader.ImportDefinition; public class BaseResolverTest { + static BoxRuntime runtime; + + @BeforeAll + public static void setUp() { + runtime = BoxRuntime.getInstance( true ); + } + @DisplayName( "It can create a base resolver" ) @Test void testItCanCreateIt() { - BaseResolver target = new BaseResolver( "test", "TEST" ); + BaseResolver target = new BaseResolver( "test", "TEST", runtime.getClassLocator() ); assertThat( target ).isInstanceOf( BaseResolver.class ); assertThat( target.getName() ).isEqualTo( "test" ); assertThat( target.getPrefix() ).isEqualTo( "test" ); @@ -53,29 +62,29 @@ void testItCanResolveBasicImports() { ImportDefinition.parse( "bx:models.test.HelloWorld" ), ImportDefinition.parse( "java:java.lang.List as jList" ) ); - BaseResolver jResolver = JavaResolver.getInstance(); - jResolver.clearImportCache(); + BaseResolver resolver = runtime.getClassLocator().getJavaResolver(); + resolver.clearImportCache(); - String fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "String", imports ); + String fqn = resolver.expandFromImport( new ScriptingRequestBoxContext(), "String", imports ); assertThat( fqn ).isEqualTo( "java.lang.String" ); - assertThat( jResolver.getImportCacheSize() ).isEqualTo( 1 ); + assertThat( resolver.getImportCacheSize() ).isEqualTo( 1 ); - fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "Integer", imports ); + fqn = resolver.expandFromImport( new ScriptingRequestBoxContext(), "Integer", imports ); assertThat( fqn ).isEqualTo( "java.lang.Integer" ); - assertThat( jResolver.getImportCacheSize() ).isEqualTo( 2 ); + assertThat( resolver.getImportCacheSize() ).isEqualTo( 2 ); // The Java resolver will ignore this mapping - fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "HelloWorld", imports ); + fqn = resolver.expandFromImport( new ScriptingRequestBoxContext(), "HelloWorld", imports ); assertThat( fqn ).isEqualTo( "HelloWorld" ); - assertThat( jResolver.getImportCacheSize() ).isEqualTo( 2 ); + assertThat( resolver.getImportCacheSize() ).isEqualTo( 2 ); - fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "BaseResolver", imports ); + fqn = resolver.expandFromImport( new ScriptingRequestBoxContext(), "BaseResolver", imports ); assertThat( fqn ).isEqualTo( "ortus.boxlang.runtime.loader.resolvers.BaseResolver" ); - assertThat( jResolver.getImportCacheSize() ).isEqualTo( 3 ); + assertThat( resolver.getImportCacheSize() ).isEqualTo( 3 ); - fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "jList", imports ); + fqn = resolver.expandFromImport( new ScriptingRequestBoxContext(), "jList", imports ); assertThat( fqn ).isEqualTo( "java.lang.List" ); - assertThat( jResolver.getImportCacheSize() ).isEqualTo( 4 ); + assertThat( resolver.getImportCacheSize() ).isEqualTo( 4 ); } } diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java index 71b32193f..80f6bbe9f 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/BoxResolverTest.java @@ -45,11 +45,13 @@ public class BoxResolverTest { static BoxRuntime runtime; static IBoxContext context; + static BoxResolver boxResolver; @BeforeAll public static void setUp() { - runtime = BoxRuntime.getInstance( true ); - context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); + runtime = BoxRuntime.getInstance( true ); + context = new ScriptingRequestBoxContext( runtime.getRuntimeContext() ); + boxResolver = runtime.getClassLocator().getBoxResolver(); // Create a mapping to the `resources` directory Path resourcesDirectory = Paths.get( "src/test/resources" ).toAbsolutePath(); @@ -64,7 +66,6 @@ public static void teardown() { @DisplayName( "It can find be created" ) @Test void testItCanBeCreated() { - BoxResolver boxResolver = BoxResolver.getInstance(); assertThat( boxResolver.getName() ).isEqualTo( "BoxResolver" ); assertThat( boxResolver.getPrefix() ).isEqualTo( "bx" ); } @@ -73,8 +74,7 @@ void testItCanBeCreated() { @Test @Disabled void testFindFromModules() { - BoxResolver boxResolver = BoxResolver.getInstance(); - String className = "apppath.models.User"; // Example class name + String className = "apppath.models.User"; // Example class name assertThat( boxResolver.findFromModules( new ScriptingRequestBoxContext(), className, new ArrayList<>() ).isPresent() ).isFalse(); } @@ -83,7 +83,6 @@ void testFindFromModules() { void testFindFromLocal() throws URISyntaxException { // You can find this in src/test/resources/tests/components/User.cfc String testComponent = "tests.components.User"; - BoxResolver boxResolver = BoxResolver.getInstance(); // System.out.println( "mappings: " + Arrays.toString( runtime.getConfiguration().getRegisteredMappings() ) ); @@ -104,8 +103,6 @@ void testFindFromLocal() throws URISyntaxException { @DisplayName( "It can resolve classes" ) @Test void testResolve() { - BoxResolver boxResolver = BoxResolver.getInstance(); - IBoxContext context = new ScriptingRequestBoxContext(); String className = "apppath.models.User"; // Example class name diff --git a/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java b/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java index 03421d8f8..ea1edf84a 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/resolvers/JavaResolverTest.java @@ -48,11 +48,13 @@ public class JavaResolverTest { static BoxRuntime runtime; - IBoxContext context; + static JavaResolver javaResolver; + static IBoxContext context; @BeforeAll public static void setUp() { - runtime = BoxRuntime.getInstance( true ); + runtime = BoxRuntime.getInstance( true ); + javaResolver = runtime.getClassLocator().getJavaResolver(); } @BeforeEach @@ -68,7 +70,6 @@ public static void teardown() { @DisplayName( "It can find be created" ) @Test public void testItCanBeCreated() { - JavaResolver javaResolver = JavaResolver.getInstance(); assertThat( javaResolver.getName() ).isEqualTo( "JavaResolver" ); assertThat( javaResolver.getPrefix() ).isEqualTo( "java" ); } @@ -76,7 +77,6 @@ public void testItCanBeCreated() { @DisplayName( "It can find inner classes using the $ separator" ) @Test public void testFindInnerClasses() { - JavaResolver javaResolver = JavaResolver.getInstance(); String className = "java.util.Map$Entry"; // Example class name Optional classLocation = javaResolver.findFromSystem( className, new ArrayList<>(), context ); @@ -91,7 +91,6 @@ public void testFindInnerClasses() { @DisplayName( "It can find inner class enums using the $ separator" ) @Test public void testFindInnerClassEnums() { - JavaResolver javaResolver = JavaResolver.getInstance(); String className = "ortus.boxlang.runtime.types.IStruct$TYPES"; Optional classLocation = javaResolver.findFromSystem( className, new ArrayList<>(), context ); @@ -106,7 +105,6 @@ public void testFindInnerClassEnums() { @DisplayName( "It can find classes from the system" ) @Test public void testFindFromSystem() { - JavaResolver javaResolver = JavaResolver.getInstance(); String className = "java.util.logging.ConsoleHandler"; Optional classLocation = javaResolver.findFromSystem( className, new ArrayList<>(), context ); @@ -121,7 +119,6 @@ public void testFindFromSystem() { @DisplayName( "It can find classes from dependent libraries" ) @Test public void testFindFromDependentLibraries() { - JavaResolver javaResolver = JavaResolver.getInstance(); String className = "org.apache.commons.lang3.ClassUtils"; Optional classLocation = javaResolver.findFromSystem( className, new ArrayList<>(), context ); @@ -136,7 +133,6 @@ public void testFindFromDependentLibraries() { @DisplayName( "It can resolve classes" ) @Test public void testResolve() { - JavaResolver javaResolver = JavaResolver.getInstance(); String className = "org.apache.commons.lang3.ClassUtils"; Optional classLocation = javaResolver.findFromSystem( className, new ArrayList<>(), context ); @@ -151,22 +147,21 @@ public void testResolve() { @DisplayName( "It can resolve wildcard imports from the JDK itself" ) @Test void testItCanResolveWildcardImports() throws Exception { - List imports = Arrays.asList( + List imports = Arrays.asList( ImportDefinition.parse( "java:java.lang.*" ), ImportDefinition.parse( "java:java.util.*" ) ); - JavaResolver jResolver = JavaResolver.getInstance(); - jResolver.clearJdkImportCache(); - assertThat( jResolver.getJdkImportCacheSize() ).isEqualTo( 0 ); + javaResolver.clearJdkImportCache(); + assertThat( javaResolver.getJdkImportCacheSize() ).isEqualTo( 0 ); - String fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "String", imports ); + String fqn = javaResolver.expandFromImport( new ScriptingRequestBoxContext(), "String", imports ); assertThat( fqn ).isEqualTo( "java.lang.String" ); - fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "Integer", imports ); + fqn = javaResolver.expandFromImport( new ScriptingRequestBoxContext(), "Integer", imports ); assertThat( fqn ).isEqualTo( "java.lang.Integer" ); - fqn = jResolver.expandFromImport( new ScriptingRequestBoxContext(), "List", imports ); + fqn = javaResolver.expandFromImport( new ScriptingRequestBoxContext(), "List", imports ); assertThat( fqn ).isEqualTo( "java.util.List" ); } @@ -182,9 +177,8 @@ void testItCanLoadLibsFromHomeLibs() throws IOException { System.out.println( Arrays.toString( runtime.getRuntimeLoader().getURLs() ) ); - JavaResolver javaResolver = JavaResolver.getInstance(); - String targetClass = "com.github.benmanes.caffeine.cache.Caffeine"; - Optional location = javaResolver.resolve( ctx, targetClass ); + String targetClass = "com.github.benmanes.caffeine.cache.Caffeine"; + Optional location = javaResolver.resolve( ctx, targetClass ); assertThat( location.isPresent() ).isTrue(); assertThat( location.get().clazz().getName() ).isEqualTo( targetClass ); diff --git a/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java b/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java index 1291c13cc..255d3d0ce 100644 --- a/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java +++ b/src/test/java/ortus/boxlang/runtime/modules/ModuleRecordTest.java @@ -193,6 +193,7 @@ void testItCanActivateAModule() throws ClassNotFoundException { ModuleRecord moduleRecord = new ModuleRecord( physicalPath ); IBoxContext context = new ScriptingRequestBoxContext(); ModuleService moduleService = runtime.getModuleService(); + JavaResolver javaResolver = runtime.getClassLocator().getJavaResolver(); // When moduleRecord @@ -219,11 +220,11 @@ void testItCanActivateAModule() throws ClassNotFoundException { assertThat( clazz.getName() ).isEqualTo( "HelloWorld" ); // JavaResolver can find the class explicitly - Optional classLocation = JavaResolver.getInstance().findFromModules( "HelloWorld@test", List.of(), context ); + Optional classLocation = javaResolver.findFromModules( "HelloWorld@test", List.of(), context ); assertThat( classLocation.isPresent() ).isTrue(); assertThat( classLocation.get().clazz().getName() ).isEqualTo( "HelloWorld" ); // JavaResolver can find the class by discovery, it should interrogate all modules for it. - classLocation = JavaResolver.getInstance().findFromModules( "HelloWorld", List.of(), context ); + classLocation = javaResolver.findFromModules( "HelloWorld", List.of(), context ); assertThat( classLocation.isPresent() ).isTrue(); assertThat( classLocation.get().clazz().getName() ).isEqualTo( "HelloWorld" ); From 62862b2c6a048397a15d7d86ed243a972f59a917 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 10 Oct 2024 16:46:22 -0500 Subject: [PATCH 34/38] BL-633 --- .../ortus/boxlang/runtime/bifs/global/math/RandRange.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/math/RandRange.java b/src/main/java/ortus/boxlang/runtime/bifs/global/math/RandRange.java index 2bb4cf2b0..1bd1bcc85 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/math/RandRange.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/math/RandRange.java @@ -67,7 +67,10 @@ public Number _invoke( IBoxContext context, ArgumentsScope arguments ) { RoundingMode.DOWN ); } - return ( long ) ( number1.doubleValue() + Rand._invoke( seed ) * ( number2.doubleValue() - number1.doubleValue() ) ); + // For integer values, ensure the range is inclusive of the top number + long lower = number1.longValue(); + long upper = number2.longValue(); + return lower + ( long ) ( Rand._invoke( seed ) * ( upper - lower + 1 ) ); } } From 750195237397ba249291fc194d51b37dcee1e26d Mon Sep 17 00:00:00 2001 From: Michael Born Date: Fri, 11 Oct 2024 11:07:58 -0400 Subject: [PATCH 35/38] QueryColumnType - Add 'idstamp' support --- .../java/ortus/boxlang/runtime/types/QueryColumnType.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java b/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java index ab72daa6c..2b31fd882 100644 --- a/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java +++ b/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java @@ -75,6 +75,8 @@ public static QueryColumnType fromString( String type ) { return TIMESTAMP; case "object" : return OBJECT; + case "idstamp" : + return CHAR; case "other" : return OTHER; case "null" : @@ -97,6 +99,8 @@ public String toString() { return "numeric"; case DECIMAL : return "decimal"; + case CHAR : + return "string"; case VARCHAR : return "string"; case BINARY : From c73897da313c8e9ae2ebb53d4ef0b849c5331e56 Mon Sep 17 00:00:00 2001 From: Michael Born Date: Fri, 11 Oct 2024 11:10:14 -0400 Subject: [PATCH 36/38] QueryColumnType - Add rudimentary support for all known cf_sql types I'm not sure these are correct, but they can be adjusted from here. --- .../runtime/types/QueryColumnType.java | 61 +++++++++++----- .../runtime/types/QueryColumnTypeTest.java | 69 +++++++++++++++++++ 2 files changed, 113 insertions(+), 17 deletions(-) create mode 100644 src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java diff --git a/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java b/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java index 2b31fd882..2ba881c58 100644 --- a/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java +++ b/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java @@ -27,6 +27,7 @@ public enum QueryColumnType { BIGINT( Types.BIGINT ), BINARY( Types.BINARY ), BIT( Types.BIT ), + CHAR( Types.CHAR ), DATE( Types.DATE ), DECIMAL( Types.DECIMAL ), DOUBLE( Types.DOUBLE ), @@ -49,36 +50,62 @@ public enum QueryColumnType { */ public static QueryColumnType fromString( String type ) { type = type.toLowerCase(); - // TODO: handle other types + switch ( type ) { - case "integer" : - return INTEGER; + case "array": + case "refcursor": + case "struct": + case "sqlxml": + return OTHER; case "bigint" : return BIGINT; - case "double" : - case "numeric" : - return DOUBLE; - case "decimal" : - return DECIMAL; - case "varchar" : - case "string" : - return VARCHAR; case "binary" : + case "varbinary" : + case "longvarbinary" : return BINARY; + case "blob": + case "clob": + case "nclob": + return OBJECT; case "bit" : return BIT; - case "time" : - return TIME; + case "nchar" : + case "char": + return CHAR; case "date" : return DATE; - case "timestamp" : - return TIMESTAMP; - case "object" : - return OBJECT; + case "distinct" : + return OTHER; + case "decimal" : + return DECIMAL; + case "real" : + case "money" : + case "money4" : + case "float" : + case "double" : + case "numeric" : + return DOUBLE; case "idstamp" : return CHAR; + case "tinyint" : + case "smallint" : + case "integer" : + return INTEGER; + case "nvarchar" : + case "longvarchar" : + case "longnvarchar" : + return VARCHAR; + case "object" : + return OBJECT; case "other" : return OTHER; + case "time" : + return TIME; + case "timestamp" : + return TIMESTAMP; + case "varchar" : + case "string" : + return VARCHAR; case "null" : return NULL; default : diff --git a/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java b/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java new file mode 100644 index 000000000..fbb5dedc8 --- /dev/null +++ b/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java @@ -0,0 +1,69 @@ + + /** + * [BoxLang] + * + * Copyright [2023] [Ortus Solutions, Corp] + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ortus.boxlang.runtime.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class QueryColumnTypeTest {; + + + @DisplayName( "Test fromString() constructor against all known cf_sql parameter types" ) + @Test + void testFromStringConstructor() { + assertEquals( QueryColumnType.OTHER, QueryColumnType.fromString( "ARRAY" ) ); + assertEquals( QueryColumnType.BIGINT, QueryColumnType.fromString( "BIGINT" ) ); + assertEquals( QueryColumnType.BINARY, QueryColumnType.fromString( "BINARY" ) ); + assertEquals( QueryColumnType.BIT, QueryColumnType.fromString( "BIT" ) ); + assertEquals( QueryColumnType.OBJECT, QueryColumnType.fromString( "BLOB" ) ); + assertEquals( QueryColumnType.CHAR, QueryColumnType.fromString( "CHAR" ) ); + assertEquals( QueryColumnType.OBJECT, QueryColumnType.fromString( "CLOB" ) ); + assertEquals( QueryColumnType.DATE, QueryColumnType.fromString( "DATE" ) ); + assertEquals( QueryColumnType.DECIMAL, QueryColumnType.fromString( "DECIMAL" ) ); + assertEquals( QueryColumnType.OTHER, QueryColumnType.fromString( "DISTINCT" ) ); + assertEquals( QueryColumnType.DOUBLE, QueryColumnType.fromString( "DOUBLE" ) ); + assertEquals( QueryColumnType.DOUBLE, QueryColumnType.fromString( "FLOAT" ) ); + assertEquals( QueryColumnType.CHAR, QueryColumnType.fromString( "IDSTAMP" ) ); + assertEquals( QueryColumnType.INTEGER, QueryColumnType.fromString( "INTEGER" ) ); + assertEquals( QueryColumnType.BINARY, QueryColumnType.fromString( "LONGVARBINARY" ) ); + assertEquals( QueryColumnType.VARCHAR, QueryColumnType.fromString( "LONGNVARCHAR" ) ); + assertEquals( QueryColumnType.VARCHAR, QueryColumnType.fromString( "LONGVARCHAR" ) ); + assertEquals( QueryColumnType.DOUBLE, QueryColumnType.fromString( "MONEY" ) ); + assertEquals( QueryColumnType.DOUBLE, QueryColumnType.fromString( "MONEY4" ) ); + assertEquals( QueryColumnType.CHAR, QueryColumnType.fromString( "NCHAR" ) ); + assertEquals( QueryColumnType.OBJECT, QueryColumnType.fromString( "NCLOB" ) ); + assertEquals( QueryColumnType.NULL, QueryColumnType.fromString( "NULL" ) ); + assertEquals( QueryColumnType.DOUBLE, QueryColumnType.fromString( "NUMERIC" ) ); + assertEquals( QueryColumnType.VARCHAR, QueryColumnType.fromString( "NVARCHAR" ) ); + assertEquals( QueryColumnType.OTHER, QueryColumnType.fromString( "OTHER" ) ); + assertEquals( QueryColumnType.DOUBLE, QueryColumnType.fromString( "REAL" ) ); + assertEquals( QueryColumnType.OTHER, QueryColumnType.fromString( "REFCURSOR" ) ); + assertEquals( QueryColumnType.INTEGER, QueryColumnType.fromString( "SMALLINT" ) ); + assertEquals( QueryColumnType.OTHER, QueryColumnType.fromString( "STRUCT" ) ); + assertEquals( QueryColumnType.OTHER, QueryColumnType.fromString( "SQLXML" ) ); + assertEquals( QueryColumnType.TIME, QueryColumnType.fromString( "TIME" ) ); + assertEquals( QueryColumnType.TIMESTAMP, QueryColumnType.fromString( "TIMESTAMP" ) ); + assertEquals( QueryColumnType.INTEGER, QueryColumnType.fromString( "TINYINT" ) ); + assertEquals( QueryColumnType.BINARY, QueryColumnType.fromString( "VARBINARY" ) ); + assertEquals( QueryColumnType.VARCHAR, QueryColumnType.fromString( "VARCHAR" ) ); + } + +} From 6baa6e4a9c30d34f8f5783bce858426b6adcb8c9 Mon Sep 17 00:00:00 2001 From: michaelborn Date: Fri, 11 Oct 2024 15:11:12 +0000 Subject: [PATCH 37/38] Apply cfformat changes --- .../boxlang/runtime/types/QueryColumnType.java | 18 +++++++++--------- .../runtime/types/QueryColumnTypeTest.java | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java b/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java index 2ba881c58..d011e9ef6 100644 --- a/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java +++ b/src/main/java/ortus/boxlang/runtime/types/QueryColumnType.java @@ -50,12 +50,12 @@ public enum QueryColumnType { */ public static QueryColumnType fromString( String type ) { type = type.toLowerCase(); - + switch ( type ) { - case "array": - case "refcursor": - case "struct": - case "sqlxml": + case "array" : + case "refcursor" : + case "struct" : + case "sqlxml" : return OTHER; case "bigint" : return BIGINT; @@ -63,14 +63,14 @@ public static QueryColumnType fromString( String type ) { case "varbinary" : case "longvarbinary" : return BINARY; - case "blob": - case "clob": - case "nclob": + case "blob" : + case "clob" : + case "nclob" : return OBJECT; case "bit" : return BIT; case "nchar" : - case "char": + case "char" : return CHAR; case "date" : return DATE; diff --git a/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java b/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java index fbb5dedc8..f6acf6e21 100644 --- a/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java +++ b/src/test/java/ortus/boxlang/runtime/types/QueryColumnTypeTest.java @@ -1,5 +1,5 @@ - /** +/** * [BoxLang] * * Copyright [2023] [Ortus Solutions, Corp] @@ -23,8 +23,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -public class QueryColumnTypeTest {; - +public class QueryColumnTypeTest { + ; @DisplayName( "Test fromString() constructor against all known cf_sql parameter types" ) @Test From 2ba2eb2aec816b6f66afd908ad5ce8149fb5eace Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Fri, 11 Oct 2024 17:14:29 +0200 Subject: [PATCH 38/38] BL-634 #resolve array.find - can't cast closure to string --- .../boxlang/runtime/dynamic/Attempt.java | 55 +++++++++++++------ .../runtime/dynamic/casters/CastAttempt.java | 50 +++++++++++++++++ .../bifs/global/array/ArrayFindTest.java | 21 ++++--- 3 files changed, 102 insertions(+), 24 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java b/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java index 557e0c35a..b262ef6e7 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/Attempt.java @@ -52,7 +52,7 @@ public class Attempt { * |-------------------------------------------------------------------------- */ - protected static final Attempt EMPTY = new Attempt<>(); + protected static final Attempt EMPTY = new Attempt<>(); /** * The target value to evaluate @@ -65,6 +65,12 @@ public class Attempt { */ protected ValidationRecord validationRecord; + /** + * Simple eval for the attempt + * No truthy or validation checks + */ + protected Boolean simpleEval = false; + /** * |-------------------------------------------------------------------------- * | Constructors @@ -435,6 +441,11 @@ public boolean isPresent() { return false; } + // If we have a simple eval, we are done + if ( this.simpleEval ) { + return true; + } + // Verify truthy/falsey values var castAttempt = BooleanCaster.attempt( this.value ); if ( castAttempt.wasSuccessful() ) { @@ -547,7 +558,7 @@ public T getOrDefault( T other ) { */ public T orElseGet( Supplier supplier ) { Objects.requireNonNull( supplier ); - if ( this.isEmpty() ) { + if ( isEmpty() ) { return supplier.get(); } return this.value; @@ -573,11 +584,11 @@ public T getOrSupply( Supplier other ) { */ public Attempt map( java.util.function.Function mapper ) { Objects.requireNonNull( mapper ); - if ( this.isEmpty() ) { - return new Attempt<>(); + if ( isEmpty() ) { + return empty(); } - return new Attempt<>( mapper.apply( this.value ) ); + return of( mapper.apply( this.value ) ); } /** @@ -602,8 +613,8 @@ public Attempt map( java.util.function.Function m @SuppressWarnings( "unchecked" ) public Attempt flatMap( java.util.function.Function> mapper ) { Objects.requireNonNull( mapper ); - if ( this.isEmpty() ) { - return new Attempt<>(); + if ( isEmpty() ) { + return empty(); } Attempt r = ( Attempt ) mapper.apply( this.value ); return Objects.requireNonNull( r ); @@ -631,7 +642,7 @@ public T orThrow() { * @return The value of the attempt if present or throws an exception */ public T orThrow( String type, String message ) { - if ( this.isEmpty() ) { + if ( isEmpty() ) { orThrow( new CustomException( message, "", @@ -656,7 +667,7 @@ public T orThrow( String type, String message ) { * @return The value of the attempt if present */ public T orThrow( String message ) { - if ( this.isEmpty() ) { + if ( isEmpty() ) { throw new NoElementException( message ); } return this.value; @@ -673,7 +684,7 @@ public T orThrow( String message ) { * @return The value of the attempt if present */ public T orThrow( Exception throwable ) { - if ( this.isEmpty() ) { + if ( isEmpty() ) { try { throw throwable; } catch ( Exception e ) { @@ -719,12 +730,12 @@ public boolean equals( Object obj ) { if ( obj instanceof Attempt castedAttempt ) { // If both are empty, they are equal - if ( this.isEmpty() && castedAttempt.isEmpty() ) { + if ( isEmpty() && castedAttempt.isEmpty() ) { return true; } // If one is empty and the other is not, they are not equal - if ( this.isEmpty() || castedAttempt.isEmpty() ) { + if ( isEmpty() || castedAttempt.isEmpty() ) { return false; } @@ -733,12 +744,12 @@ public boolean equals( Object obj ) { } // If we are empty and the incoming object is null, they are equal - if ( this.isEmpty() && obj == null ) { + if ( isEmpty() && obj == null ) { return true; } // If we are not empty and the incoming object is null, they are not equal - if ( this.isEmpty() && obj != null ) { + if ( isEmpty() && obj != null ) { return false; } @@ -755,15 +766,25 @@ public boolean equals( Object obj ) { * @return The attempt if the predicate is true, else an empty attempt */ public Attempt filter( Predicate predicate ) { - if ( this.isEmpty() ) { - return new Attempt<>(); + if ( isEmpty() ) { + return empty(); } if ( predicate.test( this.value ) ) { return this; } - return new Attempt<>(); + return empty(); + } + + /** + * Set the attempt to a simple evaluation + * + * @param eval True if the attempt is a simple evaluation, false otherwise + */ + public Attempt setSimpleEval( Boolean eval ) { + this.simpleEval = eval; + return this; } } diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java b/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java index 494327ccb..a79915e38 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/CastAttempt.java @@ -20,6 +20,7 @@ import java.util.Objects; import ortus.boxlang.runtime.dynamic.Attempt; +import ortus.boxlang.runtime.types.Function; import ortus.boxlang.runtime.types.exceptions.BoxCastException; /** @@ -44,6 +45,7 @@ public final class CastAttempt extends Attempt { */ private CastAttempt() { super(); + this.simpleEval = true; } /** @@ -53,6 +55,7 @@ private CastAttempt() { */ private CastAttempt( T value ) { super( Objects.requireNonNull( value ) ); + this.simpleEval = true; } /** @@ -129,4 +132,51 @@ public boolean isPresent() { return this.value != null; } + /** + * Map the attempt to a new value with a supplier + * + * @param mapper The mapper to map the attempt to + * + * @return The new attempt + */ + @Override + public CastAttempt map( java.util.function.Function mapper ) { + Objects.requireNonNull( mapper ); + if ( isEmpty() ) { + return empty(); + } + + return of( mapper.apply( this.value ) ); + } + + /** + * If a value is present, returns the result of applying the given + * {@code Attempt}-bearing mapping function to the value, otherwise returns + * an empty {@code Attempt}. + * + *

+ * This method is similar to {@link #map(Function)}, but the mapping + * function is one whose result is already an {@code Attempt}, and if + * invoked, {@code flatMap} does not wrap it within an additional + * {@code Attempt}. + * + * @param The type of value of the {@code Attempt} returned by the + * mapping function + * @param mapper the mapping function to apply to a value, if present + * + * @return the result of applying an {@code Attempt}-bearing mapping + * function to the value of this {@code Attempt}, if a value is + * present, otherwise an empty {@code Attempt} + */ + @SuppressWarnings( "unchecked" ) + @Override + public CastAttempt flatMap( java.util.function.Function> mapper ) { + Objects.requireNonNull( mapper ); + if ( isEmpty() ) { + return empty(); + } + CastAttempt r = ( CastAttempt ) mapper.apply( this.value ); + return Objects.requireNonNull( r ); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java index 7036b8abf..797955725 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/array/ArrayFindTest.java @@ -22,7 +22,6 @@ import java.util.function.Predicate; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -47,11 +46,6 @@ public static void setUp() { instance = BoxRuntime.getInstance( true ); } - @AfterAll - public static void teardown() { - - } - @BeforeEach public void setupEach() { context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); @@ -175,7 +169,7 @@ public void testMatchClosure() { @Test // @Disabled( "See comments on https://ortussolutions.atlassian.net/browse/BL-617" ) public void testJavaFunctionalInterface() { - Predicate javaPredicate = ( s ) -> s.equals( "b" ); + Predicate javaPredicate = s -> s.equals( "b" ); variables.put( "javaPredicate", javaPredicate ); // @formatter:off instance.executeSource( @@ -189,4 +183,17 @@ public void testJavaFunctionalInterface() { int found = ( int ) variables.get( result ); assertThat( found ).isEqualTo( 2 ); } + + @DisplayName( "It can't cast closure to string" ) + @Test + public void testClosureToString() { + // @formatter:off + instance.executeSource( + """ + result = ["x"].find(() => false) + """, + context ); + // @formatter:on + assertThat( variables.get( result ) ).isEqualTo( 0 ); + } }