0

I'm trying to run a javascript file from a java project. Currently i'm using Nashorn java script engine to run the script and maven to compile and create a jar file.

1) Using Nashorn?

I get the warning:

Warning: Nashorn engine is planned to be removed from a future JDK release

I'm using version openjdk 11. Would this be much of an issue? and if so what would be the best alternative for what i'm trying to do?

2) I can't get it to work when its in a jar file

I get a the FileNotFoundException

Java.io.FileNotFoundException: /Script.js (No such file or directory)

I have a javascript file Script.js and two java files, App.java which includes my main method, and Process.java which is trying to find the javascript file.

src
 ┣ main
 ┃ ┣ java
 ┃ ┃ ┗ com
 ┃ ┃ ┃ ┗ group
 ┃ ┃ ┃ ┃ ┣ App.java
 ┃ ┃ ┃ ┃ ┗ Processor.java
 ┃ ┗ resources
 ┃ ┃ ┗ Script.js
 ┗ test
 ┃ ┗ java
 ┃ ┃ ┗ com
 ┃ ┃ ┃ ┗ group
 ┃ ┃ ┃ ┃ ┗ AppTest.java

The App.java file will construct a Processor object and call the function connect.

public class App 
{
    public static void main( String[] args )
    {
        Processor p = new Processor();
        p.connect();
    }
}

Process.java

import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import java.io.FileReader;
import java.io.FileNotFoundException;

public class Processor {

...

  /*LINK TO JAVASCRIPT*/
    public void connect(){
        // get instance of the JavaScript engine
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("nashorn");
 
        // Load and execute the script
        try {
            engine.eval(new FileReader("/Script.js"));
        }
        catch (FileNotFoundException ex) {
            ex.printStackTrace();
        }
        catch (ScriptException ex) {
            ex.printStackTrace();
        }
    }
    
}

3) EDIT:

I found this might help:

getClass().getClassLoader().getResourceAsStream("/Script.js");

However this returns a InputStream, Would there be a way to run the javascript file from an InputStream or a way to get InputStream as a String?

noo
  • 304
  • 1
  • 9

2 Answers2

1

Using nashorn

I'm using version OpenJDK 11. Would this be much of an issue?

I'd say so. JDK15 is the first JDK that does not include nashorn. The latest 'long term support release is JDK17, and the current OpenJDK release is 18, so at this point, yes, this is a big issue.

and if so what would be the best alternative for what I'm trying to do?

Instead of relying on a javascript engine shipped with the JDK, you should pick a third-party javascript engine and ship it with your app. Java apps should be having many many dependencies (hundreds, perhaps) - JDK doesn't ship web template engines, web routing, DB interaction libraries (JDBC ships with the JDK but it isn't intended to be directly used, it's more the low-level library that things like JDBI, JOOQ, and Hibernate are built on top of), and so much more. On purpose - the JDK can never break backward compatibility, and that's too much handcuffed for many kinds of libraries. The solution is to use the robust and extensive open-source library. Every java build tool that is popular (be it Maven, Gradle, or even ant+ivy) makes it extremely simple; just put the GAV (Group::Artifact::Version) ID of the library you want in your build file and the tool takes care of everything (downloading it from a network of mirrored servers, tools to keep it up to date, shipping it, etc).

Here is a tutorial that adds the GraalVM javascript engine to a project. It's not the only javascript engine, you have alternatives. But it shows the principle.

I can't get it to work when it's in a jar file

new FileReader("/Script.js")

In java, File means File. An entry in a jar file is not a file. Hence, FileReader? You've already lost, you can't use that stuff for resources.

What you want to use is the GRAS mechanism instead: Ask java's classloader infrastructure to load resources from the same place it loads class files (think about it as follows: class files are resources - your java application doesn't work without them, and the JVM itself needs to load them as your app runs. Why should, say, an icon file for your GUI, or some javascript you want to throw through a javascript engine be any different?)

The only way to do that is GRAS. It comes in 2 forms:

Form 1: When the API takes a URL object.

Some APIs have a method that takes a URL. For example, ImageIcon and most other things in the GUI libraries java offers (swing, JavaFX, etc). For these:

class Example {
  void example() {
    URL resource = Example.class.getResource("foo.png");
    new ImageIcon(resource);
  }
}

Form 2: InputStream

If the API doesn't offer a method variant that takes a URL, surely there's one that takes either an InputStream (for byte-based things, like image files) or a Reader (for char-based things, like javascript). You can trivially turn an inputstream into a reader by specifying a charset encoding:

try (InputStream in = ...) {
  Reader r = new InputStreamReader(in, StandardCharsets.UTF_8);
  somethingThatNeedsAReader(r);
}

Given that these are resources, you must safely close them, hence try-with-resources is required (or roll your try/finally if you must). Otherwise, your app will eventually start crashing because it ran out of file handles.

