3

I am developing a simple Java client application, and I want to use HTML and a web application framework for its GUI instead of using Swing or JavaFX.

So I am designing, building and testing my entire application with Angular 6 (generated with angular-cli), and I'd like to use the JavaFX WebView and WebEngine components to point to my Angular UI.

Is there any easy/out-of-the-box/best-practice way to wire everything up?

I'd like the user just starting the application and not even realizing he's browsing something webby. If possible, I wouldn't even start any web server. I just want a self-contained application and using angular framework for GUI purpose only.

If I had to start a web server, I'd like to do so behind the scenes and don't want the user to realize.

Problems

  • I tried to just make the WebEngine point to the built angular app, but this does not work out-of-the-box with Angular-cli;
  • I tried to use com.sun.net.httpserver but all the explanations on the web only say how to implement your own dummy HttpHandler, so I was not able to understand how to bind the server to my actual built web application (which lies in a folder in my project path) or how to deploy my web application into the HttpServer, whichever is correct.
Daniele Repici
  • 312
  • 3
  • 18

2 Answers2

3

You can embed any HTML application using the webkit -- the webkit version of the jvm is relatively up-to-date, for the last time i checked, there may be few functions missing, WebGL maybe.

To make it work, don't use file:// protocol, and it is overkill to create your own webserver. You just have to define your own protocl and declare it in java layers.

Examples are here: Registering and using a custom java.net.URL protocol

Here is my custom protocol implementation:

/**
 * Register a protocol handler for URLs like this: <code>myapp:///pics/sland.gif</code><br>
 */
public class MyURLConnection extends URLConnection
{

    protected MyURLConnection(URL url) {
        super(url);
    }

    private byte[] data;

    @Override
    public void connect() throws IOException
    {
        if (connected)
        {
            return;
        }
        loadImage();
        connected = true;
    }

    public String getHeaderField(String name)
    {
        if ("Content-Type".equalsIgnoreCase(name))
        {
            return getContentType();
        }
        else if ("Content-Length".equalsIgnoreCase(name))
        {
            return "" + getContentLength();
        }
        return null;
    }

    public String getContentType()
    {
        String fileName = getURL().getFile();
        String ext = fileName.substring(fileName.lastIndexOf('.')+1);
        switch(ext){
        case "html":return "text/html";
        case "jpg":
        case "png":return "image/"+ext;
        case "js": return "application/javascript";
        }
        return "text/"+ext;
         // TODO: switch based on file-type "image/" + ext
    }

    public int getContentLength()
    {
        return data.length;
    }

    public long getContentLengthLong()
    {
        return data.length;
    }

    public boolean getDoInput()
    {
        return true;
    }

    public InputStream getInputStream() throws IOException
    {
        connect();
        return new ByteArrayInputStream(data);
    }

    /**
     * Reads all bytes from an input stream and writes them to an output stream.
     */
    private static long copy(InputStream source, OutputStream sink)
        throws IOException
    {
        long nread = 0L;
        byte[] buf = new byte[8192];
        int n;
        while ((n = source.read(buf)) > 0) {
            sink.write(buf, 0, n);
            nread += n;
        }
        return nread;
    }

    private void loadImage() throws IOException
    {
        if (data != null)
        {
            return;
        }

            String fileName = getURL().getFile();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            copy(this.getClass().getResourceAsStream(fileName),byteArrayOutputStream);
           data = byteArrayOutputStream.toByteArray();

    }

    public OutputStream getOutputStream() throws IOException
    {
        // this might be unnecessary - the whole method can probably be omitted for our purposes
        return new ByteArrayOutputStream();
    }

    public java.security.Permission getPermission() throws IOException
    {
        return null; // we need no permissions to access this URL
    }

}

public class MyURLHandler extends URLStreamHandler
{

    @Override
    protected URLConnection openConnection(URL url) throws IOException
    {
        return new MyURLConnection(url);
    }

}

public class MyURLStreamHandlerFactory implements URLStreamHandlerFactory
{

    public URLStreamHandler createURLStreamHandler(String protocol)
    {
        if (protocol.equals("myapp"))
        {
            return new MyURLHandler();
        }
        return null;
    }

}

Then just register the protocol in your javafx startup:

public void start(Stage stage) {
    URL.setURLStreamHandlerFactory(new MyURLStreamHandlerFactory());
    // etc. 

and use the url myapp://anyhost/yourfile.html in your webkit (anyhost is not used, use anything you want or modify the protocol)

Edit: I did it on my project (in progress) on github: you just have to launch the WebDesktop main class. It uses resources of src/main/resources https://github.com/pdemanget/blue-browser

pdem
  • 3,880
  • 1
  • 24
  • 38
  • I was able to make this solution work with a dummy *myfile.html*. But **it won't work** with an Angular application, i.e. it won't work when I make the WebEngine load the *index.html* of either the *src* folder or the *dist/my-app* folder created by angular-cli. – Daniele Repici Jun 21 '18 at 13:43
  • Did you really use another protocol than file? it is related to javascript, the content-type, and the same origin plicy: you have to serve your js with application/javascript content-type and have the same origin policy. My github example works with a javascript project, the compiled JS version of your angular project should work the same way. – pdem Jun 21 '18 at 14:50
  • @DanieleRepici Did you try without file protocol? – pdem Jun 21 '18 at 15:29
  • Yes, I did but unfortunately it did not work. Might [this problem](https://github.com/angular/angular/issues/13948) be related? EDIT: I don't think it is, because that still mentions the file protocol... Have you tried your solution with a compiled angular application? – Daniele Repici Jun 22 '18 at 07:48
  • Excuse me, I didn't, but it's up to you to try, we can't just give the solution , you have my project, and just have to replace my JS files with yours, it should work. – pdem Jun 22 '18 at 08:25
  • Tested and works with angluar (no reason why it should'nt work it's regular JS) – pdem Jun 22 '18 at 12:06
  • 1
    Sorry, I made it work now, not sure what was going on. Thank you for your support. – Daniele Repici Jun 25 '18 at 08:58
1

Eventually I managed to solve this by using embedded Jetty to serve my angular application.

@Slf4j
public class MyEmbeddedAngularApp extends Application {

    private static final String APPLICATION_TITLE = "My Embedded Angular Application";

    /**
     * The service executing the server.
     */
    private ExecutorService executor = Executors.newSingleThreadExecutor();

    @Override
    public void start(Stage stage) throws Exception {
        startServer();

        //Create the root AnchorPane
        AnchorPane root = new AnchorPane();

        //Initialize the Web UI
        WebView browser = new WebView();
        WebEngine webEngine = browser.getEngine();
        webEngine.load("http://localhost:2342");

        //add WebView to root
        root.getChildren().add(browser);

        //Setting up the scene
        Scene scene = new Scene(root);
        stage.setTitle(APPLICATION_TITLE);
        stage.setScene(scene);

        //Let the show begin!
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    private void startServer() throws Exception {
        Server server = new Server(2342);

        WebAppContext webapp = new WebAppContext();
        webapp.setContextPath("/");
        File warFile = new File("./src/main/resources/my-app/dist/my-app");
        webapp.setWar(warFile.getAbsolutePath());

        server.setHandler(webapp);
        server.setStopAtShutdown(true);

        server.start();

        server.dumpStdErr();

        executor.submit(() -> {
            try {
                server.join();
            } catch (InterruptedException e) {
                log.error("Could not start the server");
            }
        });
    }

}
Daniele Repici
  • 312
  • 3
  • 18