/*
* Copyright (C) 2000 - 2008 TagServlet Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD 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 OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://www.openbluedragon.org/
*/
/*
* Created on Dec 28, 2004
*
*/
package com.naryx.tagfusion.cfm.xml.ws.dynws;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.Map;
import com.nary.io.FileUtils;
import com.nary.util.FastMap;
public class DynamicCacheClassLoader extends ClassLoader {
public static final Object STUB_MUTEX = new Object();
public static final Object SKEL_MUTEX = new Object();
public static final int STUB_CLASSES = 0;
public static final int SKEL_CLASSES = 1;
private static Map stubLoaders = new FastMap();
private static Map skelLoaders = new FastMap();
private int category = STUB_CLASSES;
private String cacheDir = null;
private boolean validating = false;
private boolean invalidating = false;
private boolean registeringClasses = false;
private Map allClasses = null;
private Map associatedCFCs = null;
private Map associatedCLs = null;
private Map iComplexObjects = null;
private String iQueryBean = null;
private String iStructMap = null;
/**
* Constructs a new DynamicCacheClassLoader instance with the specified
* directory as the directory it can load class definitions from and with the
* specified category (either STUB_CLASSES or SKEL_CLASSES).
*
* NOTE: as this constructor alters the static list of STUB and SKEL
* DynamicCacheClassLoaders, it should be called from within a block that's
* synchronized on either the DynamicCacheClassLoader.STUB_MUTEX or the
* DynamicCacheClassLoader.SKEL_MUTEX.
*
* @param dir
* @param cat
*/
public DynamicCacheClassLoader(String dir, int cat) {
this(Thread.currentThread().getContextClassLoader(), dir, cat);
}
/**
* Constructs a new DynamicCacheClassLoader instance with the specified
* directory as the directory it can load class definitions from and with the
* specified category (either STUB_CLASSES or SKEL_CLASSES).
*
* NOTE: as this constructor alters the static list of STUB and SKEL
* DynamicCacheClassLoaders, it should be called from within a block that's
* synchronized on either the DynamicCacheClassLoader.STUB_MUTEX or the
* DynamicCacheClassLoader.SKEL_MUTEX.
*
* @param cl
* @param dir
* @param cat
*/
public DynamicCacheClassLoader(ClassLoader cl, String dir, int cat) {
super(cl);
init(dir, cat);
}
/**
* Performs initialization. Loads all the class definitions in this
* DynamicCacheClassLoader's directory. Adds this instance to one of the
* static DynamicCacheClassLoader lists (either the STUB or SKEL list).
*
* @param dir
* @param cat
*/
protected void init(String dir, int cat) {
if (!dir.endsWith(File.separator))
dir += File.separator;
this.category = cat;
this.cacheDir = dir;
// Add to the appropriate global list
addClassLoader(this);
// Populate the allClasses list
this.allClasses = new FastMap();
this.associatedCFCs = new FastMap();
this.associatedCLs = new FastMap();
this.iComplexObjects = new FastMap(FastMap.CASE_INSENSITIVE);
addClassFromDir(new File(this.cacheDir));
}
/**
* Provides a meaningful description of this DynamicCacheClassLoader.
*/
public String toString() {
return super.toString() + " " + (this.category == STUB_CLASSES ? "stubs" : "skels") + " " + this.cacheDir;
}
/**
* Returns the cache directory this DynamicCacheClassLoader loads from.
*
* @return
*/
public String getCacheDir() {
return this.cacheDir;
}
/**
* Returns the category this DynamicCacheClassLoader belongs to (either
* STUB_CLASSES or SKEL_CLASSES).
*
* @return
*/
public int getCategory() {
return this.category;
}
/**
* Returns the DynamicCacheClassLoader that corresponds to the specified
* directory. If no DynamicCacheClassLoader exists that is responsible for
* that directory, null is returned.
*
* @param dir
* @param cat
* @return
*/
public static DynamicCacheClassLoader findClassLoader(String dir, int cat) {
if (!dir.endsWith(File.separator))
dir += File.separator;
if (cat == STUB_CLASSES)
return (DynamicCacheClassLoader) stubLoaders.get(dir);
else
return (DynamicCacheClassLoader) skelLoaders.get(dir);
}
/**
* Returns a Class that corresponds to the fully qualified name. If the class
* was loaded by a DynamicCacheClassLoader, that class is returned. Otherwise,
* null.
*
* @param clsName
* @param cat
* @return
*/
public static Class findLoadedClass(String clsName, int cat) {
Iterator itr = null;
if (cat == STUB_CLASSES)
itr = stubLoaders.values().iterator();
else
itr = skelLoaders.values().iterator();
if (itr != null) {
DynamicCacheClassLoader dcl = null;
while (itr.hasNext()) {
dcl = (DynamicCacheClassLoader) itr.next();
Class klass = (Class) dcl.allClasses.get(clsName);
if (klass != null)
return klass;
}
}
// Not loaded
return null;
}
/**
* Adds the specified DynamicCacheClassLoader to the global list of
* DynamicCacheClassLoader instances. This list is used to locate
* DynamicCacheClassLoader instances when searching for class definitions.
*
* @param cl
*/
private static void addClassLoader(DynamicCacheClassLoader cl) {
if (cl.getCategory() == STUB_CLASSES) {
if (!stubLoaders.containsKey(cl.getCacheDir()))
stubLoaders.put(cl.getCacheDir(), cl);
} else {
if (!skelLoaders.containsKey(cl.getCacheDir()))
skelLoaders.put(cl.getCacheDir(), cl);
}
}
/**
* Locates the specified class by name (if available). Returns the Class
* instance or null if not found.
*
* @param name
* @return
*/
protected Class findClass(String name) {
Class cls = null;
// Try any loaded classes first
cls = DynamicCacheClassLoader.findLoadedClass(name, this.category);
if (cls != null)
return cls;
// OK, no luck. Look for the class on the f/s
cls = findClassOnDisk(name);
return cls;
}
/**
* Looks for a class definition in a file on the filesystem. Returns a Class
* instance representing that class if found. Returns null otherwise.
*
* @param name
* @return
*/
private Class findClassOnDisk(String name) {
// Locate the file
String pkg = name.substring(0, name.lastIndexOf('.'));
String n = name.substring(name.lastIndexOf('.') + 1);
File dir = new File(new File(this.cacheDir), pkg.replace('.', File.separatorChar));
File file = null;
if (!dir.exists())
return null;
File[] pFiles = dir.listFiles();
if (pFiles != null) {
for (int i = 0; i < pFiles.length; i++) {
String t = pFiles[i].getName();
int dotPos = t.indexOf(".");
int tLength = t.length();
if ( dotPos != -1 && dotPos == (tLength-6) && t.substring(tLength-6).equalsIgnoreCase(".class")) {
if (t.substring(0, dotPos).equals(n)) {
file = pFiles[i];
break;
}
}
}
}
if (file == null)
return null;
// OK load up the file data
byte[] b = loadClassData(file);
if (b != null) {
Class rtn = defineClass(name, b, 0, b.length);
this.allClasses.put(rtn.getName(), rtn);
return rtn;
} else {
return null;
}
}
/**
* Returns the contents of a class definition from the specified File. Returns
* null if there was a problem reading the File.
*
* @param classFile
* @return
*/
private byte[] loadClassData(File classFile) {
BufferedInputStream bin = null;
FileInputStream fin = null;
ByteArrayOutputStream bout = null;
try {
bout = new ByteArrayOutputStream();
fin = new FileInputStream(classFile);
bin = new BufferedInputStream(fin);
int read = -1;
byte[] buf = new byte[1024];
while ((read = bin.read(buf, 0, buf.length)) != -1)
bout.write(buf, 0, read);
return bout.toByteArray();
} catch (IOException xpn) {
com.nary.Debug.printStackTrace(xpn);
return null;
} finally {
try {
if (fin != null)
fin.close();
if (bin != null)
bin.close();
if (bout != null)
bout.close();
} catch (IOException ex) {
// Just eat it;
}
}
}
/**
* Returns a URL for the requested resource if such a resource exists in the
* directory this DynamicCacheClassLoader loads from.
*
* @param name
* @return
*/
@SuppressWarnings("deprecation")
protected URL findResource(String name) {
File f = new File(new File(this.cacheDir), name);
try {
if (f.exists())
return f.toURL();
else
return null;
} catch (MalformedURLException ex) {
return null;
}
}
/**
* Returns an array of Class instances that this DynamicCacheClassLoader
* manages (is responsible for). If the DynamicCacheClassLoader has been
* invalidated, an empty array is returned.
*
* @return
*/
public Class[] findAllClasses() {
// Return what we have
return (Class[]) this.allClasses.values().toArray(new Class[this.allClasses.size()]);
}
/**
* Associates a CFC File with this DynamicCacheClassLoader instance. This
* allows the DynamicCacheClassLoader to check for changes in CFC files. If
* changes have been made to the CFC File, this DynamicCacheClassLoader's
* classes should be invalidated.
*
* @param cfcFile
* File representation of the CFC on disk
*/
public void associateCFC(File cfcFile) {
this.associatedCFCs.put(cfcFile, new Long(cfcFile.lastModified()));
}
/**
* Associates a CFC name and IComplexObject implementation Class name with
* this DynamicCacheClassLoader instance. This allows the
* DynamicCacheClassLoader to keep track of the IComplexObject implementations
* generated for the CFC.
*
* @param cfcName
* name of the CFC
* @param impl
* name of the generated IComplexObject impl Class for this CFC
*/
public void setIComplexObject(String cfcName, String impl) {
this.iComplexObjects.put(cfcName, impl);
}
/**
* Sets the IQueryBean implementation Class name for the set of classes
* managed by this DynamicCacheClassLoader.
*
* @param impl
* IQueryBean implementation Class name
*/
public void setIQueryBean(String impl) {
this.iQueryBean = impl;
}
/**
* Sets the IStructMap implementation Class name for the set of classes
* managed by this DynamicCacheClassLoader.
*
* @param impl
* IStructMap implementation Class name
*/
public void setIStructMap(String impl) {
this.iStructMap = impl;
}
/**
* Returns the IComplexObject implementation Class corresponding to the
* specified CFC name if it exists in this DynamicCacheClassLoader.
*
* @param cfcName
* CFC name corresponding to a IComplexObject implementation Class
* @return IComplexObject implementation Class corresponding to the specified
* CFC name
*/
public Class findIComplexObject(String cfcName) {
String cls = (String) this.iComplexObjects.get(cfcName);
if (cls != null)
return (Class) this.allClasses.get(cls);
else
return null;
}
/**
* Returns the IQueryBean implementation Class corresponding to the web
* service for which this DynamicCacheClassLoader was created, or null if no
* such implementation Class exists.
*
* @return IQueryBean implementation Class corresponding to the web service
* for which this DynamicCacheClassLoader was created
*/
public Class getIQueryBean() {
if (iQueryBean != null)
return (Class) this.allClasses.get(iQueryBean);
else
return null;
}
/**
* Returns the IStructMap implementation Class corresponding to the web
* service for which this DynamicCacheClassLoader was created, or null if no
* such implementation Class exists.
*
* @return IStructMap implementation Class corresponding to the web service
* for which this DynamicCacheClassLoader was created
*/
public Class getIStructMap() {
if (iStructMap != null)
return (Class) this.allClasses.get(iStructMap);
else
return null;
}
/**
* Associates a DynamicCacheClassLoader instance with this
* DynamicCacheClassLoader instance. The association means this instance
* relies on classes maintained by the specified/associated instance. When
* checking for class validity, the specified/associated instance (and any
* chained instances) will be checked as well.
*
* @param cl
*/
public void associateDynamicCacheClassLoader(DynamicCacheClassLoader cl) {
if (this != cl)
this.associatedCLs.put(cl.getCacheDir(), cl);
}
/**
* Returns true if the classes loaded by this DynamicCacheClassLoader are
* still valid. False otherwise. The classes are considered invalid when any
* associated CFC files are modified from the time when the classes were first
* generated.
*
* NOTE: this method should be called whenever the a class loaded by this
* DynamicCacheClassLoader is used (or more specifically when an appropriate
* time would be to regenerate the class definition).
*
* @return
*/
public boolean areClassesValid() {
// In the case of circular references don't continue recursion
if (!this.validating) {
try {
this.validating = true;
DynamicCacheClassLoader cl = null;
File cfcFile = null;
Iterator itr = null;
// Check the directly associated CFC files
itr = this.associatedCFCs.keySet().iterator();
while (itr.hasNext()) {
cfcFile = (File) itr.next();
if (!cfcFile.exists() || cfcFile.lastModified() > ((Long) this.associatedCFCs.get(cfcFile)).longValue())
return false;
}
// Check the dependent DynamicCacheClassLoader instance
itr = this.associatedCLs.values().iterator();
while (itr.hasNext()) {
cl = (DynamicCacheClassLoader) itr.next();
if (!cl.areClassesValid())
return false;
}
} finally {
this.validating = false;
}
}
return true;
}
/**
* Causes this DynamicCacheClassLoader instance to unload all classes from
* it's list of known classes and removes the corresponding class files off
* the filesystem. This is typically used to invalidate classes that are now
* out of date.
*
* NOTE: as this method alters the static list of STUB and SKEL
* DynamicCacheClassLoaders, it should be called from within a block that's
* synchronized on either the DynamicCacheClassLoader.STUB_MUTEX or the
* DynamicCacheClassLoader.SKEL_MUTEX.
*/
public void invalidate() {
// In the case of circular references don't continue recursion
if (!this.invalidating) {
try {
this.invalidating = true;
// Delete the directory reference
if (getCategory() == STUB_CLASSES)
stubLoaders.remove(getCacheDir());
else
skelLoaders.remove(getCacheDir());
// Clear the loaded classes
allClasses.clear();
// Delete the directory
try {
FileUtils.recursiveDelete( new File(getCacheDir()), true );
} catch (IOException e) {}
// Invalidate any dependent classes
Iterator itr = associatedCLs.values().iterator();
while (itr.hasNext())
((DynamicCacheClassLoader) itr.next()).invalidate();
associatedCFCs.clear();
associatedCLs.clear();
iComplexObjects.clear();
iQueryBean = null;
iStructMap = null;
} finally {
this.invalidating = false;
}
}
}
/**
* Registers this DynamicCacheClassLoader's classes and all dependent
* DynamicCacheClassLoader's classes with the specified ContextRegistrar.
*
* @param ctxtReg
*/
public void registerClasses(ContextRegistrar ctxtReg) {
// In the case of circular references don't continue recursion
if (!this.registeringClasses) {
try {
this.registeringClasses = true;
Iterator itr = null;
// Register the classes
itr = this.allClasses.values().iterator();
while (itr.hasNext())
ctxtReg.registerClass((Class) itr.next());
// Pass to any associated DynamicCacheClassLoaders
itr = this.associatedCLs.values().iterator();
while (itr.hasNext())
((DynamicCacheClassLoader) itr.next()).registerClasses(ctxtReg);
} finally {
this.registeringClasses = false;
}
}
}
/**
* Unregisters this DynamicCacheClassLoader's classes and all dependent
* DynamicCacheClassLoader's classes with the specified ContextRegistrar.
*
* @param ctxtReg
*/
public void unregisterClasses(ContextRegistrar ctxtReg) {
if (!this.registeringClasses) {
try {
this.registeringClasses = true;
Iterator itr = null;
// Unregister the classes
itr = this.allClasses.values().iterator();
while (itr.hasNext())
ctxtReg.unregisterClass((Class) itr.next());
// Pass to any associated DynamicCacheClassLoaders
itr = this.associatedCLs.values().iterator();
while (itr.hasNext())
((DynamicCacheClassLoader) itr.next()).unregisterClasses(ctxtReg);
} finally {
this.registeringClasses = false;
}
}
}
/**
* Recursively adds all class files in the specified directory to this
* DynamicCacheClassLoader's list of loaded/available classes.
*
* @param dir
*/
private void addClassFromDir(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
if (files[i].isDirectory()) {
addClassFromDir(files[i]);
} else {
try {
File outputDir = new File(this.cacheDir);
String p = getRelativePath(outputDir.getCanonicalPath(), files[i].getCanonicalPath());
if (p.startsWith(File.separator))
p = p.substring(1);
int ndx = p.lastIndexOf('.');
// Only look for those ending with '.class'
if (ndx == p.length() - 6 && p.substring(ndx).equalsIgnoreCase(".class")) {
p = p.substring(0, p.length() - 6);
p = p.replace(File.separatorChar, '.');
// Calling defineClass() on a class will cause classes that it
// extends
// or implements to be loaded too so check if this class was
// already loaded
// when a previous class was loaded.
Class klass = findLoadedClass(p);
if (klass == null) {
// This class wasn't already loaded so try to load it from disk.
klass = findClassOnDisk(p);
}
}
} catch (IOException ex) {
com.nary.Debug.printStackTrace(ex);
// Just move on.
}
}
}
}
}
/**
* Returns a relative path string created using the specified parentDir and
* fullPath strings. Specifically, it returns the portion of the fullPath that
* is not part of the parentDir.
*
* @param parentDir
* @param fullPath
* @return
*/
private String getRelativePath(String parentDir, String fullPath) {
if (fullPath.length() >= parentDir.length()) {
if (fullPath.substring(0, parentDir.length()).equalsIgnoreCase(parentDir))
return fullPath.substring(parentDir.length());
}
return fullPath;
}
}