package nxm.sys.lib;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;

import nxm.sys.inc.AsciiMap;
import nxm.sys.inc.InternalUseOnly;

/**
   Class that supports the interaction of NeXtMidas with IO facilities.
   Supports reading and writing to and from most data sources including files,
   HTTP connections, JAR files, etc.

   @author Jeff Schoen
*/
public class CoreIO implements AsciiMap {
  /** Operational flags */
  public static final int UNTANGLE=1;

  /** File status: Return values used by the various <code>read</code> and <code>avail</code> methods to
                   indicate that no more data is available for reading. All of these are negative
                   values. A typical primitive should just check for a negative value and treat it
                   as an end-of-file. The differentiation here is for use within the i/o libraries.
      <pre>
        {@link #CLOSED} - End-of-file reached or the file has been closed.
        {@link #REOPEN} - Reopen required (this is typically used during a {@link Pipe} restart).
        {@link #ERROR}  - Unable to read due to an error (this is just used for debugging, some
                 just return {@link #CLOSED} in the event of an error).
      </pre>
   */
  public static final int CLOSED=-1, REOPEN=-2, ERROR=-3;

  /** Global I/O options enum (for setting in mask values). <br>
      <br>
      WARNING: Rename/remove the constants in this enum with care as it will "break"
               previously valid options for the IOOPTIONS environment variable
               unless they are annotated with @InternalUseOnly.
      @since NeXtMidas 2.9.0
   */
  // TODO: access to CoreIO.IOOptions should not be more restrictive than access to CoreIO.isOptionSet(IOOptions)
  static enum IOOptions { // package-private access for now
    /** Strictly follow HTTP 1.1 (RFC 2616) rules. E.g. Work around HTTP servers that return 200
        status code to a GET/POST request with Range header (with desire to read partial content),
        which will potentially break communication with NeXtMidas 2.8.2 and prior HTTP servers that
        did not correctly return a 206 status code.
      */
    StrictHTTP,

    /** When overwriting existing BLUE files, truncate them. See {@link DataFile#TRUNCATE}. */
    TruncateBLUE,

    /** Turn off the handling of escape characters in double quoted Strings using the backslash
        character (i.e. '\') just like Java/C String escape sequence.
        See {@link StringUtil#unescapeString(CharSequence)} for a list of escape sequences.
        Note: this does not apply to {@link Table#Table(String)}, {@link Table#Table(byte[])},
        {@link Table#Table(byte[], int, int)}, {@link Table#fromBytes(byte[], int, int)},
        {@link Table#toBytes()}, or {@link Table#append(String)}.
     */
    DisableEscapeSequences,

    /** Turn off the equality checks in {@link DataFile} that check quadwords in Type 5000 files.
        Setting this will enable the (buggy) behavior of NeXtMidas 3.2.0 and earlier which ignored
        quadwords in equality checks.
        @since NeXtMidas 3.3.0
     */
    DisableQuadwordsEqualityChecks,

    /** As of 3.9.4, this behavior is default: fix for PipeResources reversing the behavior of Timeline keywords being
     *  propagated along with other keywords to the output file.  PipeResource.propagateTimecode
     *  is handling the timeline updates each transfer length.  Having the timeline keywords in
     *  the file causes an initial erroneous timeline to be created and then for PipeResource to
     *  try to add to it starting with offset 0
     *  @since NeXtMidas 3.8.1
     */
    @Deprecated
    DoNotPropagateTimelineKeywordsInPipe,

    /** Revert back to previous behavior where in a pipe timeline keywords where both
     *  propagated along with other keywords and PipeResource.propagateTimecode also
     *  propagated timeline updates each transfer length.  Having the timeline keywords in
     *  the file causes an initial erroneous timeline to be created and then for PipeResource to
     *  try to add to it starting with offset 0
     *  @since NeXtMidas 3.9.4
     */
    DoublePropagateTimelineKeywordsInPipe,

    /** Prefer Platinum timeline keywords (high precision) over the
        NeXtMidas TIMELINE keyword (microsecond precision) for output of Midas BLUE 1.1 file.
        Reading will automatically choose the Platinum timeline keywords or TIMELINE keyword.
        @since NeXtMidas 3.3.0
     */
    PreferPlatinumTimeLine,

    /** Prefer BLUE TC (Time Code) keywords as outlined in the JICD 4.2 ICD
     *  @since NeXtMidas 3.7.2 NXM-3010
     */
    PreferBlueTCTimeLine,

    /** This fix is now the default, since 3.9.4
     *  @since NeXtMidas 3.8.1
     */
    @Deprecated
    FixPipeRestartTimeLines,

    /** Disable the fix for PipeResource's handling of timeline entries during pipe restarts
     *  @since NeXtMidas 3.9.4
     */
    DisablePipeRestartTimeLinesFix,

    /** Suppress errors/warnings about invalid TimeLine entries
     *  @since NeXtMidas 3.7.3 NXM-3040
     */
    SuppressInvalidTimeLineEntryErrors,

    /** Do not abort, even if the write triggered an exception
        @since NeXtMidas 3.5.4 Bug 2920
     */
    WriteNoAbort,

    /** Do Not Abort when an IOException occurs during File Write or Read
     *  (i.e. Revert to pre-NeXtMidas 3.7.1 behavior where writes and reads failed silently
     *  and at most returned a -1 indicating failure, rather than throwing a MidasIOException)
     *  @since NeXtMidas 3.7.1
     */
    FileIONoAbort,

    /** To go back to legacy behavior of using semi-colons in jar MANIFEST file as package list separator.
     *  @since NeXtMidas 3.9.0
     */
    UseSemiColonsInGenerateManifest,

    /** This will set JPanels to optimize the painting. This may cause graphical issues with overlapping panels.
     *  @since NeXtMidas 3.9.0
     */
    EnableGraphicsOptimizationRequiringNoOverlappingPanes,

    /** This will enable all improvements (as they are added) designed to make NeXtMidas follow Java behavior
     *  (ex: use Number classes instead of Data, handle reaching maximum values as Java does, etc.), rather
     *  than use the default pre-existing Midas behavior
     *  @since NeXtMidas 3.9.0
     */
    FavorJavaBehaviorOverTraditionalMidasBehavior,

    /** Continue to put all Numbers in a Double and do not use any o2n or s2n code improvements intended to
     *  put values in their proper Number class (making sure values fit without saturation and loss of precision)
     *  i.e avoid using Long, BigDecimal, or BigInteger to store results
     *  Note: As of NeXtMidas 3.9.0, Double is still the default when looking up a result or evaluating an expression.
     *  If the Object can be directly evaluated (ex: a Time or a numeric String), then the proper Number sub-class
     *  is chosen to ensure the value fits and precision is kept.
     *  @since NeXtMidas 3.9.0
     */
    DefaultToDoubleOnConvertO2nS2nEvenWhenPrecisionIsLost,

