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.Collections;
  36 import java.util.HashMap;
  37 import java.util.HashSet;
  38 import java.util.List;
  39 import java.util.Map;
  40 import java.util.Objects;
  41 import java.util.Set;
  42 import java.util.stream.Collectors;
  43 import java.util.stream.Stream;
  44 
  45 import jdk.tools.jlink.internal.Archive.Entry;
  46 import jdk.tools.jlink.internal.Archive.Entry.EntryType;
  47 import jdk.tools.jlink.internal.ResourcePoolManager.CompressedModuleData;
  48 import jdk.tools.jlink.plugin.PluginException;
  49 import jdk.tools.jlink.plugin.ResourcePool;
  50 import jdk.tools.jlink.plugin.ResourcePoolEntry;
  51 
  52 /**
  53  * An image (native endian.)
  54  * <pre>{@code
  55  * {
  56  *   u4 magic;
  57  *   u2 major_version;
  58  *   u2 minor_version;
  59  *   u4 resource_count;
  60  *   u4 table_length;
  61  *   u4 location_attributes_size;
  62  *   u4 strings_size;
  63  *   u4 redirect[table_length];
  64  *   u4 offsets[table_length];
  65  *   u1 location_attributes[location_attributes_size];
  66  *   u1 strings[strings_size];
  67  *   u1 content[if !EOF];
  68  * }
  69  * }</pre>
  70  */
  71 public final class ImageFileCreator {
  72     private final Map<String, List<Entry>> entriesForModule = new HashMap<>();
  73     private final ImagePluginStack plugins;
  74     private ImageFileCreator(ImagePluginStack plugins) {
  75         this.plugins = Objects.requireNonNull(plugins);
  76     }
  77 
  78     public static ExecutableImage create(Set<Archive> archives,
  79             ImagePluginStack plugins)
  80             throws IOException {
  81         return ImageFileCreator.create(archives, Collections.emptySet(), ByteOrder.nativeOrder(),
  82                 plugins);
  83     }
  84 
  85     public static ExecutableImage create(Set<Archive> archives,
  86             ByteOrder byteOrder)
  87             throws IOException {
  88         return ImageFileCreator.create(archives, Collections.emptySet(), byteOrder,
  89                 new ImagePluginStack());
  90     }
  91 
  92     public static ExecutableImage create(Set<Archive> archives,
  93             Set<String> rootModules,
  94             ByteOrder byteOrder,
  95             ImagePluginStack plugins)
  96             throws IOException
  97     {
  98         ImageFileCreator image = new ImageFileCreator(plugins);
  99         try {
 100             image.readAllEntries(archives);
 101             // write to modular image
 102             image.writeImage(archives, rootModules, byteOrder);
 103         } finally {
 104             //Close all archives
 105             for (Archive a : archives) {
 106                 a.close();
 107             }
 108         }
 109 
 110         return plugins.getExecutableImage();
 111     }
 112 
 113     private void readAllEntries(Set<Archive> archives) {
 114         archives.stream().forEach((archive) -> {
 115             Map<Boolean, List<Entry>> es;
 116             try (Stream<Entry> entries = archive.entries()) {
 117                 es = entries.collect(Collectors.partitioningBy(n -> n.type()
 118                         == EntryType.CLASS_OR_RESOURCE));
 119             }
 120             String mn = archive.moduleName();
 121             List<Entry> all = new ArrayList<>();
 122             all.addAll(es.get(false));
 123             all.addAll(es.get(true));
 124             entriesForModule.put(mn, all);
 125         });
 126     }
 127 
 128     public static void recreateJimage(Path jimageFile,
 129             Set<Archive> archives,
 130             ImagePluginStack pluginSupport)
 131             throws IOException {
 132         try {
 133             Map<String, List<Entry>> entriesForModule
 134                     = archives.stream().collect(Collectors.toMap(
 135                                     Archive::moduleName,
 136                                     a -> {
 137                                         try (Stream<Entry> entries = a.entries()) {
 138                                             return entries.collect(Collectors.toList());
 139                                         }
 140                                     }));
 141             ByteOrder order = ByteOrder.nativeOrder();
 142             BasicImageWriter writer = new BasicImageWriter(order);
 143             ResourcePoolManager pool = createPoolManager(archives, entriesForModule, order, writer);
 144             try (OutputStream fos = Files.newOutputStream(jimageFile);
 145                     BufferedOutputStream bos = new BufferedOutputStream(fos);
 146                     DataOutputStream out = new DataOutputStream(bos)) {
 147                 generateJImage(pool, writer, pluginSupport, out);
 148             }
 149         } finally {
 150             //Close all archives
 151             for (Archive a : archives) {
 152                 a.close();
 153             }
 154         }
 155     }
 156 
 157     private void writeImage(Set<Archive> archives,
 158             Set<String> rootModules,
 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(rootModules, 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 
 269         for (Archive archive : archives) {
 270             String mn = archive.moduleName();
 271             entriesForModule.get(mn).stream()
 272                 .map(e -> new ArchiveEntryResourcePoolEntry(mn,
 273                                     e.getResourcePoolEntryName(), e))
 274                 .forEach(resources::add);
 275         }
 276         return resources;
 277     }
 278 
 279     /**
 280      * Helper method that splits a Resource path onto 3 items: module, parent
 281      * and resource name.
 282      *
 283      * @param path
 284      * @return An array containing module, parent and name.
 285      */
 286     public static String[] splitPath(String path) {
 287         Objects.requireNonNull(path);
 288         String noRoot = path.substring(1);
 289         int pkgStart = noRoot.indexOf("/");
 290         String module = noRoot.substring(0, pkgStart);
 291         List<String> result = new ArrayList<>();
 292         result.add(module);
 293         String pkg = noRoot.substring(pkgStart + 1);
 294         String resName;
 295         int pkgEnd = pkg.lastIndexOf("/");
 296         if (pkgEnd == -1) { // No package.
 297             resName = pkg;
 298         } else {
 299             resName = pkg.substring(pkgEnd + 1);
 300         }
 301 
 302         pkg = toPackage(pkg, false);
 303         result.add(pkg);
 304         result.add(resName);
 305 
 306         String[] array = new String[result.size()];
 307         return result.toArray(array);
 308     }
 309 
 310     /**
 311      * Returns the path of the resource.
 312      */
 313     public static String resourceName(String path) {
 314         Objects.requireNonNull(path);
 315         String s = path.substring(1);
 316         int index = s.indexOf("/");
 317         return s.substring(index + 1);
 318     }
 319 
 320     public static String toPackage(String name) {
 321         return toPackage(name, false);
 322     }
 323 
 324     private static String toPackage(String name, boolean log) {
 325         int index = name.lastIndexOf('/');
 326         if (index > 0) {
 327             return name.substring(0, index).replace('/', '.');
 328         } else {
 329             // ## unnamed package
 330             if (log) {
 331                 System.err.format("Warning: %s in unnamed package%n", name);
 332             }
 333             return "";
 334         }
 335     }
 336 }