2
\$\begingroup\$

I've been working on a project originally for school regarding object-oriented design, where I was supposed to write a class for a book and a bookshelf, and then have a driver class to add a couple books and print out their details. I originally started this project a while ago, adding and changing things as I've learned about new Java constructs, but just recently came back to it to "finish" it for now with file input/output, sorts and comparators and some refactoring.

Mostly, I'm just wondering if anyone would be willing to do a cursory code review; that would be awesome! Most specifically, though, I was wondering if there was a way of making the Book class more terse/elegant, especially with all the sorts and comparators. Also, were the methods broken down enough? Or are they too broken up? I've been told before methods should really only do one thing, although I've seen methods do multiple things all the time, and I've seen people be told they've broken down methods too far. Finally, did I handle the file I/O reasonably? I've never done File I/O before, and I pretty much just did it all by reading from the library's docs, so I'm not sure I used good methodology.

Book.java

import java.util.Comparator;
import java.util.ArrayList;
import java.util.Collections;

/**
 * This class contains instance data for a book that will be used in {@link Bookshelf}, including
 * the title, author, genre, and number of pages.
 *
 * @author Terry Weiss
 * @see    Bookshelf
 * @see    Librarian
 */
public class Book implements Comparable<Book> {
    private static final String DEFAULT_AUTHOR = "Unknown";
    private static final String DEFAULT_GENRE  = "General";

    private static int untitledBooks = 0; //Total count of all untitled books to uniquely name them
    private String title;
    private String author;
    private String genre;
    private int pages;


    /**
     *  Creates a new Book with everything specified.
     *
     *  @param title  The title of the Book
     *  @param pages  The number of pages in the Book
     *  @param author The author of the Book
     *  @param genre  The Book's genre
     */
    public Book(String title, int pages, String author, String genre) {
        setTitle(title);
        setPages(pages);
        setAuthor(author);
        setGenre(genre);
    }

    /**
     * Creates a new Book using the default genre name.
     *
     * @param title  The title of the Book
     * @param pages  The number of pages in the Book
     * @param author The author of the Book
     */
    Book(String title, int pages, String author) {
        setTitle(title);
        setPages(pages);
        setAuthor(author);
        setGenre(DEFAULT_GENRE);
    }

    /**
     * Creates a new Book using the default author and genre names.
     *
     * @param title The title of the Book
     * @param pages The number of pages in the Book
     */
    Book(String title, int pages) {
        setTitle(title);
        setPages(pages);
        setAuthor(DEFAULT_AUTHOR);
        setGenre(DEFAULT_GENRE);
    }


    /*
     * Internally sets the Book's title in Title Case. If no title is specified, the title will be
     * uniquely named by using "Untitled #<next_book_number>" format.
     *
     * @param title New title to be used
     * @return Book's new title name
     * // @see #titleCase
     */
    private String setTitle(String title) {
        if (title.length() == 0) {
            untitledBooks++;
            title = "Untitled #" + untitledBooks;
        }
        else {
            title = titleCase(title);
        }

        return (this.title = title);
    }

    /*
     * Sets the book's number of pages.
     *
     * @param pages Number of pages in the book
     * @return New number of pages
     */
    private int setPages(int pages) {
        if (pages < 1) {
            pages = 1;
        }

        return (this.pages = pages);
    }

    /*
     * Sets the book's author in Capital Case.
     *
     * @param author Name of the book's author
     * @return New author's name
     */
    private String setAuthor(String author) {
        String fullName = "";
        String[] names = author.split(" ");

        for (int currentName = 0; currentName < names.length - 1; currentName++) {
            fullName += capitalize(names[currentName]) + " ";
        }

        fullName += capitalize(names[names.length - 1]);

        return (this.author = fullName);
    }

    /*
     * Sets the book's genre in Capital Case.
     *
     * @param genre Name of the book's genre
     * @return New genre
     */
    private String setGenre(String genre) {
        if (genre.length() == 0) {
            genre = DEFAULT_GENRE;
        }
        else {
            genre = capitalize(genre);
        }

        return (this.genre = genre);
    }


    /**
     * Provides the Book's title.
     *
     * @return Book's title
     */
    public String getTitle() {
        return title;
    }

    /**
     * Provides the number of pages in the book.
     *
     * @return Number of pages in the Book
     */
    public int getPages() {
        return pages;
    }

    /**
     * Provides the Book's author.
     *
     * @return Book's author
     */
    public String getAuthor() {
        return author;
    }

    /**
     * Provides the Book's genre.
     *
     * @return Book's genre
     */
    public String getGenre() {
        return genre;
    }


    /**
     * Provides a table of the Book's details.
     *
     * @return Book's Title, Author, Genre and number of Pages
     */
    public String displayDetails() {
        String details = "Title:  " + title  + "\n"
                       + "Author: " + author + "\n"
                       + "Genre:  " + genre  + "\n"
                       + "Pages:  " + pages;

        return details;
    }


    /*
     *  Converts a string into Title Case. Certain words aren't capitalized per
     *  Chicago Manual of Style.
     *
     *  @param str Title to be converted
     *  @return Title converted to Title Case
     *  @see #capitalize
     *  @see #isTitleWord
     */
    private static String titleCase(String str) {
        String[] words = str.split(" ");
        String ret;
        int currentWord;

        //Always capitalize first and last words, with other words in Title Case as needed
        ret = capitalize(words[0]) + " ";

        for (currentWord = 1; currentWord < (words.length - 1); currentWord++) {
            String word = words[currentWord];

            if (isTitleWord(word)) {
                ret += capitalize(word) + " ";
            }
            else {
                ret += word.toLowerCase() + " ";
            }
        }

        if (words.length > 1) {
            ret += capitalize(words[currentWord]);
        }

        return ret.trim();
    }

