Directory Class

package javaxt.io;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

//******************************************************************************
//**  Directory Class
//******************************************************************************
/**
 *  Used to represent a directory on a file system. In many ways, this class
 *  is an extension of the java.io.File class. However, unlike the java.io.File
 *  class, this object provides functions that are relevant and specific to
 *  directories. For example, this class provides a mechanism to move and copy
 *  directories - something not offered by the java.io.File class. In addition,
 *  this class provides a mechanism to retrieve files and folders found in a
 *  directory AND any subdirectories. This is accomplished via a multi-threaded
 *  recursive search. Finally, this class provides a powerful tool to monitor
 *  changes made to the directory (e.g. getEvents).
 *
 ******************************************************************************/

public class Directory implements Comparable {

    private java.io.File file; //<--DO NOT USE DIRECTLY! Use getFile() instead.
    private String name = "";
    private String path = "";

    //private boolean useCache = false;
    private FileSystemWatcher FileSystemWatcher;
    private File.FileAttributes attr;
    private long lastAttrUpdate = 0;

    public static final String PathSeparator = System.getProperty("file.separator");
    protected static final boolean isWindows = System.getProperty("os.name").toLowerCase().startsWith("windows");


  //**************************************************************************
  //** Constructor
  //**************************************************************************
  /** Creates a new instance of Directory using a path to a directory.
   */
    public Directory(String Path) {

	if (Path==null) throw new NullPointerException();

        if (Path.startsWith("\"") && Path.endsWith("\"")){
            Path = Path.substring(1,Path.length()-1);
        }

        init(Path);
    }


  //**************************************************************************
  //** Constructor
  //**************************************************************************
  /** Creates a new instance of Directory using a java.io.File
   */
    public Directory(java.io.File File) {
        init(File);
    }


  //**************************************************************************
  //** init
  //**************************************************************************

    private void init(java.io.File File){

	if (File==null) throw new NullPointerException();

        if (File.exists() && !File.isDirectory()){
            File = File.getParentFile();
        }

        attr = null;

        String p = File.getAbsolutePath();
        init(p);


        if (p.equals(toString())){
            this.file = File;
        }
    }


  //**************************************************************************
  //** init
  //**************************************************************************
  /** Used to parse the absolute path to the directory. For performance reasons,
   *  we do not rely on the java.io.File to determine the path and name
   *  variables.
   */
    private void init(String Path){
        Path = Path.replace("\\", "/");
        String[] arr = Path.split("/");
        if (arr.length>1){
            name = arr[arr.length-1];
            path = Path.substring(0, Path.lastIndexOf(name));
        }
        else{
            path = Path;
        }

        if (path.isEmpty()) path = PathSeparator;
        else path = path.replace("/", PathSeparator);

        if (!path.endsWith(PathSeparator)) path += PathSeparator;


      //Special case for relative paths
        if (path.startsWith(".")){
            try{
                init(new java.io.File(path + name).getCanonicalPath());
            }
            catch(Exception e){
                throw new IllegalArgumentException("Invalid Path.");
            }
        }
    }


  //**************************************************************************
  //** getFile
  //**************************************************************************
  /**  Returns the java.io.File representation of this object. */

    private java.io.File getFile(){
        if (file==null) file = new java.io.File(path + name);
        return file;
    }


  //**************************************************************************
  //** getRootDirectories
  //**************************************************************************
  /** Returns an array of root directories on the filesystem (e.g. "/" or C:\").
   *  On Windows, this method will return all mounted drives, including any
   *  that might have been disconnected. The array will be empty if there are
   *  no root directories or if the set of roots could not be determined.
   */
    public static Directory[] getRootDirectories(){

        java.util.TreeSet<Directory> directories = new java.util.TreeSet<Directory>();
        java.io.File[] files = java.io.File.listRoots();
        if (files==null) return new Directory[0];
        for (int i=0; i<files.length; i++){
            directories.add(new Directory(files[i]));
        }

      //Pick up any windows mounted drives that might not have been returned
      //from the listRoots() method (e.g. disconnected mounted drives).
        if (isWindows){

            boolean doNetUse = false;
            if (File.loadDLL()){
                try{
                    String drives = File.GetNetworkDrives();
                    if (drives!=null){
                        for (String drive : drives.split("\n")){
                            if (drive.trim().length()>0){
                                String[] arr = drive.split("\t");
                                String driveName = arr[0];
                                String driveType = arr[1];
                                directories.add(new Directory(driveName));
                            }
                        }
                    }
                }
                catch(Exception e){
                    doNetUse = true;
                }
            }
            else{
                doNetUse = true;
            }

            if (doNetUse){
                javaxt.io.Shell cmd = new javaxt.io.Shell("net use");
                try{cmd.run();}catch(Exception e){}

                java.util.Iterator<String> it = cmd.getOutput().iterator();
                boolean parse = false;
                while (it.hasNext()){
                    String line = it.next();
                    if (line==null) break;
                    else line = line.trim();
                    if (!parse && line.startsWith("----")){
                        parse = true;
                        line = it.next();
                        if (line==null) break;
                    }
                    if (line.contains(":")){
                        line = line.substring(0, line.indexOf(":")+1);
                        line = line.substring(line.lastIndexOf(" ")+1);
                        directories.add(new Directory(line.trim()));
                    }
                }
            }
        }

        return directories.toArray(new Directory[directories.size()]);
    }


  //**************************************************************************
  //** Exists
  //**************************************************************************
  /** Used to determine whether a directory exists on the file system.
   */
    public boolean exists(){

        String path = this.path + name;

      //Special case for a directory whose path represents a server name (e.g. "\\192.168.0.1")
        if (isWindows && path.startsWith("\\\\")){
            if (path.endsWith(PathSeparator)) path = path.substring(0, path.length()-1);
            if (!path.substring(2).contains(PathSeparator)){
                boolean doNetUse = false;
                if (File.loadDLL()){
                    try{
                        File.GetSharedDrives(path.substring(2));
                        return true;
                    }
                    catch(Exception e){
                        if (e.getMessage().trim().equals("53")){
                            return false;
                        }
                        else{
                            doNetUse = true;
                        }
                    }
                }
                else{
                    doNetUse = true;
                }

                if (doNetUse){
                    javaxt.io.Shell cmd = new javaxt.io.Shell("net view " + path);
                    try{cmd.run();}catch(Exception e){}
                    java.util.List errors = cmd.getErrors();
                    errors.remove(null);
                    if (errors.isEmpty()){
                        return true;
                    }
                    else{
                        return false;
                    }
                }

            }
        }


        if (file!=null){
            return (file.isDirectory() && file.exists());
        }

        try{
            File.FileAttributes attr = getFileAttributes();
            if (attr!=null) return true;
            else{
                getFile();
                return (file.isDirectory() && file.exists());
            }
        }
        catch(java.io.FileNotFoundException e){
            return false;
        }
    }


  //**************************************************************************
  //** isEmpty
  //**************************************************************************
  /** Used to determine whether the directory is empty. Returns true if no
   *  files or directories are present in the current directory.
   */
    public boolean isEmpty(){
        if (!exists()) return true;
        Object[] files = listFiles(null);
        return (files==null);
    }


  //**************************************************************************
  //** Create Directory
  //**************************************************************************
  /**  Used to create the directory. */

    public boolean create(){
        return getFile().mkdirs();
    }


  //**************************************************************************
  //** Delete Directory
  //**************************************************************************
  /**  Used to delete the directory. */

    public boolean delete(){
        if (getFile().delete()){
            attr = null;
            return true;
        }
        else{
            return false;
        }
    }


  //**************************************************************************
  //** Copy To
  //**************************************************************************
  /** Used to copy a directory to another directory. Preserves the last modified
   *  date associated with the source files and directories. Returns a list of
   *  any files that failed to copy.
   */
    public String[] copyTo(Directory Destination, boolean Overwrite){
        return copyTo(Destination, null, Overwrite);
    }


  //**************************************************************************
  //** Copy To
  //**************************************************************************
  /** Used to copy a directory to another directory. Provides a filter option
   *  to copy specific files (e.g. "*.jpg"). Preserves the last modified date
   *  associated with the source files and directories. Returns a list of any
   *  files that failed to copy.
   *
   *  @param filter A file filter. You can pass in a java.io.FileFilter, a
   *  String (e.g. "*.txt"), or an array of Strings (e.g. String[]{"*.txt", "*.doc"}).
   *  Wildcard filters are supported. Note that the filter is only applied to
   *  files, not directories.
   *
   *  @param Overwrite If true, overwrites any existing files/directories
   */
    public String[] copyTo(Directory Destination, Object filter, boolean Overwrite){

        int source = toString().length();
        String destination = Destination.toString();
        java.util.ArrayList<String> failures = new java.util.ArrayList<String>();


      //Create new folder
        if (!Destination.exists()) Destination.create();



      //Initiate search
        java.util.List results = this.getChildren(true, filter, false);
        while (true){

            Object item;
            synchronized (results) {


              //Wait for files/directories to be added to the list
                while (results.isEmpty()) {
                    try {
                        results.wait();
                    }
                    catch (InterruptedException e) {
                        break;
                    }
                }

              //Grab the next available file/directory from the list. Note that
              //we are INSIDE the synchronized block! This forces the directory
              //search to insert only one record at a time. The primary motivation
              //for this is to ensure that we don't run out of memory when
              //copying a large number of files/directories.
                item = results.get(0);
                if (item!=null){

                    if (item instanceof javaxt.io.File){

                        javaxt.io.File file = (javaxt.io.File) item;
                        String FilePath = file.toString();
                        File out = new File(destination + FilePath.substring(source));

                        boolean success = file.copyTo(out, Overwrite);
                        if (!success) failures.add(FilePath);
                    }
                    else{
                        javaxt.io.Directory dir = (javaxt.io.Directory) item;
                        new Directory(destination + dir.toString().substring(source)).create();
                    }

                }
                else{
                    break;
                }

                results.remove(0);
                results.notifyAll();
            }
        }

      //Return list of failed copies
        return failures.toArray(new String[failures.size()]);
    }


