1 /*
   2  * Copyright (c) 2014, 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.io.BufferedOutputStream;
  28 import java.io.DataOutputStream;
  29 import java.io.IOException;
  30 import java.io.OutputStream;
  31 import java.nio.ByteOrder;
  32 import java.nio.file.Files;
  33 import java.nio.file.Path;
  34 import java.util.ArrayList;
  35 import java.util.HashMap;
  36 import java.util.HashSet;
  37 import java.util.List;
  38 import java.util.Map;
  39 import java.util.Objects;
  40 import java.util.Set;
  41 import java.util.stream.Collectors;
  42 import java.util.stream.Stream;
  43 import jdk.tools.jlink.internal.Archive.Entry;
  44 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
  45 import jdk.tools.jlink.internal.ResourcePoolManager.CompressedModuleData;
  46 import jdk.tools.jlink.plugin.PluginException;
  47 import jdk.tools.jlink.plugin.ResourcePool;
  48 import jdk.tools.jlink.plugin.ResourcePoolEntry;
  49 
  50 /**
  51  * An image (native endian.)
  52  * <pre>{@code
  53  * {
  54  *   u4 magic;
  55  *   u2 major_version;
  56  *   u2 minor_version;
  57  *   u4 resource_count;
  58  *   u4 table_length;
  59  *   u4 location_attributes_size;
  60  *   u4 strings_size;
  61  *   u4 redirect[table_length];
  62  *   u4 offsets[table_length];
  63  *   u1 location_attributes[location_attributes_size];
  64  *   u1 strings[strings_size];
  65  *   u1 content[if !EOF];
  66  * }
  67  * }</pre>
  68  */
  69 public final class ImageFileCreator {
  70     private final Map<String, List<Entry>> entriesForModule = new HashMap<>();
  71     private final ImagePluginStack plugins;
  72     private ImageFileCreator(ImagePluginStack plugins) {
  73         this.plugins = Objects.requireNonNull(plugins);
  74     }
  75 
  76     public static ExecutableImage create(Set<Archive> archives,
  77             ImagePluginStack plugins)
  78             throws IOException {
  79         return ImageFileCreator.create(archives, ByteOrder.nativeOrder(),
  80                 plugins);
  81     }
  82 
  83     public static ExecutableImage create(Set<Archive> archives,
  84             ByteOrder byteOrder)
  85             throws IOException {
  86         return ImageFileCreator.create(archives, byteOrder,
  87                 new ImagePluginStack());
  88     }
  89 
  90     public static ExecutableImage create(Set<Archive> archives,
  91             ByteOrder byteOrder,
  92             ImagePluginStack plugins)
  93             throws IOException
  94     {
  95         ImageFileCreator image = new ImageFileCreator(plugins);
  96         try {
  97             image.readAllEntries(archives);
  98             // write to modular image
  99             image.writeImage(archives, byteOrder);
 100         } finally {
 101             //Close all archives
 102             for (Archive a : archives) {
 103                 a.close();
 104             }
 105         }
 106 
 107         return plugins.getExecutableImage();
 108     }
 109 
 110     private void readAllEntries(Set<Archive> archives) {
 111         archives.stream().forEach((archive) -> {
 112             Map<Boolean, List<Entry>> es;
 113             try (Stream<Entry> entries = archive.entries()) {
 114                 es = entries.collect(Collectors.partitioningBy(n -> n.type()
 115                         == EntryType.CLASS_OR_RESOURCE));
 116             }
 117             String mn = archive.moduleName();
 118             List<Entry> all = new ArrayList<>();
 119             all.addAll(es.get(false));
 120             all.addAll(es.get(true));
 121             entriesForModule.put(mn, all);
 122         });
 123     }
 124 
 125     public static boolean isClassPackage(String path) {
 126         return path.endsWith(".class") && !path.endsWith("module-info.class");
 127     }
 128 
 129     public static void recreateJimage(Path jimageFile,
 130             Set<Archive> archives,
 131             ImagePluginStack pluginSupport)
 132             throws IOException {
 133         try {
 134             Map<String, List<Entry>> entriesForModule
 135                     = archives.stream().collect(Collectors.toMap(
 136                                     Archive::moduleName,
 137                                     a -> {
 138                                         try (Stream<Entry> entries = a.entries()) {
 139                                             return entries.collect(Collectors.toList());
 140                                         }
 141                                     }));
 142             ByteOrder order = ByteOrder.nativeOrder();
 143             BasicImageWriter writer = new BasicImageWriter(order);
 144             ResourcePoolManager pool = createPoolManager(archives, entriesForModule, order, writer);
 145             try (OutputStream fos = Files.newOutputStream(jimageFile);
 146                     BufferedOutputStream bos = new BufferedOutputStream(fos);
 147                     DataOutputStream out = new DataOutputStream(bos)) {
 148                 generateJImage(pool, writer, pluginSupport, out);
 149             }
 150         } finally {
 151             //Close all archives
 152             for (Archive a : archives) {
 153                 a.close();
 154             }
 155         }
 156     }
 157 
 158     private void writeImage(Set<Archive> archives,
 159             ByteOrder byteOrder)
 160             throws IOException {
 161         BasicImageWriter writer = new BasicImageWriter(byteOrder);
 162         ResourcePoolManager allContent = createPoolManager(archives,
 163                 entriesForModule, byteOrder, writer);
 164         ResourcePool result = generateJImage(allContent,
 165              writer, plugins, plugins.getJImageFileOutputStream());
 166 
 167         //Handle files.
 168         try {
 169             plugins.storeFiles(allContent.resourcePool(), result, writer);
 170         } catch (Exception ex) {
 171             if (JlinkTask.DEBUG) {
 172                 ex.printStackTrace();
 173             }
 174             throw new IOException(ex);
 175         }
 176     }
 177 
 178     private static ResourcePool generateJImage(ResourcePoolManager allContent,
 179             BasicImageWriter writer,
 180             ImagePluginStack pluginSupport,
 181             DataOutputStream out
 182     ) throws IOException {
 183         ResourcePool resultResources;
 184         try {
 185             resultResources = pluginSupport.visitResources(allContent);
 186         } catch (PluginException pe) {
 187             if (JlinkTask.DEBUG) {
 188                 pe.printStackTrace();
 189             }
 190             throw pe;
 191         } catch (Exception ex) {
 192             if (JlinkTask.DEBUG) {
 193                 ex.printStackTrace();
 194             }
 195             throw new IOException(ex);
 196         }
 197         Set<String> duplicates = new HashSet<>();
 198         long[] offset = new long[1];
 199 
 200         List<ResourcePoolEntry> content = new ArrayList<>();
 201         List<String> paths = new ArrayList<>();
 202                  // the order of traversing the resources and the order of
 203         // the module content being written must be the same
 204         resultResources.entries().forEach(res -> {
 205             if (res.type().equals(ResourcePoolEntry.Type.CLASS_OR_RESOURCE)) {
 206                 String path = res.path();
 207                 content.add(res);
 208                 long uncompressedSize = res.contentLength();
 209                 long compressedSize = 0;
 210                 if (res instanceof CompressedModuleData) {
 211                     CompressedModuleData comp
 212                             = (CompressedModuleData) res;
 213                     compressedSize = res.contentLength();
 214                     uncompressedSize = comp.getUncompressedSize();
 215                 }
 216                 long onFileSize = res.contentLength();
 217 
 218                 if (duplicates.contains(path)) {
 219                     System.err.format("duplicate resource \"%s\", skipping%n",
 220                             path);
 221                     // TODO Need to hang bytes on resource and write
 222                     // from resource not zip.
 223                     // Skipping resource throws off writing from zip.
 224                     offset[0] += onFileSize;
 225                     return;
 226                 }
 227                 duplicates.add(path);
 228                 writer.addLocation(path, offset[0], compressedSize, uncompressedSize);
 229                 paths.add(path);
 230                 offset[0] += onFileSize;
 231             }
 232         });
 233 
 234         ImageResourcesTree tree = new ImageResourcesTree(offset[0], writer, paths);
 235 
 236         // write header and indices
 237         byte[] bytes = writer.getBytes();
 238         out.write(bytes, 0, bytes.length);
 239 
 240         // write module content
 241         content.stream().forEach((res) -> {
 242             res.write(out);
 243         });
 244 
 245         tree.addContent(out);
 246 
 247         out.close();
 248 
 249         return resultResources;
 250     }
 251 
 252     private static ResourcePoolManager createPoolManager(Set<Archive> archives,
 253             Map<String, List<Entry>> entriesForModule,
 254             ByteOrder byteOrder,
 255             BasicImageWriter writer) throws IOException {
 256         ResourcePoolManager resources = new ResourcePoolManager(byteOrder, new StringTable() {
 257 
 258             @Override
 259             public int addString(String str) {
 260                 return writer.addString(str);
 261             }
 262 
 263             @Override
 264             public String getString(int id) {
 265                 return writer.getString(id);
 266             }
 267         });
 268         for (Archive archive : archives) {
 269             String mn = archive.moduleName();
 270             for (Entry entry : entriesForModule.get(mn)) {
 271                 String path;
 272                 if (entry.type() == EntryType.CLASS_OR_RESOURCE) {
 273                     // Removal of "classes/" radical.
 274                     path = entry.name();
 275                     if (path.endsWith("module-info.class")) {
 276                         path = "/" + path;
 277                     } else {
 278                         path = "/" + mn + "/" + path;
 279                     }
 280                 } else {
 281                     // Entry.path() contains the kind of file native, conf, bin, ...
 282                     // Keep it to avoid naming conflict (eg: native/jvm.cfg and config/jvm.cfg
 283                     path = "/" + mn + "/" + entry.path();
 284                 }
 285 
 286                 resources.add(new ArchiveEntryResourcePoolEntry(mn, path, entry));
 287             }
 288         }
 289         return resources;
 290     }
 291 
 292     /**
 293      * Helper method that splits a Resource path onto 3 items: module, parent
 294      * and resource name.
 295      *
 296      * @param path
 297      * @return An array containing module, parent and name.
 298      */
 299     public static String[] splitPath(String path) {
 300         Objects.requireNonNull(path);
 301         String noRoot = path.substring(1);
 302         int pkgStart = noRoot.indexOf("/");
 303         String module = noRoot.substring(0, pkgStart);
 304         List<String> result = new ArrayList<>();
 305         result.add(module);
 306         String pkg = noRoot.substring(pkgStart + 1);
 307         String resName;
 308         int pkgEnd = pkg.lastIndexOf("/");
 309         if (pkgEnd == -1) { // No package.
 310             resName = pkg;
 311         } else {
 312             resName = pkg.substring(pkgEnd + 1);
 313         }
 314 
 315         pkg = toPackage(pkg, false);
 316         result.add(pkg);
 317         result.add(resName);
 318 
 319         String[] array = new String[result.size()];
 320         return result.toArray(array);
 321     }
 322 
 323     private static String toPackage(String name, boolean log) {
 324         int index = name.lastIndexOf('/');
 325         if (index > 0) {
 326             return name.substring(0, index).replace('/', '.');
 327         } else {
 328             // ## unnamed package
 329             if (log) {
 330                 System.err.format("Warning: %s in unnamed package%n", name);
 331             }
 332             return "";
 333         }
 334     }
 335 }