Understanding java IO, opening/closing files using JFileChooser and opening/closing/saving binary files

1k Views Asked by At

I've been assigned a Java project by my professor and per usual, he didn't teach us how to use the tools we need to get the job done. So I'm trying to teach myself the basics of file manipulation as well as how to use JFileChooser, and I'm pretty confused.

In this project, he wanted us to create a JMenuBar with 3 JMenus(File, Batch Processing, Interactive Processing). Under the File JMenu, I have 5 JMenuItems(Open Binary File, Close Binary File, Save Binary File, Create Report, Exit). I listed these to hopefully give an idea of what the app is trying to accomplish.

Here are some snippets of instruction, which is what I've been interpreting and trying to mess around with:

1) Globally declare and create an object of the JFileChooser class: JFileChooser jfc = new JFileChooser();

2) Complete the actionPerformed as follows: for each JMenuItem selected, you will be calling a method or performing an action:

OpenMI: call the OpenFile method to open the binary access file for input

CloseMI: provide warning (JOptionPane) to Save first, if yes, then close the binary file and repaint

SaveMI: call method to open binary file for output then call method to output to it

ReportMI: call method to open text file and output to it

ExitMI: System.exeit(0);

PopulateBatchMI: call method to read from opened binary sequential file and put data into the array of tools objects(a class we created previously)

DisplayMI: display the contents of the toolsarray in a JTextArea on this screen

HideMI: hide the contents of the JTextArea displaying the toolsarray contents

*3) Write each method according to notes given in the text and in the lecture!

Part 4 Next, complete the following: (BSAF = Binary Sequentia Access File)*

1.Write a method called SaveBSAFile and within it a. Create an object of the DataOutputStream b. Create an object of the JFileChooser. Include all JFileChooser code, use the Save option 1. Open the binary sequential file for output 2. print a message to state that it is opened (use a JOptionPane) c. call the SaveBSAFile method when the SaveFile menu item is clicked

I'm having trouble understanding how to use the FileInputStream/FileOutputStream DataInputStream/DataOutputStream classes in conjunction with JFileChooser. All I've learned so far has been off of this site or others, so my understanding of how these classes work, especially together, has me confused. If anyone wanted to give me an explanation, I would really appreciate it.

Here is the code I've been working with so far, although it doesn't seem to be working correctly. There is an actionPerformed() that is used when the user clicks one of the menu options, and calls the methods I'm trying to write here:

public void saveBSAFile()
    {
        filename = JOptionPane.showInputDialog("Please specify a file name for the " 
                                             + " file you wish to save");

        try
        {
            FileOutputStream FOStream = new FileOutputStream(filename);
            DataOutputStream DOStream = new DataOutputStream(FOStream);
        }
        catch (FileNotFoundException e)
        {
            System.out.println("file name was not found");
        }

        jfc.setDialogTitle("Specify a file to save");
        int userSelection = jfc.showSaveDialog(this);

        if (userSelection == jfc.APPROVE_OPTION)
        {
            File filename = jfc.getSelectedFile();
            JOptionPane.showMessageDialog(null, "File to save " + filename,
                                         "Save Review", JOptionPane.INFORMATION_MESSAGE);
        }
        else if (userSelection == jfc.CANCEL_OPTION)
        {
            return;
        }

    }

    public void openBSAFile()
    {
        int status = jfc.showOpenDialog(this);

        if (status == JFileChooser.APPROVE_OPTION)
        {
            File selectedFile = jfc.getSelectedFile();
            filename = selectedFile.getAbsolutePath();
            JOptionPane.showMessageDialog(null, "You selected " + filename, 
                                         "File Review",
                                         JOptionPane.INFORMATION_MESSAGE);
            try
            {
                FileInputStream fstream = new FileInputStream(filename);
                DataInputStream dstream = new DataInputStream(fstream);
            }
            catch (FileNotFoundException e)
            {
                JOptionPane.showMessageDialog(null, "File not found. Please select a " +
                                             "valid file to open", "File Not found",
                                             JOptionPane.ERROR_MESSAGE);
                System.err.println(e);
                e.printStackTrace();
            }

        }
    }

There are no errors, but nothing seems to be happening. The saveBSAFile method is misleading because it seems its purpose is to create a file and save data from the array of tool objects (which has data members such as ID, price, numberInStock, ect).

No file is created, and when I try to open one I've created and specified, nothing happens, either.

2

There are 2 best solutions below

1
On

You are opening the streams, but you do not use them. Since the streams are declared in a (try-)block, they are only alive and visible in the try-block. Your manipulation of the stream should be happening inside the try-block. Furthermore, you should use the try-with-resources statement. Here you find a tutorial on how to use basic I/O-streams. You can use a JFileChooser to handle the open dialog.

0
On

