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