org.apache.wicket.application.ReloadingClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.application.ReloadingClassLoader.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.wicket.application;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import org.apache.wicket.util.collections.UrlExternalFormComparator;
import org.apache.wicket.util.file.File;
import org.apache.wicket.util.listener.IChangeListener;
import org.apache.wicket.util.time.Duration;
import org.apache.wicket.util.watch.IModifiable;
import org.apache.wicket.util.watch.IModificationWatcher;
import org.apache.wicket.util.watch.ModificationWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Custom ClassLoader that reverses the classloader lookups, and that is able to notify a listener
 * when a class file is changed.
 * 
 * @author <a href="mailto:jbq@apache.org">Jean-Baptiste Quenot</a>
 */
public class ReloadingClassLoader extends URLClassLoader {
    private static final Logger log = LoggerFactory.getLogger(ReloadingClassLoader.class);

    private static final Set<URL> urls = new TreeSet<>(new UrlExternalFormComparator());

    private static final List<String> patterns = new ArrayList<>();

    private IChangeListener<Class<?>> listener;

    private final IModificationWatcher watcher;

    static {
        addClassLoaderUrls(ReloadingClassLoader.class.getClassLoader());
        excludePattern("org.apache.wicket.*");
        includePattern("org.apache.wicket.examples.*");
    }

    /**
     * 
     * @param name
     * @return true if class if found, false otherwise
     */
    protected boolean tryClassHere(String name) {
        // don't include classes in the java or javax.servlet package
        if (name != null && (name.startsWith("java.") || name.startsWith("javax.servlet"))) {
            return false;
        }
        // Scan includes, then excludes
        boolean tryHere;

        // If no explicit includes, try here
        if (patterns == null || patterns.size() == 0) {
            tryHere = true;
        } else {
            // See if it matches include patterns
            tryHere = false;
            for (String rawpattern : patterns) {
                if (rawpattern.length() <= 1) {
                    continue;
                }
                // FIXME it seems that only "includes" are handled. "Excludes" are ignored
                boolean isInclude = rawpattern.substring(0, 1).equals("+");
                String pattern = rawpattern.substring(1);
                if (WildcardMatcherHelper.match(pattern, name) != null) {
                    tryHere = isInclude;
                }
            }
        }

        return tryHere;
    }

    /**
     * Include a pattern
     * 
     * @param pattern
     *            the pattern to include
     */
    public static void includePattern(String pattern) {
        patterns.add("+" + pattern);
    }

    /**
     * Exclude a pattern
     * 
     * @param pattern
     *            the pattern to exclude
     */
    public static void excludePattern(String pattern) {
        patterns.add("-" + pattern);
    }

    /**
     * Returns the list of all configured inclusion or exclusion patterns
     * 
     * @return list of patterns as String
     */
    public static List<String> getPatterns() {
        return patterns;
    }

    /**
     * Add the location of a directory containing class files
     * 
     * @param url
     *            the URL for the directory
     */
    public static void addLocation(URL url) {
        urls.add(url);
    }

    /**
     * Returns the list of all configured locations of directories containing class files
     * 
     * @return list of locations as URL
     */
    public static Set<URL> getLocations() {
        return urls;
    }