  //**************************************************************************
  //** Move To
  //**************************************************************************
  /**  Used to move a directory from one directory to another. */

    public void moveTo(Directory Destination, boolean Overwrite){
        if (Overwrite || !Destination.exists()){
            java.io.File newFile = Destination.toFile();
            if (getFile().renameTo(newFile)){
                init(newFile);
            }
        }
    }


  //**************************************************************************
  //** Rename
  //**************************************************************************
  /**  Used to rename the directory. */

    public void rename(String Name){
        java.io.File newFile = new java.io.File(path + Name);
        if (getFile().renameTo(newFile)){
            init(newFile);
        }
    }


  //**************************************************************************
  //** getName
  //**************************************************************************
  /**  Returns the name of the directory (excluding the path). */

    public String getName(){
        return name;
    }


  //**************************************************************************
  //** getPath
  //**************************************************************************
  /** Returns the full path to this directory, including the directory name.
   *  The path includes a system-dependent default name-separator
   *  character at the end of the string (e.g. "/" or "\").
   */
    public String getPath(){
       String path = this.path + name;
       if (path.endsWith(PathSeparator)) return path;
       else return path + PathSeparator;
    }


  //**************************************************************************
  //** getDate
  //**************************************************************************
  /** Returns a timestamp of when the directory was last modified. This is
   *  identical to the getLastModifiedTime() method.
   */
    public java.util.Date getDate(){
        if (file!=null) return getFileDate();

        try{
            File.FileAttributes attr = getFileAttributes();
            if (attr!=null) return attr.getLastWriteTime();
            else{
                getFile();
                return getFileDate();
            }
        }
        catch(java.io.FileNotFoundException e){
            return null;
        }

    }

    private java.util.Date getFileDate(){
        if (file.exists() && file.isDirectory()) return new java.util.Date(file.lastModified());
        else return null;
    }


  //**************************************************************************
  //** setDate
  //**************************************************************************
  /**  Used to set the timestamp of when the directory was last modified.
   */
    public boolean setDate(java.util.Date lastModified){
        if (lastModified!=null){
            java.io.File file = getFile();
            if (file.exists() && file.isDirectory()){
                long t = lastModified.getTime();
                if (getDate().getTime()!=t){
                    attr = null;
                    return getFile().setLastModified(t);
                }
            }
        }
        return false;
    }


//  //**************************************************************************
//  //** getSize
//  //**************************************************************************
//  /** Used to retrieve the size of the directory object, in bytes. Note that
//   *  the getContentSize() method will return the total size of all the files
//   *  and folders found in this directory.
//   */
//    public long getSize(){
//        if (file!=null) return getFileSize();
//
//        try{
//            File.FileAttributes attr = getFileAttributes();
//            if (attr!=null) return attr.getSize();
//            else{
//                getFile();
//                return getFileSize();
//            }
//        }
//        catch(java.io.FileNotFoundException e){
//            return 0L;
//        }
//    }
//
//    private long getFileSize(){
//        long size = file.length();
//        if (size>0L && file.isDirectory()) return size;
//        else return 0L;
//    }


  //**************************************************************************
  //** getSize
  //**************************************************************************
  /** Returns the total size of all the files and folders found in this
   *  directory, in bytes.
   */
    public long getSize(){

        long size = 0L;

      //Initiate search
        java.util.List results = getChildren(true, null, false);

        while (true){
            Object item;
            synchronized (results) {
                while (results.isEmpty()) {
                  try {
                      results.wait();
                  }
                  catch (InterruptedException e) {
                      break;
                  }
                }
                item = results.remove(0);
                results.notifyAll();
            }

            if (item!=null){ //Do something with the item.

                if (item instanceof javaxt.io.File){
                    javaxt.io.File file = (javaxt.io.File) item;
                    size += file.getSize();
                }
                else{
                    javaxt.io.Directory dir = (javaxt.io.Directory) item;
                    try{
                        //size += dir.getFileAttributes().getSize();
                    }
                    catch(Exception e){
                    }
                }
            }
            else{ //item is null. This is our queue that the search is done!
                return size;
            }
        }
    }



  //**************************************************************************
  //** isHidden
  //**************************************************************************
  /**  Used to determine whether this Directory is Hidden */

    public boolean isHidden(){

        if (file!=null){
            return (file.isDirectory() && file.isHidden());
        }

        try{
            File.FileAttributes attr = getFileAttributes();
            if (attr!=null) return attr.isHidden();
            else{
                getFile();
                return (file.isDirectory() && file.isHidden());
            }
        }
        catch(java.io.FileNotFoundException e){
            return false;
        }
    }


  //**************************************************************************
  //** isLink
  //**************************************************************************
  /**  Used to determine whether the directory is actually a link to another
   *   file or directory. Returns true for symbolic links and Windows
   *   junctions.
   */
    public boolean isLink(){
        return getLink()!=null;
    }


  //**************************************************************************
  //** getLink
  //**************************************************************************
  /**  Returns the target of a symbolic link or Windows junction
   */
    public java.io.File getLink(){
        try{
            return getFileAttributes().getLink();
        }
        catch(Exception e){
            return null;
        }
    }


  //**************************************************************************
  //** getLastModifiedTime
  //**************************************************************************
  /** Returns a timestamp of when the directory was last modified. This is
   *  identical to the getDate() method.
   */
    public java.util.Date getLastModifiedTime(){
        return this.getDate();
    }


  //**************************************************************************
  //** getCreationTime
  //**************************************************************************
  /** Returns a timestamp of when the directory was first created. Returns a
   *  null if the timestamp is not available.
   */
    public java.util.Date getCreationTime(){
        try{
            return getFileAttributes().getCreationTime();
        }
        catch(Exception e){
            return null;
        }
    }


  //**************************************************************************
  //** getLastAccessTime
  //**************************************************************************
  /** Returns a timestamp of when the directory was last accessed. Returns a
   *  null if the timestamp is not available.
   */
    public java.util.Date getLastAccessTime(){
        try{
            return getFileAttributes().getLastAccessTime();
        }
        catch(Exception e){
            return null;
        }
    }


  //**************************************************************************
  //** getFlags
  //**************************************************************************
  /** Returns keywords representing directory attributes. Returns an empty
   *  HashSet if the attributes are not available.
   */
    public java.util.HashSet<String> getFlags(){
        try{
            return getFileAttributes().getFlags();
        }
        catch(Exception e){
            return new java.util.HashSet<String>();
        }
    }


  //**************************************************************************
  //** getFileAttributes
  //**************************************************************************
  /** Returns file attributes such as when the file was first created and when
   *  it was last accessed. File attributes are cached for up to one second.
   *  This provides users the ability to retrieve multiple attributes at once.
   *  Without caching, we would have to ping the file system every time we call
   *  getLastAccessTime(), getLastAccessTime(), getLastWriteTime(), etc. The
   *  cached attributes are automatically updated when the file is updated or
   *  deleted by this class.
   */
    public File.FileAttributes getFileAttributes() throws java.io.FileNotFoundException {

        if (attr==null) lastAttrUpdate = 0;

        if (attr==null || (new java.util.Date().getTime()-lastAttrUpdate)>1000){
            try{
              //Get directory attributes
                String pathToFile = toString();
                attr = new File.FileAttributes(pathToFile);
                if (!attr.isDirectory()) throw new java.io.FileNotFoundException(pathToFile);

              //Set lastUpdate (used to cache file attributes)
                lastAttrUpdate = new java.util.Date().getTime();
            }
            catch(java.io.FileNotFoundException e){
                attr = null;
                throw e;
            }
            catch(Exception e){
                //e.printStackTrace();
                attr = null;
            }
        }
        return attr;
    }


  //**************************************************************************
  //** getParentDirectory
  //**************************************************************************
  /** Used to retrieve this Directory's Parent. Returns null if there is no
   *  parent directory.
   */
    public Directory getParentDirectory(){
        if (name.isEmpty()){
            return null;
        }
        else{
            return new Directory(path);
        }
    }


  //**************************************************************************
  //** toFile
  //**************************************************************************
  /**  Used to retrieve the java.io.File representation by this object. */

    public java.io.File toFile(){
        return getFile();
    }



  //**************************************************************************
  //** getFiles
  //**************************************************************************
  /**  Used to retrieve an array of files found in this directory. Returns an
   *   empty array if no files are found.
   *
   *   @param filter A file filter. You can pass in a java.io.FileFilter, a
   *   String (e.g. "*.txt"), or an array of Strings (e.g. String[]{"*.txt", "*.doc"}).
   *   Wildcard filters are supported. Note that the filter is only applied to
   *   files, not directories.
   */
    public File[] getFiles(Object filter){

        if (this.exists()){

            FileFilter fileFilter = new FileFilter(filter);
            Object[] files = listFiles(fileFilter);
            if (files==null){
                return new File[0];
            }
            else{

                java.util.ArrayList<javaxt.io.File> list = new java.util.ArrayList<javaxt.io.File>();
                for (int i=0; i<files.length; i++){
                    java.io.File file = null;
                    if (files[i] instanceof java.io.File){
                        file = (java.io.File) files[i];
                    }
                    else{
                        file = new java.io.File(files[i].toString());
                    }
                    if (file.exists() && file.isFile()){
                        list.add(new javaxt.io.File(file));
                    }
                }
                sort(list, this);
                return list.toArray(new javaxt.io.File[list.size()]);
            }
        }
        else{
            return new File[0];
        }
    }


  //**************************************************************************
  //** getFiles
  //**************************************************************************
  /**  Used to retrieve an array of files found in this directory. Returns an
   *   empty array if no files are found.
   */
    public File[] getFiles(){
        return getFiles(null);
    }