So, all we need to know is how to obtain InputStream objects - as we can use the above trick to turn them into readers. It works like this:

try (InputStream in = Example.class.getResourceAsStream("foo.js")) {
  ....
}

That path string

The string you pass to getResource and getResourceAsStream is not quite a file path. It looks in the same place that Example.class (the class file that has the code for your Example class) is at, even if that is in a jar file. Even if it's generated on the fly, downloaded on the fly - java's classloader system is an abstract concept, who knows how it's loading these. That's the point: You just load from wherever that might be.

Such entries have a 'root' - and you can go from the root as well, by prefixing it with a slash.

Example:

  1. You have a class named Example, in package com.foo.
  2. It's packed into a jar file together with the js file.
  3. Therefore, in that jar file, there's an entry /com/foo/Example.class.
  4. Let's say the js file is in /com/foo/js/myapp.js

Then you can load that resource as an inputstream using either of these:

Example.class.getResource("js/myapp.js"); // relative to com/foo
Example.class.getResource("/com/foo/js/myapp.js"); // relative to root

It's not a file system - .. does not work, for example. Had it been in /js/myapp.js, then the leading-slash-strategy is the only option.

Alternatives that suck

These are common in tutorials and SO answers but you should never use them:

  1. getClass().getResource instead of Example.class.getResource - the getClass trick breaks if someone subclasses. It's unlikely, but why write code that is strictly worse, right? Avoid the style guide debate and use the version that works in strictly more cases.
  2. Example.class.getClassLoader().getResource() - this is needlessly longer, removes the ability for using relative paths, and breaks in certain cases. For example, classes loaded as boot load don't have a classloader, and the above code would therefore throw NullPointerException. But e.g. String.class.getResource("String.class") works fine.

Things you can't do

The resource loader system only has the 'primitive' of 'load a resource'. There is no list() operation! - a few libraries suggest it exists but these are hacks that will break in certain circumstances. Hence, if you want something like 'can I get a list of all js files in the js dir?' the answer is simple: No, that is not possible.

The general solution to this is to make a text file that lists the resources you're interested in, saves that into a text file with a known name. Now to 'fake' a "list all resources" operation, you load that file using getResource, process it, loading each entry with .getResource in turn. This is exactly how SPI works (which is how e.g. java itself discovers available JDBC drivers, charset providers, crypto providers, etc). Now you know what to search the web for if you need 'list all resources of some kind' :)

Fatih ARI
  • 15
  • 3
rzwitserloot
  • 85,357
  • 5
  • 51
  • 72
  • Thank you for the very detailed explanation and additional tips :) I'v changed to GraalVM and tried both methods of GRAS. For the inputstream method I have changed the `try` to `try (InputStream in = Process.class.getResourceAsStream("Script.js")) { Reader read = new InputStreamReader(in, StandardCharsets.UTF_8); graalEngine.eval(read); }` but get `Exception in thread "main" java.lang.NullPointerException`. Sorry if I miss understood anything. – noo Apr 10 '22 at 20:51
  • for the URL method when i print the url. I get `jar:file:/home/..../target/project-1.0-SNAPSHOT.jar!/Script.js` as an output. does this seem right? – noo Apr 10 '22 at 21:00
  • Yes, that seems fine. You get NPE if the resource isn't there. Clearly, `Script.js` isn't in your jar file. Make it be there. – rzwitserloot Apr 10 '22 at 22:38
  • I'v checked the jar. All resources get put in root. **projectJar/Script.js**. I also have: **ProjectJar/com/..** and **jar/META-INF/..** – noo Apr 11 '22 at 09:32
  • If that is true, then NPE can't happen. Clearly then that's not true. `jar tvf thejar.jar`, does that print `/Script.js`? Then put that (__with__ the leading slash!) in your `getResource` argument. Note the casing. It's case sensitive. – rzwitserloot Apr 11 '22 at 16:18
1

Added GraalVM to pom.xml

    <!-- https://mvnrepository.com/artifact/org.graalvm.js/js -->
        <dependency>
          <groupId>org.graalvm.js</groupId>
          <artifactId>js</artifactId>
          <version>22.0.0.2</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.graalvm.js/js-scriptengine -->
        <dependency>
          <groupId>org.graalvm.js</groupId>
          <artifactId>js-scriptengine</artifactId>
          <version>22.0.0</version>
        </dependency>

And changed the Process.java file:

    ScriptEngineManager manager = new ScriptEngineManager();
    ScriptEngine graalEngine = manager.getEngineByName("graal.js");
    
    try (InputStream in = Process.class.getResourceAsStream("/Script.js")) {
       graalEngine.eval(new InputStreamReader(in, StandardCharsets.UTF_8));
    }
noo
  • 304
  • 1
  • 9