-
-
Notifications
You must be signed in to change notification settings - Fork 938
Expand file tree
/
Copy pathJarCache.java
More file actions
205 lines (166 loc) · 7.2 KB
/
JarCache.java
File metadata and controls
205 lines (166 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package org.jruby.util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
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.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.jruby.util.cli.Options;
import static org.jruby.RubyFile.canonicalize;
/**
* 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 lastModified;
private Long lastModifiedExpiration;
JarIndex(String jarPath) throws IOException {
this.jar = new JarFile(jarPath);
this.lastModified = getLastModified(jarPath);
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);
}
/**
* Determines the last modification timestamp of a file.
* <p>
* NOTE: Getting the value is an expensive operation on Windows systems (see
* {@link <a href="https://github.com/jruby/jruby/issues/6730}").
* Therefore a cached value is used with a maximum lifetime of {@link Options#JAR_CACHE_EXPIRATION} milliseconds.
*
* @param jarPath The path to the JAR file.
* @return The last modification timestamp.
*/
private long getLastModified(String jarPath) {
long currentTimeMillis = System.currentTimeMillis();
if (lastModifiedExpiration != null && currentTimeMillis < lastModifiedExpiration) {
return lastModified;
}
this.lastModifiedExpiration = currentTimeMillis + Options.JAR_CACHE_EXPIRATION.load();
return new File(jarPath).lastModified();
}
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 getLastModified(jar.getName()) <= lastModified;
}
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 static class SoftJarIndex extends SoftReference<JarIndex> {
private final String key;
public SoftJarIndex(String key, JarIndex index) {
super(index);
this.key = key;
}
public String getKey() {
return key;
}
}
private final Map<String, SoftJarIndex> indexCache = new ConcurrentHashMap<>();
private final ReferenceQueue<JarIndex> indexQueue = new ReferenceQueue<>();
public JarIndex getIndex(String jarPath) {
String cacheKey = jarPath;
cleanup();
SoftReference<JarIndex> indexRef = indexCache.get(cacheKey);
JarIndex index = indexRef == null ? null : indexRef.get();
// 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.
// The old index will be dereferenced once no longer in use and eventually get cleaned up.
if (index != null && !index.isValid()) {
index = null;
}
if (index == null) {
try {
index = new JarIndex(jarPath);
indexCache.put(cacheKey, new SoftJarIndex(cacheKey, index));
} catch (IOException ioe) {
return null;
}
}
return index;
}
public void remove(String jarPath) {
// remove but do not otherwise damage the index associated with this path, since it may still be in use
indexCache.remove(jarPath);
}
// must be called under locked indexCache
private void cleanup() {
SoftJarIndex indexRef;
while ((indexRef = (SoftJarIndex) indexQueue.poll()) != null) {
indexCache.remove(indexRef.getKey());
}
}
}