Java Arena
Project · After module 43: Writing files

A loan desk

The workshop on Birch Lane lends its tools to its members: a drill for a weekend, a ladder for a week. In this project you build the program that runs its loan desk. When it's finished, it loads the shelf from a file when it starts, takes commands at the desk, and saves the shelf again when the desk closes.

Days are plain day numbers (day 1, day 2, ...), so no calendar is needed. A loan lasts 7 days: a tool lent on day 14 is due back on day 21.

The shelf is kept in shelf.csv, one line per tool, member and loan, with the parts separated by commas. loan,T3,M1,11 means that the tool T3 is out with the member whose card is M1, due back on day 11:

tool,T1,Cordless drill
tool,T2,Hand saw
tool,T3,Ladder
member,M1,Noor
member,M2,Ilse
loan,T3,M1,11

A day at the desk. The person at the desk types these commands:

day 14
loans
lend T1 M2
lend T3 M2
return T3
late
quit

and the program answers:

It's day 14
T3 Ladder: Noor (M1), due day 11
Lent: T1 Cordless drill: Ilse (M2), due day 21
Sorry: T3 Ladder is out, due back on day 11
T3 Ladder is back, 3 days late
Nothing is late
Saved shelf.csv

The ladder can't be lent to Ilse while Noor still has it, so the desk refuses with a message instead of crashing. After quit, shelf.csv has the loan of the drill instead of the ladder's.

The finished program has these classes (and an exception class of its own, LoanException):

The diagram as text
  • TextUi, a class
    • private field today: int
    • public method start(): void
  • ToolShelf, a class
    • public method lend(code: String, card: String, today: int): Loan
    • public method giveBack(code: String, today: int): int
    • public method save(fileName: String): void
    • public method load(fileName: String): void
    • more members, not shown
  • Loan, a class
    • private field dueDay: int
    • public method isLate(today: int): boolean
  • Tool, a class
    • private field code: String
    • private field name: String
  • Member, a class
    • private field card: String
    • private field name: String

Connections:

  • Each TextUi has one ToolShelf object.
  • Each ToolShelf has any number of Tool objects.
  • Each ToolShelf has any number of Member objects.
  • Each ToolShelf has any number of Loan objects.
  • Each Loan has one Tool object.
  • Each Loan has one Member object.

What you need

Everything up to the end of part 11: classes and objects (modules 17 to 19), reading files (module 20), a text UI (module 25), hash maps (module 31), interfaces and Comparable (modules 35 and 38), exceptions (module 42) and writing files (module 43).

The milestones

  1. Tools and members, in two hash maps, printed in order.
  2. Loans with due days, and which ones are late.
  3. Refusals: an exception class of your own for a loan that can't happen.
  4. Saving the shelf to a CSV file.
  5. Loading it back, with a clear message for a damaged line.
  6. The desk: a text UI over the shelf.

The checks of the first five milestones call your classes' methods directly; main becomes the desk only in the last one.

Milestones

  1. Tools and members
  2. Loans and due days
  3. Refusals
  4. Saving to a CSV file
  5. Loading, and damaged lines
  6. The desk

Milestone 1: Tools and members

Every tool on the shelf has a code, such as T1, and a name. Tool.java is ready. Every member has a card, such as M1, and a name.

In Member.java, write the class Member:

  • a constructor Member(String card, String name)
  • getCard() and getName(), which return them
  • toString(), which returns the name and the card in parentheses: Noor (M1)

In ToolShelf.java, write the class ToolShelf. It keeps the tools in a HashMap<String, Tool> called tools, with the codes as keys, and the members in a HashMap<String, Member> called members, with the cards as keys. A map finds a tool by its code at once, which the next milestones need.

  • a constructor without parameters, which creates the empty maps
  • addTool(Tool tool) and addMember(Member member), which put them in their maps
  • printTools(), which prints every tool, one per line, in the alphabetical order of their codes
  • printMembers(), which prints every member the same way, in the order of their cards

A HashMap keeps its keys in an order of its own, so copy the keys into a list and sort it before printing. The check runs:

ToolShelf shelf = new ToolShelf();
shelf.addTool(new Tool("T2", "Hand saw"));
shelf.addTool(new Tool("T1", "Cordless drill"));
shelf.addMember(new Member("M2", "Ilse"));
shelf.addMember(new Member("M1", "Noor"));
shelf.printTools();
shelf.printMembers();

and expects:

T1 Cordless drill
T2 Hand saw
Noor (M1)
Ilse (M2)

Milestone 2: Loans and due days

