BoxLang: Our new JVM Dynamic Language made by Ortus! Check it out: https://www.boxlang.io

Java Interop - StringBuilder usage is incorrectly passed through to String `replace` BIF

Description

The following code should produce a String builder object containing “blah,bar”:

myString="foo,bar"; builder = createObject( "java", "java.lang.StringBuilder" ).init( myString ) start = builder.indexOf( "foo" ); end = len( "foo" ); writeDump( builder.replace( start, end, "blah" ) );

In BoxLang, it results in the exception:

ortus.boxlang.runtime.types.exceptions.BoxRuntimeException: Invalid replacement scope: [blah]. Valid options are 'one' or 'all'. at ortus.boxlang.runtime.bifs.global.string.Replace._invoke(Replace.java:99) at ortus.boxlang.runtime.bifs.BIF.invoke(BIF.java:113) at ortus.boxlang.runtime.bifs.BIFDescriptor.invoke(BIFDescriptor.java:210) at ortus.boxlang.runtime.bifs.MemberDescriptor.invoke(MemberDescriptor.java:128) at ortus.boxlang.runtime.interop.DynamicInteropService.dereferenceAndInvoke(DynamicInteropService.java:1994) at ortus.boxlang.runtime.interop.DynamicInteropService.dereferenceAndInvoke(DynamicInteropService.java:1958) at ortus.boxlang.runtime.dynamic.Referencer.getAndInvoke(Referencer.java:89) at boxgenerated.scripts.Statement__5a2f45a02efb4263ce668c5e7cfd72c6._invoke(unknown:5) at ortus.boxlang.runtime.runnables.BoxScript.invoke(BoxScript.java:77) at ortus.boxlang.runtime.BoxRuntime.executeStatement(BoxRuntime.java:1451) at ortus.boxlang.runtime.BoxRuntime.executeStatement(BoxRuntime.java:1423) at ortus.boxlang.runtime.BoxRuntime.executeStatement(BoxRuntime.java:1434) at ortus.boxlang.tryboxlang.LambdaRunner.handleRequest(LambdaRunner.java:128) at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source) at java.base/java.lang.reflect.Method.invoke(Unknown Source)

Activity

Brad WoodMarch 26, 2025 at 9:56 PM

string caster has been broke in two

  • StringCaster – used for everything but member methods

  • StringCasterStrict -- used for member methods

Things the StringCaster considers strings

  • now() (BoxLang DateTime)

  • java.util.Locale

  • XML (result of XMLParse())

  • java.io.InputStream

  • java.lang.StringBuilder

  • java.lang.StringBuffer

  • byte[]

  • java.nio.file.Path

  • java.sql.Date

  • java.util.Date

  • java.time.Instant

  • java.time.ZoneId

  • java.time.LocalTime

  • java.time.LocalDateTime

  • java.time.LocalDate

  • java.sql.Timestamp

  • java.time.ZonedDateTime

  • java.util.Calendar

  • java.time.Duration

  • java.net.URI

  • java.net.URL

  • java.net.InetSocketAddress

  • java.lang.Class (with CF compat)

  • java.lang.Throwable (with CF compat)

 

Things StringCasterStrict considers strings

  • Key

  • java.lang.String

  • java.lang.Integer

  • java.lang.Long

  • java.lang.Short

  • java.lang.Byte

  • java.math.BigInteger

  • java.math.BigDecimal

  • java.lang.Float

  • java.lang.Double

  • java.lang.Boolean

  • java.lang.Character <-- different from CF

  • java.lang.Character[] <-- different from CF

  • char[] <-- different from CF

  • java.util.UUID <-- different from CF

BoxLang String member methods will only work now on the second list, preventing “hiding” of native Java methods on those classes,

Brad WoodMarch 25, 2025 at 8:31 PM

One of our selling points for BL is Java interop

One of our selling points for BL is also consistency. We dislike how CF has haphazardly dealt with types, treating them one way in one scenario only to treat them differently in another scenario. That is the “good reason” why if BL decides StringBuilders are strings, then it makes a great deal of sense that they are strings everywhere, all the time, in all situations. Not sometimes, on Tuesdays after a full moon, when concatenating. 🙂 CF’s inconsistent behavior of auto-casting in some cases but not others was one of the behaviors we wanted to avoid in BL if possible.

The issue of how to handle conflicts in member methods is really a larger conversation as it potentially affects all types. String Builder just happens to be the current example. And even if we decide stringbuilders are no longer strings (which would also break CF compat, but in a different way), we still won’t have removed the possibility of member method conflicts. We will have just removed one specific instance of conflict. Our language is basically designed in a way that allows for conflicts and that’s something we’ve got to figure out how to deal with.

Back it out of the StringCaster, go back to the original bug report, and then add custom handling for it in the context it was reported.