  //**************************************************************************
  //** getFiles
  //**************************************************************************
  /**  Used to retrieve an array of files found in this directory. Returns an
   *   empty array if no files are found.
   *
   *   @param filter A file filter. You can pass in a java.io.FileFilter, a
   *   String (e.g. "*.txt"), or an array of Strings (e.g. String[]{"*.txt", "*.doc"}).
   *   Wildcard filters are supported. Note that the filter is only applied to
   *   files, not directories.
   *
   *   @param RecursiveSearch If true, will perform a multi-threaded, recursive
   *   directory search to find all the files found in the current directory,
   *   including any subdirectories. If false, the method will simply return
   *   files found in the current directory. <br/>
   *
   *   Note that if the thread is interrupted for whatever reason during a
   *   recursive search, the search will stop immediately. Consequently, the
   *   returned array may be incomplete. You can check the interrupted status
   *   with the Thread.isInterrupted() method. Alternatively, you can read and
   *   clear the interrupted status in a single operation using the
   *   Thread.interrupted() method.
   */
    public File[] getFiles(Object filter, boolean RecursiveSearch){

        if (this.exists()){
            if (RecursiveSearch){

              //Get list of all the files and folders in the directory
                List items = getChildren(true, filter, false);
                ArrayList<File> files = new ArrayList<File>();
                try {
                    Object item;
                    while (true){
                        synchronized (items) {
                            while (items.isEmpty()) {
                              items.wait();
                            }
                            item = items.remove(0);
                            items.notifyAll();
                        }

                        if (item==null){
                            break;
                        }
                        else{
                            if (item instanceof File){
                                files.add((File) item);
                            }
                        }
                    }
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }

              //Sort the list
                sort(files, this);


              //Convert the list into an array of files
                return files.toArray(new File[files.size()]);
            }
            else{
                return getFiles(filter);
            }
        }
        else{
            return new File[0];
        }
    }


  //**************************************************************************
  //** getFiles
  //**************************************************************************
  /**  Used to retrieve an array of files found in this directory.
   *
   *   @param RecursiveSearch If true, will perform a multi-threaded, recursive
   *   directory search to find all the files found in the current directory,
   *   including any subdirectories. If false, the method will simply return
   *   files found in the current directory. <br/>
   *
   *   Note that if the thread is interrupted for whatever reason during a
   *   recursive search, the search will stop immediately. Consequently, the
   *   returned array may be incomplete. You can check the interrupted status
   *   with the Thread.isInterrupted() method. Alternatively, you can read and
   *   clear the interrupted status in a single operation using the
   *   Thread.interrupted() method.
   */
    public File[] getFiles(boolean RecursiveSearch){
        return getFiles(null, RecursiveSearch);
    }


  //**************************************************************************
  //** getSubDirectories
  //**************************************************************************
  /**  Used to retrieve an array of directories found in this directory.
   */
    public Directory[] getSubDirectories(){
        return getSubDirectories(false);
    }


  //**************************************************************************
  //** getSubDirectories
  //**************************************************************************
  /**  Used to retrieve an array of directories found in this directory.
   *
   *   @param RecursiveSearch If true, will perform a multi-threaded, recursive
   *   directory search to find all the directories found in the current
   *   directory, including any subdirectories. If false, the method will simply
   *   return directories found in the current directory. <br/>
   *
   *   Note that if the thread is interrupted for whatever reason during a
   *   recursive search, the search will stop immediately. Consequently, the
   *   returned array may be incomplete. You can check the interrupted status
   *   with the Thread.isInterrupted() method. Alternatively, you can read and
   *   clear the interrupted status in a single operation using the
   *   Thread.interrupted() method.
   */
    public Directory[] getSubDirectories(boolean RecursiveSearch){
        if (this.exists()){

            if (RecursiveSearch){

              //Get list of all the files and folders in the directory
                List items = getChildren(true, null, false);
                ArrayList<Directory> directories = new ArrayList<Directory>();
                try {
                    Object item;
                    while (true){
                        synchronized (items) {
                            while (items.isEmpty()) {
                              items.wait();
                            }
                            item = items.remove(0);
                            items.notifyAll();
                        }

                        if (item==null){
                            break;
                        }
                        else{
                            if (item instanceof Directory){
                                directories.add((Directory) item);
                            }
                        }
                    }
                }
                catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return directories.toArray(new Directory[directories.size()]);

            }
            else{
                java.io.FileFilter fileFilter = new java.io.FileFilter() {
                    public boolean accept(java.io.File file) {
                        if (file.isDirectory()){
                            return true;
                        }
                        else{
                            return false;
                        }
                    }
                };


                Object[] files = listFiles(new FileFilter(fileFilter));
                if (files==null) return new Directory[0];
                else{
                    Directory[] dirs = new Directory[files.length];
                    for (int i=0; i<files.length; i++){
                         dirs[i] = new Directory(files[i].toString());
                    }
                    return dirs;
                }
            }
        }
        else{
             return new Directory[0];
        }
    }


  //**************************************************************************
  //** getChildren
  //**************************************************************************
  /**  Used to retrieve an array of both files and folders found in this
   *   directory.
   */
    public List getChildren(){
        return getChildren(false);
    }


  //**************************************************************************
  //** getChildren
  //**************************************************************************
  /**  Used to retrieve an array of both files and folders found in this
   *   directory.
   */
    public List getChildren(boolean RecursiveSearch){
        return getChildren(RecursiveSearch, null, true);
    }


  //**************************************************************************
  //** getChildren
  //**************************************************************************
  /**  Used to retrieve an list of files and folders found in this directory.
   *
   *   @param RecursiveSearch If true, will include files found in the
   *   subdirectories.
   *
   *   @param filter A file filter. You can pass in a java.io.FileFilter, a
   *   String (e.g. "*.txt"), or an array of Strings (e.g. String[]{"*.txt", "*.doc"}).
   *   Wildcard filters are supported. Note that the filter is only applied to
   *   files, not directories.
   *
   */
    public List getChildren(boolean RecursiveSearch, Object filter){
        return getChildren(RecursiveSearch, filter, true);
    }


  //**************************************************************************
  //** getChildren
  //**************************************************************************
  /**  Used to retrieve an list of files and folders found in this directory.
   *
   *   @param RecursiveSearch If true, will perform a multi-threaded, recursive
   *   directory search to find all the files and folders found in the current
   *   directory, including any subdirectories. If false, the method will simply
   *   return items found in the current directory. <br/>
   *
   *   Note that if the thread is interrupted for whatever reason during a
   *   recursive search, the search will stop immediately. Consequently, the
   *   returned array may be incomplete. You can check the interrupted status
   *   with the Thread.isInterrupted() method. Alternatively, you can read and
   *   clear the interrupted status in a single operation using the
   *   Thread.interrupted() method.
   *
   *   @param filter A file filter. You can pass in a java.io.FileFilter, a
   *   String (e.g. "*.txt"), or an array of Strings (e.g. String[]{"*.txt", "*.doc"}).
   *   Wildcard filters are supported. Note that the filter is only applied to
   *   files, not directories.
   *
   *   @param wait Used to indicate whether to wait for the list to be fully
   *   populated before returning the response. When traversing very a large
   *   set of files, it maybe a good idea to process the files as they get added
   *   to the list. In this case you should set this parameter to false. If this
   *   parameter is set to false, a null entry will be added to the end of the
   *   list to indicate that the directory search is complete.
   *
   *   Example:
   <pre>
    boolean wait = false;
    java.util.List files = directory.getChildren(true, null, wait);
    if (wait){
        for (int i=0; i<files.size(); i++){
            System.out.println(files.get(i));
        }
    }
    else{
        Object obj;
        while (true){
            synchronized (files) {
                while (files.isEmpty()) {
                  try {
                      files.wait();
                  }
                  catch (InterruptedException e) {
                      break;
                  }
                }
                obj = files.remove(0);
                files.notifyAll();
            }

            if (obj==null){
                break;
            }
            else{
                System.out.println(obj);
                if (obj instanceof javaxt.io.File){
                    javaxt.io.File file = (javaxt.io.File) obj;
                }
                else if (obj instanceof javaxt.io.Directory){
                    javaxt.io.Directory dir = (javaxt.io.Directory) obj;
                }
            }
        }
    }
   </pre>
   *
   */
    public List getChildren(boolean RecursiveSearch, Object filter, boolean wait){

        if (this.exists()){

            if (RecursiveSearch){

              //Create list to store items found in the directory
                List items = new LinkedList();


              //Create a file filter
                FileFilter fileFilter = new FileFilter(filter);


              //Spawn threads used to crawl through the file system
                long directoryID = Long.valueOf(java.util.Calendar.getInstance().getTimeInMillis() + "" + new Random().nextInt(100000)).longValue();
                int numThreads = 20; //<-- this should be set dynamically and self tuning
                DirectorySearch.deleteCache();
                DirectorySearch search = new DirectorySearch(fileFilter, items, directoryID, numThreads);
                for (int i=0; i<numThreads; i++) {
                     Thread t = new Thread(search);
                     t.setName("DirectorySearch_" + directoryID + "-" + i);
                     t.start();
                }


              //Initiate search
                DirectorySearch.updatePool(this);

                if (wait){

                    synchronized (items) {
                        while (!items.contains(null)) {
                          try {
                              items.wait();
                          }
                          catch (InterruptedException e) {
                              DirectorySearch.stop();
                              Thread.currentThread().interrupt();
                              return items;
                          }
                        }

                        items.remove(null);
                        items.notifyAll();
                    }

                    sort(items, this);
                }


              //Return list
                return items;


            }//end recursive search
            else{
                List list = new LinkedList();
                Object[] files = listFiles(new FileFilter(filter));
                if (files==null) {
                    if (!wait) list.add(null);
                    return list;
                }
                else{

                    for (int i=0; i<files.length; i++){

                        //System.out.println(files[i]);
                        boolean isDirectory = false;

                        java.io.File file = null;
                        if (files[i] instanceof java.io.File){
                            file = (java.io.File) files[i];
                            isDirectory = file.isDirectory();
                        }
                        else{
                            file = new java.io.File(files[i].toString());
                            isDirectory = files[i].toString().replace("\\", "/").endsWith("/"); //<-- Need this check for windows sym links...
                        }

                        if (isDirectory){
                            list.add(new Directory(file));
                        }
                        else{
                            list.add(new File(file));
                        }
                    }
                    sort(list, this);
                    if (!wait){
                        synchronized(list){
                            list.add(null);
                        }
                    }
                    return list;
                }
            }
        }
        else{
          //Directory does not exist, return an empty list
            List list = new LinkedList();
            if (!wait) list.add(null);
            return list;
        }
    }


