001: /*
002: * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
003: * Distributed under the terms of either:
004: * - the common development and distribution license (CDDL), v1.0; or
005: * - the GNU Lesser General Public License, v2.1 or later
006: */
007: package winstone.classLoader;
008:
009: import java.io.File;
010: import java.io.IOException;
011: import java.net.URL;
012: import java.util.Date;
013: import java.util.Enumeration;
014: import java.util.HashMap;
015: import java.util.HashSet;
016: import java.util.Map;
017: import java.util.Set;
018: import java.util.jar.JarEntry;
019: import java.util.jar.JarFile;
020:
021: import javax.servlet.ServletContextEvent;
022: import javax.servlet.ServletContextListener;
023:
024: import winstone.Logger;
025: import winstone.WebAppConfiguration;
026: import winstone.WinstoneResourceBundle;
027:
028: /**
029: * This subclass of WinstoneClassLoader is the reloading version. It runs a
030: * monitoring thread in the background that checks for updates to any files in
031: * the class path.
032: *
033: * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
034: * @version $Id: ReloadingClassLoader.java,v 1.11 2007/02/17 01:55:12 rickknowles Exp $
035: */
036: public class ReloadingClassLoader extends WebappClassLoader implements
037: ServletContextListener, Runnable {
038: private static final int RELOAD_SEARCH_SLEEP = 10;
039: private static final WinstoneResourceBundle CL_RESOURCES = new WinstoneResourceBundle(
040: "winstone.classLoader.LocalStrings");
041: private boolean interrupted;
042: private WebAppConfiguration webAppConfig;
043: private Set loadedClasses;
044: private File classPaths[];
045: private int classPathsLength;
046:
047: public ReloadingClassLoader(URL urls[], ClassLoader parent) {
048: super (urls, parent);
049: this .loadedClasses = new HashSet();
050: if (urls != null) {
051: this .classPaths = new File[urls.length];
052: for (int n = 0; n < urls.length; n++) {
053: this .classPaths[this .classPathsLength++] = new File(
054: urls[n].getFile());
055: }
056: }
057: }
058:
059: protected void addURL(URL url) {
060: super .addURL(url);
061: synchronized (this .loadedClasses) {
062: if (this .classPaths == null) {
063: this .classPaths = new File[10];
064: this .classPathsLength = 0;
065: } else if (this .classPathsLength == (this .classPaths.length - 1)) {
066: File temp[] = this .classPaths;
067: this .classPaths = new File[(int) (this .classPathsLength * 1.75)];
068: System.arraycopy(temp, 0, this .classPaths, 0,
069: this .classPathsLength);
070: }
071: this .classPaths[this .classPathsLength++] = new File(url
072: .getFile());
073: }
074: }
075:
076: public void contextInitialized(ServletContextEvent sce) {
077: this .webAppConfig = (WebAppConfiguration) sce
078: .getServletContext();
079: this .interrupted = false;
080: synchronized (this ) {
081: this .loadedClasses.clear();
082: }
083: Thread thread = new Thread(this , CL_RESOURCES
084: .getString("ReloadingClassLoader.ThreadName"));
085: thread.setDaemon(true);
086: thread.setPriority(Thread.MIN_PRIORITY);
087: thread.start();
088: }
089:
090: public void contextDestroyed(ServletContextEvent sce) {
091: this .interrupted = true;
092: this .webAppConfig = null;
093: synchronized (this ) {
094: this .loadedClasses.clear();
095: }
096: }
097:
098: /**
099: * The maintenance thread. This makes sure that any changes in the files in
100: * the classpath trigger a classLoader self destruct and recreate.
101: */
102: public void run() {
103: Logger.log(Logger.FULL_DEBUG, CL_RESOURCES,
104: "ReloadingClassLoader.MaintenanceThreadStarted");
105:
106: Map classDateTable = new HashMap();
107: Map classLocationTable = new HashMap();
108: Set lostClasses = new HashSet();
109: while (!interrupted) {
110: try {
111: String loadedClassesCopy[] = null;
112: synchronized (this ) {
113: loadedClassesCopy = (String[]) this .loadedClasses
114: .toArray(new String[0]);
115: }
116:
117: for (int n = 0; (n < loadedClassesCopy.length)
118: && !interrupted; n++) {
119: Thread.sleep(RELOAD_SEARCH_SLEEP);
120: String className = transformToFileFormat(loadedClassesCopy[n]);
121: File location = (File) classLocationTable
122: .get(className);
123: Long classDate = null;
124: if ((location == null) || !location.exists()) {
125: for (int j = 0; (j < this .classPaths.length)
126: && (classDate == null); j++) {
127: File path = this .classPaths[j];
128: if (!path.exists()) {
129: continue;
130: } else if (path.isDirectory()) {
131: File classLocation = new File(path,
132: className);
133: if (classLocation.exists()) {
134: classDate = new Long(classLocation
135: .lastModified());
136: classLocationTable.put(className,
137: classLocation);
138: }
139: } else if (path.isFile()) {
140: classDate = searchJarPath(className,
141: path);
142: if (classDate != null)
143: classLocationTable.put(className,
144: path);
145: }
146: }
147: } else if (location.exists())
148: classDate = new Long(location.lastModified());
149:
150: // Has class vanished ? Leave a note and skip over it
151: if (classDate == null) {
152: if (!lostClasses.contains(className)) {
153: lostClasses.add(className);
154: Logger.log(Logger.DEBUG, CL_RESOURCES,
155: "ReloadingClassLoader.ClassLost",
156: className);
157: }
158: continue;
159: }
160: if ((classDate != null)
161: && lostClasses.contains(className)) {
162: lostClasses.remove(className);
163: }
164:
165: // Stash date of loaded files, and compare with last
166: // iteration
167: Long oldClassDate = (Long) classDateTable
168: .get(className);
169: if (oldClassDate == null) {
170: classDateTable.put(className, classDate);
171: } else if (oldClassDate.compareTo(classDate) != 0) {
172: // Trigger reset of webAppConfig
173: Logger
174: .log(
175: Logger.INFO,
176: CL_RESOURCES,
177: "ReloadingClassLoader.ReloadRequired",
178: new String[] {
179: className,
180: ""
181: + new Date(
182: classDate
183: .longValue()),
184: ""
185: + new Date(
186: oldClassDate
187: .longValue()) });
188: this .webAppConfig.resetClassLoader();
189: }
190: }
191: } catch (Throwable err) {
192: Logger.log(Logger.ERROR, CL_RESOURCES,
193: "ReloadingClassLoader.MaintenanceThreadError",
194: err);
195: }
196: }
197: Logger.log(Logger.FULL_DEBUG, CL_RESOURCES,
198: "ReloadingClassLoader.MaintenanceThreadFinished");
199: }
200:
201: protected Class findClass(String name)
202: throws ClassNotFoundException {
203: synchronized (this ) {
204: this .loadedClasses.add("Class:" + name);
205: }
206: return super .findClass(name);
207: }
208:
209: public URL findResource(String name) {
210: synchronized (this ) {
211: this .loadedClasses.add(name);
212: }
213: return super .findResource(name);
214: }
215:
216: /**
217: * Iterates through a jar file searching for a class. If found, it returns that classes date
218: */
219: private Long searchJarPath(String classResourceName, File path)
220: throws IOException, InterruptedException {
221: JarFile jar = new JarFile(path);
222: for (Enumeration e = jar.entries(); e.hasMoreElements()
223: && !interrupted;) {
224: JarEntry entry = (JarEntry) e.nextElement();
225: if (entry.getName().equals(classResourceName))
226: return new Long(path.lastModified());
227: }
228: return null;
229: }
230:
231: private static String transformToFileFormat(String name) {
232: if (!name.startsWith("Class:"))
233: return name;
234: else
235: return WinstoneResourceBundle.globalReplace(name
236: .substring(6), ".", "/")
237: + ".class";
238: }
239: }
|