I don’t know if there is a specific bug report per se, but CF basically allows a StringBuilder instance to be used and treated as a string in every possible way that a normal string is allowed with the exception of member methods.

  • SB can be passed to any BIF accepting a string

  • SB can be passed to any UDF argument typed as string

  • SB can be passed to native Java methods typed as a String and will be auto-cast

  • SB can be returned from a function with a return type of string

  • SB can be concatenated with other strings

  • SB can be directly output to a webpage without any serialization

Every single one of those examples above use the StringCaster to decide if the value in question can be coerced to a string. The one and only box CF didn’t check was

  • SB can have string member methods called on it

That also uses the String Caster. And this was a huge annoyance with CF’s inconsistencies. My argument was always that if this works:

typeAction( value )

then this too should work in a well-designed language

value.action()

So if

sb = createOBject( 'java', 'java.lang.StringBuilder' ).init( "test" ) ucase( sb )

works, then a well-designed language will also allow for this as well

sb = createOBject( 'java', 'java.lang.StringBuilder' ).init( "test" ) sb.ucase()

Anyway, my point was that the member methods not working is actually the exception, not the other way around. CF treats StringBuilders as full fledged strings in every single possible way but member methods, which is the one exception. Which is why it feels more correct for me to leave SB in the string caster and make the member method check be the place where we have the special exception logic.

Another option entirely is we relegate the “StringBuilders are Strings” behavior to CF compat only, but I find it pretty useful and would hate to lose it in BL proper.

Maybe we can figure out a more elegant way to deal with the occasional member method conflict in the future. Perhaps that means reviewing all our BIF member methods and renaming any of them which conflict with any member methods of any of the underlying Java types we consider castable so we manually eliminate any conflicts.

Jon ClausenMarch 25, 2025 at 9:01 PM

Another option entirely is we relegate the “StringBuilders are Strings” behavior to CF compat only, but I find it pretty useful and would hate to lose it in BL proper.

I totally agree with that. I like your suggestion of just having the member method check be the one to handle it. If we do that, it will probably address 99.9% of everything related to using member functions on Java objects - and we hopefully won’t have to circle back to this again.

Jon ClausenMarch 25, 2025 at 5:38 PM

My two cents: We keep using the StringCaster as our one-stop shop for dealing with bug reports on casting issues - Java classes, StringBuilder classes, etc. It has caused numerous regressions and fire drills to fix implementations in our modules and BIFs.

I can think of no reason we need to generically cast, all the time, a StringBuilder object to a string everywhere, every time.

If a separate caster handles that, then fine, but it feels like, lately, we have taken reports as they come in and go “wierd, Lucee/ACF cast that to a string” and then just add it to the StringCaster.

One of our selling points for BL is Java interop. We can’t really lead with that, if the methods of a basic Java class can’t be used upon that class - simply because we decide to opinionatedly cast it to a String which has matching member BIF.

Personally, I would punt, given we are in RC. Back it out of the StringCaster, go back to the original bug report, and then add custom handling for it in the context it was reported.

Brad WoodMarch 25, 2025 at 5:04 PM

Nope, you have it backwards. BoxLang type member methods have always “hidden” underlying Java methods of the same name. It’s rare to have conflicts like this-- I don’t recall there ever being a member method conflict on hashmaps, but I do recall List.clear() and List.sort() overlap (meaning you can only call the BoxLang version). It’s gonna be a “darned if you do, darned if you don’t” scenario either way, because no matter how we make it work, it will “break” the other version.

Adobe and Lucee’s precedent is also that CF type member methods take precedence. See this example:

arr = [1,2,3] writeDump( arr.sort( (a,b)=>-1 ) )

Both Lucee, and ACF will call the arraySort() BIF, not the List.sort() method, which requires a java.util.Comparator instance.

The more “loose”we make our casters, the more chance we have of conflicts. For example, I don’t think any of the Java String member methods overlap with our BIF names, but the more things we treat as a “string”, the more likely we are to have these conflicts and there’s not a great answer to it. It would be interesting to look at some annotation above method calls to force the native version, but I don’t know how much I like that. Other options would be something like another reserved property along the lines of $bx to force access to the native Java object:

arr.$native.sort( ... )

The best way to approximate CF’s behavior on StringBuilder is as I said above. Come up with one caster that is used to output or concat strings, and another, more strict one, which is used to test for member methods. This is one of the problems we deal with when we design a language which layers our own type system and type member methods on top of Java classes with their own member methods which we also want to expose and it has the potential to bite us on any member method.

Luis MajanoMarch 25, 2025 at 3:48 AM

I thought we reviewed this that on Java classes, we would want to call the Java native method first, didn’t we have this same issue with concurrent hash maps? Where we needed to call the native map method instead of the member method? The expectation is to run the native Java method, not the member method.

Fixed
Pinned fields
Click on the next to a field label to start pinning.

Details

Assignee

Reporter

Fix versions

Priority

Sentry

Created March 20, 2025 at 11:49 PM
Updated March 26, 2025 at 9:56 PM
Resolved March 26, 2025 at 9:52 PM

Flag notifications