Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,48 @@ try {
}
```

### Promises

As of release 0.4.5 it is possible to create async methods that return promises by setting the asyncOptions property of the java object.

Example:

```javascript
var java = require("java");
java.asyncOptions = {
promiseSuffix: 'Promise',
promisify: require('when/node').lift
};
java.classpath.push("commons-lang3-3.1.jar");
java.classpath.push("commons-io.jar");

java.newInstancePromise("java.util.ArrayList")
.then(function(list) { return list.addPromise("item1"); })
.then(function(list) { return list.addPromise("item2"); })
.catch(function(err) { /* handle error */ });
```

* If you don't need promise-returning methods, simply leave java.asyncOptions unset.
* Sync and standard async methods are still generated as in previous releases. In the future we may provide the option to disable generation of standard async methods.
* You are free to choose whatever non-empty suffix you want for the promise-returning methods, but you must specify a value.
* asyncOptions.promisify must be a function that given a node.js style async function as input returns a function that returns a promise that is resolved (or rejected) when the async function has completed. Several Promises libraries provide such functions. This *should* just work, but at the moment one prominent promises library doesn't.
* Note that it should be possible to mix use of two different Promises/A+ conforming libraries. You may be able to use one library for installing the asyncOptions.promisify function, and then use another library everywhere else in your application.

#### Tested Promises Libraries

##### [when](https://www.npmjs.com/package/when)
We use this package in our unit tests, and it passes under all 9 cases of our [test matrix](https://travis-ci.org/joeferner/node-java).

`promisify: require('when/node').lift`

##### [bluebird](https://www.npmjs.com/package/bluebird)
Does not work with node 0.8, but works with node 0.10 and 0.11.

`promisify: require('bluebird').promisify`

##### [Q](https://www.npmjs.com/package/q)
Unfortunately, the popular Q promises library currently does **NOT** work.

# Release Notes

### v0.2.0
Expand Down
23 changes: 23 additions & 0 deletions lib/nodeJavaBridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ java.classpath.push(path.resolve(__dirname, "../commons-lang3-node-java.jar"));
java.classpath.push(path.resolve(__dirname, __dirname, "../src-java"));
java.nativeBindingLocation = binaryPath;

java.onJvmCreated = function() {
if (java.asyncOptions) {
var suffix = java.asyncOptions.promiseSuffix;
var promisify = java.asyncOptions.promisify;
if (typeof suffix === 'string' && typeof promisify === 'function') {
var methods = ['newInstance', 'callMethod', 'callStaticMethod'];
methods.forEach(function (name) {
java[name + suffix] = promisify(java[name]);
});
}
}
}

var MODIFIER_PUBLIC = 1;
var MODIFIER_STATIC = 8;

Expand Down Expand Up @@ -42,6 +55,13 @@ java.import = function(name) {
}
}

var promisify = undefined;
var promiseSuffix;
if (java.asyncOptions && java.asyncOptions.promisify) {
promisify = java.asyncOptions.promisify;
promiseSuffix = java.asyncOptions.promiseSuffix;
}

// copy static methods
var methods = clazz.getDeclaredMethodsSync();
for (i = 0; i < methods.length; i++) {
Expand All @@ -50,6 +70,9 @@ java.import = function(name) {
var methodName = methods[i].getNameSync();
result[methodName + 'Sync'] = java.callStaticMethodSync.bind(java, name, methodName);
result[methodName] = java.callStaticMethod.bind(java, name, methodName);
if (promisify) {
result[methodName + promiseSuffix] = promisify(java.callStaticMethod.bind(java, name, methodName));
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
},
"devDependencies": {
"async": "~0.1.22",
"nodeunit": "0.9.0"
"nodeunit": "0.9.0",
"when": "~3.6.4"
},
"scripts": {
"test": "nodeunit test test8",
Expand Down
29 changes: 29 additions & 0 deletions src/java.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ NAN_METHOD(Java::New) {
NanObjectWrapHandle(self)->Set(NanNew<v8::String>("classpath"), NanNew<v8::Array>());
NanObjectWrapHandle(self)->Set(NanNew<v8::String>("options"), NanNew<v8::Array>());
NanObjectWrapHandle(self)->Set(NanNew<v8::String>("nativeBindingLocation"), NanNew<v8::String>("Not Set"));
NanObjectWrapHandle(self)->Set(NanNew<v8::String>("asyncOptions"), NanNull());

NanReturnValue(args.This());
}
Expand All @@ -104,6 +105,20 @@ v8::Local<v8::Value> Java::createJVM(JavaVM** jvm, JNIEnv** env) {
JavaVM* jvmTemp;
JavaVMInitArgs args;

v8::Local<v8::Value> asyncOptions = NanObjectWrapHandle(this)->Get(NanNew<v8::String>("asyncOptions"));
if (asyncOptions->IsObject()) {
v8::Local<v8::Object> asyncOptionsObj = asyncOptions.As<v8::Object>();
v8::Local<v8::Value> promisify = asyncOptionsObj->Get(NanNew<v8::String>("promisify"));
if (!promisify->IsFunction()) {
return NanTypeError("asyncOptions.promisify must be a function");
}
v8::Local<v8::Value> suffix = asyncOptionsObj->Get(NanNew<v8::String>("promiseSuffix"));
if (!suffix->IsString()) {
return NanTypeError("asyncOptions.promiseSuffix must be a string");
}
NanAssignPersistent(m_asyncOptions, asyncOptionsObj);
}

// setup classpath
std::ostringstream classPath;
classPath << "-Djava.class.path=";
Expand Down Expand Up @@ -170,10 +185,20 @@ v8::Local<v8::Value> Java::createJVM(JavaVM** jvm, JNIEnv** env) {

m_classLoader = getSystemClassLoader(*env);

v8::Local<v8::Value> onJvmCreated = NanObjectWrapHandle(this)->Get(NanNew<v8::String>("onJvmCreated"));

// TODO: this handles sets put doesn't prevent modifing the underlying data. So java.classpath.push will still work which is invalid.
NanObjectWrapHandle(this)->SetAccessor(NanNew<v8::String>("classpath"), AccessorProhibitsOverwritingGetter, AccessorProhibitsOverwritingSetter);
NanObjectWrapHandle(this)->SetAccessor(NanNew<v8::String>("options"), AccessorProhibitsOverwritingGetter, AccessorProhibitsOverwritingSetter);
NanObjectWrapHandle(this)->SetAccessor(NanNew<v8::String>("nativeBindingLocation"), AccessorProhibitsOverwritingGetter, AccessorProhibitsOverwritingSetter);
NanObjectWrapHandle(this)->SetAccessor(NanNew<v8::String>("asyncOptions"), AccessorProhibitsOverwritingGetter, AccessorProhibitsOverwritingSetter);
NanObjectWrapHandle(this)->SetAccessor(NanNew<v8::String>("onJvmCreated"), AccessorProhibitsOverwritingGetter, AccessorProhibitsOverwritingSetter);

if (onJvmCreated->IsFunction()) {
v8::Local<v8::Function> onJvmCreatedFunc = onJvmCreated.As<v8::Function>();
v8::Local<v8::Object> context = NanNew<v8::Object>();
onJvmCreatedFunc->Call(context, 0, NULL);
}

return NanNull();
}
Expand All @@ -188,6 +213,10 @@ NAN_GETTER(Java::AccessorProhibitsOverwritingGetter) {
NanReturnValue(self->m_optionsArray);
} else if(!strcmp("nativeBindingLocation", *nameStr)) {
NanReturnValue(NanNew<v8::String>(Java::s_nativeBindingLocation.c_str()));
} else if(!strcmp("asyncOptions", *nameStr)) {
NanReturnValue(self->m_asyncOptions);
} else if(!strcmp("onJvmCreated", *nameStr)) {
// There is no good reason to get onJvmCreated, so just fall through to error below.
}

std::ostringstream errStr;
Expand Down
1 change: 1 addition & 0 deletions src/java.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Java : public node::ObjectWrap {
static std::string s_nativeBindingLocation;
v8::Persistent<v8::Array> m_classPathArray;
v8::Persistent<v8::Array> m_optionsArray;
v8::Persistent<v8::Object> m_asyncOptions;
};

#endif
26 changes: 26 additions & 0 deletions src/javaObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@
std::replace(className.begin(), className.end(), '[', 'a');
className = "nodeJava_" + className;

// Set up promisification
v8::Local<v8::Object> asyncOptions = NanObjectWrapHandle(java)->Get(NanNew<v8::String>("asyncOptions")).As<v8::Object>();
v8::Local<v8::Function> promisify;
std::string promiseSuffix;
bool promisifying = asyncOptions->IsObject();
if(promisifying) {
v8::Local<v8::Value> promisifyValue = asyncOptions->Get(NanNew<v8::String>("promisify"));
promisify = promisifyValue.As<v8::Function>();
v8::Local<v8::String> suffix = asyncOptions->Get(NanNew<v8::String>("promiseSuffix"))->ToString();
v8::String::Utf8Value utf8(suffix);
promiseSuffix.assign(*utf8);
}

v8::Local<v8::FunctionTemplate> funcTemplate;
if(sFunctionTemplates.find(className) != sFunctionTemplates.end()) {
//printf("existing className: %s\n", className.c_str());
Expand Down Expand Up @@ -54,6 +67,19 @@
v8::Handle<v8::String> methodNameSync = NanNew<v8::String>((methodNameStr + "Sync").c_str());
v8::Local<v8::FunctionTemplate> methodCallSyncTemplate = NanNew<v8::FunctionTemplate>(methodCallSync, methodName);
funcTemplate->PrototypeTemplate()->Set(methodNameSync, methodCallSyncTemplate->GetFunction());

if (promisifying) {
v8::Local<v8::Object> recv = NanNew<v8::Object>();
v8::Local<v8::Value> argv[] = { methodCallTemplate->GetFunction() };
v8::Local<v8::Value> result = promisify->Call(recv, 1, argv);
if (!result->IsFunction()) {
fprintf(stderr, "Promisified result is not a function -- asyncOptions.promisify must return a function.\n");
assert(result->IsFunction());
}
v8::Local<v8::Function> promFunction = result.As<v8::Function>();
v8::Handle<v8::String> methodNamePromise = NanNew<v8::String>((methodNameStr + promiseSuffix).c_str());
funcTemplate->PrototypeTemplate()->Set(methodNamePromise, promFunction);
}
}

std::list<jobject> fields;
Expand Down
149 changes: 149 additions & 0 deletions test/promises-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
var java = require("../testHelpers").java;

var nodeunit = require("nodeunit");
var util = require("util");

function isOpenJDK() {
var javaVendor = java.callStaticMethodSync('java.lang.System', 'getProperty', 'java.vendor');
var javaVersion = java.callStaticMethodSync('java.lang.System', 'getProperty', 'java.version');
return /Sun Microsystems Inc/.test(javaVendor) && /^1\.6/.test(javaVersion);
}

exports['Promises'] = nodeunit.testCase({
"create an instance of a class and call methods (getClassPromise & getNamePromise)": function(test) {
// Adapted from a test in simple-test.js
java.newInstance("java.util.ArrayList", function(err, list) {
test.ifError(err);
test.ok(list);
list.getClassPromise()
.then(function(clazz) {
test.ok(clazz);
return clazz.getNamePromise();
})
.then(function(name) {
test.strictEqual(name, "java.util.ArrayList");
})
.catch(function(err) {
test.ifError(err);
})
.then(function() {
test.expect(4);
test.done();
});
});
},

"import and execute promisified static method": function (test) {
var Test = java.import('Test');
Test.staticMethodPromise(99)
.then(function (result) {
test.strictEqual(100, result);
})
.catch(function (err) {
test.ifError(err);
})
.then(function() {
test.expect(1);
test.done();
});
},

"run promisified method of Java module (newInstancePromise)": function (test) {
java.newInstancePromise("java.util.ArrayList")
.then(function(list) {
test.ok(list);
return list.getClassPromise();
})
.then(function(clazz) {
test.ok(clazz);
return clazz.getNamePromise();
})
.then(function(name) {
test.strictEqual(name, "java.util.ArrayList");
})
.catch(function(err) {
test.ifError(err);
})
.then(function() {
test.expect(3);
test.done();
});
},

"run chained promisified methods (of class java.util.ArrayList)": function (test) {
var openJDK = isOpenJDK();
if (openJDK) {
// This test exposes a latent node-java bug with OpenJDK 1.6.
// See https://github.com/joeferner/node-java/issues/186
// For now, we simply don't run this test on OpenJDK 1.6.
test.done();
return;
}
var list;
var it;
var expectException = false;
java.newInstancePromise("java.util.ArrayList")
.then(function(_list) {
test.ok(_list);
list = _list;
return list.getClassPromise();
})
.then(function(clazz) {
test.ok(clazz);
return clazz.getNamePromise();
})
.then(function(name) {
test.strictEqual(name, "java.util.ArrayList");
})
.then(function() {
list.addPromise('hello');
})
.then(function() {
list.addPromise('world');
})
.then(function() {
list.addPromise('boo');
})
.then(function() {
return list.iteratorPromise();
})
.then(function(_it) {
test.ok(_it);
it = _it;
return it.nextPromise();
})
.then(function(val) {
test.ok(val);
console.log(typeof val, val);
test.strictEqual(val, 'hello'); // java.lang.InternalError exception thrown here with OpenJDK
return it.nextPromise();
})
.then(function(val) {
test.ok(val);
console.log(typeof val, val);
test.strictEqual(val, 'world');
return it.nextPromise();
})
.then(function(val) {
test.ok(val);
console.log(typeof val, val);
test.strictEqual(val, 'boo');
return it.hasNextPromise();
})
.then(function(more) {
console.log(typeof more, more);
test.strictEqual(more, false);
expectException = true;
return it.nextPromise();
})
.catch(function(err) {
test.ok(expectException);
})
.then(function() {
test.expect(12);
test.done();
});
}

});

18 changes: 17 additions & 1 deletion testHelpers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

var java = require("./");
java.options.push("-Djava.awt.headless=true");
//java.options.push('-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005');
Expand All @@ -7,4 +6,21 @@ java.classpath.push("test/");
java.classpath.push("test/commons-lang3-3.1.jar");
java.classpath.push("test8/");

java.asyncOptions = {
promiseSuffix: 'Promise',
promisify: require('when/node').lift // when works with all three node versions

// PASSES in all three node versions: 0.8.28, 0.10.35, 0.11.14
// promisify: require('when/node').lift // when works with all three node versions
// promisify: require('promise').denodeify // promise works with all three node versions
// promisify: require('vow-node').promisify // vow-node works with all three node versions

// PASSES in Node 0.10, 0.11. (incompatible with Node 0.8).
// promisify: require('bluebird').promisify // bluebird requires node >=0.10

// FAILS:
// promisify: require('q').denodeify // FAILS: Q triggers assertion failure in node_object_wrap.h, line 61
// promisify: require('p-promise').denodeify // FAILS: P-promise does not implement catch().
};

module.exports.java = java;