  //**************************************************************************
  //** getSharedDrives
  //**************************************************************************
  /**  Returns a list of shared directories found on a given server. */

    private java.io.File[] getSharedDrives(String serverName){
        if (isWindows){
            serverName = serverName.replace("/", "\\\\");
            if (serverName.startsWith("\\\\")) serverName = serverName.substring(2);
            if (serverName.contains("\\")) serverName = serverName.substring(0, serverName.indexOf("\\"));
            boolean doNetView = false;

            if (File.loadDLL()){
                try{
                    String drives = File.GetSharedDrives(serverName);
                    if (drives!=null){
                        java.util.ArrayList<java.io.File> files = new java.util.ArrayList<java.io.File>();
                        for (String drive : drives.split("\n")){
                            drive = drive.trim();
                            if (drive.length()>0){
                                String[] arr = drive.split("\t");
                                files.add(new java.io.File("\\\\" + serverName + "\\" + arr[0]));
                            }
                        }
                        if (files.isEmpty()) return null;
                        else return files.toArray(new java.io.File[files.size()]);
                    }
                }
                catch(Exception e){
                    doNetView = true;
                }
            }
            else{
                doNetView = true;
            }

          //If we're still here, something went wrong with the JNI. Try using
          //net view instead. Note that unlike the JNI, net view won't return
          //hidden network drives.
            if (doNetView){

                javaxt.io.Shell cmd = new javaxt.io.Shell("net view " + serverName);
                try{cmd.run();}catch(Exception e){}
                java.util.List errors = cmd.getErrors();
                errors.remove(null);
                if (errors.isEmpty()){

                    String path = "\\\\" + serverName + "\\";
                    java.util.List output = cmd.getOutput();


                  //Remove empty lines from the standard output stream
                    java.util.List<String> tmp = new java.util.Vector<String>();
                    java.util.Iterator<String> it = output.iterator();
                    while (it.hasNext()){
                        String row = it.next();
                        if (row==null || row.trim().length()==0){} else tmp.add(row);
                    }
                    output = tmp;
                    tmp = null;


                  //Parse the standard output stream and create a list of shared directories
                    java.util.ArrayList<java.io.File> files = new java.util.ArrayList<java.io.File>();
                    int x = 0;
                    int len = -1;
                    it = output.iterator();
                    while (it.hasNext()){
                        String row = it.next();
                        if (row.startsWith("---")){
                            String colHeader = (String) output.get(x-1);
                            if (colHeader.startsWith("Share name") && colHeader.contains("Type")){
                                len = colHeader.indexOf("Type");
                            }
                        }
                        else{

                            if (row.startsWith("The command completed successfully.")){
                                break;
                            }

                            if (len>0 && row.length()>len){
                                String type = row.substring(len);
                                type = type.substring(0, type.indexOf(" "));
                                if (type.equalsIgnoreCase("Disk")){
                                    files.add(new java.io.File(path + row.substring(0, len).trim()));
                                }
                            }
                        }
                        x++;
                    }

                  //Convert the list of files into an array
                    if (files.isEmpty()) return null;
                    else return files.toArray(new java.io.File[files.size()]);

                }
            }
        }
        return null;
    }


  //**************************************************************************
  //** listFiles
  //**************************************************************************
  /**  Used to return a list of files found in this directory. */

    protected Object[] listFiles(){
        return listFiles(null);
    }


  //**************************************************************************
  //** listFiles
  //**************************************************************************
  /**  Used to return a list of files found in this directory.
   *
   *   @param filter A file filter. You can pass in a java.io.FileFilter, a
   *   String (e.g. "*.txt"), or an array of Strings (e.g. String[]{"*.txt", "*.doc"}).
   *   Wildcard filters are supported. Note that the filter is only applied to
   *   files, not directories.
   *
   *   @return An array of java.io.File or an array of Strings representing
   *   paths to files. If the input FileFilter is generated using a
   *   java.io.FileFilter, the method will return an array of java.io.File.
   *   Otherwise, this method will return an array of Strings for most cases.
   *   Note that any subdirectories that are found in this directory are ALWAYS
   *   included in the result, regardless of file filter.
   */
    private Object[] listFiles(Object filter){

        FileFilter fileFilter;
        if (filter instanceof FileFilter) fileFilter = (FileFilter) filter;
        else fileFilter = new FileFilter(filter);


        String path = this.path + name;


      //Get a list of shared drives on a windows server (e.g. "\\192.168.0.80")
        if (isWindows && path.startsWith("\\\\")){

            if (path.endsWith(PathSeparator)) path = path.substring(0, path.length()-1);
            if (!path.substring(2).contains(PathSeparator)){
                java.io.File[] sharedDrives = getSharedDrives(path);
                if (sharedDrives!=null){
                    java.util.ArrayList<java.io.File> files = new java.util.ArrayList<java.io.File>();
                    for (java.io.File file : sharedDrives){
                        if (file.exists()){
                            if (fileFilter==null) {
                                files.add(file);
                            }
                            else{
                                if (fileFilter.accept(file)){
                                    files.add(file);
                                }
                            }
                        }
                    }
                    return files.toArray(new java.io.File[files.size()]);
                }
                return null;
            }
        }



      //Generate a list of files in this directory that match the file filter
        java.util.List files = new java.util.ArrayList();
        if (isWindows){
            path = getPath();
            String[] list = dir();
            for (int i=0; i<list.length; i++){

                boolean isDirectory = list[i].endsWith("\\");

                if (fileFilter==null || isDirectory){
                    files.add(path + list[i]);
                }
                else{

                    if (fileFilter.usesIOFilter()){
                        java.io.File file = new java.io.File(path + list[i]);
                        if (fileFilter.accept(file)){
                            files.add(file);
                        }
                    }
                    else{
                        if (fileFilter.accept(list[i])){
                            files.add(path + list[i]);
                        }
                    }
                }
            }

        }
        else { //UNIX
            java.io.File dir = getFile();
            java.io.File[] list = dir.listFiles();
            if (list!=null){

                for (int i=0; i<list.length; i++){
                    java.io.File file = list[i];
                    if (fileFilter==null){
                        files.add(file);
                    }
                    else{
                        if (fileFilter.accept(file)){
                            files.add(file);
                        }
                    }
                }
            }

        }

        if (files.size()<1) return null;



      //Sort the list and return an array
        sort(files, null);
        return files.toArray(new Object[files.size()]);
    }


  //**************************************************************************
  //** ls
  //**************************************************************************
  /** Used to list contents of a directory using a unix ls command.
   *
    private String[] ls(){


        java.util.List<String> files = new java.util.ArrayList<String>();
        try{
            String path = this.getPath();


          //Execute a ls command to get a directory listing
            String[] params = new String[]{"ls", "-ap", path};
            javaxt.io.Shell cmd = new javaxt.io.Shell(params);
            java.util.List<String> output = cmd.getOutput();
            cmd.run(true);



          //Parse the output
            String line;
            while (true){
                synchronized (output) {
                    while (output.isEmpty()) {
                        output.wait();
                    }
                    line = output.remove(0);
                }
                if (line!=null){
                    line = line.trim();
                    if (line.length()>0){
                        //System.out.println(line);
                        //boolean isDirectory = (line.endsWith("/"));

                        if (!line.equals("./") && !line.equals("../")){
                            files.add(line);
                        }
                    }
                }
                else{
                    break;
                }
            }
        }
      //Catch Exceptions thrown by cmd.run()
        catch(java.io.IOException e){
        }
        catch(InterruptedException e){
            Thread.currentThread().interrupt();
        }


      //Convert the Vector to an Array
        String[] arr = new String[files.size()];
        for (int i=0; i<arr.length; i++){
            arr[i] = files.get(i);
        }

        return arr;
    }
    */

