Lab 12: Files and Persistence

Due Date: Monday 4/11 11:59PM. Note that this lab has a different deadline than the usual Friday deadline due to Test 2.

A. Intro

So far in this class, you have exclusively made programs whose state only persists while the program is running, and all traces of the program disappear once the program exits. For example, in Project 2, you created a game that you could play while the program was running, but there was no way to save the game state, quit the program, go do some other stuff, and then run the program again, load up your previous progress, and continue playing the game. In this lab, we will go over two methods to make the state of your program persist past the execution of your program: one through writing plain text to a file, and the other through serializing objects to a file. This will be directly applicable Project 3: Gitlet as well as any future projects you want to do where you want to be able to save state between programs.

As always, you can get the skeleton files with the following commands:

 git fetch shared
 git merge shared/lab12 -m "Start Lab 12"

Here are useful links for content review for this lab:

B. Files and Directories in Java

Before we jump into manipulating files and directories in Java, let's go through some file system basics.

Current Working Directory

The current working directory (CWD) of a Java program is the directory from where you execute that Java program. Examples follow for Windows & Mac/Linux users - they are very similar, just different stylistically.

Windows For example, for Windows users, let's say we have this small Java program located in the folder C:/Users/Linda/example (or ~/example) named Example.java:

// file C:/Users/Linda/example/Example.java
class Example {
  public static void main(String[] args) {
     System.out.println(System.getProperty("user.dir"));
  }
}

This is a program that prints out the CWD of that Java program.

If I ran:

cd C:/Users/Linda/example/
javac Example.java
java Example

the output should read:

C:\Users\Linda\example

Mac & Linux For example, for Mac & Linux users, let's say we have this small Java program located in the folder /home/Linda/example (or ~/example) named Example.java:

// file /home/Linda/example/Example.java
class Example {
  public static void main(String[] args) {
     System.out.println(System.getProperty("user.dir"));
  }
}

This is a program that prints out the CWD of that Java program.

If I ran:

cd /home/Linda/Example
javac Example.java
java Example

the output should read:

/home/Linda/example

IntelliJ In IntelliJ, you can view the CWD of your program under Run > Edit Configurations > Working Directory.

IntelliJ Working Directory

Terminal In terminal / Git Bash, the command pwd will give you the CWD.

Absolute and Relative Paths

A path is the location of a file or directory. There are two kinds of paths: absolute paths and relative paths. An absolute path is the location of a file or directory relative to the root of the file system. In the example above, the absolute path of Example.java was C:/Users/Linda/example/Example.java (Windows) or /home/Linda/example/Example.java (Mac/Linux). Notice that these paths start with the root which is C:/ for Windows and / for Mac/Linux. A relative path is the location of a file or directory relative to the CWD of your program. In the example above, if I was in the C:/Users/Linda/example/ (Windows) or /home/Linda/example/ (Mac/Linux) folders, then the relative path to Example.java would just be Example.java. If I were in C:/Users/Linda/ or /home/Linda/, then the relative path to Example.java would be example/Example.java.

Note: the root of your file system is different from your home directory. Your home directory is usually located at C:/Users/<your username> (Windows) or /home/<your username> (Mac/Linux). We use ~ as a shorthand to refer to your home directory, so when you are at ~/repo, you are actually at C:/Users/<your username>/repo (Windows) or /home/<your username>/repo (Mac/Linux).

When using paths, . refers to the CWD. Therefore, the relative path ./example/Example.java is the same as example/Example.java.

File & Directory Manipulation in Java

The Java File class represents a file or directory in your operating system and allows you to do operations on those files and directories. In this class, you usually will want to be doing operations on files and directories by referring to them to their relative paths. You'll want any new files or directories you create to be in the same directory as where you run your program (in this lab, the ~/repo/lab12 folder) and not some random place on your computer.

Files

You can make a File object in Java with the File constructor and passing in the path to the file:

 File f = new File("dummy.txt");

