src/jdk.jlink/share/classes/jdk/tools/jlink/internal/ImageResourcesTree.java
author mr
Tue, 29 Oct 2019 08:26:55 -0700
changeset 58842 6c255334120d
parent 47216 71c04702a3d5
permissions -rw-r--r--
8232080: jlink plugins for vendor information and run-time options Reviewed-by: ihse, alanb, kvn, bobv, mchung

/*
 * Copyright (c) 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.tools.jlink.internal;

import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

/**
 * A class to build a sorted tree of Resource paths as a tree of ImageLocation.
 *
 */
// XXX Public only due to the JImageTask / JImageTask code duplication
public final class ImageResourcesTree {
    public static boolean isTreeInfoResource(String path) {
        return path.startsWith("/packages") || path.startsWith("/modules");
    }

    /**
     * Path item tree node.
     */
    private static class Node {

        private final String name;
        private final Map<String, Node> children = new TreeMap<>();
        private final Node parent;
        private ImageLocationWriter loc;

        private Node(String name, Node parent) {
            this.name = name;
            this.parent = parent;

            if (parent != null) {
                parent.children.put(name, this);
            }
        }

        public String getPath() {
            if (parent == null) {
                return "/";
            }
            return buildPath(this);
        }

        public String getName() {
            return name;
        }

        public Node getChildren(String name) {
            Node item = children.get(name);
            return item;
        }

        private static String buildPath(Node item) {
            if (item == null) {
                return null;
            }
            String path = buildPath(item.parent);
            if (path == null) {
                return item.getName();
            } else {
                return path + "/" + item.getName();
            }
        }
    }

    private static final class ResourceNode extends Node {

        public ResourceNode(String name, Node parent) {
            super(name, parent);
        }
    }

    private static class PackageNode extends Node {
        /**
         * A reference to a package. Empty packages can be located inside one or
         * more modules. A package with classes exist in only one module.
         */
        final static class PackageReference {

            private final String name;
            private final boolean isEmpty;

            PackageReference(String name, boolean isEmpty) {
                this.name = Objects.requireNonNull(name);
                this.isEmpty = isEmpty;
            }

            @Override
            public String toString() {
                return name + "[empty:" + isEmpty + "]";
            }
        }

        private final Map<String, PackageReference> references = new TreeMap<>();

        PackageNode(String name, Node parent) {
            super(name, parent);
        }

        private void addReference(String name, boolean isEmpty) {
            PackageReference ref = references.get(name);
            if (ref == null || ref.isEmpty) {
                references.put(name, new PackageReference(name, isEmpty));
            }
        }

        private void validate() {
            boolean exists = false;
            for (PackageReference ref : references.values()) {
                if (!ref.isEmpty) {
                    if (exists) {
                        throw new RuntimeException("Multiple modules to contain package "
                                + getName());
                    } else {
                        exists = true;
                    }
                }
            }
        }
    }

    /**
     * Tree of nodes.
     */
    private static final class Tree {

        private final Map<String, Node> directAccess = new HashMap<>();
        private final List<String> paths;
        private final Node root;
        private Node modules;
        private Node packages;

        private Tree(List<String> paths) {
            this.paths = paths;
            root = new Node("", null);
            buildTree();
        }

