| /* ******************************************************************* |
| * Copyright (c) 2002, 2017 Contributors |
| * All rights reserved. |
| * This program and the accompanying materials are made available |
| * under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * Palo Alto Research Center, Incorporated (PARC). |
| * ******************************************************************/ |
| package org.aspectj.weaver.bcel; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.net.URI; |
| import java.nio.file.FileSystem; |
| import java.nio.file.FileSystems; |
| import java.nio.file.FileVisitResult; |
| import java.nio.file.Files; |
| import java.nio.file.Path; |
| import java.nio.file.SimpleFileVisitor; |
| import java.nio.file.attribute.BasicFileAttributes; |
| import java.util.ArrayList; |
| import java.util.Enumeration; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.zip.ZipEntry; |
| import java.util.zip.ZipFile; |
| |
| import org.aspectj.bridge.IMessageHandler; |
| import org.aspectj.bridge.MessageUtil; |
| import org.aspectj.util.LangUtil; |
| import org.aspectj.util.SoftHashMap; |
| import org.aspectj.weaver.BCException; |
| import org.aspectj.weaver.UnresolvedType; |
| import org.aspectj.weaver.WeaverMessages; |
| import org.aspectj.weaver.tools.Trace; |
| import org.aspectj.weaver.tools.TraceFactory; |
| |
| /** |
| * @author Andy Clement |
| * @author Mario Ivankovits |
| */ |
| public class ClassPathManager { |
| |
| private static Trace trace = TraceFactory.getTraceFactory().getTrace(ClassPathManager.class); |
| |
| private static int maxOpenArchives = -1; |
| |
| private static URI JRT_URI = URI.create("jrt:/"); //$NON-NLS-1$ |
| |
| private static final int MAXOPEN_DEFAULT = 1000; |
| |
| private List<Entry> entries; |
| |
| // In order to control how many open files we have, we maintain a list. |
| // The max number is configured through the property: |
| // org.aspectj.weaver.openarchives |
| // and it defaults to 1000 |
| private List<ZipFile> openArchives = new ArrayList<ZipFile>(); |
| |
| static { |
| String openzipsString = getSystemPropertyWithoutSecurityException("org.aspectj.weaver.openarchives", |
| Integer.toString(MAXOPEN_DEFAULT)); |
| maxOpenArchives = Integer.parseInt(openzipsString); |
| if (maxOpenArchives < 20) { |
| maxOpenArchives = 1000; |
| } |
| } |
| |
| public ClassPathManager(List<String> classpath, IMessageHandler handler) { |
| if (trace.isTraceEnabled()) { |
| trace.enter("<init>", this, new Object[] { classpath==null?"null":classpath.toString(), handler }); |
| } |
| entries = new ArrayList<Entry>(); |
| for (String classpathEntry: classpath) { |
| addPath(classpathEntry,handler); |
| } |
| if (trace.isTraceEnabled()) { |
| trace.exit("<init>"); |
| } |
| } |
| |
| protected ClassPathManager() { |
| } |
| |
| public void addPath(String name, IMessageHandler handler) { |
| File f = new File(name); |
| String lc = name.toLowerCase(); |
| if (!f.isDirectory()) { |
| if (!f.isFile()) { |
| if (!lc.endsWith(".jar") || lc.endsWith(".zip")) { |
| // heuristic-only: ending with .jar or .zip means probably a |
| // zip file |
| MessageUtil.info(handler, WeaverMessages.format(WeaverMessages.ZIPFILE_ENTRY_MISSING, name)); |
| } else { |
| MessageUtil.info(handler, WeaverMessages.format(WeaverMessages.DIRECTORY_ENTRY_MISSING, name)); |
| } |
| return; |
| } |
| try { |
| if (lc.endsWith(LangUtil.JRT_FS)) { // Java9 |
| entries.add(new JImageEntry()); |
| } else { |
| entries.add(new ZipFileEntry(f)); |
| } |
| } catch (IOException ioe) { |
| MessageUtil.warn(handler, |
| WeaverMessages.format(WeaverMessages.ZIPFILE_ENTRY_INVALID, name, ioe.getMessage())); |
| return; |
| } |
| } else { |
| entries.add(new DirEntry(f)); |
| } |
| } |
| |
| public ClassFile find(UnresolvedType type) { |
| if (trace.isTraceEnabled()) { |
| trace.enter("find", this, type); |
| } |
| String name = type.getName(); |
| for (Iterator<Entry> i = entries.iterator(); i.hasNext();) { |
| Entry entry = i.next(); |
| try { |
| ClassFile ret = entry.find(name); |
| if (trace.isTraceEnabled()) { |
| trace.event("searching for "+type+" in "+entry.toString()); |
| } |
| if (ret != null) { |
| if (trace.isTraceEnabled()) { |
| trace.exit("find", ret); |
| } |
| return ret; |
| } |
| } catch (IOException ioe) { |
| // this is NOT an error: it's valid to have missing classpath entries |
| if (trace.isTraceEnabled()) { |
| trace.error("Removing classpath entry for "+entry,ioe); |
| } |
| i.remove(); |
| } |
| } |
| if (trace.isTraceEnabled()) { |
| trace.exit("find", null); |
| } |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuffer buf = new StringBuffer(); |
| boolean start = true; |
| for (Iterator<Entry> i = entries.iterator(); i.hasNext();) { |
| if (start) { |
| start = false; |
| } else { |
| buf.append(File.pathSeparator); |
| } |
| buf.append(i.next()); |
| } |
| return buf.toString(); |
| } |
| |
| public abstract static class ClassFile { |
| public abstract InputStream getInputStream() throws IOException; |
| public abstract String getPath(); |
| public abstract void close(); |
| } |
| |
| abstract static class Entry { |
| public abstract ClassFile find(String name) throws IOException; |
| } |
| |
| static class ByteBasedClassFile extends ClassFile { |
| |
| private byte[] bytes; |
| private ByteArrayInputStream bais; |
| private String path; |
| |
| public ByteBasedClassFile(byte[] bytes, String path) { |
| this.bytes = bytes; |
| this.path = path; |
| } |
| |
| @Override |
| public InputStream getInputStream() throws IOException { |
| this.bais = new ByteArrayInputStream(bytes); |
| return this.bais; |
| } |
| |
| @Override |
| public String getPath() { |
| return this.path; |
| } |
| |
| @Override |
| public void close() { |
| if (this.bais!=null) { |
| try { |
| this.bais.close(); |
| } catch (IOException e) { |
| } |
| this.bais = null; |
| } |
| } |
| |
| } |
| |
| static class FileClassFile extends ClassFile { |
| private File file; |
| private FileInputStream fis; |
| |
| public FileClassFile(File file) { |
| this.file = file; |
| } |
| |
| @Override |
| public InputStream getInputStream() throws IOException { |
| fis = new FileInputStream(file); |
| return fis; |
| } |
| |
| @Override |
| public void close() { |
| try { |
| if (fis != null) |
| fis.close(); |
| } catch (IOException ioe) { |
| throw new BCException("Can't close class file : " + file.getName(), ioe); |
| } finally { |
| fis = null; |
| } |
| } |
| |
| @Override |
| public String getPath() { |
| return file.getPath(); |
| } |
| } |
| |
| class DirEntry extends Entry { |
| private String dirPath; |
| |
| public DirEntry(File dir) { |
| this.dirPath = dir.getPath(); |
| } |
| |
| public DirEntry(String dirPath) { |
| this.dirPath = dirPath; |
| } |
| |
| @Override |
| public ClassFile find(String name) { |
| File f = new File(dirPath + File.separator + name.replace('.', File.separatorChar) + ".class"); |
| if (f.isFile()) |
| return new FileClassFile(f); |
| else |
| return null; |
| } |
| |
| @Override |
| public String toString() { |
| return dirPath; |
| } |
| } |
| |
| static class ZipEntryClassFile extends ClassFile { |
| private ZipEntry entry; |
| private ZipFileEntry zipFile; |
| private InputStream is; |
| |
| public ZipEntryClassFile(ZipFileEntry zipFile, ZipEntry entry) { |
| this.zipFile = zipFile; |
| this.entry = entry; |
| } |
| |
| @Override |
| public InputStream getInputStream() throws IOException { |
| is = zipFile.getZipFile().getInputStream(entry); |
| return is; |
| } |
| |
| @Override |
| public void close() { |
| try { |
| if (is != null) |
| is.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } finally { |
| is = null; |
| } |
| } |
| |
| @Override |
| public String getPath() { |
| return entry.getName(); |
| } |
| |
| } |
| |
| /** |
| * Maintains a shared package cache for java runtime image. This maps packages (for example: |
| * java/lang) to a starting root position in the filesystem (for example: /modules/java.base/java/lang). |
| * When searching for a type we work out the package name, use it to find where in the filesystem |
| * to start looking then run from there. Once found we do cache what we learn to make subsequent |
| * lookups of that type even faster. Maintaining just a package cache rather than complete type cache |
| * helps reduce memory usage but still gives reasonably fast lookup performance. |
| */ |
| static class JImageEntry extends Entry { |
| |
| private static FileSystem fs = null; |
| |
| private final static Map<String, Path> fileCache = new SoftHashMap<String, Path>(); |
| |
| private final static Map<String, Path> packageCache = new HashMap<String, Path>(); |
| |
| private static boolean packageCacheInitialized = false; |
| |
| public JImageEntry() { |
| if (fs == null) { |
| try { |
| fs = FileSystems.getFileSystem(JRT_URI); |
| } catch (Throwable t) { |
| throw new IllegalStateException("Unexpectedly unable to initialize a JRT filesystem", t); |
| } |
| } |
| buildPackageMap(); |
| } |
| |
| class PackageCacheBuilderVisitor extends SimpleFileVisitor<Path> { |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { |
| if (file.getNameCount() > 3 && file.toString().endsWith(".class")) { |
| int fnc = file.getNameCount(); |
| if (fnc > 3) { // There is a package name - e.g. /modules/java.base/java/lang/Object.class |
| Path packagePath = file.subpath(2, fnc-1); // e.g. java/lang |
| String packagePathString = packagePath.toString(); |
| packageCache.put(packagePathString, file.subpath(0, fnc-1)); // java/lang -> /modules/java.base/java/lang |
| } |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| } |
| |
| /** |
| * Create a map from package names to the specific directory of the package members in the filesystem. |
| */ |
| private synchronized void buildPackageMap() { |
| if (!packageCacheInitialized) { |
| packageCacheInitialized = true; |
| Iterable<java.nio.file.Path> roots = fs.getRootDirectories(); |
| PackageCacheBuilderVisitor visitor = new PackageCacheBuilderVisitor(); |
| try { |
| for (java.nio.file.Path path : roots) { |
| Files.walkFileTree(path, visitor); |
| } |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| } |
| } |
| |
| class TypeIdentifier extends SimpleFileVisitor<Path> { |
| |
| // What are we looking for? |
| private String name; |
| |
| // If set, where did we find it? |
| public Path found; |
| |
| // Basic metric count of how many files we checked before finding it |
| public int filesSearchedCount; |
| |
| public TypeIdentifier(String name) { |
| this.name = name; |
| } |
| |
| @Override |
| public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { |
| filesSearchedCount++; |
| if (file.getNameCount() > 2 && file.toString().endsWith(".class")) { |
| int fnc = file.getNameCount(); |
| Path filePath = file.subpath(2, fnc); |
| String filePathString = filePath.toString(); |
| if (filePathString.equals(name)) { |
| fileCache.put(filePathString, file); |
| found = file; |
| return FileVisitResult.TERMINATE; |
| } |
| } |
| return FileVisitResult.CONTINUE; |
| } |
| } |
| |
| private Path searchForFileAndCache(final Path startPath, final String name) { |
| TypeIdentifier locator = new TypeIdentifier(name); |
| try { |
| Files.walkFileTree(startPath, locator); |
| } catch (IOException e) { |
| throw new RuntimeException(e); |
| } |
| return locator.found; |
| } |
| |
| @Override |
| public ClassFile find(String name) throws IOException { |
| String fileName = name.replace('.', '/') + ".class"; |
| Path file = fileCache.get(fileName); |
| if (file == null) { |
| // Check the packages map to see if we know about this package |
| int idx = fileName.lastIndexOf('/'); |
| if (idx == -1) { |
| // Package not here |
| return null; |
| } |
| Path packageStart = null; |
| String packageName = null; |
| if (idx !=-1 ) { |
| packageName = fileName.substring(0, idx); |
| packageStart = packageCache.get(packageName); |
| if (packageStart != null) { |
| file = searchForFileAndCache(packageStart, fileName); |
| } |
| } |
| } |
| if (file == null) { |
| return null; |
| } |
| byte[] bs = Files.readAllBytes(file); |
| ClassFile cf = new ByteBasedClassFile(bs, fileName); |
| return cf; |
| } |
| |
| static Map<String, Path> getPackageCache() { |
| return packageCache; |
| } |
| |
| static Map<String, Path> getFileCache() { |
| return fileCache; |
| } |
| |
| } |
| |
| class ZipFileEntry extends Entry { |
| private File file; |
| private ZipFile zipFile; |
| |
| public ZipFileEntry(File file) throws IOException { |
| this.file = file; |
| } |
| |
| public ZipFileEntry(ZipFile zipFile) { |
| this.zipFile = zipFile; |
| } |
| |
| public ZipFile getZipFile() { |
| return zipFile; |
| } |
| |
| @Override |
| public ClassFile find(String name) throws IOException { |
| ensureOpen(); |
| String key = name.replace('.', '/') + ".class"; |
| ZipEntry entry = zipFile.getEntry(key); |
| if (entry != null) |
| return new ZipEntryClassFile(this, entry); |
| else |
| return null; // This zip will be closed when necessary... |
| } |
| |
| public List<ZipEntryClassFile> getAllClassFiles() throws IOException { |
| ensureOpen(); |
| List<ZipEntryClassFile> ret = new ArrayList<ZipEntryClassFile>(); |
| for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements();) { |
| ZipEntry entry = e.nextElement(); |
| String name = entry.getName(); |
| if (hasClassExtension(name)) |
| ret.add(new ZipEntryClassFile(this, entry)); |
| } |
| // if (ret.isEmpty()) close(); |
| return ret; |
| } |
| |
| private void ensureOpen() throws IOException { |
| if (zipFile != null && openArchives.contains(zipFile)) { |
| if (isReallyOpen()) |
| return; |
| } |
| if (openArchives.size() >= maxOpenArchives) { |
| closeSomeArchives(openArchives.size() / 10); // Close 10% of |
| // those open |
| } |
| zipFile = new ZipFile(file); |
| if (!isReallyOpen()) { |
| throw new FileNotFoundException("Can't open archive: " + file.getName() + " (size() check failed)"); |
| } |
| openArchives.add(zipFile); |
| } |
| |
| private boolean isReallyOpen() { |
| try { |
| zipFile.size(); // this will fail if the file has been closed |
| // for |
| // some reason; |
| return true; |
| } catch (IllegalStateException ex) { |
| // this means the zip file is closed... |
| return false; |
| } |
| |
| } |
| |
| public void closeSomeArchives(int n) { |
| for (int i = n - 1; i >= 0; i--) { |
| ZipFile zf = openArchives.get(i); |
| try { |
| zf.close(); |
| } catch (IOException e) { |
| e.printStackTrace(); |
| } |
| openArchives.remove(i); |
| } |
| } |
| |
| public void close() { |
| if (zipFile == null) |
| return; |
| try { |
| openArchives.remove(zipFile); |
| zipFile.close(); |
| } catch (IOException ioe) { |
| throw new BCException("Can't close archive: " + file.getName(), ioe); |
| } finally { |
| zipFile = null; |
| } |
| } |
| |
| @Override |
| public String toString() { |
| return file.getName(); |
| } |
| } |
| |
| /* private */static boolean hasClassExtension(String name) { |
| return name.toLowerCase().endsWith((".class")); |
| } |
| |
| public void closeArchives() { |
| for (Entry entry : entries) { |
| if (entry instanceof ZipFileEntry) { |
| ((ZipFileEntry) entry).close(); |
| } |
| openArchives.clear(); |
| } |
| } |
| |
| // Copes with the security manager |
| private static String getSystemPropertyWithoutSecurityException(String aPropertyName, String aDefaultValue) { |
| try { |
| return System.getProperty(aPropertyName, aDefaultValue); |
| } catch (SecurityException ex) { |
| return aDefaultValue; |
| } |
| } |
| |
| // Mainly exposed for testing |
| public List<Entry> getEntries() { |
| return entries; |
| } |
| |
| } |