1 /*
2 * Copyright (c) 2015, 2025, 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 package jdk.tools.jlink.internal;
26
27 import java.lang.module.ModuleDescriptor;
28 import java.nio.ByteBuffer;
29 import java.nio.ByteOrder;
30 import java.util.HashSet;
31 import java.util.LinkedHashMap;
32 import java.util.Map;
33 import java.util.Objects;
34 import java.util.Optional;
35 import java.util.Set;
36 import java.util.stream.Stream;
37 import jdk.internal.jimage.decompressor.CompressedResourceHeader;
38 import jdk.internal.module.Checks;
39 import jdk.internal.module.ModuleInfo;
40 import jdk.internal.module.ModuleInfo.Attributes;
41 import jdk.internal.module.ModuleTarget;
42 import jdk.tools.jlink.plugin.ResourcePool;
43 import jdk.tools.jlink.plugin.ResourcePoolBuilder;
44 import jdk.tools.jlink.plugin.ResourcePoolEntry;
45 import jdk.tools.jlink.plugin.ResourcePoolModule;
46 import jdk.tools.jlink.plugin.ResourcePoolModuleView;
47 import jdk.tools.jlink.plugin.PluginException;
48
49 /**
50 * A manager for pool of resources.
51 */
52 public class ResourcePoolManager {
53 // utility to read Module Attributes of the given ResourcePoolModule
54 static Attributes readModuleAttributes(ResourcePoolModule mod) {
55 String p = "/" + mod.name() + "/module-info.class";
56 Optional<ResourcePoolEntry> content = mod.findEntry(p);
57 if (content.isEmpty()) {
58 throw new PluginException("module-info.class not found for " +
59 mod.name() + " module");
60 }
61 ByteBuffer bb = ByteBuffer.wrap(content.get().contentBytes());
62 try {
63 return ModuleInfo.read(bb, null);
64 } catch (RuntimeException re) {
65 throw new RuntimeException("module info cannot be read for " + mod.name(), re);
66 }
67 }
68
69 static class ResourcePoolModuleImpl implements ResourcePoolModule {
70 private static final String PREVIEW_PREFIX = "META-INF/preview/";
71
72 final Map<String, ResourcePoolEntry> moduleContent = new LinkedHashMap<>();
73 // lazily initialized
74 private ModuleDescriptor descriptor;
75 private ModuleTarget target;
76
77 final String name;
78
79 private ResourcePoolModuleImpl(String name) {
80 this.name = name;
81 }
82
83 @Override
84 public String name() {
85 return name;
86 }
87
88 @Override
89 public Optional<ResourcePoolEntry> findEntry(String path) {
90 if (!path.startsWith("/")) {
91 path = "/" + path;
92 }
93 if (!path.startsWith("/" + name + "/")) {
94 path = "/" + name + path; // path already starts with '/'
95 }
96 return Optional.ofNullable(moduleContent.get(path));
97 }
98
99 @Override
100 public ModuleDescriptor descriptor() {
101 initModuleAttributes();
102 return descriptor;
103 }
104
105 @Override
106 public String targetPlatform() {
107 initModuleAttributes();
108 return target != null? target.targetPlatform() : null;
109 }
110
111 private void initModuleAttributes() {
112 if (this.descriptor == null) {
113 Attributes attr = readModuleAttributes(this);
114 this.descriptor = attr.descriptor();
115 this.target = attr.target();
116 }
117 }
118
119 @Override
120 public Set<String> packages() {
121 Set<String> pkgs = new HashSet<>();
122 moduleContent.values().stream()
123 .filter(m -> m.type() == ResourcePoolEntry.Type.CLASS_OR_RESOURCE)
124 .forEach(res -> inferPackageName(res).ifPresent(pkgs::add));
125 return pkgs;
126 }
127
128 @Override
129 public String toString() {
130 return name();
131 }
132
133 @Override
134 public Stream<ResourcePoolEntry> entries() {
135 return moduleContent.values().stream();
136 }
137
138 @Override
139 public int entryCount() {
140 return moduleContent.values().size();
141 }
142
143 /**
144 * Returns a valid non-empty package name, inferred from a resource pool
145 * entry's path.
146 *
147 * <p>If the resource pool entry is for a preview resource (i.e. with
148 * path {@code "/mod-name/META-INF/preview/pkg-path/resource-name"})
149 * the package name is the non-preview name based on {@code "pkg-path"}.
150 *
151 * @return the inferred package name, or {@link Optional#empty() empty}
152 * if no name could be inferred.
153 */
154 private static Optional<String> inferPackageName(ResourcePoolEntry res) {
155 // Expect entry paths to be "/mod-name/pkg-path/resource-name", but
156 // may also get "/mod-name/META-INF/preview/pkg-path/resource-name"
157 String name = res.path();
158 if (name.charAt(0) == '/') {
159 int pkgStart = name.indexOf('/', 1) + 1;
160 int pkgEnd = name.lastIndexOf('/');
161 if (pkgStart > 0 && pkgEnd > pkgStart) {
162 String pkgPath = name.substring(pkgStart, pkgEnd);
163 // Handle preview paths by removing the prefix.
164 if (pkgPath.startsWith(PREVIEW_PREFIX)) {
165 pkgPath = pkgPath.substring(PREVIEW_PREFIX.length());
166 }
167 String pkgName = pkgPath.replace('/', '.');
168 if (Checks.isPackageName(pkgName)) {
169 return Optional.of(pkgName);
170 }
171 }
172 }
173 return Optional.empty();
174 }
175 }
176
177 public class ResourcePoolImpl implements ResourcePool {
178 @Override
179 public ResourcePoolModuleView moduleView() {
180 return ResourcePoolManager.this.moduleView();
181 }
182
183 @Override
184 public Stream<ResourcePoolEntry> entries() {
185 return ResourcePoolManager.this.entries();
186 }
187
188 @Override
189 public int entryCount() {
190 return ResourcePoolManager.this.entryCount();
191 }
192
193 @Override
194 public Optional<ResourcePoolEntry> findEntry(String path) {
195 return ResourcePoolManager.this.findEntry(path);
196 }
197
198 @Override
199 public Optional<ResourcePoolEntry> findEntryInContext(String path, ResourcePoolEntry context) {
200 return ResourcePoolManager.this.findEntryInContext(path, context);
201 }
202
203 @Override
204 public boolean contains(ResourcePoolEntry data) {
205 return ResourcePoolManager.this.contains(data);
206 }
207
208 @Override
209 public boolean isEmpty() {
210 return ResourcePoolManager.this.isEmpty();
211 }
212
213 @Override
214 public ByteOrder byteOrder() {
215 return ResourcePoolManager.this.byteOrder();
216 }
217
218 public StringTable getStringTable() {
219 return ResourcePoolManager.this.getStringTable();
220 }
221 }
222
223 class ResourcePoolBuilderImpl implements ResourcePoolBuilder {
224 private boolean built;
225
226 @Override
227 public void add(ResourcePoolEntry data) {
228 if (built) {
229 throw new IllegalStateException("resource pool already built!");
230 }
231 ResourcePoolManager.this.add(data);
232 }
233
234 @Override
235 public ResourcePool build() {
236 built = true;
237 return ResourcePoolManager.this.resourcePool();
238 }
239 }
240
241 class ResourcePoolModuleViewImpl implements ResourcePoolModuleView {
242 @Override
243 public Optional<ResourcePoolModule> findModule(String name) {
244 return ResourcePoolManager.this.findModule(name);
245 }
246
247 @Override
248 public Stream<ResourcePoolModule> modules() {
249 return ResourcePoolManager.this.modules();
250 }
251
252 @Override
253 public int moduleCount() {
254 return ResourcePoolManager.this.moduleCount();
255 }
256 }
257
258 private final Map<String, ResourcePoolEntry> resources = new LinkedHashMap<>();
259 private final Map<String, ResourcePoolModule> modules = new LinkedHashMap<>();
260 private final ByteOrder order;
261 private final StringTable table;
262 private final ResourcePool poolImpl;
263 private final ResourcePoolBuilder poolBuilderImpl;
264 private final ResourcePoolModuleView moduleViewImpl;
265
266 public ResourcePoolManager() {
267 this(ByteOrder.nativeOrder());
268 }
269
270 public ResourcePoolManager(ByteOrder order) {
271 this(order, new StringTable() {
272
273 @Override
274 public int addString(String str) {
275 return -1;
276 }
277
278 @Override
279 public String getString(int id) {
280 return null;
281 }
282 });
283 }
284
285 public ResourcePoolManager(ByteOrder order, StringTable table) {
286 this.order = Objects.requireNonNull(order);
287 this.table = Objects.requireNonNull(table);
288 this.poolImpl = new ResourcePoolImpl();
289 this.poolBuilderImpl = new ResourcePoolBuilderImpl();
290 this.moduleViewImpl = new ResourcePoolModuleViewImpl();
291 }
292
293 public ResourcePool resourcePool() {
294 return poolImpl;
295 }
296
297 public ResourcePoolBuilder resourcePoolBuilder() {
298 return poolBuilderImpl;
299 }
300
301 public ResourcePoolModuleView moduleView() {
302 return moduleViewImpl;
303 }
304
305 /**
306 * Add a ResourcePoolEntry.
307 *
308 * @param data The ResourcePoolEntry to add.
309 */
310 public void add(ResourcePoolEntry data) {
311 Objects.requireNonNull(data);
312 if (resources.get(data.path()) != null) {
313 throw new PluginException("Resource " + data.path()
314 + " already present");
315 }
316 String modulename = data.moduleName();
317 ResourcePoolModuleImpl m = (ResourcePoolModuleImpl)modules.get(modulename);
318 if (m == null) {
319 m = new ResourcePoolModuleImpl(modulename);
320 modules.put(modulename, m);
321 }
322 resources.put(data.path(), data);
323 m.moduleContent.put(data.path(), data);
324 }
325
326 /**
327 * Retrieves the module for the provided name.
328 *
329 * @param name The module name
330 * @return the module of matching name, if found
331 */
332 public Optional<ResourcePoolModule> findModule(String name) {
333 Objects.requireNonNull(name);
334 return Optional.ofNullable(modules.get(name));
335 }
336
337 /**
338 * The stream of modules contained in this ResourcePool.
339 *
340 * @return The stream of modules.
341 */
342 public Stream<ResourcePoolModule> modules() {
343 return modules.values().stream();
344 }
345
346 /**
347 * Return the number of ResourcePoolModule count in this ResourcePool.
348 *
349 * @return the module count.
350 */
351 public int moduleCount() {
352 return modules.size();
353 }
354
355 /**
356 * Get all ResourcePoolEntry contained in this ResourcePool instance.
357 *
358 * @return The stream of ResourcePoolModuleEntries.
359 */
360 public Stream<ResourcePoolEntry> entries() {
361 return resources.values().stream();
362 }
363
364 /**
365 * Return the number of ResourcePoolEntry count in this ResourcePool.
366 *
367 * @return the entry count.
368 */
369 public int entryCount() {
370 return resources.values().size();
371 }
372
373 /**
374 * Get the ResourcePoolEntry for the passed path.
375 *
376 * @param path A data path
377 * @return A ResourcePoolEntry instance or null if the data is not found
378 */
379 public Optional<ResourcePoolEntry> findEntry(String path) {
380 Objects.requireNonNull(path);
381 return Optional.ofNullable(resources.get(path));
382 }
383
384 /**
385 * Get the ResourcePoolEntry for the passed path restricted to supplied context.
386 *
387 * @param path A data path
388 * @param context A context of the search
389 * @return A ResourcePoolEntry instance or null if the data is not found
390 */
391 public Optional<ResourcePoolEntry> findEntryInContext(String path, ResourcePoolEntry context) {
392 Objects.requireNonNull(path);
393 Objects.requireNonNull(context);
394 ResourcePoolModule module = modules.get(context.moduleName());
395 Objects.requireNonNull(module);
396 Optional<ResourcePoolEntry> entry = module.findEntry(path);
397 // Navigating other modules via requires and exports is problematic
398 // since we cannot construct the runtime model of loaders and layers.
399 return entry;
400 }
401
402 /**
403 * Check if the ResourcePool contains the given ResourcePoolEntry.
404 *
405 * @param data The module data to check existence for.
406 * @return The module data or null if not found.
407 */
408 public boolean contains(ResourcePoolEntry data) {
409 Objects.requireNonNull(data);
410 return findEntry(data.path()).isPresent();
411 }
412
413 /**
414 * Check if the ResourcePool contains some content at all.
415 *
416 * @return True, no content, false otherwise.
417 */
418 public boolean isEmpty() {
419 return resources.isEmpty();
420 }
421
422 /**
423 * The ByteOrder currently in use when generating the jimage file.
424 *
425 * @return The ByteOrder.
426 */
427 public ByteOrder byteOrder() {
428 return order;
429 }
430
431 public StringTable getStringTable() {
432 return table;
433 }
434
435 /**
436 * A resource that has been compressed.
437 */
438 public static final class CompressedModuleData extends ByteArrayResourcePoolEntry {
439
440 final long uncompressed_size;
441
442 private CompressedModuleData(String module, String path,
443 byte[] content, long uncompressed_size) {
444 super(module, path, ResourcePoolEntry.Type.CLASS_OR_RESOURCE, content);
445 this.uncompressed_size = uncompressed_size;
446 }
447
448 public long getUncompressedSize() {
449 return uncompressed_size;
450 }
451
452 @Override
453 public boolean equals(Object other) {
454 if (!(other instanceof CompressedModuleData)) {
455 return false;
456 }
457 CompressedModuleData f = (CompressedModuleData) other;
458 return f.path().equals(path());
459 }
460
461 @Override
462 public int hashCode() {
463 return super.hashCode();
464 }
465 }
466
467 public static CompressedModuleData newCompressedResource(ResourcePoolEntry original,
468 ByteBuffer compressed,
469 String plugin,
470 StringTable strings,
471 ByteOrder order) {
472 Objects.requireNonNull(original);
473 Objects.requireNonNull(compressed);
474 Objects.requireNonNull(plugin);
475
476 boolean isTerminal = !(original instanceof CompressedModuleData);
477 long uncompressed_size = original.contentLength();
478 if (original instanceof CompressedModuleData) {
479 CompressedModuleData comp = (CompressedModuleData) original;
480 uncompressed_size = comp.getUncompressedSize();
481 }
482 int nameOffset = strings.addString(plugin);
483 CompressedResourceHeader rh
484 = new CompressedResourceHeader(compressed.limit(), original.contentLength(),
485 nameOffset, isTerminal);
486 // Merge header with content;
487 byte[] h = rh.getBytes(order);
488 ByteBuffer bb = ByteBuffer.allocate(compressed.limit() + h.length);
489 bb.order(order);
490 bb.put(h);
491 bb.put(compressed);
492 byte[] contentWithHeader = bb.array();
493
494 CompressedModuleData compressedResource
495 = new CompressedModuleData(original.moduleName(), original.path(),
496 contentWithHeader, uncompressed_size);
497 return compressedResource;
498 }
499 }