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