• Main.java

  • §
    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";
  • §

    MenuItem

    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;
            }
        }
  • §

    CallbackMenuItem

    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();
            }
        }
  • §

    SelectionMenuItem

    The selection menu items will be things like:

    • the sorting options
    • the filter options
    • the tracks to edit
        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)" : "");
            }
        }
  • §

    Menu

    The inner class Menu shows the numbered list of options.

        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;
            }
        }
  • §

    Menu stack enter/exit

    Because there is a stack of menus, I can enter and exit submenus.

        /**
         * 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()

    Here the application actually starts its execution.

        /**
         * 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);
            }
        }
  • §

    Program actions

    The second half of the file is all the methods that implement the main menu actions.

  • §

    Display selection

        /**
         * 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 track

        /**
         * 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 track

    First a helper function.

        /**
         * 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 selection

        /**
         * Reset the track container selection.
         */
        public void resetSelection() {
            db.reset();
            db.sort(theComp, asc);
            System.out.println("Resetting selection. " + db.selection().length + " tracks selected.");
        }
  • §

    Select formatting

    This also does a submenu from the formatting options.

        /**
         * 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));
        }
  • §

    Sorting

    First a helper function to print out the current sorting.

        /**
         * 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 selection

        /**
         * 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 selection

        /**
         * 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));
        }
  • §

    Import/export CSV

  • §

    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());
            }
        }
  • §

    Import/export XML

        /**
         * 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);
        }
    }