/* ClassPath Copyright (C) 1998-1999 Jochen Hoenicke. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program 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 this program; see the file COPYING. If not, write to * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. * * $Id$ */ package net.sf.jode.bytecode; import java.io.ByteArrayInputStream; import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.util.Enumeration; import java.util.NoSuchElementException; import java.util.Hashtable; import java.util.StringTokenizer; import java.util.Vector; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; ///#def COLLECTIONS java.util import java.util.Iterator; ///#enddef import net.sf.jode.GlobalOptions; import net.sf.jode.util.UnifyHash; /** * A path in which class files are searched for. * * Class files can be loaded from several different locations, these * locations can be: * * * We use standard java means to find a class file: package correspong * to directories and the class file must have the .class * extension. For example if the class path points to * /home/java, the class java.lang.Object is * loaded from /home/java/java/lang/Object.class. * * A class path can have another ClassPath as fallback. If * ClassInfo.loadInfo is called and the class isn't found the fallback * ClassPath is searched instead. This repeats until there is no * further fallback. * * The main method for creating classes is {@link #getClassInfo}. The * other available methods are useful to find other files in the * ClassPath and to get a listing of all available files and classes. * * A ClassPath handles some IOExceptions and * SecurityExceptions skipping the path that produced * them. * * @author Jochen Hoenicke * @version 1.1 */ public class ClassPath { /** * We need a different pathSeparatorChar, since ':' (used for most * UNIX System) is used a protocol separator in URLs. * * We currently allow both pathSeparatorChar and * altPathSeparatorChar and decide if it is a protocol separator * by context. This doesn't always work, so use * altPathSeparator, or the ClassPath(String[]) * constructor. */ public static final char altPathSeparatorChar = ','; private class Path { public boolean exists(String file) { return false; } public boolean isDirectory(String file) { return false; } public InputStream getFile(String file) throws IOException { return null; } public Enumeration listFiles(String directory) { return null; } public boolean loadClass(ClassInfo clazz, int howMuch) throws IOException, ClassFormatException { String file = clazz.getName().replace('.', '/') + ".class"; if (!exists(file)) return false; DataInputStream input = new DataInputStream (new BufferedInputStream (getFile(file))); clazz.read(input, howMuch); return true; } } private class ReflectionPath extends Path { public boolean loadClass(ClassInfo classinfo, int howMuch) throws IOException, ClassFormatException { if (howMuch > ClassInfo.DECLARATIONS) return false; Class clazz = null; try { clazz = Class.forName(classinfo.getName()); } catch (ClassNotFoundException ex) { return false; } catch (NoClassDefFoundError ex) { return false; } try { classinfo.loadFromReflection(clazz, howMuch); return true; } catch (SecurityException ex) { return false; } } public String toString() { return "reflection:"; } } private class LocalPath extends Path { private File dir; public LocalPath(File path) { dir = path; } public boolean exists(String filename) { if (java.io.File.separatorChar != '/') filename = filename .replace('/', java.io.File.separatorChar); try { return new File(dir, filename).exists(); } catch (SecurityException ex) { return false; } } public boolean isDirectory(String filename) { if (java.io.File.separatorChar != '/') filename = filename .replace('/', java.io.File.separatorChar); return new File(dir, filename).isDirectory(); } public InputStream getFile(String filename) throws IOException { if (java.io.File.separatorChar != '/') filename = filename .replace('/', java.io.File.separatorChar); File f = new File(dir, filename); return new FileInputStream(f); } public Enumeration listFiles(String directory) { if (File.separatorChar != '/') directory = directory .replace('/', File.separatorChar); File f = new File(dir, directory); final String[] files = f.list(); if (files == null) return null; if (!directory.endsWith(File.separator)) directory += File.separator; final String prefix = directory; return new Enumeration() { int i = 0; public boolean hasMoreElements() { return i < files.length; } public Object nextElement() { try { return files[i++]; } catch (ArrayIndexOutOfBoundsException ex) { return new NoSuchElementException(); } } }; } public String toString() { return dir.getName(); } } private class ZipPath extends Path { private Hashtable entries = new Hashtable(); private ZipFile file; private byte[] contents; private String prefix; private void addEntry(ZipEntry ze) { String name = ze.getName(); if (prefix != null) { if (!name.startsWith(prefix)) return; name = name.substring(prefix.length()); } if (ze.isDirectory() /* || !name.endsWith(".class")*/) return; do { String dir = ""; int pathsep = name.lastIndexOf("/"); if (pathsep != -1) { dir = name.substring(0, pathsep); name = name.substring(pathsep+1); } Vector dirContent = (Vector) entries.get(dir); if (dirContent != null) { dirContent.addElement(name); return; } dirContent = new Vector(); dirContent.addElement(name); entries.put(dir, dirContent); name = dir; } while (name.length() > 0); } public ZipPath(ZipFile zipfile, String prefix) { this.file = zipfile; this.prefix = prefix; Enumeration zipEnum = file.entries(); entries = new Hashtable(); while (zipEnum.hasMoreElements()) { addEntry((ZipEntry) zipEnum.nextElement()); } } public ZipPath(byte[] zipcontents, String prefix) throws IOException { this.contents = zipcontents; this.prefix = prefix; // fill entries into hash table ZipInputStream zis = new ZipInputStream (new ByteArrayInputStream(zipcontents)); entries = new Hashtable(); ZipEntry ze; while ((ze = zis.getNextEntry()) != null) { addEntry(ze); zis.closeEntry(); } zis.close(); } public boolean exists(String filename) { if (entries.containsKey(filename)) return true; String dir = ""; String name = filename; int index = filename.lastIndexOf('/'); if (index >= 0) { dir = filename.substring(0, index); name = filename.substring(index+1); } Vector directory = (Vector)entries.get(dir); if (directory != null && directory.contains(name)) return true; return false; } public boolean isDirectory(String filename) { return entries.containsKey(filename); } public InputStream getFile(String filename) throws IOException { String fullname = prefix != null ? prefix + filename : filename; if (contents != null) { ZipInputStream zis = new ZipInputStream (new ByteArrayInputStream(contents)); ZipEntry ze; while ((ze = zis.getNextEntry()) != null) { if (ze.getName().equals(fullname)) { ///#ifdef JDK11 /// // The skip method in jdk1.1.7 ZipInputStream /// // is buggy. We return a wrapper that fixes /// // this. /// return new FilterInputStream(zis) { /// private byte[] tmpbuf = new byte[512]; /// public long skip(long n) throws IOException { /// long skipped = 0; /// while (n > 0) { /// int count = read(tmpbuf, 0, /// (int)Math.min(n, 512L)); /// if (count == -1) /// return skipped; /// skipped += count; /// n -= count; /// } /// return skipped; /// } /// }; ///#else return zis; ///#endif } zis.closeEntry(); } zis.close(); } else { ZipEntry ze = file.getEntry(fullname); if (ze != null) return file.getInputStream(ze); } return null; } public Enumeration listFiles(String directory) { Vector direntries = (Vector) entries.get(directory); if (direntries != null) return direntries.elements(); return null; } public String toString() { return file.getName(); } } private class URLPath extends Path { private URL base; public URLPath(URL base) { this.base = base; } public boolean exists(String filename) { try { URL url = new URL(base, filename); URLConnection conn = url.openConnection(); conn.connect(); conn.getInputStream().close(); return true; } catch (IOException ex) { return false; } } public InputStream getFile(String filename) throws IOException { try { URL url = new URL(base, filename); URLConnection conn = url.openConnection(); conn.setAllowUserInteraction(true); return conn.getInputStream(); } catch (IOException ex) { return null; } } public boolean loadClass(ClassInfo clazz, int howMuch) throws IOException, ClassFormatException { String file = clazz.getName().replace('.', '/') + ".class"; InputStream is = getFile(file); if (is == null) return false; DataInputStream input = new DataInputStream (new BufferedInputStream(is)); clazz.read(input, howMuch); return true; } public String toString() { return base.toString(); } } private Path[] paths; private UnifyHash classes = new UnifyHash(); ClassPath fallback = null; /** * Creates a new class path for the given path. See the class * description for more information, which kind of paths are * supported. * @param path An array of paths. * @param fallback The fallback classpath. */ public ClassPath(String[] paths, ClassPath fallback) { this.fallback = fallback; initPath(paths); } /** * Creates a new class path for the given path. See the class * description for more information, which kind of paths are * supported. * @param path An array of paths. */ public ClassPath(String[] paths) { initPath(paths); } /** * Creates a new class path for the given path. See the class * description for more information, which kind of paths are * supported. * @param path One or more paths. They should be separated by the * altPathSeparatorChar or pathSeparatorChar, but the latter is * deprecated since it may give problems for UNIX machines. * @see #ClassPath(String[] paths) */ public ClassPath(String path, ClassPath fallback) { this.fallback = fallback; initPath(tokenizeClassPath(path)); } /** * Creates a new class path for the given path. See the class * description for more information, which kind of paths are * supported. * @param path One or more paths. They should be separated by the * altPathSeparatorChar or pathSeparatorChar, but the latter is * deprecated since it may give problems for UNIX machines. * @see #ClassPath(String[] paths) */ public ClassPath(String path) { initPath(tokenizeClassPath(path)); } private String[] tokenizeClassPath(String path) { // Calculate a good approximation (rounded upwards) of the tokens // in this path. int length = 1; for (int index=path.indexOf(File.pathSeparatorChar); index != -1; length++) index = path.indexOf(File.pathSeparatorChar, index+1); if (File.pathSeparatorChar != altPathSeparatorChar) { for (int index=path.indexOf(altPathSeparatorChar); index != -1; length++) index = path.indexOf(altPathSeparatorChar, index+1); } String[] tokens = new String[length]; int i = 0; for (int ptr=0; ptr < path.length(); ptr++, i++) { int next = ptr; while (next < path.length() && path.charAt(next) != File.pathSeparatorChar && path.charAt(next) != altPathSeparatorChar) next++; int index = ptr; colon_separator: while (next > ptr && next < path.length() && path.charAt(next) == ':') { // Check if this is a URL instead of a pathSeparator // Since this is a while loop it allows nested urls like // jar:ftp://ftp.foo.org/pub/foo.jar!/ while (index < next) { char c = path.charAt(index); // According to RFC 1738 letters, digits, '+', '-' // and '.' are allowed SCHEMA characters. We // disallow '.' because it is a good marker that // the user has specified a filename instead of a // URL. if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && "+-".indexOf(c) == -1) { break colon_separator; } index++; } next++; index++; while (next < path.length() && path.charAt(next) != File.pathSeparatorChar && path.charAt(next) != altPathSeparatorChar) next++; } tokens[i] = path.substring(ptr, next); ptr = next; } return tokens; } private byte[] readURLZip(URLConnection conn) { int length = conn.getContentLength(); if (length <= 0) // Give a approximation if length is unknown length = 10240; else // Increase the length by one, so we hopefully don't need // to grow the array later (we need a little overshot to // know when the end is reached). length++; byte[] contents = new byte[length]; try { InputStream is = conn.getInputStream(); int pos = 0; for (;;) { // This is ugly, is.available() may return zero even // if there are more bytes. int avail = Math.max(is.available(), 1); if (pos + is.available() > contents.length) { // grow the byte array. byte[] newarr = new byte [Math.max(2*contents.length, pos + is.available())]; System.arraycopy(contents, 0, newarr, 0, pos); contents = newarr; } int count = is.read(contents, pos, contents.length-pos); if (count == -1) break; pos += count; } if (pos < contents.length) { // shrink the byte array again. byte[] newarr = new byte[pos]; System.arraycopy(contents, 0, newarr, 0, pos); contents = newarr; } return contents; } catch (IOException ex) { return null; } } private void initPath(String[] tokens) { int length = tokens.length; paths = new Path[length]; for (int i = 0; i < length; i++) { String path = tokens[i]; if (path == null) continue; String zipPrefix = null; // The special reflection URL if (path.startsWith("reflection:")) { paths[i] = new ReflectionPath(); continue; } // We handle jar URL's ourself. if (path.startsWith("jar:")) { int index = 0; do { index = path.indexOf('!', index); } while (index != -1 && index != path.length()-1 && path.charAt(index+1) != '/'); if (index == -1 || index == path.length() - 1) { GlobalOptions.err.println("Warning: Illegal jar url " + path + "."); continue; } zipPrefix = path.substring(index+2); if (!zipPrefix.endsWith("/")) zipPrefix += "/"; path = path.substring(4, index); } int index = path.indexOf(':'); if (index != -1 && index < path.length()-2 && path.charAt(index+1) == '/' && path.charAt(index+2) == '/') { // This looks like an URL. try { URL base = new URL(path); try { URLConnection connection = base.openConnection(); if (zipPrefix != null || path.endsWith(".zip") || path.endsWith(".jar") || connection.getContentType().endsWith("/zip")) { // This is a zip file. Read it into memory. byte[] contents = readURLZip(connection); if (contents != null) paths[i] = new ZipPath(contents, zipPrefix); } else paths[i] = new URLPath(base); } catch (IOException ex) { GlobalOptions.err.println ("Warning: IO exception while accessing " +path+"."); } catch (SecurityException ex) { GlobalOptions.err.println ("Warning: Security exception while accessing " +path+"."); } } catch (MalformedURLException ex) { GlobalOptions.err.println ("Warning: Malformed URL "+ path + "."); } } else { try { File dir = new File(path); if (zipPrefix != null || !dir.isDirectory()) { try { paths[i] = new ZipPath(new ZipFile(dir), zipPrefix); } catch (java.io.IOException ex) { GlobalOptions.err.println ("Warning: Can't read "+ path + "."); } } else paths[i] = new LocalPath(dir); } catch (SecurityException ex) { GlobalOptions.err.println ("Warning: SecurityException while accessing " + path + "."); } } } } /** * Creates a new class info for a class residing in this search * path. This doesn't load the class immediately, this is done by * ClassInfo.loadInfo. It is no error if class doesn't exists. * @param classname the dot-separated full qualified name of the class. * For inner classes you must use the bytecode name with $, * e.g. java.util.Map$Entry. * @exception IllegalArgumentException if class name isn't valid. */ public ClassInfo getClassInfo(String classname) { checkClassName(classname); int hash = classname.hashCode(); Iterator iter = classes.iterateHashCode(hash); while (iter.hasNext()) { ClassInfo clazz = (ClassInfo) iter.next(); if (clazz.getName().equals(classname)) return clazz; } ClassInfo clazz = new ClassInfo(classname, this); classes.put(hash, clazz); return clazz; } /** * Updates the classes unify hash for a class renaming. This * should be only called by {@link ClassInfo#setName}. */ void renameClassInfo(ClassInfo classInfo, String classname) { classes.remove(classInfo.getName().hashCode(), classInfo); /* Now remove any class already loaded with that name, just * in case we're overwriting one. */ Iterator iter = classes.iterateHashCode(classname.hashCode()); while (iter.hasNext()) { ClassInfo clazz = (ClassInfo) iter.next(); if (clazz.getName().equals(classname)) { iter.remove(); break; } } classes.put(classname.hashCode(), classInfo); } /** * Checks, if a class with the given name exists somewhere in this * path. * @param classname the class name. * @exception IllegalArgumentException if class name isn't valid. */ public boolean existsClass(String classname) { checkClassName(classname); return existsFile(classname.replace('.', '/') + ".class"); } /** * Checks, if a file with the given name exists somewhere in this * path. * @param filename the file name. * @see #existsClass */ public boolean existsFile(String filename) { for (int i=0; i/. * @return An InputStream for the file. */ public InputStream getFile(String filename) throws IOException { for (int i=0; i < paths.length; i++) { if (paths[i] != null && paths[i].exists(filename)) return paths[i].getFile(filename); } throw new FileNotFoundException(filename); } /** * Searches for a filename in the class path and tells if it is a * directory. * @param filename the filename. The path components should be separated * by /. * @return true, if filename exists and is a directory, false otherwise. */ public boolean isDirectory(String filename) { for (int i=0; i < paths.length; i++) { if (paths[i] != null && paths[i].exists(filename)) return paths[i].isDirectory(filename); } return false; } /** * Searches for a filename in the class path and tells if it is a * package. This is the same as isDirectory. * @param fqn the full qualified name. The components should be dot * separated. * @return true, if filename exists and is a package, false otherwise. * @see #isDirectory */ public boolean isPackage(String fqn) { return isDirectory(fqn.replace('.', '/')); } /** * Get a list of all files in a given directory. * @param dirName the directory name. The path components must * be separated by /. * @return An enumeration with all files/directories in the given * directory. If dirName doesn't denote a directory it returns null. */ public Enumeration listFiles(final String dirName) { return new Enumeration() { int i = 0; Enumeration enum; public boolean hasMoreElements() { while (true) { while (enum == null && i < paths.length) { if (paths[i] != null && paths[i].exists(dirName) && paths[i].isDirectory(dirName)) enum = paths[i].listFiles(dirName); i++; } if (enum == null) return false; if (enum.hasMoreElements()) return true; enum = null; } } public Object nextElement() { if (!hasMoreElements()) return new NoSuchElementException(); return enum.nextElement(); } }; } /** * Get a list of all classes and packages in the given package. * @param package a dot-separated package name. * @return An enumeration with all class/subpackages in the given * package. If package doesn't denote a package it returns null. */ public Enumeration listClassesAndPackages(String packageName) { String dir = packageName.replace('.','/'); final Enumeration enum = listFiles(dir); final String prefix = dir.length() > 0 ? dir + "/" : dir; return new Enumeration() { String next = getNext(); private String getNext() { while (enum.hasMoreElements()) { String name = (String) enum.nextElement(); if (name.indexOf('.') == -1 && isDirectory(prefix + name)) // This is a package return name; if (name.endsWith(".class")) // This is a class return name.substring(0, name.length()-6); } return null; } public boolean hasMoreElements() { return next != null; } public Object nextElement() { if (next == null) throw new NoSuchElementException(); String result = next; next = getNext(); return result; } }; } boolean loadClass(ClassInfo clazz, int howMuch) throws IOException, ClassFormatException { for (int i = 0; i < paths.length; i++) { if (paths[i] != null && paths[i].loadClass(clazz, howMuch)) return true; } if (fallback != null) return fallback.loadClass(clazz, howMuch); return false; } public String toString() { StringBuffer sb = new StringBuffer("ClassPath["); for (int i = 0; i < paths.length; i++) { if (paths[i] != null) sb.append(paths[i]).append(','); } sb.append(fallback).append(']'); return sb.toString(); } }