From 58c594d1aae89eb09dbf6c9c16771d798617bc4c Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 25 Oct 2024 14:06:22 +0000 Subject: [PATCH 01/33] Version bump --- changelog.md | 6 +++++- gradle.properties | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/changelog.md b/changelog.md index 7152c51e7..42a6fb29f 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-beta20] - 2024-10-25 + ## [1.0.0-beta19] - 2024-10-18 ## [1.0.0-beta18] - 2024-10-11 @@ -51,7 +53,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-beta19...HEAD +[Unreleased]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta20...HEAD + +[1.0.0-beta20]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta19...v1.0.0-beta20 [1.0.0-beta19]: https://github.com/ortus-boxlang/BoxLang/compare/v1.0.0-beta18...v1.0.0-beta19 diff --git a/gradle.properties b/gradle.properties index c699e2294..2a7b673f4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -#Fri Oct 18 14:34:03 UTC 2024 +#Fri Oct 25 14:06:18 UTC 2024 antlrVersion=4.13.1 jdkVersion=21 -version=1.0.0-beta20 +version=1.0.0-beta21 From 8faff9626638111730b7b18b9f4b411b5a100480 Mon Sep 17 00:00:00 2001 From: jclausen Date: Fri, 25 Oct 2024 15:33:56 -0400 Subject: [PATCH 02/33] BL-710 resolve - implements algorithms for random methods --- .../runtime/bifs/global/math/Rand.java | 28 +++++------------- .../runtime/bifs/global/math/RandRange.java | 9 ++---- .../boxlang/runtime/util/EncryptionUtil.java | 29 +++++++++++++++++++ .../bifs/global/math/RandRangeTest.java | 13 +++++++++ 4 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/math/Rand.java b/src/main/java/ortus/boxlang/runtime/bifs/global/math/Rand.java index b82503e74..bda0307ae 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/math/Rand.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/math/Rand.java @@ -22,7 +22,7 @@ import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; -import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.util.EncryptionUtil; @BoxBIF public class Rand extends BIF { @@ -54,23 +54,7 @@ public Rand() { public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { String algorithm = arguments.getAsString( Key.algorithm ); Long seed = context.getAttachment( Key.bxRandomSeed ); - - return algorithm != null ? _invoke( algorithm, seed ) : _invoke( seed ); - } - - /** - * Return a random double between 0 and 1 - * - * @param seed The seed to use for the random number generator - * - * @return A random double between 0 and 1 - */ - public static double _invoke( Long seed ) { - if ( seed != null && visitedSeed != seed ) { - randomGenerator.setSeed( seed ); - visitedSeed = seed; - } - return randomGenerator.nextDouble(); + return _invoke( algorithm, seed ); } /** @@ -81,8 +65,12 @@ public static double _invoke( Long seed ) { * * @return A random double between 0 and 1 */ - public double _invoke( String algorithm, Long seed ) { - throw new BoxRuntimeException( "The algorithm argument has not yet been implemented" ); + public static double _invoke( String algorithm, Long seed ) { + Random randomInstance = EncryptionUtil.getRandom( algorithm ); + if ( seed != null ) { + randomInstance.setSeed( seed ); + } + return randomInstance.nextDouble(); } } 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 1bd1bcc85..36cf8813f 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 @@ -24,7 +24,6 @@ import ortus.boxlang.runtime.scopes.ArgumentsScope; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Argument; -import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.types.util.MathUtil; @BoxBIF @@ -49,13 +48,11 @@ public RandRange() { * @param arguments Argument scope for the BIF. */ public Number _invoke( IBoxContext context, ArgumentsScope arguments ) { - if ( arguments.get( Key.algorithm ) != null ) { - throw new BoxRuntimeException( "The algorithm argument has not yet been implemented" ); - } Number number1 = arguments.getAsNumber( Key.number1 ); Number number2 = arguments.getAsNumber( Key.number2 ); Long seed = context.getAttachment( Key.bxRandomSeed ); + String algorithm = arguments.getAsString( Key.algorithm ); boolean isNumber1 = number1 instanceof BigDecimal; boolean isNumber2 = number2 instanceof BigDecimal; @@ -63,14 +60,14 @@ public Number _invoke( IBoxContext context, ArgumentsScope arguments ) { if ( isNumber1 || isNumber2 ) { BigDecimal bdl = isNumber1 ? ( BigDecimal ) number1 : BigDecimalCaster.cast( number1 ); BigDecimal bdr = isNumber2 ? ( BigDecimal ) number2 : BigDecimalCaster.cast( number2 ); - return bdl.add( new BigDecimal( Rand._invoke( seed ), MathUtil.getMathContext() ).multiply( bdr.subtract( bdl ) ) ).setScale( 0, + return bdl.add( new BigDecimal( Rand._invoke( algorithm, seed ), MathUtil.getMathContext() ).multiply( bdr.subtract( bdl ) ) ).setScale( 0, RoundingMode.DOWN ); } // 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 ) ); + return lower + ( long ) ( Rand._invoke( algorithm, seed ) ) * ( upper - lower + 1 ); } } diff --git a/src/main/java/ortus/boxlang/runtime/util/EncryptionUtil.java b/src/main/java/ortus/boxlang/runtime/util/EncryptionUtil.java index 2fca046d8..f2a7cb757 100644 --- a/src/main/java/ortus/boxlang/runtime/util/EncryptionUtil.java +++ b/src/main/java/ortus/boxlang/runtime/util/EncryptionUtil.java @@ -34,7 +34,9 @@ import java.security.spec.AlgorithmParameterSpec; import java.security.spec.InvalidKeySpecException; import java.util.Base64; +import java.util.HashMap; import java.util.HexFormat; +import java.util.Random; import java.util.stream.IntStream; import javax.crypto.BadPaddingException; @@ -101,6 +103,11 @@ public final class EncryptionUtil { */ public final static int DEFAULT_ENCRYPTION_ITERATIONS = 1000; + /** + * Threadsafe instances of Random and Secure random instances which are used by the getRandom method + */ + private static HashMap randomStore = new HashMap(); + /** * Supported key algorithms * key factory algorithms @@ -713,6 +720,28 @@ public static byte[] convertToByteArray( Object obj ) { } } + /** + * Retrieves an instance of the specified random generator. If an agorithm is provided the method will return a SecureRandom instance + * + * @param algorithm + * + * @return + */ + public static Random getRandom( String algorithm ) { + Key algorithmKey = algorithm == null ? Key._DEFAULT : Key.of( algorithm ); + + Random random = randomStore.get( algorithmKey ); + if ( random == null ) { + try { + random = algorithmKey.equals( Key._DEFAULT ) ? new Random() : SecureRandom.getInstance( algorithm ); + } catch ( NoSuchAlgorithmException e ) { + throw new BoxRuntimeException( "The algorithm: " + algorithm + " is not implemented in the current Java runtime", e ); + } + randomStore.put( algorithmKey, random ); + } + return random; + } + /** * Returns the algorithm parameters for the specified algorithm * diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/math/RandRangeTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/math/RandRangeTest.java index f777958b3..5760d5d2f 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/math/RandRangeTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/math/RandRangeTest.java @@ -96,4 +96,17 @@ public void testItReturnsARandomNumberInRange() { """, context ); } + @DisplayName( "It returns a random number in range using an algorithm" ) + @Test + public void testItReturnsARandomNumberInRangeWithAlgorithm() { + instance.executeSource( + """ + loop times=1000 { + result = randRange( 0, 12, "SHA1PRNG" ); + assert result >= 0; + assert result <= 12; + } + """, context ); + } + } From 3663da4e3af53ff57ecea70260bbe1ed85d66c25 Mon Sep 17 00:00:00 2001 From: jclausen Date: Fri, 25 Oct 2024 15:39:43 -0400 Subject: [PATCH 03/33] consolidate test that fails intermittently due to concurrency issues --- .../runtime/config/ConfigLoaderTest.java | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java b/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java index 4642673b7..2a2b5531d 100644 --- a/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java +++ b/src/test/java/ortus/boxlang/runtime/config/ConfigLoaderTest.java @@ -215,7 +215,7 @@ private void assertConfigTest( Configuration config ) { @DisplayName( "It can merge environmental properties in to the config" ) @Test void testItCanMergeEnvironmentalProperties() { - System.setProperty( "boxlang.security.allowedFileOperationExtensions", ".exe" ); + System.setProperty( "BOXLANG_SECURITY_ALLOWEDFILEOPERATIONEXTENSIONS", ".exe" ); System.setProperty( "boxlang.experimental.compiler", "asm" ); Configuration config = ConfigLoader.getInstance().loadCore(); // Core config checks @@ -263,52 +263,4 @@ void testItCanMergeEnvironmentalProperties() { assertThat( config.experimental.getAsString( Key.of( "compiler" ) ) ).isEqualTo( "asm" ); } - @DisplayName( "It can merge environmental properties in to the config using the alternate syntax" ) - @Test - void testItCanMergeEnvironmentalPropertiesAlt() { - System.setProperty( "BOXLANG_SECURITY_ALLOWEDFILEOPERATIONEXTENSIONS", ".exe" ); - Configuration config = ConfigLoader.getInstance().loadCore(); - // Core config checks - // Compiler Checks - assertThat( config.classGenerationDirectory ).doesNotContainMatch( "(ignorecase)\\{java-temp\\}" ); - - // Runtime Checks - assertThat( config.mappings ).isNotEmpty(); - assertThat( config.modulesDirectory.size() ).isGreaterThan( 0 ); - // First one should be the user home directory - assertThat( config.modulesDirectory.get( 0 ) ).doesNotContainMatch( "(ignorecase)\\{boxlang-home\\}" ); - - // Log Directory Check - assertThat( config.logsDirectory ).isNotEmpty(); - - // Cache Checks - assertThat( config.caches ).isNotEmpty(); - - // Default Cache Checks - CacheConfig defaultCache = ( CacheConfig ) config.defaultCache; - assertThat( defaultCache ).isNotNull(); - assertThat( defaultCache.name.getNameNoCase() ).isEqualTo( "DEFAULT" ); - assertThat( defaultCache.provider.getNameNoCase() ).isEqualTo( "BOXCACHEPROVIDER" ); - assertThat( defaultCache.properties ).isNotNull(); - assertThat( defaultCache.properties.get( "maxObjects" ) ).isEqualTo( 1000 ); - assertThat( defaultCache.properties.get( "reapFrequency" ) ).isEqualTo( 120 ); - assertThat( defaultCache.properties.get( "evictionPolicy" ) ).isEqualTo( "LRU" ); - assertThat( defaultCache.properties.get( "objectStore" ) ).isEqualTo( "ConcurrentStore" ); - assertThat( defaultCache.properties.get( "useLastAccessTimeouts" ) ).isEqualTo( true ); - - // Import Cache Checks - CacheConfig importCache = ( CacheConfig ) config.caches.get( "bxImports" ); - assertThat( importCache.provider.getNameNoCase() ).isEqualTo( "BOXCACHEPROVIDER" ); - assertThat( importCache.properties ).isNotNull(); - assertThat( importCache.properties.get( "maxObjects" ) ).isEqualTo( 200 ); - assertThat( importCache.properties.get( "reapFrequency" ) ).isEqualTo( 120 ); - assertThat( importCache.properties.get( "evictionPolicy" ) ).isEqualTo( "LRU" ); - assertThat( importCache.properties.get( "objectStore" ) ).isEqualTo( "ConcurrentStore" ); - assertThat( importCache.properties.get( "useLastAccessTimeouts" ) ).isEqualTo( true ); - - // Check the debug mode - assertThat( config.security.allowedFileOperationExtensions ).isInstanceOf( List.class ); - assertThat( config.security.allowedFileOperationExtensions ).contains( ".exe" ); - } - } From 326e21a725bd6973a7654b72a5a1cea70bb50719 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 25 Oct 2024 16:03:08 -0500 Subject: [PATCH 04/33] BL-677 --- .../boxlang/runtime/runnables/BoxClassSupport.java | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java b/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java index e862e9cd6..3f5144d4c 100644 --- a/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java +++ b/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java @@ -188,14 +188,8 @@ public static void setSuper( IClassRunnable thisClass, IClassRunnable _super ) { thisClass.getGetterLookup().putAll( _super.getGetterLookup() ); thisClass.getSetterLookup().putAll( _super.getSetterLookup() ); - // merge annotations - for ( var entry : _super.getAnnotations().entrySet() ) { - Key key = entry.getKey(); - if ( !thisClass.getAnnotations().containsKey( key ) && !key.equals( Key._EXTENDS ) && !key.equals( Key._IMPLEMENTS ) - && !key.equals( Key._ABSTRACT ) ) { - thisClass.getAnnotations().put( key, entry.getValue() ); - } - } + // DO NOT merge annotations. They stay separate between parent/child classes and must be merged at runtime, if desired. + // https://ortussolutions.atlassian.net/browse/BL-677 } From da9ecf0ef84d22da7a8a4e9dab7808ba75f5f111 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 25 Oct 2024 16:20:11 -0500 Subject: [PATCH 05/33] BL-679 --- .../runtime/components/system/Loop.java | 45 ++++++++++--------- .../dynamic/casters/GenericCaster.java | 3 ++ .../runtime/components/system/LoopTest.java | 14 ++++++ 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/components/system/Loop.java b/src/main/java/ortus/boxlang/runtime/components/system/Loop.java index 81f7bb90b..e376e8464 100644 --- a/src/main/java/ortus/boxlang/runtime/components/system/Loop.java +++ b/src/main/java/ortus/boxlang/runtime/components/system/Loop.java @@ -1,3 +1,4 @@ + /** * [BoxLang] * @@ -17,6 +18,7 @@ */ package ortus.boxlang.runtime.components.system; +import java.util.Collection; import java.util.Set; import ortus.boxlang.runtime.components.Attribute; @@ -57,7 +59,7 @@ public Loop() { new Attribute( Key.file, "string", Set.of( Validator.requires( Key.index ) ) ), new Attribute( Key.list, "string" ), new Attribute( Key.delimiters, "string" ), - new Attribute( Key.collection, "Struct", Set.of( Validator.requires( Key.item ) ) ), + new Attribute( Key.collection, "collection", Set.of( Validator.requires( Key.item ) ) ), new Attribute( Key.condition, "function" ), new Attribute( Key.query, "any" ), new Attribute( Key.group, "string", Set.of( Validator.NON_EMPTY ) ), @@ -87,23 +89,23 @@ public Loop() { * */ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBody body, IStruct executionState ) { - Array array = attributes.getAsArray( Key.array ); - String item = attributes.getAsString( Key.item ); - String index = attributes.getAsString( Key.index ); - Double to = attributes.getAsDouble( Key.to ); - Double from = attributes.getAsDouble( Key.from ); - String file = attributes.getAsString( Key.file ); - String list = attributes.getAsString( Key.list ); - String delimiters = attributes.getAsString( Key.delimiters ); - IStruct collection = attributes.getAsStruct( Key.collection ); - Function condition = attributes.getAsFunction( Key.condition ); - String group = attributes.getAsString( Key.group ); - Boolean groupCaseSensitive = attributes.getAsBoolean( Key.groupCaseSensitive ); - Integer startRow = attributes.getAsInteger( Key.startRow ); - Integer endRow = attributes.getAsInteger( Key.endRow ); - Object queryOrName = attributes.get( Key.query ); - String label = attributes.getAsString( Key.label ); - Integer times = attributes.getAsInteger( Key.times ); + Array array = attributes.getAsArray( Key.array ); + String item = attributes.getAsString( Key.item ); + String index = attributes.getAsString( Key.index ); + Double to = attributes.getAsDouble( Key.to ); + Double from = attributes.getAsDouble( Key.from ); + String file = attributes.getAsString( Key.file ); + String list = attributes.getAsString( Key.list ); + String delimiters = attributes.getAsString( Key.delimiters ); + Collection collection = ( Collection ) attributes.get( Key.collection ); + Function condition = attributes.getAsFunction( Key.condition ); + String group = attributes.getAsString( Key.group ); + Boolean groupCaseSensitive = attributes.getAsBoolean( Key.groupCaseSensitive ); + Integer startRow = attributes.getAsInteger( Key.startRow ); + Integer endRow = attributes.getAsInteger( Key.endRow ); + Object queryOrName = attributes.get( Key.query ); + String label = attributes.getAsString( Key.label ); + Integer times = attributes.getAsInteger( Key.times ); if ( times != null ) { return _invokeTimes( context, times, item, index, body, executionState, label ); @@ -184,10 +186,11 @@ private BodyResult _invokeCondition( IBoxContext context, Function condition, Co return DEFAULT_RETURN; } - private BodyResult _invokeCollection( IBoxContext context, IStruct collection, String item, ComponentBody body, IStruct executionState, String label ) { + private BodyResult _invokeCollection( IBoxContext context, Collection collection, String item, ComponentBody body, IStruct executionState, + String label ) { // Loop over array, executing body every time - for ( Key key : collection.keySet() ) { - ExpressionInterpreter.setVariable( context, item, key.getName() ); + for ( Object key : collection ) { + ExpressionInterpreter.setVariable( context, item, key ); // Run the code inside of the output loop BodyResult bodyResult = processBody( context, body ); // IF there was a return statement inside our body, we early exit now 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 814568c94..70e1f34f4 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/casters/GenericCaster.java @@ -226,6 +226,9 @@ public static Object cast( IBoxContext context, Object object, Object oType, Boo if ( type.equals( "struct" ) ) { return StructCaster.cast( object, fail ); } + if ( type.equals( "collection" ) ) { + return CollectionCaster.cast( object, fail ); + } if ( type.equals( "structloose" ) ) { return StructCasterLoose.cast( object, fail ); } diff --git a/src/test/java/ortus/boxlang/runtime/components/system/LoopTest.java b/src/test/java/ortus/boxlang/runtime/components/system/LoopTest.java index e05dc0adc..6c484509d 100644 --- a/src/test/java/ortus/boxlang/runtime/components/system/LoopTest.java +++ b/src/test/java/ortus/boxlang/runtime/components/system/LoopTest.java @@ -304,4 +304,18 @@ public void testLoopListNoIndex() { } + @Test + public void testLoopArrayCollection() { + instance.executeSource( + """ + + + + + + """, + context, BoxSourceType.BOXTEMPLATE ); + assertThat( variables.getAsString( Key.of( "result" ) ) ).isEqualTo( "123" ); + } + } From 5a0a8b9b47a7a10531312a3ca6e2dcc2d9253353 Mon Sep 17 00:00:00 2001 From: Michael Born Date: Fri, 25 Oct 2024 18:19:58 -0400 Subject: [PATCH 06/33] Tests - Realign cached query meta key expectations, just because --- .../runtime/bifs/global/jdbc/QueryExecuteTest.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java index 522f2b477..725bb5e10 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java @@ -629,18 +629,15 @@ public void testCacheResultMeta() { IStruct result = ( IStruct ) resultObject; assertThat( result ).containsKey( Key.cached ); - assertThat( result.getAsBoolean( Key.cached ) ).isEqualTo( true ); - assertThat( result ).containsKey( Key.cacheProvider ); - assertEquals( "default", result.getAsString( Key.cacheProvider ) ); - assertThat( result ).containsKey( Key.cacheKey ); - assertEquals( "adminDevs", result.getAsString( Key.cacheKey ) ); - assertThat( result ).containsKey( Key.cacheTimeout ); - assertEquals( Duration.ofHours( 1 ), result.get( Key.cacheTimeout ) ); - assertThat( result ).containsKey( Key.cacheLastAccessTimeout ); + + assertThat( result.getAsBoolean( Key.cached ) ).isEqualTo( true ); + assertEquals( "default", result.getAsString( Key.cacheProvider ) ); + assertEquals( "adminDevs", result.getAsString( Key.cacheKey ) ); + assertEquals( Duration.ofHours( 1 ), result.get( Key.cacheTimeout ) ); assertEquals( Duration.ofMinutes( 30 ), result.get( Key.cacheLastAccessTimeout ) ); } From 00607f4c1e02011dd9f2956ed4f1704825cf6a50 Mon Sep 17 00:00:00 2001 From: michaelborn Date: Fri, 25 Oct 2024 22:20:47 +0000 Subject: [PATCH 07/33] Apply cfformat changes --- .../boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java index 725bb5e10..a15205044 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/jdbc/QueryExecuteTest.java @@ -633,7 +633,7 @@ public void testCacheResultMeta() { assertThat( result ).containsKey( Key.cacheKey ); assertThat( result ).containsKey( Key.cacheTimeout ); assertThat( result ).containsKey( Key.cacheLastAccessTimeout ); - + assertThat( result.getAsBoolean( Key.cached ) ).isEqualTo( true ); assertEquals( "default", result.getAsString( Key.cacheProvider ) ); assertEquals( "adminDevs", result.getAsString( Key.cacheKey ) ); From 5ccd85fbe86505802256629c9faa531328907185 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:06:14 +0000 Subject: [PATCH 08/33] Bump ch.qos.logback:logback-classic from 1.5.11 to 1.5.12 Bumps [ch.qos.logback:logback-classic](https://github.com/qos-ch/logback) from 1.5.11 to 1.5.12. - [Commits](https://github.com/qos-ch/logback/compare/v_1.5.11...v_1.5.12) --- updated-dependencies: - dependency-name: ch.qos.logback:logback-classic 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 31a744400..01838ab86 100644 --- a/build.gradle +++ b/build.gradle @@ -128,7 +128,7 @@ dependencies { // https://mvnrepository.com/artifact/org.slf4j/slf4j-api implementation 'org.slf4j:slf4j-api:2.0.16' // https://mvnrepository.com/artifact/ch.qos.logback/logback-classic - implementation 'ch.qos.logback:logback-classic:1.5.11' + implementation 'ch.qos.logback:logback-classic:1.5.12' // https://mvnrepository.com/artifact/com.zaxxer/HikariCP implementation 'com.zaxxer:HikariCP:6.0.0' // https://mvnrepository.com/artifact/org.ow2.asm/asm-tree From d2c22074e0f097b7d365e520785342cc7572640e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Oct 2024 16:06:21 +0000 Subject: [PATCH 09/33] Bump org.wiremock:wiremock from 3.9.1 to 3.9.2 Bumps [org.wiremock:wiremock](https://github.com/wiremock/wiremock) from 3.9.1 to 3.9.2. - [Release notes](https://github.com/wiremock/wiremock/releases) - [Commits](https://github.com/wiremock/wiremock/compare/3.9.1...3.9.2) --- updated-dependencies: - dependency-name: org.wiremock:wiremock 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 31a744400..05b72e282 100644 --- a/build.gradle +++ b/build.gradle @@ -97,7 +97,7 @@ dependencies { testImplementation "com.google.truth:truth:1.+" testImplementation "commons-cli:commons-cli:1.9.0" // https://wiremock.org/ - testImplementation "org.wiremock:wiremock:3.9.1" + testImplementation "org.wiremock:wiremock:3.9.2" // https://mvnrepository.com/artifact/org.apache.derby/derby testImplementation 'org.apache.derby:derby:10.17.1.0' testImplementation 'io.undertow:undertow-core:2.3.18.Final' From af6d2e9b4624b02bd9b564b9962abef368e622fe Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 28 Oct 2024 23:39:04 -0500 Subject: [PATCH 10/33] BL-711 --- src/main/antlr/BoxScriptGrammar.g4 | 3 +- src/main/antlr/CFGrammar.g4 | 3 +- .../compiler/parser/BoxScriptParser.java | 26 ++++-- .../boxlang/compiler/parser/CFParser.java | 24 ++++-- .../toolchain/BoxExpressionVisitor.java | 63 +++++++------- .../compiler/toolchain/BoxVisitor.java | 10 +-- .../toolchain/CFExpressionVisitor.java | 64 +++++++------- .../boxlang/compiler/toolchain/CFVisitor.java | 10 +-- .../TestCases/asm/phase3/ApplicationTest.java | 8 +- src/test/java/TestCases/phase3/ClassTest.java | 51 +++++++++-- src/test/java/TestCases/phase3/StaticTest.bx | 85 ++++++++++--------- src/test/java/TestCases/phase3/StaticTest2.bx | 45 ++++++++++ .../java/TestCases/phase3/StaticTestCF.cfc | 5 ++ .../java/TestCases/phase3/StaticTestCF2.cfc | 33 +++++++ 14 files changed, 274 insertions(+), 156 deletions(-) create mode 100644 src/test/java/TestCases/phase3/StaticTest2.bx create mode 100644 src/test/java/TestCases/phase3/StaticTestCF2.cfc diff --git a/src/main/antlr/BoxScriptGrammar.g4 b/src/main/antlr/BoxScriptGrammar.g4 index fafdcc529..cd2d45f6e 100644 --- a/src/main/antlr/BoxScriptGrammar.g4 +++ b/src/main/antlr/BoxScriptGrammar.g4 @@ -539,14 +539,13 @@ el2 | LPAREN expression RPAREN # exprPrecedence // (foo) | new # exprNew // new foo.bar.Baz() | el2 LPAREN argumentList? RPAREN # exprFunctionCall // foo(bar, baz) - | el2 QM? DOT el2 # exprDotAccess // xc.y?.z recursive + | el2 (QM? DOT | COLONCOLON) el2 # exprDotOrColonAccess // xc.y?.z or foo::bar recursive | el2 QM? DOT_FLOAT_LITERAL # exprDotFloat // foo.50 | el2 QM? DOT_NUMBER_PREFIXED_IDENTIFIER # exprDotFloatID // foo.50bar | el2 LBRACKET expression RBRACKET # exprArrayAccess // foo[bar] | op = (NOT | BANG | MINUS | PLUS) el2 # exprUnary // !foo, -foo, +foo | op = (PLUSPLUS | MINUSMINUS | BITWISE_COMPLEMENT) el2 # exprPrefix // ++foo, --foo, ~foo | el2 op = (PLUSPLUS | MINUSMINUS) # exprPostfix // foo++, bar-- - | el2 COLONCOLON el2 # exprStaticAccess // foo::bar | el2 POWER el2 # exprPower // foo ^ bar | el2 op = (STAR | SLASH | PERCENT | MOD | BACKSLASH) el2 # exprMult // foo * bar | el2 op = (PLUS | MINUS) el2 # exprAdd // foo + bar diff --git a/src/main/antlr/CFGrammar.g4 b/src/main/antlr/CFGrammar.g4 index c69ca7f58..bb4d53de1 100644 --- a/src/main/antlr/CFGrammar.g4 +++ b/src/main/antlr/CFGrammar.g4 @@ -518,14 +518,13 @@ el2 | LPAREN expression RPAREN # exprPrecedence // ( foo ) | new # exprNew // new foo.bar.Baz() | el2 LPAREN argumentList? RPAREN # exprFunctionCall // foo(bar, baz) - | el2 QM? DOT DOT? el2 # exprDotAccess // xc.y?.z recursive and Adobe's stupid foo..bar bug they allow + | el2 (QM? DOT DOT? | COLONCOLON) el2 # exprDotOrColonAccess // xc.y?.z or foo::bar recursive and Adobe's stupid foo..bar bug they allow | el2 QM? DOT? DOT_FLOAT_LITERAL # exprDotFloat // foo.50 | el2 QM? DOT? DOT_NUMBER_PREFIXED_IDENTIFIER # exprDotFloatID // foo.50bar | el2 LBRACKET expression RBRACKET # exprArrayAccess // foo[bar] | op = (NOT | BANG | MINUS | PLUS) el2 # exprUnary // !foo, -foo, +foo | op = (PLUSPLUS | MINUSMINUS | BITWISE_COMPLEMENT) el2 # exprPrefix // ++foo, --foo, ~foo | el2 op = (PLUSPLUS | MINUSMINUS) # exprPostfix // foo++, bar-- - | el2 COLONCOLON el2 # exprStaticAccess // foo::bar | el2 POWER el2 # exprPower // foo ^ bar | el2 op = (STAR | SLASH | PERCENT | MOD | BACKSLASH) el2 # exprMult // foo * bar | el2 op = (PLUS | MINUS) el2 # exprAdd // foo + bar diff --git a/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java b/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java index b35f3e1d2..3f36a3f09 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java +++ b/src/main/java/ortus/boxlang/compiler/parser/BoxScriptParser.java @@ -51,6 +51,7 @@ import ortus.boxlang.compiler.ast.expression.BoxDecimalLiteral; import ortus.boxlang.compiler.ast.expression.BoxDotAccess; import ortus.boxlang.compiler.ast.expression.BoxExpressionInvocation; +import ortus.boxlang.compiler.ast.expression.BoxFQN; import ortus.boxlang.compiler.ast.expression.BoxFunctionInvocation; import ortus.boxlang.compiler.ast.expression.BoxIdentifier; import ortus.boxlang.compiler.ast.expression.BoxIntegerLiteral; @@ -59,6 +60,8 @@ import ortus.boxlang.compiler.ast.expression.BoxNull; import ortus.boxlang.compiler.ast.expression.BoxParenthesis; import ortus.boxlang.compiler.ast.expression.BoxScope; +import ortus.boxlang.compiler.ast.expression.BoxStaticAccess; +import ortus.boxlang.compiler.ast.expression.BoxStaticMethodInvocation; import ortus.boxlang.compiler.ast.expression.BoxStringLiteral; import ortus.boxlang.compiler.ast.expression.BoxStructLiteral; import ortus.boxlang.compiler.toolchain.BoxExpressionVisitor; @@ -548,13 +551,13 @@ public BoxScriptParser setSubParser( boolean subParser ) { * @param left the left side of the dot access left.right * @param right the right side of the dot access left.right */ - public void checkDotAccess( BoxExpression left, BoxExpression right ) { + public void checkDotAccess( BoxExpression left, BoxExpression right, boolean isStatic ) { // Check the right hand side to see if it is a valid access method - checkRight( right ); + checkRight( right, isStatic ); // Check to see if the left hand side is something that is valid to be accessed via a dot access - checkLeft( left ); + checkLeft( left, isStatic ); // Now we know the LHS is valid for access by a dot method and the RHS is a valid access method, so // we can check the combinations here if needed. @@ -566,7 +569,7 @@ public void checkDotAccess( BoxExpression left, BoxExpression right ) { * * @param right the right side of the dot access left.right */ - private void checkRight( BoxExpression right ) { + private void checkRight( BoxExpression right, boolean isStatic ) { // Check the right hand side is a valid access method and fall through if it is not switch ( right ) { case BoxFunctionInvocation ignored -> { @@ -587,7 +590,8 @@ private void checkRight( BoxExpression right ) { } case BoxExpressionInvocation ignored -> { } - default -> errorListener.semanticError( "dot access via " + right.getDescription() + " is not a valid access method", right.getPosition() ); + default -> errorListener.semanticError( ( isStatic ? "static" : "dot" ) + " access via " + right.getDescription() + " is not a valid access method", + right.getPosition() ); } } @@ -596,7 +600,7 @@ private void checkRight( BoxExpression right ) { * * @param left the left side of the dot access left.right */ - private void checkLeft( BoxExpression left ) { + private void checkLeft( BoxExpression left, boolean isStatic ) { // Check the left hand side is a valid construct for dot access and fall through if it is not switch ( left ) { case BoxFunctionInvocation ignored -> { @@ -624,9 +628,15 @@ private void checkLeft( BoxExpression left ) { case BoxDecimalLiteral ignored -> { } case BoxParenthesis ignored -> { - // TODO: Brad - Should we allow this always, or check what is inside the parenthesis? } - default -> errorListener.semanticError( left.getDescription() + " is not a valid construct for dot access", left.getPosition() ); + case BoxStaticMethodInvocation ignored -> { + } + case BoxStaticAccess ignored -> { + } + case BoxFQN ignored -> { + } + default -> errorListener.semanticError( left.getDescription() + " is not a valid construct for " + ( isStatic ? "static" : "dot" ) + " access", + left.getPosition() ); } } diff --git a/src/main/java/ortus/boxlang/compiler/parser/CFParser.java b/src/main/java/ortus/boxlang/compiler/parser/CFParser.java index 259eb792f..d3dc9e670 100644 --- a/src/main/java/ortus/boxlang/compiler/parser/CFParser.java +++ b/src/main/java/ortus/boxlang/compiler/parser/CFParser.java @@ -62,6 +62,8 @@ import ortus.boxlang.compiler.ast.expression.BoxNull; import ortus.boxlang.compiler.ast.expression.BoxParenthesis; import ortus.boxlang.compiler.ast.expression.BoxScope; +import ortus.boxlang.compiler.ast.expression.BoxStaticAccess; +import ortus.boxlang.compiler.ast.expression.BoxStaticMethodInvocation; import ortus.boxlang.compiler.ast.expression.BoxStringInterpolation; import ortus.boxlang.compiler.ast.expression.BoxStringLiteral; import ortus.boxlang.compiler.ast.expression.BoxStructLiteral; @@ -1481,13 +1483,13 @@ public CFParser setSubParser( boolean subParser ) { * @param left the left side of the dot access left.right * @param right the right side of the dot access left.right */ - public void checkDotAccess( BoxExpression left, BoxExpression right ) { + public void checkDotAccess( BoxExpression left, BoxExpression right, boolean isStatic ) { // Check the right hand side to see if it is a valid access method - checkRight( right ); + checkRight( right, isStatic ); // Check to see if the left hand side is something that is valid to be accessed via a dot access - checkLeft( left ); + checkLeft( left, isStatic ); // Now we know the LHS is valid for access by a dot method and the RHS is a valid access method, so // we can check the combinations here if needed. @@ -1499,7 +1501,7 @@ public void checkDotAccess( BoxExpression left, BoxExpression right ) { * * @param right the right side of the dot access left.right */ - private void checkRight( BoxExpression right ) { + private void checkRight( BoxExpression right, boolean isStatic ) { // Check the right hand side is a valid access method and fall through if it is not switch ( right ) { case BoxFunctionInvocation ignored -> { @@ -1520,7 +1522,8 @@ private void checkRight( BoxExpression right ) { } case BoxExpressionInvocation ignored -> { } - default -> errorListener.semanticError( "dot access via " + right.getDescription() + " is not a valid access method", right.getPosition() ); + default -> errorListener.semanticError( ( isStatic ? "static" : "dot" ) + " access via " + right.getDescription() + " is not a valid access method", + right.getPosition() ); } } @@ -1529,7 +1532,7 @@ private void checkRight( BoxExpression right ) { * * @param left the left side of the dot access left.right */ - private void checkLeft( BoxExpression left ) { + private void checkLeft( BoxExpression left, boolean isStatic ) { // Check the left hand side is a valid construct for dot access and fall through if it is not switch ( left ) { case BoxFunctionInvocation ignored -> { @@ -1558,7 +1561,14 @@ private void checkLeft( BoxExpression left ) { } case BoxParenthesis ignored -> { } - default -> errorListener.semanticError( left.getDescription() + " is not a valid construct for dot access", left.getPosition() ); + case BoxStaticMethodInvocation ignored -> { + } + case BoxStaticAccess ignored -> { + } + case BoxFQN ignored -> { + } + default -> errorListener.semanticError( left.getDescription() + " is not a valid construct for " + ( isStatic ? "static" : "dot" ) + " access", + left.getPosition() ); } } diff --git a/src/main/java/ortus/boxlang/compiler/toolchain/BoxExpressionVisitor.java b/src/main/java/ortus/boxlang/compiler/toolchain/BoxExpressionVisitor.java index 7399321b3..3eaaaa54a 100644 --- a/src/main/java/ortus/boxlang/compiler/toolchain/BoxExpressionVisitor.java +++ b/src/main/java/ortus/boxlang/compiler/toolchain/BoxExpressionVisitor.java @@ -108,9 +108,9 @@ import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprBorContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprCastAsContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprCatContext; -import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotAccessContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotFloatContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotFloatIDContext; +import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotOrColonAccessContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprElvisContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprEqualContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprFunctionCallContext; @@ -130,7 +130,6 @@ import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprPrefixContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprRelationalContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprStatInvocableContext; -import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprStaticAccessContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprTernaryContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprUnaryContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprVarDeclContext; @@ -267,7 +266,7 @@ public BoxExpression visitExprDotFloat( ExprDotFloatContext ctx ) { // the case. As other types may also need conversion, we hand off to a helper method. var leftId = convertDotElement( left, false ); - tools.checkDotAccess( leftId, right ); + tools.checkDotAccess( leftId, right, false ); return new BoxDotAccess( leftId, ctx.QM() != null, right, pos, src ); } @@ -285,7 +284,7 @@ public BoxExpression visitExprDotFloatID( ExprDotFloatIDContext ctx ) { // the case. As other types may also need conversion, we hand off to a helper method. var leftId = convertDotElement( left, false ); - tools.checkDotAccess( leftId, right ); + tools.checkDotAccess( leftId, right, false ); return new BoxDotAccess( leftId, ctx.QM() != null, right, pos, src ); } @@ -311,16 +310,21 @@ public BoxExpression visitExprIllegalIdentifier( ExprIllegalIdentifierContext ct * @return the AST for a particular accessor operation */ @Override - public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { - + public BoxExpression visitExprDotOrColonAccess( ExprDotOrColonAccessContext ctx ) { + boolean isStatic = ctx.COLONCOLON() != null; // Positions is based upon the right hand side of the dot, but strangely, includes the dot - var pos = tools.getPosition( ctx.el2( 1 ) ); - var start = pos.getStart(); + var pos = tools.getPosition( ctx.el2( 1 ) ); + var start = pos.getStart(); start.setColumn( start.getColumn() - 1 ); - var src = "." + tools.getSourceText( ctx.el2( 1 ) ); + var src = ( isStatic ? "::" : "." ) + tools.getSourceText( ctx.el2( 1 ) ); var left = ctx.el2( 0 ).accept( this ); var right = ctx.el2( 1 ).accept( this ); + // foo.bar.baz::property or foo.bar.baz::method() MUST be seen as a class name on the LHS, not dot access + if ( isStatic && left instanceof BoxDotAccess ) { + left = new BoxFQN( ctx.el2( 0 ).getText(), tools.getPosition( ctx.el2( 0 ) ), tools.getSourceText( ctx.el2( 0 ) ) ); + } + // Because Booleans take precedence over keywords as identifiers, we will get a // boolean literal for left or right and so we convert them to Identifiers if that is // the case. As other types may also need conversion, we hand off to a helper method. @@ -329,7 +333,7 @@ public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { // Check validity of this type of access. Will add an issue to the list if there is an invalid access // but we will still generate a DotAccess node and carry on, so we catch all errors in one pass. - tools.checkDotAccess( leftId, rightId ); + tools.checkDotAccess( leftId, rightId, isStatic ); switch ( right ) { @@ -397,11 +401,16 @@ public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { // The Id needs to end in the correct column, not the end of the invocation var iPos = new Position( ids, ide ); - // Some messing around to get the text correct for the MethodInvocation - // as FunctionInvocation only stores a string, not an identifier for the function for - // some reason. - return new BoxMethodInvocation( new BoxIdentifier( iName, iPos, iName ), left, invocation.getArguments(), ctx.QM() != null, true, - invocation.getPosition(), invocation.getSourceText() ); + if ( isStatic ) { + return new BoxStaticMethodInvocation( new BoxIdentifier( iName, iPos, iName ), left, + invocation.getArguments(), invocation.getPosition(), "::" + invocation.getSourceText() ); + } else { + // Some messing around to get the text correct for the MethodInvocation + // as FunctionInvocation only stores a string, not an identifier for the function for + // some reason. + return new BoxMethodInvocation( new BoxIdentifier( iName, iPos, iName ), left, invocation.getArguments(), ctx.QM() != null, true, + invocation.getPosition(), invocation.getSourceText() ); + } } case BoxExpressionInvocation invocation -> { @@ -464,8 +473,12 @@ public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { return new BoxArrayAccess( leftId, ctx.QM() != null, arrayAccess.getAccess(), pos, src ); } case null, default -> { + if ( isStatic ) { + return new BoxStaticAccess( leftId, false, rightId, tools.getPosition( ctx.el2( 1 ) ), src ); + } else { + return new BoxDotAccess( leftId, ctx.QM() != null, rightId, pos, src ); + } - return new BoxDotAccess( leftId, ctx.QM() != null, rightId, pos, src ); } } } @@ -949,24 +962,6 @@ public BoxExpression visitIdentifier( IdentifierContext ctx ) { return new BoxIdentifier( ctx.getText(), pos, src ); } - @Override - public BoxExpression visitExprStaticAccess( ExprStaticAccessContext ctx ) { - var pos = tools.getPosition( ctx ); - var src = tools.getSourceText( ctx ); - - var left = ctx.el2( 0 ).accept( this ); - if ( left instanceof BoxDotAccess ) { - left = new BoxFQN( ctx.el2( 0 ).getText(), tools.getPosition( ctx.el2( 0 ) ), tools.getSourceText( ctx.el2( 0 ) ) ); - } - var right = ctx.el2( 1 ).accept( this ); - - if ( right instanceof BoxFunctionInvocation invocation ) { - return new BoxStaticMethodInvocation( new BoxIdentifier( invocation.getName(), invocation.getPosition(), invocation.getSourceText() ), left, - invocation.getArguments(), invocation.getPosition(), "::" + invocation.getSourceText() ); - } - return new BoxStaticAccess( left, false, right, tools.getPosition( ctx.el2( 1 ) ), "::" + tools.getSourceText( ctx.el2( 1 ) ) ); - } - @Override public BoxExpression visitExprNew( ExprNewContext ctx ) { return ctx.new_().accept( this ); diff --git a/src/main/java/ortus/boxlang/compiler/toolchain/BoxVisitor.java b/src/main/java/ortus/boxlang/compiler/toolchain/BoxVisitor.java index bc34081e6..93e817e87 100644 --- a/src/main/java/ortus/boxlang/compiler/toolchain/BoxVisitor.java +++ b/src/main/java/ortus/boxlang/compiler/toolchain/BoxVisitor.java @@ -90,8 +90,8 @@ import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprBorContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprCastAsContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprCatContext; -import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotAccessContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotFloatContext; +import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprDotOrColonAccessContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprElvisContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprEqualContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprFunctionCallContext; @@ -110,7 +110,6 @@ import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprRelationalContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprStatAnonymousFunctionContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprStatInvocableContext; -import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprStaticAccessContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprTernaryContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprUnaryContext; import ortus.boxlang.parser.antlr.BoxScriptGrammar.ExprXorContext; @@ -669,7 +668,7 @@ public BoxNode visitExprDotFloat( ExprDotFloatContext ctx ) { } @Override - public BoxNode visitExprDotAccess( ExprDotAccessContext ctx ) { + public BoxNode visitExprDotOrColonAccess( ExprDotOrColonAccessContext ctx ) { return buildExprStat( ctx ); } @@ -791,11 +790,6 @@ public BoxNode visitExprArrayLiteral( ExprArrayLiteralContext ctx ) { return buildExprStat( ctx ); } - @Override - public BoxNode visitExprStaticAccess( ExprStaticAccessContext ctx ) { - return buildExprStat( ctx ); - } - @Override public BoxNode visitExprStatAnonymousFunction( ExprStatAnonymousFunctionContext ctx ) { return buildExprStat( ctx ); diff --git a/src/main/java/ortus/boxlang/compiler/toolchain/CFExpressionVisitor.java b/src/main/java/ortus/boxlang/compiler/toolchain/CFExpressionVisitor.java index 1e5a9cb3a..d7b772f79 100644 --- a/src/main/java/ortus/boxlang/compiler/toolchain/CFExpressionVisitor.java +++ b/src/main/java/ortus/boxlang/compiler/toolchain/CFExpressionVisitor.java @@ -98,9 +98,9 @@ import ortus.boxlang.parser.antlr.CFGrammar.ExprAtomsContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprBinaryContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprCatContext; -import ortus.boxlang.parser.antlr.CFGrammar.ExprDotAccessContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprDotFloatContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprDotFloatIDContext; +import ortus.boxlang.parser.antlr.CFGrammar.ExprDotOrColonAccessContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprElvisContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprEqualContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprFunctionCallContext; @@ -119,7 +119,6 @@ import ortus.boxlang.parser.antlr.CFGrammar.ExprPrefixContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprRelationalContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprStatInvocableContext; -import ortus.boxlang.parser.antlr.CFGrammar.ExprStaticAccessContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprTernaryContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprUnaryContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprVarDeclContext; @@ -263,7 +262,7 @@ public BoxExpression visitExprDotFloat( ExprDotFloatContext ctx ) { // the case. As other types may also need conversion, we hand off to a helper method. var leftId = convertDotElement( left, false ); - tools.checkDotAccess( leftId, right ); + tools.checkDotAccess( leftId, right, false ); return new BoxDotAccess( leftId, ctx.QM() != null, right, pos, src ); } @@ -281,7 +280,7 @@ public BoxExpression visitExprDotFloatID( ExprDotFloatIDContext ctx ) { // the case. As other types may also need conversion, we hand off to a helper method. var leftId = convertDotElement( left, false ); - tools.checkDotAccess( leftId, right ); + tools.checkDotAccess( leftId, right, false ); return new BoxDotAccess( leftId, ctx.QM() != null, right, pos, src ); } @@ -307,16 +306,21 @@ public BoxExpression visitExprIllegalIdentifier( ExprIllegalIdentifierContext ct * @return the AST for a particular accessor operation */ @Override - public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { - + public BoxExpression visitExprDotOrColonAccess( ExprDotOrColonAccessContext ctx ) { + boolean isStatic = ctx.COLONCOLON() != null; // Positions is based upon the right hand side of the dot, but strangely, includes the dot - var pos = tools.getPosition( ctx.el2( 1 ) ); - var start = pos.getStart(); + var pos = tools.getPosition( ctx.el2( 1 ) ); + var start = pos.getStart(); start.setColumn( start.getColumn() - 1 ); - var src = "." + tools.getSourceText( ctx.el2( 1 ) ); + var src = ( isStatic ? "::" : "." ) + tools.getSourceText( ctx.el2( 1 ) ); var left = ctx.el2( 0 ).accept( this ); var right = ctx.el2( 1 ).accept( this ); + // foo.bar.baz::property or foo.bar.baz::method() MUST be seen as a class name on the LHS, not dot access + if ( isStatic && left instanceof BoxDotAccess ) { + left = new BoxFQN( ctx.el2( 0 ).getText(), tools.getPosition( ctx.el2( 0 ) ), tools.getSourceText( ctx.el2( 0 ) ) ); + } + // Because Booleans take precedence over keywords as identifiers, we will get a // boolean literal for left or right and so we convert them to Identifiers if that is // the case. As other types may also need conversion, we hand off to a helper method. @@ -325,7 +329,7 @@ public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { // Check validity of this type of access. Will add an issue to the list if there is an invalid access // but we will still generate a DotAccess node and carry on, so we catch all errors in one pass. - tools.checkDotAccess( leftId, rightId ); + tools.checkDotAccess( leftId, rightId, isStatic ); switch ( right ) { @@ -393,11 +397,16 @@ public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { // The Id needs to end in the correct column, not the end of the invocation var iPos = new Position( ids, ide ); - // Some messing around to get the text correct for the MethodInvocation - // as FunctionInvocation only stores a string, not an identifier for the function for - // some reason. - return new BoxMethodInvocation( new BoxIdentifier( iName, iPos, iName ), left, invocation.getArguments(), ctx.QM() != null, true, - invocation.getPosition(), invocation.getSourceText() ); + if ( isStatic ) { + return new BoxStaticMethodInvocation( new BoxIdentifier( iName, iPos, iName ), left, + invocation.getArguments(), invocation.getPosition(), "::" + invocation.getSourceText() ); + } else { + // Some messing around to get the text correct for the MethodInvocation + // as FunctionInvocation only stores a string, not an identifier for the function for + // some reason. + return new BoxMethodInvocation( new BoxIdentifier( iName, iPos, iName ), left, invocation.getArguments(), ctx.QM() != null, true, + invocation.getPosition(), invocation.getSourceText() ); + } } case BoxExpressionInvocation invocation -> { @@ -461,8 +470,11 @@ public BoxExpression visitExprDotAccess( ExprDotAccessContext ctx ) { return new BoxArrayAccess( leftId, ctx.QM() != null, arrayAccess.getAccess(), pos, src ); } case null, default -> { - - return new BoxDotAccess( leftId, ctx.QM() != null, rightId, pos, src ); + if ( isStatic ) { + return new BoxStaticAccess( leftId, false, rightId, tools.getPosition( ctx.el2( 1 ) ), src ); + } else { + return new BoxDotAccess( leftId, ctx.QM() != null, rightId, pos, src ); + } } } } @@ -879,24 +891,6 @@ public BoxExpression visitIdentifier( IdentifierContext ctx ) { return new BoxIdentifier( ctx.getText(), pos, src ); } - @Override - public BoxExpression visitExprStaticAccess( ExprStaticAccessContext ctx ) { - var pos = tools.getPosition( ctx ); - var src = tools.getSourceText( ctx ); - - var left = ctx.el2( 0 ).accept( this ); - if ( left instanceof BoxDotAccess ) { - left = new BoxFQN( ctx.el2( 0 ).getText(), tools.getPosition( ctx.el2( 0 ) ), tools.getSourceText( ctx.el2( 0 ) ) ); - } - var right = ctx.el2( 1 ).accept( this ); - - if ( right instanceof BoxFunctionInvocation invocation ) { - return new BoxStaticMethodInvocation( new BoxIdentifier( invocation.getName(), invocation.getPosition(), invocation.getSourceText() ), left, - invocation.getArguments(), invocation.getPosition(), "::" + invocation.getSourceText() ); - } - return new BoxStaticAccess( left, false, right, tools.getPosition( ctx.el2( 1 ) ), "::" + tools.getSourceText( ctx.el2( 1 ) ) ); - } - @Override public BoxExpression visitExprNew( ExprNewContext ctx ) { return ctx.new_().accept( this ); diff --git a/src/main/java/ortus/boxlang/compiler/toolchain/CFVisitor.java b/src/main/java/ortus/boxlang/compiler/toolchain/CFVisitor.java index d4aa2426d..8bc5f3ce6 100644 --- a/src/main/java/ortus/boxlang/compiler/toolchain/CFVisitor.java +++ b/src/main/java/ortus/boxlang/compiler/toolchain/CFVisitor.java @@ -82,8 +82,8 @@ import ortus.boxlang.parser.antlr.CFGrammar.ExprAssignContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprBinaryContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprCatContext; -import ortus.boxlang.parser.antlr.CFGrammar.ExprDotAccessContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprDotFloatContext; +import ortus.boxlang.parser.antlr.CFGrammar.ExprDotOrColonAccessContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprElvisContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprEqualContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprFunctionCallContext; @@ -101,7 +101,6 @@ import ortus.boxlang.parser.antlr.CFGrammar.ExprRelationalContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprStatAnonymousFunctionContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprStatInvocableContext; -import ortus.boxlang.parser.antlr.CFGrammar.ExprStaticAccessContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprTernaryContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprUnaryContext; import ortus.boxlang.parser.antlr.CFGrammar.ExprXorContext; @@ -659,7 +658,7 @@ public BoxNode visitExprDotFloat( ExprDotFloatContext ctx ) { } @Override - public BoxNode visitExprDotAccess( ExprDotAccessContext ctx ) { + public BoxNode visitExprDotOrColonAccess( ExprDotOrColonAccessContext ctx ) { return buildExprStat( ctx ); } @@ -751,11 +750,6 @@ public BoxNode visitExprArrayLiteral( ExprArrayLiteralContext ctx ) { return buildExprStat( ctx ); } - @Override - public BoxNode visitExprStaticAccess( ExprStaticAccessContext ctx ) { - return buildExprStat( ctx ); - } - @Override public BoxNode visitExprStatAnonymousFunction( ExprStatAnonymousFunctionContext ctx ) { return buildExprStat( ctx ); diff --git a/src/test/java/TestCases/asm/phase3/ApplicationTest.java b/src/test/java/TestCases/asm/phase3/ApplicationTest.java index 989e30a58..98cb2dea3 100644 --- a/src/test/java/TestCases/asm/phase3/ApplicationTest.java +++ b/src/test/java/TestCases/asm/phase3/ApplicationTest.java @@ -81,7 +81,7 @@ public void testBasicApplication() { // @formatter:off instance.executeSource( """ - application name="myAppsdfsdf" sessionmanagement="true"; + application name="myAppsdfsdfasm" sessionmanagement="true"; result = application; result2 = session; @@ -98,7 +98,7 @@ public void testBasicApplication() { Instant now = Instant.now(); long differenceInSeconds = ChronoUnit.SECONDS.between( actual, now ); - assertThat( app.getName().getName() ).isEqualTo( "myAppsdfsdf" ); + assertThat( app.getName().getName() ).isEqualTo( "myAppsdfsdfasm" ); assertThat( app.getSessionsCache() ).isNotNull(); assertThat( app.getApplicationScope() ).isNotNull(); assertThat( app.getApplicationScope().getName().getName() ).isEqualTo( "application" ); @@ -112,13 +112,13 @@ public void testGetAppMeta() { // @formatter:off instance.executeSource( """ - application name="myAppsdfsdf2" sessionmanagement="true"; + application name="myAppsdfsdf2asm" sessionmanagement="true"; result = GetApplicationMetadata(); """, context ); // @formatter:on assertThat( variables.get( result ) ).isInstanceOf( IStruct.class ); - assertThat( variables.getAsStruct( result ).get( "name" ) ).isEqualTo( "myAppsdfsdf2" ); + assertThat( variables.getAsStruct( result ).get( "name" ) ).isEqualTo( "myAppsdfsdf2asm" ); assertThat( variables.getAsStruct( result ).get( "sessionmanagement" ).toString() ).isEqualTo( "true" ); } diff --git a/src/test/java/TestCases/phase3/ClassTest.java b/src/test/java/TestCases/phase3/ClassTest.java index 41729c2de..a29a27098 100644 --- a/src/test/java/TestCases/phase3/ClassTest.java +++ b/src/test/java/TestCases/phase3/ClassTest.java @@ -1086,7 +1086,10 @@ public void testStaticStatic() { myStaticUDF = src.test.java.TestCases.phase3.StaticTest::sayHello; result7 = myStaticUDF(); result8 = src.test.java.TestCases.phase3.StaticTest::123; - """, context, BoxSourceType.BOXSCRIPT ); + result9 = src.test.java.TestCases.phase3.StaticTest2::getInstance().getStaticBrad(); + result10 = src.test.java.TestCases.phase3.StaticTest2::getInstance().thisStaticBrad; + """, 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" ); @@ -1094,6 +1097,34 @@ public void testStaticStatic() { assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( "luis" ); assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "Hello" ); assertThat( variables.get( Key.of( "result8" ) ) ).isEqualTo( 456 ); + assertThat( variables.get( Key.of( "result9" ) ) ).isEqualTo( "wood" ); + assertThat( variables.get( Key.of( "result10" ) ) ).isEqualTo( "wood" ); + } + + @Test + public void testStaticNestedMethod() { + instance.executeSource( + """ + import java.lang.Thread; + result = Thread::currentThread().getContextClassLoader(); + // This is the same as above + result2 = Thread.currentThread().getContextClassLoader(); + """, context, BoxSourceType.BOXSCRIPT ); + assertThat( variables.get( result ) ).isNotNull(); + assertThat( variables.get( result ) ).isInstanceOf( ClassLoader.class ); + assertThat( variables.get( Key.of( "result2" ) ) ).isNotNull(); + assertThat( variables.get( Key.of( "result2" ) ) ).isInstanceOf( ClassLoader.class ); + } + + @Test + public void testStaticNestedProperty() { + instance.executeSource( + """ + import java.lang.System; + System::out.println( "testStaticNestedProperty test" ); + // This is the same as above. + System.out.println( "testStaticNestedProperty test2" ); + """, context, BoxSourceType.BOXSCRIPT ); } @Test @@ -1125,19 +1156,23 @@ public void testStaticInstanceCF() { public void testStaticStaticCF() { instance.executeSource( """ - result1 = src.test.java.TestCases.phase3.StaticTest::foo; - result2 = src.test.java.TestCases.phase3.StaticTest::myStaticFunc(); - result4 = src.test.java.TestCases.phase3.StaticTest::scoped; - result5 = src.test.java.TestCases.phase3.StaticTest::unscoped; - result6 = src.test.java.TestCases.phase3.StaticTest::again; - result7 = src.test.java.TestCases.phase3.StaticTest::123; - """, context, BoxSourceType.CFSCRIPT ); + result1 = src.test.java.TestCases.phase3.StaticTest::foo; + result2 = src.test.java.TestCases.phase3.StaticTest::myStaticFunc(); + result4 = src.test.java.TestCases.phase3.StaticTest::scoped; + result5 = src.test.java.TestCases.phase3.StaticTest::unscoped; + result6 = src.test.java.TestCases.phase3.StaticTest::again; + result7 = src.test.java.TestCases.phase3.StaticTest::123; + result9 = src.test.java.TestCases.phase3.StaticTestCF2::getInstance().getStaticBrad(); + result10 = src.test.java.TestCases.phase3.StaticTestCF2::getInstance().thisStaticBrad; + """, context, BoxSourceType.CFSCRIPT ); assertThat( variables.get( Key.of( "result1" ) ) ).isEqualTo( 9000 ); assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "static9000" ); 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( 456 ); + assertThat( variables.get( Key.of( "result9" ) ) ).isEqualTo( "wood" ); + 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 dd2767ec3..24f7289b7 100644 --- a/src/test/java/TestCases/phase3/StaticTest.bx +++ b/src/test/java/TestCases/phase3/StaticTest.bx @@ -1,40 +1,45 @@ - class { - static { - static.scoped = "brad"; - unscoped = "wood" - static foo = 9000; - '123' = 456; - final brad = "wood" - } - - static.foo = 42; - variables.staticBrad = static.brad; - - static { - static.again = "luis" - } - - static function myStaticFunc() { - return "static" & static.foo; - } - - function myInstanceFunc() { - return "instance" & myStaticFunc(); - } - - array function myInstanceFunc2() { - return [static.scoped, - static.unscoped, - static.foo]; - } - - static function sayHello() { - return "Hello"; - } - - function getStaticBrad() { - return staticBrad; - } - - - } \ No newline at end of file +class { + static { + static.scoped = "brad"; + unscoped = "wood" + static foo = 9000; + '123' = 456; + final brad = "wood" + } + + static.foo = 42; + variables.staticBrad = static.brad; + this.thisStaticBrad = variables.staticBrad; + + static { + static.again = "luis" + } + + static function myStaticFunc() { + return "static" & static.foo; + } + + static function getInstance() { + return new src.test.java.TestCases.phase3.StaticTest(); + } + + function myInstanceFunc() { + return "instance" & myStaticFunc(); + } + + array function myInstanceFunc2() { + return [static.scoped, + static.unscoped, + static.foo]; + } + + static function sayHello() { + return "Hello"; + } + + function getStaticBrad() { + return staticBrad; + } + + +} \ No newline at end of file diff --git a/src/test/java/TestCases/phase3/StaticTest2.bx b/src/test/java/TestCases/phase3/StaticTest2.bx new file mode 100644 index 000000000..36be7b45a --- /dev/null +++ b/src/test/java/TestCases/phase3/StaticTest2.bx @@ -0,0 +1,45 @@ +class { + static { + static.scoped = "brad"; + unscoped = "wood" + static foo = 9000; + '123' = 456; + final brad = "wood" + } + + static.foo = 42; + variables.staticBrad = static.brad; + this.thisStaticBrad = variables.staticBrad; + + static { + static.again = "luis" + } + + static function myStaticFunc() { + return "static" & static.foo; + } + + static function getInstance() { + return new src.test.java.TestCases.phase3.StaticTest2(); + } + + function myInstanceFunc() { + return "instance" & myStaticFunc(); + } + + array function myInstanceFunc2() { + return [static.scoped, + static.unscoped, + static.foo]; + } + + static function sayHello() { + return "Hello"; + } + + function getStaticBrad() { + return staticBrad; + } + + +} \ No newline at end of file diff --git a/src/test/java/TestCases/phase3/StaticTestCF.cfc b/src/test/java/TestCases/phase3/StaticTestCF.cfc index 27bcb8cf8..5b5154d93 100644 --- a/src/test/java/TestCases/phase3/StaticTestCF.cfc +++ b/src/test/java/TestCases/phase3/StaticTestCF.cfc @@ -8,6 +8,7 @@ component { static.foo = 42; variables.staticBrad = static.brad; + this.thisStaticBrad = variables.staticBrad; static { static.again = "luis" @@ -25,4 +26,8 @@ component { return staticBrad; } + static function getInstance() { + return new src.test.java.TestCases.phase3.StaticTestCF(); + } + } \ No newline at end of file diff --git a/src/test/java/TestCases/phase3/StaticTestCF2.cfc b/src/test/java/TestCases/phase3/StaticTestCF2.cfc new file mode 100644 index 000000000..e7dbf66e1 --- /dev/null +++ b/src/test/java/TestCases/phase3/StaticTestCF2.cfc @@ -0,0 +1,33 @@ +component { + static { + static.scoped = "brad"; + unscoped = "wood" + static foo = 9000; + final brad = "wood" + } + + static.foo = 42; + variables.staticBrad = static.brad; + this.thisStaticBrad = variables.staticBrad; + + static { + static.again = "luis" + } + + static function myStaticFunc() { + return "static" & static.foo; + } + + function myInstanceFunc() { + return "instance" & myStaticFunc(); + } + + function getStaticBrad() { + return staticBrad; + } + + static function getInstance() { + return new src.test.java.TestCases.phase3.StaticTestCF2(); + } + +} \ No newline at end of file From cf6d770b1de9d9105c43a5f00ff45b8f3570ef77 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Mon, 28 Oct 2024 23:47:21 -0500 Subject: [PATCH 11/33] BL-712 --- .../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 97e1502eb..4dea6d4f8 100644 --- a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java +++ b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java @@ -1183,7 +1183,7 @@ public static MethodRecord discoverMethodHandle( MethodHandle targetHandle; // If we are calling an instance method, but have no instance, and have the dynamicobject reference, then initialize it if there is a no-arg constructor - if ( !Modifier.isStatic( targetMethod.getModifiers() ) && targetInstance == null && dynamicObject != null ) { + if ( targetMethod != null && targetInstance == null && dynamicObject != null && !Modifier.isStatic( targetMethod.getModifiers() ) ) { // Check if the dynamic object has a no-arg constructor try { targetInstance = invokeConstructor( context, targetClass ); From 48444f42eec0487806d709e6b205a1bfd845f097 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Tue, 29 Oct 2024 20:01:51 +0100 Subject: [PATCH 12/33] BL-713 #resolve Global events for Application events --- .../application/BaseApplicationListener.java | 242 +++++++++++++----- .../boxlang/runtime/events/BoxEvent.java | 17 +- 2 files changed, 187 insertions(+), 72 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java index b038ef928..c7829e6a6 100644 --- a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java +++ b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java @@ -44,6 +44,7 @@ import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.scopes.SessionScope; import ortus.boxlang.runtime.services.ApplicationService; +import ortus.boxlang.runtime.services.InterceptorService; import ortus.boxlang.runtime.types.Array; import ortus.boxlang.runtime.types.Function; import ortus.boxlang.runtime.types.IStruct; @@ -68,27 +69,27 @@ public abstract class BaseApplicationListener { /** * The application name */ - protected Key appName = null; + protected Key appName = null; /** * The application linked to this listener */ - protected Application application; + protected Application application; /** * The request context bound to this listener */ - protected RequestBoxContext context; + protected RequestBoxContext context; /** * The listener's interception pool */ - protected InterceptorPool interceptorPool; + protected InterceptorPool interceptorPool; /** * The available request pool interceptors */ - private static final Key[] REQUEST_INTERCEPTION_POINTS = List.of( + private static final Key[] REQUEST_INTERCEPTION_POINTS = List.of( Key.onRequest, Key.onRequestStart, Key.onRequestEnd, @@ -102,14 +103,24 @@ public abstract class BaseApplicationListener { Key.missingTemplate ).toArray( new Key[ 0 ] ); + /** + * Runtime + */ + private static final BoxRuntime runtime = BoxRuntime.getInstance(); + + /** + * Interceptor Service + */ + private static final InterceptorService interceptorService = runtime.getInterceptorService(); + /** * All Application settings (which are really set per-request). This includes any "expected" ones from the BoxLog core, plus any additional settings * that a module or add-on may be looking for. This also determines default values for all settings. *

