1 /*
   2  * Copyright (c) 2019, 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.incubator.jpackage.internal;
  26 
  27 import java.io.File;
  28 import java.io.IOException;
  29 import java.nio.file.Files;
  30 import java.nio.file.Path;
  31 import java.util.ArrayList;
  32 import java.util.Collection;
  33 import java.util.Collections;
  34 import java.util.HashMap;
  35 import java.util.List;
  36 import java.util.Map;
  37 import java.util.function.BiFunction;
  38 import java.util.stream.Collectors;
  39 import java.util.stream.Stream;
  40 
  41 
  42 /**
  43  * Group of paths.
  44  * Each path in the group is assigned a unique id.
  45  */
  46 final class PathGroup {
  47     PathGroup(Map<Object, Path> paths) {
  48         entries = new HashMap<>(paths);
  49     }
  50 
  51     Path getPath(Object id) {
  52         if (id == null) {
  53             throw new NullPointerException();
  54         }
  55         return entries.get(id);
  56     }
  57 
  58     void setPath(Object id, Path path) {
  59         if (path != null) {
  60             entries.put(id, path);
  61         } else {
  62             entries.remove(id);
  63         }
  64     }
  65 
  66     /**
  67      * All configured entries.
  68      */
  69     List<Path> paths() {
  70         return entries.values().stream().collect(Collectors.toList());
  71     }
  72 
  73     /**
  74      * Root entries.
  75      */
  76     List<Path> roots() {
  77         // Sort by the number of path components in ascending order.
  78         List<Map.Entry<Path, Path>> sorted = normalizedPaths().stream().sorted(
  79                 (a, b) -> a.getKey().getNameCount() - b.getKey().getNameCount()).collect(
  80                         Collectors.toList());
  81 
  82         // Returns `true` if `a` is a parent of `b`
  83         BiFunction<Map.Entry<Path, Path>, Map.Entry<Path, Path>, Boolean> isParentOrSelf = (a, b) -> {
  84             return a == b || b.getKey().startsWith(a.getKey());
  85         };
  86 
  87         return sorted.stream().filter(
  88                 v -> v == sorted.stream().sequential().filter(
  89                         v2 -> isParentOrSelf.apply(v2, v)).findFirst().get()).map(
  90                         v -> v.getValue()).collect(Collectors.toList());
  91     }
  92 
  93     long sizeInBytes() throws IOException {
  94         long reply = 0;
  95         for (Path dir : roots().stream().filter(f -> Files.isDirectory(f)).collect(
  96                 Collectors.toList())) {
  97             try (Stream<Path> stream = Files.walk(dir)) {
  98                 reply += stream.filter(p -> Files.isRegularFile(p)).mapToLong(
  99                         f -> f.toFile().length()).sum();
 100             }
 101         }
 102         return reply;
 103     }
 104 
 105     PathGroup resolveAt(Path root) {
 106         return new PathGroup(entries.entrySet().stream().collect(
 107                 Collectors.toMap(e -> e.getKey(),
 108                         e -> root.resolve(e.getValue()))));
 109     }
 110 
 111     void copy(PathGroup dst) throws IOException {
 112         copy(this, dst, null, false);
 113     }
 114 
 115     void move(PathGroup dst) throws IOException {
 116         copy(this, dst, null, true);
 117     }
 118 
 119     void transform(PathGroup dst, TransformHandler handler) throws IOException {
 120         copy(this, dst, handler, false);
 121     }
 122 
 123     static interface Facade<T> {
 124         PathGroup pathGroup();
 125 
 126         default Collection<Path> paths() {
 127             return pathGroup().paths();
 128         }
 129 
 130         default List<Path> roots() {
 131             return pathGroup().roots();
 132         }
 133 
 134         default long sizeInBytes() throws IOException {
 135             return pathGroup().sizeInBytes();
 136         }
 137 
 138         T resolveAt(Path root);
 139 
 140         default void copy(Facade<T> dst) throws IOException {
 141             pathGroup().copy(dst.pathGroup());
 142         }
 143 
 144         default void move(Facade<T> dst) throws IOException {
 145             pathGroup().move(dst.pathGroup());
 146         }
 147 
 148         default void transform(Facade<T> dst, TransformHandler handler) throws
 149                 IOException {
 150             pathGroup().transform(dst.pathGroup(), handler);
 151         }
 152     }
 153 
 154     static interface TransformHandler {
 155         public void copyFile(Path src, Path dst) throws IOException;
 156         public void createDirectory(Path dir) throws IOException;
 157     }
 158 
 159     private static void copy(PathGroup src, PathGroup dst,
 160             TransformHandler handler, boolean move) throws IOException {
 161         List<Map.Entry<Path, Path>> copyItems = new ArrayList<>();
 162         List<Path> excludeItems = new ArrayList<>();
 163 
 164         for (var id: src.entries.keySet()) {
 165             Path srcPath = src.entries.get(id);
 166             if (dst.entries.containsKey(id)) {
 167                 copyItems.add(Map.entry(srcPath, dst.entries.get(id)));
 168             } else {
 169                 excludeItems.add(srcPath);
 170             }
 171         }
 172 
 173         copy(move, copyItems, excludeItems, handler);
 174     }
 175 
 176     private static void copy(boolean move, List<Map.Entry<Path, Path>> entries,
 177             List<Path> excludePaths, TransformHandler handler) throws
 178             IOException {
 179 
 180         if (handler == null) {
 181             handler = new TransformHandler() {
 182                 @Override
 183                 public void copyFile(Path src, Path dst) throws IOException {
 184                     Files.createDirectories(dst.getParent());
 185                     if (move) {
 186                         Files.move(src, dst);
 187                     } else {
 188                         Files.copy(src, dst);
 189                     }
 190                 }
 191 
 192                 @Override
 193                 public void createDirectory(Path dir) throws IOException {
 194                     Files.createDirectories(dir);
 195                 }
 196             };
 197         }
 198 
 199         // destination -> source file mapping
 200         Map<Path, Path> actions = new HashMap<>();
 201         for (var action: entries) {
 202             Path src = action.getKey();
 203             Path dst = action.getValue();
 204             if (src.toFile().isDirectory()) {
 205                try (Stream<Path> stream = Files.walk(src)) {
 206                    stream.sequential().forEach(path -> actions.put(dst.resolve(
 207                             src.relativize(path)).normalize(), path));
 208                }
 209             } else {
 210                 actions.put(dst.normalize(), src);
 211             }
 212         }
 213 
 214         for (var action : actions.entrySet()) {
 215             Path dst = action.getKey();
 216             Path src = action.getValue();
 217 
 218             if (excludePaths.stream().anyMatch(src::startsWith)) {
 219                 continue;
 220             }
 221 
 222             if (src.equals(dst) || !src.toFile().exists()) {
 223                 continue;
 224             }
 225 
 226             if (src.toFile().isDirectory()) {
 227                 handler.createDirectory(dst);
 228             } else {
 229                 handler.copyFile(src, dst);
 230             }
 231         }
 232 
 233         if (move) {
 234             // Delete source dirs.
 235             for (var entry: entries) {
 236                 File srcFile = entry.getKey().toFile();
 237                 if (srcFile.isDirectory()) {
 238                     IOUtils.deleteRecursive(srcFile);
 239                 }
 240             }
 241         }
 242     }
 243 
 244     private static Map.Entry<Path, Path> normalizedPath(Path v) {
 245         final Path normalized;
 246         if (!v.isAbsolute()) {
 247             normalized = Path.of("./").resolve(v.normalize());
 248         } else {
 249             normalized = v.normalize();
 250         }
 251 
 252         return Map.entry(normalized, v);
 253     }
 254 
 255     private List<Map.Entry<Path, Path>> normalizedPaths() {
 256         return entries.values().stream().map(PathGroup::normalizedPath).collect(
 257                 Collectors.toList());
 258     }
 259 
 260     private final Map<Object, Path> entries;
 261 }