    /** This will allow previous versions of NeXtMidas to use a string value of the Boolean when reading in Shape Files.
     *  @since NeXtMidas 3.9.0
     */
    ShapeFileBooleanAsString,

    /** Force the class based logging to use the Midas Logger
     *  @since NeXtMidas 3.9.0
     */
    ClassBasedLoggerShouldUseMidasLogger,

    /** Disable the use of a Caret (^) in the value of the table to reference previous keys of the table. */
    DisableResultUsageWithCaretInTable,

    /** Switch regular Midas Logger to use LogUtlities */
    MidasLoggerShouldUseLogUtilities,

    /** Disable the auto-correction of BLUE file timelines, where the timeline keywords
     *  do not have an entry for offset 0. The auto-correction uses the file's timecode
     *  to set the time for the first element, so this does not cover all cases.
     *  Note: As of 3.9.4, this is back to being enabled by default (3.9.3 removed this)
     *  @since NeXtMidas 3.9.1
     */
    DisableTimeLineAutoCorrectNoOffset0Entry,

    /** Enable the ability of NeXtMidas to create a file without adding the extension .tmp
     *  @since NeXtMidas 3.9.1
     */
    CreateFileWithoutTmpExtenstion,

    /** Enable the creation of a file without changing the case of the file
     *  @since NeXtMidas 3.9.1
     */
    CreateFileMaintainCase,

    /** Used to set the option for FLAT in the Keyword intrinsic to false. Caution, this may be unsafe.
     *  @since NeXtMidas 3.9.1
     */
    SetKeywordDefaultFlatValueToFalse,

    /** By default, volatile keywords are propagated. Use this option to turn that off.
     *  @since NeXtMidas 3.9.1
     */
    DoNotPropagateVolatileKeywords,

    /** Remove the functionality of calling external macros.
     *  @since NeXtMidas 3.9.2
     */
    DoNotAllowExecutionOfMacroOutsideOfHomepath,

    /** Add Minimal support for some unsupported Data types
     *  @since NeXtMidas 3.9.3
     */
    MinimalSupportForUnsupportedDataTypes,

    /** Add option to propagate timeline keywords without any modification
     *  @since NeXtMidas 3.9.3
     */
    SameTimelineKeywords,
  };

  /** default I/O options enum mask */
  private static final MaskValue<IOOptions> DEF_IOOPTIONS_OPTIONS = MaskValue.constant(IOOptions.class); // default is none

  /** I/O options for this CoreIO instance */
  private MaskValue<IOOptions> ioOptions;

  /** The midas context that this object is operating within. */
  private Midas M;

  /** Create a new instance that is not associated with any Midas context. <br>
   *  <br>
   *  This constructor should only be used by {@link Midas} and {@link BaseFile}, the proper way to get
   *  an instance of {@link CoreIO} is to get a {@link Midas} reference and then call {@link Midas#getCoreIO()}.
   */
  @InternalUseOnly("This should only be used by Midas and BaseFile")
  public CoreIO () {
    ioOptions = DEF_IOOPTIONS_OPTIONS.copy();
  }

  /** Create a new instance that is associated with the given Midas context. <br>
   *  <br>
   *  This constructor should only be used by {@link Midas} and {@link BaseFile}, the proper way to get
   *  an instance of {@link CoreIO} is to get a {@link Midas} reference and then call {@link Midas#getCoreIO()}.
   *  @param M the target Midas context.
   */
  @InternalUseOnly("This should only be used by Midas and BaseFile")
  public CoreIO (Midas M) {
    this();
    this.M = M;
    if (M.io != null) {
      ioOptions = M.io.ioOptions;
    }
  }

  /** Searches the Midas aux list for the named IO resource.
      If it exists, the search stops.  The last search resource is always returned.
      @param filename The file name
      @param dir      File I/O direction. See "Direction Values" at the top of {@link IOResource}
                      for details.
      @return The applicable IOResource or null if not found.
   */
  public IOResource findResource (Object filename, int dir) {
    return findResource( filename, dir, "", "");
  }

  /** Searches the Midas aux list for the named IO resource.
   *  If it exists, the search stops.  The last search resource is always returned.
   *
   * @param filename The file name
   * @param dir      File I/O direction. See "Direction Values" at the top of {@link IOResource}
   *                  for details.
   * @param ext      The extension
   * @param aux      The AUX
   * @return The applicable IOResource or null if not found.
   */
  public IOResource findResource (Object filename, int dir, String ext, String aux) {
    return findResource(filename, dir, ext, aux, false);
  }
  /** Searches the Midas aux list for the named IO resource with optional extension and aux. If it
      exists, the search stops. The last search resource is always returned.
      @param filename The name of the file (may contain full path and with any qualifiers).
                      If pointing to a directory, it should be path terminated (e.g. ends with "/" under Unix).
      @param dir      File I/O direction. See "Direction Values" at the top of {@link IOResource} for details.
      @param ext      The file name extension (if this is <code>null</code> or the empty
                      String the extension will be taken from the file name).
      @param aux      The Aux to search. (If this is the empty string and the filename
                      starts with an underscore (<code>'_'</code>) then the "ram:" aux
                      will be used. If this is <code>null</code> or the empty string
                      then the read aux and write aux will be searched.)
      @return The applicable IOResource or null if not found.
   */
  IOResource findResource (Object filename, int dir, String ext, String aux, boolean doNotAddExtension) {
    IOResource res    = null;
//*  In 3.7.0, reverted back to single line below since it is same as next 3 lines
    //         plus we would only need to change FileName.toFileName(..) vs there and here
    FileName   fn     = FileName.toFileName(filename);
/*/ // TODO: delete the next block of code (~3 lines) in 3.9+
    // In 2.9.0 Getting the filename changed to the method below to account for a FileName passed in (Bug 1691)
    FileName   fn;
    if (filename instanceof FileName) fn = (FileName)filename;
    else fn = new FileName(Convert.o2s(filename));
//*/
    Table      quals  = fn.getQualifierTable();
    String     path   = fn.getPath();
    String     root   = fn.getRoot();
    boolean    noPath = ((path == null) || path.isEmpty());

    if (ext==null || ext.length()==0)  ext = fn.getExt(); // extension override
    if (aux!=null && aux.length()!=0) path = getAuxPath(aux)+path; // Since 3.3.0, we add to the given path instead of replacing it
    if (noPath && root.startsWith("_")) path="ram:"; // auto pipe path
    if (path!=null && path.length()>0) { // path specified
      if (path.startsWith("./")) {
        path = getAuxPath("CWD")+path.substring(2);
      }
      res = findResource(path,root,ext,quals, dir,true, doNotAddExtension);
    }
    else {                      // no path specified - search aux list
      Table taux = M.results.getTable("AUX");
      String waux = taux.getString("WRITE");
      if ((dir > 0) && waux.startsWith("-")) {
        waux = waux.substring(1);
        // If the write aux is "negative" and the file is output only do not look at the
        // read aux (note that files opened to be modified/appended still result in a search of the read aux,
        // as is done in X-Midas)
        res = findResource(getAuxPath(waux), root, ext, quals, dir, true, doNotAddExtension);
      }
      else {
        String raux = taux.getString("READ");
        // Parser auxes = new Parser(raux+"|"+waux);
        Parser auxes = new Parser(raux);
        auxes.setDelimiter('|');
        for (auxes.reset(); auxes.more() && res==null; ) {
          aux = auxes.next();
          path = getAuxPath(aux);
          res = findResource(path,root,ext, quals, dir,false, doNotAddExtension);
        }
        if (res==null) res = findResource(getAuxPath(waux),root,ext,quals, dir,true, doNotAddExtension);
      }
    }
    // if nothing exists, get the last resource searched

    return res;
  }