    /*
     * Converts every word into Capital Case.
     *
     * @param word Word to be converted to Capital Case
     * @return Capitalized word
     */
    private static String capitalize(String word) {
        return (word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase());
    }

    /*
     * Determines if a word should be capitalized when it's in a title,
     * per Chicago Manual of Style.
     *
     * @param word Word to be checked
     * @return <tt>true</tt> if it should be capitalized
     */
    private static boolean isTitleWord(String word) {
        boolean isCapitalized;
        word = word.toLowerCase();

        //All articles, coordinate conjunctions, and prepositions are lower case in titles,
        //per Chicago Manual of Style
        switch (word) {
            //Articles:
            case "a":       case "an":      case "the":
            //Coordinate conjunctions:
            case "and":     case "but":     case "for":     case "nor":     case "or":
            case "so":      case "yet":
            //Most common single-word English prepositions:
            case "at":      case "by":      case "in":      case "into":    case "near":
            case "of":      case "on":      case "to":      case "than":    case "via":
                isCapitalized = false;
                break;

            default:
                isCapitalized = true;
        }

        return isCapitalized;
    }


    /**
     *  Represents the class as a String, currently using the book's Title.
     *
     *  @return Book's title
     */
    @Override
    public String toString() {
        return getTitle();
    }

    /**
     *  Allows Book objects to be sorted by their size based on the number of pages.
     *
     *  @param book2 The second Book object being compared against
     *  @return The number of pages more than the book being compared against
     */
    @Override
    public int compareTo(Book book2) {
        return (this.getPages() - book2.getPages());
    }

    /**
     *  Rule to compare books by their titles in ascending order (case insensitive, ignoring a 
     *  leading "the").
     */
    public static Comparator<Book> sortByTitle = (Book book1, Book book2) -> {
        String bookTitle1 = book1.getTitle().toLowerCase();
        if (bookTitle1.startsWith("the ")) {
            bookTitle1 = bookTitle1.substring(4, bookTitle1.length());
        }

        String bookTitle2 = book2.getTitle().toLowerCase();
        if (bookTitle2.startsWith("the ")) {
            bookTitle2 = bookTitle2.substring(4, bookTitle2.length());
        }

        return bookTitle1.compareTo(bookTitle2);
    };

    /**
     *  Rule to compare books by their authors in ascending order (case insensitive).
     *  Sort is by last names first, followed by first name, etc.
     */
    public static Comparator<Book> sortByAuthor = (Book book1, Book book2) -> {
        String bookAuthor1 = book1.getAuthor().toLowerCase();
        String bookAuthor2 = book2.getAuthor().toLowerCase();

        // Sort by last name first
        String[] author1 = bookAuthor1.split(" ");
        bookAuthor1 = author1[author1.length - 1];
        for (int i = 0; i < author1.length - 2; i++) {
            bookAuthor1 += author1[i] + " ";
        }

        String[] author2 = bookAuthor2.split(" ");
        bookAuthor2 = author2[author2.length - 1];
        for (int i = 0; i < author2.length - 2; i++) {
            bookAuthor2 += author2[i] + " ";
        }

        return bookAuthor1.compareTo(bookAuthor2);
    };

    /**
     *  Rule to compare books by their genre in ascending order (case insensitive).
     */
    public static Comparator<Book> sortByGenre = (Book book1, Book book2) -> {
        String bookGenre1 = book1.getGenre().toLowerCase();
        String bookGenre2 = book2.getGenre().toLowerCase();

        return bookGenre1.compareTo(bookGenre2);
    };
}

Bookshelf.java

import java.util.ArrayList;
import java.util.Collections;


/**
 * Contains instance data for a bookshelf that holds {@link Book}s. It allows the User to store a
 * Book, browse the list of Books, view the details of a particular Book and take a Book off
 * the Bookshelf.
 *
 * @author Terry Weiss
 * @see    Book
 * @see    Librarian
 */
public class Bookshelf {
    private final String DEFAULT_NAME = "Bookshelf";

    /*
     *  Holds all {@link Book}s, genres and authors. Genres and authors are saved here to be
     *  quickly referenced when browsing by category.
     */
    private ArrayList<Book>   bookList   = new ArrayList<>();
    private ArrayList<String> genreList  = new ArrayList<>();
    private ArrayList<String> authorList = new ArrayList<>();
    private final String name;


    /**
     * Constructs a new Bookshelf instance using the default name
     */
    public Bookshelf() {
        name = DEFAULT_NAME;
    }

    /**
     * Constructs a new Bookshelf instance using the name provided
     *
     * @param name Name of the bookshelf
     */
    public Bookshelf(String name) {
        this.name = name;
    }


    /**
     * Provides current number of {@link Book}s on the shelf
     *
     * @return Number of Books
     */
    public int getNumberOfBooks() {
        return bookList.size();
    }

    /**
     * Provides the name of the bookshelf.
     *
     * @return Bookshelf's name
     */
    public String getName() {
        return name;
    }


    /**
     * Adds a pre-created {@link Book} to the Bookshelf.
     *
     * @param book Book object to be added
     * @return Book object that was added
     */
    public Book addBook(Book book) {
        bookList.add(book);

        String genre = book.getGenre();
        if (!genreList.contains(genre)) {
            genreList.add(genre);
        }

        String author = book.getAuthor();
        if (!authorList.contains(author)) {
            authorList.add(author);
        }

        return book;
    }

