/* * Copyright 2000-2016 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 com.intellij.util.lang; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.ShutDownTracker; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.util.SmartList; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.Stack; import com.intellij.util.io.URLUtil; import org.jetbrains.annotations.Nullable; import java.io.*; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.Attributes; import java.util.jar.JarInputStream; import java.util.jar.Manifest; public class ClassPath { private static final ResourceStringLoaderIterator ourCheckedIterator = new ResourceStringLoaderIterator(true); private static final ResourceStringLoaderIterator ourUncheckedIterator = new ResourceStringLoaderIterator(false); private static final LoaderCollector ourLoaderCollector = new LoaderCollector(); private final Stack<URL> myUrls = new Stack<URL>(); private final List<Loader> myLoaders = new ArrayList<Loader>(); private volatile boolean myAllUrlsWereProcessed; private final AtomicInteger myLastLoaderProcessed = new AtomicInteger(); private final Map<URL, Loader> myLoadersMap = new HashMap<URL, Loader>(); private final ClasspathCache myCache = new ClasspathCache(); private final boolean myCanLockJars; private final boolean myCanUseCache; private final boolean myAcceptUnescapedUrls; private final boolean myPreloadJarContents; private final boolean myCanHavePersistentIndex; @Nullable private final CachePoolImpl myCachePool; @Nullable private final UrlClassLoader.CachingCondition myCachingCondition; public ClassPath(List<URL> urls, boolean canLockJars, boolean canUseCache, boolean acceptUnescapedUrls, boolean preloadJarContents, boolean canHavePersistentIndex, @Nullable CachePoolImpl cachePool, @Nullable UrlClassLoader.CachingCondition cachingCondition) { myCanLockJars = canLockJars; myCanUseCache = canUseCache; myAcceptUnescapedUrls = acceptUnescapedUrls; myPreloadJarContents = preloadJarContents; myCachePool = cachePool; myCachingCondition = cachingCondition; myCanHavePersistentIndex = canHavePersistentIndex; push(urls); } /** @deprecated to be removed in IDEA 15 */ void addURL(URL url) { push(Collections.singletonList(url)); } private void push(List<URL> urls) { if (!urls.isEmpty()) { synchronized (myUrls) { for (int i = urls.size() - 1; i >= 0; i--) { myUrls.push(urls.get(i)); } myAllUrlsWereProcessed = false; } } } @Nullable public Resource getResource(String s, boolean flag) { final long started = startTiming(); try { int i; if (myCanUseCache) { boolean allUrlsWereProcessed; allUrlsWereProcessed = myAllUrlsWereProcessed; i = allUrlsWereProcessed ? 0 : myLastLoaderProcessed.get(); Resource prevResource = myCache.iterateLoaders(s, flag ? ourCheckedIterator : ourUncheckedIterator, s, this); if (prevResource != null || allUrlsWereProcessed) return prevResource; } else { i = 0; } String shortName = ClasspathCache.transformName(s); Loader loader; while ((loader = getLoader(i++)) != null) { if (myCanUseCache) { if (!myCache.loaderHasName(s, shortName, loader)) continue; } Resource resource = loader.getResource(s, flag); if (resource != null) { return resource; } } } finally { logTiming(this, started, s); } return null; } public Enumeration<URL> getResources(final String name, final boolean check) { return new MyEnumeration(name, check); } @Nullable private synchronized Loader getLoader(int i) { while (myLoaders.size() < i + 1) { boolean lastOne; URL url; synchronized (myUrls) { if (myUrls.empty()) { if (myCanUseCache) { myCache.nameSymbolsLoaded(); myAllUrlsWereProcessed = true; } return null; } url = myUrls.pop(); lastOne = myUrls.isEmpty(); } if (myLoadersMap.containsKey(url)) continue; try { initLoaders(url, lastOne, myLoaders.size()); } catch (IOException e) { Logger.getInstance(ClassPath.class).info("url: " + url, e); } } return myLoaders.get(i); } public List<URL> getBaseUrls() { List<URL> result = new ArrayList<URL>(); for (Loader loader : myLoaders) { result.add(loader.getBaseURL()); } return result; } /** * Used in com.intellij.openapi.projectRoots.JdkUtil#isClassPathJarEnabled(List, String) * as a condition that UrlClassLoader supports classpath jars. Please modify it accordingly. */ private void initLoaders(final URL url, boolean lastOne, int index) throws IOException { String path; if (myAcceptUnescapedUrls) { path = url.getFile(); } else { try { path = url.toURI().getSchemeSpecificPart(); } catch (URISyntaxException e) { Logger.getInstance(ClassPath.class).error("url: " + url, e); path = url.getFile(); } } if (path != null && URLUtil.FILE_PROTOCOL.equals(url.getProtocol())) { Loader loader = createLoader(url, index, new File(path), true); if (loader != null) { initLoader(url, lastOne, loader); } } } private Loader createLoader(URL url, int index, File file, boolean processRecursively) throws IOException { if (file.isDirectory()) { return new FileLoader(url, index, myCanHavePersistentIndex); } else if (file.isFile()) { Loader loader = new JarLoader(url, myCanLockJars, index, myPreloadJarContents); if (processRecursively) { String[] referencedJars = loadManifestClasspath(file); if (referencedJars != null) { for (String referencedJar : referencedJars) { try { URI uri = new URI(referencedJar); File referencedFile = new File(uri); URL referencedUrl = uri.toURL(); Loader referencedLoader = createLoader(referencedUrl, index++, referencedFile, false); if (referencedLoader != null) { initLoader(referencedUrl, false, referencedLoader); } } catch (Exception e) { Logger.getInstance(ClassPath.class).warn("url: " + url + " / " + referencedJar, e); } } } } return loader; } return null; } private void initLoader(URL url, boolean lastOne, Loader loader) throws IOException { if (myCanUseCache) { ClasspathCache.LoaderData data = myCachePool == null ? null : myCachePool.getCachedData(url); if (data == null) { data = loader.buildData(); if (myCachePool != null && myCachingCondition != null && myCachingCondition.shouldCacheData(url)) { myCachePool.cacheData(url, data); } } myCache.applyLoaderData(data, loader); if (lastOne) { myCache.nameSymbolsLoaded(); myAllUrlsWereProcessed = true; } myLastLoaderProcessed.incrementAndGet(); //assert myLastLoaderProcessed.get() == myLoaders.size(); } myLoaders.add(loader); myLoadersMap.put(url, loader); } private class MyEnumeration implements Enumeration<URL> { private int myIndex = 0; private Resource myRes = null; private final String myName; private final String myShortName; private final boolean myCheck; private final List<Loader> myLoaders; public MyEnumeration(String name, boolean check) { myName = name; myShortName = ClasspathCache.transformName(name); myCheck = check; List<Loader> loaders = null; if (myCanUseCache && myAllUrlsWereProcessed) { loaders = new SmartList<Loader>(); myCache.iterateLoaders(name, ourLoaderCollector, loaders, this); if (!name.endsWith("/")) { myCache.iterateLoaders(name.concat("/"), ourLoaderCollector, loaders, this); } } myLoaders = loaders; } private boolean next() { if (myRes != null) return true; long started = startTiming(); try { Loader loader; if (myLoaders != null) { while (myIndex < myLoaders.size()) { loader = myLoaders.get(myIndex++); if (!myCache.loaderHasName(myName, myShortName, loader)) { myRes = null; continue; } myRes = loader.getResource(myName, myCheck); if (myRes != null) return true; } } else { while ((loader = getLoader(myIndex++)) != null) { if (myCanUseCache && !myCache.loaderHasName(myName, myShortName, loader)) continue; myRes = loader.getResource(myName, myCheck); if (myRes != null) return true; } } } finally { logTiming(ClassPath.this, started, myName); } return false; } public boolean hasMoreElements() { return next(); } public URL nextElement() { if (!next()) { throw new NoSuchElementException(); } else { Resource resource = myRes; myRes = null; return resource.getURL(); } } } private static class ResourceStringLoaderIterator extends ClasspathCache.LoaderIterator<Resource, String, ClassPath> { private final boolean myFlag; private ResourceStringLoaderIterator(boolean flag) { myFlag = flag; } @Override Resource process(Loader loader, String s, ClassPath classPath) { if (!classPath.myCache.loaderHasName(s, ClasspathCache.transformName(s), loader)) return null; Resource resource = loader.getResource(s, myFlag); if (resource != null) printOrder(loader, s, resource); return resource; } } private static class LoaderCollector extends ClasspathCache.LoaderIterator<Object, List<Loader>, Object> { @Override Object process(Loader loader, List<Loader> parameter, Object parameter2) { parameter.add(loader); return null; } } private static final boolean ourDumpOrder = "true".equals(System.getProperty("idea.dump.order")); private static PrintStream ourOrder; private static long ourOrderSize; private static final Set<String> ourOrderedUrls = new HashSet<String>(); @SuppressWarnings("UseOfSystemOutOrSystemErr") private static synchronized void printOrder(Loader loader, String url, Resource resource) { if (!ourDumpOrder) return; if (!ourOrderedUrls.add(url)) return; String home = FileUtil.toSystemIndependentName(PathManager.getHomePath()); try { if (resource instanceof MemoryResource) { ourOrderSize += resource.getBytes().length; } } catch (IOException e) { e.printStackTrace(System.out); } if (ourOrder == null) { final File orderFile = new File(PathManager.getBinPath(), "order.txt"); try { if (!FileUtil.ensureCanCreateFile(orderFile)) return; ourOrder = new PrintStream(new FileOutputStream(orderFile, true)); ShutDownTracker.getInstance().registerShutdownTask(new Runnable() { @Override public void run() { closeOrderStream(); } }); } catch (IOException e) { return; } } if (ourOrder != null) { String jarURL = FileUtil.toSystemIndependentName(loader.getBaseURL().getFile()); jarURL = StringUtil.trimStart(jarURL, "file:/"); if (jarURL.startsWith(home)) { jarURL = jarURL.replaceFirst(home, ""); jarURL = StringUtil.trimEnd(jarURL, "!/"); ourOrder.println(url + ":" + jarURL); } } } @SuppressWarnings("UseOfSystemOutOrSystemErr") private static synchronized void closeOrderStream() { ourOrder.close(); System.out.println(ourOrderSize); } private static final boolean ourLogTiming = Boolean.getBoolean("idea.print.classpath.timing"); private static long ourTotalTime = 0; private static int ourTotalRequests = 0; private static long startTiming() { return ourLogTiming ? System.nanoTime() : 0; } @SuppressWarnings("UseOfSystemOutOrSystemErr") private static void logTiming(ClassPath path, long started, String msg) { if (!ourLogTiming) return; long time = System.nanoTime() - started; ourTotalTime += time; ++ourTotalRequests; if (time > 10000000L) { System.out.println((time / 1000000) + " ms for " + msg); } if (ourTotalRequests % 1000 == 0) { System.out.println(path.toString() + ", requests:" + ourTotalRequests + ", time:" + (ourTotalTime / 1000000) + "ms"); } } private static String[] loadManifestClasspath(File file) { try { JarInputStream inputStream = new JarInputStream(new FileInputStream(file)); try { Manifest manifest = inputStream.getManifest(); if (manifest != null) { final String classPath = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH); if (classPath != null) { String[] urls = classPath.split(" "); if (urls.length > 0 && urls[0].startsWith("file:")) { return urls; } } } } finally { inputStream.close(); } } catch (Exception ignore) { } return null; } }