package org.jruby.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessControlException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import static org.jruby.RubyFile.canonicalize;

Instances of JarCache provides jar index information.

Implementation is threadsafe. Since loading index information is O(jar-entries) we cache the snapshot in a WeakHashMap. The implementation pays attention to lastModified timestamp of the jar and will invalidate the cache entry if jar has been updated since the snapshot calculation.

****************************************************************************************** DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER ****************************************************************************************** The spec for this cache is disabled currently for #2655, because of last-modified time oddities on CloudBees. Please be cautious modifying this code and make sure you run the associated spec locally.
/** * Instances of JarCache provides jar index information. * * <p> * Implementation is threadsafe. * * Since loading index information is O(jar-entries) we cache the snapshot in a WeakHashMap. * The implementation pays attention to lastModified timestamp of the jar and will invalidate * the cache entry if jar has been updated since the snapshot calculation. * </p> * * ****************************************************************************************** * DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER DANGER * ****************************************************************************************** * * The spec for this cache is disabled currently for #2655, because of last-modified time * oddities on CloudBees. Please be cautious modifying this code and make sure you run the * associated spec locally. */
class JarCache { static class JarIndex { private static final String ROOT_KEY = ""; private final Map<String, String[]> cachedDirEntries; private final JarFile jar; private final long snapshotCalculated; JarIndex(String jarPath) throws IOException { this.jar = new JarFile(jarPath); this.snapshotCalculated = new File(jarPath).lastModified(); Map<String, HashSet<String>> mutableCache = new HashMap<>(); // Always have a root directory mutableCache.put(ROOT_KEY, new HashSet<>()); Enumeration<JarEntry> entries = jar.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); String path = entry.getName(); int lastPathSep; while ((lastPathSep = path.lastIndexOf('/')) != -1) { String dirPath = path.substring(0, lastPathSep); HashSet<String> paths = mutableCache.get(dirPath); if (paths == null) { mutableCache.put(dirPath, paths = new HashSet<>()); } String entryPath = path.substring(lastPathSep + 1); // "" is not really a child path, even if we see foo/ entry if (entryPath.length() > 0) paths.add(entryPath); path = dirPath; } mutableCache.get(ROOT_KEY).add(path); } Map<String, String[]> cachedDirEntries = new HashMap<>(mutableCache.size() + 8, 1); for (Map.Entry<String, HashSet<String>> entry : mutableCache.entrySet()) { Set<String> value = entry.getValue(); cachedDirEntries.put(entry.getKey(), value.toArray(new String[value.size()])); } this.cachedDirEntries = Collections.unmodifiableMap(cachedDirEntries); } public JarEntry getJarEntry(String entryPath) { return jar.getJarEntry(canonicalJarPath(entryPath)); } public String[] getDirEntries(String entryPath) { return cachedDirEntries.get(canonicalJarPath(entryPath)); } public InputStream getInputStream(JarEntry entry) throws IOException, IllegalStateException { return jar.getInputStream(entry); } public void release() { try { jar.close(); } catch (IOException ioe) { } } public boolean isValid() { return new File(jar.getName()).lastModified() <= snapshotCalculated; } private static String canonicalJarPath(String path) { String canonical = canonicalize(path); // When hitting root, canonicalize tends to add a slash (so "foo/../bar" becomes "/bar"), // which doesn't quite work with jar entry paths, since most jar paths tends to be // relative (e.g. foo.jar!foo/bar). So we fix it. if (canonical.startsWith("/") && !path.startsWith("/")) { canonical = canonical.substring(1); } return canonical; } } private final Map<String, JarIndex> indexCache = new WeakHashMap<>(); public JarIndex getIndex(String jarPath) { String cacheKey = jarPath; synchronized (indexCache) { JarIndex index = indexCache.get(cacheKey); // If the index is invalid (jar has changed since snapshot was loaded) // we can just treat it as a "new" index and cache the updated results. if (index != null && !index.isValid()) { index.release(); index = null; } if (index == null) { try { index = new JarIndex(jarPath); indexCache.put(cacheKey, index); } catch (IOException ioe) { return null; } catch (AccessControlException ace) { // No permissions to index the given path, bail out return null; } } return index; } } public boolean remove(String jarPath){ String cacheKey = jarPath; synchronized (indexCache) { JarIndex index = indexCache.get(cacheKey); if (index == null){ return false; } else{ index.release(); indexCache.remove(cacheKey); return true; } } } }