Skip to content

Commit 98c9b79

Browse files
committed
Use alternate name for static members with readonly property names
1 parent 78317c8 commit 98c9b79

9 files changed

Lines changed: 390 additions & 29 deletions

README.md

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ _See testIntegration/webkit for a working example_
8686

8787
## Using node-java in existing maven projects
8888

89-
When using node-java in existing maven projects, all the dependencies and the class files of the project have to be pushed to the classpath.
89+
When using node-java in existing maven projects, all the dependencies and the class files of the project have to be pushed to the classpath.
9090

9191
One possible solution would be:
9292

@@ -571,7 +571,7 @@ __Example__
571571
572572
var thread = java.newInstanceSync("java.lang.Thread", myProxy);
573573
thread.start();
574-
574+
575575
<a name="javaisJvmCreated" />
576576
**java.isJvmCreated()**
577577
@@ -697,6 +697,53 @@ ShutdownHookHelper.setShutdownHookSync(java.newProxy('java.lang.Runnable', {
697697
698698
When you call a Java method through node-java, any arguments (V8/JavaScript objects) will be converted to Java objects on the v8 main thread via a call to v8ToJava (found in utils.cpp). The JavaScript object is not held on to and can be garbage collected by v8. If this is an async call, the reference count on the Java objects will be incremented. The Java method will be invoked in a node.js async thread (see uv_queue_work). When the method returns, the resulting object will be returned to the main v8 thread and converted to JavaScript objects via a call to javaToV8 and the Java object's reference count will then be decremented to allow for garbage collection. The resulting v8 object will then be returned to the callers callback function.
699699
700+
# Static member name conficts ('name', 'arguments', 'caller')
701+
702+
The Javscript object returned by `java.import(classname)` is a Javascript constructor Function, implemented such that you can create instances of the Java class. For example:
703+
704+
```javascript
705+
var Test = java.import('Test');
706+
var test = new Test();
707+
708+
Test.someStaticMethod(function(err, result) { ... });
709+
710+
var value1 = Test.NestedEnum.Value1;
711+
```
712+
713+
But Javascript reserves a few property names of Function objects: `name`, `arguments`, and `caller`. If your class has public static members (either methods or fields) with these names, node-java is unable to create the necessary property to implement the class's API. For example, suppose your class `Test` implements a static method named `caller`, or has a `NestedEnum` with a value `name`:
714+
715+
```java
716+
public class Test {
717+
...
718+
public static String caller() { return "something"; }
719+
public enum NestedEnum { foo, name };
720+
}
721+
```
722+
723+
In Javascript, you would expect to be able to use those static members like this:
724+
725+
```javascript
726+
var Test = java.import('Test');
727+
Test.caller(function(err, result) { ... }); // ERROR
728+
var value = Test.NestedEnum.name; // ERROR
729+
```
730+
731+
Node-java can't create those properties, so the above code won't work. Instead, node-java appends a suffix to the name. The default suffix is simpy an underscore `_`, but you can change the suffix using asyncOptions:
732+
733+
```javascript
734+
var java = require('java');
735+
736+
java.asyncOptions = {
737+
asyncSuffix: "",
738+
syncSuffix: "Sync",
739+
ifReadOnlySuffix: "_alt"
740+
};
741+
742+
var Test = java.import('Test');
743+
Test.caller_alt(function(err, result) { ... }); // OK
744+
var value = Test.NestedEnum.name_alt; // OK
745+
```
746+
700747
# Troubleshooting
701748
702749
## Error: Cannot find module '../build/jvm_dll_path.json'

lib/nodeJavaBridge.js

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ java.nativeBindingLocation = binaryPath;
2121

2222
var syncSuffix = undefined;
2323
var asyncSuffix = undefined;
24+
var ifReadOnlySuffix = '_';
2425

2526
var SyncCall = function(obj, method) {
2627
if (syncSuffix === undefined)
@@ -170,6 +171,10 @@ java.onJvmCreated = function() {
170171
} else {
171172
throw new Error('In asyncOptions, if either promiseSuffix or promisify is defined, both most be.');
172173
}
174+
175+
if (_.isString(java.asyncOptions.ifReadOnlySuffix) && java.asyncOptions.ifReadOnlySuffix !== '') {
176+
ifReadOnlySuffix = java.asyncOptions.ifReadOnlySuffix;
177+
}
173178
} else {
174179
syncSuffix = 'Sync';
175180
asyncSuffix = '';
@@ -179,19 +184,26 @@ java.onJvmCreated = function() {
179184
var MODIFIER_PUBLIC = 1;
180185
var MODIFIER_STATIC = 8;
181186

182-
function isWritable(obj, prop) {
183-
if (!prop || !obj) {
184-
return false;
185-
}
187+
function isWritable(prop) {
188+
// If the property has no descriptor, or wasn't explicitly marked as not writable or not configurable, assume it is.
189+
// We check both desc.writable and desc.configurable, since checking desc.writable alone is not sufficient
190+
// (e.g. for either .caller or .arguments).
191+
// It may be that checking desc.configurable is sufficient, but the specification doesn't make this definitive,
192+
// and there is no harm in checking both.
193+
var desc = Object.getOwnPropertyDescriptor(function() {}, prop) || {};
194+
return desc.writable !== false && desc.configurable !== false;
195+
}
186196

187-
// If the property has no descriptor, or wasn't explicitly marked as not writable,
188-
// assume it is.
189-
return (Object.getOwnPropertyDescriptor(obj, prop) || {}).writable !== false;
197+
function usableName(name) {
198+
if (!isWritable(name)) {
199+
name = name + ifReadOnlySuffix;
200+
}
201+
return name;
190202
}
191203

192204
java.import = function(name) {
193205
var clazz = java.findClassSync(name); // TODO: change to Class.forName when classloader issue is resolved.
194-
var result = function() {
206+
var result = function javaClassConstructorProxy() {
195207
var args = [name];
196208
for (var i = 0; i < arguments.length; i++) {
197209
args.push(arguments[i]);
@@ -209,10 +221,11 @@ java.import = function(name) {
209221
if (((modifiers & MODIFIER_PUBLIC) === MODIFIER_PUBLIC)
210222
&& ((modifiers & MODIFIER_STATIC) === MODIFIER_STATIC)) {
211223
var fieldName = SyncCall(fields[i], 'getName')();
212-
result.__defineGetter__(fieldName, function(name, fieldName) {
224+
var jsfieldName = usableName(fieldName);
225+
result.__defineGetter__(jsfieldName, function(name, fieldName) {
213226
return java.getStaticFieldValue(name, fieldName);
214227
}.bind(this, name, fieldName));
215-
result.__defineSetter__(fieldName, function(name, fieldName, val) {
228+
result.__defineSetter__(jsfieldName, function(name, fieldName, val) {
216229
java.setStaticFieldValue(name, fieldName, val);
217230
}.bind(this, name, fieldName));
218231
}
@@ -232,19 +245,19 @@ java.import = function(name) {
232245
if (((modifiers & MODIFIER_PUBLIC) === MODIFIER_PUBLIC)
233246
&& ((modifiers & MODIFIER_STATIC) === MODIFIER_STATIC)) {
234247
var methodName = SyncCall(methods[i], 'getName')();
235-
var syncName = methodName + syncSuffix;
236-
var asyncName = methodName + asyncSuffix;
237-
var promiseName = methodName + promiseSuffix;
238248

239-
if (isWritable(result, syncName)) {
249+
if (_.isString(syncSuffix)) {
250+
var syncName = usableName(methodName + syncSuffix);
240251
result[syncName] = java.callStaticMethodSync.bind(java, name, methodName);
241252
}
242253

243-
if (typeof asyncSuffix === 'string' && isWritable(result, asyncName)) {
254+
if (_.isString(asyncSuffix)) {
255+
var asyncName = usableName(methodName + asyncSuffix);
244256
result[asyncName] = java.callStaticMethod.bind(java, name, methodName);
245257
}
246258

247-
if (promisify && isWritable(result, promiseName)) {
259+
if (promisify && _.isString(promiseSuffix)) {
260+
var promiseName = usableName(methodName + promiseSuffix);
248261
result[promiseName] = promisify(java.callStaticMethod.bind(java, name, methodName));
249262
}
250263
}

test/Test$Enum.class

972 Bytes
Binary file not shown.

test/Test.class

144 Bytes
Binary file not shown.

test/Test.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,23 @@ public static enum StaticEnum {
171171
public static String varArgsSignature(Long... args) { return "Long..."; }
172172
public static String varArgsSignature(Number... args) { return "Number..."; }
173173
public static String varArgsSignature(String... args) { return "String..."; }
174-
// Test readonly properties on functions that throw errors in certain versions of
175-
// node.
176-
public static void name() {}
174+
175+
// The Javascript object returned by java.import(classname) is a Function object
176+
// so that it can be used as a constructor.
177+
// Javascript reserves some properties of Function as non-writable or non-configurable:
178+
// 'name', 'arguments', 'caller'.
179+
// This means we can't expose a static member such as 'name()' with that name.
180+
// We instead append a suffix (asyncOptions.ifReadOnlySuffix)
181+
// The following static members are used for unit tests involving these cases.
182+
183+
// For testing static member functions with reserved names
184+
public static String name() { return "name"; }
185+
public static String arguments() { return "arguments"; }
186+
public static String caller() { return "caller"; }
187+
188+
// For testing public static fields with reserved names
189+
public enum Enum {
190+
foo, bar, // non-reserved
191+
name, arguments, caller // reserved
192+
};
177193
}

test/importClass-test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
var java = require("../testHelpers").java;
44
var nodeunit = require("nodeunit");
55
var util = require("util");
6+
var _ = require("lodash");
67

78
exports['Import Class'] = nodeunit.testCase({
89
tearDown: function (callback) {
@@ -25,5 +26,43 @@ exports['Import Class'] = nodeunit.testCase({
2526
test.equals(5, testObj.getIntSync());
2627
test.done();
2728
});
29+
},
30+
31+
"import TestEnum with unsable name": function (test) {
32+
test.expect(5);
33+
var TestEnum = java.import('Test$Enum');
34+
35+
// 'foo' and 'bar' are valid enum names
36+
test.strictEqual(TestEnum.foo.toStringSync(), "foo");
37+
test.strictEqual(TestEnum.bar.toStringSync(), "bar");
38+
39+
_.forEach(['name', 'arguments', 'caller'], function(prop) {
40+
test.throws(
41+
function() {
42+
// The enum also defines 'name', 'caller', and 'attributes', but Javascript prevents us from using them,
43+
// since these are unwritable properties of Function.
44+
var x = TestEnum[prop].toStringSync();
45+
},
46+
TypeError
47+
);
48+
});
49+
test.done();
50+
},
51+
52+
"import TestEnum and use alternate name": function (test) {
53+
test.expect(5);
54+
var TestEnum = java.import('Test$Enum');
55+
56+
// 'foo' and 'bar' are valid enum names
57+
test.strictEqual(TestEnum.foo.toStringSync(), "foo");
58+
test.strictEqual(TestEnum.bar.toStringSync(), "bar");
59+
60+
// 'name', 'caller', and 'arguments' are not, so we must use e.g. 'name_' to reference the enum.
61+
// But note that the value is still e.g. "name".
62+
test.strictEqual(TestEnum.name_.toStringSync(), "name");
63+
test.strictEqual(TestEnum.arguments_.toStringSync(), "arguments");
64+
test.strictEqual(TestEnum.caller_.toStringSync(), "caller");
65+
test.done();
2866
}
67+
2968
});

test/java-callStaticMethod-test.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ exports['Java - Call Static Method'] = nodeunit.testCase({
268268
test.equal(str, "Value1");
269269
test.done();
270270
},
271-
271+
272272
"Call static method with varargs": function(test) {
273273
var Test = java.import("Test");
274274

@@ -279,6 +279,14 @@ exports['Java - Call Static Method'] = nodeunit.testCase({
279279
test.equal(str, "5abc");
280280

281281
test.done();
282-
}
282+
},
283283

284+
"Call static method named name_": function(test) {
285+
test.expect(1);
286+
var Test = java.import("Test");
287+
Test.name_(function(err) {
288+
test.ifError(err);
289+
test.done();
290+
});
291+
}
284292
});

testAsyncOptions/testAsyncSuffixSyncDefault.js

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,35 @@ var java = require("../");
66
var assert = require("assert");
77
var _ = require('lodash');
88

9-
java.asyncOptions = {
10-
syncSuffix: "",
11-
asyncSuffix: "Async"
12-
};
13-
149
module.exports = {
10+
launch: function(test) {
11+
test.expect(4);
12+
java.asyncOptions = {
13+
syncSuffix: "",
14+
asyncSuffix: "Async",
15+
ifReadOnlySuffix: "_alt"
16+
};
17+
18+
function before(callback) {
19+
java.classpath.push('test/');
20+
test.ok(!java.isJvmCreated());
21+
callback();
22+
}
23+
24+
function after(callback) {
25+
test.ok(java.isJvmCreated());
26+
callback();
27+
}
28+
29+
java.registerClient(before, after);
30+
31+
java.ensureJvm(function(err) {
32+
test.ifError(err);
33+
test.ok(java.isJvmCreated());
34+
test.done();
35+
});
36+
},
37+
1538
testAPI: function(test) {
1639
test.expect(5);
1740
var arrayList = java.newInstanceSync("java.util.ArrayList");
@@ -82,5 +105,58 @@ module.exports = {
82105
});
83106
});
84107
});
85-
}
108+
},
109+
110+
// See testUnusableMethodName.js for the purpose of these last two tests.
111+
// In that test, Test.name_alt() is an async method.
112+
// In this test, it is a sync method.
113+
testUnusableMethodNameThrows: function(test) {
114+
test.expect(1);
115+
var Test = java.import("Test");
116+
test.ok(Test);
117+
test.throws(
118+
function() {
119+
Test.name();
120+
},
121+
function(err) {
122+
if (err instanceof TypeError) {
123+
test.done();
124+
return true;
125+
} else {
126+
test.done(err);
127+
return false;
128+
}
129+
}
130+
);
131+
},
132+
133+
testAlternateMethodNameWorks: function(test) {
134+
test.expect(4);
135+
var Test = java.import("Test");
136+
test.ok(Test);
137+
test.strictEqual(Test.name_alt(), "name");
138+
test.strictEqual(Test.caller_alt(), "caller");
139+
test.strictEqual(Test.arguments_alt(), "arguments");
140+
test.done();
141+
},
142+
143+
testReservedFieldName: function(test) {
144+
test.expect(7);
145+
var TestEnum = java.import("Test$Enum");
146+
test.ok(TestEnum);
147+
148+
// 'foo' and 'bar' are valid enum names
149+
test.strictEqual(TestEnum.foo.toString(), "foo");
150+
test.strictEqual(TestEnum.bar.toString(), "bar");
151+
152+
// TestEnum.name is actually the name of the proxy constructor function.
153+
test.strictEqual(TestEnum.name, "javaClassConstructorProxy");
154+
155+
// Instead we need to acccess TestEnum.name_alt
156+
test.strictEqual(TestEnum.name_alt.toString(), "name");
157+
test.strictEqual(TestEnum.caller_alt.toString(), "caller");
158+
test.strictEqual(TestEnum.arguments_alt.toString(), "arguments");
159+
160+
test.done();
161+
},
86162
}

0 commit comments

Comments
 (0)