11
11
import java .security .ProtectionDomain ;
12
12
import java .security .cert .Certificate ;
13
13
import java .util .Objects ;
14
- import java .util .concurrent .CompletableFuture ;
15
- import java .util .concurrent .atomic .AtomicReference ;
14
+ import java .util .concurrent .locks .Lock ;
15
+ import java .util .concurrent .locks .ReadWriteLock ;
16
+ import java .util .concurrent .locks .ReentrantReadWriteLock ;
16
17
import java .util .jar .JarEntry ;
17
18
import java .util .jar .JarFile ;
18
19
import java .util .zip .ZipEntry ;
20
+ import java .util .zip .ZipFile ;
19
21
20
22
import io .smallrye .common .io .jar .JarEntries ;
23
+ import io .smallrye .common .io .jar .JarFiles ;
21
24
22
25
/**
23
26
* A jar resource
24
27
*/
25
28
public class JarResource implements ClassLoadingResource {
26
29
27
- private volatile ProtectionDomain protectionDomain ;
28
30
private final ManifestInfo manifestInfo ;
31
+ private final Path jarPath ;
32
+
33
+ private final Lock readLock ;
34
+ private final Lock writeLock ;
35
+
36
+ private volatile ProtectionDomain protectionDomain ;
29
37
30
- final Path jarPath ;
31
- final AtomicReference <CompletableFuture <JarFileReference >> jarFileReference = new AtomicReference <>();
38
+ //Guarded by the read/write lock; open/close operations on the JarFile require the exclusive lock,
39
+ //while using an existing open reference can use the shared lock.
40
+ //If a lock is acquired, and as long as it's owned, we ensure that the zipFile reference
41
+ //points to an open JarFile instance, and read operations are valid.
42
+ //To close the jar, the exclusive lock must be owned, and reference will be set to null before releasing it.
43
+ //Likewise, opening a JarFile requires the exclusive lock.
44
+ private volatile JarFile zipFile ;
32
45
33
46
public JarResource (ManifestInfo manifestInfo , Path jarPath ) {
34
47
this .manifestInfo = manifestInfo ;
35
48
this .jarPath = jarPath ;
49
+ final ReadWriteLock readWriteLock = new ReentrantReadWriteLock ();
50
+ this .readLock = readWriteLock .readLock ();
51
+ this .writeLock = readWriteLock .writeLock ();
36
52
}
37
53
38
54
@ Override
@@ -53,48 +69,38 @@ public void init() {
53
69
54
70
@ Override
55
71
public byte [] getResourceData (String resource ) {
56
- return JarFileReference .withJarFile (this , resource , JarResourceDataProvider .INSTANCE );
57
- }
58
-
59
- private static class JarResourceDataProvider implements JarFileReference .JarFileConsumer <byte []> {
60
- private static final JarResourceDataProvider INSTANCE = new JarResourceDataProvider ();
61
-
62
- @ Override
63
- public byte [] apply (JarFile jarFile , Path path , String res ) {
64
- ZipEntry entry = jarFile .getEntry (res );
72
+ final ZipFile zipFile = readLockAcquireAndGetJarReference ();
73
+ try {
74
+ ZipEntry entry = zipFile .getEntry (resource );
65
75
if (entry == null ) {
66
76
return null ;
67
77
}
68
- try (InputStream is = jarFile .getInputStream (entry )) {
78
+ try (InputStream is = zipFile .getInputStream (entry )) {
69
79
byte [] data = new byte [(int ) entry .getSize ()];
70
80
int pos = 0 ;
71
81
int rem = data .length ;
72
82
while (rem > 0 ) {
73
83
int read = is .read (data , pos , rem );
74
84
if (read == -1 ) {
75
- throw new RuntimeException ("Failed to read all data for " + res );
85
+ throw new RuntimeException ("Failed to read all data for " + resource );
76
86
}
77
87
pos += read ;
78
88
rem -= read ;
79
89
}
80
90
return data ;
81
91
} catch (IOException e ) {
82
- throw new RuntimeException ("Failed to read zip entry " + res , e );
92
+ throw new RuntimeException ("Failed to read zip entry " + resource , e );
83
93
}
94
+ } finally {
95
+ readLock .unlock ();
84
96
}
85
97
}
86
98
87
99
@ Override
88
100
public URL getResourceURL (String resource ) {
89
- return JarFileReference .withJarFile (this , resource , JarResourceURLProvider .INSTANCE );
90
- }
91
-
92
- private static class JarResourceURLProvider implements JarFileReference .JarFileConsumer <URL > {
93
- private static final JarResourceURLProvider INSTANCE = new JarResourceURLProvider ();
94
-
95
- @ Override
96
- public URL apply (JarFile jarFile , Path path , String res ) {
97
- JarEntry entry = jarFile .getJarEntry (res );
101
+ final JarFile jarFile = readLockAcquireAndGetJarReference ();
102
+ try {
103
+ JarEntry entry = jarFile .getJarEntry (resource );
98
104
if (entry == null ) {
99
105
return null ;
100
106
}
@@ -104,7 +110,15 @@ public URL apply(JarFile jarFile, Path path, String res) {
104
110
if (realName .endsWith ("/" )) {
105
111
realName = realName .substring (0 , realName .length () - 1 );
106
112
}
107
- final URL resUrl = getUrl (path , realName );
113
+ final URI jarUri = jarPath .toUri ();
114
+ // first create a URI which includes both the jar file path and the relative resource name
115
+ // and then invoke a toURL on it. The URI reconstruction allows for any encoding to be done
116
+ // for the "path" which includes the "realName"
117
+ var ssp = new StringBuilder (jarUri .getPath ().length () + realName .length () + 2 );
118
+ ssp .append (jarUri .getPath ());
119
+ ssp .append ("!/" );
120
+ ssp .append (realName );
121
+ final URL resUrl = new URI (jarUri .getScheme (), ssp .toString (), null ).toURL ();
108
122
// wrap it up into a "jar" protocol URL
109
123
//horrible hack to deal with '?' characters in the URL
110
124
//seems to be the only way, the URI constructor just does not let you handle them in a sane way
@@ -122,18 +136,8 @@ public URL apply(JarFile jarFile, Path path, String res) {
122
136
} catch (MalformedURLException | URISyntaxException e ) {
123
137
throw new RuntimeException (e );
124
138
}
125
- }
126
-
127
- private static URL getUrl (Path jarPath , String realName ) throws MalformedURLException , URISyntaxException {
128
- final URI jarUri = jarPath .toUri ();
129
- // first create a URI which includes both the jar file path and the relative resource name
130
- // and then invoke a toURL on it. The URI reconstruction allows for any encoding to be done
131
- // for the "path" which includes the "realName"
132
- var ssp = new StringBuilder (jarUri .getPath ().length () + realName .length () + 2 );
133
- ssp .append (jarUri .getPath ());
134
- ssp .append ("!/" );
135
- ssp .append (realName );
136
- return new URI (jarUri .getScheme (), ssp .toString (), null ).toURL ();
139
+ } finally {
140
+ readLock .unlock ();
137
141
}
138
142
}
139
143
@@ -147,16 +151,60 @@ public ProtectionDomain getProtectionDomain() {
147
151
return protectionDomain ;
148
152
}
149
153
154
+ private JarFile readLockAcquireAndGetJarReference () {
155
+ while (true ) {
156
+ readLock .lock ();
157
+ final JarFile zipFileLocal = this .zipFile ;
158
+ if (zipFileLocal != null ) {
159
+ //Expected fast path: returns a reference to the open JarFile while owning the readLock
160
+ return zipFileLocal ;
161
+ } else {
162
+ //This Lock implementation doesn't allow upgrading a readLock to a writeLock, so release it
163
+ //as we're going to need the WriteLock.
164
+ readLock .unlock ();
165
+ //trigger the JarFile being (re)opened.
166
+ ensureJarFileIsOpen ();
167
+ //Now since we no longer own any lock, we need to try again to obtain the readLock
168
+ //and check for the reference still being valid.
169
+ //This exposes us to a race with closing the just-opened JarFile;
170
+ //however this should be extremely rare, so we can trust we won't loop much;
171
+ //A counter doesn't seem necessary, as in fact we know that methods close()
172
+ //and resetInternalCaches() are invoked each at most once, which limits the amount
173
+ //of loops here in practice.
174
+ }
175
+ }
176
+ }
177
+
178
+ private void ensureJarFileIsOpen () {
179
+ writeLock .lock ();
180
+ try {
181
+ if (this .zipFile == null ) {
182
+ try {
183
+ this .zipFile = JarFiles .create (jarPath .toFile ());
184
+ } catch (IOException e ) {
185
+ throw new RuntimeException ("Failed to open " + jarPath , e );
186
+ }
187
+ }
188
+ } finally {
189
+ writeLock .unlock ();
190
+ }
191
+ }
192
+
150
193
@ Override
151
194
public void close () {
152
- var futureRef = jarFileReference .get ();
153
- if (futureRef != null ) {
154
- // The jarfile has been already used and it's going to be removed from the cache,
155
- // so the future must be already completed
156
- var ref = futureRef .getNow (null );
157
- if (ref != null ) {
158
- ref .close (this );
195
+ writeLock .lock ();
196
+ try {
197
+ final JarFile zipFileLocal = this .zipFile ;
198
+ if (zipFileLocal != null ) {
199
+ try {
200
+ this .zipFile = null ;
201
+ zipFileLocal .close ();
202
+ } catch (Throwable e ) {
203
+ //ignore
204
+ }
159
205
}
206
+ } finally {
207
+ writeLock .unlock ();
160
208
}
161
209
}
162
210
0 commit comments