1 /*
2 * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation. Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26 package jdk.internal.module;
27
28 import java.io.File;
29 import java.io.IOError;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.UncheckedIOException;
33 import java.lang.module.ModuleReader;
34 import java.lang.module.ModuleReference;
35 import java.net.URI;
36 import java.nio.ByteBuffer;
37 import java.nio.file.Files;
38 import java.nio.file.Path;
39 import java.util.LinkedHashSet;
40 import java.util.List;
41 import java.util.Objects;
42 import java.util.Optional;
43 import java.util.Set;
44 import java.util.concurrent.locks.Lock;
45 import java.util.concurrent.locks.ReadWriteLock;
46 import java.util.concurrent.locks.ReentrantReadWriteLock;
47 import java.util.function.Supplier;
48 import java.util.jar.JarEntry;
49 import java.util.jar.JarFile;
50 import java.util.stream.Stream;
51 import java.util.zip.ZipFile;
52
53 import jdk.internal.jmod.JmodFile;
54 import jdk.internal.module.ModuleHashes.HashSupplier;
55 import sun.net.www.ParseUtil;
56
57 /**
58 * A factory for creating ModuleReference implementations where the modules are
59 * packaged as modular JAR file, JMOD files or where the modules are exploded
60 * on the file system.
61 */
62
63 class ModuleReferences {
64 private ModuleReferences() { }
65
66 /**
67 * Creates a ModuleReference to a possibly-patched module
68 */
69 private static ModuleReference newModule(ModuleInfo.Attributes attrs,
70 URI uri,
71 Supplier<ModuleReader> supplier,
72 ModulePatcher patcher,
73 HashSupplier hasher) {
74 ModuleReference mref = new ModuleReferenceImpl(attrs.descriptor(),
75 uri,
76 supplier,
77 null,
78 attrs.target(),
79 attrs.recordedHashes(),
80 hasher,
81 attrs.moduleResolution());
82 if (patcher != null)
83 mref = patcher.patchIfNeeded(mref);
84
85 return mref;
86 }
87
88 /**
89 * Creates a ModuleReference to a possibly-patched module in a modular JAR.
90 */
91 static ModuleReference newJarModule(ModuleInfo.Attributes attrs,
92 ModulePatcher patcher,
93 Path file) {
94 URI uri = file.toUri();
95 String fileString = file.toString();
96 Supplier<ModuleReader> supplier = new Supplier<>() {
97 @Override
98 public ModuleReader get() {
99 return new JarModuleReader(fileString, uri);
100 }
101 };
102 HashSupplier hasher = new HashSupplier() {
103 @Override
104 public byte[] generate(String algorithm) {
105 return ModuleHashes.computeHash(supplier, algorithm);
106 }
107 };
108 return newModule(attrs, uri, supplier, patcher, hasher);
109 }
110
111 /**
112 * Creates a ModuleReference to a module in a JMOD file.
113 */
114 static ModuleReference newJModModule(ModuleInfo.Attributes attrs, Path file) {
115 URI uri = file.toUri();
116 Supplier<ModuleReader> supplier = () -> new JModModuleReader(file, uri);
117 HashSupplier hasher = (a) -> ModuleHashes.computeHash(supplier, a);
118 return newModule(attrs, uri, supplier, null, hasher);
119 }
120
121 /**
122 * Creates a ModuleReference to a possibly-patched exploded module.
123 */
124 static ModuleReference newExplodedModule(ModuleInfo.Attributes attrs,
125 ModulePatcher patcher,
126 boolean previewMode,
127 Path dir) {
128 Supplier<ModuleReader> supplier = () -> new ExplodedModuleReader(dir, previewMode);
129 return newModule(attrs, dir.toUri(), supplier, patcher, null);
130 }
131
132
133 /**
134 * A base module reader that encapsulates machinery required to close the
135 * module reader safely.
136 */
137 abstract static class SafeCloseModuleReader implements ModuleReader {
138
139 // RW lock to support safe close
140 private final ReadWriteLock lock = new ReentrantReadWriteLock();
141 private final Lock readLock = lock.readLock();
142 private final Lock writeLock = lock.writeLock();
143 private boolean closed;
144
145 SafeCloseModuleReader() { }
146
147 /**
148 * Returns a URL to resource. This method is invoked by the find
149 * method to do the actual work of finding the resource.
150 */
151 abstract Optional<URI> implFind(String name) throws IOException;
152
153 /**
154 * Returns an input stream for reading a resource. This method is
155 * invoked by the open method to do the actual work of opening
156 * an input stream to the resource.
157 */
158 abstract Optional<InputStream> implOpen(String name) throws IOException;
159
160 /**
161 * Returns a stream of the names of resources in the module. This
162 * method is invoked by the list method to do the actual work of
163 * creating the stream.
164 */
165 abstract Stream<String> implList() throws IOException;
166
167 /**
168 * Closes the module reader. This method is invoked by close to do the
169 * actual work of closing the module reader.
170 */
171 abstract void implClose() throws IOException;
172
173 @Override
174 public final Optional<URI> find(String name) throws IOException {
175 readLock.lock();
176 try {
177 if (!closed) {
178 return implFind(name);
179 } else {
180 throw new IOException("ModuleReader is closed");
181 }
182 } finally {
183 readLock.unlock();
184 }
185 }
186
187
188 @Override
189 public final Optional<InputStream> open(String name) throws IOException {
190 readLock.lock();
191 try {
192 if (!closed) {
193 return implOpen(name);
194 } else {
195 throw new IOException("ModuleReader is closed");
196 }
197 } finally {
198 readLock.unlock();
199 }
200 }
201
202 @Override
203 public final Stream<String> list() throws IOException {
204 readLock.lock();
205 try {
206 if (!closed) {
207 return implList();
208 } else {
209 throw new IOException("ModuleReader is closed");
210 }
211 } finally {
212 readLock.unlock();
213 }
214 }
215
216 @Override
217 public final void close() throws IOException {
218 writeLock.lock();
219 try {
220 if (!closed) {
221 closed = true;
222 implClose();
223 }
224 } finally {
225 writeLock.unlock();
226 }
227 }
228 }
229
230
231 /**
232 * A ModuleReader for a modular JAR file.
233 */
234 static class JarModuleReader extends SafeCloseModuleReader {
235 private final JarFile jf;
236 private final URI uri;
237
238 static JarFile newJarFile(String path) {
239 try {
240 return new JarFile(new File(path),
241 true, // verify
242 ZipFile.OPEN_READ,
243 JarFile.runtimeVersion());
244 } catch (IOException ioe) {
245 throw new UncheckedIOException(ioe);
246 }
247 }
248
249 JarModuleReader(String path, URI uri) {
250 this.jf = newJarFile(path);
251 this.uri = uri;
252 }
253
254 private JarEntry getEntry(String name) {
255 return jf.getJarEntry(Objects.requireNonNull(name));
256 }
257
258 @Override
259 Optional<URI> implFind(String name) throws IOException {
260 JarEntry je = getEntry(name);
261 if (je != null) {
262 if (jf.isMultiRelease())
263 name = je.getRealName();
264 if (je.isDirectory() && !name.endsWith("/"))
265 name += "/";
266 String encodedPath = ParseUtil.encodePath(name, false);
267 String uris = "jar:" + uri + "!/" + encodedPath;
268 return Optional.of(URI.create(uris));
269 } else {
270 return Optional.empty();
271 }
272 }
273
274 @Override
275 Optional<InputStream> implOpen(String name) throws IOException {
276 JarEntry je = getEntry(name);
277 if (je != null) {
278 return Optional.of(jf.getInputStream(je));
279 } else {
280 return Optional.empty();
281 }
282 }
283
284 @Override
285 Stream<String> implList() throws IOException {
286 // take snapshot to avoid async close
287 List<String> names = jf.versionedStream()
288 .map(JarEntry::getName)
289 .toList();
290 return names.stream();
291 }
292
293 @Override
294 void implClose() throws IOException {
295 jf.close();
296 }
297 }
298
299
300 /**
301 * A ModuleReader for a JMOD file.
302 */
303 static class JModModuleReader extends SafeCloseModuleReader {
304 private final JmodFile jf;
305 private final URI uri;
306
307 static JmodFile newJmodFile(Path path) {
308 try {
309 return new JmodFile(path);
310 } catch (IOException ioe) {
311 throw new UncheckedIOException(ioe);
312 }
313 }
314
315 JModModuleReader(Path path, URI uri) {
316 this.jf = newJmodFile(path);
317 this.uri = uri;
318 }
319
320 private JmodFile.Entry getEntry(String name) {
321 Objects.requireNonNull(name);
322 return jf.getEntry(JmodFile.Section.CLASSES, name);
323 }
324
325 @Override
326 Optional<URI> implFind(String name) {
327 JmodFile.Entry je = getEntry(name);
328 if (je != null) {
329 if (je.isDirectory() && !name.endsWith("/"))
330 name += "/";
331 String encodedPath = ParseUtil.encodePath(name, false);
332 String uris = "jmod:" + uri + "!/" + encodedPath;
333 return Optional.of(URI.create(uris));
334 } else {
335 return Optional.empty();
336 }
337 }
338
339 @Override
340 Optional<InputStream> implOpen(String name) throws IOException {
341 JmodFile.Entry je = getEntry(name);
342 if (je != null) {
343 return Optional.of(jf.getInputStream(je));
344 } else {
345 return Optional.empty();
346 }
347 }
348
349 @Override
350 Stream<String> implList() throws IOException {
351 // take snapshot to avoid async close
352 List<String> names = jf.stream()
353 .filter(e -> e.section() == JmodFile.Section.CLASSES)
354 .map(JmodFile.Entry::name)
355 .toList();
356 return names.stream();
357 }
358
359 @Override
360 void implClose() throws IOException {
361 jf.close();
362 }
363 }
364
365
366 /**
367 * A ModuleReader for an exploded module.
368 */
369 static class ExplodedModuleReader implements ModuleReader {
370 private final Path dir;
371 private final Path previewDir;
372 private volatile boolean closed;
373
374 ExplodedModuleReader(Path dir, boolean previewMode) {
375 this.dir = dir;
376 Path path = dir.resolve("META-INF", "preview");
377 this.previewDir = (previewMode && Files.isDirectory(path)) ? path : null;
378 }
379
380 /**
381 * Throws IOException if the module reader is closed;
382 */
383 private void ensureOpen() throws IOException {
384 if (closed) throw new IOException("ModuleReader is closed");
385 }
386
387 private Path toFilePath(String name) throws IOException {
388 if (previewDir != null) {
389 if (isPreviewEntry(name)) {
390 return null;
391 }
392 Path previewPath = Resources.toFilePath(previewDir, name);
393 if (previewPath != null && Files.exists(previewPath)) {
394 return previewPath;
395 }
396 }
397 return Resources.toFilePath(dir, name);
398 }
399
400 @Override
401 public Optional<URI> find(String name) throws IOException {
402 ensureOpen();
403 Path path = toFilePath(name);
404 if (path != null) {
405 try {
406 return Optional.of(path.toUri());
407 } catch (IOError e) {
408 throw (IOException) e.getCause();
409 }
410 } else {
411 return Optional.empty();
412 }
413 }
414
415 @Override
416 public Optional<InputStream> open(String name) throws IOException {
417 ensureOpen();
418 Path path = toFilePath(name);
419 if (path != null) {
420 return Optional.of(Files.newInputStream(path));
421 } else {
422 return Optional.empty();
423 }
424 }
425
426 @Override
427 public Optional<ByteBuffer> read(String name) throws IOException {
428 ensureOpen();
429 Path path = toFilePath(name);
430 if (path != null) {
431 return Optional.of(ByteBuffer.wrap(Files.readAllBytes(path)));
432 } else {
433 return Optional.empty();
434 }
435 }
436
437 @Override
438 public Stream<String> list() throws IOException {
439 ensureOpen();
440 LinkedHashSet<String> names = new LinkedHashSet<>();
441 collectNames(dir, names);
442 if (previewDir != null) {
443 collectNames(previewDir, names);
444 }
445 return names.stream();
446 }
447
448 private void collectNames(Path dir, Set<String> dest) throws IOException {
449 try (Stream<Path> files = Files.walk(dir, Integer.MAX_VALUE)) {
450 files.map(f -> Resources.toResourceName(dir, f))
451 .filter(s -> !s.isEmpty())
452 .filter(s -> previewDir == null || !isPreviewEntry(s))
453 .forEach(dest::add);
454 }
455 }
456
457 @Override
458 public void close() {
459 closed = true;
460 }
461
462 // Names do not have a leading '/'.
463 private static final String PREVIEW_PREFIX = "META-INF/preview";
464
465 private static boolean isPreviewEntry(String name) {
466 return name.startsWith(PREVIEW_PREFIX) &&
467 (name.length() == PREVIEW_PREFIX.length()
468 || name.charAt(PREVIEW_PREFIX.length()) == '/');
469 }
470 }
471 }