text: /**************************************************************************
   OmegaT - Computer Assisted Translation (CAT) tool
            with fuzzy matching, translation memory, keyword search,
            glossaries, and translation leveraging into updated projects.
  
   Copyright (C) 2000-2006 Keith Godfrey, Maxym Mykhalchuk, and Henry Pijffers
                 2007 Didier Briel, Zoltan Bartko, Alex Buloichik
                 2008-2011 Didier Briel
                 2012 Martin Fleurke, Didier Briel
                 2013 Aaron Madlon-Kay, Zoltan Bartko, Didier Briel, Alex Buloichik
                 2014 Aaron Madlon-Kay, Alex Buloichik
                 2015 Aaron Madlon-Kay
                 Home page: http://www.omegat.org/
                 Support center: https://omegat.org/support
  
   This file is part of OmegaT.
  
   OmegaT is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 3 of the License, or
   (at your option) any later version.
  
   OmegaT 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 for more details.
  
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
   **************************************************************************/
  
  package org.omegat.util;
  
  import java.awt.GraphicsEnvironment;
  import java.awt.event.KeyEvent;
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.PushbackInputStream;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.net.URL;
  import java.net.URLConnection;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Paths;
  import java.util.ArrayList;
  import java.util.List;
  import java.util.Objects;
  import java.util.function.Predicate;
  import java.util.regex.Pattern;
  import java.util.zip.ZipEntry;
  import java.util.zip.ZipInputStream;
  
  import org.apache.commons.io.FileUtils;
  import org.apache.commons.io.IOUtils;
  import org.omegat.util.Platform.OsType;
  
  /**
   * Static functions taken from CommandThread to reduce file size.
   *
   * @author Keith Godfrey
   * @author Maxym Mykhalchuk
   * @author Henry Pijffers (henry.pijffers@saxnot.com)
   * @author Didier Briel
   * @author Zoltan Bartko - bartkozoltan@bartkozoltan.com
   * @author Alex Buloichik
   * @author Martin Fleurke
   * @author Aaron Madlon-Kay
   */
  public final class StaticUtils {
  
      private StaticUtils() {
      }
  
      /**
       * Configuration directory on Windows platforms
       */
      private static final String WINDOWS_CONFIG_DIR = "\\OmegaT\\";
  
      /**
       * Configuration directory on UNIX platforms
       */
      private static final String UNIX_CONFIG_DIR = "/.omegat/";
  
      /**
       * Configuration directory on Mac OS X
       */
      private static final String OSX_CONFIG_DIR = "/Library/Preferences/OmegaT/";
  
      /**
       * Script directory
       */
      private static final String SCRIPT_DIR = "script";
  
      /**
       * Char which should be used instead protected parts. It should be non-letter char, to be able to have
       * correct words counter.
       *
       * This char can be placed around protected text for separate words inside protected text and words
       * outside if there are no spaces between they.
       */
      public static final char TAG_REPLACEMENT_CHAR = '\b';
      public static final String TAG_REPLACEMENT = "\b";
  
      /**
       * Contains the location of the directory containing the configuration
       * files.
       */
      private static String configDir = null;
  
      /**
       * Contains the location of the script dir containing the exported text
       * files.
       */
      private static String scriptDir = null;
  
      /**
       * Check if specified key pressed.
       *
       * @param e
       *            pressed key event
       * @param code
       *            required key code
       * @param modifiers
       *            required modifiers
       * @return true if checked key pressed
       */
      public static boolean isKey(KeyEvent e, int code, int modifiers) {
          return e.getKeyCode() == code && e.getModifiersEx() == modifiers;
      }
  
      /**
       * Returns the names of all font families available.
       */
      public static String[] getFontNames() {
          GraphicsEnvironment graphics;
          graphics = GraphicsEnvironment.getLocalGraphicsEnvironment();
          return graphics.getAvailableFontFamilyNames();
      }
  
      /** Caching install dir */
      private static String installDir = null;
  
      /**
       * Returns OmegaT installation directory.
       */
      public static String installDir() {
          if (installDir == null) {
              File file = null;
              try {
                  URI sourceUri = StaticUtils.class.getProtectionDomain().getCodeSource().getLocation().toURI();
                  if (sourceUri.getScheme().equals("file")) {
                      File uriFile = Paths.get(sourceUri).toFile();
                      // If running from a JAR, get the enclosing folder
                      // (the JAR is assumed to be at the installation root)
                      if (uriFile.getName().endsWith(".jar")) {
                          file = uriFile.getParentFile();
                      } else {
                          // Running from an IDE or build tool; use CWD.
                      }
                  } else {
                      // Running from Java WebStart; use CWD.
                  }
              } catch (URISyntaxException e) {
              }
              if (file == null) {
                  file = Paths.get(".").toFile();
              }
              installDir = file.getAbsolutePath();
          }
          return installDir;
      }
  
      /**
       * Returns the location of the configuration directory, depending on the
       * user's platform. Also creates the configuration directory, if necessary.
       * If any problems occur while the location of the configuration directory
       * is being determined, an empty string will be returned, resulting in the
       * current working directory being used.
       *
       * <ul><li>Windows XP: &lt;Documents and Settings>\&lt;User name>\Application Data\OmegaT
       * <li>Windows Vista: User\&lt;User name>\AppData\Roaming
       * <li>Linux: ~/.omegat
       * <li>Solaris/SunOS: ~/.omegat
       * <li>FreeBSD: ~/.omegat
       * <li>Mac OS X: ~/Library/Preferences/OmegaT
       * <li>Other: User home directory
       * </ul>
       *
       * @return The full path of the directory containing the OmegaT
       *         configuration files, including trailing path separator.
       */
      public static String getConfigDir() {
          // if the configuration directory has already been determined, return it
          if (configDir != null) {
              return configDir;
          }
  
          String cd = RuntimePreferences.getConfigDir();
          if (cd != null) {
              // use the forced specified directory
              configDir = new File(cd).getAbsolutePath() + File.separator;
              return configDir;
          }
  
          OsType os = Platform.getOsType(); // name of operating system
          String home; // user home directory
  
          // get os and user home properties
          try {
              // get the user's home directory
              home = System.getProperty("user.home");
          } catch (SecurityException e) {
              // access to the os/user home properties is restricted,
              // the location of the config dir cannot be determined,
              // set the config dir to the current working dir
              configDir = new File(".").getAbsolutePath() + File.separator;
  
              // log the exception, only do this after the config dir
              // has been set to the current working dir, otherwise
              // the log method will probably fail
              Log.logErrorRB("SU_USERHOME_PROP_ACCESS_ERROR");
              Log.log(e.toString());
  
              return configDir;
          }
  
          // if os or user home is null or empty, we cannot reliably determine
          // the config dir, so we use the current working dir (= empty string)
          if (os == null || StringUtil.isEmpty(home)) {
              // set the config dir to the current working dir
              configDir = new File(".").getAbsolutePath() + File.separator;
              return configDir;
          }
  
          // check for Windows versions
          if (os == OsType.WIN32 || os == OsType.WIN64) {
              String appData = null;
  
              // We do not use %APPDATA%
              // Trying first Vista/7, because "Application Data" exists also as virtual folder,
              // so we would not be able to differentiate with 2000/XP otherwise
              File appDataFile = new File(home, "AppData\\Roaming");
              if (appDataFile.exists()) {
                  appData = appDataFile.getAbsolutePath();
              } else {
                  // Trying to locate "Application Data" for 2000 and XP
                  // C:\Documents and Settings\<User>\Application Data
                  appDataFile = new File(home, "Application Data");
                  if (appDataFile.exists()) {
                      appData = appDataFile.getAbsolutePath();
                  }
              }
  
              if (!StringUtil.isEmpty(appData)) {
                  // if a valid application data dir has been found,
                  // append an OmegaT subdir to it
                  configDir = appData + WINDOWS_CONFIG_DIR;
              } else {
                  // otherwise set the config dir to the user's home directory,
                  // usually
                  // C:\Documents and Settings\<User>\OmegaT
                  configDir = home + WINDOWS_CONFIG_DIR;
              }
          // Check for UNIX varieties
          // Solaris is generally detected as SunOS
          } else if (os == OsType.LINUX32 || os == OsType.LINUX64 || os == OsType.OTHER) {
              // set the config dir to the user's home dir + "/.omegat/", so it's
              // hidden
              configDir = home + UNIX_CONFIG_DIR;
          // check for Mac OS X
          } else if (Platform.isMacOSX()) {
              // set the config dir to the user's home dir +
              // "/Library/Preferences/OmegaT/"
              configDir = home + OSX_CONFIG_DIR;
          // other OSes / default
          } else {
              // use the user's home directory by default
              configDir = home + File.separator;
          }
  
          // create the path to the configuration dir, if necessary
          if (!configDir.isEmpty()) {
              try {
                  // check if the dir exists
                  File dir = new File(configDir);
                  if (!dir.exists()) {
                      // create the dir
                      boolean created = dir.mkdirs();
  
                      // if the dir could not be created,
                      // set the config dir to the current working dir
                      if (!created) {
                          Log.logErrorRB("SU_CONFIG_DIR_CREATE_ERROR");
                          configDir = new File(".").getAbsolutePath() + File.separator;
                      }
                  }
              } catch (SecurityException e) {
                  // the system doesn't want us to write where we want to write
                  // reset the config dir to the current working dir
                  configDir = new File(".").getAbsolutePath() + File.separator;
  
                  // log the exception, but only after the config dir has been
                  // reset
                  Log.logErrorRB("SU_CONFIG_DIR_CREATE_ERROR");
                  Log.log(e.toString());
              }
          }
  
          // we should have a correct, existing config dir now
          return configDir;
      }
  
      public static String getScriptDir() {
          // If the script directory has already been determined, return it
          if (scriptDir != null) {
              return scriptDir;
          }
          scriptDir = getConfigDir() + SCRIPT_DIR + File.separator;
  
          try {
              // Check if the directory exists
              File dir = new File(scriptDir);
              if (!dir.exists()) {
                  // Create the directory
                  boolean created = dir.mkdirs();
  
                  // If the directory could not be created,
                  // set the script directory to config directory
                  if (!created) {
                      Log.logErrorRB("SU_SCRIPT_DIR_CREATE_ERROR");
                      scriptDir = getConfigDir();
                  }
              }
          } catch (SecurityException e) {
              // The system doesn't want us to write where we want to write
              // reset the script dir to the current config dir
              scriptDir = getConfigDir();
  
              // log the exception, but only after the script dir has been reset
              Log.logErrorRB("SU_SCRIPT_DIR_CREATE_ERROR");
              Log.log(e.toString());
          }
          return scriptDir;
      }
  
      /**
       * Encodes the array of bytes to store them in a plain text file.
       */
      public static String uuencode(byte[] buf) {
          if (buf.length <= 0) {
              return "";
          }
          StringBuilder res = new StringBuilder();
          res.append(buf[0]);
          for (int i = 1; i < buf.length; i++) {
              res.append('#');
              res.append(buf[i]);
          }
          return res.toString();
      }
  
      /**
       * Decodes the array of bytes that was stored in a plain text file as a
       * string, back to array of bytes.
       */
      public static byte[] uudecode(String buf) {
          String[] bytes = buf.split("#");
          byte[] res = new byte[bytes.length];
          for (int i = 0; i < bytes.length; i++) {
              try {
                  res[i] = Byte.parseByte(bytes[i]);
              } catch (NumberFormatException e) {
                  res[i] = 0;
              }
          }
          return res;
      }
  
      /**
       * Makes the file name relative to the given path.
       */
      public static String makeFilenameRelative(String filename, String path) {
          if (filename.toLowerCase().startsWith(path.toLowerCase())) {
              return filename.substring(path.length());
          } else {
              return filename;
          }
      }
  
      /**
       * Translates a string containing word-processing "glob"-style wildcards
       * (<code>?</code> matches a single non-whitespace character, <code>*</code>
       * matches zero or more non-whitespace characters) to standard regex.
       * <p>
       * If <code>spaceMatchesNbsp</code> is <code>true</code>, non-breaking
       * spaces (<code>U+00A0</code>) will also be considered whitespace.
       * <ul>
       * <li><code>?</code> is translated to <code>\S</code> (or
       * <code>[^\s\u00A0]</code>)
       * <li><code>*</code> is translated to <code>\S*</code> (or
       * <code>[^\s\u00A0]*</code>)
       * <li>If <code>spaceMatchesNbsp</code> is <code>true</code>, then
       * '<code> </code>' is translated to <code>( |\u00A0)</code>
       * <li>All other special regex characters are escaped as literals
       * </ul>
       *
       * @param text
       *            The text to escape
       * @param spaceMatchesNbsp
       *            Whether to consider regular spaces to also match non-breaking
       *            spaces
       * @return The escaped text
       */
      public static String globToRegex(String text, boolean spaceMatchesNbsp) {
          String quoted = Pattern.quote(text);
  
          StringBuilder sb = new StringBuilder(quoted);
  
          // We should blow up if the standard library implementation ever
          // switches to a scheme other than using \Q and \E.
          assert quoted.startsWith("\\Q");
          assert quoted.endsWith("\\E");
  
          if (spaceMatchesNbsp) {
              replaceGlobs(sb, "*", "[^\\s\u00A0]*");
              replaceGlobs(sb, "?", "[^\\s\u00A0]");
              replaceGlobs(sb, " ", "(?: |\u00A0)");
          } else {
              replaceGlobs(sb, "*", "\\S*");
              replaceGlobs(sb, "?", "\\S");
          }
  
          return sb.toString();
      }
  
      private static void replaceGlobs(StringBuilder haystack, String needle, String replacement) {
          replacement = "\\E" + replacement + "\\Q";
          int current = 0;
          int globIndex = 0;
          while ((globIndex = haystack.indexOf(needle, current)) != -1) {
              haystack.replace(globIndex, globIndex + 1, replacement);
              current = globIndex + replacement.length();
          }
      }
  
      /**
       * Download a file to memory
       */
      public static String downloadFileToString(URL url, int timeout) throws IOException {
          URLConnection urlConn = url.openConnection();
          urlConn.setConnectTimeout(timeout);
          urlConn.setReadTimeout(timeout);
          try (InputStream in = urlConn.getInputStream()) {
              return IOUtils.toString(in, StandardCharsets.UTF_8);
          }
      }
  
      /**
       * Extracts files from an InputStream representing a zip archive to the specified destination path.
       *
       * @param in
       *            InputStream representing a zip archive
       * @param destination
       *            Path where archive entries will be saved
       * @param filenameFilter
       *            Filter for entry names. Return false to skip extracting an entry
       * @return List of extracted entry names
       * @throws IOException
       */
      public static List<String> extractFromZip(InputStream in, File destination,
              Predicate<String> filenameFilter) throws IOException {
          List<String> extracted = new ArrayList<>();
          try (PushbackInputStream pis = new PushbackInputStream(in, 2)) {
              byte[] sig = new byte[2];
              pis.read(sig);
              if (!(sig[0] == 0x50 && sig[1] == 0x4b)) {
                  throw new IllegalArgumentException("Input stream was not a zip file");
              }
              pis.unread(sig);
              try (ZipInputStream zis = new ZipInputStream(pis)) {
                  // parse the entries
                  ZipEntry entry;
                  while ((entry = zis.getNextEntry()) != null) {
                      if (filenameFilter.test(entry.getName())) {
                          // match found
                          File f = new File(destination, entry.getName());
                          FileUtils.copyToFile(zis, f);
                          extracted.add(entry.getName());
                      }
                  }
              }
          }
          return extracted;
      }
  
      /**
       * Parse a command line string into arguments, interpreting
       * double and single quotes as Bash does.
       * @param cmd Command string
       * @return Array of arguments
       */
      public static String[] parseCLICommand(String cmd) {
          cmd = cmd.trim();
          if (cmd.isEmpty()) {
              return new String[] { "" };
          }
  
          StringBuilder arg = new StringBuilder();
          List<String> result = new ArrayList<String>();
  
          final char noQuote = '\0';
          char currentQuote = noQuote;
          for (int cp, i = 0; i < cmd.length(); i += Character.charCount(cp)) {
              cp = cmd.codePointAt(i);
              if (cp == currentQuote) {
                  currentQuote = noQuote;
              } else if (cp == '"' && currentQuote == noQuote) {
                  currentQuote = '"';
              } else if (cp == '\'' && currentQuote == noQuote) {
                  currentQuote = '\'';
              } else if (cp == '\\' && i + 1 < cmd.length()) {
                  int ncp = cmd.codePointAt(cmd.offsetByCodePoints(i, 1));
                  if ((currentQuote == noQuote && Character.isWhitespace(ncp))
                          || (currentQuote == '"' && ncp == '"')) {
                      arg.appendCodePoint(ncp);
                      i += Character.charCount(ncp);
                  } else {
                      arg.appendCodePoint(cp);
                  }
              } else {
                  if (Character.isWhitespace(cp) && currentQuote == noQuote) {
                      if (arg.length() > 0) {
                          result.add(arg.toString());
                          arg = new StringBuilder();
                      } else {
                          // Discard
                      }
                  } else {
                      arg.appendCodePoint(cp);
                  }
              }
          }
          // Catch last arg
          if (arg.length() > 0) {
              result.add(arg.toString());
          }
          return result.toArray(new String[result.size()]);
      }
  
      public static boolean isProjectDir(File f) {
          if (f == null || f.getName().isEmpty()) {
              return false;
          }
          File projFile = new File(f.getAbsolutePath(), OConsts.FILE_PROJECT);
          return projFile.isFile();
      }
  
      /**
       * Check to see if an array contains another array.
       *
       * @param needles
       *            The contained array
       * @param haystack
       *            The containing array
       * @param offset
       *            The offset of {@code haystack} at which to start checking
       * @return Whether or not {@code haystack} contains {@code needles} at {@code offset}
       * @throws ArrayIndexOutOfBoundsException
       *             If {@code offset} is not a valid index in {@code haystack}
       */
      public static <T> boolean arraysMatchAt(T[] needles, T[] haystack, int offset) {
          if (offset < 0 || offset >= haystack.length) {
              throw new ArrayIndexOutOfBoundsException(offset);
          }
          if (haystack.length - offset < needles.length) {
              return false;
          }
          for (int i = 0; i < needles.length; i++) {
              if (!Objects.equals(haystack[i + offset], needles[i])) {
                  return false;
              }
          }
          return true;
      }
  
  } // StaticUtils
  