        private void buildTree() {
            modules = new Node("modules", root);
            directAccess.put(modules.getPath(), modules);

            Map<String, Set<String>> moduleToPackage = new TreeMap<>();
            Map<String, Set<String>> packageToModule = new TreeMap<>();

            for (String p : paths) {
                if (!p.startsWith("/")) {
                    continue;
                }
                String[] split = p.split("/");
                // minimum length is 3 items: /<mod>/<pkg>
                if (split.length < 3) {
                    System.err.println("Resources tree, invalid data structure, "
                            + "skipping " + p);
                    continue;
                }
                Node current = modules;
                String module = null;
                for (int i = 0; i < split.length; i++) {
                    // When a non terminal node is marked as being a resource, something is wrong.
                    // It has been observed some badly created jar file to contain
                    // invalid directory entry marled as not directory (see 8131762)
                    if (current instanceof ResourceNode) {
                        System.err.println("Resources tree, invalid data structure, "
                                + "skipping " + p);
                        continue;
                    }
                    String s = split[i];
                    if (!s.isEmpty()) {
                        // First item, this is the module, simply add a new node to the
                        // tree.
                        if (module == null) {
                            module = s;
                        }
                        Node n = current.children.get(s);
                        if (n == null) {
                            if (i == split.length - 1) { // Leaf
                                n = new ResourceNode(s, current);
                                String pkg = toPackageName(n.parent);
                                //System.err.println("Adding a resource node. pkg " + pkg + ", name " + s);
                                if (pkg != null && !pkg.startsWith("META-INF")) {
                                    Set<String> pkgs = moduleToPackage.get(module);
                                    if (pkgs == null) {
                                        pkgs = new TreeSet<>();
                                        moduleToPackage.put(module, pkgs);
                                    }
                                    pkgs.add(pkg);
                                }
                            } else { // put only sub trees, no leaf
                                n = new Node(s, current);
                                directAccess.put(n.getPath(), n);
                                String pkg = toPackageName(n);
                                if (pkg != null && !pkg.startsWith("META-INF")) {
                                    Set<String> mods = packageToModule.get(pkg);
                                    if (mods == null) {
                                        mods = new TreeSet<>();
                                        packageToModule.put(pkg, mods);
                                    }
                                    mods.add(module);
                                }
                            }
                        }
                        current = n;
                    }
                }
            }
            packages = new Node("packages", root);
            directAccess.put(packages.getPath(), packages);
            // The subset of package nodes that have some content.
            // These packages exist only in a single module.
            for (Map.Entry<String, Set<String>> entry : moduleToPackage.entrySet()) {
                for (String pkg : entry.getValue()) {
                    PackageNode pkgNode = new PackageNode(pkg, packages);
                    pkgNode.addReference(entry.getKey(), false);
                    directAccess.put(pkgNode.getPath(), pkgNode);
                }
            }

            // All packages
            for (Map.Entry<String, Set<String>> entry : packageToModule.entrySet()) {
                // Do we already have a package node?
                PackageNode pkgNode = (PackageNode) packages.getChildren(entry.getKey());
                if (pkgNode == null) {
                    pkgNode = new PackageNode(entry.getKey(), packages);
                }
                for (String module : entry.getValue()) {
                    pkgNode.addReference(module, true);
                }
                directAccess.put(pkgNode.getPath(), pkgNode);
            }
            // Validate that the packages are well formed.
            for (Node n : packages.children.values()) {
                ((PackageNode)n).validate();
            }

        }

        public String toResourceName(Node node) {
            if (!node.children.isEmpty()) {
                throw new RuntimeException("Node is not a resource");
            }
            return removeRadical(node);
        }

        public String getModule(Node node) {
            if (node.parent == null || node.getName().equals("modules")
                    || node.getName().startsWith("packages")) {
                return null;
            }
            String path = removeRadical(node);
            // "/xxx/...";
            path = path.substring(1);
            int i = path.indexOf("/");
            if (i == -1) {
                return path;
            } else {
                return path.substring(0, i);
            }
        }

        public String toPackageName(Node node) {
            if (node.parent == null) {
                return null;
            }
            String path = removeRadical(node.getPath(), "/modules/");
            String module = getModule(node);
            if (path.equals(module)) {
                return null;
            }
            String pkg = removeRadical(path, module + "/");
            return pkg.replaceAll("/", ".");
        }

        public String removeRadical(Node node) {
            return removeRadical(node.getPath(), "/modules");
        }

        private String removeRadical(String path, String str) {
            if (!(path.length() < str.length())) {
                path = path.substring(str.length());
            }
            return path;
        }

        public Node getRoot() {
            return root;
        }

        public Map<String, Node> getMap() {
            return directAccess;
        }
    }

    private static final class LocationsAdder {

