On this page, we will develop the ProfileFileManager class, a facility for storing profile data in files.
GitHub repository: Cartesian Plane Lesson 18
Previous Lesson: Cartesian Plane Lesson 18 Page 5: ProfileParser JUnit Test
ProfileFileManager Class
The ProfileFileManager class will be a facility for storing a Profile in a file and retrieving it on demand. It’s a simple class that uses ProfileParser.getProperties() to convert a Profile to text output and write it to a file using a PrintWriter. To restore the Profile, it will read the text data from a previously saved file using a BufferedReader, and process the input through ProfileParser.loadProperties(Stream<String>) to produce a Profile.

If an operation results in an I/O error, the file manager handles it gracefully; it posts an error dialog, waits for the operator to dismiss the dialog, and completes the operation with a failure code. Parsing errors do affect the file manager. If, for example, there’s a syntax error in an input file, the ProfileParser will handle it. If the parser works correctly, it will report the error and then complete parsing the input, eventually producing a valid Profile. From the file manager’s perspective, an operation has been completed successfully if it completes without an I/O error.
⏹ ProfileManager Operations
Supported operations are divided into the open group and the save group. You might consider new-file a third group, but it’s just a special instance of a save operation, so we’ll include it there. In addition, most of these operations have different heuristics, for example:
- Create a new profile and save it to a new file;
- Save an existing profile to a new file;
- Save an existing profile to an existing file;
- Open an existing file into a new profile;
- Open an existing file into an existing profile;
- etc.
Each operation can behave in various ways, for example:
- Save a profile to a new file:
- Start by opening a JFileChooser:
- The operator can cancel the operation.
- The operator can approve the operation. If approved:
- The operation might succeed or fail with an I/O error; if it fails, an error dialog must be posted.
In addition to operations, the file manager will maintain a state of three properties, which can be summarized as follows:
- What was the result of the most recent operation?
- Which file is currently open, if any?
- What was the last action taken by the operator?
It should be no surprise that different operations will be handled via different methods. Operations with different options will be dealt with via overloads, for example:
- save( Profile profile, File file ): save the given profile to the given file
- save( Profile profile ): save the given profile to a file of the operator’s choice.
- open( File file ): instantiate a new Profile and populate it from the given file.
- open( Profile profile ): open a file of the operator’s choice and read it into the given Profile.
Here’s a summary of the various operations:
⬛ Save Operations
All save operations write a Profile to a file. The return value of any related method is true for success and false for failure.
- save( Profile, File )
The given profile is saved to the given file.- Operator interaction: none
- Outcome:
- Success: last-result = true, curr-file = the given file, last-action = unchanged
- Failure: last-result = false, curr-file = none, last-action = unchanged
- save( File )
Create a new Profile and save it to the given file. The new profile’s property values will be derived from those currently stored by the PropertyManager.- Operator interaction: none
- Outcome:
- Success: last-result = true, curr-file = the given file, last-action = unchanged
- Failure: last-result = false, curr-file = none, last-action = unchanged
- save( Profile )
- If a file is currently open:
Save the given Profile to the current file.- Operator interaction: none
- Outcome:
- Success: last-result = true, curr-file: unchanged, last-action = unchanged
- Failure: last-result = false, curr-file = none, last-action = unchanged
- If no file is currently open:
Equivalent to saveAs( Profile )
- If a file is currently open:
- saveAs( Profile )
Save the given Profile to a file of the operator’s choice.- Operator interaction:
- Operator selects a file:
- Outcome:
- Success: last-result = true, curr-file = the selected file, last-action = approved
- Failure: last-result = false, curr-file = none, last-action = approved
- Outcome:
- Operator cancels:
- Outcome:
- Last-result = unchanged, curr-file = unchanged, last-action = canceled
- Outcome:
- Operator selects a file:
- Operator interaction:
- saveAs( )
Create a new Profile and save it to a file of the operator’s choice.- Operator interaction:
- Operator selects a file:
- Outcome:
- Success: last-result = true, curr-file = the selected file, last-action = approved
- Failure: last-result = false, curr-file = none, last-action = approved
- Outcome:
- Operator cancels:
- Outcome: Last-result = unchanged, curr-file = unchanged, last-action = canceled
- Operator selects a file:
- Operator interaction:
- newFile()
Equivalent to saveAs().
⬛ Open Operations
All open operations read data from a file into a Profile. The Profile may be provided by the client or created as part of the operation. The return value of any related method is a reference to the target Profile for success and null for failure. In this context, failure implies I/O failure, not a parse error.
- open( Profile, File )
- The given file is read into the given Profile
- Operator interaction: none
- Outcome:
- Success: last-result = true, curr-file = the given file, last-action = unchanged
- Failure: last-result = false, curr-file = none, last-action = unchanged
- open( File )
- A new Profile is created, and the given file is read into it
- Operator interaction: none
- Outcome:
- Success: last-result = true, curr-file = the given file, last-action = unchanged
- Failure: last-result = false, curr-file = none, last-action = unchanged
- open( Profile )
- A file of the operator’s choice is opened and read into the given Profile.
- Operator interaction:
- Operator selects a file:
- Outcome:
- Success: last-result = true, curr-file = the selected file, last-action = approved
- Failure: last-result = false, curr-file = none, last-action = approved
- Outcome:
- Operator cancels:
- Outcome: last-result = unchanged, curr-file = unchanged, last-action = canceled
- Operator selects a file:
- open()
- A new Profile is created and populated from the data in a file of the operator’s choice.
- Operator interaction:
- Operator selects a file:
- Outcome:
- Success: last-result = true, curr-file = the selected file, last-action = approved
- Failure: last-result = false, curr-file = none, last-action = approved
- Outcome:
- Operator cancels:
- Outcome: last-result = unchanged, curr-file = unchanged, last-action = canceled
- Operator selects a file:
⬛ Other ProfileManager Facilities
In addition to the above operations, the ProfleManager can 1) return the last result, 2) return the current file, 3) close the current file, and 4) return the encapsulated JFileChooser. Details can be found in Implementation below.
ProfileFileManager Implementation
The ProfileFileManager came together nicely, with minimal state, one constructor, and no helper methods. Our public facilities are divided into three categories:
- Miscellaneous facilities not directly related to file I/O, such as getters and the close method; see Miscellaneous Facilities.
- Facilities that perform output operations, including new-file; see Save Operations.
- Facilities that perform input operations; see Open Operations.
Below is a discussion of the details.
⬛ Infrastructure
The ProfileFileManager has four instance fields and three class fields, as shown below:
public class ProfileFileManager
{
public static final int APPROVE = JFileChooser.APPROVE_OPTION;
public static final int CANCEL = JFileChooser.CANCEL_OPTION;
/** The title of the file chooser dialog. */
private static final String title = "Profile File Manager";
private final JFileChooser chooser;
private File currFile = null;
private boolean lastResult = false;
private int lastAction = CANCEL;
// ...
}
- int APPROVE
This value indicates that, when offered a choice, the operator selected a file and approved the selection, indicating that the operation in progress should continue. It’s spelled in all caps because it is a constant variable. - int CANCEL
This value indicates that, when offered a choice, the operator canceled the operation in progress. It’s spelled in all caps because it is a constant variable. - JFileChooser chooser
This is the JFileChooser to use when asking the operator to select a file. It is initialized during instantiation and never changed. - File currFile
The currently open file. Immediately after instantiation, it is null. After a successful file operation, it is set to the file being operated on. After an unsuccessful file operation, it is set to null. The client can explicitly set this field to null by calling the close method. - boolean lastResult
This field contains the result of the previous operation, either successful (true) or unsuccessful (false). Its value immediately after instantiation, before any operation is executed, is indeterminate. - int lastAction
For operations that require operator interaction, this field documents the operator’s choice in the JFileChooser dialog, either APPROVE or CANCEL. Following operations that do not require operator interaction, the value of this field is unchanged. Its value immediately after instantiation is indeterminate.
⬛ Constructor
The constructor for this class does nothing but initialize the JFileChooser, setting its default directory to the default user directory, as shown below.
public ProfileFileManager()
{
String userDir = System.getProperty( "user.dir" );
File baseDir = new File( userDir );
chooser = new JFileChooser( baseDir );
}
⬛ Miscellaneous Facilities
Following is a discussion of those facilities not directly related to file I/O:
🟦 public JFileChooser getFileChooser()
🟦 public File getCurrFile()
🟦 public boolean getLastResult()
🟦 public int getLastAction()
These methods are simple getters for the file manager’s four fields. Their implementations are in the GitHub repository.
🟦 public void close()
The close method sets the currFile field to null. It looks like this:
public void close()
{
currFile = null;
}
⬛ Output Facility
The public methods that comprise this facility generally perform as follows:
- If a file operation is performed, a method will return true for success and false for failure and update the lastResult and currFile properties.
- If operator interaction is required, the lastAction property is updated; if no such interaction is required, it is unchanged.
- If the operator cancels an operation, the lastAction property is updated, and the lastResult and currFile properties are unchanged.
Following is a discussion of the public methods that generate file output.
🟦 public boolean save( Profile profile, File file )
This method writes the given Profile to the given file. After the operation, the lastResult and currFile properties are updated. Following is an annotated listing. See also Save Operations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public boolean save( Profile profile, File file ) { lastResult = false; currFile = null; try ( PrintWriter pWriter = new PrintWriter( file ) ) { ProfileParser parser = new ProfileParser( profile ); parser.getProperties().forEach( pWriter::println ); lastResult = true; currFile = file; } catch ( IOException exc ) { String msg = "Error writing \"" + file.getAbsolutePath() + "\": " + exc.getMessage(); JOptionPane.showMessageDialog( null, "Save File Error", msg, JOptionPane.ERROR_MESSAGE ); } return lastResult; } |
- Lines 3,4: Set instance fields to the failed state. They will be updated if the operation succeeds (lines 9,10).
- Line 5: Start a try-with-resources block. Recall that any resource declared inside the parentheses on the try statement will automatically be closed after the operation completes, whether an exception is thrown or not.
- Lines 7,8: Get a ProfileParser and use it to obtain a formatted stream of property name/value pairs from the given Profile.
- Lines 9,10: Set the object state to successful.
- Lines 12-24: Process an I/O exception (if raised).
- Lines 14-17: Formulate an error message consisting of the path to the file on which the operation failed and the description of the failure encapsulated in the Exception object.
- Lines 18-23: Post an error dialog via JOptionPane. The dialog’s title will be “Save File Error” and JOptionPane.ERROR_MESSAGE will determine its icon.
- Line 25: Return the status of the operation.
🟦 public boolean saveAs( Profile profile )
This method writes the given Profile to a file of the operator’s choice. It returns true if the operation is completed successfully and false if it fails. If the operator cancels the operation, lastAction is set to CANCEL while lastResult and currFile remain unchanged. The following is a listing of this method. See also Save Operations.
public boolean saveAs( Profile profile )
{
int result = chooser.showSaveDialog( null );
if ( result == JFileChooser.APPROVE_OPTION )
lastResult = save( profile, chooser.getSelectedFile() );
return lastResult;
}
🟦 public boolean saveAs( Profile profile )
This method writes the given Profile to a file of the operator’s choice. If the operator cancels the operation lastAction is set to CANCEL while lastResult and currFile remain unchanged. Otherwise, it returns true if the operation is completed successfully and false if it fails; the lastResult, currFile, and lastAction properties are updated. See also Save Operations. Here’s the code for this method.
public boolean saveAs( Profile profile )
{
int result = chooser.showSaveDialog( null );
if ( result == JFileChooser.APPROVE_OPTION )
lastResult = save( profile, chooser.getSelectedFile() );
return lastResult;
}
🟦 public boolean saveAs()
This method instantiates a Profile and writes it to a file of the operator’s choice. If the operator cancels the operation lastAction is set to CANCEL while lastResult and currFile remain unchanged. Otherwise, it returns true if the operation completes successfully and false if it fails; the lastResult, currFile, and lastAction properties are updated. See also Save Operations. A listing for this method follows.
public boolean saveAs()
{
saveAs( new Profile() );
return lastResult;
}
🟦 public boolean save( Profile profile )
If a file is open, this method writes the given Profile to the open file; it returns true for success and false for failure. After completion, the lastAction property is unchanged, and the lastResult property is updated; if the operation fails, the currFile property will be changed to null; otherwise, it becomes a reference to the selected file. If no file is open, this operation is equivalent to saveAs(Profile). See also Save Operations. The code follows.
public boolean save( Profile profile )
{
if ( currFile == null )
saveAs( profile );
else
save( profile, currFile );
return lastResult;
}
🟦 public boolean newFile()
This operation is equivalent to saveAs(). See also Save Operations. Here’s the code.
public boolean newFile()
{
saveAs();
return lastResult;
}
⬛ Input Facilities
The public methods that comprise this facility generally perform as follows:
- If a file operation is successfully performed, a method will populate and return a Profile object; unsuccessful operations return null. All attempted (uncanceled) operations update the lastResult and currFile properties.
- If operator interaction is required, the lastAction property is updated; if no such interaction is required, it is unchanged.
- If the operator cancels an operation, the lastResult and currFile properties are unchanged.
Following is a discussion of the public methods that generate file input.
🟦 public Profile open( File file, Profile profile )
This method attempts to read the given file and parse it to populate the given Profile. On successful completion, a reference to the Profile is returned; otherwise, null is returned. The lastResult and currFile properties are updated. Here is an annotated listing for this method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public Profile open( File file, Profile profile ) { currFile = null; lastResult = false; try ( FileReader fReader = new FileReader( file ); BufferedReader bReader = new BufferedReader( fReader ); ) { Stream<String> lines = bReader.lines(); ProfileParser parser = new ProfileParser( profile ); parser.loadProperties( lines ); currFile = file; lastResult = true; } catch ( IOException exc ) { profile = null; String msg = "Error reading \"" + file.getAbsolutePath() + "\": " + exc.getMessage(); JOptionPane.showMessageDialog( null, msg, "Read File Error", JOptionPane.ERROR_MESSAGE ); } return profile; } |
- Lines 3,4: Set the lastResult and currFile properties in anticipation of operation failure. If the operation succeeds, they will be updated (lines 13,14).
- Lines 5-8: Try-with-resources statement. The declared files will be automatically closed after the try/catch block is complete.
- Line 10: Get a stream of all lines from the given file.
- Line 11: Instantiate a parser that encapsulates the given Profile.
- Line 12: Parse the input into the given Profile.
- Lines 13,14: Update the lastResult and currFile properties.
- Lines 16-29: Process an I/O exception (if raised).
- Line 18: Set the Profile reference to null, indicating operation failure.
- Lines 19-22: Formulate an error message consisting of a path to the file on which the operation failed and the description of the failure encapsulated in the Exception object.
- Lines 23-28: Post an error dialog via JOptionPane. The dialog’s title will be “Read File Error” and JOptionPane.ERROR_MESSAGE will determine its icon.
- Line 30: Return the result of the operation.
🟦 public Profile open( File file )
Parses the given file into a newly instantiated Profile object. On successful completion, a reference to the Profile is returned; otherwise, null is returned. The lastResult and currFile properties are updated. Here’s the code for this method.
public Profile open( File file )
{
Profile profile = open( file, new Profile() );
return profile;
}
🟦 public Profile open( Profile profile )
This method populates the given Profile from a file of the operator’s choice, updating the lastAction property. If the operation is not canceled, the lastResult and currFile properties are updated. The code follows.
public Profile open( Profile profile )
{
lastAction = chooser.showOpenDialog( null );
if ( lastAction == APPROVE )
profile = open( chooser.getSelectedFile(), profile );
return profile;
}
🟦 public Profile open()
This method populates a newly instantiated Profile object from a file of the operator’s choice, updating the lastAction property. If the operation is not canceled, the lastResult and currFile properties are updated. Here is the code for this method.
public Profile open()
{
Profile profile = null;
lastAction = chooser.showOpenDialog( null );
if ( lastAction == APPROVE )
profile = open( chooser.getSelectedFile() );
return profile;
}
Summary
On this page, we wrote a simple class, ProfileFileManager, to handle file I/O for saving and restoring Profiles. Our next task is to write a JUnit test for it.