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