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 */
25package jdk.tools.jlink.internal;
26
27import java.io.DataOutputStream;
28import java.io.IOException;
29import java.lang.module.ModuleDescriptor;
30import java.nio.ByteOrder;
31import java.util.ArrayList;
32import java.util.Collection;
33import java.util.Collections;
34import java.util.Comparator;
35import java.util.HashMap;
36import java.util.List;
37import java.util.Map;
38import java.util.Objects;
39import java.util.Optional;
40import java.util.Set;
41import java.util.function.Function;
42import java.util.stream.Collectors;
43import java.util.stream.Stream;
44
45import jdk.internal.jimage.decompressor.Decompressor;
46import jdk.internal.module.ModuleInfo.Attributes;
47import jdk.internal.module.ModuleTarget;
48import jdk.tools.jlink.plugin.Plugin;
49import jdk.tools.jlink.builder.ImageBuilder;
50import jdk.tools.jlink.plugin.PluginException;
51import jdk.tools.jlink.plugin.ResourcePool;
52import jdk.tools.jlink.plugin.ResourcePoolModule;
53import jdk.tools.jlink.plugin.ResourcePoolEntry;
54import jdk.tools.jlink.internal.ResourcePoolManager.ResourcePoolImpl;
55
56/**
57 * Plugins Stack. Plugins entry point to apply transformations onto resources
58 * and files.
59 */
60public final class ImagePluginStack {
61
62    public interface ImageProvider {
63
64        ExecutableImage retrieve(ImagePluginStack stack) throws IOException;
65    }
66
67    public static final class OrderedResourcePoolManager extends ResourcePoolManager {
68        class OrderedResourcePool extends ResourcePoolImpl {
69            List<ResourcePoolEntry> getOrderedList() {
70                return OrderedResourcePoolManager.this.getOrderedList();
71            }
72        }
73
74        private final List<ResourcePoolEntry> orderedList = new ArrayList<>();
75        private final ResourcePoolImpl poolImpl = new OrderedResourcePool();
76
77        public OrderedResourcePoolManager(ByteOrder order, StringTable table) {
78            super(order, table);
79        }
80
81        @Override
82        public ResourcePool resourcePool() {
83            return poolImpl;
84        }
85
86        /**
87         * Add a resource.
88         *
89         * @param resource The Resource to add.
90         */
91        @Override
92        public void add(ResourcePoolEntry resource) {
93            super.add(resource);
94            orderedList.add(resource);
95        }
96
97        List<ResourcePoolEntry> getOrderedList() {
98            return Collections.unmodifiableList(orderedList);
99        }
100    }
101
102    private final static class CheckOrderResourcePoolManager extends ResourcePoolManager {
103
104        private final List<ResourcePoolEntry> orderedList;
105        private int currentIndex;
106
107        public CheckOrderResourcePoolManager(ByteOrder order, List<ResourcePoolEntry> orderedList, StringTable table) {
108            super(order, table);
109            this.orderedList = Objects.requireNonNull(orderedList);
110        }
111
112        /**
113         * Add a resource.
114         *
115         * @param resource The Resource to add.
116         */
117        @Override
118        public void add(ResourcePoolEntry resource) {
119            ResourcePoolEntry ordered = orderedList.get(currentIndex);
120            if (!resource.equals(ordered)) {
121                throw new PluginException("Resource " + resource.path() + " not in the right order");
122            }
123            super.add(resource);
124            currentIndex += 1;
125        }
126    }
127
128    private static final class PreVisitStrings implements StringTable {
129
130        private int currentid = 0;
131        private final Map<String, Integer> stringsUsage = new HashMap<>();
132        private final Map<String, Integer> stringsMap = new HashMap<>();
133        private final Map<Integer, String> reverseMap = new HashMap<>();
134
135        @Override
136        public int addString(String str) {
137            Objects.requireNonNull(str);
138            Integer count = stringsUsage.get(str);
139            if (count == null) {
140                count = 0;
141            }
142            count += 1;
143            stringsUsage.put(str, count);
144            Integer id = stringsMap.get(str);
145            if (id == null) {
146                id = currentid;
147                stringsMap.put(str, id);
148                currentid += 1;
149                reverseMap.put(id, str);
150            }
151
152            return id;
153        }
154
155        private List<String> getSortedStrings() {
156            Stream<java.util.Map.Entry<String, Integer>> stream
157                    = stringsUsage.entrySet().stream();
158            // Remove strings that have a single occurence
159            List<String> result = stream.sorted(Comparator.comparing(e -> e.getValue(),
160                    Comparator.reverseOrder())).filter((e) -> {
161                        return e.getValue() > 1;
162                    }).map(java.util.Map.Entry::getKey).
163                    collect(Collectors.toList());
164            return result;
165        }
166
167        @Override
168        public String getString(int id) {
169            return reverseMap.get(id);
170        }
171    }
172
173    private final ImageBuilder imageBuilder;
174    private final Plugin lastSorter;
175    private final List<Plugin> plugins = new ArrayList<>();
176    private final List<ResourcePrevisitor> resourcePrevisitors = new ArrayList<>();
177    private final boolean validate;
178
179    public ImagePluginStack() {
180        this(null, Collections.emptyList(), null);
181    }
182
183    public ImagePluginStack(ImageBuilder imageBuilder,
184            List<Plugin> plugins,
185            Plugin lastSorter) {
186        this(imageBuilder, plugins, lastSorter, true);
187    }
188
189    public ImagePluginStack(ImageBuilder imageBuilder,
190            List<Plugin> plugins,
191            Plugin lastSorter,
192            boolean validate) {
193        this.imageBuilder = Objects.requireNonNull(imageBuilder);
194        this.lastSorter = lastSorter;
195        this.plugins.addAll(Objects.requireNonNull(plugins));
196        plugins.stream().forEach((p) -> {
197            Objects.requireNonNull(p);
198            if (p instanceof ResourcePrevisitor) {
199                resourcePrevisitors.add((ResourcePrevisitor) p);
200            }
201        });
202        this.validate = validate;
203    }
204
205    public void operate(ImageProvider provider) throws Exception {
206        ExecutableImage img = provider.retrieve(this);
207        List<String> arguments = new ArrayList<>();
208        plugins.stream()
209                .filter(PostProcessor.class::isInstance)
210                .map((plugin) -> ((PostProcessor)plugin).process(img))
211                .filter((lst) -> (lst != null))
212                .forEach((lst) -> {
213                     arguments.addAll(lst);
214                });
215        img.storeLaunchArgs(arguments);
216    }
217
218    public DataOutputStream getJImageFileOutputStream() throws IOException {
219        return imageBuilder.getJImageOutputStream();
220    }
221
222    public ImageBuilder getImageBuilder() {
223        return imageBuilder;
224    }
225
226    /**
227     * Resource Plugins stack entry point. All resources are going through all
228     * the plugins.
229     *
230     * @param resources The set of resources to visit
231     * @return The result of the visit.
232     * @throws IOException
233     */
234    public ResourcePool visitResources(ResourcePoolManager resources)
235            throws Exception {
236        Objects.requireNonNull(resources);
237        if (resources.isEmpty()) {
238            return new ResourcePoolManager(resources.byteOrder(),
239                    resources.getStringTable()).resourcePool();
240        }
241        PreVisitStrings previsit = new PreVisitStrings();
242        resourcePrevisitors.stream().forEach((p) -> {
243            p.previsit(resources.resourcePool(), previsit);
244        });
245
246        // Store the strings resulting from the previsit.
247        List<String> sorted = previsit.getSortedStrings();
248        sorted.stream().forEach((s) -> {
249            resources.getStringTable().addString(s);
250        });
251
252        ResourcePool resPool = resources.resourcePool();
253        List<ResourcePoolEntry> frozenOrder = null;
254        for (Plugin p : plugins) {
255            ResourcePoolManager resMgr = null;
256            if (p == lastSorter) {
257                if (frozenOrder != null) {
258                    throw new Exception("Order of resources is already frozen. Plugin "
259                            + p.getName() + " is badly located");
260                }
261                // Create a special Resource pool to compute the indexes.
262                resMgr = new OrderedResourcePoolManager(resPool.byteOrder(),
263                        resources.getStringTable());
264            } else {// If we have an order, inject it
265                if (frozenOrder != null) {
266                    resMgr = new CheckOrderResourcePoolManager(resPool.byteOrder(),
267                            frozenOrder, resources.getStringTable());
268                } else {
269                    resMgr = new ResourcePoolManager(resPool.byteOrder(),
270                            resources.getStringTable());
271                }
272            }
273            try {
274                resPool = p.transform(resPool, resMgr.resourcePoolBuilder());
275            } catch (PluginException pe) {
276                if (JlinkTask.DEBUG) {
277                    System.err.println("Plugin " + p.getName() + " threw exception during transform");
278                    pe.printStackTrace();
279                }
280                throw pe;
281            }
282            if (resPool.isEmpty()) {
283                throw new Exception("Invalid resource pool for plugin " + p);
284            }
285            if (resPool instanceof OrderedResourcePoolManager.OrderedResourcePool) {
286                frozenOrder = ((OrderedResourcePoolManager.OrderedResourcePool)resPool).getOrderedList();
287            }
288        }
289
290        return resPool;
291    }
292
293    /**
294     * This pool wrap the original pool and automatically uncompress ResourcePoolEntry
295     * if needed.
296     */
297    private class LastPoolManager extends ResourcePoolManager {
298        private class LastModule implements ResourcePoolModule {
299
300            final ResourcePoolModule module;
301            // lazily initialized
302            ModuleDescriptor descriptor;
303            ModuleTarget target;
304
305            LastModule(ResourcePoolModule module) {
306                this.module = module;
307            }
308
309            @Override
310            public String name() {
311                return module.name();
312            }
313
314            @Override
315            public Optional<ResourcePoolEntry> findEntry(String path) {
316                Optional<ResourcePoolEntry> d = module.findEntry(path);
317                return d.isPresent()? Optional.of(getUncompressed(d.get())) : Optional.empty();
318            }
319
320            @Override
321            public ModuleDescriptor descriptor() {
322                initModuleAttributes();
323                return descriptor;
324            }
325
326            @Override
327            public String targetPlatform() {
328                initModuleAttributes();
329                return target != null? target.targetPlatform() : null;
330            }
331
332            private void initModuleAttributes() {
333                if (this.descriptor == null) {
334                    Attributes attr = ResourcePoolManager.readModuleAttributes(this);
335                    this.descriptor = attr.descriptor();
336                    this.target = attr.target();
337                }
338            }
339
340            @Override
341            public Set<String> packages() {
342                return module.packages();
343            }
344
345            @Override
346            public String toString() {
347                return name();
348            }
349
350            @Override
351            public Stream<ResourcePoolEntry> entries() {
352                List<ResourcePoolEntry> lst = new ArrayList<>();
353                module.entries().forEach(md -> {
354                    lst.add(getUncompressed(md));
355                });
356                return lst.stream();
357            }
358
359            @Override
360            public int entryCount() {
361                return module.entryCount();
362            }
363        }
364
365        private final ResourcePool pool;
366        Decompressor decompressor = new Decompressor();
367        Collection<ResourcePoolEntry> content;
368
369        LastPoolManager(ResourcePool pool) {
370            this.pool = pool;
371        }
372
373        @Override
374        public void add(ResourcePoolEntry resource) {
375            throw new PluginException("pool is readonly");
376        }
377
378        @Override
379        public Optional<ResourcePoolModule> findModule(String name) {
380            Optional<ResourcePoolModule> module = pool.moduleView().findModule(name);
381            return module.isPresent()? Optional.of(new LastModule(module.get())) : Optional.empty();
382        }
383
384        /**
385         * The collection of modules contained in this pool.
386         *
387         * @return The collection of modules.
388         */
389        @Override
390        public Stream<ResourcePoolModule> modules() {
391            List<ResourcePoolModule> modules = new ArrayList<>();
392            pool.moduleView().modules().forEach(m -> {
393                modules.add(new LastModule(m));
394            });
395            return modules.stream();
396        }
397
398        @Override
399        public int moduleCount() {
400            return pool.moduleView().moduleCount();
401        }
402
403        /**
404         * Get all resources contained in this pool instance.
405         *
406         * @return The stream of resources;
407         */
408        @Override
409        public Stream<ResourcePoolEntry> entries() {
410            if (content == null) {
411                content = new ArrayList<>();
412                pool.entries().forEach(md -> {
413                    content.add(getUncompressed(md));
414                });
415            }
416            return content.stream();
417        }
418
419        @Override
420        public int entryCount() {
421            return pool.entryCount();
422        }
423
424        /**
425         * Get the resource for the passed path.
426         *
427         * @param path A resource path
428         * @return A Resource instance if the resource is found
429         */
430        @Override
431        public Optional<ResourcePoolEntry> findEntry(String path) {
432            Objects.requireNonNull(path);
433            Optional<ResourcePoolEntry> res = pool.findEntry(path);
434            return res.isPresent()? Optional.of(getUncompressed(res.get())) : Optional.empty();
435        }
436
437        @Override
438        public Optional<ResourcePoolEntry> findEntryInContext(String path, ResourcePoolEntry context) {
439            Objects.requireNonNull(path);
440            Objects.requireNonNull(context);
441            Optional<ResourcePoolEntry> res = pool.findEntryInContext(path, context);
442            return res.map(this::getUncompressed);
443        }
444
445        @Override
446        public boolean contains(ResourcePoolEntry res) {
447            return pool.contains(res);
448        }
449
450        @Override
451        public boolean isEmpty() {
452            return pool.isEmpty();
453        }
454
455        @Override
456        public ByteOrder byteOrder() {
457            return pool.byteOrder();
458        }
459
460        private ResourcePoolEntry getUncompressed(ResourcePoolEntry res) {
461            if (res != null) {
462                if (res instanceof ResourcePoolManager.CompressedModuleData) {
463                    try {
464                        byte[] bytes = decompressor.decompressResource(byteOrder(),
465                                (int offset) -> ((ResourcePoolImpl)pool).getStringTable().getString(offset),
466                                res.contentBytes());
467                        res = res.copyWithContent(bytes);
468                    } catch (IOException ex) {
469                        if (JlinkTask.DEBUG) {
470                            System.err.println("IOException while reading resource: " + res.path());
471                            ex.printStackTrace();
472                        }
473                        throw new PluginException(ex);
474                    }
475                }
476            }
477            return res;
478        }
479    }
480
481    /**
482     * Make the imageBuilder to store files.
483     *
484     * @param original
485     * @param transformed
486     * @param writer
487     * @throws java.lang.Exception
488     */
489    public void storeFiles(ResourcePool original, ResourcePool transformed,
490            BasicImageWriter writer)
491            throws Exception {
492        Objects.requireNonNull(original);
493        Objects.requireNonNull(transformed);
494        ResourcePool lastPool = new LastPoolManager(transformed).resourcePool();
495        if (validate) {
496            ResourcePoolConfiguration.validate(lastPool);
497        }
498        imageBuilder.storeFiles(lastPool);
499    }
500
501    public ExecutableImage getExecutableImage() throws IOException {
502        return imageBuilder.getExecutableImage();
503    }
504}
505