• Track.java

  • §
    package MusicLandscape.entities;
    
    import MusicLandscape.util.ConsoleScanner;
    import MusicLandscape.util.ConsoleScanable;
    
    import java.beans.*;
    import java.util.Scanner;
  • §

    The Track is the same as before, only the scan() method needs to ask the user for all fields so they can completely edit/add tracks.

    /**
     * represents a piece of music that has been released on some kind of media (CD, vinyl, video, ...)
     *
     * @author Jonas Altrock (ew20b126@technikum-wien.at)
     * @version 5
     * @since ExerciseSheet01
     */
  • §

    This JavaBean annotation is not necessary. A “bean” in Java-speak is any object that has some properties with getters and setters. The clue is that the XMLEncoder and XMLDecoder can read and write any bean object to and from a file.

    @JavaBean(description = "A piece of music.", defaultProperty = "title")
    public class Track implements ConsoleScanable {
        /**
         * the duration of this track in seconds
         * <p>
         * the duration is a non-negative number, duration 0 (zero) represents unknown duration
         */
        private int duration = 0;
        /**
         * the artist who performs this track
         * <p>
         * the performer cannot be null
         */
        private Artist performer = new Artist();
    
        /**
         * the title of this track.
         */
        private String title;
    
        /**
         * the artist who wrote this track
         * <p>
         * the writer cannot be null
         */
        private Artist writer = new Artist();
    
        /**
         * the year in which the Track was or will be produced
         * <p>
         * valid years are between 1900-2999
         */
        private int year = 1900;
    
        /**
         * creates a default track.
         * <p>
         * a default track has the following values:
         * <ul>
         *  <li>unknown title
         *  <li>duration 0
         *  <li>default writer and performer
         *  <li>year 1900
         * </ul>
         */
        public Track() {
        }
    
        /**
         * creates a track with a certain title
         * <p>
         * the resulting track has the specified title, all other values are default
         *
         * @param title the title of this track
         */
        public Track(String title) {
            this.title = title;
        }
    
        /**
         * creates a deep copy of a Track
         *
         * @param other the track to copy
         */
        public Track(Track other) {
            title = other.getTitle();
            duration = other.getDuration();
            performer = new Artist(other.getPerformer());
            writer = new Artist(other.getWriter());
            year = other.getYear();
        }
    
        /**
         * gets the duration of this track
         *
         * @return the duration
         */
        public int getDuration() {
            return duration;
        }
    
        /**
         * returns the performer of this track
         *
         * @return the performer
         */
        public Artist getPerformer() {
            return performer;
        }
    
        /**
         * gets the title of this track. if the title is not known (null) "unknown title" is returned (without quotes)
         *
         * @return the title
         */
        public String getTitle() {
            if (title == null || title.isBlank()) {
                return "unknown title";
            }
            return title;
        }
    
        /**
         * returns the writer of this track
         *
         * @return the writer
         */
        public Artist getWriter() {
            return writer;
        }
    
        /**
         * gets the production year of this track
         *
         * @return the year
         */
        public int getYear() {
            return year;
        }
    
        /**
         * sets the duration
         * <p>
         * a negative value is ignored, the object remains unchanged
         *
         * @param duration the duration to set
         */
        public void setDuration(int duration) {
            if (duration < 0) {
                return;
            }
            this.duration = duration;
        }
    
        /**
         * sets the performer of this track
         * <p>
         * null arguments are ignored
         *
         * @param performer the performer to set
         */
        public void setPerformer(Artist performer) {
            if (performer == null) {
                return;
            }
            this.performer = performer;
        }
    
        /**
         * sets the title of this track.
         *
         * @param title the title to set
         */
        public void setTitle(String title) {
            this.title = title;
        }
    
        /**
         * sets the writer of this track
         * <p>
         * null arguments are ignored
         *
         * @param writer the writer to set
         */
        public void setWriter(Artist writer) {
            if (writer == null) {
                return;
            }
            this.writer = writer;
        }
    
        /**
         * sets the production year of this track
         * <p>
         * valid years are between 1900 and 2999
         * <p>
         * other values are ignored, the object remains unchanged
         *
         * @param year the year to set
         */
        public void setYear(int year) {
            if (year < 1900 || year > 2999) {
                return;
            }
            this.year = year;
        }
    
        /**
         * this getter is used to check if the writer of this Track is known.
         *
         * @return true if the writer of this track is known (and has a name), false otherwise.
         */
        public boolean writerIsKnown() {
            return isKnown(writer);
        }
    
        /**
         * Check whether an artist is known.
         *
         * @param artist the artist to check
         * @return whether the artist is known or not
         */
        protected boolean isKnown(Artist artist) {
            return artist != null && artist.getName() != null && !artist.getName().isBlank();
        }
    
        /**
         * returns a formatted String containing all information of this track.
         * <p>
         * the String representation is (without quotes):
         * <pre>
         * "title by writer performed by performer (min:sec)"
         * </pre>
         * <p>
         * where
         * <ul>
         *   <li>title stands for the title (exactly 10 chars wide) if not set, return unknown
         *   <li>writer stands for the writer name (exactly 10 chars wide, right justified)
         *   <li>performer stands for the performer name (exactly 10 chars wide, right justified)
         *   <li>min is the duration's amount of full minutes (at least two digits, leading zeros)
         *   <li>sec is the duration's remaining amount of seconds (at least two digits, leading zeros)
         * </ul>
         *
         * @return a String representation of this track
         */
        public String getString() {
            String title = ("unknown title").equals(getTitle()) ? "unknown" : getTitle();
            String writer = isKnown(getWriter()) ? getWriter().getName() : "unknown";
            String performer = isKnown(getPerformer()) ? getPerformer().getName() : "unknown";
  • §

    This changed from ES06: all string parts must be max 10 characters wide.

            title = String.format("%-10.10s", title);
            writer = String.format("%10.10s", writer);
            performer = String.format("%10.10s", performer);
    
            String minutes = String.format("%02d", (duration / 60) % 100);
            String seconds = String.format("%02d", duration % 60);
    
            return title + " by " + writer + " performed by " + performer + " (" + minutes + ":" + seconds + ")";
        }
    
        /**
         * returns a String representation of this track
         * <p>
         * the string representation of this track is described
         * in getString()
         *
         * @return the string representation
         */
        public String toString() {
            return getString();
  • §

    scan()

  • §

    The interactive edit feature now supports all five track fields.

        /**
         * Guides the user through a process that allows scanning/modifying of this track with a text-based user interface.
         * <p>
         * This method allows modification of the following fields, in the order listed:
         *
         * <ul>
         *   <li>title</li>
         *   <li>duration</li>
         *   <li>performer</li>
         *   <li>writer</li>
         *   <li>year</li>
         * </ul>
         *
         * <p>
         * For each modifiable field the process is the following:
         * <ul>
         *   <li>field name and current value are displayed</li>
         *   <li>new value is read and validated</li>
         * </ul>
         * <p>
         * if input is valid, field is set, otherwise a short message is shown and input of this field is repeated.
         * <p>
         * Old values can be kept for all fields by entering an empty string. The operation cannot be cancelled, instead
         * the user must keep all former values by repeatedly entering empty strings.
         *
         * @return whether this object was altered or not
         */
        @Override
        public boolean scan() {
            boolean changed = false;
            System.out.println("You are modifying Track#" + this.hashCode());
            System.out.println("Enter an empty value if you do not want to change a field.");
  • §

    It is important to instantiate Scanner once per scan() call, otherwise the tests fail, as cached Scanner objects might hold onto old System.in mock objects.

            Scanner consoleInput = new Scanner(System.in);
  • §

    I refactored my ConsoleScanner class a bit to contain global objects for the two most common use cases: some arbitrary string or some positive integer.

            String newTitle = ConsoleScanner.nonEmptyString.withScanner(consoleInput).scan("Title (" + getTitle() + ")");
    
            if (newTitle != null) {
                setTitle(newTitle);
                changed = true;
            }
    
            Integer newDuration = ConsoleScanner.positiveInteger.withScanner(consoleInput).scan("Duration (" + getDuration() + ")");
    
            if (newDuration != null) {
                setDuration(newDuration);
                changed = true;
            }
  • §

    Here the reason for why the ConsoleScanner class is generic: so that we can create an Artist object from the user input and let that be the result of the .scan() function.

            ConsoleScanner<Artist> artistScanner = new ConsoleScanner<>(
                    Artist::new,
                    (Artist input) -> !input.getName().isBlank(),
                    consoleInput
            );
    
            Artist newPerformer = artistScanner.scan("Performer (" + getPerformer() + ")");
    
            if (newPerformer != null) {
                setPerformer(newPerformer);
                changed = true;
            }
    
            Artist newWriter = artistScanner.scan("Writer (" + getWriter() + ")");
    
            if (newWriter != null) {
                setWriter(newWriter);
                changed = true;
            }
    
            Integer newYear = ConsoleScanner.positiveInteger.withScanner(consoleInput).scan("Year (" + getYear() + ")");
    
            if (newYear != null) {
                setYear(newYear);
                changed = true;
            }
    
            return changed;
        }
    }