    /**
     * Removes a specific {@link Book} object from the Bookshelf.
     *
     * @param book Book instance to be removed
     * @return <tt>true</tt> if the Book was successfully removed
     */
    public boolean removeBookBook book) {
        // If it's the only book of that genre or author, remove the genre/author from its list
        String genre = book.getGenre();
        if (numBooksWithGenre(genre) == 1) {
            genreList.remove(genre);
        }

        String author = book.getAuthor();
        if (numBooksWithAuthor(author) == 1) {
            authorList.remove(author);
        }

        return bookList.remove(book);
    }

    /**
     * Removes {@link Book} from the Bookshelf. Replaced by
     * {@link #removeBook(java.lang.String)}. Only use if the book being removed must be saved,
     * or if the Book's title isn't unique, since the positions change with each sort.
     *
     * @param position Position of the Book in the list
     * @return Book instance being removed
     */
    public Book removeBook(int position) {
        String genre = bookList.get(position).getGenre();
        if (numBooksWithGenre(genre) == 1) {
            genreList.remove(genre);
        }

        String author = bookList.get(position).getAuthor();
        if (numBooksWithGenre(genre) == 1) {
            authorList.remove(author);
        }

        return bookList.remove(position);
    }

    /**
     * Removes {@link Book} from the Bookshelf using its title.
     *
     * @param title Title of the Book to be removed
     * @return <tt>true</tt> if the Book was successfully removed
     */
    public boolean removeBook(String title) {
        if (numBooksWithTitle(title) == 1) {
            removeBook(findBook(title));
            return true;
        }
        else {
            return false;
        }
    }


    /**
     * Provides details of a specific {@link Book} from the Bookshelf, or if the Book doesn't
     * exist. Only use in cases where you want only one version of a book that has a non-unique
     * title, since the positions change with each sort.
     *
     * @param position Position of Book to be viewed
     * @deprecated Replaced by {@link #viewBook(java.lang.String)}
     * @return String holding the details of the book if it exists
     */
    public String viewBook(int position) {
        String ret;

        if (position < 0 || position >= bookList.size()) {
            ret = "That book isn't on the shelf.";
        }
        else {
            ret = bookList.get(position).displayDetails();
        }

        return ret;
    }

    /**
     * Provides details of a specific {@link Book} from the Bookshelf, or if the Book doesn't
     * exist. In cases where the title isn't unique, all Books by this title are provided and
     * separated by an empty line.
     *
     * @param title Title of the book (case insensitive)
     * @return String holding the details of the Books by that title
     */
    public String viewBook(String title) {
        String details = "";

        for (Book currentBook : bookList) {
            if (currentBook.getTitle().equalsIgnoreCase(title)) {
                //Separate each entry by a blank line in cases of duplicate titles
                if (details.length() > 0) {
                    details += "\n\n";
                }

                details += currentBook.displayDetails();
            }
        }

        if (details.length() == 0) {
            details = title + " isn't on the shelf.";
        }

        return details;
    }


    /**
     * Sorts all {@link Book}s in ascending alphabetical order by title.
     *
     * @see Book#sortByTitle
     */
    public void sortBooksByTitle() {
        Collections.sort(bookList, Book.sortByTitle);
    }

    /**
     * Sorts all {@link Book}s in ascending alphabetical by Author first and then by Title.
     *
     * @see Book#sortByAuthor
     */
    public void sortBooksByAuthor() {
        //Sorts must be done in reverse order to get benefit of sorting by Title while not
        //overriding the sort by author.
        sortBooksByTitle();
        Collections.sort(bookList, Book.sortByAuthor);
    }

    /**
     * Sorts all {@link Book}s in ascending alphabetical by Genre first and then by Title.
     *
     * @see Book#sortByGenre
     */
    public void sortBooksByGenre() {
        sortBooksByTitle();
        Collections.sort(bookList, Book.sortByGenre);
    }

    /**
     * Sorts all {@link Book}s in ascending numerical order by size first and then by Title
     * in cases where two books are the same size.
     */
    public void sortBooksBySize() {
        sortBooksByTitle();
        Collections.sort(bookList);
    }


    /**
     * Returns the index of the first occurrence of the {@link Book} in this list, or -1 if
     * this list does not contain the Book.
     *
     * @param book Book object to search for
     * @return Index of the first Book, or <tt>-1</tt> if it's not there
     */
    public int findBook(Book book) {
        return bookList.indexOf(book);
    }

    /**
     * Returns the index of the first occurrence of the {@link Book} with the same title in this
     * list, or -1 if this list does not contain the Book.
     *
     * @param title Title of the book to search for
     * @return Index of the first Book, or <tt>-1</tt> if it's not there
     */
    public int findBook(String title) {
        for (int currentBook = 0; currentBook < bookList.size(); currentBook++) {
            String currentTitle = bookList.get(currentBook).getTitle();
            if (currentTitle.equalsIgnoreCase(title)) {
                return currentBook;
            }
        }

        return -1;
    }


    /**
     * Provides the number of {@link Book}s of a specified genre.
     *
     * @param genre Name of the genre
     * @return Number of Books of the provided genre
     */
    public int numBooksWithGenre(String genre) {
        int books = 0;

        for (Book book : bookList) {
            if (book.getGenre().equalsIgnoreCase(genre)) {
                books++;
            }
        }

        return books;
    }

    /**
     * Provides the number of {@link Book}s written by a specified author.
     *
     * @param author Name of the author
     * @return Number of Books by the provided author
     */
    public int numBooksWithAuthor(String author) {
        int books = 0;

        for (Book book : bookList) {
            if (book.getAuthor().equalsIgnoreCase(author)) {
                books++;
            }
        }

        return books;
    }

    /**
     * Provides the number of {@link Book}s with a specified title.
     *
     * @param title Title to be searched for
     * @return Number of Books of the same title
     */
    public int numBooksWithTitle(String title) {
        int books = 0;

        for (Book book : bookList) {
            if (book.getTitle().equalsIgnoreCase(title)) {
                books++;
            }
        }

        return books;
    }


    /**
     * Provides a String of all {@link Book}s on the Bookshelf using their String
     * representation.
     *
     * @return List of all Books on the Bookshelf
     */
    public String browse() {
        String list = "";

        for (Book currentBook : bookList) {
            list += currentBook + "\n";
        }

        return list;
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf using their String
     * representation. Books are resorted by title.
     *
     * @return List of all Books on the Bookshelf, sorted by title
     * @see #sortBooksByTitle
     */
    public String browseByTitle() {
        sortBooksByTitle();
        return browse();
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf using their String
     * representation and author's full name. Books are resorted by author's last name first,
     * followed by first name, etc. Books are then sorted by title.
     *
     * @return List of all Books on the Bookshelf, sorted by author
     * @see #sortBooksByAuthor
     */
    public String browseByAuthor() {
        sortBooksByAuthor();
        String list = "";

        for (Book currentBook : bookList) {
            list += String.format("%-20s    %s\n", currentBook, currentBook.getAuthor());
        }

        return list;
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf using their String
     * representation and genre. Books are resorted by genre, then title.
     *
     * @return List of all Books on the Bookshelf, sorted by genre
     * @see #sortBooksByGenre
     */
    public String browseByGenre() {
        sortBooksByGenre();
        String list = "";

        for (Book currentBook : bookList) {
            list += String.format("%-20s    %s\n", currentBook, currentBook.getGenre());
        }

        return list;
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf using their String
     * representation and number of pages. Books are resorted by size, then title.
     *
     * @return List of all Books on the Bookshelf, sorted by size
     * @see #sortBooksBySize
     */
    public String browseBySize() {
        sortBooksBySize();
        String list = "";

        for (Book currentBook : bookList) {
            list += String.format("%-20s    %s\n", currentBook, currentBook.getPages());
        }

        return list;
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf in a specific genre, or if
     * no books in that genre exist. Books are sorted by title.
     *
     * @param genre Genre to be listed
     * @return List of Books in the given genre
     */
    public String browseBooksOfGenre(String genre) {
        String list = "";

        // Make the genre Capitalized to match format of genres and be case-insensitive
        genre = genre.substring(0, 1).toUpperCase() + genre.substring(1).toLowerCase();

        if (genreList.indexOf(genre) == -1) {
            list = "There are no books of that genre.\n";
        }
        else {
            sortBooksByGenre();
            int i = 0;

            // Find first book with the specified genre
            while (!bookList.get(i).getGenre().equalsIgnoreCase(genre)) {
                i++;
            }

            // Display all books until the genre changes or until it's the last book of the shelf
            while (i < bookList.size() && bookList.get(i).getGenre().equalsIgnoreCase(genre)) {
                list += bookList.get(i) + "\n";
                i++;
            }
        }

        return list;
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf by a specific author, or if
     * no books by that author exist. Books are sorted by title.
     *
     * @param author Author to be listed
     * @return List of Books by the given author
     */
    public String browseBooksOfAuthor(String author) {
        String list = "";

        // Make the author's name Capitalized to match format of genres and be case-insensitive
        String capitalizedAuthor = "";
        for (String name : author.split(" ")) {
            capitalizedAuthor += name.substring(0, 1).toUpperCase()
                                    + name.substring(1).toLowerCase() + " ";
        }
        capitalizedAuthor = capitalizedAuthor.trim();

        if (authorList.indexOf(capitalizedAuthor) == -1) {
            list = "There are no books by that author.\n";
        }
        else {
            sortBooksByAuthor();
            int i = 0;

            // Find first book by the author
            while (!bookList.get(i).getAuthor().equalsIgnoreCase(capitalizedAuthor)) {
                i++;
            }

            // Display all books until the author changes or until it's the last book of the shelf
            while (i < bookList.size() && bookList.get(i).getAuthor()
                                                            .equalsIgnoreCase(capitalizedAuthor)) {
                list += bookList.get(i) + "\n";
                i++;
            }
        }

        return list;
    }

    /**
     * Provides a String of all {@link Book}s on the Bookshelf in a specific category, or if
     * no books in that category exist. Books are sorted by title. Currently, the only supported
     * categories are <tt>"author"</tt> and <tt>"genre"</tt>.
     *
     * @param category Category to be used
     * @return List of Books in the given category
     */
    public String browseByCategory(String category) {
        String list = "";

        if (category.equalsIgnoreCase("author") || category.equalsIgnoreCase("authors")) {
            for (String author : authorList) {
                list += author + ":\n"
                      + browseBooksOfAuthor(author) + "\n";
            }
        }
        else if (category.equalsIgnoreCase("genre") || category.equalsIgnoreCase("genres")) {
            for (String genre : genreList) {
                list += genre + ":\n"
                      + browseBooksOfGenre(genre) + "\n";
            }
        }
        else {
            list = "No such category.\n";
        }

        return list;
    }


    /**
     * String representation of the Bookshelf
     *
     * @return Bookshelf's name
     */
    @Override
    public String toString() {
        return getName();
    }
}

Librarian.java

public class Librarian {
    /*
     * Definitions of all menu options for {@link #mainMenu}
     */
    private static enum Main_Options {
        ADD_BOOK, REMOVE_BOOK, BROWSE, EMPTY, SAVE, LOAD, QUIT;

        /**
         * Converts an int menu option to a Main_Options option. Since menus
         * start at 1, all options are returned as 1 less to start at 0.
         *
         * @param option Integer option to be converted
         * @return Converted value of Main_Options
         */
        public static Main_Options fromInteger( int option ) {
            return Main_Options.values()[option - 1];
        }
    }

    /*
     * Definitions of all menu options for {@link #browseMenu}
     */
    private static enum Browse_Options {
        BROWSE_ALL, BROWSE_AUTHORS, BROWSE_GENRES, BROWSE_SIZE,
        CATEGORIZE_AUTHORS, CATEGORIZE_GENRES, VIEW_BY_AUTHOR, VIEW_BY_GENRE,
        VIEW_BOOK_DETAILS, MAIN_MENU;

        /**
         * Converts an int menu option to a Browse_Options option. Since menus
         * start at 1, all options are returned as 1 less to start at 0.
         *
         * @param option Integer option to be converted
         * @return Converted value of Browse_Options
         */
        public static Browse_Options fromInteger( int option ) {
            return Browse_Options.values()[option - 1];
        }

    }


    /*
     * {@link pp5_7.Bookshelf} instance that holds each {@link pp5_7.Book}
     */
    private static Bookshelf shelf = new Bookshelf();


    /*
     * Creates the main menu prompt message and prompts the user for which
     * option to proceed.
     *
     * @return Menu option selected
     * @see pp5_7.Prompt
     */
    private static int mainMenu() {
        String prompt = "\n"
                + "\t1. Add a book\n"
                + "\t2. Remove a book\n"
                + "\t3. Browse\n"
                + "\t4. Empty bookshelf\n"
                + "\t5. Save bookshelf\n"
                + "\t6. Load bookshelf\n"
                + "\t7. Quit\n"
                + "Please select an option";

        return Prompt.intInRange(prompt, 1, Main_Options.QUIT.ordinal() + 1,
                                    "Not a valid option.");
    }

    /*
     * Prompts the user for a {@link pp5_7.Book}'s details, creates the new
     * Book and adds it to the {@link pp5_7.Bookshelf}.
     *
     * @see pp5_7.Bookshelf#addBook
     */
    private static void addBook() {
        String title = Prompt.string("Enter title");
        String author = Prompt.string("Enter author");
        int pages = Prompt.wholeNumber("Enter number of pages");
        String genre = Prompt.string("Enter genre");

        Book book = new Book(title, pages, author, genre);
        shelf.addBook(book);
        System.out.println("Added " + book + " to the shelf.");
    }

    /*
     * Removes a book from the {@link pp5_7.Bookshelf}, or displays a message
     * that the book doesn't exist.
     */
    private static void removeBook() {
        String title = Prompt.string("Enter title");

        if (shelf.removeBook(title)) {
            System.out.println("Removed " + title + " from the shelf.");
        }
        else {
            System.out.println("That book doesn't exist.");
        }
    }

    /*
     * Empties all books from the {@link pp5_7.Bookshelf}.
     */
    private static void emptyBookshelf() {
        String confirmation = Prompt.string("Are you sure you want to empty "
                + "the bookshelf?");
        if (!confirmation.equalsIgnoreCase("y")
                && !confirmation.equalsIgnoreCase("yes"))
        {
            System.out.println("Leaving bookshelf alone.");
        }
        else if (shelf.getNumberOfBooks() == 0) {
            System.out.println("Bookshelf was already empty.");
        }
        else {
            shelf.emptyBookshelf();
            System.out.println("All books have been removed.");
        }
    }

    /*
     * Saves the current {@link pp5_7.Bookshelf} to a file named after it. If
     * the file already exists, the user is prompted to confirm overwriting
     * the file.
     *
     * @throws IOException
     */
    private static void saveBookshelf() throws IOException {
        String filename = shelf.toString().toLowerCase() + ".bl";
        File savefile = new File(filename);
        boolean confirms = true;

        if (savefile.exists()) {
            String confirmation = Prompt.string("Are you sure you want to "
                    + "overwrite this saved bookshelf?");

            confirms = (confirmation.equalsIgnoreCase("y")
                            || confirmation.equalsIgnoreCase("yes"));
        }
        else {
            savefile.createNewFile();
        }

        if (confirms
                && BookshelfFilestore.save(filename, shelf.getBookList()))
        {
            System.out.println("Successfully saved " + shelf + " to "
                                + filename + ".");
        }
        else {
            System.out.println(shelf + " wasn't saved.");
        }
    }

    /*
     * Loads the {@link pp5_7.Bookshelf} from the specified file. If the
     * Bookshelf already has {@link pp5_7.Book}s in it, the User is prompted
     * to confirm overwriting the current working Bookshelf. The current
     * Bookshelf will be replaced by a new Bookshelf named after the file.
     *
     * @throws FileNotFoundException
     */
    private static void loadBookshelf() throws FileNotFoundException {
        String filename = Prompt.string("Which file would you like to load?");
        File savefile = new File(filename);

        if (!savefile.exists()) {
            System.out.println("That file doesn't exist.");
        }
        else {
            boolean confirms = true;
            List<Book> booklist;

            if (shelf.getNumberOfBooks() != 0) {
                String confirmation = Prompt.string("Are you sure you want to"
                                        + "overwrite the current bookshelf?");

                confirms = (confirmation.equalsIgnoreCase("y")
                               || confirmation.equalsIgnoreCase("yes"));
            }

            if (!confirms) {
                System.out.println("New bookshelf not loaded.");
            }
            else {
                shelf.emptyBookshelf();
                booklist = BookshelfFilestore.load(filename);
                shelf = new Bookshelf(booklist,
                                        getShelfnameFromFilename(filename));
                System.out.println("Successfully loaded " + shelf + ".");
            }
        }
    }

    private static String getShelfnameFromFilename( String filename ) {
        String name;

        if (filename.contains(".")) {
            name = filename.substring(0, filename.lastIndexOf("."));
        }
        else {
            name = filename;
        }

        name = name.substring(0, 1).toUpperCase()
                    + name.substring(1, name.length()).toLowerCase();

        return name;
    }


    /*
     * Creates the browse menu prompt message and prompts the user for which
     * option to proceed.
     *
     * @return Menu option selected
     * @see pp5_7.Prompt
     */
    private static int browseMenu() {
        String prompt = "\n"
                + "\t1.  Browse all books\n"
                + "\t2.  Browse by authors\n"
                + "\t3.  Browse by genres\n"
                + "\t4.  Browse by size\n"
                + "\t5.  Categorize by author\n"
                + "\t6.  Categorize by genre\n"
                + "\t7.  View books by a certain author\n"
                + "\t8.  View books in a certain genre\n"
                + "\t9.  View book details\n"
                + "\t10. Return to main menu\n"
                + "Please select an option";

        return Prompt.intInRange(prompt, 1,
                                    Browse_Options.MAIN_MENU.ordinal() + 1,
                                    "Not a valid option.");
    }

    /*
     * Displays all {@link pp5_7.Book}s on the {@link pp5_7.Bookshelf}, sorted
     * by their title.
     *
     * @see pp5_7.Bookshelf#browse
     */
    private static void browseAll() {
        System.out.println("\nAll books in " + shelf + ":");
        System.out.print(shelf.browse());
    }

    /*
     * Displays all {@link pp5_7.Book}s on the {@link pp5_7.Bookshelf}, sorted
     * by their author.
     *
     * @see pp5_7.Bookshelf#browseBooksByAuthor
     */
    private static void browseAuthors() {
        System.out.println("\nAll books in " + shelf + ":");
        System.out.print(shelf.browseByAuthor());
    }

    /*
     * Displays all {@link pp5_7.Book}s on the {@link pp5_7.Bookshelf}, sorted
     * by their genre.
     *
     * @see pp5_7.Bookshelf#browseBooksByGenre
     */
    private static void browseGenres() {
        System.out.println("\nAll books in " + shelf + ":");
        System.out.print(shelf.browseByGenre());
    }

    /*
     * Displays all {@link pp5_7.Book}s on the {@link pp5_7.Bookshelf}, sorted
     * by their size.
     *
     * @see pp5_7.Bookshelf#browseBySize
     */
    private static void browseSize() {
        System.out.println("\nAll books in " + shelf + ":");
        System.out.print(shelf.browseBySize());
    }

    /*
     * Displays all {@link pp5_7.Book}s on the {@link pp5_7.Bookshelf},
     * categorized and sorted by their author.
     *
     * @see pp5_7.Bookshelf#browseByCategory
     */
    private static void browseCategorizeAuthors() {
        System.out.println("\nAll books in " + shelf + ":");
        System.out.print(shelf.browseByCategory("author"));
    }

    /*
     * Displays all {@link pp5_7.Book}s on the {@link pp5_7.Bookshelf},
     * categorized and sorted by their genre.
     *
     * @see pp5_7.Bookshelf#browseByCategory
     */
    private static void browseCategorizeGenres() {
        System.out.println("\nAll books in " + shelf + ":");
        System.out.print(shelf.browseByCategory("genre"));
    }

    /*
     * Prompts for an author and displays all {@link pp5_7.Book}s on the
     * {@link pp5_7.Bookshelf} written by that author, sorted by their title.
     *
     * @see pp5_7.Bookshelf#browseBooksOfAuthor
     */
    private static void browseBooksByAuthor() {
        String author = Prompt.string("Pick an author");

        System.out.println("\nAll books by " + author + ":");
        System.out.print(shelf.browseBooksOfAuthor(author));
    }

    /*
     * Prompts for a genre and displays all {@link pp5_7.Book}s on the
     * {@link pp5_7.Bookshelf} that are in that genre, sorted by their title.
     *
     * @see pp5_7.Bookshelf#browseBooksOfGenre
     */
    private static void browseBooksByGenre() {
        String genre = Prompt.string("Pick a genre");

        System.out.println("\nAll " + genre + " books:");
        System.out.print(shelf.browseBooksOfGenre(genre));
    }

    /*
     * Prompts for a title and displays the details of that {@link pp5_7.Book}
     *
     * @see pp5_7.Bookshelf#viewBook
     */
    private static void browseViewDetails() {
        String title = Prompt.string("Enter a title");
        System.out.println(shelf.viewBook(title));
    }


    /**
     * Runs the menus to allow the User to store a book, browse the list of
     * books by various categories, view the details of a particular book, and
     * take a book off the shelf. The User may also load or save a bookshelf
     * for later use.
     *
     * @param args Command line input isn't currently supported
     * @throws pp5_7.Prompt.OptionOutOfBoundsException In case a menu choice
     * isn't valid
     * @throws IOException
     * @see bookshelf.Book
     * @see bookshelf.Bookshelf
     */
    public static void main( String args[] )
            throws IOException, Prompt.OptionOutOfBoundsException
    {
        int mainChoice, browseChoice;

        do {
            mainChoice = mainMenu();

            switch (Main_Options.fromInteger(mainChoice)) {
                case ADD_BOOK:
                    addBook();
                    break;
                case REMOVE_BOOK:
                    removeBook();
                    break;
                case BROWSE:
                    do {
                        browseChoice = browseMenu();

                        switch (Browse_Options.fromInteger(browseChoice)) {
                            case BROWSE_ALL:
                                browseAll();
                                break;
                            case BROWSE_AUTHORS:
                                browseAuthors();
                                break;
                            case BROWSE_GENRES:
                                browseGenres();
                                break;
                            case BROWSE_SIZE:
                                browseSize();
                                break;
                            case CATEGORIZE_AUTHORS:
                                browseCategorizeAuthors();
                                break;
                            case CATEGORIZE_GENRES:
                                browseCategorizeGenres();
                                break;
                            case VIEW_BY_AUTHOR:
                                browseBooksByAuthor();
                                break;
                            case VIEW_BY_GENRE:
                                browseBooksByGenre();
                                break;
                            case VIEW_BOOK_DETAILS:
                                browseViewDetails();
                                break;
                            case MAIN_MENU:
                                System.out.println("Returning to the "
                                                    + "main menu.");
                                break;
                            default:
                                throw new Prompt.OptionOutOfBoundsException(
                                                    "Browse menu option isn't"
                                                            + " supported");
                        }
                    } while (browseChoice != Browse_Options.values().length);
                    break;
                case EMPTY:
                    emptyBookshelf();
                    break;
                case SAVE:
                    saveBookshelf();
                    break;
                case LOAD:
                    loadBookshelf();
                    break;
                case QUIT:
                    System.out.println("Please come again!");
                    break;
                default:
                    throw new Prompt.OptionOutOfBoundsException("Main menu "
                                                  + "option isn't supported");
            }
        } while (mainChoice != Main_Options.values().length);
    }
}

BookshelfFilestore.java

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;


/**
 * This class loads and saves all instance data for a list of Books that will
 * be used in {@link bookshelf.Bookshelf}.
 *
 * @author Terry Weiss
 * @see bookshelf.Book
 * @see bookshelf.Bookshelf
 */
public class BookshelfFilestore {
    private static final String DEFAULT_SHELF_FILENAME = "bookshelf.bl";


    /**
     * Checks if a file by this filename exists.
     *
     * @param filename Relative path of file to be checked
     * @return <tt>true</tt> if a file by this filename exists
     * @see File#exists()
     */
    public static boolean checkFileExists(String filename) {
        return (new File(filename).exists());
    }


    /**
     * Writes all {@link bookshelf.Book}s to the specified file. The specified
     * file must already exist.
     *
     * @param filename Relative path of file to be saved to
     * @param shelf List of Books to be saved
     * @return <tt>true</tt> if any books were saved. <tt>false</tt> if the
     * file doesn't exist.
     * @throws IOException
     */
    public static boolean save(String filename, List<Book> shelf)
            throws IOException
    {
        File file = new File(filename);

        if (shelf.isEmpty() || !file.exists()) {
            return false;
        }
        else {
            PrintWriter writer = new PrintWriter(new FileWriter(file));

            for (Book book : shelf) {
                writer.write(book.getTitle() + "\n");
                writer.write(book.getPages() + "\n");
                writer.write(book.getAuthor() + "\n");
                writer.write(book.getGenre() + "\n");
                writer.write("\n");
                writer.flush();
            }

            writer.close();
            return true;
        }
    }

    /**
     * Writes all {@link bookshelf.Book}s to the specified file.
     *
     * @param shelf List of Books to be saved
     * @return <tt>true</tt> if any books were saved
     * @throws IOException
     */
    public static boolean save(List<Book> shelf) throws IOException {
        return save(DEFAULT_SHELF_FILENAME, shelf);
    }


    /**
     * Retrieves all the {@link bookshelf.Book}s from the specified file.
     *
     * @param filename Relative path of file to be read
     * @return List of Books loaded
     * @throws FileNotFoundException
     */
    public static List<Book> load(String filename)
            throws FileNotFoundException
    {
        List<Book> shelf = new ArrayList<>();
        File shelfFile = new File(filename);

        if (shelfFile.exists()) {
            Scanner fileReader = new Scanner(shelfFile);
            fileReader.useDelimiter("\n\n");

            while (fileReader.hasNext()) {
                String bookStr = fileReader.next();
                Scanner bookReader = new Scanner(bookStr);
                String title, author, genre;
                int pages;

                // For each book in the file's list
                for (int i = 0; bookReader.hasNext(); i++) {
                    title  = bookReader.nextLine();
                    pages  = Integer.parseInt(bookReader.nextLine());
                    author = bookReader.nextLine();
                    genre  = bookReader.nextLine();

                    shelf.add(new Book(title, pages, author, genre));
                }
            }
        }

        return shelf;
    }

    /**
     * Retrieves all the {@link bookshelf.Book}s from the default file.
     *
     * @return List of Books loaded
     * @throws FileNotFoundException
     */
    public static List<Book> load() throws FileNotFoundException {
        return load(DEFAULT_SHELF_FILENAME);
    }
}
\$\endgroup\$
4
\$\begingroup\$

Remove unnecessary imports

import java.util.ArrayList;

It won't hurt anything really, but it doesn't look like you need ArrayList in Book.

Be consistent

        //All articles, coordinate conjunctions, and prepositions are lower case in titles,
        //per Chicago Manual of Style

Not a coding issue, but I'm pretty sure that "[a]ll" is the wrong word here. It is inconsistent with

        //Always capitalize first and last words, with other words in Title Case as needed

That matches the rules as a I learned them.

Prefer interfaces as variable types

    private ArrayList<Book>   bookList   = new ArrayList<>();

As a general rule, unless you are using implementation specific functionality, it is better to make the variable type the interface rather than the implementation.

    private List<Book>   books   = new ArrayList<>();

Now you can change the implementation without having to change the variable type.

I also changed the name. I find names that do not contain the type to read more naturally. This is of course a stylistic opinion, so you are free to differ.

Consider other data types

    private ArrayList<String> genreList  = new ArrayList<>();
    private ArrayList<String> authorList = new ArrayList<>();

Consider making these into Set variables instead.

    private Set<String> genres  = new HashSet<>();
    private Set<String> authors = new HashSet<>();

Since you only have one entry per name, a Set would naturally enforce a restriction that you are doing manually now. So instead of

        String genre = book.getGenre();
        if (!genreList.contains(genre)) {
            genreList.add(genre);
        }

You could just say

        genres.add(book.getGenre());

You don't have to use a HashSet. Any Set will do for this purpose.

You could also define genres as an enum. Then you wouldn't have to add or remove things at all. Of course, then genres would be static and not derived from the books on the shelf.

Methods as verbs

You have methods named like numBooksWithGenre. Why not give them verb names like countBooksWithGenre as that describes what it does rather than what it returns?

Duplicate author names

Your code doesn't handle duplicate author names. If you have two authors named Joe Smith, this code will consider them the same author.

Browsing shouldn't change the list

Asking for a read of the data should not change the list. Rather than sorting the list in place, consider making a new copy of the list each time and sorting that. Also consider returning that list rather than a String representation of the list.

Another possibility would be to use SortedSet variables for each sort. Then adding and removing books would update each sorted list and you could return a sorted list without having to manually sort the list each time.

Use a database

In the real world, you'd store this in a database rather than writing your own sorts, file store, etc. If you were using a database, you could leave a lot of the implementation details in the database.

Try with resources

If you do have to write your own file manipulation routines, you can use a try with resources to correctly manage your connection.

\$\endgroup\$
1
\$\begingroup\$

Constructor chaining

public Book(String title, int pages, String author, String genre) {
    setTitle(title);
    setPages(pages);
    setAuthor(author);
    setGenre(genre);
}

Book(String title, int pages, String author) {
    setTitle(title);
    setPages(pages);
    setAuthor(author);
    setGenre(DEFAULT_GENRE);
}

Book(String title, int pages) {
    setTitle(title);
    setPages(pages);
    setAuthor(DEFAULT_AUTHOR);
    setGenre(DEFAULT_GENRE);
}

If you are fine with making Book an immutable class (as you seem to be doing now, since setters are private), then the right way of initializing with multiple constructors is to rely on constructor chaining. This encourages optional arguments to be placed at the end (for some definition of consistency), and then passing a reasonable default value to the stricter constructor as such:

public Book(String title, int pages) {
    this(title, pages, DEFAULT_AUTHOR);
}

public Book(String title, int pages, String author) {
    this(title, pages, author, DEFAULT_GENRE);
}

public Book(String title, int pages, String author, String genre) {
    // use setters here if you have to, but direct 
    // this.X = X is easier to follow
}

Helpfully, when you have final your instance fields for the immutable class approach, the use of constructor chaining can also aid you in eliminating any unassigned fields.

Comparing Books

You can rely more on the new Comparator.comparing() approach for creating your Comparator<Book> instances. For example:

public Book(String title, int pages, String author, String genre) {
    // ...
    this.title = normalizeTitle(title); // untitled handling and titleCase()
    this.author = normalizeAuthorName(author);
    this.comparingBookTitle = this.title.toLowerCase().replaceFirst("^the", "");
    this.comparingAuthor = normalizeAuthorNameByLastName(author);
}

private String getComparingBookTitle() {
    return comparingBookTitle;
}

private String getComparingAuthor() {
    return comparingAuthor;
}

public static Comparator<Book> sortByTitle = 
                    Comparator.comparing(Book::getComparingBookTitle);

public static Comparator<Book> sortByAuthor = 
                    Comparator.comparing(Book::getComparingAuthor);

public static Comparator<Book> sortByGenre = 
                    Comparator.comparing(Book::getGenre, String.CASE_INSENSITIVE_ORDER);

Java 8

Java 8's stream-based processing can simplify some of your implementation in Bookshelf. For brevity, here's two suggestions to get you started, and they will also let you refactor some of the existing methods to use largely the same code by passing different method arguments.

public int countBooksWithGenre(String genre) {
    return (int) countBooksWith(Book::getGenre, genre);
}

private <T> long countBooksWith(Function<Book, T> property, String keyword) {
    return bookList.stream()
                    .map(property)
                    .filter(keyword::equalsIgnoreCase)
                    .count();
}
  1. map() each Book to the desired property, e.g. genre in this example.
  2. filter() each genre (in this example) against the given keyword. Also, calling keyword.equalsIgnoreCase(element) (as a method reference here) works equally well as iterating each element and calling element.equalsIgnoreCase(keyword), as you do currently.
public String browseByGenre() {
    return browseBy(Book.sortByGenre, Book::getGenre);
}

private <T> String browseBy(Comparator<Book> comparator, Function<Book, T> mapper) {
    return bookList.stream()
                    .sorted(comparator)
                    .map(v -> String.format("%-20s    %s", v, mapper.apply(v))
                    .collect(Collectors.joining("\n"));
}
  1. Get a Stream<Book> sorted() by the given Comparator.
  2. map() each stream element to the desired String representation (tip: you may want to introduce this as a method in Book).
  3. collect() each element into a String by joining() them with "\n".

File I/O and serialization

Any I/O-related resources should be accessed using try-with-resources for safe and efficient handling of the resource (example):

try (FileWriter fileWriter = new FileWriter(file); 
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter); 
        PrintWriter printWriter = new PrintWriter(bufferedWriter)) {
    // ...
} catch (IOException e) {
    // handle accordingly
}

When it comes to serializing the contents of your Book instances into a file, you can also consider using one of the many third-party libraries to write it out as JSON, XML, or CSV, so that you don't have to roll your own implementation/format.

Other observations

  • The return values from your setters seem to be unused, so you will probably want to remove them (i.e. make them private void) for brevity.
  • Use String.isEmpty() instead of String.length() == 0 to check for empty Strings.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.