* You can find the majority of defaults in the {@link Configuration} class. */ - protected IStruct settings = Struct.of( - "applicationTimeout", BoxRuntime.getInstance().getConfiguration().applicationTimeout, + protected IStruct settings = Struct.of( + "applicationTimeout", runtime.getConfiguration().applicationTimeout, // CLIENT WILL BE REMOVED IN BOXLANG // Kept here for now "clientManagement", false, @@ -118,39 +129,39 @@ public abstract class BaseApplicationListener { // END: CLIENT "componentPaths", new Array(), "customTagPaths", new Array(), - "datasource", BoxRuntime.getInstance().getConfiguration().defaultDatasource, - "defaultDatasource", BoxRuntime.getInstance().getConfiguration().defaultDatasource, + "datasource", runtime.getConfiguration().defaultDatasource, + "defaultDatasource", runtime.getConfiguration().defaultDatasource, "datasources", new Struct(), - "invokeImplicitAccessor", BoxRuntime.getInstance().getConfiguration().invokeImplicitAccessor, + "invokeImplicitAccessor", runtime.getConfiguration().invokeImplicitAccessor, "javaSettings", Struct.of( "loadPaths", new Array(), "loadSystemClassPath", false, "reloadOnChange", false ), - "locale", BoxRuntime.getInstance().getConfiguration().locale.toString(), + "locale", runtime.getConfiguration().locale.toString(), "mappings", Struct.of(), - "sessionManagement", BoxRuntime.getInstance().getConfiguration().sessionManagement, - "sessionStorage", BoxRuntime.getInstance().getConfiguration().sessionStorage, - "sessionTimeout", BoxRuntime.getInstance().getConfiguration().sessionTimeout, - "setClientCookies", BoxRuntime.getInstance().getConfiguration().setClientCookies, - "setDomainCookies", BoxRuntime.getInstance().getConfiguration().setDomainCookies, + "sessionManagement", runtime.getConfiguration().sessionManagement, + "sessionStorage", runtime.getConfiguration().sessionStorage, + "sessionTimeout", runtime.getConfiguration().sessionTimeout, + "setClientCookies", runtime.getConfiguration().setClientCookies, + "setDomainCookies", runtime.getConfiguration().setDomainCookies, // These are auto-calculated at runtime "class", "", "name", "", "source", "", // end auto-calculated - "timezone", BoxRuntime.getInstance().getConfiguration().timezone.getId(), + "timezone", runtime.getConfiguration().timezone.getId(), // Stil Considering if they will be core or a module "secureJson", false, "secureJsonPrefix", "", - "allowedFileOperationExtensions", BoxRuntime.getInstance().getConfiguration().security.allowedFileOperationExtensions, - "disallowedFileOperationExtensions", BoxRuntime.getInstance().getConfiguration().security.disallowedFileOperationExtensions + "allowedFileOperationExtensions", runtime.getConfiguration().security.allowedFileOperationExtensions, + "disallowedFileOperationExtensions", runtime.getConfiguration().security.disallowedFileOperationExtensions ); /** * Logger */ - private static final Logger logger = LoggerFactory.getLogger( BaseApplicationListener.class ); + private static final Logger logger = LoggerFactory.getLogger( BaseApplicationListener.class ); /** * -------------------------------------------------------------------------- @@ -471,15 +482,24 @@ public void announce( Key state, IStruct data, IBoxContext appContext ) { */ public void onRequest( IBoxContext context, Object[] args ) { logger.trace( "Fired onRequest ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + // Announce locally this.interceptorPool.announce( Key.onRequest, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.onRequest, + eventArgs, context ); } @@ -494,17 +514,27 @@ public void onRequest( IBoxContext context, Object[] args ) { */ public boolean onRequestStart( IBoxContext context, Object[] args ) { logger.trace( "Fired onRequestStart ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + // Announce locally this.interceptorPool.announce( Key.onRequestStart, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.onRequestStart, + eventArgs, context ); + return true; } @@ -516,15 +546,24 @@ public boolean onRequestStart( IBoxContext context, Object[] args ) { */ public void onRequestEnd( IBoxContext context, Object[] args ) { logger.trace( "Fired onRequestEnd ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + // Announce locally this.interceptorPool.announce( Key.onRequestEnd, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.onRequestEnd, + eventArgs, context ); } @@ -538,14 +577,24 @@ public void onRequestEnd( IBoxContext context, Object[] args ) { public void onAbort( IBoxContext context, Object[] args ) { logger.trace( "Fired onAbort ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + + // Announce locally this.interceptorPool.announce( Key.onAbort, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.onAbort, + eventArgs, context ); } @@ -559,6 +608,14 @@ public void onAbort( IBoxContext context, Object[] args ) { public void onClassRequest( IBoxContext context, Object[] args ) { logger.trace( "Fired onClassRequest ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + + // Announce locally this.interceptorPool.announce( Key.onClassRequest, Struct.of( @@ -569,6 +626,13 @@ public void onClassRequest( IBoxContext context, Object[] args ) { ), context ); + + // Announce globally + interceptorService.announce( + Key.onClassRequest, + eventArgs, + context + ); } /** @@ -698,14 +762,24 @@ protected void classRequestNoMethod( IBoxContext context, String className ) { public void onSessionStart( IBoxContext context, Object[] args ) { logger.trace( "Fired onSessionStart ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + + // Announce locally this.interceptorPool.announce( Key.onSessionStart, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.onSessionStart, + eventArgs, context ); } @@ -719,14 +793,24 @@ public void onSessionStart( IBoxContext context, Object[] args ) { public void onSessionEnd( IBoxContext context, Object[] args ) { logger.trace( "Fired onSessionEnd ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + + // Announce locally this.interceptorPool.announce( Key.onSessionEnd, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.onSessionEnd, + eventArgs, context ); } @@ -784,16 +868,27 @@ public void onApplicationEnd( IBoxContext context, Object[] args ) { public boolean onError( IBoxContext context, Object[] args ) { logger.trace( "Fired onError ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + + // Announce locally this.interceptorPool.announce( Key.onError, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, context ); + + // Announce globally + interceptorService.announce( + Key.onError, + eventArgs, + context + ); + return true; } @@ -808,16 +903,27 @@ public boolean onError( IBoxContext context, Object[] args ) { public boolean onMissingTemplate( IBoxContext context, Object[] args ) { logger.trace( "Fired onMissingTemplate ...................." ); + IStruct eventArgs = Struct.of( + "context", context, + "args", args, + "application", this.application, + "listener", this + ); + + // Announce locally this.interceptorPool.announce( Key.missingTemplate, - Struct.of( - "context", context, - "args", args, - "application", this.application, - "listener", this - ), + eventArgs, + context + ); + + // Announce globally + interceptorService.announce( + Key.missingTemplate, + eventArgs, context ); + return true; } } diff --git a/src/main/java/ortus/boxlang/runtime/events/BoxEvent.java b/src/main/java/ortus/boxlang/runtime/events/BoxEvent.java index f1baeb2ae..462aaab1d 100644 --- a/src/main/java/ortus/boxlang/runtime/events/BoxEvent.java +++ b/src/main/java/ortus/boxlang/runtime/events/BoxEvent.java @@ -65,26 +65,35 @@ public enum BoxEvent { ON_REQUEST_FLUSH_BUFFER( "onRequestFlushBuffer" ), ON_SESSION_CREATED( "onSessionCreated" ), ON_SESSION_DESTROYED( "onSessionDestroyed" ), + ON_REQUEST( "onRequest" ), + ON_REQUEST_START( "onRequestStart" ), + ON_REQUEST_END( "onRequestEnd" ), + ON_CLASS_REQUEST( "onClassRequest" ), + ON_SESSION_START( "onSessionStart" ), + ON_SESSION_END( "onSessionEnd" ), + ON_ERROR( "onError" ), + ON_MISSING_TEMPLATE( "onMissingTemplate" ), + ON_ABORT( "onAbort" ), /** - * Request Events + * Request Context Events */ ON_REQUEST_CONTEXT_CONFIG( "onRequestContextConfig" ), /** - * Template + * Template Invocations */ PRE_TEMPLATE_INVOKE( "preTemplateInvoke" ), POST_TEMPLATE_INVOKE( "postTemplateInvoke" ), /** - * Function Execution Events + * Function Invocations */ PRE_FUNCTION_INVOKE( "preFunctionInvoke" ), POST_FUNCTION_INVOKE( "postFunctionInvoke" ), /** - * Query Events + * Query Invocations */ ON_QUERY_BUILD( "onQueryBuild" ), PRE_QUERY_EXECUTE( "preQueryExecute" ), From dbde9261e08eaecd40532eee6e955c7dc84e7eb0 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 29 Oct 2024 14:40:59 -0500 Subject: [PATCH 13/33] Make method name easier to read --- .../ortus/boxlang/runtime/interop/DynamicInteropService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java index 4dea6d4f8..c7579ea81 100644 --- a/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java +++ b/src/main/java/ortus/boxlang/runtime/interop/DynamicInteropService.java @@ -672,7 +672,7 @@ public static Object invoke( IBoxContext context, DynamicObject dynamicObject, C } catch ( RuntimeException e ) { throw e; } catch ( Throwable e ) { - throw new BoxRuntimeException( "Error invoking method " + methodName + " for class " + targetClass.getName(), e ); + throw new BoxRuntimeException( "Error invoking method " + methodName + "() for class " + targetClass.getName(), e ); } } @@ -714,7 +714,7 @@ public static Object invokeStatic( IBoxContext context, DynamicObject dynamicObj } catch ( RuntimeException e ) { throw e; } catch ( Throwable e ) { - throw new BoxRuntimeException( "Error invoking method " + methodName + " for class " + targetClass.getName(), e ); + throw new BoxRuntimeException( "Error invoking method " + methodName + "() for class " + targetClass.getName(), e ); } } From a73d5af43b7b8f21e2e710d942e99a949471dcae Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 29 Oct 2024 17:19:58 -0500 Subject: [PATCH 14/33] BL-717 --- .../runtime/loader/DynamicClassLoader.java | 32 +++++++++++++++---- .../loader/DynamicClassLoaderTest.java | 10 +++--- src/test/resources/libs/config.properties | 1 + 3 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 src/test/resources/libs/config.properties diff --git a/src/main/java/ortus/boxlang/runtime/loader/DynamicClassLoader.java b/src/main/java/ortus/boxlang/runtime/loader/DynamicClassLoader.java index 2a623fc5d..2f11f3a7b 100644 --- a/src/main/java/ortus/boxlang/runtime/loader/DynamicClassLoader.java +++ b/src/main/java/ortus/boxlang/runtime/loader/DynamicClassLoader.java @@ -75,9 +75,9 @@ public class DynamicClassLoader extends URLClassLoader { private final ConcurrentHashMap> unfoundClasses = new ConcurrentHashMap<>(); /** - * Logger + * Logger. Lazy init to avoid deadlocks on runtime startup */ - private static final Logger logger = LoggerFactory.getLogger( DynamicClassLoader.class ); + private static Logger logger = null; /** * Construct the class loader @@ -157,6 +157,7 @@ public Class findClass( String className ) throws ClassNotFoundException { * @param safe Whether to throw an exception if the class is not found */ public Class findClass( String className, Boolean safe ) throws ClassNotFoundException { + Logger logger = getLogger(); if ( closed ) { throw new BoxRuntimeException( "Class loader [" + nameAsKey.getName() + "] is closed, but you are trying to use it still! Closed by this thread: \n\n" + closedStack ); @@ -396,9 +397,12 @@ public static URL[] getJarURLs( Path targetPath ) throws IOException { // Stream all files recursively, filtering for .jar and .class files try ( Stream fileStream = Files.walk( targetPath ) ) { - return fileStream - .parallel() - .filter( path -> path.toString().endsWith( ".jar" ) || path.toString().endsWith( ".class" ) ) + return Stream.concat( + Stream.of( targetPath ), // Include the directory itself + fileStream + .parallel() + .filter( path -> path.toString().endsWith( ".jar" ) || path.toString().endsWith( ".class" ) ) + ) .map( path -> { try { // Convert Path to URL using toUri() and toURL() @@ -408,6 +412,8 @@ public static URL[] getJarURLs( Path targetPath ) throws IOException { } } ) .toArray( URL[]::new ); + } catch ( IOException e ) { + throw new UncheckedIOException( e ); } } @@ -425,7 +431,7 @@ public static URL[] inflateClassPaths( Array paths ) { .map( path -> { try { Path targetPath = Paths.get( ( String ) path ); - // If this is a directory, then get all the JARs and classes in the directory + // If this is a directory, then get all the JARs and classes in the directory as well as the dir itself // else if it's a jar/class file then just return the URL if ( Files.isDirectory( targetPath ) ) { return getJarURLs( targetPath ); @@ -438,8 +444,20 @@ public static URL[] inflateClassPaths( Array paths ) { } ) .flatMap( Arrays::stream ) .distinct() - // .peek( url -> logger.debug( "Inflated URL: [{}]", url ) ) + // .peek( url -> getLogger().debug( "Inflated URL: [{}]", url ) ) .toArray( URL[]::new ); } + private static Logger getLogger() { + if ( logger == null ) { + synchronized ( DynamicClassLoader.class ) { + if ( logger == null ) { + logger = LoggerFactory.getLogger( DynamicClassLoader.class ); + } + } + } + return logger; + + } + } diff --git a/src/test/java/ortus/boxlang/runtime/loader/DynamicClassLoaderTest.java b/src/test/java/ortus/boxlang/runtime/loader/DynamicClassLoaderTest.java index 6f47bcdc4..36dd80e58 100644 --- a/src/test/java/ortus/boxlang/runtime/loader/DynamicClassLoaderTest.java +++ b/src/test/java/ortus/boxlang/runtime/loader/DynamicClassLoaderTest.java @@ -40,11 +40,13 @@ void testLoadClassSuccessfully() throws ClassNotFoundException, IOException { URL[] urls = DynamicClassLoader.getJarURLs( libPath ); ClassLoader parentClassLoader = getClass().getClassLoader(); DynamicClassLoader dynamicClassLoader = new DynamicClassLoader( Key.of( "TestClassLoader" ), urls, parentClassLoader, false ); - // String targetClass = "com.github.benmanes.caffeine.cache.Caffeine"; - String targetClass = "HelloWorld"; - // When - Class loadedClass = dynamicClassLoader.loadClass( + assertThat( dynamicClassLoader.getURLs() ).hasLength( 3 ); + assertThat( dynamicClassLoader.getResource( "config.properties" ) ).isNotNull(); + + // String targetClass = "com.github.benmanes.caffeine.cache.Caffeine"; + String targetClass = "HelloWorld"; + Class loadedClass = dynamicClassLoader.loadClass( targetClass ); // Replace with an actual test class name diff --git a/src/test/resources/libs/config.properties b/src/test/resources/libs/config.properties new file mode 100644 index 000000000..4d61cc66b --- /dev/null +++ b/src/test/resources/libs/config.properties @@ -0,0 +1 @@ +config=value \ No newline at end of file From 99c53d8c735412880ca24c92e7bacba546ff4b1e Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 29 Oct 2024 17:51:23 -0500 Subject: [PATCH 15/33] BL-716 --- src/main/java/ortus/boxlang/runtime/BoxRuntime.java | 10 ++++++++++ .../ortus/boxlang/runtime/application/Application.java | 4 ++++ .../runtime/application/BaseApplicationListener.java | 3 +++ .../boxlang/runtime/scripting/BoxScriptingEngine.java | 4 ++++ 4 files changed, 21 insertions(+) diff --git a/src/main/java/ortus/boxlang/runtime/BoxRuntime.java b/src/main/java/ortus/boxlang/runtime/BoxRuntime.java index f37b80ef1..5308a95ac 100644 --- a/src/main/java/ortus/boxlang/runtime/BoxRuntime.java +++ b/src/main/java/ortus/boxlang/runtime/BoxRuntime.java @@ -1164,6 +1164,7 @@ public void executeClass( Class targetClass, String templatePath, // Does it have a main method? if ( target.getThisScope().containsKey( Key.main ) ) { RequestBoxContext.setCurrent( scriptingContext.getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); // Fire!!! try { boolean result = listener.onRequestStart( scriptingContext, new Object[] { templatePath } ); @@ -1217,6 +1218,7 @@ public void executeClass( Class targetClass, String templatePath, } scriptingContext.flushBuffer( false ); RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } } else { throw new BoxRuntimeException( "Class [" + targetClass.getName() + "] does not have a main method to execute." ); @@ -1238,6 +1240,7 @@ public void executeTemplate( BoxTemplate template, IBoxContext context ) { BaseApplicationListener listener = scriptingContext.getParentOfType( RequestBoxContext.class ).getApplicationListener(); Throwable errorToHandle = null; RequestBoxContext.setCurrent( scriptingContext.getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); try { boolean result = listener.onRequestStart( scriptingContext, new Object[] { templatePath } ); if ( result ) { @@ -1292,6 +1295,7 @@ public void executeTemplate( BoxTemplate template, IBoxContext context ) { } scriptingContext.flushBuffer( false ); RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } } @@ -1345,6 +1349,7 @@ public Object executeStatement( String source, IBoxContext context ) { public Object executeStatement( BoxScript scriptRunnable, IBoxContext context ) { IBoxContext scriptingContext = ensureRequestTypeContext( context ); RequestBoxContext.setCurrent( scriptingContext.getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); try { // Fire!!! return scriptRunnable.invoke( scriptingContext ); @@ -1358,6 +1363,7 @@ public Object executeStatement( BoxScript scriptRunnable, IBoxContext context ) } finally { scriptingContext.flushBuffer( false ); RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } } @@ -1411,6 +1417,7 @@ public Object executeSource( String source, IBoxContext context, BoxSourceType t Object results = null; RequestBoxContext.setCurrent( scriptingContext.getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); try { // Fire!!! results = scriptRunnable.invoke( scriptingContext ); @@ -1423,6 +1430,7 @@ public Object executeSource( String source, IBoxContext context, BoxSourceType t } finally { scriptingContext.flushBuffer( false ); RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } return results; @@ -1439,6 +1447,7 @@ public Object executeSource( InputStream sourceStream, IBoxContext context ) { BufferedReader reader = new BufferedReader( new InputStreamReader( sourceStream ) ); String source; RequestBoxContext.setCurrent( scriptingContext.getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); try { Boolean quiet = reader.ready(); @@ -1500,6 +1509,7 @@ public Object executeSource( InputStream sourceStream, IBoxContext context ) { throw new BoxRuntimeException( "Error reading source stream", e ); } finally { RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } return null; diff --git a/src/main/java/ortus/boxlang/runtime/application/Application.java b/src/main/java/ortus/boxlang/runtime/application/Application.java index cedcd08ec..579c37f53 100644 --- a/src/main/java/ortus/boxlang/runtime/application/Application.java +++ b/src/main/java/ortus/boxlang/runtime/application/Application.java @@ -214,6 +214,8 @@ public void startupClassLoaderPaths( ApplicationBoxContext appContext ) { // if we don't have any return out if ( loadPathsUrls.length == 0 ) { + // If there are no javasettings, ensure we just use the runtime CL + Thread.currentThread().setContextClassLoader( BoxRuntime.getInstance().getRuntimeLoader() ); return; } @@ -225,6 +227,8 @@ public void startupClassLoaderPaths( ApplicationBoxContext appContext ) { logger.debug( "Application ClassLoader [{}] registered with these paths: [{}]", this.name, Arrays.toString( loadPathsUrls ) ); return new DynamicClassLoader( this.name, loadPathsUrls, BoxRuntime.getInstance().getRuntimeLoader(), false ); } ); + // Make sure our thread is using the right class loader + Thread.currentThread().setContextClassLoader( this.classLoaders.get( loaderCacheKey ) ); } /** diff --git a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java index c7829e6a6..103b6d91c 100644 --- a/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java +++ b/src/main/java/ortus/boxlang/runtime/application/BaseApplicationListener.java @@ -179,6 +179,9 @@ protected BaseApplicationListener( RequestBoxContext context ) { context.setApplicationListener( this ); this.interceptorPool = new InterceptorPool( Key.appListener, BoxRuntime.getInstance() ) .registerInterceptionPoint( REQUEST_INTERCEPTION_POINTS ); + + // Ensure our thread is at least using the runtime CL. If there is an application defined later, this may get updated to a more specific request CL. + Thread.currentThread().setContextClassLoader( BoxRuntime.getInstance().getRuntimeLoader() ); } /** diff --git a/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingEngine.java b/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingEngine.java index 44d82eb9d..d6984cbe1 100644 --- a/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingEngine.java +++ b/src/main/java/ortus/boxlang/runtime/scripting/BoxScriptingEngine.java @@ -316,11 +316,13 @@ public Object invokeMethod( Object thiz, String name, Object... args ) throws Sc } RequestBoxContext.setCurrent( getBoxContext().getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); try { // This will handle any sort of referencable object, including member methods on data types return Referencer.getAndInvoke( getBoxContext(), thiz, Key.of( name ), args, false ); } finally { RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } } @@ -338,10 +340,12 @@ public Object invokeMethod( Object thiz, String name, Object... args ) throws Sc @Override public Object invokeFunction( String name, Object... args ) throws ScriptException, NoSuchMethodException { RequestBoxContext.setCurrent( boxContext.getParentOfType( RequestBoxContext.class ) ); + ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); try { return boxContext.invokeFunction( Key.of( name ), args ); } finally { RequestBoxContext.removeCurrent(); + Thread.currentThread().setContextClassLoader( oldClassLoader ); } } From 93cf12b6477bbd1942027bb29feac7537fb49969 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 29 Oct 2024 22:33:39 -0500 Subject: [PATCH 16/33] BL-718 --- .../bifs/global/struct/StructFind.java | 4 +-- .../bifs/global/struct/StructFindKey.java | 4 +-- .../bifs/global/struct/StructInsert.java | 4 +-- .../bifs/global/struct/StructKeyExists.java | 4 +-- .../bifs/global/struct/StructUpdate.java | 2 +- .../runtime/types/StructMapWrapper.java | 10 ++---- .../java/TestCases/phase1/CoreLangTest.java | 32 +++++++++++++++++++ 7 files changed, 44 insertions(+), 16 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFind.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFind.java index 46ac39a18..22fcd3fcf 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFind.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFind.java @@ -42,7 +42,7 @@ public StructFind() { super(); declaredArguments = new Argument[] { new Argument( true, "structloose", Key.struct ), - new Argument( true, "string", Key.key ), + new Argument( true, "any", Key.key ), new Argument( false, "any", Key.defaultValue ) }; } @@ -61,7 +61,7 @@ public StructFind() { */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { IStruct struct = arguments.getAsStruct( Key.struct ); - Key searchKey = Key.of( arguments.getAsString( Key.key ) ); + Key searchKey = Key.of( arguments.get( Key.key ) ); Object defaultValue = arguments.get( Key.defaultValue ); if ( !struct.containsKey( searchKey ) ) { if ( defaultValue != null ) { diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFindKey.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFindKey.java index c3f354ed7..6f65a8968 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFindKey.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructFindKey.java @@ -46,7 +46,7 @@ public StructFindKey() { super(); declaredArguments = new Argument[] { new Argument( true, "structloose", Key.struct ), - new Argument( true, "string", Key.key ), + new Argument( true, "any", Key.key ), new Argument( false, "string", Key.scope, "one" ) }; } @@ -64,7 +64,7 @@ public StructFindKey() { * @argument.scope Either one (default), which finds the first instance or all to return all values */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - Key scopeKey = Key.of( arguments.getAsString( Key.scope ) ); + Key scopeKey = Key.of( arguments.get( Key.scope ) ); Stream searchStream = StructUtil.findKey( arguments.getAsStruct( Key.struct ), arguments.getAsString( Key.key ) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructInsert.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructInsert.java index a78a91fdb..0d38c2991 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructInsert.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructInsert.java @@ -40,7 +40,7 @@ public StructInsert() { super(); declaredArguments = new Argument[] { new Argument( true, "modifiableStruct", Key.struct ), - new Argument( true, "string", Key.key ), + new Argument( true, "any", Key.key ), new Argument( true, "any", Key.value ), new Argument( false, "boolean", Key.overwrite, false ) }; @@ -62,7 +62,7 @@ public StructInsert() { */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { IStruct struct = arguments.getAsStruct( Key.struct ); - Key key = Key.of( arguments.getAsString( Key.key ) ); + Key key = Key.of( arguments.get( Key.key ) ); Boolean overwrite = arguments.getAsBoolean( Key.overwrite ); if ( !overwrite && struct.containsKey( key ) ) { diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java index e36ce39cb..6f254865b 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java @@ -38,7 +38,7 @@ public StructKeyExists() { super(); declaredArguments = new Argument[] { new Argument( true, "structloose", Key.struct ), - new Argument( true, "string", Key.key ) + new Argument( true, "any", Key.key ) }; } @@ -53,7 +53,7 @@ public StructKeyExists() { * @argument.key The key within the struct to test for existence */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - return arguments.getAsStruct( Key.struct ).containsKey( Key.of( arguments.getAsString( Key.key ) ) ); + return arguments.getAsStruct( Key.struct ).containsKey( Key.of( arguments.get( Key.key ) ) ); } } diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructUpdate.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructUpdate.java index cca6465f0..ddbea1a6c 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructUpdate.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructUpdate.java @@ -39,7 +39,7 @@ public StructUpdate() { super(); declaredArguments = new Argument[] { new Argument( true, "modifiableStruct", Key.struct ), - new Argument( true, "string", Key.key ), + new Argument( true, "any", Key.key ), new Argument( true, "any", Key.value ) }; } diff --git a/src/main/java/ortus/boxlang/runtime/types/StructMapWrapper.java b/src/main/java/ortus/boxlang/runtime/types/StructMapWrapper.java index 7e038be96..31635e54e 100644 --- a/src/main/java/ortus/boxlang/runtime/types/StructMapWrapper.java +++ b/src/main/java/ortus/boxlang/runtime/types/StructMapWrapper.java @@ -34,13 +34,11 @@ import ortus.boxlang.runtime.context.ClassBoxContext; import ortus.boxlang.runtime.context.FunctionBoxContext; import ortus.boxlang.runtime.context.IBoxContext; -import ortus.boxlang.runtime.dynamic.casters.KeyCaster; import ortus.boxlang.runtime.dynamic.casters.StringCaster; import ortus.boxlang.runtime.interop.DynamicInteropService; import ortus.boxlang.runtime.runnables.BoxInterface; import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.Key; -import ortus.boxlang.runtime.scopes.KeyCased; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; import ortus.boxlang.runtime.types.exceptions.KeyNotFoundException; import ortus.boxlang.runtime.types.meta.BoxMeta; @@ -293,7 +291,7 @@ public Object getOrDefault( String key, Object defaultValue ) { * @return The value of the key or a NullValue object, null means the key didn't exist * */ public Object getRaw( Key key ) { - return wrapped.get( key ); + return wrapped.get( key.getOriginalValue() ); } @@ -341,7 +339,7 @@ public Object put( String key, Object value ) { public Object putIfAbsent( Key key, Object value ) { if ( !containsKey( key ) ) { return wrapped.putIfAbsent( - isCaseSensitive() && ! ( key instanceof KeyCased ) ? new KeyCased( key.getName() ) : key, + key.getOriginalValue(), notifyListeners( key, value ) ); } @@ -392,9 +390,7 @@ public Object remove( String key ) { */ public Object remove( Key key ) { notifyListeners( key, null ); - return isCaseSensitive() - ? wrapped.remove( keySet().stream().filter( k -> KeyCaster.cast( k ).equalsWithCase( key ) ).findFirst().orElse( Key.EMPTY ) ) - : wrapped.remove( key ); + return wrapped.remove( key.getOriginalValue() ); } /** diff --git a/src/test/java/TestCases/phase1/CoreLangTest.java b/src/test/java/TestCases/phase1/CoreLangTest.java index a1d2e9873..3f31a8e36 100644 --- a/src/test/java/TestCases/phase1/CoreLangTest.java +++ b/src/test/java/TestCases/phase1/CoreLangTest.java @@ -3630,4 +3630,36 @@ public void testAutoInitDynamicObjectOnInstanceMethodCall() { assertThat( list.get( 0 ) ).isEqualTo( "foo" ); } + @DisplayName( "key access in struct map wrapper" ) + @Test + public void testKeyAccessInStructMapWrapper() { + // @formatter:off + instance.executeSource( + """ + myMap = CreateObject("java","java.util.concurrent.ConcurrentHashMap").init() castas "struct"; + myMap["coldbox"] = "rocks"; + result = myMap["coldbox"]; + myMap.foo = "bar" + result2 = myMap.foo; + result3 = structKeyExists( myMap, "coldbox" ); + crayCrayKey = ['whoo-hoo']; + myMap[ crayCrayKey ] = "y'all gonna make me lose my mind"; + result4 = myMap[ crayCrayKey ]; + result5 = structKeyExists( myMap, crayCrayKey ); + myMap.delete( "coldbox" ); + result6 = structKeyExists( myMap, "coldbox" ); + structUpdate( myMap, "foo", "baz" ); + result7 = myMap.foo; + """, + context ); + // @formatter:on + assertThat( variables.get( result ) ).isEqualTo( "rocks" ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( "bar" ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "result4" ) ) ).isEqualTo( "y'all gonna make me lose my mind" ); + assertThat( variables.get( Key.of( "result5" ) ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "result6" ) ) ).isEqualTo( false ); + assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "baz" ); + } + } From 228a5d19941d016ac48f6599ce1f85629c9f6ccb Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Tue, 29 Oct 2024 23:27:51 -0500 Subject: [PATCH 17/33] BL-715 --- .../boxlang/runtime/util/DuplicationUtil.java | 40 ++++++++- src/test/java/TestCases/phase3/ClassTest.java | 2 + src/test/java/TestCases/phase3/MyClass.bx | 1 + .../bifs/global/system/DuplicateTest.java | 82 +++++++++++++++++++ 4 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java b/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java index f770ccf98..89f92661d 100644 --- a/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java +++ b/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java @@ -18,6 +18,7 @@ package ortus.boxlang.runtime.util; import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; import java.util.LinkedHashMap; import java.util.Map.Entry; import java.util.concurrent.ConcurrentSkipListMap; @@ -29,6 +30,7 @@ import ortus.boxlang.runtime.bifs.global.type.NullValue; import ortus.boxlang.runtime.dynamic.casters.ArrayCaster; import ortus.boxlang.runtime.dynamic.casters.StructCaster; +import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.types.Array; import ortus.boxlang.runtime.types.DateTime; @@ -61,6 +63,8 @@ public static Object duplicate( Object target, Boolean deep ) { return target; } else if ( target instanceof Enum || target instanceof Class ) { return target; + } else if ( target instanceof IClassRunnable icr ) { + return duplicateClass( icr, deep ); } else if ( target instanceof IStruct str ) { return duplicateStruct( str, deep ); } else if ( target instanceof Array arr ) { @@ -88,6 +92,40 @@ public static Object duplicate( Object target, Boolean deep ) { } } + private static IClassRunnable duplicateClass( IClassRunnable originalClass, Boolean deep ) { + IClassRunnable newClass; + try { + newClass = originalClass.getClass().getConstructor().newInstance(); + } catch ( InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException + | SecurityException e ) { + throw new BoxRuntimeException( "An exception occurred while duplicating the class", e ); + } + // variables scope + if ( deep ) { + newClass.getVariablesScope().putAll( duplicateStruct( originalClass.getVariablesScope(), deep ) ); + } else { + newClass.getVariablesScope().putAll( originalClass.getVariablesScope() ); + } + // this scope + if ( deep ) { + newClass.getThisScope().putAll( duplicateStruct( originalClass.getThisScope(), deep ) ); + } else { + newClass.getThisScope().putAll( originalClass.getThisScope() ); + } + // super scope + if ( originalClass.getSuper() != null ) { + newClass._setSuper( duplicateClass( originalClass.getSuper(), deep ) ); + } + // child + if ( originalClass.getChild() != null ) { + newClass.setChild( duplicateClass( originalClass.getChild(), deep ) ); + } + // interfaces + newClass.getInterfaces().addAll( originalClass.getInterfaces() ); + + return newClass; + } + /** * Duplicate a Struct object * @@ -96,7 +134,7 @@ public static Object duplicate( Object target, Boolean deep ) { * * @return A new Struct copy */ - public static Struct duplicateStruct( IStruct target, Boolean deep ) { + public static IStruct duplicateStruct( IStruct target, Boolean deep ) { var entries = target.entrySet().stream(); if ( target.getType().equals( Struct.TYPES.LINKED ) ) { diff --git a/src/test/java/TestCases/phase3/ClassTest.java b/src/test/java/TestCases/phase3/ClassTest.java index a29a27098..5e0df94d0 100644 --- a/src/test/java/TestCases/phase3/ClassTest.java +++ b/src/test/java/TestCases/phase3/ClassTest.java @@ -386,6 +386,8 @@ public void testBasicClassFile() { // Can call public methods on this assert cfc.runThisFoo() == "I work! whee true true bar true"; + + assert cfc.thisVar == "thisValue"; """, context ); // @formatter:on } diff --git a/src/test/java/TestCases/phase3/MyClass.bx b/src/test/java/TestCases/phase3/MyClass.bx index 189424a13..bb1cf1a4b 100644 --- a/src/test/java/TestCases/phase3/MyClass.bx +++ b/src/test/java/TestCases/phase3/MyClass.bx @@ -20,6 +20,7 @@ class singleton gavin="pickin" inject { /**@test*/ property name; + this.thisVar = "thisValue"; variables.setup=true; // ending comment System.out.println( "word" ); request.foo="bar"; 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 c1bd0f5d1..f7ec7b19a 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 @@ -459,6 +459,88 @@ public void testDuplicateNull() { assertNull( variables.get( resultKey ) ); } + @DisplayName( "It can duplicate a class" ) + @Test + public void testDuplicateAClass() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.MyClass(); + clazz.complex = [ foo : "bar" ] + request.foo = "bar2" + clazz2 = duplicate( clazz, false ); + clazz2.complex.baz = "bum" + // struct is shared by reference + assert structKeyList( clazz.complex ) == "foo,baz"; + assert structKeyList( clazz2.complex ) == "foo,baz"; + + // execute public method + result = clazz2.foo(); + + // private methods error + try { + clazz2.bar() + assert false; + } catch( BoxRuntimeException e ) { + assert e.message contains "bar"; + } + + // pseduoconstructor should NOT run again + assert request.foo == "bar2" + + // Can call public method that accesses private method, and variables, and request scope + assert result == "I work! whee true true bar2 true"; + + // This scope is reference to actual CFC instance + assert clazz2.$bx.$class.getName() == clazz2.getThis().$bx.$class.getName(); + + // Can call public methods on this + assert clazz2.runThisFoo() == "I work! whee true true bar2 true"; + + assert clazz2.thisVar == "thisValue"; + """, + context ); + } + + @DisplayName( "It can duplicate a class deeply" ) + @Test + public void testDuplicateAClassDeeply() { + instance.executeSource( + """ + clazz = new src.test.java.TestCases.phase3.MyClass(); + clazz.complex = [ foo : "bar" ] + request.foo = "bar2" + clazz2 = duplicate( clazz, true ); + clazz2.complex.baz = "bum" + assert structKeyList( clazz.complex ) == "foo"; + assert structKeyList( clazz2.complex ) == "foo,baz"; + // execute public method + result = clazz2.foo(); + + // private methods error + try { + clazz2.bar() + assert false; + } catch( BoxRuntimeException e ) { + assert e.message contains "bar"; + } + + // pseduoconstructor should NOT run again + assert request.foo == "bar2" + + // Can call public method that accesses private method, and variables, and request scope + assert result == "I work! whee true true bar2 true"; + + // This scope is reference to actual CFC instance + assert clazz2.$bx.$class.getName() == clazz2.getThis().$bx.$class.getName(); + + // Can call public methods on this + assert clazz2.runThisFoo() == "I work! whee true true bar2 true"; + + assert clazz2.thisVar == "thisValue"; + """, + context ); + } + @Disabled( "Performance benchmark test on a struct" ) @Test public void benchmarkStruct() { From d2e3131a20fa28785c6f818897fb6b388d553783 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 30 Oct 2024 18:24:11 +0100 Subject: [PATCH 18/33] BL-720 #resolve Implement table filter for dbinfo component --- .../runtime/components/jdbc/DBInfo.java | 110 ++++++++++++++---- .../runtime/components/jdbc/DBInfoTest.java | 23 +++- 2 files changed, 107 insertions(+), 26 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java b/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java index ef39a15a4..653a339c1 100644 --- a/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java +++ b/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java @@ -45,6 +45,7 @@ import ortus.boxlang.runtime.types.QueryColumnType; import ortus.boxlang.runtime.types.Struct; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; +import ortus.boxlang.runtime.types.exceptions.BoxValidationException; import ortus.boxlang.runtime.types.exceptions.DatabaseException; import ortus.boxlang.runtime.validation.Validator; @@ -96,21 +97,23 @@ public DBInfo() { new Attribute( Key.table, "string" ), new Attribute( Key.pattern, "string" ), new Attribute( Key.dbname, "string" ), + new Attribute( Key.filter, "string" ), + + // We probably will not implement these. We can remove later if we decide not to. new Attribute( Key.username, "string", Set.of( Validator.NOT_IMPLEMENTED ) ), new Attribute( Key.password, "string", Set.of( Validator.NOT_IMPLEMENTED ) ), - new Attribute( Key.filter, "string", Set.of( - Validator.NOT_IMPLEMENTED - ) ) }; } /** - * Retrieve database metadata for a given datasource. + * Retrieve database metadata for a given datasource. This can include: column metadata, database names, table names, foreign keys, index info, + * stored procedures, and version info. *

+ * Please note that the type attribute is required, and the name attribute is required to store the result. * * @attribute.type Type of metadata to retrieve. One of: `columns`, `dbnames`, `tables`, `foreignkeys`, `index`, `procedures`, or `version`. * @@ -120,23 +123,28 @@ public DBInfo() { * * @attribute.datasource Name of the datasource to check metadata on. If not provided, the default datasource will be used. * - * @attribute.username Not currently implemented. - * - * @attribute.password Not currently implemented. - * - * @attribute.filter A lucee-only attribute to perform additional filtering on type="tables" results. Not currently implemented, as this - * should be performed by a queryFilter() call. + * @attribute.filter This is a string value that must match a table type in your database implementation. Each database is different. + * Some common filter types are: + *

    + *
  • TABLE - This is the default value and will return only tables.
  • + *
  • VIEW - This will return only views.
  • + *
  • SYSTEM TABLE - This will return only system tables.
  • + *
  • GLOBAL TEMPORARY - This will return only global temporary tables.
  • + *
  • LOCAL TEMPORARY - This will return only local temporary tables.
  • + *
  • ALIAS - This will return only aliases.
  • + *
  • SYNONYM - This will return only synonyms.
  • + *
* * @param context The context in which the Component is being invoked * @param attributes The attributes to the Component * @param body The body of the Component * @param executionState The execution state of the Component - * */ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBody body, IStruct executionState ) { IJDBCCapableContext jdbcContext = context.getParentOfType( IJDBCCapableContext.class ); ConnectionManager connectionManager = jdbcContext.getConnectionManager(); + // Prep arguments DataSource datasource = attributes.containsKey( Key.datasource ) ? connectionManager.getDatasourceOrThrow( Key.of( attributes.getAsString( Key.datasource ) ) ) : connectionManager.getDefaultDatasourceOrThrow(); @@ -144,33 +152,47 @@ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBod if ( tableNameLookup == null ) { tableNameLookup = attributes.getAsString( Key.pattern ); } + DBInfoType type = DBInfoType.fromString( attributes.getAsString( Key.type ) ); + String filter = attributes.getAsString( Key.filter ); - DBInfoType type = DBInfoType.fromString( attributes.getAsString( Key.type ) ); + // If the filter is set, the type must be "tables" + if ( filter != null && !filter.isEmpty() && !type.equals( DBInfoType.TABLES ) ) { + throw new BoxValidationException( "The 'filter' attribute can only be used with the 'tables' type" ); + } + // Now that we have the datasource, we can get the connection and metadata by type try ( Connection conn = datasource.getConnection(); ) { DatabaseMetaData databaseMetadata = conn.getMetaData(); - tableNameLookup = normalizeTableNameCasing( databaseMetadata, tableNameLookup ); String databaseName = attributes.getAsString( Key.dbname ); + if ( databaseName == null ) { // Specify database name in a dot-delimited string, i.e. "mDB.mySchema.tblUsers". databaseName = parseDatabaseFromTableName( tableNameLookup ); } + if ( databaseName == null ) { // Default to the database name set on the connection (provided by the datasource config). databaseName = getDatabaseNameFromConnection( conn ); } + String schema = parseSchemaFromTableName( tableNameLookup ); String tableName = parseTableName( tableNameLookup ); - Query result = ( switch ( type ) { - case DBNAMES -> getDbNames( databaseMetadata ); - case VERSION -> getVersion( databaseMetadata ); - case COLUMNS -> getColumnsForTable( databaseMetadata, databaseName, schema, tableName ); - case TABLES -> getTables( databaseMetadata, databaseName, schema, tableName ); - case FOREIGNKEYS -> getForeignKeys( databaseMetadata, databaseName, schema, tableName ); - case INDEX -> getIndexes( databaseMetadata, databaseName, schema, tableName ); - case PROCEDURES -> getProcedures( databaseMetadata, databaseName, schema, tableName ); - } ); + + // If the filter is not null and not empty, validate it at runtime against the valid database metadata filter types + if ( filter != null && !filter.isEmpty() ) { + validateFilter( filter, databaseMetadata.getTableTypes() ); + } + + Query result = ( switch ( type ) { + case DBNAMES -> getDbNames( databaseMetadata ); + case VERSION -> getVersion( databaseMetadata ); + case COLUMNS -> getColumnsForTable( databaseMetadata, databaseName, schema, tableName ); + case TABLES -> getTables( databaseMetadata, databaseName, schema, tableName, filter ); + case FOREIGNKEYS -> getForeignKeys( databaseMetadata, databaseName, schema, tableName ); + case INDEX -> getIndexes( databaseMetadata, databaseName, schema, tableName ); + case PROCEDURES -> getProcedures( databaseMetadata, databaseName, schema, tableName ); + } ); ExpressionInterpreter.setVariable( context, attributes.getAsString( Key._NAME ), result ); } catch ( SQLException e ) { throw new DatabaseException( "Unable to read " + attributes.getAsString( Key.type ) + " metadata", e ); @@ -178,6 +200,40 @@ public BodyResult _invoke( IBoxContext context, IStruct attributes, ComponentBod return DEFAULT_RETURN; } + /** + * Validate the filter attribute against the valid database metadata filter types. + * + * @param filter The filter value to validate. + * @param tableTypes ResultSet of valid table types from the database metadata. + * + * @throws BoxValidationException If the filter value is not found in the table types ResultSet. + */ + private void validateFilter( String filter, ResultSet tableTypes ) { + List allowedTypes = new ArrayList<>(); + + try ( tableTypes ) { + allowedTypes = new ArrayList<>(); + boolean validType = false; + + while ( tableTypes.next() ) { + String type = tableTypes.getString( 1 ); + allowedTypes.add( type ); + if ( type.equals( filter ) ) { + validType = true; + break; + } + } + + if ( !validType ) { + throw new BoxValidationException( + "Invalid [dbinfo] type=table filter [" + filter + "]. Supported table types are " + String.join( ", ", allowedTypes ) + "." + ); + } + } catch ( SQLException e ) { + throw new DatabaseException( "Error retrieving table types.", e ); + } + } + /** * Build a Query object of database names for both SCHEMA and CATALOG types. * @@ -321,13 +377,19 @@ private Query getColumnsForTable( DatabaseMetaData databaseMetadata, String data * @param databaseName Name of the database to check for tables. If not provided, the database name from the connection will be used. * @param tableName Optional pattern to filter table names by. Can use wildcards or any `LIKE`-compatible pattern such as `tbl_%`. Can use * `schemaName.tableName` syntax to additionally filter by schema. + * @param filter Optional filter to apply to the table type. Can be `TABLE`, `VIEW`, `SYSTEM TABLE`, `GLOBAL TEMPORARY`, `LOCAL TEMPORARY`, * * @return Query object where each row represents a table in the provided database. */ - private Query getTables( DatabaseMetaData databaseMetadata, String databaseName, String schema, String tableName ) throws SQLException { + private Query getTables( + DatabaseMetaData databaseMetadata, + String databaseName, + String schema, + String tableName, + String filter ) throws SQLException { Query result = new Query(); - try ( ResultSet resultSet = databaseMetadata.getTables( databaseName, schema, tableName, null ) ) { + try ( ResultSet resultSet = databaseMetadata.getTables( databaseName, schema, tableName, new String[] { filter } ) ) { ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); buildQueryColumns( result, resultSetMetaData ); diff --git a/src/test/java/ortus/boxlang/runtime/components/jdbc/DBInfoTest.java b/src/test/java/ortus/boxlang/runtime/components/jdbc/DBInfoTest.java index 36610f053..15b418c6b 100644 --- a/src/test/java/ortus/boxlang/runtime/components/jdbc/DBInfoTest.java +++ b/src/test/java/ortus/boxlang/runtime/components/jdbc/DBInfoTest.java @@ -19,8 +19,8 @@ package ortus.boxlang.runtime.components.jdbc; -import static org.junit.Assert.assertNotNull; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -28,10 +28,10 @@ import java.util.Map; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledIf; -import org.junit.jupiter.api.Disabled; import ortus.boxlang.compiler.parser.BoxSourceType; import ortus.boxlang.runtime.bifs.global.jdbc.BaseJDBCTest; @@ -216,6 +216,25 @@ public void testColumnsTypeWithNonExistentTable() { () -> getInstance().executeSource( "cfdbinfo( type='columns', name='result', table='404NotFound' )", getContext(), BoxSourceType.CFSCRIPT ) ); } + @DisplayName( "Can get db tables with a filter, but the type is NOT tables, so it must throw an exception" ) + @Test + public void testTablesTypeWithFilterAndBadType() { + assertThrows( BoxValidationException.class, + () -> getInstance().executeSource( "cfdbinfo( type='columns', name='result', filter='TABLE' )", getContext(), BoxSourceType.CFSCRIPT ) ); + } + + @DisplayName( "Can get db tables with a basic filter of 'TABLE'" ) + @Test + public void testTablesTypeWithFilter() { + getInstance().executeSource( + """ + cfdbinfo( type='tables', name='result', filter='TABLE' ) + """, + getContext(), BoxSourceType.CFSCRIPT ); + Query theResult = getVariables().getAsQuery( result ); + assertThat( theResult.size() ).isGreaterThan( 0 ); + } + @DisplayName( "Can get db tables when type=tables" ) @Test public void testTablesType() { From 9d7e34998ade721bd6096e2261b2621e450e5d7a Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Wed, 30 Oct 2024 12:34:00 -0500 Subject: [PATCH 19/33] BL-715 --- .../boxlang/runtime/util/DuplicationUtil.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java b/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java index 89f92661d..aa42165e0 100644 --- a/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java +++ b/src/main/java/ortus/boxlang/runtime/util/DuplicationUtil.java @@ -100,27 +100,30 @@ private static IClassRunnable duplicateClass( IClassRunnable originalClass, Bool | SecurityException e ) { throw new BoxRuntimeException( "An exception occurred while duplicating the class", e ); } + // variables scope if ( deep ) { newClass.getVariablesScope().putAll( duplicateStruct( originalClass.getVariablesScope(), deep ) ); } else { newClass.getVariablesScope().putAll( originalClass.getVariablesScope() ); } + // this scope if ( deep ) { newClass.getThisScope().putAll( duplicateStruct( originalClass.getThisScope(), deep ) ); } else { newClass.getThisScope().putAll( originalClass.getThisScope() ); } + // super scope if ( originalClass.getSuper() != null ) { - newClass._setSuper( duplicateClass( originalClass.getSuper(), deep ) ); - } - // child - if ( originalClass.getChild() != null ) { - newClass.setChild( duplicateClass( originalClass.getChild(), deep ) ); + IClassRunnable newSuper = duplicateClass( originalClass.getSuper(), deep ); + newSuper.setChild( newClass ); + newClass._setSuper( newSuper ); + } - // interfaces + + // interfaces (these are singletons with no instance state, so nothing to really duplicate) newClass.getInterfaces().addAll( originalClass.getInterfaces() ); return newClass; From 61bef1646d3f5fbe1dea9adebb578bf812c504f4 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 30 Oct 2024 18:39:46 +0100 Subject: [PATCH 20/33] BL-721 #resolve getComponentList() should also return the declared attributes of a component --- .../bifs/global/system/GetComponentList.java | 3 ++- .../components/ComponentDescriptor.java | 24 ++++++++++++++++++- .../global/system/GetComponentListTest.java | 16 +++++-------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetComponentList.java b/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetComponentList.java index 7c4cb5778..832a8c6d1 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetComponentList.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetComponentList.java @@ -57,7 +57,8 @@ public IStruct _invoke( IBoxContext context, ArgumentsScope arguments ) { "module", descriptor.hasModule() ? descriptor.module : "---", "className", descriptor.componentClass.getCanonicalName(), "allowsBody", descriptor.allowsBody(), - "requiresBody", descriptor.requiresBody() + "requiresBody", descriptor.requiresBody(), + "attributes", descriptor.getAttributes() ) ); } ); diff --git a/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java b/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java index 230ab06f3..7ec41e531 100644 --- a/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java +++ b/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java @@ -18,6 +18,7 @@ package ortus.boxlang.runtime.components; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -134,7 +135,7 @@ public Component getComponent() { this.componentInstance = ( ( Component ) DynamicObject.of( this.componentClass ) .invokeConstructor( ( IBoxContext ) null, new Object[] {} ) .getTargetInstance() ) - .setName( name ); + .setName( name ); interceptorService.announce( BoxEvent.ON_COMPONENT_INSTANCE, new Struct( @@ -154,6 +155,27 @@ public Component getComponent() { return this.componentInstance; } + /** + * Get a struct of the attributes the component offers + * + * @return The struct of attributes + */ + public IStruct getAttributes() { + IStruct attributeStruct = new Struct( Struct.TYPES.LINKED ); + Arrays.stream( getComponent().getDeclaredAttributes() ) + .forEach( attribute -> { + attributeStruct.put( + attribute.name(), + Struct.of( + "type", attribute.type(), + "defaultValue", attribute.defaultValue(), + "validators", attribute.validators() + ) + ); + } ); + return attributeStruct; + } + /** * Invoke the component with no arguments * diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetComponentListTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetComponentListTest.java index 12bb2ae58..9b1906062 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetComponentListTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetComponentListTest.java @@ -19,7 +19,6 @@ 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; @@ -45,11 +44,6 @@ public static void setUp() { instance = BoxRuntime.getInstance( true ); } - @AfterAll - public static void teardown() { - - } - @BeforeEach public void setupEach() { context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); @@ -60,11 +54,13 @@ public void setupEach() { @Test public void testGetComponentList() { + // @formatter:off instance.executeSource( - """ - result = getComponentList() - """, - context ); + """ + result = getComponentList() + """, + context ); + // @formatter:on System.out.println( variables.get( result ) ); IStruct functions = ( IStruct ) variables.get( result ); From 9142ffdeb4af6a4a11ff55204f60f01e4ece1db3 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 30 Oct 2024 18:44:12 +0100 Subject: [PATCH 21/33] BL-722 #resolve getFunctionList() should also return the declared arguments of a BIF --- .../boxlang/runtime/bifs/BIFDescriptor.java | 22 +++++++++++++++++++ .../bifs/global/system/GetFunctionList.java | 3 ++- .../global/system/GetFunctionListTest.java | 9 ++------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/BIFDescriptor.java b/src/main/java/ortus/boxlang/runtime/bifs/BIFDescriptor.java index e0db03c1b..5c86fedb7 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/BIFDescriptor.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/BIFDescriptor.java @@ -17,6 +17,7 @@ */ package ortus.boxlang.runtime.bifs; +import java.util.Arrays; import java.util.Map; import ortus.boxlang.runtime.BoxRuntime; @@ -27,6 +28,7 @@ import ortus.boxlang.runtime.scopes.Key; import ortus.boxlang.runtime.services.InterceptorService; import ortus.boxlang.runtime.types.Argument; +import ortus.boxlang.runtime.types.IStruct; import ortus.boxlang.runtime.types.Struct; import ortus.boxlang.runtime.util.ArgumentUtil; @@ -115,6 +117,26 @@ public Boolean hasNamespace() { return namespace != null; } + public IStruct getArguments() { + IStruct argumentsStruct = new Struct( Struct.TYPES.LINKED ); + Arrays.stream( getBIF().getDeclaredArguments() ) + .forEach( argument -> { + argumentsStruct.put( + argument.name(), + Struct.of( + "type", argument.type(), + "required", argument.required(), + "defaultValue", argument.defaultValue(), + "validators", argument.validators(), + "annotations", argument.annotations(), + "documentation", argument.documentation() + ) + ); + } ); + + return argumentsStruct; + } + /** * Get the BIF instance for this descriptor and lazily create it if needed * diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionList.java b/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionList.java index 3dbedcf19..147d5c297 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionList.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionList.java @@ -57,7 +57,8 @@ public IStruct _invoke( IBoxContext context, ArgumentsScope arguments ) { "module", bif.hasModule() ? bif.module : "---", "namespace", bif.hasNamespace() ? bif.namespace : "---", "isGlobal", bif.isGlobal, - "className", bif.BIFClass.getCanonicalName() + "className", bif.BIFClass.getCanonicalName(), + "arguments", bif.getArguments() ) ); } ); diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionListTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionListTest.java index 3e264d824..bfa9996c8 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionListTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/system/GetFunctionListTest.java @@ -19,7 +19,6 @@ 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; @@ -45,11 +44,6 @@ public static void setUp() { instance = BoxRuntime.getInstance( true ); } - @AfterAll - public static void teardown() { - - } - @BeforeEach public void setupEach() { context = new ScriptingRequestBoxContext( instance.getRuntimeContext() ); @@ -59,12 +53,13 @@ public void setupEach() { @DisplayName( "It can get the functions list" ) @Test public void testGetFunctionList() { - + // @formatter:off instance.executeSource( """ result = getFunctionList() """, context ); + // @formatter:on System.out.println( variables.get( result ) ); IStruct functions = ( IStruct ) variables.get( result ); From 99503a0f8cdfc4fe58dcc9b56b7138685caae51a Mon Sep 17 00:00:00 2001 From: lmajano Date: Wed, 30 Oct 2024 17:45:30 +0000 Subject: [PATCH 22/33] Apply cfformat changes --- .../ortus/boxlang/runtime/components/ComponentDescriptor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java b/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java index 7ec41e531..2f2a934b8 100644 --- a/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java +++ b/src/main/java/ortus/boxlang/runtime/components/ComponentDescriptor.java @@ -135,7 +135,7 @@ public Component getComponent() { this.componentInstance = ( ( Component ) DynamicObject.of( this.componentClass ) .invokeConstructor( ( IBoxContext ) null, new Object[] {} ) .getTargetInstance() ) - .setName( name ); + .setName( name ); interceptorService.announce( BoxEvent.ON_COMPONENT_INSTANCE, new Struct( From d6238c420382914a7284b989d89ce33e4c0f5145 Mon Sep 17 00:00:00 2001 From: Luis Majano Date: Wed, 30 Oct 2024 18:52:57 +0100 Subject: [PATCH 23/33] fix all db info tests --- src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java b/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java index 653a339c1..31d48d0b2 100644 --- a/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java +++ b/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java @@ -389,7 +389,7 @@ private Query getTables( String filter ) throws SQLException { Query result = new Query(); - try ( ResultSet resultSet = databaseMetadata.getTables( databaseName, schema, tableName, new String[] { filter } ) ) { + try ( ResultSet resultSet = databaseMetadata.getTables( databaseName, schema, tableName, filter == null ? null : new String[] { filter } ) ) { ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); buildQueryColumns( result, resultSetMetaData ); From 62fd53ff2c9a79717097de236ffe726b50c55d32 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Wed, 30 Oct 2024 13:53:20 -0500 Subject: [PATCH 24/33] BL-724 --- .../expression/BoxAssignmentTransformer.java | 9 ++++--- .../expression/BoxAssignmentTransformer.java | 8 +++--- .../java/TestCases/phase1/CoreLangTest.java | 25 +++++++++++++++++++ 3 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java index 27b050c71..ffabaea06 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java @@ -198,10 +198,13 @@ public List transformEquals( BoxExpression left, List nodes = new ArrayList<>(); if ( furthestLeft instanceof BoxIdentifier id ) { - if ( transpiler.matchesImport( id.getName() ) && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { - throw new ExpressionException( "You cannot assign a variable with the same name as an import: [" + id.getName() + "]", - furthestLeft.getPosition(), furthestLeft.getSourceText() ); + // imported.foo = 5 is ok, but imported = 5 is not + if ( left instanceof BoxIdentifier idl && transpiler.matchesImport( idl.getName() ) + && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { + throw new ExpressionException( "You cannot assign a variable with the same name as an import: [" + idl.getName() + "]", + idl.getPosition(), idl.getSourceText() ); } + /* * Referencer.setDeep( * ${contextName}, diff --git a/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java b/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java index 7e60a1713..5aa221f36 100644 --- a/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java @@ -206,9 +206,11 @@ public Node transformEquals( BoxExpression left, Expression jRight, BoxAssignmen } if ( furthestLeft instanceof BoxIdentifier id ) { - if ( transpiler.matchesImport( id.getName() ) && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { - throw new ExpressionException( "You cannot assign a variable with the same name as an import: [" + id.getName() + "]", - furthestLeft.getPosition(), furthestLeft.getSourceText() ); + // imported.foo = 5 is ok, but imported = 5 is not + if ( left instanceof BoxIdentifier idl && transpiler.matchesImport( idl.getName() ) + && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { + throw new ExpressionException( "You cannot assign a variable with the same name as an import: [" + idl.getName() + "]", + idl.getPosition(), idl.getSourceText() ); } Node keyNode = createKey( id.getName() ); diff --git a/src/test/java/TestCases/phase1/CoreLangTest.java b/src/test/java/TestCases/phase1/CoreLangTest.java index 3f31a8e36..8cfc60c25 100644 --- a/src/test/java/TestCases/phase1/CoreLangTest.java +++ b/src/test/java/TestCases/phase1/CoreLangTest.java @@ -3662,4 +3662,29 @@ public void testKeyAccessInStructMapWrapper() { assertThat( variables.get( Key.of( "result7" ) ) ).isEqualTo( "baz" ); } + @DisplayName( "test import name restrictions" ) + @Test + public void testImportNameRestrictions() { + // @formatter:off + instance.executeSource( + """ + import ortus.boxlang.runtime.context.BaseBoxContext; + currentValue = BaseBoxContext.nullIsUndefined; + BaseBoxContext.nullIsUndefined = currentValue; + """, + context ); + // @formatter:on + + // @formatter:off + Throwable t = assertThrows( BoxRuntimeException.class, () -> + instance.executeSource( + """ + import ortus.boxlang.runtime.context.BaseBoxContext; + BaseBoxContext = "foo"; + """, + context ) ); + // @formatter:on + assertThat( t.getMessage() ).contains( "You cannot assign a variable with the same name as an import" ); + } + } From a568c60c3147c799e2f6c1332564ed466de28947 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Wed, 30 Oct 2024 14:05:44 -0500 Subject: [PATCH 25/33] BL-715 --- .../runtime/context/BaseBoxContext.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java index 65e4ad95a..d2ea4b9f7 100644 --- a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java @@ -43,6 +43,7 @@ import ortus.boxlang.runtime.services.FunctionService; import ortus.boxlang.runtime.types.Function; import ortus.boxlang.runtime.types.IStruct; +import ortus.boxlang.runtime.types.NullValue; import ortus.boxlang.runtime.types.Query; import ortus.boxlang.runtime.types.QueryColumn; import ortus.boxlang.runtime.types.Struct; @@ -61,8 +62,16 @@ */ public class BaseBoxContext implements IBoxContext { + /** + * TODO: This can be removed later, it was put here to catch some endless recursion bugs + */ private static final ThreadLocal flushBufferDepth = ThreadLocal.withInitial( () -> 0 ); + /** + * A flag to control whether null is considered undefined or not. Used by the compat module + */ + public static boolean nullIsUndefined = false; + /** * -------------------------------------------------------------------------- * Private Properties @@ -712,6 +721,26 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean throw new BoxRuntimeException( "Unimplemented method 'scopeFindNearby'" ); } + /** + * Decide if a value found in a scope is defined or not + * + * @param value The value to check, possibly null, possibly an instance of NullValue + * + * @return True if the value is defined, else false + */ + protected boolean isDefined( Object value ) { + // If the value is null, it's not defined because the struct litearlly has no key for this + if ( value == null ) { + return false; + } + // Default BoxLang behavior is null is defined, but if compat has toggled the nullIsUndefined setting, then we need to check for our placeHolder NullValue value + if ( nullIsUndefined && value instanceof NullValue ) { + return false; + } + // Otherwise, it's defined + return true; + } + /** * Search any query loops for a column name matching the uncscoped variable * From 6cebc04fe766e6c25000746d6f4f59582ed0a92f Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 31 Oct 2024 23:27:03 -0500 Subject: [PATCH 26/33] BL-724 BL-726 --- .../compiler/asmboxpiler/AsmHelper.java | 32 +++++++++++ .../expression/BoxAssignmentTransformer.java | 38 ++++++++----- .../expression/BoxIdentifierTransformer.java | 23 +------- .../expression/BoxAssignmentTransformer.java | 21 ++++--- .../boxlang/runtime/dynamic/Referencer.java | 9 --- .../java/TestCases/phase1/CoreLangTest.java | 56 +++++++++++++++++-- 6 files changed, 123 insertions(+), 56 deletions(-) diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java index e534066a5..2e152bb98 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/AsmHelper.java @@ -25,6 +25,7 @@ import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; +import org.objectweb.asm.tree.VarInsnNode; import ortus.boxlang.compiler.asmboxpiler.transformer.ReturnValueContext; import ortus.boxlang.compiler.asmboxpiler.transformer.TransformerContext; @@ -33,6 +34,7 @@ import ortus.boxlang.compiler.ast.BoxNode; import ortus.boxlang.compiler.ast.BoxStatement; import ortus.boxlang.compiler.ast.expression.BoxArgument; +import ortus.boxlang.compiler.ast.expression.BoxIdentifier; import ortus.boxlang.compiler.ast.statement.BoxFunctionDeclaration; import ortus.boxlang.compiler.ast.statement.BoxReturnType; import ortus.boxlang.compiler.ast.statement.BoxType; @@ -987,4 +989,34 @@ public static void addLazySingleton( ClassVisitor classVisitor, Type type, Consu methodVisitor.visitMaxs( 0, 0 ); methodVisitor.visitEnd(); } + + /** + * Create the nodes for loading a class from the class locator + * + * classLocator.load( context, "NameOfClass", imports ) + * + * @param transpiler The transpiler + * @param identifier The identifier of the class to load + * + * @return The nodes + */ + public static List loadClass( Transpiler transpiler, BoxIdentifier identifier ) { + List nodes = new ArrayList<>(); + nodes.add( new VarInsnNode( Opcodes.ALOAD, 2 ) ); + transpiler.getCurrentMethodContextTracker().ifPresent( ( t ) -> nodes.addAll( t.loadCurrentContext() ) ); + nodes.add( new LdcInsnNode( identifier.getName() ) ); + nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, + transpiler.getProperty( "packageName" ).replace( '.', '/' ) + + "/" + + transpiler.getProperty( "classname" ), + "imports", + Type.getDescriptor( List.class ) ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, + Type.getInternalName( ClassLocator.class ), + "load", + Type.getMethodDescriptor( Type.getType( DynamicObject.class ), Type.getType( IBoxContext.class ), Type.getType( String.class ), + Type.getType( List.class ) ), + false ) ); + return nodes; + } } diff --git a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java index ffabaea06..17cbb192b 100644 --- a/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/asmboxpiler/transformer/expression/BoxAssignmentTransformer.java @@ -232,19 +232,29 @@ public List transformEquals( BoxExpression left, List nodes.addAll( t.loadCurrentContext() ) ); - nodes.addAll( transpiler.createKey( id.getName() ) ); - tracker.ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); - nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, - Type.getInternalName( IBoxContext.class ), - "getDefaultAssignmentScope", - Type.getMethodDescriptor( Type.getType( IScope.class ) ), - true ) ); - nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, - Type.getInternalName( IBoxContext.class ), - "scopeFindNearby", - Type.getMethodDescriptor( Type.getType( IBoxContext.ScopeSearchResult.class ), Type.getType( Key.class ), Type.getType( IScope.class ) ), - true ) ); + Class baseObjectClass; + // If id is an imported class name, load the class directly instead of searching scopes for it + boolean isBoxSyntax = transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ); + if ( transpiler.matchesImport( id.getName() ) && isBoxSyntax ) { + baseObjectClass = Object.class; + nodes.addAll( AsmHelper.loadClass( transpiler, id ) ); + } else { + // Otherwise, search for varible in scopes + baseObjectClass = IBoxContext.ScopeSearchResult.class; + tracker.ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); + nodes.addAll( transpiler.createKey( id.getName() ) ); + tracker.ifPresent( t -> nodes.addAll( t.loadCurrentContext() ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "getDefaultAssignmentScope", + Type.getMethodDescriptor( Type.getType( IScope.class ) ), + true ) ); + nodes.add( new MethodInsnNode( Opcodes.INVOKEINTERFACE, + Type.getInternalName( IBoxContext.class ), + "scopeFindNearby", + Type.getMethodDescriptor( Type.getType( IBoxContext.ScopeSearchResult.class ), Type.getType( Key.class ), Type.getType( IScope.class ) ), + true ) ); + } nodes.addAll( jRight ); @@ -258,7 +268,7 @@ public List transformEquals( BoxExpression left, List transform( BoxNode node, TransformerContext contex List nodes = new ArrayList<>(); if ( transpiler.matchesImport( identifier.getName() ) && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { - nodes.add( new VarInsnNode( Opcodes.ALOAD, 2 ) ); - nodes.add( new VarInsnNode( Opcodes.ALOAD, 1 ) ); - nodes.add( new LdcInsnNode( identifier.getName() ) ); - nodes.add( new FieldInsnNode( Opcodes.GETSTATIC, - transpiler.getProperty( "packageName" ).replace( '.', '/' ) - + "/" - + transpiler.getProperty( "classname" ), - "imports", - Type.getDescriptor( List.class ) ) ); - nodes.add( new MethodInsnNode( Opcodes.INVOKEVIRTUAL, - Type.getInternalName( ClassLocator.class ), - "load", - Type.getMethodDescriptor( Type.getType( DynamicObject.class ), Type.getType( IBoxContext.class ), Type.getType( String.class ), - Type.getType( List.class ) ), - false ) ); + // If id is an imported class name, load the class directly instead of searching scopes for it + nodes.addAll( AsmHelper.loadClass( transpiler, identifier ) ); } else { transpiler.getCurrentMethodContextTracker().ifPresent( ( t ) -> nodes.addAll( t.loadCurrentContext() ) ); nodes.addAll( transpiler.createKey( identifier.getName() ) ); diff --git a/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java b/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java index 5aa221f36..992b85d70 100644 --- a/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java +++ b/src/main/java/ortus/boxlang/compiler/javaboxpiler/transformer/expression/BoxAssignmentTransformer.java @@ -206,15 +206,22 @@ public Node transformEquals( BoxExpression left, Expression jRight, BoxAssignmen } if ( furthestLeft instanceof BoxIdentifier id ) { + boolean isBoxSyntax = transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ); // imported.foo = 5 is ok, but imported = 5 is not - if ( left instanceof BoxIdentifier idl && transpiler.matchesImport( idl.getName() ) - && transpiler.getProperty( "sourceType" ).toLowerCase().startsWith( "box" ) ) { + if ( left instanceof BoxIdentifier idl && transpiler.matchesImport( idl.getName() ) && isBoxSyntax ) { throw new ExpressionException( "You cannot assign a variable with the same name as an import: [" + idl.getName() + "]", idl.getPosition(), idl.getSourceText() ); } + String baseObjTemplate = "${contextName}.scopeFindNearby( ${accessKey}, ${contextName}.getDefaultAssignmentScope() ),"; + // imported.foo needs to swap out the furthest left object + if ( transpiler.matchesImport( id.getName() ) && isBoxSyntax ) { + baseObjTemplate = "classLocator.load( ${contextName}, \"${accessName}\", imports ),"; + } + Node keyNode = createKey( id.getName() ); String thisKey = keyNode.toString(); + values.put( "accessName", id.getName() ); values.put( "accessKey", thisKey ); values.put( "mustBeScopeName", mustBeScopeName == null ? "null" : createKey( mustBeScopeName ).toString() ); values.put( "hasFinal", hasFinal ? "true" : "false" ); @@ -229,11 +236,11 @@ public Node transformEquals( BoxExpression left, Expression jRight, BoxAssignmen ${contextName}, ${hasFinal}, ${mustBeScopeName}, - ${contextName}.scopeFindNearby( ${accessKey}, ${contextName}.getDefaultAssignmentScope() ), - ${right} - ${accessKeys} - ) - """; + """ + baseObjTemplate + """ + ${right} + ${accessKeys} + ) + """; } else { if ( accessKeys.size() == 0 ) { throw new ExpressionException( "You cannot assign a value to " + left.getClass().getSimpleName(), left.getPosition(), left.getSourceText() ); diff --git a/src/main/java/ortus/boxlang/runtime/dynamic/Referencer.java b/src/main/java/ortus/boxlang/runtime/dynamic/Referencer.java index c46899a9c..ee5eebbed 100644 --- a/src/main/java/ortus/boxlang/runtime/dynamic/Referencer.java +++ b/src/main/java/ortus/boxlang/runtime/dynamic/Referencer.java @@ -17,7 +17,6 @@ */ package ortus.boxlang.runtime.dynamic; -import java.util.List; import java.util.Map; import ortus.boxlang.runtime.context.IBoxContext; @@ -27,8 +26,6 @@ import ortus.boxlang.runtime.runnables.IClassRunnable; import ortus.boxlang.runtime.scopes.IScope; import ortus.boxlang.runtime.scopes.Key; -import ortus.boxlang.runtime.types.Query; -import ortus.boxlang.runtime.types.QueryColumn; import ortus.boxlang.runtime.types.Struct; import ortus.boxlang.runtime.types.exceptions.BoxRuntimeException; @@ -218,12 +215,6 @@ public static Object setDeep( IBoxContext context, boolean isFinal, Key mustBeSc next = new Struct(); set( context, isFinal, object, key, next ); - // If it's not null, it needs to be a Map - } else if ( ! ( next instanceof Map || next instanceof List || next instanceof Query || next instanceof QueryColumn ) ) { - throw new BoxRuntimeException( - String.format( "Cannot assign to key [%s] because it is a [%s] and not a Struct, Array, or Query", - key.getName(), - next.getClass().getName() ) ); } object = next; // Only counts the first time through diff --git a/src/test/java/TestCases/phase1/CoreLangTest.java b/src/test/java/TestCases/phase1/CoreLangTest.java index 8cfc60c25..9ec39d71f 100644 --- a/src/test/java/TestCases/phase1/CoreLangTest.java +++ b/src/test/java/TestCases/phase1/CoreLangTest.java @@ -38,6 +38,7 @@ import ortus.boxlang.compiler.parser.DocParser; import ortus.boxlang.compiler.parser.ParsingResult; import ortus.boxlang.runtime.BoxRuntime; +import ortus.boxlang.runtime.context.BaseBoxContext; import ortus.boxlang.runtime.context.FunctionBoxContext; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.context.ScriptingRequestBoxContext; @@ -64,6 +65,8 @@ public class CoreLangTest { IBoxContext context; IScope variables; static Key result = new Key( "result" ); + // Used in a test + public static int num = 0; @BeforeAll public static void setUp() { @@ -3665,26 +3668,67 @@ public void testKeyAccessInStructMapWrapper() { @DisplayName( "test import name restrictions" ) @Test public void testImportNameRestrictions() { + // @formatter:off + instance.executeSource( + """ + import ortus.boxlang.runtime.context.BaseBoxContext; + currentValue = BaseBoxContext.nullIsUndefined; + BaseBoxContext.nullIsUndefined = currentValue; + """, + context ); + // @formatter:on + + // @formatter:off + Throwable t = assertThrows( BoxRuntimeException.class, () -> + instance.executeSource( + """ + import ortus.boxlang.runtime.context.BaseBoxContext; + BaseBoxContext = "foo"; + """, + context ) ); + // @formatter:on + assertThat( t.getMessage() ).contains( "You cannot assign a variable with the same name as an import" ); + } + + @Test + public void testAssignPublicJavaPropertiesIndirectly() { // @formatter:off instance.executeSource( """ import ortus.boxlang.runtime.context.BaseBoxContext; - currentValue = BaseBoxContext.nullIsUndefined; - BaseBoxContext.nullIsUndefined = currentValue; + bbc = BaseBoxContext; + bbc.nullIsUndefined = true; + result = BaseBoxContext.nullIsUndefined; """, context ); // @formatter:on + assertThat( variables.get( result ) ).isEqualTo( true ); + BaseBoxContext.nullIsUndefined = false; + } + @Test + public void testAssignPublicJavaPropertiesDirectly() { // @formatter:off - Throwable t = assertThrows( BoxRuntimeException.class, () -> instance.executeSource( """ import ortus.boxlang.runtime.context.BaseBoxContext; - BaseBoxContext = "foo"; + import TestCases.phase1.CoreLangTest; + + BaseBoxContext.nullIsUndefined = true; + result = BaseBoxContext.nullIsUndefined; + result2 = BaseBoxContext.nullIsUndefined.len(); + + CoreLangTest.num += 5; + result3 = CoreLangTest.num; """, - context ) ); + context ); // @formatter:on - assertThat( t.getMessage() ).contains( "You cannot assign a variable with the same name as an import" ); + BaseBoxContext.nullIsUndefined = false; + assertThat( variables.get( result ) ).isEqualTo( true ); + assertThat( variables.get( Key.of( "result2" ) ) ).isEqualTo( 4 ); + assertThat( variables.get( Key.of( "result3" ) ) ).isEqualTo( 5 ); + assertThat( CoreLangTest.num ).isEqualTo( 5 ); + } } From 2da4c4098bacf587070762a6a6b8edc11512faf8 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 31 Oct 2024 23:30:34 -0500 Subject: [PATCH 27/33] BL-727 --- src/main/java/ortus/boxlang/runtime/operators/Minus.java | 9 +++++++-- src/main/java/ortus/boxlang/runtime/operators/Plus.java | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/operators/Minus.java b/src/main/java/ortus/boxlang/runtime/operators/Minus.java index 3333803b0..769f00960 100644 --- a/src/main/java/ortus/boxlang/runtime/operators/Minus.java +++ b/src/main/java/ortus/boxlang/runtime/operators/Minus.java @@ -48,8 +48,13 @@ public static Number invoke( Object left, Object right ) { // A couple shortcuts-- if both operands are integers or longs within a certain range, we can just subtract them safely if ( nLeft instanceof Integer li ) { if ( nRight instanceof Integer ri ) { - // Cast to long to avoid integer overflow - return ( long ) li - ri; + // Check if the result will overflow an int + long result = ( long ) li - ( long ) ri; + if ( result > Integer.MAX_VALUE || result < Integer.MIN_VALUE ) { + return result; // Return as long if it overflows + } else { + return ( int ) result; // Return as int if it doesn't overflow + } } if ( nRight instanceof Long rl && rl <= MAX_SAFE_LONG && rl >= MIN_SAFE_LONG ) { return ( long ) li - rl; diff --git a/src/main/java/ortus/boxlang/runtime/operators/Plus.java b/src/main/java/ortus/boxlang/runtime/operators/Plus.java index f1152d899..babf1cb5e 100644 --- a/src/main/java/ortus/boxlang/runtime/operators/Plus.java +++ b/src/main/java/ortus/boxlang/runtime/operators/Plus.java @@ -50,8 +50,13 @@ public static Number invoke( Object left, Object right ) { // BigDecimals are over twice the heap usage of a Double (~64 bits vs ~24 bits) if ( nLeft instanceof Integer li ) { if ( nRight instanceof Integer ri ) { - // Cast to long to avoid integer overflow - return ( long ) li + ri; + // Check if the result will overflow an int + long result = ( long ) li + ( long ) ri; + if ( result > Integer.MAX_VALUE || result < Integer.MIN_VALUE ) { + return result; // Return as long if it overflows + } else { + return ( int ) result; // Return as int if it doesn't overflow + } } if ( nRight instanceof Long rl && rl <= MAX_SAFE_LONG && rl >= MIN_SAFE_LONG ) { return li + rl; From dab6d6983f98c9a67c6d1f5b21c6076e8de105e2 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 31 Oct 2024 23:32:30 -0500 Subject: [PATCH 28/33] BL-728 --- .../java/ortus/boxlang/runtime/context/CatchBoxContext.java | 2 +- .../java/ortus/boxlang/runtime/context/ClassBoxContext.java | 2 +- .../ortus/boxlang/runtime/context/ClosureBoxContext.java | 4 ++-- .../ortus/boxlang/runtime/context/ContainerBoxContext.java | 2 +- .../ortus/boxlang/runtime/context/CustomTagBoxContext.java | 2 +- .../ortus/boxlang/runtime/context/FunctionBoxContext.java | 6 +++--- .../ortus/boxlang/runtime/context/InterfaceBoxContext.java | 2 +- .../ortus/boxlang/runtime/context/LambdaBoxContext.java | 4 ++-- .../boxlang/runtime/context/ScriptingRequestBoxContext.java | 3 ++- .../ortus/boxlang/runtime/context/ThreadBoxContext.java | 6 +++--- 10 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/context/CatchBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/CatchBoxContext.java index 9e3aa8b31..741b94aea 100644 --- a/src/main/java/ortus/boxlang/runtime/context/CatchBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/CatchBoxContext.java @@ -107,7 +107,7 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean // In Variables scope? (thread-safe lookup and get) Object result = variablesScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( variablesScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/ClassBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/ClassBoxContext.java index 770b79014..57185d7dd 100644 --- a/src/main/java/ortus/boxlang/runtime/context/ClassBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/ClassBoxContext.java @@ -140,7 +140,7 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean Object result = variablesScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( variablesScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/ClosureBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/ClosureBoxContext.java index 5c656fedf..9db5bcbfa 100644 --- a/src/main/java/ortus/boxlang/runtime/context/ClosureBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/ClosureBoxContext.java @@ -151,14 +151,14 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean Object result = localScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( localScope, Struct.unWrapNull( result ), key ); } result = argumentsScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( argumentsScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/ContainerBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/ContainerBoxContext.java index 9fc779989..ddae03221 100644 --- a/src/main/java/ortus/boxlang/runtime/context/ContainerBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/ContainerBoxContext.java @@ -96,7 +96,7 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean // In Variables scope? (thread-safe lookup and get) Object result = variablesScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( variablesScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/CustomTagBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/CustomTagBoxContext.java index 313b68842..44c93caa8 100644 --- a/src/main/java/ortus/boxlang/runtime/context/CustomTagBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/CustomTagBoxContext.java @@ -97,7 +97,7 @@ public IStruct getVisibleScopes( IStruct scopes, boolean nearby, boolean shallow public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean shallow ) { Object result = variablesScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( variablesScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java index 2c810f29c..d7982d434 100644 --- a/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/FunctionBoxContext.java @@ -294,14 +294,14 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean Object result = localScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( localScope, Struct.unWrapNull( result ), key ); } result = argumentsScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( argumentsScope, Struct.unWrapNull( result ), key ); } @@ -317,7 +317,7 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean IScope classVariablesScope = getThisClass().getBottomClass().getVariablesScope(); result = classVariablesScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( classVariablesScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/InterfaceBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/InterfaceBoxContext.java index 97f5aebf2..219662de5 100644 --- a/src/main/java/ortus/boxlang/runtime/context/InterfaceBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/InterfaceBoxContext.java @@ -90,7 +90,7 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean Object result = staticScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( staticScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/LambdaBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/LambdaBoxContext.java index e32eff53d..926f17dfd 100644 --- a/src/main/java/ortus/boxlang/runtime/context/LambdaBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/LambdaBoxContext.java @@ -129,14 +129,14 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean Object result = localScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( localScope, Struct.unWrapNull( result ), key ); } result = argumentsScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( argumentsScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java index 66e2ebb6b..862943fb9 100644 --- a/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java @@ -198,8 +198,9 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean // In Variables scope? (thread-safe lookup and get) Object result = variablesScope.getRaw( key ); + System.out.println( "key: " + key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( variablesScope, Struct.unWrapNull( result ), key ); } diff --git a/src/main/java/ortus/boxlang/runtime/context/ThreadBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/ThreadBoxContext.java index a92834fbc..ce80ba91d 100644 --- a/src/main/java/ortus/boxlang/runtime/context/ThreadBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/ThreadBoxContext.java @@ -159,14 +159,14 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean Object result = localScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real return new ScopeSearchResult( localScope, Struct.unWrapNull( result ), key ); } result = variablesScope.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { // A thread has special permission to "see" the variables scope from its parent, // even though it's not "nearby" to any other scopes return new ScopeSearchResult( variablesScope, Struct.unWrapNull( result ), key ); @@ -235,7 +235,7 @@ public ScopeSearchResult scopeFind( Key key, IScope defaultScope ) { Object result = threadMeta.getRaw( key ); // Null means not found - if ( result != null ) { + if ( isDefined( result ) ) { return new ScopeSearchResult( threadMeta, Struct.unWrapNull( result ), key ); } From e8f9b60743458257842f1c4dae08b166dbbc917d Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Thu, 31 Oct 2024 23:41:15 -0500 Subject: [PATCH 29/33] BL-715 --- .../boxlang/runtime/context/ScriptingRequestBoxContext.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java index 862943fb9..0b53357c7 100644 --- a/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/ScriptingRequestBoxContext.java @@ -198,7 +198,6 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean // In Variables scope? (thread-safe lookup and get) Object result = variablesScope.getRaw( key ); - System.out.println( "key: " + key ); // Null means not found if ( isDefined( result ) ) { // Unwrap the value now in case it was really actually null for real From 255cba19bdc564c2b611cacbc898a7ca8bb14403 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 1 Nov 2024 00:04:02 -0500 Subject: [PATCH 30/33] BL-697 BL-638 BL-669 --- .../bifs/global/struct/StructKeyExists.java | 3 ++- .../runtime/context/BaseBoxContext.java | 2 +- .../boxlang/runtime/context/IBoxContext.java | 9 +++++++ .../java/TestCases/phase1/CoreLangTest.java | 26 ++++++++++++++++++- .../global/struct/StructKeyExistsTest.java | 12 ++++++++- 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java index 6f254865b..2b0b289a1 100644 --- a/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java +++ b/src/main/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExists.java @@ -53,7 +53,8 @@ public StructKeyExists() { * @argument.key The key within the struct to test for existence */ public Object _invoke( IBoxContext context, ArgumentsScope arguments ) { - return arguments.getAsStruct( Key.struct ).containsKey( Key.of( arguments.get( Key.key ) ) ); + Object result = arguments.getAsStruct( Key.struct ).getRaw( Key.of( arguments.get( Key.key ) ) ); + return context.isDefined( result ); } } diff --git a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java index d2ea4b9f7..bf25e7d8e 100644 --- a/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/BaseBoxContext.java @@ -728,7 +728,7 @@ public ScopeSearchResult scopeFindNearby( Key key, IScope defaultScope, boolean * * @return True if the value is defined, else false */ - protected boolean isDefined( Object value ) { + public boolean isDefined( Object value ) { // If the value is null, it's not defined because the struct litearlly has no key for this if ( value == null ) { return false; diff --git a/src/main/java/ortus/boxlang/runtime/context/IBoxContext.java b/src/main/java/ortus/boxlang/runtime/context/IBoxContext.java index f1020e085..524ab3c3e 100644 --- a/src/main/java/ortus/boxlang/runtime/context/IBoxContext.java +++ b/src/main/java/ortus/boxlang/runtime/context/IBoxContext.java @@ -687,4 +687,13 @@ default void registerUDF( IScope scope, UDF udf, boolean override ) { } } + /** + * This implements a check if a value is defined, which allows the compat module to override for CF behavior + * + * @param value The value to check + * + * @return True if the value is defined, else false + */ + public boolean isDefined( Object value ); + } diff --git a/src/test/java/TestCases/phase1/CoreLangTest.java b/src/test/java/TestCases/phase1/CoreLangTest.java index 9ec39d71f..7291bec66 100644 --- a/src/test/java/TestCases/phase1/CoreLangTest.java +++ b/src/test/java/TestCases/phase1/CoreLangTest.java @@ -3713,7 +3713,7 @@ public void testAssignPublicJavaPropertiesDirectly() { """ import ortus.boxlang.runtime.context.BaseBoxContext; import TestCases.phase1.CoreLangTest; - + BaseBoxContext.nullIsUndefined = true; result = BaseBoxContext.nullIsUndefined; result2 = BaseBoxContext.nullIsUndefined.len(); @@ -3731,4 +3731,28 @@ public void testAssignPublicJavaPropertiesDirectly() { } + @DisplayName( "Test null scope lookup order" ) + @Test + public void testNullScopeLookupOrder() { + + // @formatter:off + instance.executeSource( """ + function testMe( string foo ) { + local.foo = null; + // local scope is checked first, but since local is null, we'll ignore it + return foo; + } + result = testMe( "arguments" ); + """ + , context ); + // @formatter:on + assertThat( variables.get( result ) ).isNull(); + + instance.executeSource( """ + foo = null; + result = foo; + """, context ); + assertThat( variables.get( result ) ).isNull(); + } + } diff --git a/src/test/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExistsTest.java b/src/test/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExistsTest.java index e7691bede..356b2d82c 100644 --- a/src/test/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExistsTest.java +++ b/src/test/java/ortus/boxlang/runtime/bifs/global/struct/StructKeyExistsTest.java @@ -75,7 +75,6 @@ public void testBif() { """, context ); assertFalse( variables.getAsBoolean( result ) ); - } @DisplayName( "It tests the Member function Struct.keyExists" ) @@ -95,7 +94,18 @@ public void testMember() { """, context ); assertFalse( variables.getAsBoolean( result ) ); + } + @DisplayName( "It tests that null exists" ) + @Test + public void testNull() { + instance.executeSource( + """ + myStruct={ "foo" : null }; + result = myStruct.keyExists( "foo" ); + """, + context ); + assertTrue( variables.getAsBoolean( result ) ); } } From 123bbe042da48932c0293bd94a5c35f3ebce6cac Mon Sep 17 00:00:00 2001 From: Michael Born Date: Fri, 1 Nov 2024 08:03:00 -0400 Subject: [PATCH 31/33] JDBC - Use string.format for 'filter' error message in DBInfo --- .../java/ortus/boxlang/runtime/components/jdbc/DBInfo.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java b/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java index 31d48d0b2..3e70ec434 100644 --- a/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java +++ b/src/main/java/ortus/boxlang/runtime/components/jdbc/DBInfo.java @@ -226,7 +226,7 @@ private void validateFilter( String filter, ResultSet tableTypes ) { if ( !validType ) { throw new BoxValidationException( - "Invalid [dbinfo] type=table filter [" + filter + "]. Supported table types are " + String.join( ", ", allowedTypes ) + "." + String.format( "Invalid [dbinfo] type=table filter [%s]. Supported table types are %s.", filter, String.join( ", ", allowedTypes ) ) ); } } catch ( SQLException e ) { @@ -292,8 +292,6 @@ private Query getVersion( DatabaseMetaData databaseMetadata ) throws SQLExceptio /** * Retrieve column metadata for a given table name. * - * @TODO: Add support for Lucee's custom foreign key and primary key fields. - * * @param datasource Datasource on which the table resides. * @param databaseName Name of the database to check for tables. If not provided, the database name from the connection will be used. * @param tableName Optional pattern to filter table names by. Can use wildcards or any `LIKE`-compatible pattern such as `tbl_%`. Can use From 94d233829d7905d77a2b1545d84c8dd412b36106 Mon Sep 17 00:00:00 2001 From: Michael Born Date: Fri, 1 Nov 2024 10:22:49 -0400 Subject: [PATCH 32/33] InterceptorPool - Add unregister() support for java classes / IInterceptor instances --- .../runtime/events/InterceptorPool.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java b/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java index d4cd937c2..952e99af2 100644 --- a/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java +++ b/src/main/java/ortus/boxlang/runtime/events/InterceptorPool.java @@ -396,6 +396,30 @@ public InterceptorPool register( IInterceptor interceptor, IStruct properties ) return register( target, states.toArray( new Key[ 0 ] ) ); } + /** + * This method UNregisters a Java interceptor with the pool by metadata inspection + * of the {@link InterceptionPoint} annotation. It will inspect the interceptor for + * methods that match the states that the pool can announce. If the + * interceptor has a method that matches the state, it will UNregister it with the pool. + * + * @param interceptor The interceptor to UNregister that must implement {@link IInterceptor} + * + * @return The same pool + */ + public InterceptorPool unregister( IInterceptor interceptor ) { + // Discover all @InterceptionPoint methods and build into an array of Keys to register + DynamicObject target = DynamicObject.of( interceptor ); + Set states = target.getMethodsAsStream( true ) + // filter only the methods that have the @InterceptionPoint annotation + .filter( method -> method.isAnnotationPresent( InterceptionPoint.class ) ) + // map it to the method name + .map( method -> Key.of( method.getName() ) ) + // Collect to the states set to register + .collect( Collectors.toSet() ); + + return unregister( target, states.toArray( new Key[ 0 ] ) ); + } + /** * This method registers a BoxLang interceptor with the pool by metadata inspection. * It will inspect the interceptor for methods that match the states that the From 6c359632dfb15fe462be52e826b39a5ba070b173 Mon Sep 17 00:00:00 2001 From: Brad Wood Date: Fri, 1 Nov 2024 12:47:15 -0500 Subject: [PATCH 33/33] BL-729 --- .../ortus/boxlang/runtime/runnables/BoxClassSupport.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java b/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java index 3f5144d4c..2e066c071 100644 --- a/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java +++ b/src/main/java/ortus/boxlang/runtime/runnables/BoxClassSupport.java @@ -21,6 +21,7 @@ import ortus.boxlang.runtime.BoxRuntime; import ortus.boxlang.runtime.bifs.MemberDescriptor; +import ortus.boxlang.runtime.context.BaseBoxContext; import ortus.boxlang.runtime.context.FunctionBoxContext; import ortus.boxlang.runtime.context.IBoxContext; import ortus.boxlang.runtime.dynamic.casters.BooleanCaster; @@ -80,7 +81,11 @@ public static void defaultProperties( IClassRunnable thisClass, IBoxContext cont // Don't override existing values, probably from a super class // But UDFs of the same name? Yeah, nuke those suckers, lol. (╥﹏╥) if ( existing == null || existing instanceof Function ) { - thisClass.getVariablesScope().assign( context, property.name(), property.getDefaultValue( context ) ); + Object defaultValue = property.getDefaultValue( context ); + // If the compat module is making null behave like CF, then don't default null properties. ColdBox blows up otherwise, and I'm sure other code as well. + if ( defaultValue != null || !BaseBoxContext.nullIsUndefined ) { + thisClass.getVariablesScope().assign( context, property.name(), defaultValue ); + } } if ( hasAccessors( thisClass ) ) { // Don't override UDFs from a parent class which may already be defined