The above path is a relative path where we are referring to the file dummy.txt in our Java program's CWD. You can think of this File object as a reference/pointer to the actual file dummy.txt - when we create the new File object, we aren't actually creating the dummy.txt file itself, we are just saying, "in the future, when I do operations with f, I want to do these operations on dummy.txt". To actually create this dummy.txt file, we could call

 f.createNewFile();

and then the file dummy.txt will actually now exist (and you could see it in File Explorer / Finder).

You can check if the file "dummy.txt" already exists or not with the exists method of the File class:

 f.exists()

We can also write to the file with the following:

 Utils.writeContents(f, "Hello World");

Now dummy.txt would now have the text "Hello World" in it. Note that Utils is a helper class provided in this lab and project 3 and is not a part of standard Java.

Directories

Directories in Java are also represented with File objects. For example, you can make a File object that represents a directory:

File d = new File("dummy");

Similar to files, this directory might not actually exist in your file system. To actually create the folder in your file system, you can run:

d.mkdir();

and now there should be a folder called dummy in your CWD.

Summary

There are many more ways to manipulate files in Java, and you can explore more by looking at the File javadocs and Googling. There are a ton of resources online and, if you Google it, doing more extensive file operations in Java can get a bit complicated. I'd recommend understanding the basics by doing this lab, and in the future if you come across a use case you don't know how to handle, then start searching or asking on Piazza. For this lab and Gitlet, we provide you with a Utils.java class that has many useful helper functions you should use for file operations.

C. Serializable

Writing text to files is great and all, but what if we want to save some more complex state in our program? For example, what if we want to be able to save the Board object in Ataxx so we can come back to it later? We could somehow write a toString method to convert a Board to a String and then write that String to a file. If we do that though, we would also need to figure out how to load the Board by parsing that file, which can get complicated.

Luckily, we have an alternative called serialization which Java has already implemented for us. Serialization is the process of translating an object to a series of bytes that can then be stored in the file. We can then deserialize those bytes and get the original object back.

To enable this feature for a given class in Java, this simply involves implementing the java.io.Serializable interface:

import java.io.Serializable;

class Board implements Serializable {
    ...
}

This interface has no methods; it simply marks its subtypes for the benefit of some special Java classes for performing I/O on objects. For example,

    Board b = ....;
    File outFile = new File(saveFileName);
    try {
        ObjectOutputStream out =
            new ObjectOutputStream(new FileOutputStream(outFile));
        out.writeObject(b);
        out.close();
    } catch (IOException excp) {
        ...
    }

will convert b to a stream of bytes and store it in the file whose name is stored in saveFileName. The object may then be reconstructed with a code sequence such as

    Board b;
    File inFile = new File(saveFileName);
    try {
        ObjectInputStream inp =
            new ObjectInputStream(new FileInputStream(inFile));
        b = (Board) inp.readObject();
        inp.close();
    } catch (IOException | ClassNotFoundException excp) {
        ...
        b = null;
    }

The Java runtime does all the work of figuring out what fields need to be converted to bytes and how to do so. We have provided helper function in Utils.java that does the above two for you and you should use them for this lab.

Note: There are some limitations to Serializable that are noted in the Project 3 spec. You will not encounter them in this lab.

D. Exercise: Canine Capers

For this lab, you will be writing a program that will be taking advantage of file operations and serialization. We have provided you with three files:

You can change the skeleton files in any way you want as long as the spec and comment above the main method in Main.java is satisfied. You do not need to worry about error cases or invalid input. You should be able to complete this lab with just the methods provided in Utils.java and other File class methods mentioned in this spec, but feel free to experiment with other methods.

Main

You should allow Main to run with the following three commands:

All persistent data should be stored in a ".capers" directory in the current working directory.

Recommended file structure (you do not have to follow this):

.capers/ -- top level folder for all persistent data
    - dogs/ -- folder containing all of the persistent data for dogs
    - story -- file containing the current story

