/* * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net> * Distributed under the terms of either: * - the common development and distribution license (CDDL), v1.0; or * - the GNU Lesser General Public License, v2.1 or later */ package winstone.classLoader; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.Date; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import winstone.Logger; import winstone.WebAppConfiguration; import winstone.WinstoneResourceBundle; /** * This subclass of WinstoneClassLoader is the reloading version. It runs a * monitoring thread in the background that checks for updates to any files in * the class path. * * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a> * @version $Id: ReloadingClassLoader.java,v 1.11 2007/02/17 01:55:12 rickknowles Exp $ */ public class ReloadingClassLoader extends WebappClassLoader implements ServletContextListener, Runnable { private static final int RELOAD_SEARCH_SLEEP = 10; private static final WinstoneResourceBundle CL_RESOURCES = new WinstoneResourceBundle("winstone.classLoader.LocalStrings"); private boolean interrupted; private WebAppConfiguration webAppConfig; private Set loadedClasses; private File classPaths[]; private int classPathsLength; public ReloadingClassLoader(URL urls[], ClassLoader parent) { super(urls, parent); this.loadedClasses = new HashSet(); if (urls != null) { this.classPaths = new File[urls.length]; for (int n = 0 ; n < urls.length; n++) { this.classPaths[this.classPathsLength++] = new File(urls[n].getFile()); } } } protected void addURL(URL url) { super.addURL(url); synchronized (this.loadedClasses) { if (this.classPaths == null) { this.classPaths = new File[10]; this.classPathsLength = 0; } else if (this.classPathsLength == (this.classPaths.length - 1)) { File temp[] = this.classPaths; this.classPaths = new File[(int) (this.classPathsLength * 1.75)]; System.arraycopy(temp, 0, this.classPaths, 0, this.classPathsLength); } this.classPaths[this.classPathsLength++] = new File(url.getFile()); } } public void contextInitialized(ServletContextEvent sce) { this.webAppConfig = (WebAppConfiguration) sce.getServletContext(); this.interrupted = false; synchronized (this) { this.loadedClasses.clear(); } Thread thread = new Thread(this, CL_RESOURCES .getString("ReloadingClassLoader.ThreadName")); thread.setDaemon(true); thread.setPriority(Thread.MIN_PRIORITY); thread.start(); } public void contextDestroyed(ServletContextEvent sce) { this.interrupted = true; this.webAppConfig = null; synchronized (this) { this.loadedClasses.clear(); } } /** * The maintenance thread. This makes sure that any changes in the files in * the classpath trigger a classLoader self destruct and recreate. */ public void run() { Logger.log(Logger.FULL_DEBUG, CL_RESOURCES, "ReloadingClassLoader.MaintenanceThreadStarted"); Map classDateTable = new HashMap(); Map classLocationTable = new HashMap(); Set lostClasses = new HashSet(); while (!interrupted) { try { String loadedClassesCopy[] = null; synchronized (this) { loadedClassesCopy = (String []) this.loadedClasses.toArray(new String[0]); } for (int n = 0; (n < loadedClassesCopy.length) && !interrupted; n++) { Thread.sleep(RELOAD_SEARCH_SLEEP); String className = transformToFileFormat(loadedClassesCopy[n]); File location = (File) classLocationTable.get(className); Long classDate = null; if ((location == null) || !location.exists()) { for (int j = 0; (j < this.classPaths.length) && (classDate == null); j++) { File path = this.classPaths[j]; if (!path.exists()) { continue; } else if (path.isDirectory()) { File classLocation = new File(path, className); if (classLocation.exists()) { classDate = new Long(classLocation.lastModified()); classLocationTable.put(className, classLocation); } } else if (path.isFile()) { classDate = searchJarPath(className, path); if (classDate != null) classLocationTable.put(className, path); } } } else if (location.exists()) classDate = new Long(location.lastModified()); // Has class vanished ? Leave a note and skip over it if (classDate == null) { if (!lostClasses.contains(className)) { lostClasses.add(className); Logger.log(Logger.DEBUG, CL_RESOURCES, "ReloadingClassLoader.ClassLost", className); } continue; } if ((classDate != null) && lostClasses.contains(className)) { lostClasses.remove(className); } // Stash date of loaded files, and compare with last // iteration Long oldClassDate = (Long) classDateTable.get(className); if (oldClassDate == null) { classDateTable.put(className, classDate); } else if (oldClassDate.compareTo(classDate) != 0) { // Trigger reset of webAppConfig Logger.log(Logger.INFO, CL_RESOURCES, "ReloadingClassLoader.ReloadRequired", new String[] {className, "" + new Date(classDate.longValue()), "" + new Date(oldClassDate.longValue()) }); this.webAppConfig.resetClassLoader(); } } } catch (Throwable err) { Logger.log(Logger.ERROR, CL_RESOURCES, "ReloadingClassLoader.MaintenanceThreadError", err); } } Logger.log(Logger.FULL_DEBUG, CL_RESOURCES, "ReloadingClassLoader.MaintenanceThreadFinished"); } protected Class findClass(String name) throws ClassNotFoundException { synchronized (this) { this.loadedClasses.add("Class:" + name); } return super.findClass(name); } public URL findResource(String name) { synchronized (this) { this.loadedClasses.add(name); } return super.findResource(name); } /** * Iterates through a jar file searching for a class. If found, it returns that classes date */ private Long searchJarPath(String classResourceName, File path) throws IOException, InterruptedException { JarFile jar = new JarFile(path); for (Enumeration e = jar.entries(); e.hasMoreElements() && !interrupted;) { JarEntry entry = (JarEntry) e.nextElement(); if (entry.getName().equals(classResourceName)) return new Long(path.lastModified()); } return null; } private static String transformToFileFormat(String name) { if (!name.startsWith("Class:")) return name; else return WinstoneResourceBundle.globalReplace(name.substring(6), ".", "/") + ".class"; } }