FileSystemOps.java
/*
** Module : FileSystemOps.java
** Abstract : file system utility methods
**
** Copyright (c) 2005-2025, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- -----------------------------------Description-----------------------------------
** 001 GES 20050706 @21665 Created initial version, supporting common
** language statements and built-in functions
** for manipulating and querying the file
** system.
** 002 GES 20050728 @21922 Added searchPath to support the built-in
** search() function.
** 003 GES 20050831 @22425 Added FILE-INFO system handle support.
** 004 GES 20060123 @24033 Moved to a context-local implementation.
** 005 GES 20060211 @24540 Split the implementation from the Progress
** interface/semantic. The FileSystem interface
** was designed to allow this. This enables
** a remote implementation of the interface.
** 006 GES 20060322 @25202 Search path must be aware of the mapping
** between Progress source filenames and the
** Java equivalent.
** 007 GES 20080123 @36900 Changed getValue to toStringMessage which is
** safer. Added null checks everywhere to make
** the code safer.
** 008 GES 20080321 @37631 Added full support for nearly all remaining
** FILE-INFO features. The remaining features
** are stubbed out.
** 009 GES 20110712 Initialize the case-sensitivity setting of the
** legacy system to allow searching to honor it.
** 010 GES 20111020 Modified per-session initialization to detect
** when the client code is running inside the server
** process AND to initialize for local redirection
** instead of using a remote object.
** 011 CS 20121104 Implement PROPATH assignment.
** 012 CA 20130112 Added asHandle, isValid, getType and isUnknown
** methods, accessed by the FILE-INFO handle.
** FILE-NAME attribute is converted to getFileName,
** as it needs to be consistent with the procedure's
** FILE-NAME attribute.
** 013 CA 20130221 getType getter for TYPE attribute was renamed to getResourceType.
** 014 OM 20130304 Refactored isValid and isUnknown of WrappedResource to valid
** and unknown.
** 015 CS 20130325 Task 1613: finish FILE-INFO system handle support
** 016 OM 20130618 Used default constructors for unknown values.
** 017 CA 20130927 Static proxies need the unknown() API. Resource type is determined
** from LegacyResource constants.
** 018 EVL 20150119 Add init server call to push the PROPATH related values to client.
** 019 CA 20150502 The resource ID needs to be kept at the resource, so it can be
** accessed after the resource is deleted.
** 020 OM 20160527 searchPath() returns legacy procedure names instead of java class.
** 021 OM 20160927 Fixed getProperty() to return unknown value when property not set.
** 022 SVL 20170126 Added MEMBER function.
** GES 20170228 Added javadoc.
** 023 SVL 20171109 Added getFileContents.
** 024 CA 20180221 Use listeners to be notified when PROPATH has been set.
** 025 SBI 20180522 Changed to set legacy file system parameters.
** 026 SBI 20180531 Added environment variables map.
** ECF 20180609 Added {get|set}WorkingDirectory methods.
** 027 ECF 20180614 Adapt to changes in EnvironmentOps API signatures.
** 20180615 Refactored and moved code from LogicalTerminal to search for
** resources in jar files.
** 028 SBI 20180704 Changed searchPath to resolve a legacy path name into a valid system
** path name on the client system.
** 029 SBI 20181207 Changed FileSystemDaemon setup taking into account the resolveFilePaths
** parameter.
** 030 OM 20190317 Fixed paths name conversion for Windows.
** 031 RFB 20200421 Added full support for LIBRARY() and MEMBER(). Revisited and made some
** performance upgrades.
** RFB 20200424 Enhanced the javadoc in searchPath (SEARCH) due to some restrictions
** on the capabilities FWD supports with respect to library and member.
** Also moved new private static method to the correct location.
** 032 RFB 20200627 Cleaned up a Javadoc error in procLibraryParser.
** EVL 20201111 Adding ability to log missed path entries and bypass client side search for ones
** that included in ignore list. Used HashSet instead of ArrayList to speed up
** string search.
** OM 20201203 Fixed handling of READ/ONLY attributes.
** 033 SBI 20210414 Fixed getRefinedPathName in the case it will be used to normalize a directory
** path ended with a path separator character.
** 20210414 Moved getRefinedPathName, JarResource to Utils, moved jclCache to JarClassLoader.
** CA 20210527 FILE-INFO now will search for converted program files, too.
** SBI 20210813 Changed searchResourceJars according to Utils.searchResourceJars.
** EVL 20220404 Added ability to get file size during initialization of FileInfo class instance.
** EVL 20220407 Opening missed file should not interrupt processing. We just have no ability to
** get file size during init FileInfo class. Made another error protection to
** disable messages on client side.
** VVT 20221003 CommonHandle.getResourceType() method renamed to resourceType() to prevent
** conflicts with namesakes in DMO. See #6694.
** EVL 20221103 Adding fixup file/directory names to avoid using invalid separator sequence.
** 034 HC 20230427 Implemented server-side file-system resource. File operations
** performed on client can be optionally performed directly on server
** without the expensive remote call.
** 035 EVL 20230512 Fix for empty file name handling for initFileInfo(). Need to reset all
** internals to unknown values.
** 036 DDF 20230705 Replace static field initialization with a method called at server bootstrap.
** 037 CA 20240225 Added 'getFileSeparator'. Now 'getWorkingDir' returns 'clientConfig/workingDir'
** when server-side file-system and normal working dir, otherwise.
** 038 GBB 20240404 JarResource class and searchResourceJars() moved from Utils to JarUtil.
** 039 CA 20240619 'initFileInfo' uses an internal silent mode, and it needs to clear the pending
** error state.
** 040 GBB 20240709 Hard-coded config name replaced by ConfigItem constant.
** 041 GBB 20240826 Reflect changes in OSResourceManager interface and package.
** 042 GBB 20240912 OSResourceManager method isServerSide to use an enum param.
** 043 OM 20250410 Local optimizations. Code maintenance.
** 044 SP 20250526 Changed searchPath() to return character.UNKNOWN when filename is empty.
*/
/*
** 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.util;
import java.util.*;
import java.util.regex.*;
import java.util.function.*;
import java.io.*;
import com.goldencode.p2j.classloader.*;
import com.goldencode.p2j.directory.*;
import com.goldencode.p2j.main.*;
import com.goldencode.p2j.security.ContextLocal;
import com.goldencode.p2j.util.logging.*;
import com.goldencode.p2j.util.osresource.*;
import com.goldencode.proxy.StaticProxy;
import com.goldencode.util.PlatformHelper;
//WARNING: Do NOT add usage of Guava. We plan to remove the current usage.
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
/**
* Progress compatible static file-system utility methods.
* <p>
* This supports some common global variables that are called built-in
* functions in Progress but which take no parameters, not even empty
* parenthesis. These variables are read-only (they cannot be assigned).
* <p>
* A partial implementation of the <code>FILE-INFO</code> system handle is
* available but only the <code>FILE-NAME</code> and <code>FILE-SIZE</code>
* attributes are supported.
* <p>
* The following is the list of supported Progress 4GL features:
* <p>
* <pre>
* Progress Variable Java Equivalent
* --------------------- ----------------------
* os-error {@link #getLastError}
* os-drives {@link #getRootList}
* os-getenv {@link #getProperty}
* </pre>
* <p>
* <pre>
* Language Statement Java Equivalent
* --------------------- ----------------------
* os-append {@link #append}
* os-copy {@link #copy}
* os-create-dir {@link #mkdir}
* os-delete {@link #delete}
* os-rename {@link #rename}
* </pre>
* <p>
* <pre>
* Function Java Equivalent
* --------------------- ----------------------
* member {@link #member}
* search {@link #searchPath}
* </pre>
* <p>
* <pre>
* System Handle:Attribute Java Equivalent
* ------------------------- ----------------------
* file-info:file-create-date {@link #fileInfoGetCreationDate} (get)
* file-info:file-create-time {@link #fileInfoGetCreationTime} (get)
* file-info:file-mod-date {@link #fileInfoGetModDate} (get)
* file-info:file-mod-time {@link #fileInfoGetModTime} (get)
* file-info:file-name {@link #initFileInfo} (set)
* file-info:file-name {@link #getFileName} (get)
* file-info:file-size {@link #fileInfoGetSize} (get)
* file-info:file-type {@link #fileInfoGetType} (get)
* file-info:full-pathname {@link #fileInfoGetFullPath} (get)
* file-info:pathname {@link #fileInfoGetPathName} (get)
* </pre>
* <p>
* This class implements the Progress interface/semantic. However, the actual
* implementation of this function is provided by a network object that
* implements the {@link com.goldencode.p2j.util.osresource.FileSystem} interface. This means that the actual
* operations occur on the remote node associated with this user's session.
* Please see {@link FileSystemDaemon} and the <code>RemoteObject</code> in
* the <code>net</code> package.
*/
public class FileSystemOps
{
/** Logger. */
private static final CentralLogger LOG = CentralLogger.get(FileSystemOps.class);
/** File name separator used in jar file entries */
private static final char JAR_SEP = '/';
/** Stores pre-compiled pattern for LIBRARY and MEMBER built-ins. */
private static final Pattern patternLibraryMember = Pattern.compile("(.*)<<(.*)>>");
/** Stores context-local state variables. */
private static final ContextContainer work = new ContextContainer();
/** Application package root path */
private static String pkgRootPath = "";
/**
* Method called at server bootstrap that initializes values from the directory
* configuration. Until this method is called, default values are used.
*/
public static void bootstrap()
{
pkgRootPath = Utils.getDirectoryNodeString(null, "legacy-system/pkgroot", "").replace('.', JAR_SEP) +
JAR_SEP;
}
/**
* Get the instance for the FILE-INFO system handle. Is obtained using
* a call to the {@link StaticProxy#obtain(Class, Class[])}, using the
* {@link CommonFileInfo} interface and its methods implemented by these
* classes: {@link FileSystemOps}.
*
* @return See above.
*/
public static handle asHandle()
{
WorkArea wa = work.obtain();
Class<?>[] classes = { FileSystemOps.class };
CommonFileInfo proxy = StaticProxy.obtain(CommonFileInfo.class, classes);
if (wa.id == null)
{
wa.id = handle.resourceId(proxy);
}
return new handle(proxy);
}
/**
* Get this resource's ID.
*
* @return The resource's ID.
*/
public static Long id()
{
WorkArea wa = work.obtain();
return wa.id;
}
/**
* Set this resource's ID.
* <p>
* This is a no-op for system handles.
*
* @param id
* The resource's ID.
*/
public static void id(long id)
{
// no-op
}
/**
* Implementation for the {@link WrappedResource#valid()} API.
*
* @return Always true.
*/
public static boolean valid()
{
return true;
}
/**
* Implementation for the {@link WrappedResource#unknown()} API.
*
* @return Always false.
*/
public static boolean unknown()
{
return false;
}
/**
* Push the PROPATH values to the client side.
*/
public static void init()
{
work.obtain();
}
/**
* API needed to implement read-only attribute assignment (a 4GL "feature").
*
* @param attribute
* The attribute's name.
*
* @see handle#readOnlyError(handle, String)
*/
public static void readOnlyError(String attribute)
{
handle.readOnlyError(asHandle(), attribute);
}
/**
* API needed to implement read-only attribute assignment (a 4GL "feature").
*
* @param attribute
* The attribute's name.
* @param expr
* The value which is attempted to be assigned to the read-only attribute.
*
* @see handle#readOnlyError(handle, String, Object)
*/
public static void readOnlyError(String attribute, Object expr)
{
handle.readOnlyError(asHandle(), attribute);
}
/**
* Get the error code of the last operating system operation.
*
* @return The Progress-compatible error code.
*/
public static integer getLastError()
{
return new integer(work.obtain().lastError);
}
/**
* Sets the error code of the last operating system operation.
*
* @param err
* The Progress-compatible error code.
*/
public static void setLastError(int err)
{
work.obtain().lastError = err;
}
/**
* Search for the specified file and initialize the related data structures if found. Relative filenames
* are searched for via the {@link EnvironmentOps#getSearchPath}. If <code>name</code> is
* <code>unknown</code> or not found, the current data structures are cleared.
* <p>
* If no file is found on the file system, a lookup will be done through the converted legacy code, jar
* resources, etc, via {@link #searchPath}. If a program is found, that will be used in the data structures.
*
* @param name
* Relative or absolute filename to obtain information on.
*/
public static void initFileInfo(character name)
{
if (name == null || name.isUnknown())
{
work.obtain().info = null;
return;
}
initFileInfo(name.toStringMessage());
}
/**
* Search for the specified file and initialize the related data structures if found. Relative filenames
* are searched for via the {@link EnvironmentOps#getSearchPath}. If <code>name</code> is
* <code>unknown</code> or not found, the current data structures are cleared.
* <p>
* If no file is found on the file system, a lookup will be done through the converted legacy code, jar
* resources, etc, via {@link #searchPath}. If a program is found, that will be used in the data structures.
*
* @param name
* Relative or absolute filename to obtain information on.
*/
public static void initFileInfo(String name)
{
WorkArea wa = work.obtain();
// for empty input file name we need to reset all internal to unknown values
if (name == null || name.isEmpty())
{
// reset internal structures
wa.info = null;
return;
}
// fixup possible file name errors like double '/'
name = resolveLegacyPath(name, wa);
wa.info = (name == null) ? null : wa.fs.accessFileInfo(name);
if (name != null && (wa.info == null || wa.info.pathname == null))
{
// try a lookup jar resources, converted programs, other files via PROPATH
character found = searchPath(name);
if (!found.isUnknown())
{
// initialize the FileInfo structure
wa.info = new FileInfo();
wa.info.name = name;
wa.info.pathname = found.toStringMessage();
wa.info.fullname = found.toStringMessage(); // WARNING: this is not a full path
// trying to get file size, should work for both jar and OS file system sources
if (wa.info.pathname != null)
{
boolean silentCurrent = ErrorManager.isSilent();
try
{
ErrorManager.setSilent(true);
Stream fs = StreamFactory.openFileStream(wa.info.pathname, false, false);
if (fs != null)
{
wa.info.size = fs.getLength().longValue();
fs.close();
}
else
{
wa.info.size = -1; // not available
}
}
catch (Exception e)
{
// the problems while opening file, may be missing
wa.info.size = -1; // not available
}
finally
{
ErrorManager.setSilent(silentCurrent);
ErrorManager.clearPending();
}
}
else
{
wa.info.size = -1; // not available
}
wa.info.type = "FR"; // file, read-only
wa.info.modification = -1; // not available
wa.info.creation = -1; // not available
}
}
}
/**
* If the current file information data structures are initialized, return
* the file name, otherwise return the <code>unknown value</code>.
*
* @return The file name of the current file being inspected as input
* by the caller (even if the file could not be found) or the
* <code>unknown value</code> if the data structures were never
* initialized.
*/
public static character getFileName()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.name == null)
{
return character.UNKNOWN;
}
return character.of(wa.info.name);
}
/**
* If the current file information data structures are initialized, return
* the absolute or relative path name, otherwise return the
* <code>unknown value</code>. If the specified file name was absolute,
* then an absolute pathname will be returned. Otherwise the relative
* path that was found via {@link #searchPath} will be returned.
*
* @return The path name of the current file being inspected or the
* <code>unknown value</code> if the data structures were never
* initialized or if initialization failed.
*/
public static character fileInfoGetPathName()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.pathname == null)
{
return character.UNKNOWN;
}
return character.of(wa.info.pathname);
}
/**
* If the current file information data structures are initialized, return
* the absolute (canonicalized) path name, otherwise return the
* <code>unknown value</code>.
*
* @return The absolute path name of the current file being inspected or
* the <code>unknown value</code> if the data structures were
* never initialized or if initialization failed.
*/
public static character fileInfoGetFullPath()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.fullname == null)
{
return character.UNKNOWN;
}
return character.of(wa.info.fullname);
}
/**
* If the current file information data structures are initialized, return
* the file's size, otherwise return the <code>unknown value</code>.
*
* @return The size in bytes of the current file being inspected or the
* <code>unknown value</code> if the data structures were never
* initialized or if initialization failed.
*/
public static integer fileInfoGetSize()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.size == -1)
{
return new integer();
}
return new integer(wa.info.size);
}
/**
* If the current file information data structures are initialized, return
* a string with characters that specify attributes about the file,
* otherwise return the <code>unknown value</code>.
* <p>
* One of the following mutually exclusive characters will be present:
* <p>
* <pre>
* Character Meaning
* --------- -----------------------------
* D directory
* F file
* X unknown
* </pre>
* <p>
* <b>There is no support for "S" (UNIX special devices) or "M" (procedure
* library members) at this time.</b>
* <p>
* One or more of the following characters will be present:
* <p>
* <pre>
* Character Meaning
* --------- -----------------------------
* H hidden
* R readable
* W writable
* </pre>
* <p>
* <b>There is no support for "L" (UNIX symbolic links) or "P" (pipes)
* at this time.</b>
*
* @return The file attributes of the current file being inspected or
* the <code>unknown value</code> if the data structures were
* never initialized or if initialization failed.
*/
public static character fileInfoGetType()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.type == null)
{
return character.UNKNOWN;
}
return character.of(wa.info.type);
}
/**
* If the current file information data structures are initialized, return
* the file's last modification date, otherwise return the
* <code>unknown value</code>.
*
* @return The last modification date of the current file being inspected
* or the <code>unknown value</code> if the data structures were
* never initialized or if initialization failed.
*/
public static date fileInfoGetModDate()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.modification == -1)
{
return new date();
}
return new date(date.neutralMillisToDate(wa.info.modification, null));
}
/**
* If the current file information data structures are initialized, return
* the file's last modification time (in seconds from midnight), otherwise
* return the <code>unknown value</code>.
*
* @return The last modification time in seconds from midnight of the
* current file being inspected or the <code>unknown value</code>
* if the data structures were never initialized or if
* initialization failed.
*/
public static integer fileInfoGetModTime()
{
WorkArea wa = work.obtain();
if (wa.info == null || wa.info.modification == -1)
{
return new integer();
}
// turn the modification millis into a specific date (this drops the
// time value and leaves just the date as a julian day number)
Date cd = date.neutralMillisToDate(wa.info.modification, null);
date create = new date(cd);
// convert the julian day number into a base millis value for that day
long base = date.julianDayToMillis(create.longValue());
// get the difference and convert to seconds
return new integer((wa.info.modification - base) / 1000);
}
/**
* If the current file information data structures are initialized, return
* the file's creation date, otherwise return the
* <code>unknown value</code>.
*
* @return Always <code>unknown value</code> at this time.
*/
public static date fileInfoGetCreationDate()
{
// the creation date is only given on windows, on Unix unknown value
// will be returned.
if (PlatformHelper.isUnderWindowsFamily())
{
WorkArea wa = work.obtain();
// if creation is -1 error occurred
if (wa.info == null || wa.info.creation == -1)
{
return new date();
}
Date createDate = new Date(wa.info.creation);
return new date(createDate);
}
else
{
return new date();
}
}
/**
* If the current file information data structures are initialized, return
* the file's creation time (in seconds from midnight), otherwise
* return the <code>unknown value</code>.
*
* @return Always returns <code>unknown value</code> at this time.
*/
public static integer fileInfoGetCreationTime()
{
// the creation time is only given on windows, on Unix no value
// will be returned.
if (PlatformHelper.isUnderWindowsFamily())
{
WorkArea wa = work.obtain();
// if creation is -1 error occurred
if (wa.info == null || wa.info.creation == -1)
{
return new integer();
}
Date createDate = new Date(wa.info.creation);
GregorianCalendar calendar = new GregorianCalendar();
calendar.setTime(createDate);
return new integer(date.secondsSinceMidnight(calendar));
}
else
{
return new integer();
}
}
/**
* Get the comma-separated list of filesystem roots (drive letters on
* platforms that support the concept, otherwise returns the equivalent
* of the empty string).
*
* @return The comma-separated list of drive letters.
*/
public static character getRootList()
{
try
{
return character.of(work.obtain().fs.getRootList());
}
catch (IOException ioe)
{
setLastError(FileSystem.ERR_IO);
return character.EMPTY_STRING;
}
}
/**
* Get the system property (user-defined properties can be specified at the Java command line)
* associated with the passed key. If the specified system property does not exist,
* {@code unknown} value is returned.
*
* @return The system property found or {@code unknown} if the property does not exist.
*/
public static character getProperty(character key)
{
if (key == null || key.isUnknown())
{
return character.UNKNOWN;
}
return getProperty(key.toStringMessage());
}
/**
* Get the system property (user-defined properties can be specified at the Java command line)
* associated with the passed key. If the specified system property does not exist,
* {@code unknown} value is returned.
*
* @return The system property found or {@code unknown} if the property does not exist.
*/
public static character getProperty(String key)
{
if (key == null)
{
return character.UNKNOWN;
}
WorkArea wa = work.obtain();
try
{
String transformedKey = key;
if (!wa.legacyCaseSensitive)
{
transformedKey = key.toUpperCase(Locale.ROOT);
}
String value = wa.environmentVariables.get(transformedKey);
if (value != null)
{
return character.of(value);
}
//try interceptors
Function<String, String> interceptor = wa.keysInterceptors.get(transformedKey);
if (interceptor != null)
{
key = interceptor.apply(transformedKey);
}
// fall back to the client system properties
return character.of(wa.fs.getProperty(key));
}
catch (Exception exc)
{
// return the unknown value if a null pointer, security or illegal argument occurs
return character.UNKNOWN;
}
}
/**
* Appends the specified source file to the end of the target file. If
* any error occurs, no exception will be thrown. The {@link #getLastError}
* can be used to retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name or
* if this is a non-existent filename, this method is converted
* into a copy operation.
*/
public static void append(character src, character tar)
{
if (src == null || src.isUnknown() || tar == null || tar.isUnknown())
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
append(src.toStringMessage(), tar.toStringMessage());
}
}
/**
* Appends the specified source file to the end of the target file. If
* any error occurs, no exception will be thrown. The {@link #getLastError}
* can be used to retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name or
* if this is a non-existent filename, this method is converted
* into a copy operation.
*/
public static void append(character src, String tar)
{
if (src == null || src.isUnknown() || tar == null)
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
append(src.toStringMessage(), tar);
}
}
/**
* Appends the specified source file to the end of the target file. If
* any error occurs, no exception will be thrown. The {@link #getLastError}
* can be used to retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name or
* if this is a non-existent filename, this method is converted
* into a copy operation.
*/
public static void append(String src, character tar)
{
if (src == null || tar == null || tar.isUnknown())
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
append(src, tar.toStringMessage());
}
}
/**
* Appends the specified source file to the end of the target file. If
* any error occurs, no exception will be thrown. The {@link #getLastError}
* can be used to retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name or
* if this is a non-existent filename, this method is converted
* into a copy operation.
*/
public static void append(String src, String tar)
{
WorkArea wa = work.obtain();
if (src == null || tar == null)
{
wa.lastError = FileSystem.ERR_NO_ERROR;
}
else
{
wa.lastError = wa.fs.copy(resolveLegacyPath(src, wa), resolveLegacyPath(tar, wa), false);
}
}
/**
* Copies the specified source file to or over the target file. This
* method always overwrites the target file. If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name,
* the source file name will be used as the target file in
* the directory given.
*/
public static void copy(character src, character tar)
{
if (src == null || src.isUnknown() || tar == null || tar.isUnknown())
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
copy(src.toStringMessage(), tar.toStringMessage());
}
}
/**
* Copies the specified source file to or over the target file. This
* method always overwrites the target file. If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name,
* the source file name will be used as the target file in
* the directory given.
*/
public static void copy(character src, String tar)
{
if (src == null || src.isUnknown() || tar == null)
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
copy(src.toStringMessage(), tar);
}
}
/**
* Copies the specified source file to or over the target file. This
* method always overwrites the target file. If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name,
* the source file name will be used as the target file in
* the directory given.
*/
public static void copy(String src, character tar)
{
if (src == null || tar == null || tar.isUnknown())
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
copy(src, tar.toStringMessage());
}
}
/**
* Copies the specified source file to or over the target file. This
* method always overwrites the target file. If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file. Must NOT be a directory name.
* @param tar
* The name of the target file. If this is a directory name,
* the source file name will be used as the target file in
* the directory given.
*/
public static void copy(String src, String tar)
{
WorkArea wa = work.obtain();
if (src == null || tar == null)
{
wa.lastError = FileSystem.ERR_NO_ERROR;
}
else
{
wa.lastError = wa.fs.copy(src, tar, true);
}
}
/**
* Creates each of the directories requested, if it doesn't exist and all
* path segments up to the last segment do exist. If the directory name
* already exists in the filesystem (even if it is a file), this method
* just returns (this is the Progress behavior). If any error occurs, no
* exception will be thrown, but no subsequent directories in the list
* will be created. The {@link #getLastError} can be used to retrieve
* the error code.
*
* @param dirs
* The list of directories to create.
*/
public static void mkdir(String[] dirs)
{
if (dirs == null)
{
return;
}
WorkArea wa = work.obtain();
// check the dir list for valid separator usage
for (int i = 0; i < dirs.length; i++)
{
dirs[i] = resolveLegacyPath(dirs[i], wa);
}
wa.lastError = wa.fs.mkdir(dirs);
}
/**
* Deletes each of the directories or files specified. If a non-empty
* directory name is specified, an error will occur unless the
* <code>recursive</code> flag is <code>true</code>. If any error occurs,
* no exception will be thrown, the deletion process just continues. The
* {@link #getLastError} can be used to retrieve the error code.
*
* @param list
* The list of files and directories to delete.
* @param recursive
* Recursively decend through all subdirectories, deleting the entire subtree.
*/
public static void delete(String[] list, boolean recursive)
{
if (list == null)
{
return;
}
WorkArea wa = work.obtain();
// check the dir list for valid separator usage
for (int i = 0; i < list.length; i++)
{
list[i] = resolveLegacyPath(list[i], wa);
}
wa.lastError = wa.fs.delete(list, recursive);
}
/**
* Get the file contents from the client-side file system.
*
* @param filename
* Name of the file in the client-side file system (relatively to the current
* directory on the client side).
*
* @return file contents as array of bytes.
*
* @throws java.io.IOException
* If file cannot not be found or read, or is too large.
*/
public static byte[] getFileContents(String filename)
throws IOException
{
WorkArea wa = work.obtain();
return wa.fs.getFileContents(resolveLegacyPath(filename, wa));
}
/**
* Renames (and moves if the path is different) the specified source file
* to the target filename (and path). If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file or directory.
* @param tar
* The name of the target file or directory. If this is a
* directory name for a directory that already exists and the
* <code>source</code> is a file, then the source file name will
* be used as the target file in the directory given.
*/
public static void rename(character src, character tar)
{
if (src == null || src.isUnknown() || tar == null || tar.isUnknown())
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
rename(src.toStringMessage(), tar.toStringMessage());
}
}
/**
* Renames (and moves if the path is different) the specified source file
* to the target filename (and path). If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file or directory.
* @param tar
* The name of the target file or directory. If this is a
* directory name for a directory that already exists and the
* <code>source</code> is a file, then the source file name will
* be used as the target file in the directory given.
*/
public static void rename(character src, String tar)
{
if (src == null || src.isUnknown() || tar == null)
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
rename(src.toStringMessage(), tar);
}
}
/**
* Renames (and moves if the path is different) the specified source file
* to the target filename (and path). If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file or directory.
* @param tar
* The name of the target file or directory. If this is a
* directory name for a directory that already exists and the
* <code>source</code> is a file, then the source file name will
* be used as the target file in the directory given.
*/
public static void rename(String src, character tar)
{
if (src == null || tar == null || tar.isUnknown())
{
setLastError(FileSystem.ERR_NO_ERROR);
}
else
{
rename(src, tar.toStringMessage());
}
}
/**
* Renames (and moves if the path is different) the specified source file
* to the target filename (and path). If any error occurs, no
* exception will be thrown. The {@link #getLastError} can be used to
* retrieve the error code.
*
* @param src
* The name of the source file or directory.
* @param tar
* The name of the target file or directory. If this is a
* directory name for a directory that already exists and the
* <code>source</code> is a file, then the source file name will
* be used as the target file in the directory given.
*/
public static void rename(String src, String tar)
{
WorkArea wa = work.obtain();
if (src == null || tar == null)
{
wa.lastError = FileSystem.ERR_NO_ERROR;
}
else
{
wa.lastError = wa.fs.rename(resolveLegacyPath(src, wa), resolveLegacyPath(tar, wa));
}
}
/**
* Search each directory listed in the {@link EnvironmentOps#getSearchPath} for the given file.
* If the file system object exists and is a file, then the filename is returned. If the path
* contains an empty string or a '.', then the current directory is searched, otherwise only
* the listed directories are searched.
* <p>
* Special processing is provided in case the original Progress code was checking for the
* availability of an external procedure. In this case the filename is checked using the
* {@link SourceNameMapper#convertName} method. If such the Java class exists, its legacy
* filename is returned. This will "fake" the converted code into thinking that the external
* procedure this still exists.
* <p>
* This method intentionally does not support the search of 4GL procedure libraries (either
* in the {@link EnvironmentOps#getSearchPath} or as an explicitly listed library). In the 4GL,
* SEARCH allows passing either a member to search for in the procedure libraries in the
* PROPATH, or a specific library plus member using the syntax "path-name<<member-name>>". In
* either case the 4GL returns the full path to the procedure library while this method does
* not. This is a known restriction.
*
* @param filename
* The absolute or relative filename for which to search.
*
* @return If the file is found in the current directory, the relative filename is returned.
* If the file is absolute or found in any other path, the full absolute filename is
* returned. If the file is not found or the referenced file system object is not a
* file, the {@code unknown value} is returned.
*/
public static character searchPath(character filename)
{
if (filename == null || filename.isUnknown())
{
return character.UNKNOWN;
}
return searchPath(filename.toStringMessage());
}
/**
* Search each directory listed in the {@link EnvironmentOps#getSearchPath} for the given file.
* If the file system object exists and is a file, then the filename is returned. If the path
* contains an empty string or a '.', then the current directory is searched, otherwise only
* the listed directories are searched.
* <p>
* Special processing is provided in case the original Progress code was checking for the
* availability of an external procedure. In this case the filename is checked using the
* {@link SourceNameMapper#convertName} method. If such the Java class exists, its legacy
* filename is returned. This will "fake" the converted code into thinking that the external
* procedure this still exists.
* <p>
* Application resource jars are searched before the remote file system.
* <p>
* This method intentionally does not support the search of 4GL procedure libraries (either
* in the {@link EnvironmentOps#getSearchPath} or as an explicitly listed library). In the 4GL,
* SEARCH allows passing either a member to search for in the procedure libraries in the
* PROPATH, or a specific library plus member using the syntax "path-name<<member-name>>". In
* either case the 4GL returns the full path to the procedure library while this method does
* not. This is a known restriction.
*
* @param filename
* The absolute or relative filename for which to search.
*
* @return If the file is found in the current directory, the relative filename is returned.
* If the file is absolute or found in any other path, the full absolute filename is
* returned. If the file is not found or the referenced file system object is not a
* file, the {@code unknown value} is returned.
*/
public static character searchPath(String filename)
{
if (filename == null || filename.isEmpty())
{
return character.UNKNOWN;
}
WorkArea wa = work.obtain();
// if the Progress code was checking for the availability of an external procedure, we
// have to "fake" it out into thinking that this still exists, try to match this given
// filename with a known program name, optionally honoring any absolute prefix passed in
String legacy = SourceNameMapper.lookupLegacyName(filename);
if (legacy != null)
{
// this is a program name, return the Java class name instead
return character.of(resolveLegacyPath(legacy, wa));
}
// avoid expensive client/jar files search for missing procedure names
if (!wa.ignoreClientSearch.isEmpty() && wa.ignoreClientSearch.contains(filename))
{
return character.UNKNOWN;
}
// check resource jar files first, before checking remote file system
JarUtil.JarResource jarRes = searchResourceJars(filename, null);
String result = (jarRes != null ? jarRes.getName() : wa.fs.searchPath(filename));
// the path entry was not found even on client side so if logging enabled, append new entry
if (result == null && wa.captureMissedFiles)
{
appendMissedPathEntry(filename);
}
return result == null ? character.UNKNOWN : character.of(result);
}
/**
* Search the resource jar files for the resource with the given filename, appending the
* extensions provided in the optional list, if present.
* <p>
* The algorithm first attempts to find an exact match for the filename. If not found, it
* applies the PROPATH to the filename, first prepending the package root to the name.
*
* @param filename
* Filename of the resource, optionally including a path.
* @param extensions
* Optional list of extensions to apply to the name. If this list is non-null,
* {@code filename} is assumed to be the file's base name, sans extension.
*
* @return A {@code JarResult} object, if the search succeeds, else {@code null}.
*/
public static JarUtil.JarResource searchResourceJars(String filename, List<String> extensions)
{
final char WIN_SEP = '\\';
final String JAR_SEP_STR = "/";
boolean caseSens = EnvironmentOps.getLegacyCaseSensitive();
String clientOS = EnvironmentOps.getRuntimeOperatingSystem().toJavaType();
String legacyOS = EnvironmentOps.getLegacyPlatform();
PathResolver pathResolver = null;
if (!legacyOS.equalsIgnoreCase(clientOS))
{
String fileSeparator = EnvironmentOps.getLegacyFileSeparator();
Map<String, String> pathMap = EnvironmentOps.getLegacyAbsolutePathPrefixMap();
pathResolver = Utils.getPathResolver(fileSeparator, caseSens, pathMap);
}
PathResolverContainer safePathResolver = new PathResolverContainer(pathResolver);
Function<JarClassLoader, Function<String, String>> coreFn = jcl -> (n -> jcl.getResourceName(n, caseSens));
// if no extensions were provided, do a straight search for the resource name in the jar;
// otherwise, concatenate each extension to the base resource name before searching the jar, and
// continue until we have a hit, or no more extensions to try
Function<JarClassLoader,Function<String, String>> searchFn =
extensions == null
? coreFn
: jcl -> (n -> Utils.tryWhileNull(extensions, e -> coreFn.apply(jcl).apply(n.concat(e))));
final Supplier<String[]> propathHeadersSuplier = Suppliers.memoize(EnvironmentOps::getSearchPathList);
Function<JarClassLoader, Function<String, String>> propathSearchFn = jcl -> n ->
{
// not found in root continue with PROPATH entries
String found = null;
final String[] propathHeader = propathHeadersSuplier.get();
for (String header : propathHeader)
{
header = safePathResolver.resolvePath(header);
// look through PROPATH related locations, clean up possible Windows separators
header = header.replace(WIN_SEP, JAR_SEP);
// inside jar we need to use UNIX-style file separator even on Windows
String name = Utils.getRefinedPathName(pkgRootPath + header + JAR_SEP + n, JAR_SEP_STR);
// try to search new name based on PROPATH header
found = searchFn.apply(jcl).apply(name);
if (found != null)
{
return found;
}
// try one more time, but using the root of the jar instead of pkgRootPath
found = searchFn.apply(jcl).apply(Utils.getRefinedPathName(header + JAR_SEP + n, JAR_SEP_STR));
if (found != null)
{
return found;
}
}
return found;
};
return JarUtil.searchResourceJars(LOG,
filename,
searchFn,
propathSearchFn,
StandardServer::getResourceJarPaths);
}
/**
* Parses a reference to 4GL r-code library and returns the simple library name.
*
* @param filename
* Pathname of the r-code library.
*
* @return The function parses a character string in the form
* <code>path-name<<member-name>></code>, where <code>path-name</code> is
* the pathname of a library and <code>member-name</code> is the name of a file within
* the library, and returns <code>path-name</code>.
*/
public static character library(String filename)
{
return library(character.of(resolveLegacyPath(filename, work.obtain())));
}
/**
* Parses a reference to 4GL r-code library and returns the simple library name.
*
* @param filename
* Pathname of the r-code library.
*
* @return The function parses a character string in the form
* <code>path-name<<member-name>></code>, where <code>path-name</code> is
* the pathname of a library and <code>member-name</code> is the name of a file within
* the library, and returns <code>path-name</code>.
*/
public static character library(character filename)
{
return procLibraryParser(filename, 1);
}
/**
* Parses a reference to a member of an 4GL r-code library and returns the simple member name.
*
* @param filename
* Pathname of the file in the r-code library.
*
* @return The function parses a character string in the form
* <code>path-name<<member-name>></code>, where <code>path-name</code> is
* the pathname of a library and <code>member-name</code> is the name of a file within
* the library, and returns <code>member-name</code>.
*/
public static character member(String filename)
{
return member(character.of(resolveLegacyPath(filename, work.obtain())));
}
/**
* Parses a reference to a member of an 4GL r-code library and returns the simple member name.
*
* @param filename
* Pathname of the file in the r-code library.
*
* @return The function parses a character string in the form
* <code>path-name<<member-name>></code>, where <code>path-name</code> is
* the pathname of a library and <code>member-name</code> is the name of a file within
* the library, and returns <code>member-name</code>.
*/
public static character member(character filename)
{
return procLibraryParser(filename, 2);
}
/**
* Get the type of its associated handle.
*
* @return Always return the PSEUDO-WIDGET value.
*/
public static character resourceType()
{
return character.of(LegacyResource.PSEUDO_WIDGET);
}
/**
* Get the current working directory.
*
* @return Current working directory.
*/
public static character getWorkingDirectory()
{
WorkArea wa = work.obtain();
String dir = wa.serverSideFs ? ConfigItem.WORKING_DIR.read(".") : wa.fs.getWorkingDirectory();
return character.of(dir);
}
/**
* Get the {@link com.goldencode.p2j.util.osresource.FileSystem#getFileSeparator() file-separator}.
*
* @return The file separator.
*/
public static String getFileSeparator()
{
return work.obtain().fs.getFileSeparator();
}
/**
* Helper to handle pattern matching for breaking up the parameters passed to {@link #library} and
* {@link #member}. Since the same pattern is used for both, this helper can optimize computation.
*
* @param name
* The path that is passed in to break up.
* @param group
* Group 1 is the library, group 2 is the member.
*
* @return Unknown character or the matched pattern.
*/
private static character procLibraryParser(character name, int group)
{
if (name != null && !name.isUnknown())
{
Matcher matcher = patternLibraryMember.matcher(name.toStringMessage());
if (matcher.matches())
{
return character.of(matcher.group(group));
}
}
return character.UNKNOWN;
}
/**
* Set the current working directory. Does nothing if argument represents unknown value.
*
* @param workDir
* New working directory.
*/
public static void setWorkingDirectory(character workDir)
{
if (workDir.isUnknown())
{
return;
}
work.obtain().fs.setWorkingDirectory(workDir.toStringMessage());
}
/**
* Sets the Progress search path on the TARGET P2J system which is used for
* the Progress compatible environment. This corresponds to the assignment of
* the "PROPATH" variable in Progress and represents a mechanism to override
* the default that is available from the directory.
*
* @param searchPath
* The new PROPATH value.
* @param wa
* The contextual work area.
*/
private static void setSearchPath(String searchPath, WorkArea wa)
{
// split the list
String[] list = EnvironmentOps.getSearchPathList();
if (list != null)
{
// use the resulting propath to set run time value on the client
wa.fs.setSearchPath(list, EnvironmentOps.getLegacyCaseSensitive());
}
}
/**
* Transforms the given legacy path into the valid client system path.
*
* @param legacyPath
* The legacy path name to the target resource.
* @param wa
* The contextual work area.
*
* @return The resolved host path on the client system
*/
private static String resolveLegacyPath(String legacyPath, WorkArea wa)
{
if (legacyPath == null || legacyPath.isEmpty())
{
return legacyPath;
}
if (wa.clientOS.equalsIgnoreCase("unix"))
{
return legacyPath.replaceAll(FileSystem.ANY_PATH_SEPARATOR, "/");
}
else
{
return legacyPath.replaceAll(FileSystem.ANY_PATH_SEPARATOR, "\\\\");
}
}
/**
* Appends yet another missed path entry to the log file. The path entry is missed on even client side
* search so it is a good candidate to include into bypass list.
*
* @param pathName
* Path name entry missed in client side search.
*/
private static void appendMissedPathEntry(String pathName)
{
// this will be in deploy/server directory
File inpFile = new File("./client_search_ignore.log");
// open log file for appending another missed path entry
try (PrintWriter log = new PrintWriter(new BufferedWriter(new FileWriter(inpFile, true))))
{
log.println(pathName);
}
catch (Exception ex)
{
// in case of exception here assume there is no ignore list file
}
// something seriously wrong happening but we ignore this
}
/**
* Stores global data relating to the state of the current context.
*/
private static class WorkArea
{
/** The resource's ID. */
private Long id = null;
/** Stores the Progress error code for the last operation. */
private int lastError = 0;
/** File information for the current file being examined. */
private FileInfo info = null;
/** Remote instance of our file system worker. */
private FileSystem fs = null;
/** Flag indicating the file-system is server-side. */
private boolean serverSideFs = false;
/** The legacy OS case-sensitive mode */
private boolean legacyCaseSensitive;
/** The environment variables map */
private Map<String, String> environmentVariables;
/** The client OS */
private String clientOS;
/** The client property key interceptors */
private Map<String, Function<String, String>> keysInterceptors;
/** The list of missed files to bypass client side search. */
private Set<String> ignoreClientSearch;
/** When TRUE we log missed files into separate log file. */
private boolean captureMissedFiles;
}
/**
* Simple container that stores and returns a context-local instance of
* the global work area.
*/
private static class ContextContainer
extends ContextLocal<WorkArea>
{
/**
* Obtains the context-local instance of the contained global work area.
*
* @return The work area associated with this context.
*/
public WorkArea obtain()
{
return this.get();
}
/**
* Initializes the work area, the first time it is requested within a new context.
*
* @return The newly instantiated work area.
*/
protected synchronized WorkArea initialValue()
{
WorkArea wa = new WorkArea();
// obtain file system resource
wa.serverSideFs = OSResourceManager.getInstance().isServerSide(OsResourceType.FILESYSTEM);
wa.fs = OSResourceManager.getFileSystem();
// push down our search path to ensure that it is always there
String[] paths = EnvironmentOps.getSearchPathList();
boolean caseSens = EnvironmentOps.getLegacyCaseSensitive();
wa.legacyCaseSensitive = caseSens;
// listener is added last... we already push the PROPATH above, so no need to notify;
// plus, it would lead to a double-initialization of this ContextContainer, as the
// listener (setSearchPath) uses the WorkArea.fs
EnvironmentOps.addSourcePathListener(searchPath -> setSearchPath(searchPath, wa));
String fileSeparator = EnvironmentOps.getLegacyFileSeparator();
Map<String, String> pathMap = EnvironmentOps.getLegacyAbsolutePathPrefixMap();
String clientOS = EnvironmentOps.getRuntimeOperatingSystem().toJavaType();
wa.clientOS = clientOS;
String legacyOS = EnvironmentOps.getLegacyPlatform();
boolean resolveFilePaths = EnvironmentOps.getResolveFilePaths();
// if legacy OS platform is not the same as client OS platform, then legacy path names
// must be resolved on the client OS.
boolean areLegacyPathsNotResolved = !legacyOS.equalsIgnoreCase(clientOS) ||
resolveFilePaths;
wa.fs.setLegacyFileSystemParameters(fileSeparator,
caseSens,
pathMap,
paths,
areLegacyPathsNotResolved);
wa.environmentVariables = EnvironmentOps.getLegacyEnvironmentVariables(!caseSens);
// add client property keys interceptors
wa.keysInterceptors = new HashMap<>();
Function<String, String> usernameInterceptor = new Function<String, String>()
{
/**
* Returns the OS dependent client user key if it applies to the legacy user key.
*
* @param key
* The given legacy user key
*
* @return The OS dependent client user key
*/
@Override
public String apply(String key)
{
if (wa.clientOS.equalsIgnoreCase("unix"))
{
return "USER";
}
return "USERNAME";
}
};
wa.keysInterceptors.put("USER", usernameInterceptor);
wa.keysInterceptors.put("USERNAME", usernameInterceptor);
// prepare per session ignore file list
Directory dir = DirectoryManager.getInstance();
wa.ignoreClientSearch = new HashSet<>(dir.getStrings(Directory.ID_RELATIVE,
"no_client_path_search"));
// missed files capturing flag
wa.captureMissedFiles = dir.getBoolean(Directory.ID_RELATIVE, "capture_missing_search_path_entries",
false);
return wa;
}
}
}