A loan lasts 7 days: a tool lent on day 4 is due back on day 11, and it's late from day 12 on. A new file, Loan.java, holds the class Loan. Write it:

  • a constructor Loan(Tool tool, Member member, int dueDay)
  • getTool(), getMember() and getDueDay()
  • isLate(int today), which returns true when today is after the due day
  • toString(), which returns the tool, a colon, the member and the due day: T3 Ladder: Noor (M1), due day 11
  • Loan implements Comparable<Loan>: its natural order is by due day, and loans with the same due day are in the order of their tools' codes.

Then add to ToolShelf a third map, a HashMap<String, Loan> called loans, with the loans by their tools' codes (a tool can be out only once), and these methods:

  • lend(String toolCode, String card, int today) creates a loan of that tool to that member, due on today + 7, puts it in the map and returns it. For now, it may assume that the tool and the member exist and that the tool is on the shelf.
  • giveBack(String toolCode, int today) removes the tool's loan and returns how many days late it was: 0 when it's back on or before its due day.
  • printLoans() prints every loan in the order of the tools' codes, or No tools are out when there are none.
  • printLate(int today) prints the loans that are late on that day, in their natural order (by due day), or Nothing is late.

The check runs:

ToolShelf shelf = new ToolShelf();
shelf.addTool(new Tool("T1", "Cordless drill"));
shelf.addTool(new Tool("T2", "Hand saw"));
shelf.addTool(new Tool("T3", "Ladder"));
shelf.addMember(new Member("M1", "Noor"));
shelf.addMember(new Member("M2", "Ilse"));
shelf.printLoans();
System.out.println(shelf.lend("T3", "M1", 4));
shelf.lend("T1", "M2", 6);
shelf.printLoans();
System.out.println("Late on day 12:");
shelf.printLate(12);
System.out.println("Late on day 14:");
shelf.printLate(14);

and expects:

No tools are out
T3 Ladder: Noor (M1), due day 11
T1 Cordless drill: Ilse (M2), due day 13
T3 Ladder: Noor (M1), due day 11
Late on day 12:
T3 Ladder: Noor (M1), due day 11
Late on day 14:
T3 Ladder: Noor (M1), due day 11
T1 Cordless drill: Ilse (M2), due day 13

Milestone 3: Refusals

So far, lend trusts what it's given. At a real desk, people mistype codes and ask for tools that are already out. Now the shelf refuses such loans with an exception of its own kind, so the desk can show the reason and go on.

In the new file LoanException.java, make LoanException a checked exception: it extends Exception, and its constructor takes the message and hands it on with super(message).

Then add to ToolShelf:

  • getTool(String code), which returns the tool with that code, or throws a LoanException with the message No tool with the code T9 (with the code it was given)
  • getMember(String card), the same for members: No member with the card M7

and change lend and giveBack so that they declare throws LoanException and refuse, before anything changes:

  • lend: a tool or a member that doesn't exist, with the messages above; a tool that is already out: T1 Cordless drill is out, due back on day 10; and a member who already has 2 tools out: Noor (M1) already has 2 tools. Check in this order and refuse at the first problem: the tool exists, the member exists, the tool is on the shelf, the member has fewer than 2 tools out. So a tool that is out gets the is out message even when the member also has 2 tools.
  • giveBack: a tool that doesn't exist, and a tool that isn't out: T3 Ladder is not out

The check runs:

ToolShelf shelf = new ToolShelf();
shelf.addTool(new Tool("T1", "Cordless drill"));
shelf.addTool(new Tool("T2", "Hand saw"));
shelf.addTool(new Tool("T3", "Ladder"));
shelf.addMember(new Member("M1", "Noor"));
shelf.lend("T1", "M1", 3);
shelf.lend("T2", "M1", 3);
try {
    shelf.lend("T9", "M1", 5);
} catch (LoanException e) {
    System.out.println(e.getMessage());
}
try {
    shelf.lend("T3", "M7", 5);
} catch (LoanException e) {
    System.out.println(e.getMessage());
}
try {
    shelf.lend("T1", "M1", 5);
} catch (LoanException e) {
    System.out.println(e.getMessage());
}
try {
    shelf.lend("T3", "M1", 5);
} catch (LoanException e) {
    System.out.println(e.getMessage());
}

and expects:

No tool with the code T9
No member with the card M7
T1 Cordless drill is out, due back on day 10
Noor (M1) already has 2 tools

Milestone 4: Saving to a CSV file

The shelf forgets everything when the program ends. Add to ToolShelf a method save(String fileName) that writes the whole shelf into that file in CSV form: one line per tool, member and loan, with the parts separated by commas.

  • first the tools, in the order of their codes: tool, the code and the name
  • then the members, in the order of their cards: member, the card and the name
  • last the loans, in the order of the tools' codes: loan, the tool's code, the member's card and the due day