  /**
   * Helper method to create the proper path. In some cases, the trailing slash is not in the path.
   * That slash gets added so that the path and root are combined correctly.
   *
   * Edge Case: root = "ram:" we check to see if there is anything after the ":", if not, we do path + root.
   * Example:
   *
   * <pre>
   *    | path                                 | root                | return                                      |
   *    +--------------------------------------+---------------------+---------------------------------------------+
   *    | "classpath:nxm/sys/dat/"             | "apenny"            | classpath:nxm/sys/dat/apenny"               |
   *    +--------------------------------------+---------------------+---------------------------------------------+
   *    | "classpath:nxm/sys/dat"              | "apenny"            | classpath:nxm/sys/dat/apenny"               |
   *    +--------------------------------------+---------------------+---------------------------------------------+
   *    | "classpath:"                         | "apenny"            | classpath:apenny"                           |
   *    +--------------------------------------+---------------------+---------------------------------------------+
   *    Note: when there are no directories after the prefix, the trailing slash is not added.
   *    Note: when it is a tar file, the trailing slash should not be added. Tar files use ! at the end while jar files use !/ at the end.
   *   </pre>
   * @param path The path of the file
   * @param root The name of the file less the path and extension.
   * @return full path with root properly post-pended
   */
  private String createClasspathPath(String path, String root) {
    //This is to fix any issues when running in windows. The slashes of course face the other direction.
    path = path.replace("\\","/");
    String[] paths = path.split(":");
    //skip adding the slash if it is only the type (ex: classpath:)
    if(paths.length>1) {
      return (path.endsWith("/")) ? path+root : path+"/"+root;
    }
    return path+root;
  }

  /** Searches the Midas aux list for the named IO resource with optional extension and aux. If it
      exists, the search stops. The last search resource is always returned. Note that the full URL
      of the resource is equal to <code>path+root+ext</code>. This is the method that handles adding
      a "tmp" or "prm" extension if the user does not specify one (i.e. <code>ext==""</code>).
      @param path   The path of the file.
      @param root   The name of the file less the path and extension.
      @param ext    The file name extension (may be the empty string but may not be null).
      @param quals  Table of file qualifiers.
      @param dir    File I/O direction. See "Direction Values" at the top of {@link IOResource}
                    for details.
      @param create Set this to true to indicate that the file is being created. (It is common
                    practice among Midas systems to overwrite an existing file with the same
                    name before trying to create a new file. Thus when this is true the method
                    searches for an existing file with the given name before it will attempt
                    to create a new file.)
      @return The applicable IOResource or null if not found.
   */

  public IOResource findResource (String path, String root, String ext,
      Table quals, int dir, boolean create) {
    return findResource(path, root, ext, quals, dir, create, false);
  }

  /** Searches the Midas aux list for the named IO resource with optional extension and aux. If it
      exists, the search stops. The last search resource is always returned. Note that the full URL
      of the resource is equal to <code>path+root+ext</code>. This is the method that handles adding
      a "tmp" or "prm" extension if the user does not specify one (i.e. <code>ext==""</code>).
      @param path   The path of the file.
      @param root   The name of the file less the path and extension.
      @param ext    The file name extension (may be the empty string but may not be null).
      @param quals  Table of file qualifiers.
      @param dir    File I/O direction. See "Direction Values" at the top of {@link IOResource}
                    for details.
      @param create Set this to true to indicate that the file is being created. (It is common
                    practice among Midas systems to overwrite an existing file with the same
                    name before trying to create a new file. Thus when this is true the method
                    searches for an existing file with the given name before it will attempt
                    to create a new file.)
      @param doNotAddExtension set to true so that .tmp is not added by default for file without extension
      @return The applicable IOResource or null if not found.
   */
  private IOResource findResource (String path, String root, String ext,
                                   Table quals, int dir, boolean create, boolean doNotAddExtension) {
    IOResource res = null;
    boolean exists = false;
    int lroot = root.length(); // length of root
    int lext  = ext.length();  // length of ext

    // NOTE: The following does not correctly form a URL (see Bug 83). It can result in a file name
    //       (e.g. "c:\foo\bar.jar" on Windows or "/foo/bar.jar" on Unix) when a fully-qualified
    //       URL is required. This causes problems for some of the underlying Java I/O routines;
    //       it shows up more often on Windows since the slashes on Unix match those of a URL.
    //       See IOResource.setURL(..) for one instance where we have code to workaround this issue.
    boolean isClasspathUrl = path.toLowerCase().startsWith("classpath:");
    String url = (isClasspathUrl) ? createClasspathPath(path,root) : (path + root);

    int type = IOResource.getType(url);
    if (type==IOResource.FILE  ||
        type==IOResource.NFILE ||
        type==IOResource.RAMD) {
      if (lroot==0) {
        exists = fexists(url);
      }
      else if (lext>0) {
        // Add the extension to the end of the file only if it is not already there
        if ( !url.endsWith("."+ext) ) { url = url+"."+ext; }
        exists = fexists(url);
      }
      else {
        exists = fexists(url);     // 1st try without extension (Bug 1584)
        if (!exists) {
          url = path+root+".prm";  // 2nd try with .prm extension
          exists = fexists(url);
        }
        if (!exists) {
          url = path+root+".tmp";  // 3rd try with .tmp extension
          exists = fexists(url);
        }
      }
      if (exists) res = IOResource.getInstance(M,type,url,dir, quals);
      else if (create) {
        if(doNotAddExtension) {
          url = path+root;
        }
        res = IOResource.getInstance(M,type,url,dir, quals);
      }
    }
    else if (type==IOResource.HTTP
          || type==IOResource.URL    // since 3.3.0: added URL resource for BUG 2484
          || type==IOResource.JAR) { // since 3.3.0: changed JAR resource to have same behavior as HTTP
           if (lroot == 0)         { url = path;              }
      else if (lext  >  0)         { url = path+root+"."+ext; }
      else {
        url = path+root;           // 1st try without extension (Bug 176)
        res = IOResource.getInstance(M,type,url,dir,quals);
        exists = res.exists();
        if (!exists) {
          url = path+root+".prm";  // 2nd try with .prm extension
          res.setURL(url);
        }
      }
      if (res==null) res = IOResource.getInstance(M,type,url,dir,quals);
      if (!exists && !res.exists() && !create) res = null;
    }
//    else if (type==IOResource.JAR) { // prior to 3.3.0 (added to above to have same behavior as HTTP resources)
//      if (lext ==0) ext="prm";
//      if (lroot==0) url=path;
//      else url = path+root+"."+ext;
//      res = IOResource.getInstance(M,type,url,dir, quals);
//      if (!res.exists() && !create) res = null;
//    }
    else if (type==IOResource.RAM) {
      if (lext>0 && lroot>0) root = root+"-"+ext;
      if (lroot==0) exists=true; // container
      else exists = M.pipes.containsKey(root);
      if (exists||create) res = IOResource.getInstance(M,type,path+root,dir, quals);
    }
    else if (type==IOResource.PIPE) {
      if (lext>0 && lroot>0) root = root+"-"+ext;
      if (lroot==0) exists=true; // container
      else exists = M.pipes.containsKey(root.toUpperCase());
      if (exists||create) res = IOResource.getInstance(M,type,path+root,dir, quals);
    }
    else { // includes (type==IOResource.USER)
      if (lroot>0 && lext>0) url = url+"."+ext;
      res = IOResource.getInstance(M,type,url,dir, quals);

      if ((res != null) && !res.exists() &&  lext==0 &&
          (type==IOResource.USER) && path.toLowerCase().startsWith("classpath")) {
        // Add extra tries checking for .prm and .tmp, when there is no
        // extension specified and we are dealing with the 'classpath:'
        // prefix, that allows discovery of resources along the classpath.
        // They are included as an #IOResource.USER type
        // Since 3.7.2 for NXM-3022
        url = path+root+".prm";  // 2nd try with .prm extension
        res = IOResource.getInstance(M,type,url,dir, quals);
        if ((res != null) && !res.exists()) {
          url = path+root+".tmp";  // 3rd try with .tmp extension
          res = IOResource.getInstance(M,type,url,dir, quals);
        }
      }
      if ((res != null) && !res.exists() && !create) res = null;
    }
    return res;
  }

