java
Version:
Bridge API to connect with existing Java APIs.
328 lines (287 loc) • 11.4 kB
JavaScript
;
process.env.PATH += require("../build/jvm_dll_path.json");
const path = require("path");
const fs = require("fs");
const util = require("util");
let binaryPath = null;
try {
if (fs.statSync && fs.statSync(path.join(__dirname, "../build/Debug/nodejavabridge_bindings.node")).isFile()) {
binaryPath = path.resolve(path.join(__dirname, "../build/Debug/nodejavabridge_bindings.node"));
console.log("****** NODE-JAVA RUNNING IN DEBUG MODE ******");
}
} catch (_err) {
// do nothing fs.statSync just couldn't find the file
}
if (!binaryPath) {
binaryPath = path.resolve(path.join(__dirname, "../build/Release/nodejavabridge_bindings.node"));
}
const bindings = require(binaryPath);
const java = (module.exports = new bindings.Java());
java.promisify = util.promisify;
java.classpath.push(path.resolve(__dirname, "../src-java/commons-lang3-node-java.jar"));
java.classpath.push(path.resolve(__dirname, __dirname, "../src-java"));
java.classpath.pushDir = function (dir) {
fs.readdirSync(dir).forEach(function (file) {
java.classpath.push(path.resolve(dir, file));
});
};
java.nativeBindingLocation = binaryPath;
const callStaticMethod = java.callStaticMethod;
const callStaticMethodSync = java.callStaticMethodSync;
const newInstanceSync = java.newInstanceSync;
let syncSuffix = undefined;
let asyncSuffix = undefined;
let ifReadOnlySuffix = "_";
const SyncCall = function (obj, method) {
if (syncSuffix === undefined) {
throw new Error("Sync call made before jvm created");
}
const syncMethodName = method + syncSuffix;
if (syncMethodName in obj) {
return obj[syncMethodName].bind(obj);
} else {
throw new Error("Sync method not found:" + syncMethodName);
}
};
java.isJvmCreated = function () {
return typeof java.onJvmCreated !== "function";
};
const clients = [];
// We provide two methods for 'clients' of node-java to 'register' their use of java.
// By registering, a client gets the opportunity to be called asynchronously just before the JVM is created,
// and just after the JVM is created. The before hook function will typically be used to add to java.classpath.
// The function may peform asynchronous operations, such as async [glob](https://github.com/isaacs/node-glob)
// resolutions of wild-carded file system paths, and then notify when it has finished via either calling
// a node-style callback function, or by resolving a promise.
// A client can register function hooks to be called before and after the JVM is created.
// If the client doesn't need to be called back for either function, it can pass null or undefined.
// Both before and after here are assumed to be functions that accept one argument that is a node-callback function.
java.registerClient = function (before, after) {
if (java.isJvmCreated()) {
throw new Error("java.registerClient() called after JVM already created.");
}
const before_ =
before && before.length === 0
? function (cb) {
before();
cb();
}
: before;
const after_ =
after && after.length === 0
? function (cb) {
after();
cb();
}
: after;
clients.push({ before: before_, after: after_ });
};
// A client can register function hooks to be called before and after the JVM is created.
// If the client doesn't need to be called back for either function, it can pass null or undefined.
// Both before and after here are assumed to be functions that return Promises/A+ `thenable` objects.
java.registerClientP = function (beforeP, afterP) {
if (java.isJvmCreated()) {
throw new Error("java.registerClient() called after JVM already created.");
}
clients.push({ beforeP: beforeP, afterP: afterP });
};
async function runBeforeHooks() {
for (const client of clients) {
if (client.before) {
await new Promise((resolve, reject) => {
client.before((err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
}
if (client.beforeP) {
await client.beforeP();
}
}
}
function createJVMAsync() {
const _ignore = java.newLong(0); // called just for the side effect that it will create the JVM
}
async function runAfterHooks() {
for (const client of clients) {
if (client.after) {
await new Promise((resolve, reject) => {
client.after((err) => {
if (err) {
return reject(err);
}
return resolve();
});
});
}
if (client.afterP) {
await client.afterP();
}
}
}
async function initializeAll() {
await runBeforeHooks();
createJVMAsync();
await runAfterHooks();
}
// This function ensures that the JVM has been launched, asynchronously. The application can be notified
// when the JVM is fully created via either a node callback function, or via a promise.
// If the parameter `callback` is provided, it is assume be a node callback function.
// This function may be called multiple times -- the 2nd and subsequent calls are no-ops.
// However, once this method has been called (or the JVM is launched as a side effect of calling other java
// methods), then clients can no longer use the registerClient API.
java.ensureJvm = function (callback) {
// First see if the promise-style API should be used.
// This must be done first in order to ensure the proper API is used.
if (typeof callback === "undefined") {
// Create a promisified version of this function.
const launchJvmPromise = util.promisify(java.ensureJvm.bind(java));
// Call the promisified function, returning its result, which should be a promise.
return launchJvmPromise();
}
// If we get here, callback must be a node-style callback function. If not, throw an error.
else if (typeof callback !== "function") {
throw new Error("java.launchJvm(cb) requires its one argument to be a callback function.");
}
// Now check if the JVM has already been created. If so, we assume that the jvm was already successfully
// launched, and we can just implement idempotent behavior, i.e. silently notify that the JVM has been created.
else if (java.isJvmCreated()) {
return setImmediate(callback);
}
// Finally, queue the initializeAll function.
else {
return setImmediate(async () => {
try {
await initializeAll();
callback();
} catch (err) {
callback(err);
}
});
}
};
java.onJvmCreated = function () {
if (java.asyncOptions) {
syncSuffix = java.asyncOptions.syncSuffix;
asyncSuffix = java.asyncOptions.asyncSuffix;
if (typeof syncSuffix !== "string") {
throw new Error("In asyncOptions, syncSuffix must be defined and must a string");
}
const promiseSuffix = java.asyncOptions.promiseSuffix;
if (typeof promiseSuffix === "string") {
const methods = ["newInstance", "callMethod", "callStaticMethod"];
methods.forEach(function (name) {
java[name + promiseSuffix] = util.promisify(java[name]);
});
}
if (typeof java.asyncOptions.ifReadOnlySuffix === "string" && java.asyncOptions.ifReadOnlySuffix !== "") {
ifReadOnlySuffix = java.asyncOptions.ifReadOnlySuffix;
}
} else {
syncSuffix = "Sync";
asyncSuffix = "";
}
};
const MODIFIER_PUBLIC = 1;
const MODIFIER_STATIC = 8;
function isWritable(prop) {
// If the property has no descriptor, or wasn't explicitly marked as not writable or not configurable, assume it is.
// We check both desc.writable and desc.configurable, since checking desc.writable alone is not sufficient
// (e.g. for either .caller or .arguments).
// It may be that checking desc.configurable is sufficient, but the specification doesn't make this definitive,
// and there is no harm in checking both.
if (prop === "caller" || prop === "arguments") {
return false;
}
const desc = Object.getOwnPropertyDescriptor(function () {}, prop) || {};
return desc.writable !== false && desc.configurable !== false;
}
function usableName(name) {
if (!isWritable(name)) {
name = name + ifReadOnlySuffix;
}
return name;
}
java.import = function (name) {
const clazz = java.findClassSync(name); // TODO: change to Class.forName when classloader issue is resolved.
const result = function javaClassConstructorProxy() {
const args = [name];
for (let i = 0; i < arguments.length; i++) {
args.push(arguments[i]);
}
return newInstanceSync.apply(java, args);
};
result.class = clazz;
// copy static fields
const fields = SyncCall(clazz, "getDeclaredFields")();
for (let i = 0; i < fields.length; i++) {
const modifiers = SyncCall(fields[i], "getModifiers")();
if ((modifiers & MODIFIER_PUBLIC) === MODIFIER_PUBLIC && (modifiers & MODIFIER_STATIC) === MODIFIER_STATIC) {
const fieldName = SyncCall(fields[i], "getName")();
const jsfieldName = usableName(fieldName);
result.__defineGetter__(
jsfieldName,
function (name, fieldName) {
return java.getStaticFieldValue(name, fieldName);
}.bind(this, name, fieldName)
);
result.__defineSetter__(
jsfieldName,
function (name, fieldName, val) {
java.setStaticFieldValue(name, fieldName, val);
}.bind(this, name, fieldName)
);
}
}
let promiseSuffix;
if (java.asyncOptions) {
promiseSuffix = java.asyncOptions.promiseSuffix;
}
// copy static methods
const methods = SyncCall(clazz, "getDeclaredMethods")();
for (let i = 0; i < methods.length; i++) {
const modifiers = SyncCall(methods[i], "getModifiers")();
if ((modifiers & MODIFIER_PUBLIC) === MODIFIER_PUBLIC && (modifiers & MODIFIER_STATIC) === MODIFIER_STATIC) {
const methodName = SyncCall(methods[i], "getName")();
if (typeof syncSuffix === "string") {
const syncName = usableName(methodName + syncSuffix);
result[syncName] = callStaticMethodSync.bind(java, name, methodName);
}
if (typeof asyncSuffix === "string") {
const asyncName = usableName(methodName + asyncSuffix);
result[asyncName] = callStaticMethod.bind(java, name, methodName);
}
if (typeof promiseSuffix === "string") {
const promiseName = usableName(methodName + promiseSuffix);
result[promiseName] = util.promisify(callStaticMethod.bind(java, name, methodName));
}
}
}
// copy static classes/enums
const classes = SyncCall(clazz, "getDeclaredClasses")();
for (let i = 0; i < classes.length; i++) {
const modifiers = SyncCall(classes[i], "getModifiers")();
if ((modifiers & MODIFIER_PUBLIC) === MODIFIER_PUBLIC && (modifiers & MODIFIER_STATIC) === MODIFIER_STATIC) {
const className = SyncCall(classes[i], "getName")();
const simpleName = SyncCall(classes[i], "getSimpleName")();
Object.defineProperty(result, simpleName, {
get: function (result, simpleName, className) {
const c = java.import(className);
// memoize the import
const d = Object.getOwnPropertyDescriptor(result, simpleName);
d.get = function (c) {
return c;
}.bind(null, c);
Object.defineProperty(result, simpleName, d);
return c;
}.bind(this, result, simpleName, className),
enumerable: true,
configurable: true,
});
}
}
return result;
};