Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rhino: Java numbers don't behave like Javascript numbers

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)
like image 411
lunicon Avatar asked May 12 '15 08:05

lunicon


2 Answers

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

like image 177
Raffaele Avatar answered Sep 21 '22 14:09

Raffaele


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.

like image 25
lunicon Avatar answered Sep 18 '22 14:09

lunicon