  /** Check for the existence of a directory
      @param url the URL of the directory to check.
      @return true if exists, false otherwise.
   */
  public boolean dexists (String url) {
    return fexists(url,true);
  }

  /** Check for the existence of a file
      @param url the URL of the file to check
      @return true if exists, false otherwise.
   */
  public boolean fexists (String url) {
    return fexists(url,false);
  }

  /**
   *
   * This is a helper function to fexists() when the url begins with "classpath:".
   * When using IOResource.getInstance().getURL(), the "classpath:" will be replaced with the actual path. If it is not, the file was not found.
   * In cases where it could be a directory, we test with the trailing slash as well. If the getURL() returns a path with "classpath:" starting, then it would be a file, otherwise it id a directory.
   * Note: is there a use case to make this public, it has to be static in the manner it is executed.
   * @param url - the url of the file/directory
   * @param checkDir - check it is a directory = true, check if file = false
   * @return true if file exists in classpath
   * @since NeXtMidas 3.8
   */
  private static boolean fexistsClasspath (String url, boolean checkDir) {

    String urlTemp = IOResource.getInstance(Shell.getMidasContext(), IOResource.UNKNOWN, url, 0, null).getURL();
    //if classpath: is still in the url then the file/dir was not found
    if (urlTemp.toLowerCase().startsWith("classpath:")) {
      return false;
    }

    if (urlTemp.endsWith("/")) {
      // If it is ending with a "/" it has to be a directory. If it is found, return checkDir.
      return checkDir;
    }

    // Try adding a slash and running the same check again, if it is a directory it should still find it,
    // otherwise it will not.
    urlTemp = url + "/";
    urlTemp = IOResource.getInstance(Shell.getMidasContext(), IOResource.UNKNOWN, urlTemp, 0, null).getURL();
    // if after adding the slash, the file/directory is no longer found (begins with "classpath:") then it has to be
    // a file.
    return urlTemp.toLowerCase().startsWith("classpath:") == !checkDir;

  }
  /** Check for the existence of a file or directory.
    @param url Input string for file, URL or directory
    @param checkDir if true, then check if url is a directory, otherwise check if it is a file
    NOTE: In NeXtMidas 2.7.3 and prior, this method incorrectly returned TRUE
          when url is an existing directory and checkDir is false. NeXtMidas 2.7.4 returns
          FALSE since the url is NOT a file in this scenario.
    @return TRUE if url is a file that exists and checkDir is false
            TRUE if url is a directory that exists and checkDir is true
            FALSE otherwise
    */
  public static boolean fexists (String url, boolean checkDir) {
    int i = url.indexOf(':');
    if (url.toLowerCase().startsWith("classpath:")) {  // Since 3.8.0 to support 'classpath:..." URLs
      return fexistsClasspath(url, checkDir);
    }
    if ( i > 0 ) {
      String prefix = url.substring(0, i + 1).toUpperCase();
      boolean isRoot = false;

      /* JC4: Java/Windows bug causes "No Disk" error if File.listRoots() is called from
              within WebStart. (DR 530241-1)
       */
      File[] roots;
      if (isWindows()) {
        roots = WindowsAltFileSystemView.listRoots();
      } else {
        roots = File.listRoots();
      }
      for (int j = 0; !isRoot && j < roots.length; j++) {
        String root = ("" + roots[j]).toUpperCase();
        isRoot = root.startsWith(prefix);
      }

      if ( !isRoot ) url = url.substring(i+1);
    }
    File f = new File(url);
    boolean retVal;
    try {
//ORIG      retVal = f.exists();                              // NeXtMidas 2.7.3 and prior
//ORIG      if (checkDir && retVal) retVal = f.isDirectory(); // NeXtMidas 2.7.3 and prior
      if (checkDir) retVal = f.isDirectory();  // check if directory exists (fix for Bug 2076)
      else          retVal = f.isFile();       // check if file exists      (fix for Bug 2076)
    } catch(java.lang.SecurityException e) {
      // This exception was common when running via an applet (applet support has been removed).
      retVal = false;
    }
    return retVal;
  }

