/*
* 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();
}
}