Working with Local Files

by Tom Trinkos

While most of the hype about Java centers on its use in applets on the Web, its greatest long-term strength may be in delivering cross-platform applications. I'd say that coding in Java will be a lot cheaper than in C++ because of Java's automatic garbage collection and lack of pointers—i.e., bugs waiting to happen.

However, a key aspect of real-world applications is working with files, something not often covered in Java discussions. The good news is that Java has abstracted the concept of files sufficiently to allow you to write code that will work with files on any platform that supports Java.

In this article, we'll show you how to find all of the files contained in a directory (or folder for us Mac fans), including those contained in subdirectories, sub-subdirectories, etc. We'll also show how to pass functions into classes without using function pointers—which Java doesn't support. The code we'll develop provides a skeleton you can use to apply an arbitrary method to all of the files in any directory or on any hard disk or group of disks.

Overview

We'll use an application for this program because we need to access files. The same approach should work with applets that have been granted security approval, either via browser options or because they're digitally signed.

The application lets the user select a directory, then generates a file containing a report about all of the files contained in that directory or in any of its subdirectories. We'll see how we can modify this framework by changing only one class, so that we can apply an arbitrary function—say delete—to every file found. Before we dive into the details of how this all works, it's a good idea to get a quick overview of what each of the five classes does.

find_file class

This class contains the main method. It performs some initialization and cleanup when everything is finished. Most of the real work is done by the other classes.

directory class

This is a class for a directory. This class finds all of the files and applies the user-specified function, contained as a method on the file_function class, to each file found.

file_function class

We use this class to pass a method to the directory class. We can customize the method in this class, thereby changing what the application does, without modifying any of the other classes.

read_write_file class

This abstract class contains the methods necessary to open and close files for reading and writing.

report file class

This is a subclass of the read_write_file class that writes a text report out to a disk file. The main method in the find_file class lets the user select a directory. It then creates an instance of the file_function class, which in turn creates a new instance of the report_file class. Next, it creates an instance of the directory class, which it then tells to find all of the files it contains. As the directory class finds files, it calls the the_function method of the file_function class for each of them. The sample method in this example writes a report with one line/file containing the path of the file and the size of the file in KB. When the directory instance is finished, the find_file class tells the file_function class to clean up the report file.

The sample application

Let's walk through this sample program to see how to work with files.

The application framework—the find_file class

The find_file class has no attributes and only one method, called main. The main() method controls the operation of our example. It imports the following items:

import java.awt.*;
import java.io.*;
import java.util.Vector;

While the program won't use every item in these libraries, it's not worth our time to list the items we'll use. The compiler will make sure that only the ones we need are called so that runtime efficiency won't be affected, but we'll save lots of typing and testing time.

The main() method does three things: initialization, starting the work, and cleaning things up when the work is done. The main() method uses several local variables, as shown in Listing A.

Listing A: Our main() method uses several local variables