  /** Indicates if this is a Windows platform.
      @return Indicates if this is a Windows platform.
   */
  private static boolean isWindows() {
    return "DOS".equals(Shell.ostype);
  }

  /** Creates a file name in a system independent manner. This is the same as:
      <code>getName("SYS",area,name,ext)</code>.
      @param area Area of the "SYS" option tree.
      @param name Name of the file.
      @param ext  File name extension.
      @return The file name.
      @deprecated Since NeXtMidas 3.3.1 - This function was for internal use only
                  (it always assumes "SYS") and is no longer used.
   */
  @InternalUseOnly
  @Deprecated
  public String name (String area, String name, String ext) {
    return getName("SYS",area,name,ext);
  }

  /** Creates a file name in a system independent manner. This is the same as:
      <code>getName(opt,area,name,ext)</code>.
      <b>
        WARNING: This method does not properly handle mixed-case file names and
        will be deprecated in the next release. Please use {@link FileName#getFileName}
        in place of this method.
      </b>
      @param area Area of the "SYS" option tree.
      @param name Name of the file.
      @param ext  File name extension.
      @param opt  The option tree.
      @return The file name.
   */
  public String name (String area, String name, String ext, String opt) {
    return getName(opt,area,name,ext);
  }

  /** Creates a file name in a system independent manner. <br>
      <br>
      <b>
        WARNING: This method does not properly handle mixed-case file names and
        will be deprecated in the next release. Please use {@link FileName#getFileName}
        in place of this method.
      </b>
      @param opt  The option tree (use "AUX" to get file from {@code <area>} AUX)
      @param area Area (sub-directory) of the option tree -or- AUX name when {@code <opt>} is "AUX"
      @param name Name of the file.
      @param ext  File name extension.
      @return The file name.
   */
  public String getName (String opt, String area, String name, String ext) {
    String path,fname;
    if (opt.equals("AUX")) path = getAuxPath(area);
    else path = Shell.getOptPath(opt,area);

    if (name==null) return path;

    // if name is not mixed case, lowercase it
    if (name.equals(name.toUpperCase())) name = name.toLowerCase();
    fname = path + name;

    // add the filename extension, always lowercase
    if (ext != null && ext.length()>0) fname += "."+ext.toLowerCase();

    return fname;
  }

  /** Gets a listing of all available AUX entries. (Explicitly excludes "READ" and "WRITE".)
      @return The names of each of the AUX entries.
      @since NeXtMidas 2.5.0
   */
  public String[] getAuxList() {
    String[] keys = M.results.getTable("AUX").getKeys();
    ArrayList<String> list = new ArrayList<>(keys.length - 2);

    for (String aux : keys) {
      if (aux.equals("READ") || aux.equals("WRITE") || (aux.startsWith("_") && aux.endsWith("_"))) {
        // Skip special entries
      } else {
        list.add(aux);
      }
    }
    return list.toArray(StringUtil.EMPTY_STRING_ARRAY);
  }

  /** Gets the current write AUX (null if none set).
      @since NeXtMidas 2.5.0
      @return the current WRITE AUX, or null if not set.
   */
  public String getWriteAux() {
    String write = M.results.getTable("AUX").getS("WRITE");
    return ((write == null) || (write.length() == 0))? null: write;
  }

  /** Gets a listing of all available AUX entries. (Explicitly excludes "READ" and "WRITE".)
      @return The names of each of the AUX entries.
      @since NeXtMidas 2.5.0
   */
  public String[] getReadAux() {
    String   read   = M.results.getS("AUX.READ","");
    Parser   parser = new Parser(read,'|');
    String[] list   = new String[parser.elements()];
    for (int i = 0; i < list.length; i++) {
      list[i] = parser.get(i+1);
    }
    return list;
  }

  /** Creates an auxiliary path in a system independent manner. This is the same as
      <code>getAuxPath(aux,true)</code>.
      @param aux The AUX name.
      @return an auxiliary path in a system independent manner via {@link #getAuxPath(java.lang.String, boolean)}
   */
  public String getAuxPath (String aux) {
    return getAuxPath(aux,true,true);
  }

  /** Creates an auxiliary path in a system independent manner. This handles many special cases,
      such as removal of parameters on an AUX entry (common for ramdisk), substitution of user
      name for '+', terminating path for File resources.
      @param aux   The AUX name.
      @param clean Substitute the user name for any occurrences of '+', remove parameters, etc.
                   (Note: Path will be terminated even if clean is false.)
      @return The path.
   */
  public String getAuxPath (String aux, boolean clean) {
    return getAuxPath(aux,clean,true);
  }

  /** Creates an auxiliary path in a system independent manner. This handles many special cases,
      such as removal of parameters on an AUX entry (common for ramdisk), substitution of user
      name for '+', terminating path for File resources.
      FYI: for "RAM" aux, this returns "ram:".
      @param aux   The AUX name.
      @param clean Substitute the user name for any occurrences of '+', remove parameters, etc.
                   (Note: Path will be terminated even if clean is false.)
      @param trans Do translation of aux alias.
      @return The path.
   */
  String getAuxPath (String aux, boolean clean, boolean trans) { // package-private, system use only
    if (trans) aux = XmUtil.checkAuxName(aux); // Handles translation in case of AUX numbers.
    Table taux = M.results.getTable("AUX");
    if (taux==null) return null;
    String path = taux.getString(aux);
    // Since 3.5.4 Get actual WRITE aux path (not just AUX it is set to)
    if (aux.equals("WRITE")) {
      path = taux.getString(path);
    }
    if (path==null) return null;
    // JC4 (21SEPT06): Catch the case where a AUX is not properly terminated.
    if ((IOResource.getType(path) == IOResource.FILE) && !path.endsWith(File.separator)) {
      path = FileName.terminatePath(path);
    }
    if (clean) {
      int j = path.indexOf(',');
      if (j>0) path = path.substring(0,j);
      int i = path.indexOf('+');
      if (i<0) return path;
      String user = M.results.getString("ENV.USER").toLowerCase();
      path = path.substring(0,i) + user + path.substring(i+1);
    }
    return path;
  }

  /** Creates an option path in a system independent manner. This is identical to
      {@link Shell#getOptPath(String,String)}.
      @param opt   The name of the option tree.
      @param area  The area in the option tree.
      @return The path to the given area of the option tree.
   */
  public String getOptPath (String opt, String area) {
    return Shell.getOptPath(opt,area);
  }

