// Import all required libraries. Not using .* as it is not good practice due to potential conflicts. import java.util.List; import java.util.ArrayList; import java.util.HashSet; import java.util.Scanner; import java.util.Random; import java.util.Map; import java.util.HashMap; import java.util.Date; import java.io.File; import java.io.IOException; import java.awt.FileDialog; import java.awt.Frame; import java.io.PrintWriter; /** * Class to create objects of a Library, which can hold and report on assets held within * * @author George Wilkinson * @version 3.0 */ public class Library { // private List itemList; // Initialise an ArrayList of name itemList to store Library Items // private List userList; // Initialise an ArrayList of name userList to store Library Users /* No longer needed, as we can use the keys of customerMap. private HashSet uuidSet; // Initialise a Hash Set of name uuidSet ( unique user identifier ) to store unique user IDs for efficient O(1) searching */ private Map customerMap; private Map itemsMap; private Map libraryReservationMap; private Diary diary; /* * Constructor for objects of class Library */ public Library() { // itemList = new ArrayList(); // userList = new ArrayList(); /* No longer needed, can use keys of customerMap. uuidSet = new HashSet(); */ customerMap = new HashMap(); itemsMap = new HashMap(); libraryReservationMap = new HashMap(); diary = new Diary(); } /** * Storing Objects Start */ /* * Inserts object value item alongside key of @itemCode into @itemsMap. */ private void storeItem( LibraryItem item ) { // itemList.add( item ); itemsMap.put( item.getItemCode(), item ); } /* * Inserts object value user alongside key of @userID into @customerMap. * If the userID is set to unknown, it will call @generateUserID. */ private void storeUser( LibraryUser user ) { // userList.add( user ); if ( user.getUserID().equals( "unknown" ) ) { user.setUserID( generateUserID( "AB-", 6 ) ); } customerMap.put( user.getUserID(), user ); // Store the user along with the userID. } /* * Inserts object value reservation alongside key of @reservationNo into @libraryReservationMap. */ private void storeLibraryReservation( LibraryReservation reservation ) { libraryReservationMap.put( reservation.getReservationNo(), reservation ); diary.addReservation( reservation ); } /** * Storing Objects End */ /** * Generate IDs Start */ /* * Generate a sequential reservation number. Since Maps do not have an index, we cannot simply get the last value, * and recursion would be too intensive for this application. As a placeholder for the file output, a field variable * is used to hold the last highest value for the reservation number. Unlike @generateUserID, this has a maximum value * hardcoded to the spec. This likely will not be an issue since we can still have 1,000,000 reservations. */ public String generateReservationNo() { /* * Originally, I did not check if the number was already taken, however the last part of step 4 introduced deletion. * When a reservation is deleted that does not have the highest reservation number, conflicting reservation numbers are generated. * e.g. 5 reservations, reservation 3 ID: 000003 is deleted. The next reservation number would be 000004, as the size + 1 = 4 */ String candidateNo = String.format("%06d", libraryReservationMap.size() + 1); for ( int i = 1; libraryReservationMap.containsKey( candidateNo ); i++ ) { candidateNo = String.format( "%06d", libraryReservationMap.size() + i ); } return candidateNo; } /* * Returns a random unique user ID by specifying * @prefix - arbitrary alphanumeric prefix * @length - length of numeric ID * and returning a unique user id * @uuid - a unique string starting with @prefix, and concat. with a random number * * Example: length = 6, expected result should be under 999,999 and above 99,999. * If we just use 10^(length), this would generate any number under 1,000,000. * This is an issue since any integers below 100,000 can be used, which would be incorrect. * By using the offset of a factor of 10 below the desired length we can generate between 0 and 899,999. * After this, we can add 100,000 back to the number to ensure a baseline length is maintained. * * Note: I am aware that this is overengineered, and that several random integers of amount @length could be used, * but this is considerably more efficient since there is no iteration involved in the creation of the ID. O(1) */ private String generateUserID( String prefix, int length ) { Random random = new Random(); final int offset = (int) Math.pow( 10, (length-1) ); // Static integer equal to 10^(length-1) lower than length. This will allow for the factor to be consistent. int intLength = (int) ( Math.pow( 10, length ) ) - offset ; // Integer equal to 10^(length) minus the offset. This creates a ceiling for the ID range. if ( customerMap.size() == intLength ) { System.out.println("Too many user IDs delegated for current range, increasing ID range by a factor of 10"); return generateUserID( prefix, length+1 ); /* * This is essential for eliminating a stack overflow error when the specified range is full. This would be incorrect to catch. * By incrementing the length value when the ID range is full, roughly 2.1 billion users can be created without running into an overflow error. */ } String uuid = prefix + ( random.nextInt( intLength ) + offset ); /* * Sets the uid to the prefix, concat. with a random integer of specified length * Add the offset to a random number, to create a floor to the ID range. */ if ( customerMap.containsKey( uuid ) ) // Previously uuidSet.contains, replaced to de-dupe held data. { generateUserID( prefix, length ); // If the ID generated is already contained in the hashset, the method should be called again. } /* No longer needed, as we add the user and the ID to the customerMap when storing. * uuidSet.add( uuid ) */ return uuid; } /** * Generate IDs End */ /** * Print Object Details Start */ /* * Prints to the terminal, in a human-readable format, library reservation in the itemList * * Contains a marker at the start and end to visualise in terminal output. */ public void printLibraryReservations() { System.out.println( "Reservation Details Start" ); for ( LibraryReservation reservation : libraryReservationMap.values() ){ System.out.println("---------------"); reservation.printDetails(); } System.out.println( "---------------\nReservation Details End\n" ); } /* * Prints to the terminal, in a human-readable format, all items in the itemList * * Contains a marker at the start and end to visualise in terminal output. */ public void printAll() { System.out.println("\n\nItem Details Start"); for( LibraryItem item : itemsMap.values() ) { System.out.println("---------------"); item.printDetails(); } System.out.println("---------------"); System.out.println("Items Details End\n\n---------------\n\nUser Details Start\n"); for ( LibraryUser user : customerMap.values() ) { System.out.println("---------------"); user.printDetails(); } System.out.println("---------------\nUser Details End\n"); printLibraryReservations(); } public void printDiaryEntries( String start, String end ) { diary.printEntries( DateUtil.convertStringToDate( start ), DateUtil.convertStringToDate( end ) ); } /** * Print Object Details End */ /** * Write Files Start */ /* * Write all current library reservations to a file specified by the user. */ public void writeLibraryReservationData() { try { Frame frame = null; // Initialise null frame FileDialog fileBox = new FileDialog( frame, "Save", FileDialog.SAVE ); // Initialise file dialog box to save written data. fileBox.setVisible( true ); PrintWriter writer = new PrintWriter( new File( fileBox.getDirectory() + fileBox.getFile() ) ); for ( LibraryReservation reservation : libraryReservationMap.values() ) { reservation.writeData( writer ); } writer.close(); } catch( IOException e ) { // Catch any IO Exceptions that may occur from improper file selection. System.err.println( "Caught IOException: " + e.getMessage() + "\nAn I/O Exception has occurred, please check file is readable and correct format." ); } } /* * A method to output all user data to a file using a fileDialog to specify file location. * */ public void writeUserData() { try { Frame frame = null; // Initialise null frame FileDialog fileBox = new FileDialog( frame, "Save", FileDialog.SAVE ); // Initialise file dialog box to save written data. fileBox.setVisible( true ); PrintWriter writer = new PrintWriter( new File( fileBox.getDirectory() + fileBox.getFile() ) ); // for ( LibraryUser user : userList ){ writer.println("[Library User data]"); for ( LibraryUser user : customerMap.values() ) { user.writeData( writer ); } writer.close(); } catch( IOException e ) { // Catch any IO Exceptions that may occur from improper file selection. System.err.println( "Caught IOException: " + e.getMessage() + "\nAn I/O Exception has occurred, please check file is readable and correct format." ); } } /** * Write Files End */ /** * Read Files Start */ /* * Read Library Reservation Data from a file specified by the user. */ public void readLibraryReservationData() { try { Frame frame = null; // Initialise null frame FileDialog fileBox = new FileDialog( frame, "Load", FileDialog.LOAD ); // Initialise file dialog box to save written data. fileBox.setVisible( true ); Scanner reservationScanner = new Scanner ( new File( fileBox.getDirectory() + fileBox.getFile() ) ); while ( reservationScanner.hasNextLine() ) { Scanner detailScanner = new Scanner( reservationScanner.nextLine() ).useDelimiter(","); LibraryReservation reservation = new LibraryReservation(); reservation.readData( detailScanner ); storeLibraryReservation( reservation ); detailScanner.close(); } reservationScanner.close(); // Close Scanner } catch( IOException e ) { // Catch any IO Exceptions that may occur from improper file selection. System.err.println( "Caught IOException: " + e.getMessage() + "\nAn I/O Exception has occurred, please check file is readable and correct format." ); } } /* * A method to read all data from files using a fileDialog to specify file location. * This will create the corresponding objects depending on flags contained in the file * and populate it's fields. * * Default flag value: "book", to support legacy files. This will not interfere with * files of different flag orders. */ public void readData() { Frame frame = null; // Initialise a null frame FileDialog fileBox = new FileDialog( frame, "Open", FileDialog.LOAD ); // Initialise filebox with the null frame pointer fileBox.setVisible( true ); // Open a file selection dialog to the user. try { Scanner fileScanner = new Scanner( new File( fileBox.getDirectory() + fileBox.getFile() ) ); String typeFlag = "book"; // Set a default flag to support legacy files. while( fileScanner.hasNextLine() ) { String lineItem = fileScanner.nextLine(); // Ensure no comments or empty lines are included if ( lineItem.contains( "//" ) || lineItem.trim().isEmpty() ){} // Check current line is a candidate flag else if ( lineItem.startsWith("[" ) ) { if ( lineItem.toLowerCase().contains("book") ) typeFlag = "book"; else if ( lineItem.toLowerCase().contains("periodical") ) typeFlag = "periodical"; else if ( lineItem.toLowerCase().contains("cd") ) typeFlag = "cd"; else if ( lineItem.toLowerCase().contains("dvd") ) typeFlag = "dvd"; else if ( lineItem.toLowerCase().contains("user") ) typeFlag = "user"; else { System.out.println( "Flag detected, but no accepted format...\n Cannot store item in Library. Changing Flag to generic and skipping the following: "); typeFlag = "generic"; } } // Unfortunately cannot use switch case with string comparisons in Java 8. // Check current flag for data processing. else { Scanner detailScanner = new Scanner ( lineItem ).useDelimiter(","); // Create a new scanner to grab the values in a comma separated list LibraryItem item = null; // Initialise LibraryItem object. Java compiler was being cautious here, so I have to assign the value null. if ( typeFlag.equals( "book" ) ) { // Process Book Data item = new Book(); } else if ( typeFlag.equals( "periodical" ) ) { // Process Periodic Data item = new Periodical(); } else if ( typeFlag.equals( "cd" ) ) { //Process CD Data item = new CD(); } else if ( typeFlag.equals( "dvd" ) ) { //Process DVD Data item = new DVD(); } else if ( typeFlag.equals( "user" ) ) { //Process User Data LibraryUser user = new LibraryUser(); user.readData( detailScanner ); storeUser( user ); continue; } else if ( typeFlag.equals( "generic" ) ) { // Output unaccepted lines to terminal System.out.println( lineItem ); continue; } item.readItemData( detailScanner ); storeItem( item ); detailScanner.close(); } } fileScanner.close(); } catch( IOException e ) { // Catch any IO Exceptions that may occur from improper file selection. System.err.println( "Caught IOException: " + e.getMessage() + "\nAn I/O Exception has occurred, please check file is readable and correct format." ); } } /** * Read Files End */ /** * Other Methods */ /* * Create a reservation to allow a user to reserve an item from the library. */ public boolean makeLibraryReservation( String userID, String itemCode, String startDate, int noOfDays ) { if ( !customerMap.containsKey( userID ) ) { System.out.println( "User does not exist" ); return false; } if ( !itemsMap.containsKey( itemCode ) ) { System.out.println( "Item does not exist in library" ); return false; } if ( !DateUtil.isValidDateString( startDate ) ) { System.out.println( "Date is not valid" ); return false; } if ( !(noOfDays > 0) ) { System.out.println( "Reservation must be more than 0 days" ); return false; } Date start = DateUtil.convertStringToDate( startDate ); /* * Unneeded, since addReservations returns any item currently on loan. Dates do not need to be checked for every day in loan. * Date end = DateUtil.incrementDate( start, noOfDays-1 ); */ String rID = generateReservationNo(); LibraryReservation reservation = new LibraryReservation( rID, itemCode, userID, startDate, noOfDays ); /** Instead of the following For loop, I could've implemented * Arrays.asList( diary.getReservation( start, end ).contains( reservation ) * Although this would've been easier, this implementation is more in the scope of what we have learned. **/ for ( LibraryReservation diaryReservation : diary.getReservations( start ) ) { if ( diaryReservation.toString().equals( reservation.toString() ) ) { return false; } } storeLibraryReservation( reservation ); return true; } public void deleteLibraryReservation( String reservationNo ) { if ( libraryReservationMap.containsKey( reservationNo ) ) { diary.deleteReservation( libraryReservationMap.get( reservationNo ) ); libraryReservationMap.remove( reservationNo ); } } }