public static void main(){
//This is the object for the directory 
// we'll search.
   directory a_directory;
//We need a frame to show the user a dialog.
   Frame a_frame; 
//The dialog we'll use to let the user select which directory we'll search
   FileDialog get_directory_dialog;
//The directory path as a string
   String directory_name;
//The directory that will be searched
   File start_directory;
//loop variable
   int i;
//This is the object that contains the method which will be applied to every file found.
   file_function a_function;

The first initialization step is to have the user select the top-level directory the program will work with. We do this with the code in Listing B.

Listing B: Code to select top-level directory

//Need a frame to display a dialog. Note that we don't have to show the frame.
   a_frame = new Frame("testing stuff");
//Create a file dialog that lets the user select a file.
   get_directory_dialog = new 
   FileDialog(a_frame,"Select a file in the directory you wish to work with");
//Show the dialog. 
   get_directory_dialog.show();
//Get the users selection. 
   directory_name = get_directory_dialog.getDirectory();
//Make a file object with the path you get from the dialog. NOTE:we should do some checking //here to make sure that the user didn't cancel the dialog before trying to make a File //instance.
   start_directory = new File(directory_name);

This code first creates a frame, because we can't display a dialog without one. If we were writing a real application, we'd probably have a frame we could use, but in this simple example, we have to make one. Interestingly enough, though, we don't need to display the frame in order to create a dialog.

Please Note: You can use this same technique in applets to display dialogs.

The message to the user in the dialog is needed to explain what he or she is supposed to do. There's no way with a standard Java AWT dialog to select a directory. Hence, we have the user select a file in a folder, then get the folder's path from the file.

Once we've created the dialog, we can use the show() method to display it. The default behavior for the dialog is modal—that is, the call to show won't return until the user has finished with the dialog. As a result, we can immediately get the directory of the file the user selected using the getDirectory() method.

At this point, we can create a file object that represents both files and directories, using the path we just got for the directory. In a real application, we should check the value returned by the getDirectory() method to make sure that the user selected a file and didn't cancel out of the dialog.

The next step is to create and initialize an instance of a directory object (this is a class we define, not a Java built-in one) as shown in the code in Listing C.

Listing C: Code to create and initialize an instance of a directory object.

//The directory class contains the  
//search functions we want to use.
a_directory = new   directory(start_directory);

//The file function class allows us to 
//apply arbitrary functions to every
//file we find.
a_function = new 
file_function();

//Tell the directory object to use the
//file_function instance with every 
//file found.
   a_directory.set_function(a_function);

We'll see the full definition of the directory class shortly, but the important thing to realize is that the directory object is the one that will discover all of the files contained in the user-selected directory and apply the user-defined function to each of them.

Part of the initialization of the directory object is to tell it which function to apply to each file. Since Java doesn't have function pointers, we pass in an instance of the file_function class. That class has one method that the directory instance will call every time a file is found.

Now that everything is set up, we can tell the directory instance to find all files in the directory and display the results. The rest of the main() method, shown in Listing D, accomplishes that.

Listing D: The rest of the main method

//Get a list of the files found. Each element is a instance of File.
Vector found_files = a_directory.get_files();

//This tells the file_function 
// instance that we're done calling it so it can do any necessary cleanup.
   a_function.done();
   } //end of main method

The get_files() method returns a vector containing File instances for all of the files and directories that are found. The done() method, invoked on the file_function instance, makes sure that we've accomplished any cleanup, such as closing a file or displaying a report, required by the processing we've done on each file.

Working with directories—the directory class

This class provides an easy way to recurse through file structures and apply a user-specified method to each file that's found. The directory class definition begins with the code shown in Listing E. The creator method for the directory class sets the directory that the directory instance points to.

Listing E: Our initial code looks like this

import java.io.*;
import java.util.Vector;
/** This class is designed to recurse through a directory and all its subdirectories finding all files and applying a user supplied function to each one. */
public class directory {
//This is the top directory which will be searched.
   private File path;
//This is a list of File instances for every file contained, directly or indirectly, inside //the directory specified in the path attribute.
   private Vector contained_files;
//This is the object that contains the function we'll apply to each file.
   private file_function function;
/** Creator method 
@param a_path An instance of File for the top level directory */
   directory (File a_path) {
//NOTE: To be safe we should check here to make sure the supplied file is a directory using //the isDirectory()method--see below.
      path = a_path;
   } //end creator method

The set_function method sets the file_function instance that we'll use. While we could avoid the need for this instance by allowing the function attribute to be public, using accessor methods helps encapsulate data and allows us to change how the directory class works with file_function instances without impacting code in other classes. As a trivial example, with accessor methods we can change the name of the function attribute without having to worry about potential problems with code in other objects.

The get_files() method starts off the search for files contained in the directory specified in the path attribute, as shown in Listing F. The real work is done in the process_directory method.

Listing F: The get_files method starts the search for files contained in the directory specified in the path attribute

/**This method invokes a search for all files contained in the directory
specified by the path attribute. */
   public Vector get_files() {
      contained_files = new Vector(10);
      process_directory(path);
      return contained_files;
   }
private void process_directory(File top_directory) {
//Path of the directory supplied as an input parameter
   String the_directory;
//List of file names found in this directory
   String files[];
//local temp variables
   int i;
   File f;
//Some compilers require this line or you'll get a variable undefined error.
   f = null;
//First thing make sure the input file is in fact a directory.
   if (top_directory.isDirectory()) {
//Need to get the path to the directory as a string so we can use it to
//build the full path to the files as you'll see below.
      the_directory =           top_directory.getPath();
//This returns a list of the names of the files contained in the directory. We'll create full //paths for the files below.
      files =              top_directory.list();
//Loop over the found files/directories.
      for(i=0;i<files.length;i++) {
//Create a File instance for each found 
//item.
      f = new File(the_directory          + files[i]);
//If the item is a directory add a / to the end of the full file path and
//recurse.
      if (f.isDirectory()) {
       f = new File(the_directory + files[i] + "/");
       process_directory(f);
      } else {      
//If the item is a file then process the file.
       process_file(f);
      }
   } //Close the loop over items.
  } else { //if the input isn't a directory
   System.out.println("Error:director    y:process_directory: "
   + top_directory + " isn't a    directory");
  } //finish if that tests input param
}//finish method
/** This method applies whatever processing is required to each file 
@param a_file The file to be processed */
private void process_file(File a_file) {
//First update the instance attribute. 
  contained_files.addElement(a_file);
//Now call the specified function on the file.
   function.the_function(a_file);
}

The first thing this method does is to verify that the input parameter, top_directory, is a directory using the isDirectory() method of the File class. If it isn't, an error message is printed out. In a real program, you'd probably also want to raise an exception or do some other error processing.

If top_directory is a directory, then we get a list, using the list() method, of the names of the items, both files and directories, that it contains. The list returned is a list of strings with only the item names, not their full paths, e.g., test not /home/test. That's why we use the getPath() method to find the path to top_directory. We then append the filename to the containing directory path when we make a new File object.

We then check to see if the new object is a file or a directory. If it's a directory, we make a new File instance with a forward slash (/) at the end –(the name returned by list doesn't contain this) and call process_directory again on this newly found subdirectory.

