/* * Copyright 2003-2015 JetBrains s.r.o. * * 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 jetbrains.mps.reloading; import gnu.trove.THashSet; import jetbrains.mps.project.MPSExtentions; import jetbrains.mps.util.ConditionalIterable; import jetbrains.mps.util.FileUtil; import jetbrains.mps.util.InternUtil; import jetbrains.mps.util.ReadUtil; import jetbrains.mps.vfs.IFile; import jetbrains.mps.vfs.impl.IoFileSystem; import jetbrains.mps.vfs.openapi.FileSystem; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.mps.util.Condition; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; public class JarFileClassPathItem extends RealClassPathItem { private static final Logger LOG = LogManager.getLogger(JarFileClassPathItem.class); //computed during init private boolean myIsInitialized = false; private String myPrefix; private File myFile; private final MyCache myCache = new MyCache(); private final String myPath; JarFileClassPathItem(FileSystem fileSystem, String path) { myPath = path; if (path.endsWith("!/")) { path = path.substring(0, path.length() - 2); } try { myFile = transformFile(fileSystem.getFile(path)); myPrefix = "jar:" + myFile.toURI().toURL() + "!/"; } catch (IOException e) { LOG.error("invalid class path: " + path, e); } } @Override public String getPath() { return myPath; } public String getAbsolutePath() { return myFile.getAbsolutePath(); } public File getFile() { return myFile; } @Override public boolean hasClass(String qualifiedClassName) { ensureInitialized(); final int ix = qualifiedClassName.lastIndexOf('.'); String packageName = ix == -1 ? "" : qualifiedClassName.substring(0, ix); String className = qualifiedClassName.substring(ix + 1); return myCache.hasClass(packageName, className); } @Override public boolean hasPackage(@NotNull String packageName) { ensureInitialized(); return myCache.hasPackage(packageName); } @Override public synchronized ClassBytes getClassBytes(String qualifiedClassName) { ensureInitialized(); InputStream inp = null; ZipFile zf = null; try { zf = new ZipFile(myFile); String entryName = toClassEntry(qualifiedClassName); ZipEntry entry = zf.getEntry(entryName); if (entry == null) { return null; } inp = zf.getInputStream(entry); if (inp == null) { return null; } // safe to assume int as class files have size limit way lower than 2^31 byte[] bytes = ReadUtil.read(inp, (int) entry.getSize()); return bytes == null ? null : new DefaultClassBytes(bytes, myFile.toURI().toURL()); } catch (IOException e) { LOG.error(getClass().getName(), e); return null; } finally { FileUtil.closeFileSafe(inp); closeZipFile(zf); } } private static String toClassEntry(String classQualifiedName) { StringBuilder sb = new StringBuilder(classQualifiedName); for (int i = 0; i < classQualifiedName.length(); i++) { if (sb.charAt(i) == '.') { sb.setCharAt(i, '/'); } } sb.append(MPSExtentions.DOT_CLASSFILE); return sb.toString(); } private static void closeZipFile(ZipFile zf) { if (zf != null) { try { zf.close(); } catch (IOException e) { LOG.error(JarFileClassPathItem.class.getName(), e); } } } @Override public URL getResource(String name) { ZipFile zf = null; try { zf = new ZipFile(myFile); if (zf.getEntry(name) == null) return null; return new URL(myPrefix + name); } catch (MalformedURLException e) { LOG.error(null, e); return null; } catch (IOException e) { LOG.error(null, e); return null; } finally { if (zf != null) { try { zf.close(); } catch (IOException e) { LOG.error(null, e); } } } } @Override public synchronized Iterable<String> getAvailableClasses(String namespace) { ensureInitialized(); Collection<String> start = myCache.getClassesSetFor(namespace); Condition<String> cond = new Condition<String>() { @Override public boolean met(String className) { return !isAnonymous(className); } }; return new ConditionalIterable<String>(start, cond); } @Override public synchronized Iterable<String> getSubpackages(String namespace) { ensureInitialized(); return myCache.getSubpackagesSetFor(namespace); } @Override public List<RealClassPathItem> flatten() { List<RealClassPathItem> result = new ArrayList<RealClassPathItem>(); result.add(this); return result; } @Override public void accept(IClassPathItemVisitor visitor) { visitor.visit(this); } public String toString() { return "jar-cp: " + myFile; } private void ensureInitialized() { if (myIsInitialized) return; myIsInitialized = true; buildCaches(); } private long getClassTimestamp(String name) { String path = name.replace('.', '/') + ".class"; ZipFile zf = null; try { zf = new ZipFile(myFile); ZipEntry entry = zf.getEntry(path); assert entry != null : path; return entry.getTime(); } catch (IOException e) { LOG.error(null, e); return 0; } finally { if (zf != null) { try { zf.close(); } catch (IOException e) { LOG.error(null, e); } } } } private synchronized void buildCaches() { ZipFile zf = null; try { zf = new ZipFile(myFile); Enumeration<? extends ZipEntry> entries = zf.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (!entry.isDirectory()) { String name = entry.getName(); if (!name.endsWith(MPSExtentions.DOT_CLASSFILE)) continue; int packEnd = name.lastIndexOf('/'); String pack; String className; if (packEnd == -1) { pack = ""; className = name.substring(0, name.length() - MPSExtentions.DOT_CLASSFILE.length()); } else { pack = packEnd > 0 ? name.substring(0, packEnd).replace('/', '.') : name; className = name.substring(packEnd + 1, name.length() - MPSExtentions.DOT_CLASSFILE.length()); } myCache.addClass(pack, InternUtil.intern(className)); } } } catch (IOException e) { LOG.error(String.format("Path %s (%s) \nFile exists: %s", myFile.getPath(), myFile.getAbsolutePath(), myFile.exists()), e); } finally { if (zf != null) { try { zf.close(); } catch (IOException e) { LOG.error(null, e); } } } } private static File transformFile(IFile f) throws IOException { if (!f.isInArchive()) { return new File(f.getPath()); } File tmpFile = File.createTempFile(f.getName(), "tmp"); tmpFile.deleteOnExit(); OutputStream os = null; InputStream is = null; try { is = new BufferedInputStream(f.openInputStream()); os = new BufferedOutputStream(new FileOutputStream(tmpFile)); int result; while ((result = is.read()) != -1) { os.write(result); } } finally { if (is != null) { is.close(); } if (os != null) { os.close(); } } return tmpFile; } //do not touch this class if you are not sure in your changes - this can lead to excess memory consumption (see #53513) private static class MyCache { private final Entry myTopPackage = new Entry(""); private Entry getEntry(String pack) { Entry e = myTopPackage; PackageNameIterator it = new PackageNameIterator(pack); while (it.hasNext() && e != null) { e = e.getSubPackage(it.next()); } return e; } public Collection<String> getClassesSetFor(String pack) { Entry e = getEntry(pack); return e == null ? Collections.<String>emptyList() : e.getClasses(); } public boolean hasClass(String pack, String className) { Entry e = getEntry(pack); return e != null && e.hasClass(className); } public boolean hasPackage(String pack) { return getEntry(pack) != null; } public Collection<String> getSubpackagesSetFor(String pack) { Entry e = getEntry(pack); return e == null ? Collections.<String>emptyList() : e.getImmediateSubPackages(pack); } public void addClass(String pack, String className) { //namespace is never null; Entry e = myTopPackage; PackageNameIterator it = new PackageNameIterator(pack); while (it.hasNext()) { e = e.createSubPackage(it.next()); } e.addClass(className); } } /** * PackageNameIterator("").hasNext() == false * PackageNameIterator("a").hasNext() == true * PackageNameIterator("a").hasNext().hasNext() == false */ private static class PackageNameIterator implements Iterator<String> { private final String myPackageName; private int start = 0; private int dotIndex; public PackageNameIterator(String packageName) { myPackageName = packageName; advance(); } @Override public boolean hasNext() { return dotIndex > 0 && dotIndex <= myPackageName.length(); } @Override public String next() { String rv = myPackageName.substring(start, dotIndex); start = dotIndex + 1; advance(); return rv; } private void advance() { dotIndex = myPackageName.indexOf('.', start); if (dotIndex == -1 && start < myPackageName.length()) { dotIndex = myPackageName.length(); } } @Override public void remove() { throw new UnsupportedOperationException(); } } private static class Entry { private final String myPackageName; private ArrayList<Entry> mySubpackages; private THashSet<String> myClassNames; public Entry(String packageName) { myPackageName = packageName; } public Entry createSubPackage(String packageNamePart) { if (mySubpackages == null) { mySubpackages = new ArrayList<Entry>(4); final Entry rv = new Entry(new String(packageNamePart)); mySubpackages.add(rv); return rv; } final int ix = indexOf(packageNamePart); if (ix < 0) { final Entry rv = new Entry(new String(packageNamePart)); mySubpackages.add(-ix - 1, rv); return rv; } else { return mySubpackages.get(ix); } } public Entry getSubPackage(String packageNamePart) { final int ix = indexOf(packageNamePart); if (ix < 0) { return null; } return mySubpackages.get(ix); } public void addClass(String className) { if (myClassNames == null) { myClassNames = new THashSet<String>(); } myClassNames.add(className); } public boolean hasClass(String className) { return myClassNames != null && myClassNames.contains(className); } public Collection<String> getImmediateSubPackages(String parent) { if (mySubpackages == null) { return Collections.emptyList(); } ArrayList<String> rv = new ArrayList<String>(mySubpackages.size()); for (Entry e : mySubpackages) { if (parent == null || parent.isEmpty()) { rv.add(e.myPackageName); } else { rv.add(parent + '.' + e.myPackageName); } } return rv; } public Collection<String> getClasses() { return myClassNames == null ? Collections.<String>emptyList() : myClassNames; } @Override public String toString() { return String.format("%s - %d;%d", myPackageName, mySubpackages == null ? 0 : mySubpackages.size(), myClassNames == null ? 0 : myClassNames.size()); } private int indexOf(String packageName) { if (mySubpackages == null) { return -1; } int low = 0; int high = mySubpackages.size() - 1; while (low <= high) { int mid = (low + high) >>> 1; Entry c = mySubpackages.get(mid); int cmp = packageName.compareTo(c.myPackageName); if (cmp < 0) { high = mid - 1; } else if (cmp > 0) { low = mid + 1; } else { return mid; } } return -(low + 1); } } }