Details about LIVE css hacking

Post to Twitter

We have a short video about our LIVE css hacking feature for mobile JavaFX applications on YouTube:

JavaFX mobile LIVE CSS hacking


We demonstrate a mobile JavaFX application, running on a Nexus 9 Tablet. It's a pure JavaFX application with our mobile application frame and it was deployed with JavaFXPorts. In the video, we change some styles on our Laptop and the application updates its styles on demand. This is really useful because resolution/screen size can be different. Sometimes the background color looks different than on a Desktop, ... And it's easy to try out new styles without application restarts - same principle as developer tools for Chrome or FireBug for Firefox.

But how did we implement this feature?

It was too easy :)

Here are the "secrets":

  • A simple socket server for the application
  • A custom URL handler for loading remote-retrieved styles
  • A simple File watcher for the client

That's it!

But to be honest, the custom URL handler was very tricky, because we tried to find a very smart, non-static, solution. And every platform has its specific handling and URL loading mechanism. But one step after another.

I guess you know that a socket server isn't a real problem. We wrote our own "generic" socket server some years ago, for JVx and it's not tricky. In principle, it works like this snippet:

ServerSocket serverSocket = new ServerSocket(9999);
Socket clientSocket = serverSocket.accept();

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

The problem is that you need a sort of protocol for the content. We've used our UniversalSerializer and send simple POJOs. The stylesheet POJO contains the css file as byte[]. Here's our server code:

CommunicationServer server = new CommunicationServer(findHost(), 9999);
server.addReceiver(r ->
{
    javafx.application.Platform.runLater(() ->
    {
        synchronized (oSync)
        {
            if (sLastCss != null)
            {
                logger.info("Remove remote CSS file ", sLastCss);

                launcher.getScene().getStylesheets().remove("remotecss://" + sLastCss);
               
                ObjectCache.remove(sLastCss);
            }

            sLastCss = ObjectCache.createKey() + ".css";
       
            ObjectCache.put(sLastCss, r.getObject(), -1);

            logger.info("Install remote CSS file ", sLastCss);
       
            launcher.getScene().getStylesheets().add("remotecss://" + sLastCss);
        }
    });
   
    return null;
});
server.start();

We add custom stylesheets with remotecss as protocol. The resource loading mechanism of Java(FX) tries to load the URL automatically. The protocol isn't a standard protocol and so we had to create a custom URL handler. The problem wasn't the handler itself because it's straight forward:

public class Handler extends URLStreamHandler
{
    @Override
    protected URLConnection openConnection(URL url) throws IOException
    {
        if (url.toString().toLowerCase().endsWith(".css"))
        {
            return new CssURLConnection(url);
        }
       
        throw new FileNotFoundException();
    }
}

And the CssURLConnection also wasn't a problem:

private class CssURLConnection extends URLConnection
{
    public CssURLConnection(URL pUrl)
    {
        super(pUrl);
    }

    @Override
    public void connect() throws IOException
    {
    }

    @Override
    public InputStream getInputStream() throws IOException
    {
        Object oCss = ObjectCache.get(getURL().getHost());
       
        if (oCss instanceof String)
        {
            return new ByteArrayInputStream(((String)oCss).getBytes("UTF-8"));
        }
        else if (oCss instanceof byte[])
        {
            return new ByteArrayInputStream((byte[])oCss);
        }
       
        throw new FileNotFoundException();
    }    
}

The problem was that the JVM didn't know how to load remotecss:// URLs because it doesn't have a default handler. The JVM offers different solutions for this problem and the well known is:

URL.setURLStreamHandlerFactory(factory);

But this is a static mechanism and we didn't know if another library uses the same method. There's no getURLStreamHandlerFactory in URL and the solution wasn't good enough for us. For a simple test application this restriction shouldn't be a problem, but we didn't like it because we're framework developers.

There's another solution for the problem, because the source code of URL contains the static method:

static URLStreamHandler getURLStreamHandler(String protocol)

This method tries to load protocol handlers automatically, if definded via system property java.protocol.handler.pkgs or from package: sun.net.www.protocol.protocolname.Handler

After reading the source code, we found a blogpost about this feature, read more. It was easy to search with the right keyword after we knew the solution.

...be careful with security manager, but this shouldn't matter.

The URL implementation for Android is a little bit different because but if you set the system property, it'll work as expected.

The hardest parts were done and the last thing was the file watcher client because we planned to send the changed CSS file(s) to the app automatically. Here's our solution (Java8 style):

thWatcher = new Thread(() ->
{
    File fi = ResourceUtil.getFileForClass("/live.css").getParentFile();

    FileSystem fsys = FileSystems.getDefault();

    Path pathCss = fsys.getPath(fi.getAbsolutePath());

    try
    {
        try (final WatchService service = fsys.newWatchService())
        {
            WatchKey wkey = pathCss.register(service, StandardWatchEventKinds.ENTRY_MODIFY);

            while (!ThreadHandler.isStopped(thWatcher))
            {
                WatchKey key = service.take();

                for (WatchEvent<?> event : key.pollEvents())
                {
                    Kind<?> kind = event.kind();

                    if (kind == StandardWatchEventKinds.ENTRY_MODIFY)
                    {
                        Path changed = (Path)event.context();

                        if (changed.toString().equals(FileUtil.getName(CSS)))
                        {
                            File fiModified = new File(fi, changed.toString());

                            if (fiModified.lastModified() > 0)
                            {
                                if (lLastModified != fiModified.lastModified())
                                {
                                    try
                                    {
                                        client = getClient();
                                        client.send(FileUtil.getContent
                                           (ResourceUtil.getResourceAsStream("/live.css")));

                                        lLastModified = fiModified.lastModified();

                                        System.out.println("Sent CSS to application");
                                    }
                                    catch (Exception e)
                                    {
                                        // next try
                                        client = CommonUtil.close(client);
                                    }
                                }
                            }
                        }
                    }
                    else if (kind == StandardWatchEventKinds.OVERFLOW)
                    {
                        continue;
                    }
                }

                if (!key.reset())
                {
                    thWatcher = ThreadHandler.stop(thWatcher);
                }
            }

            wkey.reset();
        }
    }
    catch (Exception e)
    {
        e.printStackTrace();

        synchronized (TestCommunication.this)
        {
            TestCommunication.this.notify();
        }
    }
});

thWatcher.start();
ThreadHandler.add(thWatcher);

The source code examples aren't complete but I guess you can implement your own solution based on them. We made tests with iOS, Android and Desktop applications and didn't have any problems. The CSS editing feature would be a very useful extension for ScenicView but it's not available right now.

Our test devices:

Test devices

CSS hacking - test devices

Leave a Reply

Spam protection by WP Captcha-Free