  //**************************************************************************
  //** dir
  //**************************************************************************
  /** Listing files in using the listFiles() method is super slow on windows
   *  when listing contents from a network share. The alternative is to use
   *  list() method which is MUCH faster. Unfortunately, there's no way to
   *  distinguish between files and directories in the array. My only recourse
   *  is to either use a JNI or shell out a "dir" command and parse the output.
   *  @return An array of strings representing the names of files and
   *  directories found in this directory. Note that directory names include
   *  a path separator which is used to distinguish files from directories.
   */
    private String[] dir(){

        java.util.ArrayList<String> files = new java.util.ArrayList<String>();

        String dir = getPath();
        //dir = this.toFile().getCanonicalPath() + PathSeparator;


      //Try listing files using the JNI
        boolean doDir = false;
        if (File.loadDLL()){
            try{
                String list = File.GetFiles(dir + "*");
                if (list!=null){
                    for (String name : list.split("\n")){
                        name = name.trim();
                        if (name.length()>0 && !(name.equals(".\\") || name.equals("..\\")))
                        files.add(name);
                    }
                }
            }
            catch(Exception e){
                doDir = true;
            }
        }
        else{
            doDir = true;
        }


      //If we're still here, list files using a command prompt
        if (doDir)
        try{

          //Execute a windows shell command to get a directory listing
            javaxt.io.Shell cmd = new javaxt.io.Shell("cmd.exe /c dir /OG " + (dir.contains(" ") ? "\"" + dir + "\"" : dir));
            java.util.List<String> output = cmd.getOutput();
            cmd.run();


          //Parse files returned from the directory listing
            boolean parseFiles = false;
            int colWidth = -1;
            String line;
            while (true){
                synchronized (output) {
                    while (output.isEmpty()) {
                        output.wait();
                    }
                    line = output.remove(0);
                }


                if (line!=null){
                    if (line.length()==0 || line.startsWith(" ")){
                        if (parseFiles==true) parseFiles = false;
                    }
                    else{
                        if (parseFiles==false) parseFiles = true;


                        if (parseFiles){


                            if (colWidth<0){
                                int offset = 20;
                                //String date = line.substring(0, 20);

                              //Get File Type
                                String type = line.substring(offset);
                                if (type.trim().startsWith("<")){
                                    offset += type.indexOf(">")+1;
                                    type = type.substring(type.indexOf("<"), type.indexOf(">")+1);
                                }
                                else{
                                    type = "";
                                }
                                boolean isDirectory = (type.contains("<DIR>"));
                                boolean isSymLink = (type.contains("<SYMLINK>"));
                                boolean isJunction = (type.contains("<JUNCTION>"));

                              //Get File Name
                                String name = line.substring(offset);
                                while (name.substring(0, 1).equals(" ")){
                                    name = name.substring(1);
                                    offset++;
                                }


                              //Set Column Width
                                if (isDirectory || isSymLink || isJunction){
                                    colWidth = offset;
                                }
                                else{
                                    if (name.contains(" ")){
                                        if (isNumeric(name.substring(0, name.indexOf(" ")))){
                                            colWidth = offset + name.indexOf(" ")+1;
                                        }
                                        else{
                                            colWidth = offset;
                                        }
                                    }
                                    else{
                                        colWidth = offset;
                                    }
                                }
                            }
                            if (colWidth>0){
                                //String date = line.substring(0,20);
                                String name = line.substring(colWidth);
                                String type = line.substring(20, colWidth);
                                boolean isDirectory = (type.contains("<DIR>"));
                                boolean isSymLink = (type.contains("<SYMLINK>"));
                                boolean isJunction = (type.contains("<JUNCTION>"));

                                if (isDirectory){
                                    if (!name.equals(".") && !name.equals(".."))
                                    files.add(name + this.PathSeparator);
                                }
                                else if (isSymLink || isJunction){
                                    java.io.File file = null; //<--Note that we're serializing to a file which is going to slow things down...
                                    if (name.contains("[") && name.contains("]")) {
                                        String link = name.substring(name.indexOf("[")+1, name.indexOf("]"));
                                        name = name.substring(0, name.indexOf("[")).trim();
                                        file = new java.io.File(link);
                                    }
                                    else {
                                        file = new java.io.File(dir, name);
                                    }
                                    if (file.isDirectory()) name += this.PathSeparator;
                                    files.add(name);
                                }
                                else{
                                    files.add(name);
                                }
                            }

                        }

                    }

                }
                else{
                    break;
                }
            }

        }
        catch(InterruptedException e){
            Thread.currentThread().interrupt();
        }
        catch(RuntimeException e){
        }


      //Convert the list to an array
        return files.toArray(new String[files.size()]);
    }


  //**************************************************************************
  //** isNumeric
  //**************************************************************************
  /**  Used to determine whether a string is a number. This is used in the
   *   dir() method when parsing output from a Windows "dir" command. This
   *   method is called to see if a string represents a file size (e.g. 12,345).
   *   Since the number is meant to represent a file size, an attempt it made
   *   to convert the string to a long instead of an int or double.
   */
    private boolean isNumeric(String str){
        try{
            if (str.contains(",")) str = str.replace(",", "");
            Long.parseLong(str);
            return true;
        }
        catch(Exception e){
            return false;
        }
    }


    @Override
  //**************************************************************************
  //** toString
  //**************************************************************************
  /**  Returns the full path to this directory, including the directory name.
   */
    public String toString(){
        return getPath();
    }


    @Override
    public int hashCode(){
        return getFile().hashCode();
    }

    //@Override
    public int compareTo(Object obj){
        if (obj==null) return -1;
        else return -obj.toString().compareTo(getPath());
    }


    @Override
  //**************************************************************************
  //** equals
  //**************************************************************************
  /** Returns true if the given Object is a Directory (or java.io.File) that
   *  refers to the current directory.
   */
    public boolean equals(Object obj){
        if (obj instanceof Directory){
            return getFile().equals(((Directory) obj).toFile());
        }
        else if (obj instanceof java.io.File){
            if (((java.io.File) obj).isDirectory())
                return getFile().equals(obj);
            else
                return false;
        }
        else{
            return false;
        }
    }


  //**************************************************************************
  //** clone
  //**************************************************************************
  /** Creates a copy of this object. */

    public Directory clone(){
        return new Directory(this.toString());
    }


  //**************************************************************************
  //** getEvents
  //**************************************************************************
  /** Used to start monitoring changes made to the directory. Changes include
   *  creating, modifying, or deleting files/folders found in this directory.
   *  Returns a list of Directory.Event(s). Caller can wait for new events
   *  using the wait() method like this:
   <pre>
    java.util.List events = directory.getEvents();
    while (true){
        Directory.Event event;
        synchronized (events) {
            synchronized (events) {
                while (events.isEmpty()) {
                    try {
                        events.wait();
                    }
                    catch (InterruptedException e) {}
                }
                event = (Directory.Event) events.remove(0);
            }
        }

        if (event!=null){
            System.out.println(event.toString());
            if (event.getEventID()==event.RENAME){
                System.out.println(
                    event.getOriginalFile() + " vs " +
                    event.getFile()
                );
            }
        }
    }
    </pre>
   *  Note that in this example, we are removing events from the list in the
   *  order that they occur. It is critical to remove events from the list to
   *  as quickly as possible to avoid potential memory issues. Use the stop()
   *  method to stop monitoring events. Obviously you will need to start the
   *  monitor in a separate thread in order to call the stop() method. Example:
   <pre>

  //Start directory monitor
    new Thread(() -> {
        java.util.List events = directory.getEvents();
        while (true){
            ...
        }
    }).start();


  //Stop directory monitor
    directory.stop();
   </pre>
   */
    public List getEvents() throws Exception {

        if (FileSystemWatcher==null){
            FileSystemWatcher = new FileSystemWatcher(this);
            new Thread(FileSystemWatcher).start();
        }
        return FileSystemWatcher.getEvents();
    }

  //**************************************************************************
  //** Stop
  //**************************************************************************
  /** Used to stop any worker threads that may be running (recursive search or
   *  event monitor).
   */
    public void stop(){
        if (FileSystemWatcher!=null) FileSystemWatcher.stop();

        try{
            //for (int i=0; i<20; i++)
            DirectorySearch.stop();
        }
        catch(Exception e){}
    }


  //**************************************************************************
  //** Finalize
  //**************************************************************************
  /** Method called by Java garbage collector to dispose operating system
   *  resource.
   */
    protected void finalize() throws Throwable {
       stop();
       super.finalize();
    }


  //**************************************************************************
  //** Event Class
  //**************************************************************************
  /** Used to encapsulate an event on the file system (e.g. create, update,
   *  rename, or delete).
   */
    public static class Event {

        private String file;
        private String orgFile;
        private java.util.Date date;
        private String action;

        public static final int DELETE = 0;
        public static final int CREATE = 1;
        public static final int RENAME = 2;
        public static final int MODIFY = 3;

      //************************************************************************
      //** Constructor
      //************************************************************************
      /**  Creates a new instance of FileSystemEvent by parsing a string
       *   returned from FileSystemWatcherNative.ReadDirectoryChangesW()
       */
        protected Event(String event) {
            if (event!=null){
                event = event.trim();
                if (event.length()==0) event = null;
            }
            if (event!=null){
                try{

                  //Parse event string
                    String date = event.substring(1,event.indexOf("]")).trim();
                    String text = event.substring(event.indexOf("]")+1).trim();
                    String path = text.substring(text.indexOf(" ")).trim();
                    String action = text.substring(0,text.indexOf(" ")).trim();



                  //Set local variables
                    this.date = parseDate(date);
                    this.file = path;
                    this.action = action;

                }
                catch(Exception ex){
                    //ex.printStackTrace();
                }
            }
        }

      //************************************************************************
      //** Constructor
      //************************************************************************
      /**  Creates a new instance of FileSystemEvent for EventMonitor
       */
        protected Event(String action, String file){
            this.date = new java.util.Date();
            this.action = action;
            this.file = file;
        }


      //************************************************************************
      //** parseDate
      //************************************************************************
        private java.util.Date parseDate(String date){
            try{
                return new java.text.SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy").parse(date);
            }
            catch(Exception e){
                return null;
            }
        }


      //************************************************************************
      //** getAction
      //************************************************************************
      /**  Returns a decription of the event (created, modified, deleted, etc.)
       */
        public String getAction(){
            return action;
        }

        protected void setAction(String action){
            this.action = action;
        }

      //************************************************************************
      //** getFile
      //************************************************************************
      /** Returns the path of the file or directory that was created, modified,
       *  or deleted.
       */
        public String getFile(){
            return file;
        }


      //************************************************************************
      //** getOriginalFile
      //************************************************************************
      /** If a file or directory was moved or renamed, returns the path to the
       *  original file or directory.
       */
        public String getOriginalFile(){
            return orgFile;
        }

        protected void setOrgFile(String orgFile){
            this.orgFile = orgFile;
        }


      //************************************************************************
      //** getEventID
      //************************************************************************
        public final int getEventID(){
            if (action.equalsIgnoreCase("create")) return this.CREATE;
            if (action.equalsIgnoreCase("delete")) return this.DELETE;
            if (action.equalsIgnoreCase("modify")) return this.MODIFY;
            if (action.equalsIgnoreCase("rename")) return this.RENAME;
            return -1;
        }

      //************************************************************************
      //** getDate
      //************************************************************************
      /** Returns the date/time stamp when the event occurred.
       */
        public java.util.Date getDate(){
            return date;
        }

