1 /*
   2  * Copyright (c) 2012, 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 
  26 package jdk.incubator.jpackage.internal;
  27 
  28 import java.io.*;
  29 import java.lang.reflect.InvocationHandler;
  30 import java.lang.reflect.Method;
  31 import java.lang.reflect.Proxy;
  32 import java.nio.channels.FileChannel;
  33 import java.nio.file.FileVisitResult;
  34 import java.nio.file.Files;
  35 import java.nio.file.Path;
  36 import java.nio.file.SimpleFileVisitor;
  37 import java.nio.file.attribute.BasicFileAttributes;
  38 import java.util.*;
  39 import javax.xml.stream.XMLOutputFactory;
  40 import javax.xml.stream.XMLStreamException;
  41 import javax.xml.stream.XMLStreamWriter;
  42 
  43 /**
  44  * IOUtils
  45  *
  46  * A collection of static utility methods.
  47  */
  48 public class IOUtils {
  49 
  50     public static void deleteRecursive(File path) throws IOException {
  51         if (!path.exists()) {
  52             return;
  53         }
  54         Path directory = path.toPath();
  55         Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
  56             @Override
  57             public FileVisitResult visitFile(Path file,
  58                             BasicFileAttributes attr) throws IOException {
  59                 if (Platform.getPlatform() == Platform.WINDOWS) {
  60                     Files.setAttribute(file, "dos:readonly", false);
  61                 }
  62                 Files.delete(file);
  63                 return FileVisitResult.CONTINUE;
  64             }
  65 
  66             @Override
  67             public FileVisitResult preVisitDirectory(Path dir,
  68                             BasicFileAttributes attr) throws IOException {
  69                 if (Platform.getPlatform() == Platform.WINDOWS) {
  70                     Files.setAttribute(dir, "dos:readonly", false);
  71                 }
  72                 return FileVisitResult.CONTINUE;
  73             }
  74 
  75             @Override
  76             public FileVisitResult postVisitDirectory(Path dir, IOException e)
  77                             throws IOException {
  78                 Files.delete(dir);
  79                 return FileVisitResult.CONTINUE;
  80             }
  81         });
  82     }
  83 
  84     public static void copyRecursive(Path src, Path dest) throws IOException {
  85         copyRecursive(src, dest, List.of());
  86     }
  87 
  88     public static void copyRecursive(Path src, Path dest,
  89             final List<String> excludes) throws IOException {
  90         Files.walkFileTree(src, new SimpleFileVisitor<Path>() {
  91             @Override
  92             public FileVisitResult preVisitDirectory(final Path dir,
  93                     final BasicFileAttributes attrs) throws IOException {
  94                 if (excludes.contains(dir.toFile().getName())) {
  95                     return FileVisitResult.SKIP_SUBTREE;
  96                 } else {
  97                     Files.createDirectories(dest.resolve(src.relativize(dir)));
  98                     return FileVisitResult.CONTINUE;
  99                 }
 100             }
 101 
 102             @Override
 103             public FileVisitResult visitFile(final Path file,
 104                     final BasicFileAttributes attrs) throws IOException {
 105                 if (!excludes.contains(file.toFile().getName())) {
 106                     Files.copy(file, dest.resolve(src.relativize(file)));
 107                 }
 108                 return FileVisitResult.CONTINUE;
 109             }
 110         });
 111     }
 112 
 113     public static void copyFile(File sourceFile, File destFile)
 114             throws IOException {
 115         destFile.getParentFile().mkdirs();
 116 
 117         //recreate the file as existing copy may have weird permissions
 118         destFile.delete();
 119         destFile.createNewFile();
 120 
 121         try (FileChannel source = new FileInputStream(sourceFile).getChannel();
 122             FileChannel destination =
 123                     new FileOutputStream(destFile).getChannel()) {
 124 
 125             if (destination != null && source != null) {
 126                 destination.transferFrom(source, 0, source.size());
 127             }
 128         }
 129 
 130         //preserve executable bit!
 131         if (sourceFile.canExecute()) {
 132             destFile.setExecutable(true, false);
 133         }
 134         if (!sourceFile.canWrite()) {
 135             destFile.setReadOnly();
 136         }
 137         destFile.setReadable(true, false);
 138     }
 139 
 140     // run "launcher paramfile" in the directory where paramfile is kept
 141     public static void run(String launcher, File paramFile)
 142             throws IOException {
 143         if (paramFile != null && paramFile.exists()) {
 144             ProcessBuilder pb =
 145                     new ProcessBuilder(launcher, paramFile.getName());
 146             pb = pb.directory(paramFile.getParentFile());
 147             exec(pb);
 148         }
 149     }
 150 
 151     public static void exec(ProcessBuilder pb)
 152             throws IOException {
 153         exec(pb, false, null);
 154     }
 155 
 156     static void exec(ProcessBuilder pb, boolean testForPresenseOnly,
 157             PrintStream consumer) throws IOException {
 158         List<String> output = new ArrayList<>();
 159         Executor exec = Executor.of(pb).setOutputConsumer(lines -> {
 160             lines.forEach(output::add);
 161             if (consumer != null) {
 162                 output.forEach(consumer::println);
 163             }
 164         });
 165 
 166         if (testForPresenseOnly) {
 167             exec.execute();
 168         } else {
 169             exec.executeExpectSuccess();
 170         }
 171     }
 172 
 173     public static int getProcessOutput(List<String> result, String... args)
 174             throws IOException, InterruptedException {
 175 
 176         ProcessBuilder pb = new ProcessBuilder(args);
 177 
 178         final Process p = pb.start();
 179 
 180         List<String> list = new ArrayList<>();
 181 
 182         final BufferedReader in =
 183                 new BufferedReader(new InputStreamReader(p.getInputStream()));
 184         final BufferedReader err =
 185                 new BufferedReader(new InputStreamReader(p.getErrorStream()));
 186 
 187         Thread t = new Thread(() -> {
 188             try {
 189                 String line;
 190                 while ((line = in.readLine()) != null) {
 191                     list.add(line);
 192                 }
 193             } catch (IOException ioe) {
 194                 Log.verbose(ioe);
 195             }
 196 
 197             try {
 198                 String line;
 199                 while ((line = err.readLine()) != null) {
 200                     Log.error(line);
 201                 }
 202             } catch (IOException ioe) {
 203                   Log.verbose(ioe);
 204             }
 205         });
 206         t.setDaemon(true);
 207         t.start();
 208 
 209         int ret = p.waitFor();
 210 
 211         result.clear();
 212         result.addAll(list);
 213 
 214         return ret;
 215     }
 216 
 217     static void writableOutputDir(Path outdir) throws PackagerException {
 218         File file = outdir.toFile();
 219 
 220         if (!file.isDirectory() && !file.mkdirs()) {
 221             throw new PackagerException("error.cannot-create-output-dir",
 222                     file.getAbsolutePath());
 223         }
 224         if (!file.canWrite()) {
 225             throw new PackagerException("error.cannot-write-to-output-dir",
 226                     file.getAbsolutePath());
 227         }
 228     }
 229 
 230     public static Path replaceSuffix(Path path, String suffix) {
 231         Path parent = path.getParent();
 232         String filename = path.getFileName().toString().replaceAll("\\.[^.]*$", "")
 233                 + Optional.ofNullable(suffix).orElse("");
 234         return parent != null ? parent.resolve(filename) : Path.of(filename);
 235     }
 236 
 237     public static Path addSuffix(Path path, String suffix) {
 238         Path parent = path.getParent();
 239         String filename = path.getFileName().toString() + suffix;
 240         return parent != null ? parent.resolve(filename) : Path.of(filename);
 241     }
 242 
 243     public static String getSuffix(Path path) {
 244         String filename = replaceSuffix(path.getFileName(), null).toString();
 245         return path.getFileName().toString().substring(filename.length());
 246     }
 247 
 248     @FunctionalInterface
 249     public static interface XmlConsumer {
 250         void accept(XMLStreamWriter xml) throws IOException, XMLStreamException;
 251     }
 252 
 253     public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws
 254             IOException {
 255         XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
 256         try (Writer w = Files.newBufferedWriter(dstFile)) {
 257             // Wrap with pretty print proxy
 258             XMLStreamWriter xml = (XMLStreamWriter) Proxy.newProxyInstance(
 259                     XMLStreamWriter.class.getClassLoader(), new Class<?>[]{
 260                 XMLStreamWriter.class}, new PrettyPrintHandler(
 261                     xmlFactory.createXMLStreamWriter(w)));
 262 
 263             xml.writeStartDocument();
 264             xmlConsumer.accept(xml);
 265             xml.writeEndDocument();
 266             xml.flush();
 267             xml.close();
 268         } catch (XMLStreamException ex) {
 269             throw new IOException(ex);
 270         } catch (IOException ex) {
 271             throw ex;
 272         }
 273     }
 274 
 275     private static class PrettyPrintHandler implements InvocationHandler {
 276 
 277         PrettyPrintHandler(XMLStreamWriter target) {
 278             this.target = target;
 279         }
 280 
 281         @Override
 282         public Object invoke(Object proxy, Method method, Object[] args) throws
 283                 Throwable {
 284             switch (method.getName()) {
 285                 case "writeStartElement":
 286                     // update state of parent node
 287                     if (depth > 0) {
 288                         hasChildElement.put(depth - 1, true);
 289                     }
 290                     // reset state of current node
 291                     hasChildElement.put(depth, false);
 292                     // indent for current depth
 293                     target.writeCharacters(EOL);
 294                     target.writeCharacters(repeat(depth, INDENT));
 295                     depth++;
 296                     break;
 297                 case "writeEndElement":
 298                     depth--;
 299                     if (hasChildElement.get(depth) == true) {
 300                         target.writeCharacters(EOL);
 301                         target.writeCharacters(repeat(depth, INDENT));
 302                     }
 303                     break;
 304                 case "writeProcessingInstruction":
 305                 case "writeEmptyElement":
 306                     // update state of parent node
 307                     if (depth > 0) {
 308                         hasChildElement.put(depth - 1, true);
 309                     }
 310                     // indent for current depth
 311                     target.writeCharacters(EOL);
 312                     target.writeCharacters(repeat(depth, INDENT));
 313                     break;
 314                 default:
 315                     break;
 316             }
 317             method.invoke(target, args);
 318             return null;
 319         }
 320 
 321         private static String repeat(int d, String s) {
 322             StringBuilder sb = new StringBuilder();
 323             while (d-- > 0) {
 324                 sb.append(s);
 325             }
 326             return sb.toString();
 327         }
 328 
 329         private final XMLStreamWriter target;
 330         private int depth = 0;
 331         private final Map<Integer, Boolean> hasChildElement = new HashMap<>();
 332         private static final String INDENT = "  ";
 333         private static final String EOL = "\n";
 334     }
 335 }