javarestart.WebClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for javarestart.WebClassLoader.java

Source

/*
 * Copyright (c) 2013-2015, Nikita Lipsky, Excelsior LLC.
 *
 *  Java ReStart is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Java ReStart is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with Java ReStart.  If not, see <http://www.gnu.org/licenses/>.
 *
*/
package javarestart;

import javarestart.protocols.ResourceRequest;
import org.json.simple.JSONObject;

import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;

/**
 * @author Nikita Lipsky
 */
public class WebClassLoader extends URLClassLoader {

    private URL baseURL;

    private final JSONObject descriptor;

    private List<ClassLoaderListener> listeners;

    private HashMap<String, byte[]> initialBundle = new HashMap<>();
    private volatile String curLoading;

    private AtomicReference<ResourceRequest> resourceRequest = new AtomicReference<>();

    private volatile boolean preloading;

    // Workaround for javafx.scene.media.AudioClip limitation.
    // It only supports http:// and file:// protocols,
    // so we convert java:// and wfx:// URLs to http:// to let .wav resources to be played
    // by AudioClip.
    // TODO: fix the limitation and contribute it to OpenJDK
    private boolean change4WavToHttp;

    private HashSet<String> includePackages = new HashSet<>();

    private WebClassLoader(URL url, JSONObject desc, boolean baseURL) throws IOException {
        super(new URL[0], Thread.currentThread().getContextClassLoader());
        this.baseURL = baseURL ? url : new URL(url, (String) desc.get("root"));
        final String protocol = this.baseURL.getProtocol();
        this.change4WavToHttp = Stream.of("java", "wfx").anyMatch(protocol::equals);
        this.descriptor = desc;
        parseIncludePackages();
        preLoadInitial();
    }

    private void preLoadInitial() {
        preloading = true;
        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    InputStream in = new URL(baseURL.getProtocol(), baseURL.getHost(), baseURL.getPort(),
                            baseURL.getPath() + "?bundle=initial").openStream();
                    while (true) {
                        int nameLength = Integer.parseInt(Utils.readAsciiLine(in), 16);
                        if (nameLength == 0) {
                            return;
                        }
                        String name = Utils.readAsciiLine(in); //TODO: name can be not Ascii
                        curLoading = name;
                        int resLength = Integer.parseUnsignedInt(Utils.readAsciiLine(in), 16);
                        if (resLength != -1) {
                            final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                            Utils.copy(in, buffer, resLength);
                            initialBundle.put(name, buffer.toByteArray());
                            Utils.readAsciiLine(in);
                        } else {
                            initialBundle.put(name, null);
                        }

                        ResourceRequest resourceRequest = WebClassLoader.this.resourceRequest.get();
                        if ((resourceRequest != null) && isPreLoaded(resourceRequest.getRequest())) {
                            resourceRequest.contentReady(getPreloadResource(resourceRequest.getRequest()));
                            WebClassLoader.this.resourceRequest.compareAndSet(resourceRequest, null);
                        }
                    }

                } catch (Throwable e) {
                    e.printStackTrace();
                } finally {
                    preloading = false;
                    curLoading = null;
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
    }

    private void parseIncludePackages() {
        String packsProp = getDescField("includePackages");
        if (packsProp == null) {
            return;
        }
        includePackages.addAll(Arrays.asList(packsProp.split(";")));
    }

    WebClassLoader(URL url, JSONObject desc) throws IOException {
        this(url, desc, false);
    }

    public void addListener(ClassLoaderListener listener) {
        if (listeners == null) {
            listeners = new LinkedList<>();
        }
        listeners.add(listener);
    }

    private void fireClassLoaded(String classname) {
        if (listeners == null)
            return;
        for (ClassLoaderListener l : listeners) {
            l.classLoaded(classname);
        }
    }

    private Class<?> tryToLoadClass(InputStream in) throws IOException {
        final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        Utils.copy(in, buffer);
        buffer.flush();

        byte buf[] = buffer.toByteArray();
        return defineClass(null, buf, 0, buf.length);
    }

