This lesson is one project: a small inventory program for a shop. It reads the stock from a file and keeps one
object per item. It refuses lines that make no sense, and prints a report.
Every piece comes from an earlier lesson. Each exercise leaves a method or two for you to write; the rest is there
already.
Step 2 of 20 · Concept
From the file to the report
The data flows through the classes in order:
InventoryFile.load reads every line of inventory.txt.
LineParser.parse turns one line into an Item, or refuses it.
Inventory keeps the items, by name.
Report prints the table.
Step 3 of 20 · Quiz
Who refuses a line?
A line of inventory.txt says fig,x,80. Which class finds out that it is wrong?
Two tries
Step 4 of 20 · Demo
The project and its data
Run it. It prints the lines of inventory.txt, then one Item and its value. Item is a record (lesson 16) with
one method of its own, value().
Step 5 of 20 · Concept
Real data has mistakes
Each line is name,quantity,price. The price is in cents, so every number is an int: 12 apples at 30 cents each.
Four lines are wrong on purpose:
line
what is wrong
fig,x,80
x is not a number
plum,-4,60
a negative quantity
pear,7,45
a second pear
lemon,8
no price
A good program says which line is wrong, and carries on.
Step 6 of 20 · Concept
The inventory in memory
Inventory keeps the items in a TreeMap from name to item, so all() lists them by name.
add checks its argument, as withdraw did in lesson 14. A second item with a name it already has is refused with
an IllegalArgumentException.
Step 7 of 20 · Quiz
A second pear
The inventory holds a pear. What does inv.add(new Item("pear", 7, 45)) do?
Two tries
Step 8 of 20 · Exercise
Your turn: stock and value
In Inventory.java, write two methods, each with a stream (lesson 18):
LineParser.parse turns one line into an Item. When the line is wrong, it throws a BadLineException, the
project's own checked exception (lesson 14). Its message says what is wrong with the data.
Integer.parseInt throws a NumberFormatException for x. parse catches it and throws a BadLineException
instead. The caller then has one kind of mistake to handle.
Step 10 of 20 · Quiz
Why a new exception?
parse catches the NumberFormatException of fig,x,80 and throws a BadLineException. What does that give
the caller?
Two tries
Step 11 of 20 · Exercise
Your turn: parse a line
Write parse. Split the line at commas, and throw a BadLineException with the line after this message:
expected name,quantity,price: when there are not 3 parts,
not a number: when a number does not parse,
negative number: when a number is below 0.
Otherwise return the Item. The program must print exactly:
OK: Item[name=apple, quantity=12, price=30]
Bad: not a number: fig,x,80
Bad: negative number: plum,-4,60
Bad: expected name,quantity,price: lemon,8
Check in the order of the list: first parts.length != 3, then the numbers, then whether one is negative.
Declare int quantity; and int price; before a try. Parse both inside it. Its catch (NumberFormatException e) throws the BadLineException.
Write load: read every line of the file, parse it and add the item to the inventory.
A line can fail two ways: a BadLineException from parse, or an IllegalArgumentException from add. Catch
each, print Line , the line's number from 1, : and the message, and go on. The output must be exactly:
Line 3: not a number: fig,x,80
Line 5: negative number: plum,-4,60
Line 6: duplicate item: pear
Line 8: expected name,quantity,price: lemon,8
Loaded 4 items
Read the lines with Files.readAllLines(file) (lesson 19). Loop over the indices, so you know each line's number.
One try around inv.add(LineParser.parse(lines.get(i)));, with two catch blocks after it, one per exception.
for(int i =0; i < lines.size(); i++){
String where ="Line "+(i +1)+": ";try{
inv.add(LineParser.parse(lines.get(i)));}catch(BadLineException e){
System.out.println(where + e.getMessage());}
Step 13 of 20 · Concept
Columns that line up
The report needs two more:
format
prints
%7s
text in 7 columns; without the -, the spaces go before it
%7.2f
a double in 7 columns, the point and 2 decimals included
Spaces before the numbers line up their last digits, so a column of numbers reads well.
Step 14 of 20 · Quiz
What will it print?
The | marks where each column ends. Type exactly what this prints, spaces included.
Change only those four lines. Every line keeps the shape name,quantity,price, with no spaces.
Step 18 of 20 · Challenge · optional
Challenge: a restock file
Optional. It earns the lesson's third star.
The data can go back to storage too. Write saveRestock: one line name,amount for each item under limit, where
amount brings its quantity up to 10. main then prints the file:
Line 3: not a number: fig,x,80
Line 5: negative number: plum,-4,60
Line 6: duplicate item: pear
Line 8: expected name,quantity,price: lemon,8
banana,10
pear,7
inv.lowStock(limit) gives the items. Build the text in one String, as save did in lesson 19, then write it.
An item's amount is 10 - item.quantity(). Each line ends with "\n". Write it all with Files.writeString(file, text).
Step 19 of 20 · Concept
What you can build now
You can write a Java program of several classes and packages. It reads and writes files, and keeps its data in
lists, maps and records. It checks its input with exceptions, and prints what it found.
That is enough for:
a to-do list that keeps its tasks in todo.txt between runs,
a quiz that reads its questions from a file,
a grade book or a budget that prints a report,
a text game of rooms and items, each a class of its own.
The Playground, linked from the course page, is an editor with a file tree and the same Java.
Build any of these there.
A project splits into packages, one folder each: app, app.model, app.io.
Data flows from a file, through a parser, into objects, and out as a report.
A parser refuses bad lines with its own exception, and the loader carries on.
printf widths such as %-8s and %7.2f line up the columns.
Every piece came from an earlier lesson: records, maps, streams, exceptions and files.
Next: the Playground. The course is complete: build a program of your own.
Review
4 questions from this lesson, in a new order each time. Answer from memory: 3 of 4 right on the first try passes. On a day after you finish the lesson, a pass earns 15 XP.
The review needs JavaScript.
package app;
import app.model.Item;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
for (String line : Files.readAllLines(file)) {
System.out.println(line);
}
Item apple = new Item("apple", 12, 30);
System.out.println(apple);
System.out.println("Value: " + apple.value());
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
apple,12,30
pear,3,45
fig,x,80
kiwi,25,20
plum,-4,60
pear,7,45
banana,0,25
lemon,8
package app;
import app.model.Item;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
for (String line : Files.readAllLines(file)) {
System.out.println(line);
}
Item apple = new Item("apple", 12, 30);
System.out.println(apple);
System.out.println("Value: " + apple.value());
}
}
package app;
import app.model.Inventory;
import app.model.Item;
public class Main {
public static void main(String[] args) {
Inventory inv = new Inventory();
inv.add(new Item("apple", 12, 30));
inv.add(new Item("pear", 3, 45));
inv.add(new Item("banana", 0, 25));
try {
inv.add(new Item("pear", 7, 45));
} catch (IllegalArgumentException e) {
System.out.println("Refused: " + e.getMessage());
}
for (Item item : inv.lowStock(5)) {
System.out.println("Low: " + item.name());
}
System.out.println("Total value: " + inv.totalValue());
}
}
package app.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Inventory {
private final Map<String, Item> items = new TreeMap<>();
public void add(Item item) {
if (items.containsKey(item.name())) {
throw new IllegalArgumentException(
"duplicate item: " + item.name());
}
items.put(item.name(), item);
}
public int size() {
return items.size();
}
public List<Item> all() {
return new ArrayList<>(items.values());
}
// the items with a quantity under limit, by name
public List<Item> lowStock(int limit) {
return List.of();
}
// the value of every item added up, in cents
public int totalValue() {
return 0;
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
package app;
import app.io.BadLineException;
import app.io.LineParser;
import app.model.Item;
public class Main {
public static void main(String[] args) {
String[] lines = {
"apple,12,30", "fig,x,80", "plum,-4,60", "lemon,8"
};
for (String line : lines) {
try {
Item item = LineParser.parse(line);
System.out.println("OK: " + item);
} catch (BadLineException e) {
System.out.println("Bad: " + e.getMessage());
}
}
}
}
package app.io;
import app.model.Item;
public class LineParser {
// a line "name,quantity,price" as an Item
public static Item parse(String line)
throws BadLineException {
throw new BadLineException("not written yet: " + line);
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
package app.io;
public class BadLineException extends Exception {
public BadLineException(String message) {
super(message);
}
}
package app;
import app.io.BadLineException;
import app.io.LineParser;
import app.model.Item;
public class Main {
public static void main(String[] args) {
String[] lines = {
"apple,12,30", "fig,x,80", "plum,-4,60", "lemon,8"
};
for (String line : lines) {
try {
Item item = LineParser.parse(line);
System.out.println("OK: " + item);
} catch (BadLineException e) {
System.out.println("Bad: " + e.getMessage());
}
}
}
}
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(file);
System.out.println("Loaded " + inv.size() + " items");
}
}
package app.io;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class InventoryFile {
// every line parsed and added; a bad line is
// printed with its number and skipped
public static Inventory load(Path file) throws IOException {
Inventory inv = new Inventory();
return inv;
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
package app.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Inventory {
private final Map<String, Item> items = new TreeMap<>();
public void add(Item item) {
if (items.containsKey(item.name())) {
throw new IllegalArgumentException(
"duplicate item: " + item.name());
}
items.put(item.name(), item);
}
public int size() {
return items.size();
}
public List<Item> all() {
return new ArrayList<>(items.values());
}
// the items with a quantity under limit, by name
public List<Item> lowStock(int limit) {
return all().stream()
.filter(i -> i.quantity() < limit)
.toList();
}
// the value of every item added up, in cents
public int totalValue() {
return all().stream().mapToInt(i -> i.value()).sum();
}
}
package app.io;
public class BadLineException extends Exception {
public BadLineException(String message) {
super(message);
}
}
package app.io;
import app.model.Item;
public class LineParser {
// a line "name,quantity,price" as an Item
public static Item parse(String line)
throws BadLineException {
String[] parts = line.split(",");
if (parts.length != 3) {
throw new BadLineException(
"expected name,quantity,price: " + line);
}
int quantity;
int price;
try {
quantity = Integer.parseInt(parts[1]);
price = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
throw new BadLineException("not a number: " + line);
}
if (quantity < 0 || price < 0) {
throw new BadLineException(
"negative number: " + line);
}
return new Item(parts[0], quantity, price);
}
}
apple,12,30
pear,3,45
fig,x,80
kiwi,25,20
plum,-4,60
pear,7,45
banana,0,25
lemon,8
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(file);
System.out.println("Loaded " + inv.size() + " items");
}
}
public class Widths {
public static void main(String[] args) {
System.out.printf("%-6s|%4d|%n", "fig", 3);
System.out.printf("%-6s|%4d|%n", "banana", 12);
}
}
public class Widths {
public static void main(String[] args) {
System.out.printf("%-6s|%4d|%n", "fig", 3);
System.out.printf("%-6s|%4d|%n", "banana", 12);
}
}
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(file);
Report.printTable(inv);
Report.printLowStock(inv, 5);
}
}
package app;
import app.model.Inventory;
import app.model.Item;
import java.util.stream.Collectors;
public class Report {
// the header, one row per item (prices and
// values in dollars), then the total value
public static void printTable(Inventory inv) {
System.out.printf("%-8s %5s %7s %7s%n",
"Item", "Qty", "Price", "Value");
}
// the names of the items with a quantity
// under limit, separated by ", "
public static void printLowStock(Inventory inv, int limit) {
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
package app.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Inventory {
private final Map<String, Item> items = new TreeMap<>();
public void add(Item item) {
if (items.containsKey(item.name())) {
throw new IllegalArgumentException(
"duplicate item: " + item.name());
}
items.put(item.name(), item);
}
public int size() {
return items.size();
}
public List<Item> all() {
return new ArrayList<>(items.values());
}
// the items with a quantity under limit, by name
public List<Item> lowStock(int limit) {
return all().stream()
.filter(i -> i.quantity() < limit)
.toList();
}
// the value of every item added up, in cents
public int totalValue() {
return all().stream().mapToInt(i -> i.value()).sum();
}
}
package app.io;
public class BadLineException extends Exception {
public BadLineException(String message) {
super(message);
}
}
package app.io;
import app.model.Item;
public class LineParser {
// a line "name,quantity,price" as an Item
public static Item parse(String line)
throws BadLineException {
String[] parts = line.split(",");
if (parts.length != 3) {
throw new BadLineException(
"expected name,quantity,price: " + line);
}
int quantity;
int price;
try {
quantity = Integer.parseInt(parts[1]);
price = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
throw new BadLineException("not a number: " + line);
}
if (quantity < 0 || price < 0) {
throw new BadLineException(
"negative number: " + line);
}
return new Item(parts[0], quantity, price);
}
}
package app.io;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class InventoryFile {
// every line parsed and added; a bad line is
// printed with its number and skipped
public static Inventory load(Path file) throws IOException {
Inventory inv = new Inventory();
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String where = "Line " + (i + 1) + ": ";
try {
inv.add(LineParser.parse(lines.get(i)));
} catch (BadLineException e) {
System.out.println(where + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println(where + e.getMessage());
}
}
return inv;
}
}
apple,12,30
pear,3,45
fig,x,80
kiwi,25,20
plum,-4,60
pear,7,45
banana,0,25
lemon,8
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(file);
Report.printTable(inv);
Report.printLowStock(inv, 5);
}
}
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(file);
Report.printTable(inv);
Report.printLowStock(inv, 5);
}
}
package app;
import app.model.Inventory;
import app.model.Item;
import java.util.stream.Collectors;
public class Report {
public static void printTable(Inventory inv) {
System.out.printf("%-8s %5s %7s %7s%n",
"Item", "Qty", "Price", "Value");
for (Item item : inv.all()) {
System.out.printf("%-8s %5d %7.2f %7.2f%n",
item.name(), item.quantity(),
item.price() / 100.0, item.value() / 100.0);
}
System.out.printf("Total value: %.2f%n",
inv.totalValue() / 100.0);
}
public static void printLowStock(Inventory inv, int limit) {
String names = inv.lowStock(limit).stream()
.map(i -> i.name())
.collect(Collectors.joining(", "));
System.out.println("Low stock (under " + limit + "): "
+ names);
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
package app.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Inventory {
private final Map<String, Item> items = new TreeMap<>();
public void add(Item item) {
if (items.containsKey(item.name())) {
throw new IllegalArgumentException(
"duplicate item: " + item.name());
}
items.put(item.name(), item);
}
public int size() {
return items.size();
}
public List<Item> all() {
return new ArrayList<>(items.values());
}
// the items with a quantity under limit, by name
public List<Item> lowStock(int limit) {
return all().stream()
.filter(i -> i.quantity() < limit)
.toList();
}
// the value of every item added up, in cents
public int totalValue() {
return all().stream().mapToInt(i -> i.value()).sum();
}
}
package app.io;
public class BadLineException extends Exception {
public BadLineException(String message) {
super(message);
}
}
package app.io;
import app.model.Item;
public class LineParser {
// a line "name,quantity,price" as an Item
public static Item parse(String line)
throws BadLineException {
String[] parts = line.split(",");
if (parts.length != 3) {
throw new BadLineException(
"expected name,quantity,price: " + line);
}
int quantity;
int price;
try {
quantity = Integer.parseInt(parts[1]);
price = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
throw new BadLineException("not a number: " + line);
}
if (quantity < 0 || price < 0) {
throw new BadLineException(
"negative number: " + line);
}
return new Item(parts[0], quantity, price);
}
}
package app.io;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class InventoryFile {
// every line parsed and added; a bad line is
// printed with its number and skipped
public static Inventory load(Path file) throws IOException {
Inventory inv = new Inventory();
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String where = "Line " + (i + 1) + ": ";
try {
inv.add(LineParser.parse(lines.get(i)));
} catch (BadLineException e) {
System.out.println(where + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println(where + e.getMessage());
}
}
return inv;
}
}
apple,12,30
pear,3,45
fig,x,80
kiwi,25,20
plum,-4,60
pear,7,45
banana,0,25
lemon,8
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path file = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(file);
Report.printTable(inv);
Report.printLowStock(inv, 5);
}
}
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path data = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(data);
Path out = Path.of("restock.txt");
Files.deleteIfExists(out);
InventoryFile.saveRestock(inv, 5, out);
if (Files.exists(out)) {
System.out.print(Files.readString(out));
} else {
System.out.println("There is no restock.txt");
}
}
}
package app.io;
import app.model.Inventory;
import app.model.Item;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class InventoryFile {
// every line parsed and added; a bad line is
// printed with its number and skipped
public static Inventory load(Path file) throws IOException {
Inventory inv = new Inventory();
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String where = "Line " + (i + 1) + ": ";
try {
inv.add(LineParser.parse(lines.get(i)));
} catch (BadLineException e) {
System.out.println(where + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println(where + e.getMessage());
}
}
return inv;
}
// a line "name,amount" per item under limit;
// amount: what brings its quantity to 10
public static void saveRestock(Inventory inv, int limit,
Path file) throws IOException {
}
}
package app.model;
public record Item(String name, int quantity, int price) {
public int value() {
return quantity * price;
}
}
package app.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public class Inventory {
private final Map<String, Item> items = new TreeMap<>();
public void add(Item item) {
if (items.containsKey(item.name())) {
throw new IllegalArgumentException(
"duplicate item: " + item.name());
}
items.put(item.name(), item);
}
public int size() {
return items.size();
}
public List<Item> all() {
return new ArrayList<>(items.values());
}
// the items with a quantity under limit, by name
public List<Item> lowStock(int limit) {
return all().stream()
.filter(i -> i.quantity() < limit)
.toList();
}
// the value of every item added up, in cents
public int totalValue() {
return all().stream().mapToInt(i -> i.value()).sum();
}
}
package app.io;
public class BadLineException extends Exception {
public BadLineException(String message) {
super(message);
}
}
package app.io;
import app.model.Item;
public class LineParser {
// a line "name,quantity,price" as an Item
public static Item parse(String line)
throws BadLineException {
String[] parts = line.split(",");
if (parts.length != 3) {
throw new BadLineException(
"expected name,quantity,price: " + line);
}
int quantity;
int price;
try {
quantity = Integer.parseInt(parts[1]);
price = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
throw new BadLineException("not a number: " + line);
}
if (quantity < 0 || price < 0) {
throw new BadLineException(
"negative number: " + line);
}
return new Item(parts[0], quantity, price);
}
}
apple,12,30
pear,3,45
fig,x,80
kiwi,25,20
plum,-4,60
pear,7,45
banana,0,25
lemon,8
package app;
import app.io.InventoryFile;
import app.model.Inventory;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Main {
public static void main(String[] args) throws IOException {
Path data = Path.of("inventory.txt");
Inventory inv = InventoryFile.load(data);
Path out = Path.of("restock.txt");
Files.deleteIfExists(out);
InventoryFile.saveRestock(inv, 5, out);
if (Files.exists(out)) {
System.out.print(Files.readString(out));
} else {
System.out.println("There is no restock.txt");
}
}
}