java-bridge 2.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ts-src/java.ts ADDED
@@ -0,0 +1,676 @@
1
+ import {
2
+ getClassFields,
3
+ getField,
4
+ getStaticField,
5
+ Java,
6
+ JavaOptions,
7
+ setField,
8
+ setStaticField,
9
+ } from '../native';
10
+ import {
11
+ JavaClassInstance,
12
+ JavaClassProxy,
13
+ JavaClassType,
14
+ JavaConstructor,
15
+ JavaVersion,
16
+ } from './definitions';
17
+ import { getJavaLibPath, getNativeLibPath } from './nativeLib';
18
+
19
+ /**
20
+ * The static java instance
21
+ */
22
+ let javaInstance: Java | null = null;
23
+
24
+ interface ImportedJavaClass {
25
+ 'class.proxy': object;
26
+ new (...args: any[]): any;
27
+ }
28
+
29
+ /**
30
+ * Options for creating the Java VM.
31
+ */
32
+ export interface JVMOptions extends JavaOptions {
33
+ /***
34
+ * The path to the native library
35
+ */
36
+ libPath?: string | null;
37
+ /**
38
+ * The version of the jvm to request
39
+ */
40
+ version?: string | JavaVersion | null;
41
+ /**
42
+ * Additional arguments to pass to the JVM
43
+ */
44
+ opts?: Array<string> | null;
45
+ }
46
+
47
+ /**
48
+ * Ensure the java vm is created.
49
+ * If the jvm is already created, this does nothing.
50
+ * If the vm is not created yet, the jvm will be created upon this call.
51
+ * This method is also called every time with no arguments when any call
52
+ * to the jvm is done in another method.
53
+ *
54
+ * ## Examples
55
+ * Specify the path to jvm.(dylib|dll|so) manually,
56
+ * specify the java version to use and set to use daemon threads.
57
+ * ```ts
58
+ * import { ensureJvm, JavaVersion } from 'java-bridge';
59
+ *
60
+ * ensureJvm({
61
+ * libPath: 'path/to/jvm.dll',
62
+ * version: JavaVersion.VER_9,
63
+ * useDaemonThreads: true
64
+ * });
65
+ * ```
66
+ *
67
+ * Let the plugin find the jvm.(dylib|dll|so)
68
+ * ```ts
69
+ * ensureJvm({
70
+ * JavaVersion.VER_9,
71
+ * useDaemonThreads: true
72
+ * });
73
+ * ```
74
+ *
75
+ * Let the plugin find the jvm.(dylib|dll|so) and use the default options
76
+ * ```ts
77
+ * ensureJvm();
78
+ * ```
79
+ *
80
+ * @param options the options to use when creating the jvm
81
+ */
82
+ export function ensureJvm(options?: JVMOptions): void {
83
+ if (!javaInstance) {
84
+ javaInstance = new Java(
85
+ options?.libPath,
86
+ options?.version,
87
+ options?.opts,
88
+ options,
89
+ getJavaLibPath(),
90
+ getNativeLibPath()
91
+ );
92
+ }
93
+ }
94
+
95
+ function defineFields(object: Record<string, any>, getStatic: boolean): void {
96
+ for (const field of getClassFields(object['class.proxy'], getStatic)) {
97
+ const getter = (): any =>
98
+ getStatic
99
+ ? getStaticField(object, field.name)
100
+ : getField(object, field.name);
101
+ if (field.isFinal) {
102
+ Object.defineProperty(object, field.name, {
103
+ get: getter,
104
+ enumerable: true,
105
+ });
106
+ } else {
107
+ Object.defineProperty(object, field.name, {
108
+ get: getter,
109
+ set: (value: any) =>
110
+ getStatic
111
+ ? setStaticField(object, field.name, value)
112
+ : setField(object, field.name, value),
113
+ enumerable: true,
114
+ });
115
+ }
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Import a class.
121
+ * Returns the constructor of the class to be created.
122
+ * For example, import "java.util.ArrayList" for a java Array List.
123
+ *
124
+ * Define a custom class type for the imported class and pass the
125
+ * constructor type of the class as the template parameter to get
126
+ * the proper type returned. You could also just cast the result.
127
+ *
128
+ * ## Examples
129
+ * ### Import ``java.util.ArrayList`` and create a new instance of it
130
+ * ```ts
131
+ * import { importClass } from 'java-bridge';
132
+ *
133
+ * // Import java.util.ArrayList
134
+ * const ArrayList = importClass('java.util.ArrayList');
135
+ *
136
+ * // Create a new instance of ArrayList
137
+ * const list = new ArrayList();
138
+ * ```
139
+ *
140
+ * ### Import ``java.util.ArrayList`` with types
141
+ * ```ts
142
+ * import { importClass, JavaClassInstance, JavaType } from 'java-bridge';
143
+ *
144
+ * /**
145
+ * * Definitions for class java.util.List
146
+ * *\/
147
+ * declare class List <T extends JavaType> extends JavaClassInstance {
148
+ * size(): Promise<number>;
149
+ * sizeSync(): number;
150
+ * add(e: T): Promise<void>;
151
+ * addSync(e: T): void;
152
+ * get(index: number): Promise<T>;
153
+ * getSync(index: number): T;
154
+ * toArray(): Promise<T[]>;
155
+ * toArraySync(): T[];
156
+ * isEmpty(): Promise<boolean>;
157
+ * isEmptySync(): boolean;
158
+ * }
159
+ *
160
+ * /**
161
+ * * Definitions for class java.util.ArrayList
162
+ * *\/
163
+ * declare class ArrayListClass<T extends JavaType> extends List<T> {
164
+ * public constructor(other: ArrayListClass<T>);
165
+ * public constructor();
166
+ * }
167
+ *
168
+ * // This causes the class to be import when the module is loaded.
169
+ * class ArrayList<T> extends importClass<typeof ArrayListClass>('java.util.ArrayList')<T> {}
170
+ *
171
+ * // Create a new ArrayList instance
172
+ * const list = new ArrayList<string>();
173
+ *
174
+ * // Add some contents to the list
175
+ * list.add('Hello');
176
+ * list.add('World');
177
+ *
178
+ * // Check the list contents
179
+ * assert.equals(list.sizeSync(), 2);
180
+ * assert.equals(list.getSync(0), 'Hello');
181
+ * assert.equals(list.getSync(1), 'World');
182
+ * ```
183
+ *
184
+ * @template T the type of the java class to import as a js type
185
+ * @param classname the name of the class to resolve
186
+ * @return the java class constructor
187
+ */
188
+ export function importClass<T extends JavaClassType = JavaClassType>(
189
+ classname: string
190
+ ): JavaConstructor<T> {
191
+ ensureJvm();
192
+ const constructor = javaInstance!.importClass(
193
+ classname
194
+ ) as ImportedJavaClass;
195
+ defineFields(constructor, true);
196
+
197
+ constructor.constructor = function (...args: any[]) {
198
+ const object = new constructor.prototype.constructor(...args);
199
+ defineFields(object, false);
200
+
201
+ return object;
202
+ };
203
+
204
+ return constructor as unknown as JavaConstructor<T>;
205
+ }
206
+
207
+ /**
208
+ * @inheritDoc importClass
209
+ */
210
+ export async function importClassAsync<T extends JavaClassType = JavaClassType>(
211
+ classname: string
212
+ ): Promise<JavaConstructor<T>> {
213
+ ensureJvm();
214
+ const constructor = (await javaInstance!.importClassAsync(
215
+ classname
216
+ )) as ImportedJavaClass;
217
+ defineFields(constructor, true);
218
+
219
+ constructor.constructor = function (...args: any[]) {
220
+ const object = new constructor.prototype.constructor(...args);
221
+ defineFields(object, false);
222
+
223
+ return object;
224
+ };
225
+
226
+ return constructor as unknown as JavaConstructor<T>;
227
+ }
228
+
229
+ /**
230
+ * Append a single or multiple jars to the class path.
231
+ *
232
+ * Just replaces the old internal class loader with a new one containing the new jars.
233
+ * This doesn't check if the jars are valid and/or even exist.
234
+ * The new classpath will be available to all classes imported after this call.
235
+ *
236
+ * ## Example
237
+ * ```ts
238
+ * import { appendClasspath } from 'java-bridge';
239
+ *
240
+ * // Append a single jar to the class path
241
+ * appendClasspath('/path/to/jar.jar');
242
+ *
243
+ * // Append multiple jars to the class path
244
+ * appendClasspath(['/path/to/jar1.jar', '/path/to/jar2.jar']);
245
+ * ```
246
+ * or
247
+ * ```ts
248
+ * import { classpath } from 'java-bridge';
249
+ *
250
+ * // Append a single jar to the class path
251
+ * classpath.append('/path/to/jar.jar');
252
+ * ```
253
+ *
254
+ * @param path the path(s) to add
255
+ */
256
+ export function appendClasspath(path: string | string[]): void {
257
+ ensureJvm();
258
+ javaInstance!.appendClasspath(path);
259
+ }
260
+
261
+ /**
262
+ * Check if `this_obj` is instance of `other`.
263
+ * This uses the native java `instanceof` operator.
264
+ * You may want to use this if {@link JavaClassInstance.instanceOf}
265
+ * is overridden, as that method itself does not override
266
+ * any method defined in the specific java class named 'instanceOf'.
267
+ *
268
+ * ## Example
269
+ * ```ts
270
+ * import { instanceOf, importClass } from 'java-bridge';
271
+ *
272
+ * const ArrayList = importClass('java.util.ArrayList');
273
+ * const list = new ArrayList();
274
+ *
275
+ * isInstanceOf(list, ArrayList); // true
276
+ * isInstanceOf(list, 'java.util.ArrayList'); // true
277
+ * isInstanceOf(list, 'java.util.List'); // true
278
+ * isInstanceOf(list, 'java.util.Collection'); // true
279
+ * isInstanceOf(list, 'java.lang.Object'); // true
280
+ * isInstanceOf(list, 'java.lang.String'); // false
281
+ *
282
+ * // You can also use the instanceOf method (if not overridden)
283
+ * list.instanceOf(ArrayList); // true
284
+ * list.instanceOf('java.util.ArrayList'); // true
285
+ * list.instanceOf('java.util.List'); // true
286
+ * list.instanceOf('java.util.Collection'); // true
287
+ * list.instanceOf('java.lang.Object'); // true
288
+ * list.instanceOf('java.lang.String'); // false
289
+ * ```
290
+ *
291
+ * @param this_obj the object to check
292
+ * @param other the class or class name to check against
293
+ * @return true if `this_obj` is an instance of `other`
294
+ */
295
+ export function isInstanceOf<T extends typeof JavaClassInstance>(
296
+ this_obj: JavaClassInstance,
297
+ other: string | T
298
+ ): boolean {
299
+ ensureJvm();
300
+ return javaInstance!.isInstanceOf(this_obj, other);
301
+ }
302
+
303
+ /**
304
+ * Methods for altering and querying the class path.
305
+ * @example
306
+ * import { classpath } from 'java-bridge';
307
+ *
308
+ * // Append a jar to the class path
309
+ * classpath.append('/path/to/jar.jar');
310
+ *
311
+ * assert.equal(classpath.get().length, 1);
312
+ * assert.equal(classpath.get()[0], '/path/to/jar.jar');
313
+ */
314
+ export namespace classpath {
315
+ /**
316
+ * @inheritDoc appendClasspath
317
+ */
318
+ export function append(path: string | string[]): void {
319
+ appendClasspath(path);
320
+ }
321
+
322
+ /**
323
+ * Get the loaded jars in the class path
324
+ *
325
+ * @returns a list of the loaded jars
326
+ */
327
+ export function get(): string[] {
328
+ ensureJvm();
329
+ return javaInstance!.loadedJars;
330
+ }
331
+ }
332
+
333
+ /**
334
+ * A callback for any output redirected from stdout/stderr from the java process.
335
+ *
336
+ * @param err an error if the conversion of the output failed.
337
+ * This is null if the output was valid. This will probably never be set.
338
+ * @param data the data that was converted. This is unset if <code>err</code> is set.
339
+ */
340
+ export type StdoutCallback = (err: Error | null, data?: string) => void;
341
+
342
+ /**
343
+ * The class guarding the stdout redirect.
344
+ * Keep this instance in scope to not lose the redirect.
345
+ * As soon as this gets garbage collected, the redirection
346
+ * of the stdout/stderr will be stopped. Only one instance
347
+ * of this can exist at a time. Call {@link reset} to stop
348
+ * redirecting the program output and release this class
349
+ * instance early.
350
+ *
351
+ * This can be created by calling {@link stdout.enableRedirect}.
352
+ *
353
+ * ## Example
354
+ * ```ts
355
+ * import { stdout } from 'java-bridge';
356
+ *
357
+ * const guard = stdout.enableRedirect((_, data) => {
358
+ * console.log('Stdout:', data);
359
+ * }, (_, data) => {
360
+ * console.error('Stderr:', data);
361
+ * });
362
+ *
363
+ * // Change the receiver method
364
+ * guard.on('stderr', (_, data) => {
365
+ * console.warn('Stderr:', data);
366
+ * });
367
+ *
368
+ * // Disable a receiver
369
+ * guard.on('stdout', null);
370
+ *
371
+ * // Disable stdout redirect
372
+ * guard.reset();
373
+ * ```
374
+ *
375
+ * ## See also
376
+ * * {@link stdout.enableRedirect}
377
+ */
378
+ export interface StdoutRedirectGuard {
379
+ /**
380
+ * Set the stdout/stderr event handler.
381
+ * Pass <code>null</code> to disable this specific handler.
382
+ * Only accepts 'stdout' and 'stderr' as the <code>event</code>
383
+ * argument. Overwrites the previous handler.
384
+ *
385
+ * @param event the event to listen on
386
+ * @param callback the callback
387
+ */
388
+ on(event: 'stdout' | 'stderr', callback: StdoutCallback | null): void;
389
+
390
+ /**
391
+ * Reset this <code>StdoutRedirectGuard</code> instance.
392
+ * After this call, the stdout/stderr will no longer
393
+ * be redirected to the specified methods and any call
394
+ * to this class will throw an error as this counts as destroyed.
395
+ */
396
+ reset(): void;
397
+ }
398
+
399
+ /**
400
+ * A namespace containing methods for redirecting the stdout/stderr of the java process.
401
+ *
402
+ * ## See also
403
+ * * {@link StdoutRedirectGuard}
404
+ * * {@link stdout.enableRedirect}
405
+ */
406
+ export namespace stdout {
407
+ /**
408
+ * Enable stdout/stderr redirection.
409
+ *
410
+ * Pass methods for the stdout and stderr output to be redirected to.
411
+ * These methods must accept an error as the first argument,
412
+ * although this will probably never be set and can be ignored.
413
+ * The second argument is the data that was redirected.
414
+ *
415
+ * Setting any method to ``null`` or ``undefined`` will disable the redirect for that method.
416
+ * This also allows you not set any handler which does not make any sense at all.
417
+ *
418
+ * ## Examples
419
+ * ### Redirect all data to the js console
420
+ * ```ts
421
+ * import { stdout } from 'java-bridge';
422
+ *
423
+ * const guard = stdout.enableRedirect((_, data) => {
424
+ * console.log('Stdout:', data);
425
+ * }, (_, data) => {
426
+ * console.error('Stderr:', data);
427
+ * });
428
+ * ```
429
+ *
430
+ * ### Redirect stdout to the js console
431
+ * ```ts
432
+ * const guard = stdout.enableRedirect((_, data) => {
433
+ * console.log('Stdout:', data);
434
+ * });
435
+ * ```
436
+ *
437
+ * ### Redirect stderr to the js console
438
+ * ```ts
439
+ * const guard = stdout.enableRedirect(null, (_, data) => {
440
+ * console.error('Stderr:', data);
441
+ * });
442
+ * ```
443
+ *
444
+ * ### Redirect nothing to the js console (y tho)
445
+ * This enables you to print nothing to nowhere.
446
+ * ```ts
447
+ * // Why would you do this?
448
+ * const guard = stdout.enableRedirect(null, null);
449
+ *
450
+ * // Or
451
+ * const guard = stdout.enableRedirect();
452
+ * ```
453
+ *
454
+ * @see StdoutRedirectGuard
455
+ * @see StdoutCallback
456
+ * @param stdout the callback to be called when stdout is received
457
+ * @param stderr the callback to be called when stderr is received
458
+ * @returns a <code>StdoutRedirectGuard</code> instance. Keep this instance in scope to not lose the redirect.
459
+ */
460
+ export function enableRedirect(
461
+ stdout?: StdoutCallback | null,
462
+ stderr?: StdoutCallback | null
463
+ ): StdoutRedirectGuard {
464
+ ensureJvm();
465
+ return javaInstance!.setStdoutCallbacks(stdout, stderr);
466
+ }
467
+ }
468
+
469
+ /**
470
+ * The class for implementing java interfaces.
471
+ * Keep this instance in scope to not destroy the java object.
472
+ * Call {@link reset} to instantly destroy this instance.
473
+ *
474
+ * ## Notes
475
+ * Keeping this instance alive may cause your process not to exit
476
+ * early. Thus, you must wait for the javascript garbage collector
477
+ * to destroy this instance even if you called {@link reset}.
478
+ *
479
+ * Once this instance has been destroyed, either by calling {@link reset}
480
+ * or the garbage collector, any call to any method defined earlier
481
+ * by {@link newProxy} will throw an error in the java process.
482
+ *
483
+ * ## Example
484
+ * ```ts
485
+ * import { newProxy } from 'java-bridge';
486
+ *
487
+ * const proxy = newProxy('path.to.MyInterface', {
488
+ * // Define methods...
489
+ * });
490
+ *
491
+ * // Do something with the proxy
492
+ * instance.someMethod(proxy);
493
+ *
494
+ * // Destroy the proxy
495
+ * proxy.reset();
496
+ * ```
497
+ *
498
+ * ## See also
499
+ * * {@link newProxy}
500
+ */
501
+ export interface JavaInterfaceProxy {
502
+ /**
503
+ * Destroy the proxy class.
504
+ * After this call any call to any method defined by the
505
+ * interface will throw an error on the java side. This error
506
+ * may be thrown back to the node process, if you are not
507
+ * specifically implementing methods that will be called
508
+ * from another (java) thread.
509
+ * Throws an error if the proxy has already been destroyed.
510
+ */
511
+ reset(): void;
512
+ }
513
+
514
+ /**
515
+ * An interface proxy method.
516
+ * Any arguments passed to this method are values converted from java values.
517
+ * The return value will be converted back to a java type.
518
+ *
519
+ * @param args the arguments passed from the java process
520
+ * @return the value to pass back to the java process
521
+ */
522
+ export type ProxyMethod = (...args: any[]) => any;
523
+ type InternalProxyRecord = Parameters<
524
+ typeof Java.prototype.createInterfaceProxy
525
+ >[1];
526
+
527
+ /**
528
+ * Create a new java interface proxy.
529
+ * This allows you to implement java interfaces in javascript.
530
+ *
531
+ * Pass an object as the second argument with the names of the
532
+ * methods you want to implement as keys and the implementations
533
+ * as values in order to expose these methods to the java process.
534
+ * Any arguments will be converted to javascript values and
535
+ * return values will be converted to java values.
536
+ *
537
+ * When the java process tries to call any method which is
538
+ * not implemented by the proxy, an error will be thrown.
539
+ *
540
+ * ## Examples
541
+ * ### Implement ``java.lang.Runnable``
542
+ * ```ts
543
+ * import { newProxy, importClass } from 'java-bridge';
544
+ *
545
+ * // Define the interface
546
+ * const runnable = newProxy('java.lang.Runnable', {
547
+ * run: (): void => {
548
+ * console.log('Hello World!');
549
+ * }
550
+ * });
551
+ *
552
+ * // Note: You can't do something like this:
553
+ * // runnable.run();
554
+ *
555
+ * // Pass the proxy to a java method instead:
556
+ * const Thread = importClass('java.lang.Thread');
557
+ * const thread = new Thread(runnable); // <- Pass the proxy here
558
+ *
559
+ * // NOTE: You don't have to call this asynchronously
560
+ * // as this call instantly returns.
561
+ * thread.startSync();
562
+ * ```
563
+ *
564
+ * ### Implement ``java.util.function.Function`` to transform a string
565
+ * ```ts
566
+ * const func = newProxy('java.util.function.Function', {
567
+ * // Any parameters and return types will be automatically converted
568
+ * apply: (str: string): string => {
569
+ * return str.toUpperCase();
570
+ * }
571
+ * });
572
+ *
573
+ * // Import the string class
574
+ * const JString = java.importClass('java.lang.String');
575
+ * const str = new JString('hello');
576
+ *
577
+ * // Pass the proxy.
578
+ * // NOTE: You must call this method async otherwise your program will hang.
579
+ * // See notes for more info.
580
+ * const transformed = await str.transform(func);
581
+ *
582
+ * assert.assertEquals(transformed, 'HELLO');
583
+ * ```
584
+ *
585
+ * Which is equivalent to the following java code:
586
+ * ```java
587
+ * Function<String, String> func = new Function<>() {
588
+ * @Override
589
+ * public String apply(String str) {
590
+ * return str.toUpperCase();
591
+ * }
592
+ * };
593
+ *
594
+ * String str = "hello";
595
+ * String transformed = str.transform(func);
596
+ * assert.assertEquals(transformed, "HELLO");
597
+ * ```
598
+ *
599
+ * #### Throwing exceptions
600
+ * Any exceptions thrown by the proxy will be converted to java exceptions
601
+ * and then rethrown in the java process. This may cause the exception
602
+ * to again be rethrown in the javascript process.
603
+ * ```ts
604
+ * const func = newProxy('java.util.function.Function', {
605
+ * apply: (str: string): string => {
606
+ * throw new Error('Something went wrong');
607
+ * }
608
+ * });
609
+ *
610
+ * const JString = java.importClass('java.lang.String');
611
+ * const str = new JString('hello');
612
+ *
613
+ * // This will re-throw the above error
614
+ * const transformed: never = await str.transform(func);
615
+ * ```
616
+ *
617
+ * ## Notes
618
+ * * Keep this instance in scope to not destroy the interface proxy.
619
+ * * Call {@link JavaInterfaceProxy.reset} to instantly destroy this instance.
620
+ * * If any method is queried by the java process and not implemented in here,
621
+ * an exception will be thrown in the java process.
622
+ * * Any errors thrown in the javascript process will be rethrown in the java process.
623
+ * * **When calling a java method that uses an interface defined by this, you must call
624
+ * that method using the interface asynchronously as Node.js is single threaded and can't
625
+ * wait for the java method to return while calling the proxy method at the same time.**
626
+ *
627
+ * ## See also
628
+ * * {@link JavaInterfaceProxy}
629
+ *
630
+ * @param interfaceName the name of the java interface to implement
631
+ * @param methods the methods to implement.
632
+ * @returns a proxy class to pass back to the java process
633
+ */
634
+ export function newProxy(
635
+ interfaceName: string,
636
+ methods: Record<string, ProxyMethod>
637
+ ): JavaInterfaceProxy {
638
+ ensureJvm();
639
+ const proxyMethods: InternalProxyRecord = Object.create(null);
640
+
641
+ for (const [name, method] of Object.entries(methods)) {
642
+ proxyMethods[name] = (
643
+ err: null | Error,
644
+ callback: (err: Error | null, data?: any | null) => void,
645
+ ...args: any[]
646
+ ): void => {
647
+ if (err) {
648
+ throw err;
649
+ }
650
+
651
+ try {
652
+ const res = method(...args);
653
+ callback(null, res);
654
+ } catch (e: any) {
655
+ if (e instanceof Error) {
656
+ callback(e);
657
+ } else {
658
+ callback(new Error(e.toString()));
659
+ }
660
+ }
661
+ };
662
+ }
663
+
664
+ return javaInstance!.createInterfaceProxy(
665
+ interfaceName,
666
+ proxyMethods
667
+ ) as JavaInterfaceProxy;
668
+ }
669
+
670
+ /**
671
+ * Get the static java instance.
672
+ * This has no real use, all important methods are exported explicitly.
673
+ */
674
+ export function getJavaInstance(): Java | null {
675
+ return javaInstance;
676
+ }