In addition to reading tutorials, StackOverflow questions, Javadocs, and general trial-and error, most of what I know about Swing-based GUIs and I/O came from Introduction to Programming Using Java, a free textbook by David Eck. He provides very readable working code, and if anything that follows is unclear, I recommend reading along in the source to SimplePaintWithFiles.java.

Remember that a variable only exists within the section of code where it is declared, regardless of the variable's type. In this case, that means that fstream and dstream only exist in your try block, so you can only use them there. Do also brush up on when to use public, protected, and private access modifiers for methods.

What follows is a flowchart of the high-level steps to I/O, as I understand them. It contains simple code snippets that are deliberately out of context because they turn up in too many different contexts to include here.

1. Choose a path to interact with for I/O

  1. via a String literal in your code
  2. via System.in, a Scanner, or some other input stream
  3. via a GUI object such as a JFileChooser

2. Construct a File object pointing to that pathname (and probably keep a pointer to it)

  1. case 1.1: pass the String literal (or the variable you declared with it) to a File constructor e.g. File someFile = new File("/a/hardcoded/absolute/path"); This isn't flexible, easy to maintain, or necessarily secure for distributable code, but comes in handy for unit tests, quick and dirty scripts, and tiny snippets in answers and forums where the poster assumes the reader knows better than to put a million literals all over their code.
  2. case 1.2: assuming some Scanner is in scope called scnr, String thePath = scnr.next(); Put check code after this to determine is thePath is really an acceptable path for your use case, before passing thePath to other methods, including constructors.
  3. case 1.3: this is why JFileChooser jfc = new // whatever constructor is best for your use case; is usually declared globally. It's always in scope; the menu items all have a single repository of information about the last-chosen file, so their actionPerformed methods can "talk" to each other; and the private utility methods such as openBSAFile() can query File targetFile = jfc.getSelectedFile(); for the File to operate on.

3. Now I have a File! What do I do with it?

That depends on whether there is already a file on disk at the chosen address and whether you expect a file to be there. This is one reason to keep a pointer to the currently-active File, so you can call it's exists() method, mkdir(), etc as needed.

  1. If the file already exists on disk, perform interaction0. An output method should probably throw an error or ask for permission to overwrite. An input method will probably proceed normally.
  2. If the file does not exist on disk, perform interaction1. An input method will probably throw an error (and maybe catch its own error, such as to display a dialog). An output method will probably proceed normally.

4. Remember to flush

Just because you have called dstream.writeChars("Content of some string-type variable you need to save"); doesn't mean that your variable has actually been sent to the disk. call dstream.flush() before closing, returning, or otherwise assuming that the data has safely arrived. See TextIO.java at the second link above for some great examples of when flushing is the solution to bugs.

5. Close your open streams! (This is what confused me the most)

When to close an open stream? The short answer is "when you're done with it," but that's as vague as saying "a piece of rope is as long as you need it to be." So how do you know when you're done? There are two broad categories of "done with a stream." I still don't fully understand when streams are implicitly closed, so when in doubt I tend to add close() statements that might be redundant. There is some debate about whether unclosed streams can fairly be called "memory leaks," but they do tie up scarce resources.

  1. An IOException has occurred after which your code cannot function correctly. In addition to throwing an exception, catching an exception, displaying dialogs, etc, you are also done with your streams. In your code above, calling dstream.close() will also close fstream because dstream is its wrapper.
  2. Your code has finished interacting with the stream normally. In this case, you have written or read all the data you need, no errors have occurred, and the method using the stream is about to return.

Deeper discussion of cases 3.1, 3.2, and 5.1

It's metaphor time. Opening a stream with a File is like calling someone on the phone. If an exception is not thrown and the stream is opened, consider that "picking up the phone." You called, they picked up, now it's your turn to say something. Saying something might mean reading data from the stream, or it might mean writing data to it. Either way, complete the business you called about, then hang up.

Things could still be going wrong even if an input stream hasn't thrown an error. If you are supposed to be reading a list of integers from a file, and the file actually contains Moby Dick, something has gone wrong that Java's built-in I/O classes can't detect. It is our job as programmers to include code to check the semantic correctness of the data we read from a file, for the same reasons we must include code to check the semantic correctness of a pathname obtained in case 2.2. Only you know what "correctness" looks like for your data, and relying on a file extension is not enough.

Aside about binary save files:

The assignment specifications provided in the question don't call for Serialization. They do, however, call for a known set of fields and a known representation of those fields. Most of the searches I have done about binary save data recommend Serialization because you don't have to do so much hand coding. However, such an assignment isn't just making you work harder; arguments could be made that your save files will be more useful without Serialization. I don't have the space to get into details here, nor am I attempting to open an opinion-based can of worms. Instead, you might have a look at the unofficial specification of the .xcf file format for inspiration on how and in what order you might manually save the data needed to recreate any given object.