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();
      }
   }
}