graaljs
graaljs copied to clipboard
Allow get/set JavaBean style methods on casted interfaces
I can cast a JavaScript object to a Java interface that has one function per JS property. But if those functions have names that don't exactly match the JS side, it won't work.
Why would I have such names? Because I used Kotlin with this:
interface JSFoo {
val a: String
val b: Int
}
and this compiles to getA(), getB() etc. Rather than bail out it'd be nice if the function proxy handler understood this pattern.
Hi @mikehearn
Thanks for your report, we will investigate. We have support for this in principle, but we only provide it in our nashorn-compat mode (see https://github.com/graalvm/graaljs/blob/master/docs/user/NashornMigrationGuide.md#accessors).
We'll check if this applies here and whether we need to open up more (or provide it under an explicit flag).
Best, Christian
There is another way of doing this with the Polyglot API. It is arguably not the most efficient way of doing it, but you can do the following:
public class JavaBeanTest {
public static class PersonBean {
private boolean deceased;
private String name;
public PersonBean() {
}
public String getName() {
return name;
}
public void setName(final String value) {
this.name = value;
}
public boolean isDeceased() {
return deceased;
}
public void setDeceased(boolean value) {
deceased = value;
}
public int someMethod() {
return 42;
}
}
public static void main(String[] args) {
PersonBean javaObject = new PersonBean();
javaObject.setName("foobarbazz");
Context context = Context.create();
context.getBindings("js").putMember("javaObject", javaObject);
context.getBindings("js").putMember("bean", BeanWrapper.wrap(context.asValue(javaObject)));
System.out.println(context.eval("js", "javaObject.getName()").asString().equals("foobarbazz")); // true
context.eval("js", "javaObject.setName('foo')");
System.out.println(context.eval("js", "bean.name").asString().equals("foo")); // true
context.eval("js", "bean.name = 'bar' ");
System.out.println(javaObject.getName().equals("bar")); // true
System.out.println(context.eval("js", "bean.someMethod()").asInt()); // 42
}
static String firstLetterUpperCase(String name) {
if (name.isEmpty()) {
return name;
}
return Character.toUpperCase(name.charAt(0)) + name.substring(1, name.length());
}
static class BeanWrapper implements ProxyObject {
private final Value delegate;
BeanWrapper(Value delegate) {
this.delegate = delegate;
}
@Override
public Object getMember(String key) {
Value getter = getGetter(key);
if (getter != null && getter.canExecute()) {
return getter.execute();
} else {
return delegate.getMember(key);
}
}
@Override
public Object getMemberKeys() {
List<String> members = new ArrayList<>();
for (String key : delegate.getMemberKeys()) {
if (getGetter(key) != null) {
members.add(key);
} else {
members.add(key);
}
}
return members;
}
@Override
public boolean hasMember(String key) {
return getGetter(key) != null || delegate.hasMember(key);
}
@Override
public void putMember(String key, Value value) {
Value setter = getSetter(key);
if (setter != null && setter.canExecute()) {
setter.execute(value);
} else {
delegate.putMember(key, value);
}
}
private Value getGetter(String key) {
Value getter = delegate.getMember("get" + firstLetterUpperCase(key));
if (getter == null) {
getter = delegate.getMember("is" + firstLetterUpperCase(key));
}
return getter;
}
private Value getSetter(String key) {
return delegate.getMember("set" + firstLetterUpperCase(key));
}
public static final Object wrap(Value javaBean) {
if (javaBean.isHostObject() && javaBean.asHostObject() instanceof BeanWrapper) {
return javaBean;
} else {
return new BeanWrapper(javaBean);
}
}
}
}
I'm revisiting this topic. Thanks for the code but it does the other way around to what I'm asking for.
Let's say a JS object has a property. I can read and write to it by treating the object as a map, but, for calling methods there's a convenience: I can cast it to an interface that has the strongly typed functions on it, and use those instead. Great! I can even read properties by defining functions with the property names. OK! But I don't see a way to set properties that way: they aren't turned into functions.
What I'd actually like is for the JS object to be castable to a "bean pattern" interface, as a lot of Java tools and libraries understand that. It's nearly there - just not quite!
i just wanted to second this issue. in my case, i don't need any Nashorn compatibility outside of annotating Kotlin getters/setters with HostAccess.Export.
data class Example(
@get:HostAccess.Export val readOnly: String,
@get:HostAccess.Export @set:HostAccess.Export var mutable: Boolean,
)
@chumer's approach does make it work, but each object is returned as a generic [object Object], at least on the version of GVM i'm running on:
openjdk version "19.0.1" 2022-10-18
OpenJDK Runtime Environment GraalVM CE 23.0.0-dev (build 19.0.1+10-jvmci-23.0-b04)
OpenJDK 64-Bit Server VM GraalVM CE 23.0.0-dev (build 19.0.1+10-jvmci-23.0-b04, mixed mode, sharing)
edit: @wirthi is there a way we could annotate getters/setters explicitly and have them bridge properly? or use another annotation? i'm trying to avoid (A) keeping Nahorn compat on forever (it will eventually sunset, and it probably adds overhead, and certainly a bunch of other details in the context which are undesirable for a non-Nashorn use case), and also want to avoid (B) creating proxy images for each type, because AFAICT it requires writing code on each side of the VM to make sure the casting is smooth.
for this case, i don't need all getters and setters to dispatch this way; it's sufficient if HostAccess.Export or a similar flag mechanism allow-lists such behavior.
+1 I need this too. I don't want to enable Nashorn compatibility only for accessors. It would be awesome to have explicit option or annotation for them.