      //************************************************************************
      //** toString
      //************************************************************************
      /** Returns a string representation of this event.
       */
        public String toString(){
          if (action.equalsIgnoreCase("rename"))
            return "[" + date.toString() + "] " + action + " " + orgFile + " To " + file;
          else
            return "[" + date.toString() + "] " + action + " " + file;
        }

        public boolean equals(Object obj){
            //return obj.toString().equalsIgnoreCase(action);
            if (obj instanceof Event){
                Event event = (Event) obj;
                if (event.getFile().equals(this.file) &&
                    event.getDate().equals(this.date) &&
                    event.getAction().equals(this.action)){
                    return true;
                }
            }
            return false;
        }

    } //End Event Class



  //**************************************************************************
  //** sort
  //**************************************************************************
    private void sort(List files, Directory dir){
        try{
            Collections.sort(files, new FileComparer(dir));
        }
        catch(Throwable e){
            //java.lang.IllegalArgumentException: Comparison method violates its general contract!
        }
    }


  //**************************************************************************
  //** FileComparer Class
  //**************************************************************************
  /**  Used to sort a list containing files/folders in alphabetical order.
   *   Note that directories are listed first.
   */
    private class FileComparer implements Comparator {

        private int z;

        public FileComparer(Directory dir){
            if (dir==null) z = 0;
            else z = dir.toString().replace("\\", "/").length();
        }

        public final int compare(Object a, Object b){

            String x = a.toString().toUpperCase();
            String y = b.toString().toUpperCase();
            x = x.replace("\\", "/").substring(z);
            y = y.replace("\\", "/").substring(z);


          //Check whether a or b is a file in the root directory.
            if (!x.contains("/") || !y.contains("/")){

              //If both a and b are files, compare file names. Use a multiplier
              //to ensure that the file falls toward the end of the list.
                if (!x.contains("/") && !y.contains("/")){
                    return (x.compareTo(y)) * 10000;
                }
                else{
                    return 100000;
                }
            }
            else{
              //Niether a or b are in the root directory


              //Check whether a and b are in the same directory
                String dir1 = x.substring(0, x.lastIndexOf("/"));
                String dir2 = y.substring(0, y.lastIndexOf("/"));

                if (!dir1.equals(dir2)){
                    return dir1.compareTo(dir2);
                }
                else{

                  //a and b are in the same directory

                    return x.compareTo(y);
                }
            }


        }

    } //End FileComparer Class







//******************************************************************************
//**  FileFilter
//******************************************************************************
/**
 *   Used to filter files and file names using regular expressions or java.io
 *   FileFilters. Note that directories are always returned.
 *
 ******************************************************************************/

private class FileFilter {

    private java.io.FileFilter fileFilter = null;
    private List<java.util.regex.Pattern> regex = null;


    public FileFilter(Object filter){

        if (filter==null){
            filter = "*";
        }

        if (filter instanceof java.io.FileFilter){
            fileFilter = (java.io.FileFilter) filter;
        }

        if (filter instanceof String){
            filter = new String[]{(String) filter};
        }

        if (filter instanceof String[]){
            regex = new ArrayList<java.util.regex.Pattern>();
            String[] filters = (String[]) filter;

            for (int i=0; i<filters.length; i++){
                regex.add(
                        java.util.regex.Pattern.compile(
                            getRegEx(filters[i]),
                            java.util.regex.Pattern.CASE_INSENSITIVE
                        )
                );
            }
        }
    }


    public boolean accept(String file){
        if (fileFilter!=null){
            return accept(new java.io.File(file));
        }
        else{
            file = file.replace("\\", "/");
            if (file.endsWith("/")){ //then we're dealing with a directory
                return true;
            }
            else{
                file = file.substring(file.lastIndexOf("/")+1);
            }

            for (int i=0; i<regex.size(); i++){
                java.util.regex.Matcher matcher = regex.get(i).matcher(file);
                if (matcher.find()){
                    return true;
                }
            }
        }
        return false;
    }

    public boolean accept(java.io.File file){
        if (file.isDirectory()) {
            return true;
        }
        else{
            if (fileFilter!=null){
                return (fileFilter.accept(file));
            }
            else{
                return accept(file.toString());
            }
        }
    }

  //**************************************************************************
  //** usesIOFilter
  //**************************************************************************
  /**  Used to indicate whether this filter relies on a java.io.FileFilter
   */
    public boolean usesIOFilter(){
        return (fileFilter!=null);
    }


  //**************************************************************************
  //** getRegEx
  //**************************************************************************
  /**  Used to convert a wildcard (e.g. "*.txt") into a regular expression.
   */
    private String getRegEx(String wildcardSearch){
        String regex = wildcardSearch.trim();
        //if (!regex.startsWith("*")) regex = "^" + regex;
        if (!regex.endsWith("*")) regex += "$";

        if (regex.endsWith("*")){
            //regex = regex.substring(0, regex.length()-1) + "/";
            regex = regex.substring(0, regex.length()-1) + ")";
            if (regex.contains("*")){
                regex = regex.substring(0, regex.lastIndexOf("*")+1) + "(" + regex.substring(regex.lastIndexOf("*")+1);
            }
            else{
                regex = "(" + regex;
            }
        }

        regex = regex.replace(".", "\\.");
        regex = regex.replace("*", ".*");
        //System.out.println(regex);
        return regex;
    }

}//End FileFilter Class


//******************************************************************************
//**  Directory Search
//******************************************************************************
/**
 *   Thread used crawl through a file system and find files/folders. This is a
 *   recursive search and may take some time to complete.
 *
 ******************************************************************************/

private static class DirectorySearch implements Runnable {



    private static java.util.concurrent.ConcurrentHashMap map;
    private static java.util.concurrent.ConcurrentHashMap lut;


  //**************************************************************************
  //** Constructor
  //**************************************************************************
  /**  Creates a new instance of DirectorySearch. Note that this thread needs
   *   to be named! Unfortunately, this has to happen outside of this class.
   *   Example:
   *
   <pre>
      Thread t = new Thread(new DirectorySearch(filter, files));
      t.setName("DirectorySearch-"+id);
      t.start();
   </pre>
   *
   *   @param filter A file filter.    *
   *   @param items A List (e.g. LinkedList) used to store items found in this
   *   directory.
   */
    public DirectorySearch(FileFilter filter, List items, long directoryID, int numThreads) {


        String parentThread = Thread.currentThread().getName();
        //System.out.println(parentThread);

        if (map==null) {
            map = new ConcurrentHashMap();
        }



        if (getVars()==null){
            createVars(filter, items, directoryID, numThreads);
        }


        if (lut==null) lut = new ConcurrentHashMap();
        synchronized(lut){
            if (lut.get(parentThread)==null)
                lut.put(parentThread, directoryID);
            else{

                //System.out.println(parentThread + " entering wait state...");
                while (!lut.isEmpty()) {
                    try {
                        lut.wait();
                    }
                    catch (InterruptedException e) {
                      //If interrupted, return immediately
                        Thread.currentThread().interrupt();
                        return;
                    }
                }
                //System.out.println(parentThread + " exited wait state!");

                createVars(filter, items, directoryID, numThreads);

                lut.put(parentThread, directoryID);
                lut.notifyAll();
            }
        }

        //System.out.println(parentThread + " " + lut.get(parentThread) + " vs " + directoryID);
    }


  //**************************************************************************
  //** createVars
  //**************************************************************************
  /** Used to instantiate local variables used by this thread and it's siblings.
   *  The variables are all stored in a hashmap and accessed via a directoryID.
   */
    private static void createVars(FileFilter filter, List items, long directoryID, int numThreads){
        synchronized(map){
            ConcurrentHashMap vars = new ConcurrentHashMap();
            vars.put("items", items);
            vars.put("filter", filter);
            vars.put("pool", new java.util.LinkedList());
            vars.put("path", new java.util.LinkedList());
            vars.put("status", new java.util.LinkedList());
            vars.put("threads", new LinkedList()); //<--list of active threads
            vars.put("activatedThreads", new ConcurrentHashMap());
            vars.put("numThreads", numThreads);
            map.put(directoryID, vars);
        }
    }



  //**************************************************************************
  //** getVars
  //**************************************************************************
  /** Used to retrieve variables associated with this thread.
   */
    private static ConcurrentHashMap getVars(){

        String currentThread = Thread.currentThread().getName();
        if (currentThread.startsWith("DirectorySearch_")){
            Long directoryID = getDirectoryID();
            return (ConcurrentHashMap) map.get(directoryID);
        }
        else{

            if (lut==null) return null;

            Long directoryID = null;
            synchronized(lut){
                directoryID = (Long) lut.get(currentThread);
            }

            //System.out.println(currentThread + " vs " + directoryID);

            if (directoryID==null) return null;
            else return (ConcurrentHashMap) map.get(directoryID);
        }
    }


  //**************************************************************************
  //** deleteCache
  //**************************************************************************

    public static void deleteCache(){

        if (map!=null){
            ConcurrentHashMap vars = getVars();
            if (vars!=null){
                synchronized(vars){
                    List path = (List) vars.get("path");
                    List items = (List) vars.get("items");
                    List status = (List) vars.get("status");


                    synchronized(path){ path.clear(); }
                    synchronized(items){ items.clear(); }
                    synchronized(status){ status.clear(); }

                    vars.remove("startTime");
                    vars.remove("root");
                }
            }
        }
    }


    private static Long getDirectoryID(){
        try{
            String id = Thread.currentThread().getName();
            id = id.substring(id.indexOf("_")+1);
            id = id.substring(0,id.indexOf("-"));
            //System.out.println(id + " vs " + Thread.currentThread().getName());
            return Long.valueOf(id).longValue();
        }
        catch(Exception e){
            return 0L;
        }
    }




  //**************************************************************************
  //** Stop
  //**************************************************************************
  /** Used to stop the current thread. */

    public static void stop(){
        updatePool(null);
    }


  //**************************************************************************
  //** updatePool
  //**************************************************************************
  /**  Used to add a new directory to the pool. */