You should not create these manually, your program should create these folders and files.

Note: Naming a folder or file with a period in the front makes it hidden - to be able to see it in terminal, run ls -a instead of just ls. If you want to remove all saved data from your program, just remove the .capers directory (NOT the capers directory) with rm -rf .capers. (There is no space between the dot . and capers). But BE CAREFUL that you don't make a typo with the rm -rf ... command since deletion is not undoable and you can accidently delete your whole file system if ran incorrectly.

Suggested Order of Completion

Please be sure to read the comments above each method in the skeleton for a description of what they do.

  1. Fill out the main method in Main.java. This should consist mostly of calling other methods.
  2. Fill out CAPERS_FOLDER in Main.java, then DOG_FOLDER in Dog.java, and then setUpPersistence in Main.java.
  3. Fill out writeStory in Main.java. The story command should now work.
  4. Fill out saveDog and then fromFile in Dog.java. You will also need to address the FIXME at the top of Dog.java. Remember dog names are unique!
  5. Fill out makeDog and celebrateBirthday in Main.java using methods in Dog.java. You will find the haveBirthday method in the Dog class useful. The dog and birthday commands should now work.

Each FIXME should take at most around 8 lines, but many are fewer.

Usage

The easiest way to run and test your program is to compile it in terminal with javac and then run it from there. E.g.

 cd ~/repo/lab12              # Make sure you are in your lab12 folder (NOT the lab12/capers folder)
 make                         # or javac capers/*.java, make sure to recompile your program each time you make changes
 java capers.Main [args]      # Run the commands you want! e.g., java story hello

For the story command, if you want to pass in a long string that includes spaces as the argument, you will want to put it in quotes, e.g.

 java capers.Main story "hello world"

If running in IntelliJ, you will need to use Run > Edit Configurations > Program Arguments to add the arguments.

Useful Util Functions

Useful Util functions (as a start, may need more and you may not need all of them):

Testing

You should test your program yourself by running it in the command line. The Gradescope autograder will also run a small set of tests. The AG tests are a combination of running these commands in order:

$ java capers.Main story Hello
Hello

$ java capers.Main story World
Hello
World

$ java capers.Main dog Sammie Samoyed 5
Woof! My name is Sammie and I am a Samoyed! I am 5 years old! Woof!
$ java capers.Main birthday Sammie
Woof! My name is Sammie and I am a Samoyed! I am 6 years old! Woof!
Happy birthday! Woof! Woof!
$ java capers.Main dog Larry Lab 11
Woof! My name is Larry and I am a Lab! I am 11 years old! Woof!
$ java capers.Main birthday Sammie
Woof! My name is Sammie and I am a Samoyed! I am 7 years old! Woof!
Happy birthday! Woof! Woof!
$ java capers.Main birthday Larry
Woof! My name is Larry and I am a Lab! I am 12 years old! Woof!
Happy birthday! Woof! Woof!

It also ignores whitespace at the beginning and end of the output so don't worry too much about that.

Note: If you'd like to re-test your code, make sure to remove the .capers hidden directory to make sure you have a fresh new directory to start with. As noted in the previous section, to remove .capers (NOT the capers directory), you run rm -rf .capers.

E. Submission

You should have made changes in capers/Main.java and capers/Dog.java. If you worked with a partner, please also update the partner.txt file. You should not be submitting a .capers data folder. Do not use git add . or git add -A to add your files, and git add your files one by one. Submit the lab as always:

 git commit -m "submitting lab12"
 git tag lab12-0 # or the next highest submission number
 git push
 git push --tags

There is no style check for this lab.

F. Tips, FAQs, Misconceptions

Tips

These are tips if you're stuck!

FAQs & Misconceptions

G. Credits

Capers was originally written by Sean Dooher in Fall 2019. Spec and lab adaptation were written by Michelle Hwang in Spring 2020 and Linda Deng in Fall 2021 and Spring 2022.