        private long offset;
        private final List<byte[]> content = new ArrayList<>();
        private final BasicImageWriter writer;
        private final Tree tree;

        LocationsAdder(Tree tree, long offset, BasicImageWriter writer) {
            this.tree = tree;
            this.offset = offset;
            this.writer = writer;
            addLocations(tree.getRoot());
        }

        private int addLocations(Node current) {
            if (current instanceof PackageNode) {
                PackageNode pkgNode = (PackageNode) current;
                int size = pkgNode.references.size() * 8;
                writer.addLocation(current.getPath(), offset, 0, size);
                offset += size;
            } else {
                int[] ret = new int[current.children.size()];
                int i = 0;
                for (java.util.Map.Entry<String, Node> entry : current.children.entrySet()) {
                    ret[i] = addLocations(entry.getValue());
                    i += 1;
                }
                if (current != tree.getRoot() && !(current instanceof ResourceNode)) {
                    int size = ret.length * 4;
                    writer.addLocation(current.getPath(), offset, 0, size);
                    offset += size;
                }
            }
            return 0;
        }

        private List<byte[]> computeContent() {
            // Map used to associate Tree item with locations offset.
            Map<String, ImageLocationWriter> outLocations = new HashMap<>();
            for (ImageLocationWriter wr : writer.getLocations()) {
                outLocations.put(wr.getFullName(), wr);
            }
            // Attach location to node
            for (Map.Entry<String, ImageLocationWriter> entry : outLocations.entrySet()) {
                Node item = tree.getMap().get(entry.getKey());
                if (item != null) {
                    item.loc = entry.getValue();
                }
            }
            computeContent(tree.getRoot(), outLocations);
            return content;
        }

        private int computeContent(Node current, Map<String, ImageLocationWriter> outLocations) {
            if (current instanceof PackageNode) {
                // /packages/<pkg name>
                PackageNode pkgNode = (PackageNode) current;
                int size = pkgNode.references.size() * 8;
                ByteBuffer buff = ByteBuffer.allocate(size);
                buff.order(writer.getByteOrder());
                for (PackageNode.PackageReference mod : pkgNode.references.values()) {
                    buff.putInt(mod.isEmpty ? 1 : 0);
                    buff.putInt(writer.addString(mod.name));
                }
                byte[] arr = buff.array();
                content.add(arr);
                current.loc = outLocations.get(current.getPath());
            } else {
                int[] ret = new int[current.children.size()];
                int i = 0;
                for (java.util.Map.Entry<String, Node> entry : current.children.entrySet()) {
                    ret[i] = computeContent(entry.getValue(), outLocations);
                    i += 1;
                }
                if (ret.length > 0) {
                    int size = ret.length * 4;
                    ByteBuffer buff = ByteBuffer.allocate(size);
                    buff.order(writer.getByteOrder());
                    for (int val : ret) {
                        buff.putInt(val);
                    }
                    byte[] arr = buff.array();
                    content.add(arr);
                } else {
                    if (current instanceof ResourceNode) {
                        // A resource location, remove "/modules"
                        String s = tree.toResourceName(current);
                        current.loc = outLocations.get(s);
                    } else {
                        // empty "/packages" or empty "/modules" paths
                        current.loc = outLocations.get(current.getPath());
                    }
                }
                if (current.loc == null && current != tree.getRoot()) {
                    System.err.println("Invalid path in metadata, skipping " + current.getPath());
                }
            }
            return current.loc == null ? 0 : current.loc.getLocationOffset();
        }
    }

    private final List<String> paths;
    private final LocationsAdder adder;

    public ImageResourcesTree(long offset, BasicImageWriter writer, List<String> paths) {
        this.paths = new ArrayList<>();
        this.paths.addAll(paths);
        Collections.sort(this.paths);
        Tree tree = new Tree(this.paths);
        adder = new LocationsAdder(tree, offset, writer);
    }

    public void addContent(DataOutputStream out) throws IOException {
        List<byte[]> content = adder.computeContent();
        for (byte[] c : content) {
            out.write(c, 0, c.length);
        }
    }
}