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