If the new item is a file, we call the process_file() method. The process_file() method calls the the_function() method on the file_function instance, as shown above.

The key thing to note is that with this approach, we can completely change the nature of the method we apply to each file without having to change anything in the directory class.

Working with a file—the file_function class

This class encapsulates the processing we wish to apply to each file and provides an easy way to pass a function into another class—the directory class, in this example. In order to show how this works, we've assumed that our objective is to generate a report on the names and sizes of the files we find, which is useful for tracking down large, forgotten files, for example. The class definition, attributes, and creator method for file_function are shown in Listing G.

Listing G: The class definition, attributes, and creator method for file_function

import java.io.*;
import java.awt.*;
/**The purpose of this class is to provide a standard way to provide a function which is to be applied to every file found by some other class. This works because the other class just calls the_function method on this class so that the other class doesn't have to change when this class is changed as long as the signature of the_function doesn't change. */
public class file_function {
//This object will handle building a permanent report.
   report_file the_report;
//This is the total number of bytes in files processed.
   long total_bytes;
//This is the total number of files processed.
   int total_files;
/** Creator method initializes attributes and sets the file that the report will
be written to. */   
   file_function() {
      set_report_file();
      total_bytes = 0;
      total_files = 0;
   }
/** Accessor method to set the file that the report will be written to. */
   public void set_report_file() {
      the_report = new report_file();
   }

The only thing of interest here is how the initialization creates a new instance of the report_file class. We'll see below how that class's creator method will interact with the users to allow them to pick where they want the final report saved to. The method that's called for each file is shown in Listing H.

Listing H: The file_function class

/** This method is called for each file found 
@param a_file The file to process */
   public void the_function(File a_file) {
//This is the report on this file.
      String file_report;
//This is the size of the file in Kbytes.
      long size;
//Get the full file path/name.
      file_report = a_file.getPath();
//Get the size of the file in bytes.
      size = a_file.length();
//Keep the running total in bytes to minimize round off error.
      total_bytes = total_bytes + size;
//Convert file size to Kbytes.
      size = size/1024;
      file_report = file_report + " " + size + " Kbytes " + "\r";
      total_files++;
  the_report.write_data(file_report);
   }

It gets the size of the file, in bytes, by using the length() method of the File class, generating a one-line report showing the file path and its size, then writing that string to the report file by calling the write_data() method of the report_file class.

Working with reading/writing files—the read_write_file class

This class is an abstract class that provides all of the calls necessary for opening and closing files for reading and writing. The read_write_file class is shown in Listing I.

Listing I: Our read_write_file class

import java.io.*;
import java.net.*;
import java.util.Vector;
/** This is an abstract class which does the stuff necessary to open and close input and output. */
public class read_write_file {
   DataOutputStream file_out;
   DataInputStream file_in;
   String file_name;
   
