code: /**
   * This file is part of Eclipse Steady.
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   *
   * SPDX-License-Identifier: Apache-2.0
   * SPDX-FileCopyrightText: Copyright (c) 2018-2020 SAP SE or an SAP affiliate company and Eclipse Steady contributors
   */
  package org.eclipse.steady.shared.util;
  
  import java.io.ByteArrayOutputStream;
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.net.URL;
  import java.net.URLClassLoader;
  import java.nio.charset.Charset;
  import java.nio.file.FileVisitResult;
  import java.nio.file.SimpleFileVisitor;
  import java.nio.file.attribute.BasicFileAttributes;
  import java.nio.file.CopyOption;
  import java.nio.charset.IllegalCharsetNameException;
  import java.nio.charset.StandardCharsets;
  import java.nio.charset.UnsupportedCharsetException;
  import java.nio.file.Files;
  import java.nio.file.Path;
  import java.nio.file.Paths;
  import java.security.DigestInputStream;
  import java.security.MessageDigest;
  import java.security.NoSuchAlgorithmException;
  import java.util.HashSet;
  import java.util.Set;
  import java.util.zip.CRC32;
  import java.util.zip.Checksum;
  import java.util.zip.ZipInputStream;
  
  import org.apache.logging.log4j.Logger;
  import org.eclipse.steady.shared.enums.DigestAlgorithm;
  
  /**
   * Helper methods for checking the presence of files and directories, and for dealing with Java class path.
   * See VULAS-204 in case we run into problems related to special characters in paths.
   */
  public class FileUtil {
  
    private static final Logger log = org.apache.logging.log4j.LogManager.getLogger(FileUtil.class);
  
    /**
     * Returns the file extension of the given {@link File} or null if the file does not have an extension.
     *
     * @param _file the file whose extension is to be returned
     * @return the file extension of the given file (converted to lower case)
     * @throws java.lang.IllegalArgumentException if the file argument is null or the file does not exist
     */
    public static String getFileExtension(File _file) throws IllegalArgumentException {
      String ext = null;
      if (_file == null || !_file.exists())
        throw new IllegalArgumentException("File [" + _file + "] not found");
  
      final int ext_index = _file.getAbsolutePath().lastIndexOf(".");
      if (ext_index != -1) ext = _file.getAbsolutePath().substring(ext_index + 1).toLowerCase();
  
      return ext;
    }
  
    /**
     * Checks whether the given {@link Path} points to a file that has one of the given file extensions.
     *
     * @param _p a {@link java.nio.file.Path} object.
     * @param _extensions an array of {@link java.lang.String} objects.
     * @return a boolean.
     */
    public static boolean hasFileExtension(Path _p, String[] _extensions) {
      if (_p.toFile().isFile()) {
        final String ext = FileUtil.getFileExtension(_p.toFile());
        if (ext != null) {
          for (int i = 0; i < _extensions.length; i++) {
            if (ext.equalsIgnoreCase(_extensions[i])) {
              return true;
            }
          }
        }
      }
      return false;
    }
  
    /**
     * <p>toString.</p>
     *
     * @param _paths a {@link java.util.Set} object.
     * @param _sep a {@link java.lang.String} object.
     * @return a {@link java.lang.String} object.
     */
    public static String toString(Set<Path> _paths, String _sep) {
      final StringBuilder b = new StringBuilder();
      int i = 0;
      for (Path p : _paths) {
        if (i++ > 0) b.append(_sep);
        b.append(p.toString());
      }
      return b.toString();
    }
  
    /**
     * Creates a new temporary directory with the given prefix inside the directory returned by {@link VulasConfiguration#getTmpDir()}.
     *
     * @throws java.io.IOException if any.
     * @param _prefix a {@link java.lang.String} object.
     * @return a {@link java.nio.file.Path} object.
     */
    public static Path createTmpDir(String _prefix) throws IOException {
      return Files.createTempDirectory(VulasConfiguration.getGlobal().getTmpDir(), _prefix);
    }
  
    /**
     * <p>isAccessibleFile.</p>
     *
     * @param _arg a {@link java.lang.String} object.
     * @return a boolean.
     */
    public static boolean isAccessibleFile(String _arg) {
      if (_arg == null || _arg.equals("")) return false;
      else return FileUtil.isAccessibleFile(Paths.get(_arg));
    }
  
    /**
     * <p>isAccessibleFile.</p>
     *
     * @param _arg a {@link java.nio.file.Path} object.
     * @return a boolean.
     */
    public static boolean isAccessibleFile(Path _arg) {
      boolean result = false;
      if (_arg != null) {
        final File f = _arg.toFile();
        if (f.isFile() && f.exists() && f.canRead()) result = true;
      }
      return result;
    }
  
    /**
     * <p>isAccessibleDirectory.</p>
     *
     * @param _dir a {@link java.nio.file.Path} object.
     * @return a boolean.
     */
    public static final boolean isAccessibleDirectory(Path _dir) {
      if (_dir == null) return false;
      else {
        return FileUtil.isAccessibleDirectory(_dir.toFile());
      }
    }
  
    /**
     * <p>isAccessibleDirectory.</p>
     *
     * @param _dir a {@link java.io.File} object.
     * @return a boolean.
     */
    public static final boolean isAccessibleDirectory(File _dir) {
      if (_dir == null) return false;
      else {
        return _dir.exists() && _dir.isDirectory() && _dir.canRead();
      }
    }
  
    /**
     * <p>isAccessibleDirectory.</p>
     *
     * @param _dir a {@link java.lang.String} object.
     * @return a boolean.
     */
    public static final boolean isAccessibleDirectory(String _dir) {
      if (_dir == null || _dir.equals("")) return false;
      else return FileUtil.isAccessibleDirectory(Paths.get(_dir));
    }
  
    /**
     * <p>createDirectory.</p>
     *
     * @param _p a {@link java.nio.file.Path} object.
     */
    public static final void createDirectory(Path _p) {
      if (!_p.toFile().exists()) {
        try {
          Files.createDirectories(_p);
        } catch (IOException e) {
          FileUtil.log.error("Error while creating directory [" + _p + "]: " + e.getMessage());
        }
      }
    }
  
    /**
     * Removes path information from the argument as to return the file name.
     *
     * @param _file_path complete file path
     * @return file name
     */
    public static String getFileName(String _file_path) {
      return FileUtil.getFileName(_file_path, true);
    }
  
    /**
     * Removes path information and, if requested, the file extension from the given file name.
     *
     * @param _file_path complete file path
     * @return file name
     * @param _keep_ext a boolean.
     */
    public static String getFileName(String _file_path, boolean _keep_ext) {
      String n = _file_path;
  
      // Remove path
      final int idx = (n == null ? -1 : n.lastIndexOf(File.separator));
      if (idx != -1) n = n.substring(idx + 1);
  
      // Remove file extension
      if (!_keep_ext && n.lastIndexOf(".") != -1) n = n.substring(0, n.lastIndexOf("."));
  
      return n;
    }
  
    /**
     * Returns the name of the {@link Charset} configured via configuration parameter {@link VulasConfiguration#CHARSET}.
     *
     * @see FileUtil#readFile(Path)
     * @see FileUtil#writeToFile(File, byte[])
     * @return a {@link java.lang.String} object.
     */
    public static String getCharsetName() {
      return FileUtil.getCharset().name();
    }
  
    /**
     * Returns the {@link Charset} configured via configuration parameter {@link VulasConfiguration#CHARSET}.
     *
     * @see FileUtil#readFile(Path)
     * @see FileUtil#writeToFile(File, byte[])
     * @return a {@link java.nio.charset.Charset} object.
     */
    public static Charset getCharset() {
      final String cs =
          VulasConfiguration.getGlobal()
              .getConfiguration()
              .getString(VulasConfiguration.CHARSET, "UTF-8");
      try {
        return Charset.forName(cs);
      } catch (IllegalCharsetNameException | UnsupportedCharsetException unused) {
        log.error(
            "Defaulting to UTF-8 since charset with name ["
                + cs
                + "] cannot be created: "
                + unused.getMessage());
        return StandardCharsets.UTF_8;
      }
    }
  
    /**
     * Copies the given source file or directory to the given target directory. If
     * no _target_name is provided, the source will be copied with the identical
     * name into _target_dir. If _target_name is provided, that name will be
     * taken.
     *
     * @throws java.io.IOException if any.
     * @throws java.lang.IllegalArgumentException if _source is neither an
     * existing file nor directory or the target folder does not exist.
     * @param _source a path to the source file
     * @param _target_dir a path to the target file
     * @param _target_name a name of the target file or directory (optional)
     * @param options a {@link java.nio.file.CopyOption} object
     * @return a {@link java.nio.file.Path} object to the target
     */
    public static Path copy(Path _source, Path _target_dir, Path _target_name, CopyOption... options)
        throws IOException, IllegalArgumentException {
  
      // Check args
      if (!FileUtil.isAccessibleFile(_source) && !FileUtil.isAccessibleDirectory(_source)) {
        throw new IllegalArgumentException("Source [" + _source + "] does not exist");
      } else if (!FileUtil.isAccessibleDirectory(_target_dir)) {
        throw new IllegalArgumentException(
            "Target [" + _target_dir + "] does not exist or is not a directory");
      }
  
      Path p = _target_dir.resolve(_target_name == null ? _source.getFileName() : _target_name);
  
      // A single file is copied
      if (FileUtil.isAccessibleFile(_source)) {
        log.info("Copying [" + _source + "] to [" + p + "]");
        Files.copy(_source, p, options);
      }
      // A directory is copied
      else if (FileUtil.isAccessibleDirectory(_source)) {
        Files.walkFileTree(
            _source,
            new SimpleFileVisitor<Path>() {
              @Override
              public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
                  throws IOException {
                Path p =
                    _target_dir
                        .resolve(_target_name == null ? _source.getFileName() : _target_name)
                        .resolve(_source.relativize(dir));
                log.debug("Visiting dir [" + dir + "], creating dir [" + p + "]");
                Files.createDirectories(p);
                return FileVisitResult.CONTINUE;
              }
  
              @Override
              public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
                  throws IOException {
                Path p =
                    _target_dir
                        .resolve(_target_name == null ? _source.getFileName() : _target_name)
                        .resolve(_source.relativize(file));
                log.debug("Visiting file [" + file + "], copying to [" + p + "]");
                Files.copy(file, p, options);
                return FileVisitResult.CONTINUE;
              }
            });
      }
  
      return p;
    }
  
    // Reading files
  
    /**
     * <p>readFile.</p>
     *
     * @param _p a {@link java.lang.String} object.
     * @return a {@link java.lang.String} object.
     * @throws java.io.IOException if any.
     */
    public static String readFile(String _p) throws IOException {
      return FileUtil.readFile(Paths.get(_p));
    }
  
    /**
     * Preserves the line breaks of the original file.
     * As such, it can be used for calculating digests.
     *
     * @see DigestUtil#getDigestAsString(String,java.nio.charset.Charset, DigestAlgorithm)
     * @param _p a {@link java.nio.file.Path} object.
     * @throws java.io.IOException if any.
     * @return a {@link java.lang.String} object.
     */
    public static String readFile(Path _p) throws IOException {
      try (final InputStream is = new FileInputStream(_p.toFile())) {
        return FileUtil.readInputStream(is, FileUtil.getCharset());
      }
    }
  
    /**
     * Reads the given {@link InputStream} into a {@link String}.
     *
     * @param _is a {@link java.io.InputStream} object.
     * @param _cs a {@link java.nio.charset.Charset} object.
     * @throws java.io.IOException if any.
     * @return a {@link java.lang.String} object.
     */
    public static String readInputStream(InputStream _is, Charset _cs) throws IOException {
      return new String(FileUtil.readInputStream(_is), _cs);
    }
  
    /**
     * Reads the given {@link InputStream} into a byte array.
     *
     * @param _is a {@link java.io.InputStream} object.
     * @throws java.io.IOException if any.
     * @return an array of {@link byte} objects.
     */
    public static byte[] readInputStream(InputStream _is) throws IOException {
      final ByteArrayOutputStream bos = new ByteArrayOutputStream();
      final byte[] byte_buffer = new byte[1024];
      int len = 0;
      while ((len = _is.read(byte_buffer)) != -1) bos.write(byte_buffer, 0, len);
      bos.flush();
      return bos.toByteArray();
    }
  
    // Writing files
  
    /**
     * Writes a {@link String} to the given {@link File}.
     *
     * @param _f a {@link java.io.File} object.
     * @param _content a {@link java.lang.String} object.
     * @throws java.io.IOException if any.
     */
    public static final void writeToFile(File _f, String _content) throws IOException {
      if (_content != null) FileUtil.writeToFile(_f, _content.getBytes(FileUtil.getCharset()));
    }
  
    /**
     * Writes the given content to a file that is in
     * the temporary directory returned by {@link VulasConfiguration#getTmpDir()}.
     *
     * @return the path of the file
     * @param _filename a {@link java.lang.String} object.
     * @param _suffix a {@link java.lang.String} object.
     * @param _content a {@link java.lang.String} object.
     * @throws java.io.IOException if any.
     */
    public static final Path writeToTmpFile(String _filename, String _suffix, String _content)
        throws IOException {
      final Path dir = VulasConfiguration.getGlobal().getTmpDir();
      final String prefix = (_filename != null ? _filename + "-" : "vulas-tmp-");
      final File f = File.createTempFile(prefix, "." + _suffix, dir.toFile());
      FileUtil.writeToFile(f, _content.getBytes(FileUtil.getCharset()));
      return f.toPath();
    }
  
    /**
     * Writes a byte array to the given {@link File}.
     *
     * @param _f a {@link java.io.File} object.
     * @param _content an array of {@link byte} objects.
     * @throws java.io.IOException if any.
     */
    public static final void writeToFile(File _f, byte[] _content) throws IOException {
      try {
        try (final FileOutputStream fos = new FileOutputStream(_f)) {
          fos.write(_content);
        }
      } catch (Exception e) {
        throw new IOException("Error while writing to file [" + _f + "]: " + e.getMessage(), e);
      }
    }
  
    /**
     * Expects a {@link String} array with multiple paths and returns a set for all those paths that represent
     * accessible files or directories in the file system.
     *
     * @param _paths an array of {@link java.lang.String} objects.
     * @return a {@link java.util.Set} object.
     */
    public static Set<Path> getPaths(String[] _paths) {
      final Set<Path> r = new HashSet<Path>();
      if (_paths != null) {
        for (int i = 0; i < _paths.length; i++) {
          final Path p = FileUtil.getPath(_paths[i]);
          if (p != null) r.add(p);
        }
      }
      return r;
    }
  
    /**
     * <p>getPath.</p>
     *
     * @param _path a {@link java.lang.String} object.
     * @param _create a boolean.
     * @return a {@link java.nio.file.Path} object.
     */
    public static Path getPath(String _path, boolean _create) {
      if (_path == null || _path.equals("")) return null;
      else {
        final Path p = Paths.get(_path).toAbsolutePath();
        if (FileUtil.isAccessibleFile(p) || FileUtil.isAccessibleDirectory(p)) {
          return p;
        } else if (!p.toFile().exists() && _create) {
          FileUtil.createDirectory(p);
          return p;
        } else {
          FileUtil.log.warn(
              "Path ["
                  + _path
                  + "] is neither an accessible file nor an accessible directory, hence, will be"
                  + " ignored");
          return null;
        }
      }
    }
  
    /**
     * Returns a {@link Path} object for the given {@link String} in case the path exists in the file system, null otherwise.
     *
     * @param _path a {@link java.lang.String} object.
     * @return a {@link java.nio.file.Path} object.
     */
    public static Path getPath(String _path) {
      return FileUtil.getPath(_path, false);
    }
  
    /**
     * Returns the CRC-32 checksum for the given byte array.
     *
     * @param _bytes a byte array
     * @return the CRC-32 checksum of the array
     */
    public static long getCRC32(byte[] _bytes) {
      Checksum checksum = new CRC32();
      checksum.update(_bytes, 0, _bytes.length);
      return checksum.getValue();
    }
  
    /**
     * Returns the CRC-32 checksum for the given {@link File}.
     * Returns -1 if the checksum cannot be computed.
     *
     * @param _file a file
     * @return the CRC-32 checksum of the file
     */
    public static long getCRC32(File _file) {
      try {
        if (!_file.canRead()) throw new IOException("Cannot read file");
  
        Checksum checksum = new CRC32();
  
        try (final InputStream is = new FileInputStream(_file); ) {
          byte[] bytes = new byte[1024];
          int bytes_read = -1;
          while ((bytes_read = is.read(bytes)) != -1) {
            checksum.update(bytes, 0, bytes_read);
          }
          return checksum.getValue();
        }
      } catch (IOException e) {
        FileUtil.log.error("IO Error while computing CRC-32: " + e.getMessage());
      }
      return -1;
    }
  
    /**
     * Returns the SHA1 digest for the given file.
     *
     * @param _file a {@link java.io.File} object.
     * @return a {@link java.lang.String} object.
     */
    public static final String getSHA1(File _file) {
      return FileUtil.getDigest(_file, DigestAlgorithm.SHA1);
    }
  
    /**
     * Returns a digest for the given file, using the given algorithm.
     *
     * @param _file a {@link java.io.File} object.
     * @param _alg a {@link org.eclipse.steady.shared.enums.DigestAlgorithm} object.
     * @return a {@link java.lang.String} object.
     */
    public static final String getDigest(File _file, DigestAlgorithm _alg) {
      try {
        if (!_file.canRead()) throw new IOException("Cannot read file");
  
        // Digest preparation
        final MessageDigest md = MessageDigest.getInstance(_alg.toString());
        try (final InputStream is = new FileInputStream(_file);
            final DigestInputStream dis = new DigestInputStream(is, md); ) {
          byte[] bytes = new byte[1024];
          while (dis.read(bytes, 0, 1024) != -1) {
            ;
          } // read()
          byte[] digest = md.digest();
          return DigestUtil.bytesToHex(digest);
        }
      } catch (NoSuchAlgorithmException e) {
        FileUtil.log.error("Error while instantiating [" + _alg + "] digest: " + e.getMessage());
      } catch (IOException e) {
        FileUtil.log.error("IO Error while computing [" + _alg + "] digest: " + e.getMessage());
      } catch (ArrayIndexOutOfBoundsException be) {
        FileUtil.log.error("Error while computing [" + _alg + "] digest: " + be.getMessage());
      }
      return null;
    }
  
    /**
     * Searches for the JAR file containing the class file of the given {@link Class}.
     *
     * @param _clazz a {@link java.lang.Class} object.
     * @return a {@link java.lang.String} object.
     */
    public static String getJarFilePath(Class<?> _clazz) {
      final ClassLoader cl = _clazz.getClassLoader();
      final URL res_url = cl.getResource(_clazz.getName().replace('.', '/') + ".class");
      return FileUtil.getJarFilePath(res_url.toString());
    }
  
    /**
     * Provided that the given URL points to a JAR file (or classes contained therein),
     * the method transforms the string representation of the given URL
     * into a string presentation of the local file system path of the JAR file.
     * If the URL does not point to a JAR, the method returns null.
     * Examples of URLs are as follows:
     * url:file:/a/b/c.jar!123.class
     * file:/a/b/c.jar
     *
     * @param _url a {@link java.lang.String} object.
     * @return a {@link java.lang.String} object.
     */
    public static String getJarFilePath(String _url) {
      String file_url = null, file_path = null;
  
      // (1) Bring _url into form "file:/<abc>.jar"
      if (_url != null && _url.startsWith("file:") && _url.endsWith(".jar")) {
        file_url = _url;
      } else if (_url != null && _url.startsWith("jar:file:")) {
        file_url = _url.substring(4); // new String("jar:").length());
        final int idx = file_url.indexOf('!');
        if (idx != -1) file_url = file_url.substring(0, idx);
      }
  
      // 2) If that worked, transform into FS path
      if (file_url != null) {
        URI uri = null;
        try {
          uri = new URI(file_url);
          file_path = Paths.get(uri).toString();
        } catch (URISyntaxException e) {
          log.error("Cannot create URI from [" + file_url + "]");
        }
      }
  
      return file_path;
    }
  
    /**
     * Searches for JAR files containing the given resources.
     *
     * @param _cl a {@link java.lang.ClassLoader} object.
     * @param _resources an array of {@link java.lang.String} objects.
     * @return a {@link java.util.Set} object.
     */
    public static Set<String> getJarFilePathsForResources(ClassLoader _cl, String[] _resources) {
      final Set<String> jar_paths = new HashSet<String>();
      if (_cl != null && _resources != null) {
        for (String r : _resources) {
          final URL u = _cl.getResource(r);
          if (u != null) {
            final String jar_path = FileUtil.getJarFilePath(u.toString());
            if (jar_path != null) {
              jar_paths.add(jar_path);
            }
          }
        }
      }
      return jar_paths;
    }
  
    /**
     * Returns all JARs known to the given {@link URLClassLoader}.
     *
     * @param _cl a {@link java.net.URLClassLoader} object.
     * @return a {@link java.util.Set} object.
     */
    public static Set<String> getJarFilePaths(URLClassLoader _cl) {
      final Set<String> jar_paths = new HashSet<String>();
      final URL[] urls = _cl.getURLs();
      log.info(
          "Class loader search path contains ["
              + urls.length
              + "] items: Search for configurations in JAR files");
      for (int i = 0; i < urls.length; i++) {
        final String jar_path = FileUtil.getJarFilePath(urls[i].toString());
        if (jar_path != null) {
          jar_paths.add(jar_path);
        }
      }
      return jar_paths;
    }
  
    /**
     * <p>isZipped.</p>
     *
     * @param _f a {@link java.io.File} object.
     * @return a {@link java.lang.Boolean} object.
     * @throws java.io.IOException if any.
     */
    public static Boolean isZipped(File _f) throws IOException {
      ZipInputStream zis = new ZipInputStream(new FileInputStream(_f));
      boolean isZipped = zis.getNextEntry() != null;
      if (isZipped) {
        zis.close();
        return true;
      } else {
        zis.close();
        return false;
      }
    }
  }
  