  /** One liner filename form and existence test.
      @deprecated Since NeXtMidas 3.3.1 - This function was for internal use only
                  (it always assumes "SYS") and is no longer used.
      @param area Area (sub-directory) of the option tree -or- AUX name when &lt;opt&gt; is "AUX"
      @param name Name of the file.
      @param ext  File name extension.
      @return true if the file exists, false if not.

   */
  @InternalUseOnly
  @Deprecated
  public boolean exists (String area, String name, String ext) {
    IOResource res = findResource(getName("SYS",area,name,ext), -1, "", "");
    return res.exists();
  }

  /** one liner existence test for a URL.
      @param url the URL to test.
      @return true if the resource exists, false if not.
   */
  public boolean exists (String url) {
    IOResource res = findResource( url, -1, "", "");
    return res.exists();
  }

  /** One liner filename find resource and existence test. See also,
      {@link #findResource(Object, int)} with a dir of {@link IOResource#DIR_INPUT}
      and {@link IOResource#exists()}.
      @param filename file name to find and check if it exists
      @return true if the file exists, false if not.
  */
  public boolean find (String filename) {
    IOResource res = findResource( filename, IOResource.DIR_INPUT);
    return res.exists();
  }

  /** List all files in the named directory that will match the given criteria on the
      name and extension.  If no file list can be generated then null will be returned.
      If ext="dir", this filters on all directories.
      See {@link #listFiles(Object, FilenameFilter)}.
      @param path directory to list files
      @param name file name of filter to use
      @param ext  file extension of filter to use.
      @return     String array of files for the specified path and filename filter
  */
  public String[] listFiles (Object path, String name, String ext) {
    // NTN: moved most of code to {@link #listFiles(String, FilenameFilter)}.
    MatchCriteria filter = new MatchCriteria(name,ext);
    return listFiles(path, filter);
  }

  /** List all files in the named directory that matches the given FilenameFilter
      @param path directory to list files
      @param filter filename filter to use
      @return String array of files for the specified path and filename filter
      @since NeXtMidas 2.5.0
   */
  public String[] listFiles (Object path, FilenameFilter filter) {
    if (path==null) return null;
    IOResource res = findResource (path,-1,"","");
    // JC4 (26SEPT06): The default implementation of res.getEntryList(..) returns null anyhow.
    //                 By allowing it to be called at all times allows any "special" ones to
    //                 list files even though they aren't explicit containers (e.g. pipes).
    //if (res==null || !res.isContainer()) return null;
    if (res==null) return null;
    return res.getEntryList(filter);
  }

  /** get Tag
   *
   *  @param name Tag
   *  @return input value (Tag)
   */
  public String getTag (String name) {
    String tag = name;
    return tag;
  }

  /** Uses criteria for the name and extension to determine if the file passes */
  public static class MatchCriteria implements FilenameFilter {

    private String nameList, extList;
    private Parser parsedName, parsedExt;
    private boolean excludeName=false, excludeExt=false;
    private boolean dir=false, dirExists=false;

    /** default Constructor for MatchCriteria */
    public MatchCriteria () {
      setCriteria("","");
    }

    /** Constructor for MatchCriteria
    *
    *  @param name       name
    *  @param ext        extension
    */
    public MatchCriteria (String name, String ext) {
      setCriteria(name,ext);
    }

    /** Constructor for MatchCriteria
     *
     *  @param name       name
     *  @param ext        extension
     *  @param dirExists  true if matching a directory is acceptable
     */
    public MatchCriteria (String name, String ext, boolean dirExists) {
      setCriteria(name,ext);
      this.dirExists = dirExists;
    }

    /** Return true if the file in the specified directory meets the criteria */
    public boolean accept (File path, String name) {
      String lcname = name.toLowerCase();
      if (dirExists) {
        File fut = new File(path,name);
        if (fut.isDirectory()) return true;
      }
      if (excludeName == passFN(lcname)) return false;
      if (!dir && (excludeExt == passEXT(lcname))) return false;
      if (path != null) { // path==null for http:/ram:/xmpipe:/etc.
        File fut = new File(path,name);
        if (!fut.exists()) return false;
        if (dir != fut.isDirectory()) return false;
      }
      return true;
    }

    /** Sets the name and extension criteria of the instance.
        @param name the name to set.
        @param ext  the extension to set.
     */
    public void setCriteria (String name, String ext) {
      setMatchName(name);
      setMatchExt(ext);
    }

    /** Sets what to return if directory is encountered
        @param state the state value to set.
     */
    public void setDirExists (boolean state) {
      dirExists = state;
    }

    /** Set the name portion of the match criteria.
        @param name the name to set.
     */
    public void setMatchName (String name) {
      nameList = name.toLowerCase();
      excludeName = nameList.startsWith("-");
      if (excludeName) nameList = nameList.substring(1);
      parsedName =  new Parser(nameList,'|');
    }

    /** Set the extension portion of the match criteria.
        @param ext  the extension to set.
     */
    public void setMatchExt (String ext) {
      extList = ext.toLowerCase();
      dir = extList.equals("dir");
      excludeExt = extList.startsWith("-");
      if (excludeExt) extList = extList.substring(1);
      parsedExt =  new Parser(extList,'|');
    }

    /** Determines if file should be included in list based on filename
        @param name the file name to check.
        @return true if the file should be included, false otherwise.
     */
    protected boolean passFN (String name) {
      if (nameList.equals("all")) return true;
      if (nameList.equals("*")  ) return true;
      if (nameList.isEmpty()    ) return true;
      int i = name.lastIndexOf('.');
      if (i>=0) name = name.substring(0,i);
      for (parsedName.reset(); parsedName.more(); ) {
          String criteria = parsedName.next();
          if (StringUtil.isWildcardMatch(criteria, name)) return true;
      }
      return false;
    }

    /** Determines if file should be included in list based on extension. If
        the matchext member is an empty string and the ending of the name
        is either ".prm" or ".tmp" the method will return true. Otherwise
        the name * is parsed and compared against the matchext to return true.
        @param name the extension to check.
        @return true if the file should be included, false otherwise.

    */
    protected boolean passEXT (String name) {
      boolean noExt = (extList.length() == 0);
      if (extList.equals("all")) return true;
      if (extList.equals("*")) return true;
      int i = name.lastIndexOf('.');
      if (i<0) return noExt; // no extension
      name = name.substring(i+1);
      if (noExt && (name.equals("prm") || name.equals("tmp") || name.equals("txt"))) return true;
      for (parsedExt.reset(); parsedExt.more(); ) {
        String criteria = parsedExt.next();
        if (StringUtil.isWildcardMatch(criteria, name) ) return true;
      }
      return false;
    }
  }

