package MusicLandscape.application;package MusicLandscape.application;This is the meat of the LabWork assignment.
import java.beans.XMLDecoder;
import java.beans.XMLEncoder;
import java.io.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import MusicLandscape.container.MyTrackContainer;
import MusicLandscape.entities.Track;
import MusicLandscape.util.ConsoleScanable;
import MusicLandscape.util.ConsoleScanner;
import MusicLandscape.util.MyFormatter;
import MusicLandscape.util.MyMatcher;
import MusicLandscape.util.comparators.DurationComparator;
import MusicLandscape.util.comparators.PerformerComparator;
import MusicLandscape.util.comparators.TitleComparator;
import MusicLandscape.util.comparators.WriterComparator;
import MusicLandscape.util.comparators.YearComparator;
import MusicLandscape.util.formatters.CSVTrackFormatter;
import MusicLandscape.util.formatters.LongTrackFormatter;
import MusicLandscape.util.formatters.ShortTrackFormatter;
import MusicLandscape.util.io.MyTrackCSVReader;
import MusicLandscape.util.io.MyWriter;
import MusicLandscape.util.matcher.*;
import org.testng.util.Strings;
/**
* FinalTrackDataBase
* ==================
* <p>
* This is "a text based menu-driven, object-oriented console application with a sortable and searchable list of tracks
* and multiple display formats that supports saving/loading of data to/from a .csv file."
*
* <p>
* It also supports loading from and saving to XML files.
*
* @author Jonas Altrock (ew20b126@technikum-wien.at)
* @version 1
* @since LabWork
*/
public class Main {
private final Pattern confirmation = Pattern.compile("^(y|yes)$", Pattern.CASE_INSENSITIVE);These properties were in the provided skeleton.
private final MyTrackContainer db = new MyTrackContainer();
private final List<Comparator<Track>> comparators = new LinkedList<Comparator<Track>>();
private final List<MyFormatter<Track>> formatters = new LinkedList<MyFormatter<Track>>();
private final List<MyMatcher<Track>> matchers = new LinkedList<MyMatcher<Track>>();
private Comparator<Track> theComp;
private boolean asc = true;
private MyFormatter<Track> theFormat;The provided class has a single menu property, I transformed this into a Stack,
because I wanted to implement nested menus. This way if I am in the selection
for editing a track I can render a prompt
Main > Edit > Select track:
private final Stack<Menu> menus = new Stack<>();In case you didn’t know: you can just add a {...} block into the class, and this
code will be executed when the class is instantiated. Not when an object of the class
is created with new, but when the class itself is loaded into memory by Java.
{
comparators.add(theComp = new TitleComparator());
comparators.add(new DurationComparator());
comparators.add(new WriterComparator());
comparators.add(new PerformerComparator());
comparators.add(new YearComparator());
matchers.add(new AlwaysMatcher());
matchers.add(new TitleMatcher(""));
matchers.add(new DurationMatcher());
matchers.add(new PerformerMatcher(""));
matchers.add(new WriterMatcher(""));
matchers.add(new YearMatcher());
formatters.add(theFormat = new LongTrackFormatter());
formatters.add(new ShortTrackFormatter());
formatters.add(new CSVTrackFormatter());Here we can see the effect of me re-writing the menu system to suit my
needs. The provided class structure located the main menu items within
the Menu class as objects with the execute() method overridden.
The CallbackMenuItem makes it really short to define a MenuItem that
just calls some function in its execute() method.
The syntax Main.this::method is weird but useful here. It is called
the qualified this and denotes the “this” object of the
lexically enclosing instance. We only ever have one instance of Main,
so we can mostly ignore what that actually means.
It would be clearer to put these lines into a Main() constructor and use
this::display etc. to make it obvious what is happening.
Menu mainMenu = new Menu("Main", "Select action", "show menu", new MenuItem[]{
new CallbackMenuItem("display selection", Main.this::display),
new CallbackMenuItem("add track", Main.this::addTrack),
new CallbackMenuItem("edit track", Main.this::editTracks),
new CallbackMenuItem("reset selection", Main.this::resetSelection),
new CallbackMenuItem("filter selection", Main.this::filterSelection),
new CallbackMenuItem("remove selection", Main.this::removeSelection),
new CallbackMenuItem("reverse sorting order", Main.this::reverseSortOrder),
new CallbackMenuItem("select sorting", Main.this::selectSorting),
new CallbackMenuItem("select formatting", Main.this::selectFormatting),
new CallbackMenuItem("load tracks from CSV", Main.this::loadTracks),
new CallbackMenuItem("save selection to CSV", Main.this::saveTracks),
new CallbackMenuItem("load tracks from XML", Main.this::loadXML),
new CallbackMenuItem("save selection to XML", Main.this::saveXML),
});
menus.push(mainMenu);
}
private static final String WELCOME_TEXT = "Welcome to the FinalTrackDataBase";
private static final String GOOD_BYE_TEXT = "Thank you for using FinalTrackDataBase";The provided code had a final int id field per MenuItem. I pushed the numbering
into the Menu class, because I think the menu item itself does not need to know
which number it has.
private static abstract class MenuItem {
String text;
abstract void execute();
MenuItem(String s) {
text = s;
}
public String toString() {
return text;
}
}A callback in Java is an object of type Runnable. The syntax object::method
is a short-hand for that.
private static class CallbackMenuItem extends MenuItem {
Runnable cb;
CallbackMenuItem(String s, Runnable r) {
super(s);
cb = r;
}
@Override
void execute() {
cb.run();
}
}The selection menu items will be things like:
private static class SelectionMenuItem<T> extends MenuItem {
T option;
boolean selected;A Consumer is like a Runnable a reference to a function, but
one that takes an argument of type T.
Consumer<T> onSelect;
public SelectionMenuItem(T value, boolean chosen, Consumer<T> callback) {
super(value.toString());
option = value;
selected = chosen;
onSelect = callback;
}
@Override
void execute() {
onSelect.accept(option);
}
@Override
public String toString() {
return super.toString() + (selected ? " (selected)" : "");
}
} private static class Menu {
private final ArrayList<MenuItem> menu = new ArrayList<>();
private final String prompt;
private final String title;
public Menu(String title, String prompt, String displayMenuLabel) {
this.title = title;
this.prompt = prompt;The first option (0) is always to display all options again.
menu.add(new MenuItem(displayMenuLabel) {
void execute() {
display();
}
});
}
public Menu(String title, String prompt, String displayMenuLabel, MenuItem[] items) {
this(title, prompt, displayMenuLabel);
menu.addAll(Arrays.asList(items));
}
void display() {
for (int i = 0; i < menu.size(); i++) {
String item = menu.get(i).toString();
if (item != null) {
System.out.println(i + ":\t" + item);
}
}
System.out.println("99:\texit");
}
public boolean execute(int item) {
if (item < 0 || item >= menu.size()) {
return false;
}
menu.get(item).execute();
return true;
}
public String getTitle() {
return title;
}
public String getPrompt() {
return prompt;
}
} /**
* Enter into a submenu/selection.
*
* @param m the Menu
*/
public void enter(Menu m) {
menus.push(m);
m.execute(0);
}
/**
* Exit from a submenu/selection.
*
* @return the Menu we just left
*/
public Menu exit() {
if (menus.size() > 1) {
return menus.pop();
}
return null;
} /**
* Main application entry point from the command line.
*
* @param args arguments (ignored here)
*/
public static void main(String[] args) {
System.out.println(WELCOME_TEXT);
new Main().go();
System.out.println(GOOD_BYE_TEXT);
}I make a prompt before the user input like Main > Edit > Select track:.
/**
* Get a prompt that shows to the user the menu hierarchy and the currently asked item.
*
* @return a prompt string
*/
public String prompt() {
String path = Strings.join(" > ", menus.stream().map(Menu::getTitle).toArray(String[]::new));
return (path.isEmpty() ? menus.peek().getTitle() : path) + " > " + menus.peek().getPrompt();
}This is the heart of the application: the input loop.
Read in a number, pass it to the current menu to execute the corresponding option/item, and maybe exit.
/**
* Execute the menu input loop.
*/
public void go() {Initial action: display menu.
menus.peek().execute(0);
while (true) {
String input = ConsoleScanner.nonEmptyString.scan(prompt());
if (input == null) {
continue;
}
try {
int action = Integer.parseInt(input);
if (menus.peek().execute(action)) {
continue;
}
} catch (NumberFormatException ignored) {
}If we get here the user wrote some random string or a high number. Exit the current submenu first.
if (exit() != null) {
continue;
}If we were in the main menu, offer program exit.
String confirm = ConsoleScanner.nonEmptyString.scan("Exit? (y/N)");
if (confirm != null) {
if (confirmation.matcher(confirm).matches()) {
break;
}
}If no action was matched and user does not want to exit, display menu again.
menus.peek().execute(0);
}
}The second half of the file is all the methods that implement the main menu actions.
/**
* Display the track database selection
*/
public void display() {
System.out.println("Displaying selection:");
if (db.size() == 0) {
System.out.print("No tracks stored.\n");
return;
}
if (db.selection().length == 0) {
System.out.print("Selection empty.\n");
return;
}
System.out.println('\n' + theFormat.header());
System.out.println(theFormat.topSeparator());
for (Track tt : db.selection())
System.out.println(theFormat.format(tt));
System.out.println();
System.out.printf("%d out of %d tracks selected.\n", db.selection().length, db.size());
} /**
* Add a track to the database.
*/
public void addTrack() {
System.out.println("Adding new track.");
Track t = new Track();
if (t.scan()) {
System.out.println("Added track " + t);
Main.this.db.add(t);
} else {
System.out.println("You did not enter any data. Track discarded.");
}
} /**
* Edit a specific track via the ConsoleScanable process.
*
* @param t the track to modify
*/
public void editTrack(Track t) {
if (t.scan()) {
System.out.println("Successfully changed " + t);
} else {
System.out.println("Track remains unchanged.");
}
}This is the action called by the CallbackMenuItem. It creates its own Menu
so that the user can select which track to edit.
/**
* Edit tracks: select which track to edit, then modify that.
*/
public void editTracks() {
Consumer<Track> selectTrack = (t) -> {
editTrack(t);
db.sort(theComp, asc);
exit();
};
MenuItem[] tracks = Arrays.stream(db.selection()).map((t) -> new SelectionMenuItem<>(t, false, selectTrack)).toArray(MenuItem[]::new);
enter(new Menu("Edit", "Select track", null, tracks));
} /**
* Reset the track container selection.
*/
public void resetSelection() {
db.reset();
db.sort(theComp, asc);
System.out.println("Resetting selection. " + db.selection().length + " tracks selected.");
} /**
* Choose the display formatting for tracks.
*/
public void selectFormatting() {
Consumer<MyFormatter<Track>> selectFormatter = (f) -> {
System.out.println(f + " selected.");
theFormat = f;
exit();
};
MenuItem[] options = formatters.stream().map((f) -> new SelectionMenuItem<>(f, f == theFormat, selectFormatter)).toArray(MenuItem[]::new);
enter(new Menu("Formatting", "Select formatting", null, options));
} /**
* Helper function to print the current sorting.
*/
protected void printSorting() {
System.out.println("Selection is now sorted " + theComp + " (" + (asc ? "ascending" : "descending") + ").");
}Sorting selection is again a submenu of the available comparator options.
/**
* Select the sorting comparator.
*/
public void selectSorting() {
Consumer<Comparator<Track>> select = (c) -> {
theComp = c;
db.sort(theComp, asc);
printSorting();
exit();
};
MenuItem[] options = comparators.stream().map((c) -> new SelectionMenuItem<>(c, c == theComp, select)).toArray(MenuItem[]::new);
enter(new Menu("Sorting", "Select sorting", null, options));
}Reverse sorting menu action.
/**
* Reverse the selection ordering.
*/
public void reverseSortOrder() {
asc = !asc;
db.sort(theComp, asc);
printSorting();
} /**
* Remove the selected tracks from the database.
*/
public void removeSelection() {
System.out.println("Removing selection.");
String confirm = ConsoleScanner.nonEmptyString.unskippable().scan("Do you really wish to remove " + db.selection().length + " tracks? (y/N)");
if (confirmation.matcher(confirm).matches()) {
int removed = db.remove();
System.out.println("Removed " + removed + " track(s).");
} else {
System.out.println("Database remains unchanged.");
}
} /**
* Filter the track selection.
*/
public void filterSelection() {
System.out.println("Filtering selection.");
Consumer<MyMatcher<Track>> onSelectFilter = (MyMatcher<Track> m) -> {
if (m instanceof ConsoleScanable) {
System.out.println("You may now modify the filter \"" + m + "\".");
((ConsoleScanable) m).scan();
}
System.out.println("\"" + m + "\" filter applied " +
"(" + db.filter(m) + " tracks filtered, " + db.selection().length + " tracks selected).");
exit();
};
MenuItem[] options = matchers.stream().map((c) -> new SelectionMenuItem<>(c, false, onSelectFilter)).toArray(MenuItem[]::new);
enter(new Menu("Filter", "Select filtering", null, options));
}Helper function to ask for a file name.
protected String askForFileName() {
String fileName = ConsoleScanner.nonEmptyString.scan("Enter a file name");
if (fileName == null) {
System.out.println("No file name given.");
}
return fileName;
}Load tracks from CSV
/**
* Load tracks from a CSV file.
*/
public void loadTracks() {
System.out.println("Loading tracks from CSV file.");
String fileName = askForFileName();
if (fileName == null) {
return;
}
FileReader file;
try {
file = new FileReader(fileName);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
return;
}
int num = 0;
MyTrackCSVReader reader = new MyTrackCSVReader(new BufferedReader(file));
Track t = reader.get();
while (t != null) {
if (db.add(t)) {
num += 1;
}
t = reader.get();
}
System.out.println(num + " tracks imported.");
}Save tracks to CSV.
/**
* Write tracks to a CSV file.
*/
public void saveTracks() {
System.out.println("Saving selected tracks to CSV file.");
String fileName = askForFileName();
if (fileName == null) {
return;
}
FileWriter file;
try {
file = new FileWriter(fileName);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
return;
}
MyWriter<Track> writer = new MyWriter<>(file, new CSVTrackFormatter());
int num = 0;
for (Track t : db.selection()) {
if (writer.put(t)) {
num++;
}
}
try {
writer.close();
System.out.println("Successfully wrote " + num + " tracks to " + fileName);
} catch (IOException e) {
System.out.println("Error: " + e.getMessage());
}
} /**
* Write tracks to an XML file.
*/
public void saveXML() {
System.out.println("Saving selected tracks to XML file.");
String fileName = askForFileName();
if (fileName == null) {
return;
}
XMLEncoder encoder;
FileOutputStream file;
try {
file = new FileOutputStream(fileName);
encoder = new XMLEncoder(file);
for (Track t : db.selection()) {
encoder.writeObject(t);
}
encoder.flush();
encoder.close();
System.out.println("Successfully wrote " + db.selection().length + " tracks to " + fileName);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
}
/**
* Load tracks from an XML file.
*/
public void loadXML() {
System.out.println("Loading tracks from XML file.");
String fileName = askForFileName();
if (fileName == null) {
return;
}
XMLDecoder decoder;
FileInputStream file;
try {
file = new FileInputStream(fileName);
decoder = new XMLDecoder(file);
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
return;
}
Track t;
int read = 0;
try {
while (true) {
t = (Track) decoder.readObject();
if (t == null) {
break;
}
if (db.add(t)) {
read += 1;
}
}
decoder.close();
} catch (IndexOutOfBoundsException ignored) {
} catch (Exception e) {
System.out.println("Error: " + e.getMessage());
}
System.out.println("Successfully read " + read + " tracks from " + fileName);
}
}