   read_write_file(String the_file_name) {
      file_name = the_file_name;

   }
//For cases when we don't know the file name when we first create the an instance
   read_write_file() {}
//Open a file for writing.   
     public boolean open_file_out() {
      try {
         file_out = new DataOutputStream(new FileOutputStream(file_name));
         return true;
      } catch(Exception e) {
   System.out.println("ERROR:read_write_file:open_file_out: Problem opening file " + file_name + " error is " + e );
         return false;
      }
   }
//Close file opened for writing.
   public void close_file_out() {
      try {
         file_out.close();
      } catch(Exception e) {
   System.out.println("ERROR:read_write_file:close_file_out: Problem closing file " + file_name + " error is " + e );
      }
   }
//Open a file for reading.
   public boolean open_file_in() {
      File urls_temp;
      urls_temp = new File(file_name);
      if(urls_temp.exists()) {
        try {
          file_in = new DataInputStream(new FileInputStream(file_name));
          return true;
        } catch(Exception e) {
   System.out.println("ERROR:read_write_file:open_file_in: Problem opening file " + file_name + " error is " + e );
           return false;
        }
      } else {
      System.out.println("No file saved yet");
               return true;
      }
   }
//Close file opened for reading.
   public void close_file_in() {
      try {
         file_in.close();
      } catch(Exception e) {
   System.out.println("ERROR:read_write_file:close_file_in: Problem closing file " + file_name + " error is " + e );
      }
   }
} //End read_write_file class.

We can read data from and write data to a file by just providing a filename, then using the file_in and file_out attributes, as we'll see in the report_file class below.

Working with results—the report_file class

This class does two things: It finds out where the user wants to put the report generated by the file_function class, and it gives the file_function class a way to write the report to the user-selected file. This separation means that we could change the destination of the file_function class's output without having to touch the file_function class.

This class has no attributes of its own, but it does inherit some from the read_write_file class. The class definition, as well as the creator method, are shown in Listing J.

Listing J: Code containing read_write_file class with the creator method

import java.awt.*;
import java.io.*;
public class report_file extends read_write_file {
   report_file() {
      directory a_directory;
      Frame a_frame;
      FileDialog get_directory_dialog;
      String directory_name;
      File start_directory;
      int i;
      file_function a_function;
      
      a_frame = new Frame("testing stuff");
      get_directory_dialog = new FileDialog(a_frame,"Select a Directory for the file report");
      get_directory_dialog.show();
      directory_name = get_directory_dialog.getDirectory();
      //set the name of the file to "file_report"
      file_name = directory_name + "file_report";
      open_file_out();
   }
public boolean write_data(String the_text) {
      try {
   file_out.writeChars(the_text);
         return true;
      } catch(Exception e) {
   System.out.println("Error:report_file:the_function:Problem writing to file " + e);
        return false;
      }
   }

We use the same approach here in the creator() method as we did back in the find_file class for letting the user select a location to save the file to. The only other method in this class is write_data(), which writes a string out to a file, as shown above.

Because file_out is a DataOutputStream, we can just use the writeChars() method to output a string to the file, which was opened and initialized using the methods in the read_write_file class.

Summary

We've now assembled a group of classes that you can easily modify just by changing the the_function() method in the file_function class—using File methods, such as lastModified(), canRead(), or canWrite()—to generate any type of report about any file system on any type of computer. Even better, there's no reason to restrict the function applied to each file to generating a report. You could use other File methods—such as delete() or mkdir()—to perform more active functions. Finally, since we have a class that lets us read and write files—read_write_file—we can even search and modify the contents of files.

The bottom line is that it's easy to work with file systems in a platform-independent way using Java. Of course, since not all VMs  are fully up to snuff yet, there are still some platform-dependent idiosyncrasies, so it's best to test your code on as many platforms as possible.

Tom Trinkos has been programming computers since 1972. A confirmed Mac fan who's done everything from Fortran card decks to dynamic distributed CORBA tool systems, he's currently having tons of fun with Java. Check out his Web page at http://members.aol.com/trinkos/basepage.html for Java tools. You can contact him at trinkos@aol.com.

Copyright © 1998, ZD Inc. All rights reserved. ZD Journals and the ZD Journals logo are trademarks of ZD Inc. Reproduction in whole or in part in any form or medium without express written permission of ZD Inc. is prohibited. All other product names and logos are trademarks or registered trademarks of their respective owners.