  /** Rename the file. This only works with files on the local disk.
    @param fn1 The file to rename
    @param fn2  The destination file name
    @return true if successfully renamed, false otherwise.
  */
  public static boolean rename (String fn1, String fn2) {
    if (fn1.startsWith("file:")) fn1 = fn1.substring(5);
    if (fn2.startsWith("file:")) fn2 = fn2.substring(5);
    File f1 = new File(fn1);
    File toDelete = f1;
    File f2 = new File(fn2);
    boolean renamed = f1.renameTo(f2);
    if (!renamed) {
      try {
        FileUtil.copyBaseFile(null, f1, f2);
        renamed = true;
      } catch (Exception e) {
        Shell.printStackTrace("Copy attempt failed for rename across file systems",e);
      }
    }
    //This line was added because java.io.File.renameTo() deletes the original file after
    //renaming in a platform dependent way (deletes it on Unix, does not on Windows).
    //So that NeXtMidas code has a predictable outcome, always delete the original file
    //if the rename was successful so that Windows doesn't leave files in write aux.
    if (renamed && toDelete.exists()) toDelete.delete();
    return renamed;
  }

  /** Check for directory name
    @param pathname The directory path
    @return true if the given path is a directory, false if not.
  */
  public static boolean isDirectoryName (String pathname) {
    boolean isDir;
    File tempFile = new File(pathname);

    try {
      if (tempFile.exists()) {
        isDir = tempFile.isDirectory();
      } else {
        // A path name is a file name if it ends with a path separator character i.e. / for UNIX
        isDir = pathname.endsWith(System.getProperty("file.separator"));
      }
    } catch (Exception e) { // JC4: If anything happens fall back to the old behavior.
      isDir = pathname.endsWith(System.getProperty("file.separator"));
    }
    return isDir;
  }

  /** Get the current I/O options mask as a String
   *  @return the current I/O options mask as a String
   *  @since NeXtMidas 2.9.0
   */
  public String getOptions () {
    return ioOptions.toString();
  }

  /** Set I/O options mask.
      @param maskStr options mask string to set
      @since NeXtMidas 2.9.0
   */
  public void setOptions (String maskStr) {
    boolean prevDisableEscapes = ioOptions.isSet(IOOptions.DisableEscapeSequences);
    if(!isMidasLoggerOptionValid(maskStr)) {

    }
    else {
      ioOptions.setValue(maskStr, DEF_IOOPTIONS_OPTIONS);
    }
    if ((M != null) && M.macro != null) {            // if inside a macro
      boolean curDisableEscapes = ioOptions.isSet(IOOptions.DisableEscapeSequences);
      if (curDisableEscapes != prevDisableEscapes) { // if user changed DisableEscapeSequences setting
        String tmp = (curDisableEscapes)? "Adding" : "Removing";
        M.warning(tmp+" IOOPTIONS.DisableEscapeSequences DOES NOT take effect until macro is parsed.");
      }
    }
    // Since 3.1.0: default behavior is to always enable String escape sequences in macro language
    //              so print out deprecated warning when user disables string escape sequences
    if (ioOptions.isSet(IOOptions.DisableEscapeSequences)) {
      String msg = "Single backslash char ('\\') in quoted strings are deprecated since 3.1.0.\n" +
                 "  Run 'ENV set IOOPTIONS -DisableEscapeSequences' to enable Java/C-like backslash\n"
                   +"  escape sequences (i.e. two backslashes \\\\ to represent one backslash \\).";
      if (M != null) M.deprecate(msg);
      else System.out.println("DEPRECATE: "+msg);
    }
    if (ioOptions.isSet(IOOptions.MidasLoggerShouldUseLogUtilities)) {
      Midas.setUseLogUtilities(true);
    }
    else {
      Midas.setUseLogUtilities(false);
    }
    // Update Convert's behavior settings 4.0.0 - fix efficiency issue
    Convert.setFavorJavaBehavior(ioOptions.isSet(IOOptions.FavorJavaBehaviorOverTraditionalMidasBehavior));
    Convert.setAlwaysDefaultToDouble(ioOptions.isSet(IOOptions.DefaultToDoubleOnConvertO2nS2nEvenWhenPrecisionIsLost));
    Convert.setMinSupportUnsupportedDataTypes(ioOptions.isSet(IOOptions.MinimalSupportForUnsupportedDataTypes));
  }


  /**
   * This is a helper method to see if the options that are sent in are valid for the MidasLogger.
   * @param maskStr The options that are being set
   * @return True if the options are valid, false if they are not (warning printed in console)
   */
  private boolean isMidasLoggerOptionValid(String maskStr) {
    if (maskStr.toUpperCase().contains("MIDASLOGGERSHOULDUSELOGUTILITIES") && ioOptions.isSet(IOOptions.ClassBasedLoggerShouldUseMidasLogger) && !maskStr.toUpperCase().contains("-CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER")) {
      M.warning("Option not set sucessfully, please disable ClassBasedLoggerShouldUseMidasLogger in order to enable MidasLoggerShouldUseLogUtilities");
      return false;
    }
    else if (maskStr.toUpperCase().contains("CLASSBASEDLOGGERSHOULDUSEMIDASLOGGER") && ioOptions.isSet(IOOptions.MidasLoggerShouldUseLogUtilities) && !maskStr.toUpperCase().contains("-MIDASLOGGERSHOULDUSELOGUTILITIES")) {
      M.warning("Option not set sucessfully, please disable MidasLoggerShouldUseLogUtilities in order to enable ClassBasedLoggerShouldUseMidasLogger");
      return false;
    }
    return true;
  }
  /** Check if the specified I/O options flag is turn on (true) or off (false).
      @param flag IOOptions flag to check if it is set or not.
      @return true if specified flag option is on, false otherwise
      @since NeXtMidas 2.9.0
   */
  public boolean isOptionSet (IOOptions flag) {
    return ioOptions.isSet(flag);
  }

  //////////////////////////////////////////////////////////////////////////////////////////////////
  // Deprecated gmiller code...
  //////////////////////////////////////////////////////////////////////////////////////////////////
  // TODO: remove below in 3.3+ if not used anywhere
  /** deprecated Since NeXtMidas 2.5.0: Only used with deprecated methods. */
  static final int COPY_OK=1;              // made package-private in 3.1.0
  /** deprecated Since NeXtMidas 2.5.0: Only used with deprecated methods. */
  static final int COPY_FILE_NOT_FOUND=-1; // made package-private in 3.1.0
  /** deprecated Since NeXtMidas 2.5.0: Only used with deprecated methods. */
  static final int COPY_DIR_NOT_FOUND=-2;  // made package-private in 3.1.0
  /** deprecated Since NeXtMidas 2.5.0: Only used with deprecated methods. */
  static final int COPY_NO_OVERWRITE=-3;   // made package-private in 3.1.0
  /** Enumeration indicating an inability to create a directory during a copy. deprecated Since NeXtMidas 2.5.0 */
  static final int COPY_DIR_CREATION_ERROR=-4; // made package-private in 3.1.0