    public static void updatePool(Directory directory) {

        ConcurrentHashMap vars = getVars();
        Long startTime = null;
        if (vars.get("startTime")!=null) startTime = (Long) vars.get("startTime");



      //if start time is null, then this is the first time the pool has been modified
        if (startTime==null){
            synchronized(vars){
                vars.put("startTime", getStartTime());
                vars.put("root", directory);
                vars.notifyAll();
            }
        }


        boolean updatePool = true;
        if (directory == null){
            if (Thread.currentThread().getName().equalsIgnoreCase("Finalizer")){
                updatePool = false;
            }
        }

        if (updatePool){
            List pool = (List) vars.get("pool");
            synchronized (pool) {
                pool.add(directory);
                pool.notifyAll();
            }
        }


    }


  //**************************************************************************
  //** Add File
  //**************************************************************************
  /**  Used to add a file to the list of items found. */

    private static void addFile(File file) {

        ConcurrentHashMap vars = getVars();
        List items = (List) vars.get("items");

        synchronized (items) {
            items.add(file);
            items.notifyAll();
        }
    }

  //**************************************************************************
  //** Add Directory
  //**************************************************************************
  /**  Used to add a directory to the list of items found. */

    private static void addDirectory(Directory directory){

        ConcurrentHashMap vars = getVars();
        List items = (List) vars.get("items");
        Directory root = (Directory) vars.get("root");

        synchronized (items) {
            if (!directory.equals(root)){
                items.add(directory);
                items.notifyAll();
            }
        }
    }




    private static void addPath(Directory directory) {
        List path = (List) getVars().get("path");
        synchronized (path) {
            path.add(directory.toString());
        }
    }

    private static void removePath(Directory directory) {
        List path = (List) getVars().get("path");
        synchronized (path) {
            path.remove(directory.toString());
        }
    }


  //**************************************************************************
  //** Get Status
  //**************************************************************************
  /**  Used to retrieve the status object. The status object is a List that
   *   will remain empty until all of the threads have completed searching
   *   the file system. While status.isEmpty() call status.wait();
   */
    public static List getStatus(){

        ConcurrentHashMap vars = getVars();
        List status = (List) vars.get("status");

        return status;
    }


  //**************************************************************************
  //** Update Status
  //**************************************************************************
  /**  Used to insert an entry into the status object. This is used to notify
   *   the client that all threads have completed searching the file system.
   */
    private static void updateStatus(){

        ConcurrentHashMap vars = getVars();
        List status = (List) vars.get("status");
        List items = (List) vars.get("items");
        Long startTime = (Long) vars.get("startTime");

        synchronized (status) {
         status.add("ellapsedTime = " + getEllapsedTime(startTime) + " ms");
         status.notifyAll();
         //System.out.println(Thread.currentThread().getName() + " Updated Status!");
        }
        synchronized (items) {
          items.add(null);
          items.notifyAll();
        }


        ((List) vars.get("pool")).clear();
        ((List) vars.get("path")).clear();
        ((List) vars.get("threads")).clear();
        ((ConcurrentHashMap) vars.get("activatedThreads")).clear();



      //Update the lut
        synchronized(lut){
            long directoryID = getDirectoryID();
            java.util.Iterator it = lut.keySet().iterator();
            while (it.hasNext()){
                String parentThread = (String) it.next();
                if ((Long) lut.get(parentThread) == directoryID){
                    lut.remove(parentThread);
                    lut.notifyAll();
                    break;
                }
            }
        }

    }


  //**************************************************************************
  //** Get Items
  //**************************************************************************
  /**  Used to retrieve a list of files and folders found in this directory.
   */
    public static List getItems(){
        return (List) getVars().get("items");
    }






  //**************************************************************************
  //** Run
  //**************************************************************************
  /** Processes entries in the pool. Waits and add children to the array. */

    public void run() {

      //Get shared variables
        ConcurrentHashMap vars = getVars();
        if (vars==null) return; //<--This should never happen!

        List threads = (List) vars.get("threads");
        ConcurrentHashMap activatedThreads = (ConcurrentHashMap) vars.get("activatedThreads");
        List path = (List) vars.get("path");
        List pool = (List) vars.get("pool");
        FileFilter filter = (FileFilter) vars.get("filter");
        int numThreads = (Integer) vars.get("numThreads");
        String threadID = Thread.currentThread().getName();



        while (true) {
            Directory dir;

          //Wait for new directories to be added to the pool
            synchronized (pool) {
                while (pool.isEmpty()) {
                    try {
                        pool.wait();
                    }
                    catch (InterruptedException e) {
                      //If interrupted, return immediately
                        Thread.currentThread().interrupt();
                        return;
                    }
                }

              //Update the list of active threads
                synchronized (threads){threads.add(threadID);}
                synchronized (activatedThreads){
                    if (activatedThreads.get(threadID)==null){
                        activatedThreads.put(threadID, true);
                    }
                }



                Object obj = pool.get(0);
                if (obj==null){
                    //System.out.println("Terminating Thread: " + threadID);
                    synchronized (threads){threads.remove(threadID);}
                    synchronized(activatedThreads){
                        activatedThreads.replace(threadID, false);
                        if (activatedThreads.size()==numThreads){
                            if (!activatedThreads.containsValue(true)){
                                updateStatus();
                            }
                        }
                    }
                    return;
                }
                else{
                    dir = (Directory) obj;
                    pool.remove(obj);
                    pool.notifyAll();
                }
            }


          //Process directory
            if (dir!=null) {

              //Notify Path/Status Object of new task
                addPath(dir);



              //Add subdirectories to the processing pool and insert files into file array
                Object[] items = dir.listFiles();
                if (items!=null){
                    for (int i=0; i<items.length; i++){
                        Object obj = items[i];
                        boolean isDirectory = false;
                        boolean accept = false;
                        if (obj instanceof String){
                            String s = (String)obj;
                            accept = filter.accept(s);
                            isDirectory = s.replace("\\", "/").endsWith("/");
                        }
                        else if (obj instanceof java.io.File){
                            java.io.File f = (java.io.File)obj;
                            accept = filter.accept(f);
                            isDirectory = f.isDirectory();
                        }

                        if (accept){

                            if (isDirectory){

                              //Add directory to the array and update the pool
                                Directory d = null;
                                if (obj instanceof String){
                                    d = (new Directory((String)obj));
                                }
                                else if (obj instanceof java.io.File){
                                    d = (new Directory((java.io.File)obj));
                                }
                                if (d!=null){
                                    addDirectory(d);
                                    updatePool(d);
                                }
                            }
                            else{

                             //Add File to the array
                                if (obj instanceof String){
                                    addFile(new File((String)obj));
                                }
                                else if (obj instanceof java.io.File){
                                    addFile(new File((java.io.File)obj));
                                }

                            }
                        }
                    }
                }



              //Notify Path/Status Object of task completion
                removePath(dir);


                synchronized (threads){
                  threads.remove(threadID);
                }


              //Check whether we're done processing all the directories and notify the client
                synchronized (path) {
                  if (path.isEmpty()){
                      synchronized (pool) {
                          if (pool.isEmpty()){
                              synchronized (threads){
                                if (threads.isEmpty()){
                                    //System.out.println("Terminating Thread: " + threadID + "*");
                                    synchronized(activatedThreads){activatedThreads.replace(threadID, false);}
                                    updatePool(null);
                                    return;
                                }
                              }
                          }
                      }
                  }
                }



            }
            /*
            else{
              //Found null entry. This is our que to stop waiting for new directories...
                //System.out.println("Terminating Thread: " + Thread.currentThread().getName());




                synchronized (threads){
                    String threadID = Thread.currentThread().getName();
                    threads.remove(threadID);
                }


                synchronized (activatedThreads){
                    activatedThreads.replace(Thread.currentThread().getName(), false);
                    if (activatedThreads.containsValue(true)){
                        updatePool(null);
                    }
                    else{

                        if (activatedThreads.size()<numThreads){
                            updatePool(null);
                        }
                        else{
                            //System.out.println(this.getDirectoryID() + " activated " + activatedThreads.size() + " threads");
                            updateStatus();
                        }
                    }
                }

                return;
            }
            */


        }//end while


    } // end run



  //**************************************************************************
  //** Get Start Time
  //**************************************************************************
  /**  Used to set the start time. Returns the current time in milliseconds. */

    private static long getStartTime(){
        return java.util.Calendar.getInstance().getTimeInMillis();
    }


  //**************************************************************************
  //** Get Ellapsed Time
  //**************************************************************************
  /**  Used to compute the ellapsed time in milliseconds. */

    private static long getEllapsedTime(long StartTime){
        try{
            long endTime = java.util.Calendar.getInstance().getTimeInMillis();
            return endTime-StartTime;
        }
        catch(Exception e){
            return 0;
        }
    }

}//End Directory Search







//******************************************************************************
//**  FileSystemWatcher Class
//******************************************************************************
/**
 *   Used to watch changes made to files and folders (subdirectories) found in
 *   a directory. Provides an option to monitor NTFS events on Windows-based
 *   machines. This is an extremely efficient way to monitor a file system.
 *   Unlike more traditional methods, this approach minimizes the amount of
 *   disk i/o required and significantly reduces memory consumption. Note that
 *   this approach only works on Windows 2000, XP, or later and you need a
 *   dynamic link library called javaxt-core.dll. Again, this is just an option.
 *   For non-Windows operation systems, the FileWatcher class will periodically
 *   scan the file system for updates.
 *
 ******************************************************************************/

private class FileSystemWatcher implements Runnable {

    private Directory directory;
    private Timer timer;
    private boolean includeSubdirectories = true;
    private boolean terminationRequested = false;
    private Long osHandle = null;

    private List events = new LinkedList();
    private Directory.Event LastEvent = null;