    /**
     * Add all the url locations we can find for the provided class loader
     * 
     * @param loader
     *            class loader
     */
    private static void addClassLoaderUrls(ClassLoader loader) {
        if (loader != null) {
            final Enumeration<URL> resources;
            try {
                resources = loader.getResources("");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            while (resources.hasMoreElements()) {
                URL location = resources.nextElement();
                ReloadingClassLoader.addLocation(location);
            }
        }
    }

    /**
     * Create a new reloading ClassLoader from a list of URLs, and initialize the
     * ModificationWatcher to detect class file modifications
     * 
     * @param parent
     *            the parent classloader in case the class file cannot be loaded from the above
     *            locations
     */
    public ReloadingClassLoader(ClassLoader parent) {
        super(new URL[] {}, parent);
        // probably doubles from this class, but just in case
        addClassLoaderUrls(parent);

        for (URL url : urls) {
            addURL(url);
        }
        Duration pollFrequency = Duration.seconds(3);
        watcher = new ModificationWatcher(pollFrequency);
    }

    /**
     * Gets a resource from this <code>ClassLoader</class>.  If the
     * resource does not exist in this one, we check the parent.
     * Please note that this is the exact opposite of the
     * <code>ClassLoader</code> spec. We use it to work around inconsistent class loaders from third
     * party vendors.
     * 
     * @param name
     *            of resource
     */
    @Override
    public final URL getResource(final String name) {
        URL resource = findResource(name);
        ClassLoader parent = getParent();
        if (resource == null && parent != null) {
            resource = parent.getResource(name);
        }

        return resource;
    }

    /**
     * Loads the class from this <code>ClassLoader</class>.  If the
     * class does not exist in this one, we check the parent.  Please
     * note that this is the exact opposite of the
     * <code>ClassLoader</code> spec. We use it to load the class from the same classloader as
     * WicketFilter or WicketServlet. When found, the class file is watched for modifications.
     * 
     * @param name
     *            the name of the class
     * @param resolve
     *            if <code>true</code> then resolve the class
     * @return the resulting <code>Class</code> object
     * @exception ClassNotFoundException
     *                if the class could not be found
     */
    @Override
    public final Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // First check if it's already loaded
        Class<?> clazz = findLoadedClass(name);

        if (clazz == null) {
            final ClassLoader parent = getParent();

            if (tryClassHere(name)) {
                try {
                    clazz = findClass(name);
                    watchForModifications(clazz);
                } catch (ClassNotFoundException cnfe) {
                    if (parent == null) {
                        // Propagate exception
                        throw cnfe;
                    }
                }
            }

            if (clazz == null) {
                if (parent == null) {
                    throw new ClassNotFoundException(name);
                } else {
                    // Will throw a CFNE if not found in parent
                    // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6500212
                    // clazz = parent.loadClass(name);
                    clazz = Class.forName(name, false, parent);
                }
            }
        }

        if (resolve) {
            resolveClass(clazz);
        }

        return clazz;
    }

    /**
     * Sets the listener that will be notified when a class changes
     * 
     * @param listener
     *            the listener to notify upon class change
     */
    public void setListener(IChangeListener<Class<?>> listener) {
        this.listener = listener;
    }

    /**
     * Watch changes of a class file by locating it in the list of location URLs and adding the
     * corresponding file to the ModificationWatcher
     * 
     * @param clz
     *            the class to watch
     */
    private void watchForModifications(final Class<?> clz) {
        // Watch class in the future
        Iterator<URL> locationsIterator = urls.iterator();
        File clzFile = null;
        while (locationsIterator.hasNext()) {
            // FIXME only works for directories, but JARs etc could be checked
            // as well
            URL location = locationsIterator.next();
            String clzLocation = location.getFile() + clz.getName().replaceAll("\\.", "/") + ".class";
            log.debug("clzLocation=" + clzLocation);
            clzFile = new File(clzLocation);
            final File finalClzFile = clzFile;
            if (clzFile.exists()) {
                log.info("Watching changes of class " + clzFile);
                watcher.add(clzFile, new IChangeListener<IModifiable>() {
                    @Override
                    public void onChange(IModifiable modifiable) {
                        log.info("Class file " + finalClzFile + " has changed, reloading");
                        try {
                            listener.onChange(clz);
                        } catch (Exception e) {
                            log.error("Could not notify listener", e);
                            // If an error occurs when the listener is notified,
                            // remove the watched object to avoid rethrowing the
                            // exception at next check
                            // FIXME check if class file has been deleted
                            watcher.remove(finalClzFile);
                        }
                    }
                });
                break;
            } else {
                log.debug("Class file does not exist: " + clzFile);
            }
        }
        if (clzFile != null && !clzFile.exists()) {
            log.debug("Could not locate class " + clz.getName());
        }
    }

    /**
     * Remove the ModificationWatcher from the current reloading class loader
     */
    public void destroy() {
        watcher.destroy();
    }
}