/*
* Copyright (c) 2008 Bradley W. Kimmel
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package ca.eandb.jdcp.server.classmanager;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import ca.eandb.util.UnexpectedException;
import ca.eandb.util.io.FileUtil;
/**
* A <code>ParentClassManager</code> that stores class definitions in a
* directory tree rooted at a provided location.
* @author Brad Kimmel
*/
public final class FileClassManager extends AbstractClassManager implements
ParentClassManager {
/** The extension to give to class definition files. */
private static final String CLASS_EXTENSION = ".class";
/** The extension to give to class digest files. */
private static final String DIGEST_EXTENSION = ".md5";
/** The digest algorithm to use. */
private static final String DIGEST_ALGORITHM = "MD5";
/** The directory in which to store the current class definitions. */
private final File currentDirectory;
/**
* The directory in which to store class definitions for which there are
* still existing child <code>ClassManager</code>s referencing those
* classes, but for which the definition of the class was overwritten
* after the child <code>ClassManager</code> was created.
*/
private final File deprecatedDirectory;
/**
* The directory in which to store class definitions that apply only to
* particular child <code>ClassManager</code>s.
*/
private final File childrenDirectory;
/** The index of the next child <code>ClassManager</code>. */
private int nextChildIndex = 0;
/**
* A map keyed on class names. Each class name is associated with a list
* indicating the values of {@link #nextChildIndex} in effect at the points
* when that class was redefined. This allows child
* <code>ClassManager</code>s to find old class definitions (specifically,
* the one in effect when that child was created).
*/
private final Map<String, List<Integer>> deprecationMap = new HashMap<String, List<Integer>>();
/** A list of the child <code>ClassManager</code>s. */
private final List<FileChildClassManager> activeChildren = new ArrayList<FileChildClassManager>();
/**
* A list of indices associated with released
* <code>ChildClassManager</code>s, but whose corresponding snapshot
* directories (under {@link #deprecatedDirectory}) have yet to be removed.
* Snapshot directories may only be removed when all children with smaller
* indices have been released.
*/
private final List<Integer> deprecationPendingList = new ArrayList<Integer>();
/**
* Creates a new <code>FileClassManager</code>.
* @param rootDirectory The working directory.
* @throws IllegalArgumentException If <code>rootDirectory</code> does not
* refer to a directory.
*/
public FileClassManager(String rootDirectory) throws IllegalArgumentException {
this(new File(rootDirectory));
}
/**
* Creates a new <code>FileClassManager</code>.
* @param rootDirectory The working directory.
* @throws IllegalArgumentException If <code>rootDirectory</code> does not
* refer to a directory.
*/
public FileClassManager(File rootDirectory) throws IllegalArgumentException {
if (!rootDirectory.isDirectory()) {
throw new IllegalArgumentException("rootDirectory must be a directory");
}
this.currentDirectory = new File(rootDirectory, "current");
this.deprecatedDirectory = new File(rootDirectory, "deprecated");
this.childrenDirectory = new File(rootDirectory, "children");
currentDirectory.mkdir();
deprecatedDirectory.mkdir();
childrenDirectory.mkdir();
FileUtil.clearDirectory(deprecatedDirectory);
FileUtil.clearDirectory(childrenDirectory);
}
private static final Comparator<? super Object> childComparator = new Comparator<Object>() {
public int compare(Object a, Object b) {
if (a instanceof FileChildClassManager) {
return -((Integer) b).compareTo(((FileChildClassManager) a).childIndex);
} else {
return ((Integer) a).compareTo(((FileChildClassManager) b).childIndex);
}
}
};
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ParentClassManager#getChildClassManager(int)
*/
public ca.eandb.jdcp.server.classmanager.ChildClassManager getChildClassManager(int id) {
int index = Collections.binarySearch(activeChildren, id,
childComparator);
return (index >= 0) ? activeChildren.get(index) : null;
}
/**
* Gets the relative path (without the extension) for files associated with
* the given fully qualified class name.
* @param className The fully qualified class name.
* @return The relative path to a file associated with the class.
*/
private String getBaseFileName(String className) {
return className.replace('.', '/');
}
/**
* Writes a class definition.
* @param directory The directory under which to write the class
* definition.
* @param name The fully qualified name of the class.
* @param def A <code>ByteBuffer</code> containing the class definition.
*/
private void writeClass(File directory, String name, ByteBuffer def) {
writeClass(directory, name, def, computeClassDigest(def));
}
/**
* Writes a class definition.
* @param directory The directory under which to write the class
* definition.
* @param name The fully qualified name of the class.
* @param def A <code>ByteBuffer</code> containing the class definition.
* @param digest The MD5 digest of the class definition.
*/
private void writeClass(File directory, String name, ByteBuffer def, byte[] digest) {
String baseName = getBaseFileName(name);
File classFile = new File(directory, baseName + CLASS_EXTENSION);
File digestFile = new File(directory, baseName + DIGEST_EXTENSION);
try {
FileUtil.setFileContents(classFile, def, true);
FileUtil.setFileContents(digestFile, digest, true);
} catch (IOException e) {
e.printStackTrace();
classFile.delete();
digestFile.delete();
}
}
/**
* Moves a class definition from the specified directory tree to another
* specified directory tree.
* @param fromDirectory The root of the directory tree from which to move
* the class definition.
* @param name The fully qualified name of the class to move.
* @param toDirectory The root of the directory tree to move the class
* definition to.
*/
private void moveClass(File fromDirectory, String name, File toDirectory) {
String baseName = getBaseFileName(name);
File fromClassFile = new File(fromDirectory, baseName + CLASS_EXTENSION);
File toClassFile = new File(toDirectory, baseName + CLASS_EXTENSION);
File fromDigestFile = new File(fromDirectory, baseName + DIGEST_EXTENSION);
File toDigestFile = new File(toDirectory, baseName + DIGEST_EXTENSION);
File toClassDirectory = toClassFile.getParentFile();
toClassDirectory.mkdirs();
fromClassFile.renameTo(toClassFile);
fromDigestFile.renameTo(toDigestFile);
}
/**
* Determines if the specified class exists in the specified directory
* tree.
* @param directory The root of the directory tree to examine.
* @param name The fully qualified name of the class.
* @return A value indicating if the class exists in the given directory
* tree.
*/
private boolean classExists(File directory, String name) {
String baseName = getBaseFileName(name);
File classFile = new File(directory, baseName + CLASS_EXTENSION);
File digestFile = new File(directory, baseName + DIGEST_EXTENSION);
return classFile.isFile() && digestFile.isFile();
}
/**
* Gets the contents of the specified file, or null if the file does not
* exist.
* @param file The <code>File</code> whose contents to obtain.
* @return The file contents, or null if the file does not exist.
*/
private byte[] getFileContents(File file) {
if (file.exists()) {
try {
return FileUtil.getFileContents(file);
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
/**
* Gets the MD5 digest of a class definition.
* @param directory The root of the directory tree containing the class.
* @param name The fully qualified name of the class.
* @return The MD5 digest of the class definition.
*/
private byte[] getClassDigest(File directory, String name) {
String baseName = getBaseFileName(name);
File digestFile = new File(directory, baseName + DIGEST_EXTENSION);
return getFileContents(digestFile);
}
/**
* Gets the definition of a class.
* @param directory The root of the directory tree containing the class
* definition.
* @param name The fully qualified name of the class.
* @return A <code>ByteBuffer</code> containing the class definition.
*/
private ByteBuffer getClassDefinition(File directory, String name) {
String baseName = getBaseFileName(name);
File digestFile = new File(directory, baseName + CLASS_EXTENSION);
return ByteBuffer.wrap(getFileContents(digestFile));
}
/**
* Computes the MD5 digest of the given class definition.
* @param def A <code>ByteBuffer</code> containing the class definition.
* @return The MD5 digest of the class definition.
*/
private byte[] computeClassDigest(ByteBuffer def) {
try {
MessageDigest alg = MessageDigest.getInstance(DIGEST_ALGORITHM);
def.mark();
alg.update(def);
def.reset();
return alg.digest();
} catch (NoSuchAlgorithmException e) {
throw new UnexpectedException(e);
}
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ClassManager#getClassDigest(java.lang.String)
*/
public byte[] getClassDigest(String name) {
return getClassDigest(currentDirectory, name);
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ClassManager#setClassDefinition(java.lang.String, java.nio.ByteBuffer)
*/
public void setClassDefinition(String name, ByteBuffer def) {
byte[] digest = computeClassDigest(def);
if (classExists(currentDirectory, name)) {
byte[] oldDigest = getClassDigest(currentDirectory, name);
if (Arrays.equals(digest, oldDigest)) {
return;
}
if (nextChildIndex > 0) {
File deprecatedDirectory = getDeprecatedDirectory(name, nextChildIndex);
if (!classExists(deprecatedDirectory, name)) {
moveClass(currentDirectory, name, deprecatedDirectory);
List<Integer> deprecationList = deprecationMap.get(name);
if (deprecationList == null) {
deprecationList = new ArrayList<Integer>();
deprecationMap.put(name, deprecationList);
}
deprecationList.add(nextChildIndex);
}
}
}
writeClass(currentDirectory, name, def, digest);
}
private File getDeprecatedDirectory(String name, int childIndex) {
return new File(deprecatedDirectory, Integer.toString(childIndex));
}
/* (non-Javadoc)
* @see ca.eandb.util.classloader.ClassLoaderStrategy#getClassDefinition(java.lang.String)
*/
public ByteBuffer getClassDefinition(String name) {
return getClassDefinition(currentDirectory, name);
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ParentClassManager#createChildClassManager()
*/
public FileChildClassManager createChildClassManager() {
FileChildClassManager child = new FileChildClassManager();
activeChildren.add(child);
return child;
}
/**
* Releases the resources associated with a child
* <code>ClassManager</code>.
* @param child The child <code>ClassManager</code> to release.
*/
private void releaseChildClassManager(FileChildClassManager child) {
for (int i = 0; i < activeChildren.size(); i++) {
FileChildClassManager current = activeChildren.get(i);
if (current.childIndex == child.childIndex) {
FileUtil.deleteRecursive(child.childDirectory);
deprecationPendingList.add(child.childIndex);
if (i == 0) {
Collections.sort(deprecationPendingList);
for (int pendingIndex : deprecationPendingList) {
if (pendingIndex > current.childIndex) {
break;
}
File pendingDirectory = new File(deprecatedDirectory, Integer.toString(pendingIndex + 1));
FileUtil.deleteRecursive(pendingDirectory);
}
}
}
}
}
/**
* A child <code>ClassManager</code> of a <code>FileClassManager</code>.
* @author Brad Kimmel
*/
private final class FileChildClassManager extends AbstractClassManager implements ca.eandb.jdcp.server.classmanager.ChildClassManager {
/**
* The root of the directory tree in which class definitions specific
* to this <code>ChildClassManager</code> are stored.
*/
private final File childDirectory;
/** The index associated with this child. */
private final int childIndex;
/**
* A value indicating whether this <code>ChildClassManager</code> has
* been released.
*/
private boolean released = false;
/**
* Creates a new <code>ChildClassManager</code>.
*/
public FileChildClassManager() {
this.childIndex = nextChildIndex++;
this.childDirectory = new File(childrenDirectory, Integer
.toString(childIndex));
}
/**
* Ensures that this <code>ChildClassManager</code> has not been
* released.
* @throws IllegalStateException if this <code>ChildClassManager</code>
* has been released.
*/
private void check() {
if (released) {
throw new IllegalStateException("Attempt to use a released child ClassManager.");
}
}
/**
* Gets the root of the directory tree in which the current definition
* of the specified class associated with this
* <code>ChildClassManager</code> is stored.
* @param name The fully qualified name of the class.
* @return The root of the directory in which to find the current
* definition of the class associated with this
* <code>ChildClassManager</code>.
*/
private File getClassDirectory(String name) {
check();
if (classExists(childDirectory, name)) {
return childDirectory;
}
List<Integer> deprecationList = deprecationMap.get(name);
if (deprecationList != null) {
int index = Collections.binarySearch(deprecationList, childIndex);
index = Math.abs(index + 1);
if (index < deprecationList.size()) {
int deprecationIndex = deprecationList.get(index);
File deprecatedDirectory = FileClassManager.this.getDeprecatedDirectory(name, deprecationIndex);
if (!classExists(deprecatedDirectory, name)) {
throw new UnexpectedException("Deprecated class missing.");
}
return deprecatedDirectory;
}
}
return currentDirectory;
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ClassManager#getClassDigest(java.lang.String)
*/
public byte[] getClassDigest(String name) {
File directory = getClassDirectory(name);
return FileClassManager.this.getClassDigest(directory, name);
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ClassManager#setClassDefinition(java.lang.String, java.nio.ByteBuffer)
*/
public void setClassDefinition(String name, ByteBuffer def) {
check();
writeClass(childDirectory, name, def);
}
/* (non-Javadoc)
* @see ca.eandb.util.classloader.ClassLoaderStrategy#getClassDefinition(java.lang.String)
*/
public ByteBuffer getClassDefinition(String name) {
File directory = getClassDirectory(name);
return FileClassManager.this.getClassDefinition(directory, name);
}
/**
* Gets this <code>ChildClassManager</code>s parent
* <code>FileClassManager</code>.
* @return The <code>FileClassManager</code> that created this
* <code>ChildClassManager</code>.
*/
public FileClassManager getParent() {
return FileClassManager.this;
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ChildClassManager#release()
*/
public void release() {
released = true;
releaseChildClassManager(this);
}
/* (non-Javadoc)
* @see ca.eandb.jdcp.server.classmanager.ChildClassManager#getChildId()
*/
public int getChildId() {
check();
return childIndex;
}
}
}