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             destination.transferFrom(source, 0, source.size());
 125         }
 126 
 127         //preserve executable bit!
 128         if (sourceFile.canExecute()) {
 129             destFile.setExecutable(true, false);
 130         }
 131         if (!sourceFile.canWrite()) {
 132             destFile.setReadOnly();
 133         }
 134         destFile.setReadable(true, false);
 135     }
 136 
 137     // run "launcher paramfile" in the directory where paramfile is kept
 138     public static void run(String launcher, File paramFile)
 139             throws IOException {
 140         if (paramFile != null && paramFile.exists()) {
 141             ProcessBuilder pb =
 142                     new ProcessBuilder(launcher, paramFile.getName());
 143             pb = pb.directory(paramFile.getParentFile());
 144             exec(pb);
 145         }
 146     }
 147 
 148     public static void exec(ProcessBuilder pb)
 149             throws IOException {
 150         exec(pb, false, null);
 151     }
 152 
 153     static void exec(ProcessBuilder pb, boolean testForPresenseOnly,
 154             PrintStream consumer) throws IOException {
 155         List<String> output = new ArrayList<>();
 156         Executor exec = Executor.of(pb).setOutputConsumer(lines -> {
 157             lines.forEach(output::add);
 158             if (consumer != null) {
 159                 output.forEach(consumer::println);
 160             }
 161         });
 162 
 163         if (testForPresenseOnly) {
 164             exec.execute();
 165         } else {
 166             exec.executeExpectSuccess();
 167         }
 168     }
 169 
 170     public static int getProcessOutput(List<String> result, String... args)
 171             throws IOException, InterruptedException {
 172 
 173         ProcessBuilder pb = new ProcessBuilder(args);
 174 
 175         final Process p = pb.start();
 176 
 177         List<String> list = new ArrayList<>();
 178 
 179         final BufferedReader in =
 180                 new BufferedReader(new InputStreamReader(p.getInputStream()));
 181         final BufferedReader err =
 182                 new BufferedReader(new InputStreamReader(p.getErrorStream()));
 183 
 184         Thread t = new Thread(() -> {
 185             try {
 186                 String line;
 187                 while ((line = in.readLine()) != null) {
 188                     list.add(line);
 189                 }
 190             } catch (IOException ioe) {
 191                 Log.verbose(ioe);
 192             }
 193 
 194             try {
 195                 String line;
 196                 while ((line = err.readLine()) != null) {
 197                     Log.error(line);
 198                 }
 199             } catch (IOException ioe) {
 200                   Log.verbose(ioe);
 201             }
 202         });
 203         t.setDaemon(true);
 204         t.start();
 205 
 206         int ret = p.waitFor();
 207 
 208         result.clear();
 209         result.addAll(list);
 210 
 211         return ret;
 212     }
 213 
 214     static void writableOutputDir(Path outdir) throws PackagerException {
 215         File file = outdir.toFile();
 216 
 217         if (!file.isDirectory() && !file.mkdirs()) {
 218             throw new PackagerException("error.cannot-create-output-dir",
 219                     file.getAbsolutePath());
 220         }
 221         if (!file.canWrite()) {
 222             throw new PackagerException("error.cannot-write-to-output-dir",
 223                     file.getAbsolutePath());
 224         }
 225     }
 226 
 227     public static Path replaceSuffix(Path path, String suffix) {
 228         Path parent = path.getParent();
 229         String filename = path.getFileName().toString().replaceAll("\\.[^.]*$", "")
 230                 + Optional.ofNullable(suffix).orElse("");
 231         return parent != null ? parent.resolve(filename) : Path.of(filename);
 232     }
 233 
 234     public static Path addSuffix(Path path, String suffix) {
 235         Path parent = path.getParent();
 236         String filename = path.getFileName().toString() + suffix;
 237         return parent != null ? parent.resolve(filename) : Path.of(filename);
 238     }
 239 
 240     public static String getSuffix(Path path) {
 241         String filename = replaceSuffix(path.getFileName(), null).toString();
 242         return path.getFileName().toString().substring(filename.length());
 243     }
 244 
 245     @FunctionalInterface
 246     public static interface XmlConsumer {
 247         void accept(XMLStreamWriter xml) throws IOException, XMLStreamException;
 248     }
 249 
 250     public static void createXml(Path dstFile, XmlConsumer xmlConsumer) throws
 251             IOException {
 252         XMLOutputFactory xmlFactory = XMLOutputFactory.newInstance();
 253         try (Writer w = Files.newBufferedWriter(dstFile)) {
 254             // Wrap with pretty print proxy
 255             XMLStreamWriter xml = (XMLStreamWriter) Proxy.newProxyInstance(
 256                     XMLStreamWriter.class.getClassLoader(), new Class<?>[]{
 257                 XMLStreamWriter.class}, new PrettyPrintHandler(
 258                     xmlFactory.createXMLStreamWriter(w)));
 259 
 260             xml.writeStartDocument();
 261             xmlConsumer.accept(xml);
 262             xml.writeEndDocument();
 263             xml.flush();
 264             xml.close();
 265         } catch (XMLStreamException ex) {
 266             throw new IOException(ex);
 267         } catch (IOException ex) {
 268             throw ex;
 269         }
 270     }
 271 
 272     private static class PrettyPrintHandler implements InvocationHandler {
 273 
 274         PrettyPrintHandler(XMLStreamWriter target) {
 275             this.target = target;
 276         }
 277 
 278         @Override
 279         public Object invoke(Object proxy, Method method, Object[] args) throws
 280                 Throwable {
 281             switch (method.getName()) {
 282                 case "writeStartElement":
 283                     // update state of parent node
 284                     if (depth > 0) {
 285                         hasChildElement.put(depth - 1, true);
 286                     }
 287                     // reset state of current node
 288                     hasChildElement.put(depth, false);
 289                     // indent for current depth
 290                     target.writeCharacters(EOL);
 291                     target.writeCharacters(repeat(depth, INDENT));
 292                     depth++;
 293                     break;
 294                 case "writeEndElement":
 295                     depth--;
 296                     if (hasChildElement.get(depth) == true) {
 297                         target.writeCharacters(EOL);
 298                         target.writeCharacters(repeat(depth, INDENT));
 299                     }
 300                     break;
 301                 case "writeProcessingInstruction":
 302                 case "writeEmptyElement":
 303                     // update state of parent node
 304                     if (depth > 0) {
 305                         hasChildElement.put(depth - 1, true);
 306                     }
 307                     // indent for current depth
 308                     target.writeCharacters(EOL);
 309                     target.writeCharacters(repeat(depth, INDENT));
 310                     break;
 311                 default:
 312                     break;
 313             }
 314             method.invoke(target, args);
 315             return null;
 316         }
 317 
 318         private static String repeat(int d, String s) {
 319             StringBuilder sb = new StringBuilder();
 320             while (d-- > 0) {
 321                 sb.append(s);
 322             }
 323             return sb.toString();
 324         }
 325 
 326         private final XMLStreamWriter target;
 327         private int depth = 0;
 328         private final Map<Integer, Boolean> hasChildElement = new HashMap<>();
 329         private static final String INDENT = "  ";
 330         private static final String EOL = "\n";
 331     }
 332 }