# FileOperationsWorker.java

    /*
    ** Module   : FileOperationsWorker.java
    ** Abstract : useful file operations for pattern engine rules
    **
    ** Copyright (c) 2005-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --JPRM-- --------------------------- Description ---------------------------
    ** 001 GES 20050310   @20259 Created initial version with delete, rename
    **                           and some basic exist/isFile/isDirectory methods.
    ** 002 ECF 20050526   @21290 Changes to support new expression engine
    **                           implementation. Changed library registration
    **                           mechanism based on superclass' modifications
    **                           to support single library per pattern worker limit.
    ** 003 ECF 20050628   @21598 Added user functions getFileSep and
    **                           getPathSep. Return the platform-specific file
    **                           and path separators, respectively, as strings.
    ** 004 ECF 20050717   @21718 Made getFile user function public.
    ** 005 GES 20080723   @39165 Added copyFile().
    ** 006 OM  20140107          Add support for finding a specific file within PROPATH collection.
    **                           Adjusted javadocs.
    ** 007 OM  20160428          Added findFiles() method.
    ** 008 OM  20170123          Paths returned by Configuration already use the current OS
    **                           separators.
    ** 009 CA  20180511          Added path aliasing and r-code support for schema triggers.
    ** 010 CA  20190520          Added hasFiles.
    ** 011 HC  20211001          Implementation of i18n support.
    **     VVT 20221215          NPE fixed in hasFiles().
    **     CA  20220421          Added getParent(path).
    **     SVL 20230108          Improved performance by replacing some "for-each" loops with indexed "for" loops.
    ** 012 GBB 20240826          Moving FileSystemDaemon to osresource package.
    */
    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program 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 Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.pattern;

    import java.io.*;
    import java.util.*;
    import java.util.function.*;
    import java.util.stream.*;
    import com.goldencode.p2j.cfg.*;
    import com.goldencode.p2j.uast.*;
    import com.goldencode.p2j.util.*;
    // ambiguous reference
    import com.goldencode.p2j.util.osresource.FileSystemDaemon;

    /**
     * Provides useful file operations for AST processing.
     */
    public class FileOperationsWorker
    extends AbstractPatternWorker
    {
       /** Map of aliases (r-code folders) to their source folders (conversion folders). */
       private final Map<String, String> pathAliases;

       /**
        * Default constructor which defines the symbol libraries to be registered.
        */
       FileOperationsWorker()
       {
          super();

          pathAliases = Configuration.getPathAliases();

          setLibrary(new FileOps());
       }

       /**
        * Finds a filename within the configured PROPATH. All needed information are read from
        * <code>Configuration</code>, which is backed-up by p2j.cfg.xml.
        *
        * @param   filename
        *          The filename that will be searched in the PROPATH.
        * @param   proPath
        *          The propath, or <code>null</code> to compute it now.
        *
        * @return  If the file is found, the normalized filename relatively to the project root.
        *          If the file is not found or the referenced file system object is not a file,
        *          <code>null</code> is returned.
        *
        * @throws  ConfigurationException
        *          if <code>P2J_HOME</code> has not been defined for this process.
        */
       public static String findFile(String filename, String[] proPath)
       throws ConfigurationException
       {
          // use lazy initialization
          if (proPath == null)
          {
             String proPathProp = Configuration.getParameter("propath");
             if (proPathProp == null || proPathProp.isEmpty())
             {
                // use the home as default search path if not explicitly defined in configuration
                proPath = new String[] { Configuration.home() };
             }
             else
             {
                // [propath] returned by Configuration already uses the current OS separators
                proPath = proPathProp.split(File.pathSeparator);
             }
          }

          String cs = Configuration.getParameter("case-sensitive");
          String filePath = FileSystemDaemon.search(proPath,
                                                    cs == null || cs.toLowerCase().equals("true"),
                                                    filename,
                                                    false);
          if (filePath == null)
          {
             return null;
          }
          else
          {
             return Configuration.normalizeFilename(filePath);
          }
       }

       /**
        * Implementation of core file operations including delete/rename/copy of
        * arbitrary files and testing for file existance.
        */
       public class FileOps
       {
          /** This is the list of paths where progress file can be found. */
          private String[] proPath = null;

          /**
           * Get the parent folder of the specified path.
           *
           * @param    path
           *           The path.
           *
           * @return   The parent folder.
           */
          public String getParent(String path)
          {
             return new File(path).getParent();
          }

          /**
           * Check if a specified folder has any files with the given extension.
           *
           * @param    path
           *           The folder path to check.
           * @param    ext
           *           The file extension to look.
           *
           * @return   <code>true</code> if the folder has at least a file with the given extension.
           */
          public boolean hasFiles(String path, String ext)
          {
             try
             {
                String[] res = new File(path).list(new FilenameFilter()
                {

                   @Override
                   public boolean accept(File dir, String name)
                   {
                      return name.endsWith(ext);
                   }
                });
                return res != null && res.length > 0;
             }
             catch (Exception e)
             {
                return false;
             }
          }

          /**
           * Determine if a filename references a file (and not a directory).
           * The filename can be specified as either absolute or relative to the
           * current directory OR as relative to the project home directory.
           *
           * @param   filename
           *          The file to check.
           * @param   relative
           *          <code>true</code> if the filename is relative to the
           *          project home directory, <code>false</code> if the filename
           *          is absolute or relative to the current directory.
           *
           * @return  <code>true</code> if the filename references a file,
           *          else <code>false</code>.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this
           *          process.
           * @throws  IOException
           *          if there was an I/O error accessing the file system.
           */
          public boolean isFile(String filename, boolean relative)
          throws ConfigurationException,
                 IOException
          {
             File file = getFile(filename, relative);

             return file.isFile();
          }

          /**
           * Determine if a filename references a directory (and not a file).
           * The filename can be specified as either absolute or relative to the
           * current directory OR as relative to the project home directory.
           *
           * @param   filename
           *          The file to check.
           * @param   relative
           *          <code>true</code> if the filename is relative to the
           *          project home directory, <code>false</code> if the filename
           *          is absolute or relative to the current directory.
           *
           * @return  <code>true</code> if the filename references a directory,
           *          else <code>false</code>.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this
           *          process.
           * @throws  IOException
           *          if there was an I/O error accessing the file system.
           */
          public boolean isDirectory(String filename, boolean relative)
          throws ConfigurationException,
                 IOException
          {
             File file = getFile(filename, relative);

             return file.isDirectory();
          }

          /**
           * Determine if a filesystem name references an existing filesystem
           * object.  The filename can be specified as either absolute or relative
           * to the current directory OR as relative to the project home
           * directory.
           *
           * @param   filename
           *          The file to check.
           * @param   relative
           *          <code>true</code> if the filename is relative to the
           *          project home directory, <code>false</code> if the filename
           *          is absolute or relative to the current directory.
           *
           * @return  <code>true</code> if the filename exists, else
           *          <code>false</code>.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this
           *          process.
           * @throws  IOException
           *          if there was an I/O error accessing the file system.
           */
          public boolean exists(String filename, boolean relative)
          throws ConfigurationException,
                 IOException
          {
             File file = getFile(filename, relative);

             return file.exists();
          }

          /**
           * Delete an arbitrary file specified by a filename which can be either
           * absolute or relative to the current directory or relative to the
           * project home directory.
           *
           * @param   filename
           *          The file to delete.
           * @param   relative
           *          <code>true</code> if the filename is relative to the
           *          project home directory, <code>false</code> if the filename
           *          is absolute or relative to the current directory.
           *
           * @return  <code>true</code> if the file was successfully deleted,
           *          else <code>false</code>.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this
           *          process.
           * @throws  IOException
           *          if there was an I/O error deleting the file.
           */
          public boolean deleteFile(String filename, boolean relative)
          throws ConfigurationException,
                 IOException
          {
             File file = getFile(filename, relative);

             return file.delete();
          }

          /**
           * Rename an arbitrary file to a new name, which can (on some platforms)
           * be used to move a file. Source and target filenames can be specified
           * as either absolute or relative to the current directory or relative
           * to the project home directory.
           *
           * @param   source
           *          The source filename.
           * @param   target
           *          The target filename.
           * @param   relative
           *          <code>true</code> if BOTH filenames are relative to the
           *          project home directory, <code>false</code> if BOTH filenames
           *          are either absolute or relative to the current directory.
           *
           * @return  <code>true</code> if the file was successfully renamed,
           *          else <code>false</code>.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this
           *          process.
           * @throws  IOException
           *          if there was an I/O error while renaming the file.
           */
          public boolean renameFile(String  source, String target, boolean relative)
          throws ConfigurationException,
                 IOException
          {
             File src = getFile(source, relative);
             File tar = getFile(target, relative);

             return src.renameTo(tar);
          }

          /**
           * Copy an arbitrary source file to a given target file. Source and
           * target filenames can be specified as either absolute or relative to
           * the current directory or relative to the project home directory.
           *
           * @param   source
           *          The source filename.
           * @param   target
           *          The target filename.
           * @param   relative
           *          <code>true</code> if BOTH filenames are relative to the
           *          project home directory, <code>false</code> if BOTH filenames
           *          are either absolute or relative to the current directory.
           * @param   overwrite
           *          <code>true</code> to overwrite the target if it exists,
           *          <code>false</code> to abort the copy if the target exists.
           *
           * @return  <code>true</code> if the file was successfully copied,
           *          else <code>false</code> (includes the non-overwrite case
           *          where the target already exists).
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this process.
           * @throws  IOException
           *          if there was an I/O error deleting the file.
           */
          public boolean copyFile(String  source, String target, boolean relative, boolean overwrite)
          throws ConfigurationException,
                 IOException
          {
             File tar = getFile(target, relative);

             if (!overwrite && tar.exists())
             {
                return false;
             }

             File src = getFile(source, relative);

             String sname = src.getCanonicalPath();
             String tname = tar.getCanonicalPath();

             return Utils.copyFile(sname, tname, 0, false);
          }

          /**
           * Get the platform-specific file separator string for the current
           * platform.
           *
           * @return  Platform-specific file seprator.
           */
          public String getFileSep()
          {
             return File.separator;
          }

          /**
           * Get the platform-specific path separator string for the current
           * platform.
           *
           * @return  Platform-specific path seprator.
           */
          public String getPathSep()
          {
             return File.pathSeparator;
          }

          /**
           * Creates and returns a file object for a filename which can be
           * specified as either absolute or relative to the current directory OR
           * relative to the project home directory.
           *
           * @param   filename
           *          The file's name.
           * @param   relative
           *          <code>true</code> if the filenames are relative to the
           *          project home directory, <code>false</code> if the filenames
           *          are either absolute or relative to the current directory.
           *
           * @return  The resulting file object.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this
           *          process.
           * @throws  IOException
           *          if there was an I/O error deleting the file.
           */
          public File getFile(String filename, boolean relative)
          throws ConfigurationException,
                 IOException
          {
             if (relative)
             {
                return Configuration.toFile(filename);
             }

             return new File(filename);
          }

          /**
           * Finds a filename within the configured PROPATH. All needed information are read from
           * <code>Configuration</code>, which is backed-up by p2j.cfg.xml.
           *
           * @param   filename
           *          The filename that will be searched in the PROPATH.
           *
           * @return  If the file is found, the normalized filename relatively to the project root.
           *          If the file is not found or the referenced file system object is not a file,
           *          <code>null</code> is returned.
           *
           * @throws  ConfigurationException
           *          if <code>P2J_HOME</code> has not been defined for this process.
           */
          public String findFile(String filename)
          throws ConfigurationException
          {
             // use lazy initialization
             if (proPath == null)
             {
                String proPathProp = Configuration.getParameter("propath");
                if (proPathProp == null || proPathProp.isEmpty())
                {
                   // use the home as default search path if not explicitly defined in configuration
                   proPath = new String[] { Configuration.home() };
                }
                else
                {
                   // [propath] returned by Configuration already uses the current OS separators
                   proPath = proPathProp.split(File.pathSeparator);
                }
             }

             return FileOperationsWorker.findFile(filename, proPath);
          }

          /**
           * Finds all filenames within the configured {@code PROPATH}. All needed information are
           * read from {@code Configuration}, which is backed-up by {@code p2j.cfg.xml}.
           *
           * @param   filename
           *          The filename that will be searched in the {@code PROPATH}.
           *
           * @return  if any file is found, an iterator for all matching filenames relatively to the
           *          project root, normalized. If no file is found or the referenced file system
           *          object is not a file, an empty iterator is returned.
           *
           * @throws  ConfigurationException
           *          if {@code P2J_HOME} has not been defined for this process.
           */
          public Iterator<String> findFiles(String filename)
          throws ConfigurationException
          {
             // use lazy initialization
             if (proPath == null)
             {
                String proPathProp = Configuration.getParameter("propath");
                if (proPathProp == null || proPathProp.isEmpty())
                {
                   // use the home as default search path if not explicitly defined in configuration
                   proPath = new String[] { Configuration.home() };
                }
                else
                {
                   // [propath] returned by Configuration already uses the current OS separators
                   proPath = proPathProp.split(File.pathSeparator);
                }
             }

             String cs = Configuration.getParameter("case-sensitive");
             boolean caseSens = cs == null || cs.toLowerCase().equals("true");
             boolean isRcode = filename.endsWith(".r") || (!caseSens && filename.endsWith(".R"));

             ArrayList<String> files = new ArrayList<>();
             files.add(filename);
             if (pathAliases != null)
             {
                filename = CallGraphHelper.prepareFilename(filename);

                String[] aliases = SourceNameMapper.resolvePathAliases(pathAliases, filename);
                for (String alias : aliases)
                {
                   files.add(alias);
                }
             }

             Set<String> res = new HashSet<>();
             Consumer<String> lookup = file ->
             {
                List<String> filePaths = FileSystemDaemon.searchAll(proPath, caseSens, file, false);
                if (filePaths == null)
                {
                   return;
                }

                res.addAll(filePaths.stream()
                                    .map(Configuration::normalizeFilename)
                                    .collect(Collectors.toList()));
             };

             for (int i = 0; i < files.size(); i++)
             {
                String file = files.get(i);
                if (isRcode)
                {
                   file = file.substring(0, file.length() - 1);

                   for (char c : "pPwW".toCharArray())
                   {
                      lookup.accept(file + c);
                   }
                }
                else
                {
                   lookup.accept(file);
                }
             }

             return res.iterator();
          }

          /**
           * Creates the directories based on the supplied path, including any necessary nonexistent parent
           * directories.
           *
           * @param   dirs
           *          The path denoting the directory or directories to create.
           * @param   relative
           *          <code>true</code> if the filenames are relative to the
           *          project home directory, <code>false</code> if the filenames
           *          are either absolute or relative to the current directory.
           *
           * @return  {@code true} when the operation succeeds, {@code false} otherwise.
           *
           * @throws  ConfigurationException
           *          if <code>path</code> is relative, but no home path has been
           *          defined for the application.
           */
          public boolean mkdirs(String dirs, boolean relative)
          throws ConfigurationException
          {
             File dir = relative ? Configuration.toFile(dirs) : new File(dirs);
             return dir.mkdirs();
          }
       }
    }