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;
}
}
}
}