  /** deprecated Since NeXtMidas 2.5.0: Use methods in {@link FileUtil}.
      made package-private in 3.1.0 (TODO: remove in 3.3+ if not used anywhere)
   */
  static int okToCopy (File SourceFullFile, File TargetFullFile,
                              boolean OverwriteExistingFile, boolean MakeNonExistingDirs) {

    Shell.deprecate("CoreIO.okToCopy method has been deprecated. Since NeXtMidas 2.5.0");
    //  We can not copy the file under three (3) conditions:
    //  1) The source file does not exist
    //  2) The target file exists and we can not overwrite it
    //  3) The target directory structure does not exist and we can not create it

    int status = COPY_OK;

    if (!SourceFullFile.exists()) {
      //  The source file does NOT exist!
      status = COPY_FILE_NOT_FOUND;
    }
    else {
      if (TargetFullFile.exists() && !OverwriteExistingFile) {
        // We can NOT overwrite the existing file!
        status = COPY_NO_OVERWRITE;
      }
      else {
        // Does the file exist?
        if (!TargetFullFile.exists()) {
          // Set the target directory
          File TargetDirectory = TargetFullFile.getParentFile();
          // Does the directory structure exist?
          if (!TargetDirectory.exists() && !MakeNonExistingDirs) {
            //  The directory structure does NOT exist and we can NOT create it!
            status = COPY_DIR_NOT_FOUND;
          }
        }
      }
    }
    return status;
  }

  /** deprecated Since NeXtMidas 2.5.0: Use methods in {@link FileUtil}.
      made package-private in 3.1.0 (TODO: remove in 3.3+ if not used anywhere)
   */
  static int copyFile (String sourcename, String targetname,
                              boolean OverWriteExistingFile, boolean MakeNonExistingDirs) {
    Shell.deprecate("CoreIO.copyFile method has been deprecated. Since NeXtMidas 2.5.0");

    boolean DirsExist;
    final long MAX_BYTES_TO_READ = 1024*1024;
    int status;          // Return status
    File SourceFullFile; // Source file with full name
    File TargetFullFile; // Target file with full name

    // Create abstract files from names
    SourceFullFile = new File (sourcename);
    if (isDirectoryName(targetname))
    {
      // Use target directory and source file name
      String tempPathName = targetname+sourcename;
      TargetFullFile = new File (tempPathName);
    }
    else
    {
      TargetFullFile = new File (targetname);
    }

    // Make sure files have full file names
    try {
      SourceFullFile = SourceFullFile.getCanonicalFile();
      TargetFullFile = TargetFullFile.getCanonicalFile();
    }
    catch (IOException e) {
      System.out.println("Exception "+e+" attempting to build full file names");
    }

    // For convenience, set the target directory
    File TargetDirectory = TargetFullFile.getParentFile();

    status = okToCopy (SourceFullFile, TargetFullFile, OverWriteExistingFile,
                       MakeNonExistingDirs);

    if (status== COPY_OK) {
      try {

      // Make directories if necessary
      if (TargetDirectory.exists()) { DirsExist = true; }
      else { DirsExist = TargetDirectory.mkdirs(); }

      if (DirsExist) {

        FileInputStream fins   = new FileInputStream (SourceFullFile);
        FileOutputStream fouts = new FileOutputStream (TargetFullFile);

        // Determine the TOTAL number of bytes in file
        long totalBytes = SourceFullFile.length();

        // Determine how many bytes we want to read each time
        int bytesToRead = (int) Math.min (MAX_BYTES_TO_READ, totalBytes);
        byte byteBlock[]     = new byte [bytesToRead];

        // Read input and Write output
        double bytesRemaining = totalBytes;
        while (bytesRemaining > 0) {
          if (bytesRemaining < bytesToRead) {
            //byte bytesTail = (int) bytesRemaining;
            byte byteTailArray[] = new byte [(int) bytesRemaining];
            // Read and write tail block
            fins.read (byteTailArray);
            fouts.write (byteTailArray);
            break;
          }
          else {
            // Read and write next block
            fins.read (byteBlock);
            fouts.write (byteBlock);
          }
          bytesRemaining = bytesRemaining - bytesToRead;
        }

        fins.close();
        fouts.close();
      }
      else {
        status = COPY_DIR_CREATION_ERROR;
        //System.out.println("Target directory not created");
      }
      } // End of try
      catch (IOException e){
        System.out.println ("IO Exception "+e);
      }
    }
    else
    {
      // Can not copy files
    }

    return status;
  }

  /** @deprecated Since NeXtMidas 2.5.0: Use methods in {@link FileUtil}.
      @param root     The name of the file less the path and extension.
      @param filter   the {@link FilenameFilter} object to apply to the files.
      @param toDir    the destination directory for the copy.
      @param fromDir  the source directory for the copy.
      @param verbose  true to enable verbose output.
      @param flatten  true to enable flattening the output directory.
   */
  // NTN: keeping around, since we don't have replacement method in FileUtil as of 3.6.0
  @Deprecated public static void recursiveCopy (String root, FilenameFilter filter,
                                    File fromDir, File toDir, boolean verbose, boolean flatten) {
    Copier copier = new Copier (toDir,filter,root);
    copier.setVerbose (verbose);
    copier.setFlatten (flatten);
    copier.execute (fromDir);
  }

  /** deprecated Since NeXtMidas 2.5.0: Only used with deprecated methods.
      made package-private in 3.1.0 (TODO: in 3.3+ if not used anywhere)
   */
  static class Copier extends FileVisitor {
    private File toDir = null;
    private String root = "";
    private boolean verbose = false;
    private boolean flatten = false;

    public Copier (File toDir, FilenameFilter ff, String root) {
      super (ff);
      Shell.deprecate("Copier class has been deprecated. Since NeXtMidas 2.5.0");
      this.toDir = toDir;
      this.root = root;
    }

    /** Turn on or off the verbose printing of messages. */
    public void setVerbose (boolean verbose) { this.verbose = verbose; }

    /** Turn on or off the flattening of the output directory */
    public void setFlatten (boolean flatten) { this.flatten = flatten; }

    public void visit (String name) {
      if (verbose) System.out.println ("Copying "+name);
      String newName = getDestFileName (name);
      copyFile (name,newName,true,true);
    }

    /** Return the name of the destination file for the given filename. The
        assumption is that the file is going to be placed into the toDir
        directory so the returned name is relative to that. */
    public String getDestFileName (String name) {
      String newName = name;
      if (name.startsWith(root)) newName = newName.substring(root.length());
      if (flatten) newName = new File(name).getName();
      return new File(toDir, newName).getAbsolutePath();
    }
  }

}