Write the file with a PrintWriter in try-with-resources, so it's closed when the method ends. save declares throws IOException: the code that calls it decides what to do when the file can't be written. Names have no commas in them, so a comma always separates two parts.

The check builds a shelf, lends the hand saw to Ilse on day 4, and calls save("shelf.csv"):

ToolShelf shelf = new ToolShelf();
shelf.addTool(new Tool("T2", "Hand saw"));
shelf.addTool(new Tool("T1", "Cordless drill"));
shelf.addMember(new Member("M2", "Ilse"));
shelf.addMember(new Member("M1", "Noor"));
shelf.lend("T2", "M2", 4);
shelf.save("shelf.csv");

It prints nothing, and shelf.csv holds:

tool,T1,Cordless drill
tool,T2,Hand saw
member,M1,Noor
member,M2,Ilse
loan,T2,M2,11

Milestone 5: Loading, and damaged lines

Now the other direction. Add to ToolShelf a method load(String fileName) that reads a file that save wrote and adds its tools, members and loans to the shelf. It reads the file with a Scanner, line by line, and splits each line at the commas:

  • tool,T1,Cordless drill adds a tool, and member,M1,Noor a member.
  • loan,T3,M1,11 puts a loan of the tool T3 to the member M1, due on day 11, straight into the loans map (it's a loan that was already made, so lend's rules don't apply). The tool and the member must already be on the shelf: find them with getTool and getMember, which throw when they aren't.
  • An empty line is skipped.

A file can be damaged, for example by someone who edited it by hand. Then load throws a LoanException whose message starts with Line , the number of the line (the first line is 1, and empty lines count) and a colon:

  • a line that isn't one of the three kinds, or doesn't have their number of parts: Line 3: Can't read gadget,X1, with the whole line
  • a due day that isn't a whole number: Line 4: Not a day number: soon
  • a tool or a member that isn't on the shelf: Line 5: No tool with the code T9, with getTool's or getMember's message

The lines before the damaged one stay loaded. load declares throws IOException, LoanException: a file that can't be read at all throws an IOException, as Scanner does.

The check gives the program this shelf.csv:

tool,T1,Cordless drill
tool,T3,Ladder
member,M1,Noor
member,M2,Ilse
loan,T3,M1,11
loan,T1,M2,15

and runs:

ToolShelf shelf = new ToolShelf();
shelf.load("shelf.csv");
shelf.printTools();
shelf.printMembers();
shelf.printLoans();
shelf.printLate(14);

It expects:

T1 Cordless drill
T3 Ladder
Noor (M1)
Ilse (M2)
T1 Cordless drill: Ilse (M2), due day 15
T3 Ladder: Noor (M1), due day 11
T3 Ladder: Noor (M1), due day 11

Milestone 6: The desk

Last, the desk itself. The new file TextUi.java holds the class TextUi, the program's text UI. Its constructor takes a Scanner and a ToolShelf and keeps them in the fields scanner and shelf, and the day, kept in the int field today, starts as day 1. main creates the shelf and the TextUi, and calls its start(), which:

  1. Loads shelf.csv. When there's no such file (an IOException), it prints No shelf.csv yet, so the shelf starts empty and goes on. When the file is damaged (a LoanException), it prints Can't load shelf.csv. and the exception's message, and ends at once, without saving: saving would overwrite the lines it couldn't read.
  2. Reads commands, one per line, until quit or the end of the input. The words of a command are separated by spaces.
  3. Saves the shelf into shelf.csv and prints Saved shelf.csv (or Could not save: and the message when that fails).

The commands:

  • day 14 makes it day 14 and prints It's day 14. A day before today prints The day can't go back: it's day 14, and a day that isn't a whole number Not a day number: soon.
  • tools, members and loans print the shelf's tools, members and loans; late prints the loans that are late today.
  • lend T1 M2 lends T1 to M2 today and prints Lent: and the loan.
  • return T3 gives T3 back and prints T3 Ladder is back, followed by , 3 days late when it's late (, 1 day late for one day).
  • When the shelf refuses a loan or a return, the desk prints Sorry: and the exception's message.
  • Anything else, including a command with the wrong number of words, prints Unknown command: and the line.

Input isn't echoed. The check gives the program the shelf.csv of the introduction:

tool,T1,Cordless drill
tool,T2,Hand saw
tool,T3,Ladder
member,M1,Noor
member,M2,Ilse
loan,T3,M1,11

For the input shown, the program prints:

It's day 14
T3 Ladder: Noor (M1), due day 11
Lent: T1 Cordless drill: Ilse (M2), due day 21
Sorry: T3 Ladder is out, due back on day 11
T3 Ladder is back, 3 days late
Nothing is late
Saved shelf.csv