  //**************************************************************************
  //** Constructors
  //**************************************************************************
  /** Creates a new instance of FileSystemWatcher using a directory and a dll.
   */
    public FileSystemWatcher(Directory directory) throws java.io.IOException {
        if (!directory.exists()) throw new java.io.IOException("Directory not found.");
        this.directory = directory;
    }


  //**************************************************************************
  //** Run
  //**************************************************************************

    public final void run(){

        if (!File.loadDLL()){
            this.timer = new Timer();
            timer.schedule( new EventMonitor(), new java.util.Date(), 1000 );
        }
        else{
            try {
                long osWaitHandle = FileSystemWatcherNative.FindFirstChangeNotification(directory.getPath(), includeSubdirectories, -1);
                this.osHandle = new Long(osWaitHandle);

                FileSystemWatcherNative.FindNextChangeNotification(osWaitHandle);

              //Process events. Note that the ReadDirectoryChangesW method will
              //block until the next event comes in.
                String event = null;
                while(!terminationRequested && ( event = FileSystemWatcherNative.ReadDirectoryChangesW()) != null){
                    for (String e : event.split("\n")) addEvent(e.trim());
                }

              //I have no idea what this block of code does. Probably safe to delete.
                if (FileSystemWatcherNative.WaitForSingleObject(osWaitHandle,
                    FileSystemWatcherNative.INFINITE) !=  FileSystemWatcherNative.WAIT_OBJECT_0) {
                    throw new Exception("Wait failed while waiting for OS to signal file system event.");
                }
            }
            catch (Exception ex) {
              //nothing can be done here except logging the error.
              //Logger.getLogger("FileSystemWatcher").log(Level.WARNING, "Exception encountered.", ex);
            }
            finally {
                if (this.osHandle != null) {
                    System.out.println("Shutting down...");
                    try {
                      FileSystemWatcherNative.FindCloseChangeNotification(this.osHandle.longValue());
                    }
                    catch (Exception ex2) {
                      //nothing can be done here except logging the error.
                      //Logger.getLogger("FileSystemWatcher").log(Level.WARNING,
                      //"Unable to close file system watch handle.", ex2);
                    }
                    this.osHandle = null;
                }
            }
        }
    }


  //**************************************************************************
  //** addEvent
  //**************************************************************************

    private void addEvent(String str){

        Directory.Event event = new Directory.Event(str);
        String action = event.getAction();
        String path = event.getFile();
        java.util.Date date = event.getDate();


        boolean exists = true;
        boolean isDirectory = false;
        try{
            isDirectory = new File.FileAttributes(path).isDirectory();
        }
        catch(Exception e){
            exists = false;
        }

        boolean updateEvents = true;


      //Determine whether to update the events list when a file or folder is modified
        if (action.equalsIgnoreCase("modify")){
            if (isDirectory || !exists){
                updateEvents = false;
            }
            else{
                if (LastEvent!=null) {
                    if (LastEvent.getFile().equals(path)){
                        if (LastEvent.getDate().equals(date)){
                            updateEvents = false;
                        }
                    }
                    LastEvent = null;
                }
                else{
                    LastEvent = event;
                }
            }
        }


      //Special Case: If a rename event is encountered, wait for the "renam2"
      //event before updating the events list.
        if (action.equalsIgnoreCase("rename")){
            updateEvents = false;
            LastEvent = event;
        }
        else if(action.equalsIgnoreCase("renam2")){
            if (LastEvent!=null) {
                event.setOrgFile(LastEvent.getFile());
                event.setAction("Rename");
                LastEvent = null;
            }
        }


      //Update events list, as needed
        if (updateEvents){
            synchronized(events){
                events.add(event);
                events.notifyAll();
                //System.out.println(events.size());
            }
        }
    }


  //**************************************************************************
  //** getEvents
  //**************************************************************************

    public List getEvents() {
        return events;
    }


  //**************************************************************************
  //** Stop
  //**************************************************************************

    public void stop(){

        terminationRequested = true;

        if (timer!=null){
            timer.cancel();
            timer = null;
        }
    }


  //**************************************************************************
  //** EventMonitor
  //**************************************************************************
  /** Used to periodically check for changes made to the file system. This
   *  class is only used on non-windows machines.
   */
    private class EventMonitor extends TimerTask {

        private List index = null;
        private long lastUpdate = 0;
        private long interval = 0;

        public EventMonitor(){
            this.index = createIndex();
        }

        public final void run() {
            //System.out.println((lastEvent-lastUpdate) + " vs " + interval);
            long startTime = java.util.Calendar.getInstance().getTimeInMillis();
            if (interval==0) interval = 100;
            if ((startTime-lastUpdate)>(interval*2)){
                //System.out.println((startTime-lastUpdate) + " vs " + (interval));

                List orgIndex = index;
                List newIndex = createIndex();

                for (int i=0; i<newIndex.size(); i++){
                    Item item = (Item) newIndex.get(i);
                    if (orgIndex.contains(item)){
                        int x = orgIndex.indexOf(item);
                        Item orgItem = (Item) orgIndex.get(x);
                        orgIndex.remove(x);
                        if (item.isDirectory()==false){
                            if (item.getSize()!=orgItem.getSize() ||
                                item.getDate()!=orgItem.getDate())
                            {
                                addEvent("Modify",item.getPath());
                            }
                        }
                    }
                    else{
                        addEvent("Create",item.getPath());
                    }
                }
                if (orgIndex.size()>0){
                    for (int i=0; i<orgIndex.size(); i++){
                        Item item = (Item) orgIndex.get(i);
                        addEvent("Delete",item.getPath());
                    }
                    orgIndex.clear();
                }

                index = newIndex;
                long endTime = java.util.Calendar.getInstance().getTimeInMillis();
                interval = endTime-startTime;
                //System.out.println(interval);
                lastUpdate = endTime;


            }// end if
        }// end run


        private void addEvent(String action, String file){
            Directory.Event event = new Directory.Event(action,file);
            synchronized (events) {
                events.add(event);
                events.notifyAll();
            }
        }


      //************************************************************************
      //** createIndex
      //************************************************************************
      /** Used to create an array of files and folders found in a directory.
       *   Also used to update the interval variable used by the EventMonitor.
       */
        private List createIndex(){
            List index = new LinkedList();
            java.util.List files = directory.getChildren(true, null, false);
            Object obj;
            while (true){
                synchronized (files) {
                    while (files.isEmpty()) {
                      try {
                          files.wait();
                      }
                      catch (InterruptedException e) {
                          break;
                      }
                    }
                    obj = files.remove(0);
                    files.notifyAll();
                }

                if (obj==null){
                    break;
                }
                else{
                    index.add( new Item(obj) );
                }
            }
            return index;
        }



      //************************************************************************
      //** Item Class
      //************************************************************************
      /**  Used to represent a single file or folder. Records the size and date
       *   of the file/folder at the time this object is instantiated. This
       *   information is used to determine which items have changed state
       *   within the EventMonitor class.
       */
        private class Item {

            private String path;
            private long size;
            private long date;
            private boolean isDirectory;

            public Item(Object obj){

                java.io.File file = null;

                if (obj instanceof File) file=((File) obj).toFile();
                else if (obj instanceof Directory) file=((Directory) obj).toFile();
                else if (obj instanceof java.io.File) file=(java.io.File) obj;

                if (file!=null){
                    this.path = file.getPath();
                    this.size = file.length();
                    this.date = file.lastModified();
                    this.isDirectory = file.isDirectory();
                }
            }

            public String getPath() { return path; }
            public long getSize() {  return size; }
            public long getDate() { return date; }
            public boolean isDirectory() { return isDirectory; }
            public String toString(){ return path; }

            public boolean equals(Object obj){

                if (obj instanceof Item){
                    Item item = (Item) obj;
                    return item.getPath().equals(this.getPath());
                }
                else{
                    return false;
                }
            }

        } // End Item

    } //End EventMonitor Class


  //**************************************************************************
  //** Finalize
  //**************************************************************************
  /** Method called by Java garbage collector to dispose operating system
   *  resource.
   */
    protected void finalize() throws Throwable {
      if (this.osHandle != null) {
          FileSystemWatcherNative.FindCloseChangeNotification(this.osHandle.longValue());
      }
    }

} //End FileSystemWatcher Class

} //End Directory Class

//******************************************************************************
//**  FileSystemWatcherNative Class
//******************************************************************************
/**
 *   Java wrapper of Windows native APIs for file system monitoring. The name of
 *   the methods in this class corresponds to the name of Windows native APIs.
 *   You might want to check Windows Platform SDK for detailed technical
 *   information. <p/>
 *
 *   Also note that if you change the package or method name of this class, you
 *   need to use javah to regenerate a C header file and recompile the dll file.
 *   As far as I know, there is no way around it because of the way JVM binds to
 *   native dll.
 *
 ******************************************************************************/

final class FileSystemWatcherNative {

    static {}

    public static native long FindFirstChangeNotification(String lpPathName,
            boolean bWatchSubtree, int dwNotifyFilter) throws Exception;

    public static native void FindNextChangeNotification(long hChangeHandle)
    throws Exception;

    public static native void FindCloseChangeNotification(long hChangeHandle)
    throws Exception;

    public static native int WaitForSingleObject(long hHandle, int dwTimeoutMilliseconds);

    public static native String ReadDirectoryChangesW();

  //**************************************************************************
  //** Static Local Variables
  //**************************************************************************

  /** A constant value representing infinite. */
    public static final int INFINITE = 0xFFFFFFFF;  //(WinBase.h)

  /** A wait return value indicating a failed wait. */
    public static final int WAIT_FAILED = 0xFFFFFFFF;

  /** A wait return value indicating that the specified object is a mutex object that was not released by the thread.  */
    public static final int WAIT_ABANDONED = 0x00000080;

  /** A wait return value indicating The state of the specified object is signaled. */
    public static final int WAIT_OBJECT_0 = 0x00000000;

  /** The time-out interval elapsed, and the object's state is nonsignaled. */
    public static final int WAIT_TIMEOUT = 0x00000102;


} // End FileSystemWatcherNative