/*
* Copyright 2009-2010 Brian S O'Neill
*
* Licensed 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.cojen.dirmi;
import java.lang.ref.Reference;
import java.lang.ref.SoftReference;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Directory implementation which scans an ordinary classpath.
*
* @author Brian S O'Neill
*/
public class ClasspathDirectory implements PackageDirectory {
/*
public static void main(String[] args) throws Exception {
Environment env = new Environment();
Session[] sessions = env.newSessionPair();
sessions[0].send(new ClasspathDirectory());
PackageDirectory dir = (PackageDirectory) sessions[1].receive();
for (int i=0; i<args.length - 1; i++) {
dir = dir.subPackage(args[i]);
}
Pipe pipe = dir.fetchClasses(null, args[args.length - 1]);
ResourceDescriptor desc = (ResourceDescriptor) pipe.readObject();
System.out.println(desc);
int b;
while ((b = pipe.read()) >= 0) {
System.out.print(b);
System.out.print(' ');
}
System.out.println("done");
}
*/
private final ClasspathFile[] mPath;
private final String mPackageName;
private final ClasspathDirectory mParent;
private Map<String, ResourceDescriptor> mClassMap;
private Map<String, ResourceDescriptor> mResourceMap;
private Map<String, Loader> mClassLoaderMap;
private Map<String, Loader> mResourceLoaderMap;
/**
* Construct using the system classpath.
*/
public ClasspathDirectory() {
this(System.getProperty("java.class.path"));
}
/**
* Construct using the given classpath which must use system path
* separators.
*/
public ClasspathDirectory(String classpath) {
this(classpath, System.getProperty("path.separator"));
}
/**
* Construct using the given classpath which uses the given path separator.
*/
public ClasspathDirectory(String classpath, String separator) {
this(splitPath(classpath, separator));
}
/**
* Construct from a list of files and directories.
*/
public ClasspathDirectory(File... path) {
if (path == null) {
throw new IllegalArgumentException("Classpath is null");
}
Set<File> files = new LinkedHashSet<File>();
for (File file : path) {
if (file == null) {
continue;
}
try {
try {
file = file.getCanonicalFile();
} catch (Exception e) {
file = file.getAbsoluteFile();
}
} catch (SecurityException e) {
// Leave file alone for now.
}
try {
if (file.canRead()) {
ClasspathFile cf = new ClasspathFile(file);
if (cf.isDirectory() || cf.isJar()) {
files.add(cf);
}
}
} catch (SecurityException e) {
// Ignore unreadable file.
}
}
mPath = files.toArray(new ClasspathFile[files.size()]);
mPackageName = "";
mParent = null;
}
private ClasspathDirectory(ClasspathFile[] path,
String packageName,
ClasspathDirectory parent)
{
mPath = path;
mPackageName = packageName;
mParent = parent;
}
private static File[] splitPath(String classpath, String separator) {
if (classpath == null) {
throw new IllegalArgumentException("Classpath is null");
}
if (separator == null) {
throw new IllegalArgumentException("Classpath separator is null");
}
List<File> files = new ArrayList<File>();
int start = 0;
int index;
while (true) {
index = classpath.indexOf(separator, start);
String element;
if (index < 0) {
element = classpath.substring(start);
} else {
element = classpath.substring(start, index);
}
if (element.length() > 0) {
files.add(new File(new String(element)));
}
if (index < 0) {
break;
}
start = index + separator.length();
}
return files.toArray(new File[files.size()]);
}
@Override
public PackageDirectory subPackage(String name) {
if (name == null || name.length() == 0) {
return this;
}
return new ClasspathDirectory(mPath, name, this);
}
@Override
public synchronized Map<String, ResourceDescriptor> availableClasses() {
expand();
return mClassMap;
}
@Override
public synchronized Map<String, ResourceDescriptor> availableResources() {
expand();
return mResourceMap;
}
@Override
public Pipe fetchClasses(Pipe pipe, String... names) {
fetchClasses(names, pipe);
return null;
}
private void fetchClasses(String[] names, ObjectOutput output) {
Map<String, ResourceDescriptor> map;
Map<String, Loader> loaderMap;
synchronized (this) {
map = availableClasses();
loaderMap = mClassLoaderMap;
}
fetchResources(true, map, loaderMap, names, output);
}
@Override
public Pipe fetchResources(Pipe pipe, String... names) {
fetchResources(names, pipe);
return null;
}
private void fetchResources(String[] names, ObjectOutput out) {
Map<String, ResourceDescriptor> map;
Map<String, Loader> loaderMap;
synchronized (this) {
map = availableResources();
loaderMap = mResourceLoaderMap;
}
fetchResources(false, map, loaderMap, names, out);
}
private void fetchResources(boolean gettingClasses,
Map<String, ResourceDescriptor> map,
Map<String, Loader> loaderMap,
String[] names, ObjectOutput out)
{
if (names == null) {
names = map.keySet().toArray(new String[map.size()]);
}
String packagePath = buildPackagePath();
byte[] buffer = null;
try {
for (String name : names) {
ResourceDescriptor desc = map.get(name);
Loader loader = loaderMap.get(desc.getName());
if (loader == null) {
desc = null;
}
out.writeObject(desc);
if (desc == null) {
continue;
}
int remaining = desc.getLength();
if (remaining <= 0) {
continue;
}
if (buffer == null) {
buffer = new byte[1000];
}
InputStream in = loader.load(gettingClasses, packagePath, desc.getName());
try {
int amt;
while ((amt = in.read(buffer)) > 0) {
if (amt <= remaining) {
out.write(buffer, 0, amt);
remaining -= amt;
} else {
// Loaded too much, so stop writing. Make sure the
// output has the correct number of bytes, but the
// digest won't match.
out.write(buffer, 0, remaining);
remaining = 0;
break;
}
}
} finally {
try {
in.close();
} catch (IOException e) {
// Ignore.
}
}
if (remaining > 0) {
// Loaded too little. Make sure the output has the correct
// number of bytes, but the digest won't match.
Arrays.fill(buffer, (byte) 0);
int amt;
do {
amt = Math.max(buffer.length, remaining);
out.write(buffer, 0, amt);
} while ((remaining -= amt) > 0);
}
}
} catch (IOException e) {
// Ignore.
} finally {
try {
out.close();
} catch (IOException e) {
// Ignore.
}
}
}
private synchronized void expand() {
if (mClassMap != null) {
return;
}
Expander expander = new Expander();
expander.scanClasspath();
mClassMap = unmodifiable(expander.classMap);
mResourceMap = unmodifiable(expander.resourceMap);
mClassLoaderMap = unmodifiable(expander.classLoaderMap);
mResourceLoaderMap = unmodifiable(expander.resourceLoaderMap);
}
private static <K, V> Map<K, V> unmodifiable(Map<? extends K, ? extends V> map) {
if (map != null && map.size() > 0) {
return Collections.unmodifiableMap(map);
} else {
return Collections.emptyMap();
}
}
private String buildPackagePath() {
StringBuilder b = new StringBuilder();
appendPackagePath(b);
return b.toString();
}
private void appendPackagePath(StringBuilder b) {
if (mParent != null) {
mParent.appendPackagePath(b);
}
if (b.length() > 0) {
b.append('/');
}
b.append(mPackageName);
}
private class Expander {
final Map<String, ResourceDescriptor> classMap =
new HashMap<String, ResourceDescriptor>();
final Map<String, ResourceDescriptor> resourceMap =
new HashMap<String, ResourceDescriptor>();
final Map<String, Loader> classLoaderMap = new HashMap<String, Loader>();
final Map<String, Loader> resourceLoaderMap = new HashMap<String, Loader>();
final String packagePath = buildPackagePath();
String resourceName;
boolean resourceIsClass;
byte[] buffer;
void scanClasspath() {
for (ClasspathFile file : mPath) {
if (file.isDirectory()) {
scanDirectory(file);
} else if (file.isJar()) {
scanJar(file);
}
}
}
private void scanDirectory(File dir) {
Loader loader = new DirectoryLoader(dir);
File[] files = new File(dir, packagePath).listFiles();
if (files != null) for (File resourceFile : files) {
if (resourceFile.isDirectory() || !resourceFile.isFile()) {
continue;
}
if (!examineName(resourceFile.getName())) {
continue;
}
ResourceDescriptor desc;
try {
desc = createDescriptor(new FileInputStream(resourceFile));
} catch (IOException e) {
continue;
}
if (resourceIsClass) {
classMap.put(resourceName, desc);
classLoaderMap.put(resourceName, loader);
} else {
resourceMap.put(resourceName, desc);
resourceLoaderMap.put(resourceName, loader);
}
}
}
private void scanJar(File file) {
JarFile jf = null;
boolean jarInUse = false;
try {
jf = new JarFile(file);
Loader loader = new JarLoader(file, jf);
Enumeration<JarEntry> en = jf.entries();
while (en.hasMoreElements()) {
JarEntry entry = en.nextElement();
if (entry.isDirectory()) {
continue;
}
String name = entry.getName();
if (!name.startsWith(packagePath)) {
continue;
}
if (name.length() > packagePath.length() &&
name.charAt(packagePath.length()) != '/')
{
continue;
}
name = name.substring(packagePath.length() + 1);
if (name.indexOf('/') >= 0 || !examineName(name)) {
continue;
}
name = resourceName;
long length = entry.getSize();
if (length > Integer.MAX_VALUE) {
// Too large.
continue;
}
byte[] digest;
if (length < 0) {
// Need to scan file anyhow.
digest = null;
} else {
String digestStr = entry.getAttributes().getValue("SHA1-Digest");
if (digestStr == null) {
digest = null;
} else {
try {
digest = base64ToByteArray(digestStr);
} catch (IllegalArgumentException e) {
digest = null;
}
}
}
ResourceDescriptor desc;
if (digest != null) {
desc = new ResourceDescriptor(name, (int) length, digest);
} else {
try {
desc = createDescriptor(jf.getInputStream(entry));
} catch (IOException e) {
continue;
}
}
jarInUse = true;
if (resourceIsClass) {
classMap.put(name, desc);
classLoaderMap.put(name, loader);
} else {
resourceMap.put(name, desc);
resourceLoaderMap.put(name, loader);
}
}
} catch (IOException e) {
// Ignore.
} finally {
if (jf != null && !jarInUse) {
try {
jf.close();
} catch (IOException e) {
// Ignore.
}
}
}
}
// Returns false if resource should be skipped.
private boolean examineName(String name) {
resourceName = null;
resourceIsClass = false;
if (name.endsWith(".java")) {
return false;
}
if (name.endsWith(".class")) {
name = name.substring(0, name.length() - 6);
if (classMap.containsKey(name)) {
return false;
}
resourceIsClass = true;
} else {
if (resourceMap.containsKey(name)) {
return false;
}
}
resourceName = name;
return true;
}
private ResourceDescriptor createDescriptor(InputStream in) throws IOException {
try {
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
throw new IOException(e);
}
long length = 0;
byte[] buffer = buffer();
int amt;
while ((amt = in.read(buffer)) > 0) {
if ((length += amt) > Integer.MAX_VALUE) {
throw new IOException("Resource is too long");
}
md.update(buffer, 0, amt);
}
return new ResourceDescriptor(resourceName, (int) length, md.digest());
} finally {
try {
in.close();
} catch (IOException e) {
// Ignore.
}
}
}
private byte[] buffer() {
if (buffer == null) {
buffer = new byte[1000];
}
return buffer;
}
}
private static class ClasspathFile extends File {
private final int mType;
ClasspathFile(File file) {
super(file.getPath());
if (file.isDirectory()) {
mType = 1;
} else if (file.isFile() &&
file.getName().endsWith(".jar") || file.getName().endsWith(".zip")) {
mType = 2;
} else {
mType = 0;
}
}
@Override
public boolean isDirectory() {
return mType == 1;
}
public boolean isJar() {
return mType == 2;
}
}
private static abstract class Loader {
InputStream load(boolean isClass, String packagePath, String name)
throws IOException
{
if (isClass) {
name = name.concat(".class");
}
return load(packagePath, name);
}
abstract InputStream load(String packagePath, String name) throws IOException;
}
private static class DirectoryLoader extends Loader {
private final File mDirectory;
DirectoryLoader(File directory) {
mDirectory = directory;
}
@Override
InputStream load(String packagePath, String name) throws IOException {
File fullPath = new File(new File(mDirectory, packagePath), name);
return new FileInputStream(fullPath);
}
}
private static class JarLoader extends Loader {
private final File mFile;
private Reference<JarFile> mJarRef;
JarLoader(File file, JarFile jf) {
mFile = file;
synchronized (this) {
mJarRef = new SoftReference<JarFile>(jf);
}
}
@Override
InputStream load(String packagePath, String name) throws IOException {
JarFile jf = jarFile();
return jf.getInputStream(jf.getEntry(packagePath + '/' + name));
}
private synchronized JarFile jarFile() throws IOException {
JarFile jf = mJarRef.get();
if (jf == null) {
mJarRef = new SoftReference<JarFile>(jf = new JarFile(mFile));
}
return jf;
}
}
// Sigh, yet another base 64 decoder. This one was stolen from
// the hidden java.util.prefs.Base64 class.
static byte[] base64ToByteArray(String s) {
byte[] alphaToInt = base64ToInt;
int sLen = s.length();
int numGroups = sLen/4;
if (4*numGroups != sLen)
throw new IllegalArgumentException(
"String length must be a multiple of four.");
int missingBytesInLastGroup = 0;
int numFullGroups = numGroups;
if (sLen != 0) {
if (s.charAt(sLen-1) == '=') {
missingBytesInLastGroup++;
numFullGroups--;
}
if (s.charAt(sLen-2) == '=')
missingBytesInLastGroup++;
}
byte[] result = new byte[3*numGroups - missingBytesInLastGroup];
// Translate all full groups from base64 to byte array elements
int inCursor = 0, outCursor = 0;
for (int i=0; i<numFullGroups; i++) {
int ch0 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch1 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch2 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch3 = base64toInt(s.charAt(inCursor++), alphaToInt);
result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));
result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
result[outCursor++] = (byte) ((ch2 << 6) | ch3);
}
// Translate partial group, if present
if (missingBytesInLastGroup != 0) {
int ch0 = base64toInt(s.charAt(inCursor++), alphaToInt);
int ch1 = base64toInt(s.charAt(inCursor++), alphaToInt);
result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));
if (missingBytesInLastGroup == 1) {
int ch2 = base64toInt(s.charAt(inCursor++), alphaToInt);
result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
}
}
// assert inCursor == s.length()-missingBytesInLastGroup;
// assert outCursor == result.length;
return result;
}
/**
* Translates the specified character, which is assumed to be in the
* "Base 64 Alphabet" into its equivalent 6-bit positive integer.
*
* @throw IllegalArgumentException or ArrayOutOfBoundsException if
* c is not in the Base64 Alphabet.
*/
private static int base64toInt(char c, byte[] alphaToInt) {
int result = alphaToInt[c];
if (result < 0)
throw new IllegalArgumentException("Illegal character " + c);
return result;
}
/**
* This array is a lookup table that translates unicode characters
* drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045)
* into their 6-bit positive integer equivalents. Characters that
* are not in the Base64 alphabet but fall within the bounds of the
* array are translated to -1.
*/
private static final byte base64ToInt[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
};
}