Rhino: Java numbers don't behave like Javascript numbers
Asked Answered
K

2

18

I have an instance of this Java class accessible in my Javascript program

public class ContentProvider {
  public Object c(int n) {
    switch (n) {
      case 1: return 1.1;
      case 2: return 2.2;
      case 3: return 3.3;
      case 4: return "4";
      case 5: return new java.util.Date();
    }
    return null;
  }
}

This is the code inside main():

ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine engine = mgr.getEngineByName("JavaScript");
engine.put("ctx", new ContentProvider());

res = engine.eval("ctx.c(1)");

System.out.printf("rhino:> %s (%s)%n"
        , res
        , res != null ? res.getClass().getName() : null
);

The simple expression ctx.c(1) prints:

rhino:> 1.1 (java.lang.Double)

Now here is what happens with ctx.c(1) + ctx.c(2):

rhino:> 1.12.2 (java.lang.String)

And finally (ctx.c(1) + ctx.c(2)) * ctx.c(3):

rhino:> nan (java.lang.Double)

Rhino is performing string concatenation instead of number arithmetics! The following program works as expected instead:

engine.put("a", 1.1);
engine.put("b", 2.2);
engine.put("c", 3.3);
res = engine.eval("(a + b) * c");

Outputs:

rhino:> 10,89 (java.lang.Double)
Keeton answered 12/5, 2015 at 8:2 Comment(5)
Oh my god, someone actually used the "java" and "javascript" tags together correctly!Wismar
But why not just try engine.eval("typeof ctx.c(1)") and see what JavaScript thinks the type is?Wismar
typeof ctx.c(1) = objectKeeton
@Keeton Nice solution! Please add it as an answer (doesn't matter if you are the OP as well) and accept it.Destefano
@Keeton you may want to take a look at my new answer because it's easier to code and to maintainCelestial
C
8

This is a strange feature of Rhino: a Java Number set with engine.put("one", new Double(1)) works as expected, while the result of a Java method depends on the return type declared by the method itself, which is read with the reflection API:

  • if it's a primitive, like double, it's converted to a Javascript number
  • otherwise it's handled like other host objects and the + means concatenation, either Object like in your sample as well as Double

You can configure this behavior with wrapFactory.setJavaPrimitiveWrap(false) on the WrapFactory in the current Context. This way the Rhino code can be kept in the bootstrap lines of your program and doesn't clutter ContentProvider (which I guess is some sort of configuration proxy)

From the live Javadoc of WrapFactory.isJavaPrimitiveWrap()

By default the method returns true to indicate that instances of String, Number, Boolean and Character should be wrapped as any other Java object and scripts can access any Java method available in these objects

So you can set this flag to false to indicate that Java Number's should be converted to Javascript numbers. It takes just two lines of code

Context ctx = Context.enter();
ctx.getWrapFactory().setJavaPrimitiveWrap(false);

Here is the Gist with the full code I used to test

Celestial answered 12/5, 2015 at 21:40 Comment(4)
Thank you. It should be made bold in the documentation.Keeton
Agree. Instead I had to debug and download the sources. It also seems a weird defaultCelestial
I encountered this problem and searched all documentation but didn't find anything. Thank you saved my time.Recite
Update: In Rhino java7 it works for every Number classes, bug In latest Rhino 1.7.11 no wrapping for BigDecimal and BigInteger!Keeton
K
1

I created a value wrapper:

public static class JSValue extends sun.org.mozilla.javascript.internal.ScriptableObject
{
    Object value;

    public JSValue(Object value) {
        this.value = value;
    }

    public String getClassName() {
        return value != null? value.getClass().getName(): null;
    }

    @Override
    public Object getDefaultValue(Class typeHint) {
        if (typeHint == null || Number.class.isAssignableFrom(typeHint)) {
            if (value instanceof Number)
                return ((Number) value).doubleValue();
        }

        return toString();
    }

    @Override
    public String toString() {
        return value != null? value.toString(): null;
    }
}

and an edit function:

  public static class ContentProvider {
    public Object c(int n) {
    ... return new JSValue(1.1);

Now the expression works as expected. Thanks all.

Keeton answered 12/5, 2015 at 18:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.