    @Override
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        String res = name.replace('.', '/') + ".class";
        try {
            if (initialBundle.containsKey(res)) {
                byte[] classBytes = initialBundle.get(res);
                if (classBytes == null) {
                    throw new ClassNotFoundException("resource " + name + " not found");
                }
                initialBundle.remove(res);
                Class c = defineClass(null, classBytes, 0, classBytes.length);
                fireClassLoaded(name);
                return c;
            }
            URL resource = findResourceImpl(name.replace('.', '/') + ".class");
            if (resource == null) {
                throw new ClassNotFoundException("resource " + name + " not found");
            }
            Class c = tryToLoadClass(resource.openStream());
            fireClassLoaded(name);
            return c;
        } catch (ClassNotFoundException e) {
            throw e;
        } catch (final Exception e) {
            throw new ClassNotFoundException(e.getMessage(), e);
        }
    }

    private URL checkResourceExists(URL url) {
        if (url == null)
            return null;
        try {
            url.openStream().close();
            return url;
        } catch (Exception e) {
            return null;
        }
    }

    private static final int INCLUDE_PACKAGES_MAX_DEPTH = 3;

    private boolean potentiallyExists(String name) {
        if (includePackages.isEmpty()) {
            return true;
        }
        int i = 0;
        int pos = name.indexOf('/');
        if (pos == -1)
            return includePackages.contains(".");
        do {
            if (includePackages.contains(name.substring(0, pos)))
                return true;

            pos = name.indexOf('/', pos + 1);

        } while ((pos != -1) && (++i != INCLUDE_PACKAGES_MAX_DEPTH));
        return false;
    }

    private URL findResourceImpl(final String name) {
        boolean handleWavSpecially = change4WavToHttp && name.endsWith(".wav");
        if (handleWavSpecially && initialBundle.containsKey(name)) {
            int dot = name.lastIndexOf(".");
            String resName;
            String ext;
            if (dot == -1) {
                resName = name;
                ext = "";
            } else {
                resName = name.substring(0, dot);
                ext = name.substring(dot + 1);
            }
            int slash = resName.lastIndexOf("/");
            if (slash != -1) {
                resName = resName.substring(slash + 1);
            }
            File f = Utils.fetchResourceToTempFile(resName, ext, new ByteArrayInputStream(initialBundle.get(name)));
            try {
                return f.toURI().toURL();
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
        if (!potentiallyExists(name)) {
            return null;
        }
        try {
            return new URL(
                    //see comment above for this hack explanation
                    handleWavSpecially ? "http" : baseURL.getProtocol(), baseURL.getHost(), baseURL.getPort(),
                    baseURL.getPath() + '/' + name);
        } catch (final MalformedURLException e) {
            return null;
        }
    }

    @Override
    public URL findResource(final String name) {
        if (initialBundle.containsKey(name)) {
            return initialBundle.get(name) == null ? null : findResourceImpl(name);
        }
        return checkResourceExists(findResourceImpl(name));
    }

    /**
     * Returns none or zero resources: it just wraps findResource now.
     * TODO: extend client-server protocol to support fetching multiple resources by the given name.
     */
    @Override
    public Enumeration<URL> findResources(String name) throws IOException {
        URL url = findResource(name);
        if (url == null)
            return Collections.emptyEnumeration();
        return new Enumeration<URL>() {
            private boolean hasMore = true;

            @Override
            public boolean hasMoreElements() {
                return hasMore;
            }

            @Override
            public URL nextElement() {
                hasMore = false;
                return url;
            }
        };
    }

    @Override
    public InputStream getResourceAsStream(String name) {
        if (initialBundle.containsKey(name)) {
            byte[] bytes = initialBundle.get(name);
            return bytes == null ? null : new ByteArrayInputStream(bytes);
        }
        return super.getResourceAsStream(name);
    }

    @Override
    protected String findLibrary(final String libname) {
        // TODO: nix/mac/solaris?
        File temp = Utils.fetchResourceToTempFile(libname, ".dll", findResource(libname + ".dll"));
        return temp == null ? null : temp.getAbsolutePath();
    }

    private String getDescField(final String field) {
        return (String) descriptor.get(field);
    }

    public URL getFxml() {
        return getResource(getDescField("fxml"));
    }

    public URL getSplash() {
        return getResource(getDescField("splash"));
    }

    public Class<?> getMain() throws ClassNotFoundException {
        return loadClass(getDescField("main"));
    }

    public URL getBaseURL() {
        return baseURL;
    }

    private String resourceName(URL url) {
        String burl = baseURL.toExternalForm();
        String eurl = url.toExternalForm();
        int length = burl.endsWith("/") ? burl.length() : burl.length() + 1;
        return eurl.length() <= length ? "" : eurl.substring(length);
    }

    public boolean isPreLoaded(URL url) {
        return initialBundle.containsKey(resourceName(url));
    }

    public boolean isPreLoading(URL url) {
        String resName = resourceName(url);
        return resName.equals(curLoading) || initialBundle.containsKey(resName);
    }

    private byte[] getPreloadResource(URL url) {
        return initialBundle.get(resourceName(url));
    }

    public InputStream getPreLoadedResourceAsStream(URL url) throws IOException {
        byte[] bytes = getPreloadResource(url);
        if (bytes == null) {
            throw new FileNotFoundException("Resource not found: " + url.toExternalForm());
        }
        return new ByteArrayInputStream(bytes);
    }

    public long getPreLoadedResourceLength(URL url) {
        byte[] bytes = getPreloadResource(url);
        return bytes == null ? -1 : bytes.length;
    }

    public void requestResource(ResourceRequest resourceRequest) {
        this.resourceRequest.set(resourceRequest);
    }

    public boolean preloadingGoes() {
        return preloading;
    }
}