Commit 02167312 authored by Arne Styve's avatar Arne Styve
Browse files

Merge branch 'release/v0.3'

parents 2ca77d63 3fd506fe
/out/
addressbook.dat
.idea/sonarlint
Name;Phone;Address
John Wilkinson;(+1) 231 4532;Long Road 324 NE, New York, USA
Jill Henderson;(+1) 254 3424;Charlston Av. 23, Bismark ND, USA
Lise Jensen;(+47) 432 34 321;Øvstegata 23, Volda, Norge
Petter Trulsen;(+47) 873 43 123;Storgata 1, Bergen, Norge
Espen Askeladden;(+46) 123 45 678;Eventyrskogen 23, Trysil, Norge
\ No newline at end of file
# Contacts
# Contacts - an address book project
A JavaFX-project of a typical address book application. Used to demonstrate JavaFX, GUI design, use of the TableView-component and the mapping of the TabelView to the back end register. Also includes example of file handling.
\ No newline at end of file
## Introduction
A JavaFX-project of a typical address book application. Used to demonstrate JavaFX, GUI design, use of the TableView-component and the mapping of the TabelView to the backend register. Also includes example of file handling.
The project was developed for use in teaching in the course "IDATx2001 Programmering 2" at NTNU spring 2020.
![Screendump](Screendump.png)
## Release notes
**Version** | **Description**
--------|------------
v0.3 | Adds object serialization of the entire address book.
v0.2 | Added import and export from/to CSV-file. A default CSV-file is provided (Addresses.csv)
v0.1 | First release with basic add, edit, delete functionality.
## Description of the project
The project is made to demonstrate a typical application with a graphical user interface (GUI) implemented in JavaFX.
It's a classic address book example, where you can create contacts to be added to the address book, edit existing
contacts, and delete contacts.
The project does **not** make use of FXML and SceneBulider, but builds the GUI from within the Java code.
#### JavaFX concepts demonstrated in the project
The project uses JDK 8 for simplicity, since JavaFX was bundeled with the JDK up to and including JDK 8. After JDK 8
, JavaFX (and other packages) were moved into *modules* making it a bit more complex to create JavaFX based
applications.
The following JavaFX concepts are demonstrated in this project:
* General JavaFX structure: Stage, Scene, Scenegraph, Nodes etc.
* Event handling, using Lamda
* Menu bar, toolbar, status bar
* Buttons with icons
* MenuItems with icon and keyboard shortcuts.
* TableView for displaying a table, linking the table view to the backend AddressBook without adding JavaFX
specific types to neither the AddressBook-class not the ContactDetails-class
* Model-View-Controller: A separate Controller-class has been created performing the typical controller role in a
typical MVC setting. The MVC is reflected in the package names in the project. **NOTE:** we have used "model" as the
name of the package containing those classes that makes up the model-type of functionality. In a "real life
" industry project, you would never use a single package named model (or even "view" for that matter). The package
names should reflect the *functionality* and *services* provided by the classes in the package, and not the
architecture of the application. In this project, the packages *view, model* and *controllers* is used for
pedagogical purposes.
* File-handling: Import/Export to CSV text file, in addition to object serialization used to provide persistence
between sessions. Whenever the application closes down, the address book is saved to the file "addresses.dat
". Upon startup, the same file is being read to re-create the content of the address book from last session.
## Quality control of code and style
The project has used both [CheckStyle](https://checkstyle.sourceforge.io/) and the [SonarLint](https://www.sonarlint
.org/) plugin to [IntelliJ](https://www.jetbrains.com/idea/) for code quality and coding style. For coding style
, the checkstyle [checks from Google](https://checkstyle.sourceforge.io/google_style.html) have been used.
\ No newline at end of file
package no.ntnu.idata2001.contacts.controllers;
import java.io.File;
import java.io.IOException;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
......@@ -14,15 +16,21 @@ import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.stage.FileChooser;
import javafx.util.Pair;
import no.ntnu.idata2001.contacts.model.AddressBook;
import no.ntnu.idata2001.contacts.model.AddressBookFileHandler;
import no.ntnu.idata2001.contacts.model.ContactDetails;
import no.ntnu.idata2001.contacts.views.ContactDetailsDialog;
import no.ntnu.idata2001.contacts.views.ContactsApp;
/**
* The main controller for the application. Handles all actions related to the
* buttons in the user interface: "Add Literature", "Delete" ... "Exit".
* buttons in the user interface: "Add Contact", "Delete" ... "Exit".
* The controller (in a MVC-structure) takes care of the link between
* the user interface view-part, and the model in the business layer of
* the application.
* The Controller and View have to work very close together.
*
* @author Arne Styve
* @version 2020-03-16
......@@ -30,6 +38,10 @@ import no.ntnu.idata2001.contacts.views.ContactsApp;
public class MainController {
private final Logger logger;
// File used for object serialization. Used by the methods
// saveAddressBookToFile() and loadAddressBookFromFile()
private static final String DATA_FILE_NAME = "addressbook.dat";
/**
* Creates an instance of the MainController class, initialising
* the logger.
......@@ -45,8 +57,8 @@ public class MainController {
* of ContactDetails is created and added to the AddressBook provided.
*
* @param addressBook the address book to add the new contact to.
* @param parent the parent calling this method. Use this parameter to access public methods
* in the parent, like updateObservableList().
* @param parent the parent calling this method. Use this parameter to access public methods
* in the parent, like updateObservableList().
*/
public void addContact(AddressBook addressBook, ContactsApp parent) {
......@@ -66,7 +78,7 @@ public class MainController {
*
* @param selectedContact the contact to edit. Changes made by the user are updated on the
* selectedContact object provided.
* @param parent the parent view making the call
* @param parent the parent view making the call
*/
public void editContact(ContactDetails selectedContact, ContactsApp parent) {
if (selectedContact == null) {
......@@ -85,13 +97,13 @@ public class MainController {
* select which Contact to delete.
*
* @param selectedContact the Contact to delete. If no Contact has been selected,
* this parameter will be <code>null</code>
* this parameter will be <code>null</code>
* @param addressBook the contact register to delete the selectedContact from
* @param parent the parent view making the call.
* @param parent the parent view making the call.
*/
public void deleteContact(ContactDetails selectedContact,
AddressBook addressBook,
ContactsApp parent) {
AddressBook addressBook,
ContactsApp parent) {
if (selectedContact == null) {
showPleaseSelectItemDialog();
} else {
......@@ -103,22 +115,94 @@ public class MainController {
}
public void importFromCsv() {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Information");
alert.setHeaderText("Functionality not yet implemented.");
alert.setContentText("Will be released in v0.2");
/**
* Import contacts from a .CSV-file chosen by the user.
*
* @param addressBook the address book to import the read contacts into
* @param parent the parent making the call to this method. Used for refreshing the
* Observable list used by the TableView.
*/
public void importFromCsv(AddressBook addressBook, ContactsApp parent) {
FileChooser fileChooser = new FileChooser();
// Set extension filter for .csv-file
FileChooser.ExtensionFilter extFilter =
new FileChooser.ExtensionFilter("CSV files (*.csv)", "*.csv");
fileChooser.getExtensionFilters().add(extFilter);
// Show save open dialog
File file = fileChooser.showOpenDialog(null);
if (file != null) {
try {
AddressBookFileHandler.importFromCsv(addressBook, file);
parent.updateObservableList();
} catch (IOException ioe) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("File Import Error");
alert.setHeaderText("Error during CSV-import.");
alert.setContentText("Details: " + ioe.getMessage());
alert.showAndWait();
}
}
}
alert.showAndWait();
/**
* Export all contacts in the address book to a CSV-file specified by the user.
*
* @param addressBook the address book to export contacts from.
*/
public void exportToCsv(AddressBook addressBook) {
FileChooser fileChooser = new FileChooser();
// Set extension filter for .csv-file
FileChooser.ExtensionFilter extFilter =
new FileChooser.ExtensionFilter("CSV files (*.csv)", "*.csv");
fileChooser.getExtensionFilters().add(extFilter);
// Show save file dialog
File file = fileChooser.showSaveDialog(null);
if (file != null) {
try {
AddressBookFileHandler.exportToCsv(addressBook, file);
} catch (IOException ioe) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("File Export Error");
alert.setHeaderText("Error during CSV-export.");
alert.setContentText("Details: " + ioe.getMessage());
alert.showAndWait();
}
}
}
public void exportToCsv() {
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Information");
alert.setHeaderText("Functionality not yet implemented.");
alert.setContentText("Will be released in v0.2");
/**
* Saves an entire address book to a file using object serializing. The address book
* is saved to a file called "addressbook.dat".
*
* @param addressBook the address book to save
*/
public void saveAddressBookToFile(AddressBook addressBook) {
try {
File outFile = new File(DATA_FILE_NAME);
AddressBookFileHandler.saveToFile(addressBook, outFile);
} catch (IOException ioe) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("File Save Error");
alert.setHeaderText("Error while saving the address book.");
alert.setContentText("Details: " + ioe.getMessage());
alert.showAndWait();
}
}
alert.showAndWait();
/**
* Loads an entire address book from a file using object serialization.
* The address book is loaded from the file "addressbook.dat".
* If file was read successfully, an instance of AddressBook is returned.
*
* @return an address book populated by contact details loaded from the file.
*/
public AddressBook loadAddressBookFromFile() {
File inFile = new File(DATA_FILE_NAME);
return AddressBookFileHandler.loadFromFile(inFile);
}
/**
......@@ -132,17 +216,13 @@ public class MainController {
Optional<ButtonType> result = alert.showAndWait();
if (result.isPresent()) {
if (result.get() == ButtonType.OK) {
// ... user choose OK
Platform.exit();
} else {
// ... user chose CANCEL or closed the dialog
// then do nothing.
}
if (result.isPresent() && (result.get() == ButtonType.OK)) {
// ... user choose OK
Platform.exit();
}
}
/**
* Displays an example of an alert (info) dialog. In this case an "about"
* type of dialog.
......@@ -207,21 +287,20 @@ public class MainController {
// Convert the result to a username-password-pair when the login button is clicked.
dialog.setResultConverter(
dialogButton -> {
if (dialogButton == loginButtonType) {
return new Pair<>(username.getText(), password.getText());
}
return null;
});
if (dialogButton == loginButtonType) {
return new Pair<>(username.getText(), password.getText());
}
return null;
});
Optional<Pair<String, String>> result = dialog.showAndWait();
result.ifPresent(
usernamePassword -> logger.log(Level.INFO, "Username=" + usernamePassword.getKey()
+ ", Password=" + usernamePassword.getValue()));
+ ", Password=" + usernamePassword.getValue()));
}
/**
* Show details of the selected contact item.
*
......
......@@ -2,6 +2,7 @@ package no.ntnu.idata2001.contacts.model;
import java.io.Serializable;
import java.util.Collection;
import java.util.Iterator;
import java.util.TreeMap;
/**
......@@ -14,7 +15,7 @@ import java.util.TreeMap;
* @author David J. Barnes and Michael Kölling and Arne Styve
* @version 2020.03.16
*/
public class AddressBook implements Serializable {
public class AddressBook implements Serializable, Iterable<ContactDetails> {
// Storage for an arbitrary number of details.
// We have chosen to use TreeMap instead of HashMap in this example, the
// main difference being that a TreeMap is sorted. That is, the keys are sorted,
......@@ -111,4 +112,8 @@ public class AddressBook implements Serializable {
return this.book.values();
}
@Override
public Iterator<ContactDetails> iterator() {
return this.book.values().iterator();
}
}
package no.ntnu.idata2001.contacts.model;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Responsible for all file-activities related to the AddressBook, like saving and reading
* from binary file (serializing), and import/export CSV-files.
* Contains only static methods, where all methods either take an AddressBook
* instance as parameter, or returns an AddressBook instance.
*/
public class AddressBookFileHandler {
// Use ';' as delimiter for CSV-files to avoid problems with the address field using ','
private static final String CSV_DELIMITER = ";";
// Logger used for debugging
private static final Logger logger = Logger.getLogger(AddressBookFileHandler.class.getName());
/**
* Make the constructor private to avoid the possibility to
* create an object from this class. Creating an object is not necessary
* since all methods in the class are class-methods (static).
*/
private AddressBookFileHandler() {
// Intentionally left empty
}
/**
* Exports the address book to a .csv file. The first line holds the
* titles of the columns: Name, Phone and Address
*
* @param addressBook the address book to export
* @param file the file to export the address book to
* @throws IOException if the file exists but is a directory rather than a regular file,
* does not exist but cannot be created,
* or cannot be opened for any other reason
*/
public static void exportToCsv(AddressBook addressBook, File file) throws IOException {
FileWriter writer = new FileWriter(file);
PrintWriter printWriter = new PrintWriter(writer);
// Save the column titles.
printWriter.println("Name" + CSV_DELIMITER + "Phone" + CSV_DELIMITER + "Address");
for (ContactDetails contact : addressBook) {
printWriter.print(contact.getName() + CSV_DELIMITER);
printWriter.print(contact.getPhone() + CSV_DELIMITER);
printWriter.println(contact.getAddress());
}
printWriter.close();
}
/**
* Import contacts from a CSV-file into en existing address book given
* by the parameter.
* The first line is expected to hold the titles of the columns: "Name, Phone and Address
*
* @param addressBook the address book to import contacts into
* @param file the file to import from
* @throws IOException if an I/O error occurs opening the file
*/
public static void importFromCsv(AddressBook addressBook, File file) throws IOException {
// NOTE: I am using a try block here with no catch-block since
// I want the exception to be sent to the caller of the method.
// I could have left out the try-block entirely, BUT that could result in the
// file not being closed in case of an exception being thrown
// Hence, always use a try-with-resource when dealing with IO and resources like files,
// even though you cannot handle a thrown exception in this method.
try (BufferedReader reader = Files.newBufferedReader(file.toPath())) {
// The first line read is the column title line, and should
// not be parsed as ContactDetails
boolean isTitleLine = true;
String lineOfText = reader.readLine();
while (lineOfText != null) {
if (!isTitleLine) {
ContactDetails contact = parseStringAsContactDetails(lineOfText);
addressBook.addContact(contact);
} else {
isTitleLine = false;
}
lineOfText = reader.readLine();
}
}
}
/**
* Parses a string to convert the string to an instance of ContactDetails.
*
* @param line the line of text holding the CSV-separated fields making up a contact
* @return an instance of ContactDetails created from the line provided
* @throws ContactDetailsFormatException if the string cannot be parsed as ContactDetails
*/
private static ContactDetails parseStringAsContactDetails(String line) {
ContactDetails contact = null;
String[] subStrings = line.split(CSV_DELIMITER);
if (subStrings.length == 3) {
contact = new ContactDetails(subStrings[0], subStrings[1], subStrings[2]);
} else {
throw new ContactDetailsFormatException();
}
return contact;
}
/**
* Saves an entire address book to the given file using object serialization.
*
* @param addressBook the address book to save
* @param file the file to save the address book to
* @throws IOException if an I/O error occurs while writing to file
*/
public static void saveToFile(AddressBook addressBook, File file) throws IOException {
// NOTE: I am using a try block here with no catch-block since
// I want the exception to be sent to the caller of the method.
// I could have left out the try-block entirely, BUT that could result in the
// file not being closed in case of an exception being thrown
// Hence, always use a try-with-resource when dealing with IO and resources like files,
// even though you cannot handle a thrown exception in this method.
try (OutputStream outputStream = Files.newOutputStream(file.toPath())) {
ObjectOutputStream objectOutStream = new ObjectOutputStream(outputStream);
objectOutStream.writeObject(addressBook);
}
}
/**
* Loads an entire address book from a given file using object serialization.
* If the file cannot bbe read for some reason, an empty address book
* is returned.
*
* @param inFile the file from which the address book will be loaded
* @return an instance of AddressBook holding all contacts loaded from the file
*/
public static AddressBook loadFromFile(File inFile) {
// NOTE: I am using a try block here with no catch-block since
// I want the exception to be sent to the caller of the method.
// I could have left out the try-block entirely, BUT that could result in the
// file not being closed in case of an exception being thrown
// Hence, always use a try-with-resource when dealing with IO and resources like files,
// even though you cannot handle a thrown exception in this method.
try (InputStream inputStream = Files.newInputStream(inFile.toPath())) {
ObjectInputStream objectInputStream = new ObjectInputStream(inputStream);
return (AddressBook) objectInputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
logger.log(Level.INFO, "Could not open file "
+ inFile.getName() + ". An empty AddressBook was returned.");
return new AddressBook();
}
}
}
package no.ntnu.idata2001.contacts.model;
/**
* Thrown to indicate that an attempt have been made to convert a string to an instance
* of ContactDetails.
*/
public class ContactDetailsFormatException extends IllegalArgumentException {
/**
* Constructs a ContactDetailsFormatException with no detail message.
*/
public ContactDetailsFormatException() {
super("Could not convert the string to an ContactDetails-object..");
}
/**
* Constructs a ContactDetailsFormatException with the specified detail message.
* @param s the detail message
*/
public ContactDetailsFormatException(String s) {
super(s);
}
}
......@@ -148,7 +148,7 @@ public class ContactDetailsDialog extends Dialog<ContactDetails> {
if (button == ButtonType.OK) {
if (mode == Mode.NEW) {
result = new ContactDetails(name.getText(), address.getText(), phoneNumber.getText());
result = new ContactDetails(name.getText(), phoneNumber.getText(), address.getText());
} else if (mode == Mode.EDIT) {
existingContact.setName(name.getText());
existingContact.setAddress(address.getText());
......
......@@ -32,10 +32,10 @@ import no.ntnu.idata2001.contacts.model.ContactDetails;
*/
public class ContactsApp extends Application {
private static final String VERSION = "0.2";
private static final String VERSION = "0.3";
private final MainController mainController;
private final AddressBook addressBook;
private MainController mainController;
private AddressBook addressBook;
// The JavaFX ObservableListWrapper used to connect tot he underlying AddressBook
private ObservableList<ContactDetails> addressBookListWrapper;
......@@ -44,21 +44,6 @@ public class ContactsApp extends Application {
// from different places in our GUI (menu, doubleclicking, toolbar etc.)
private TableView<ContactDetails> contactDetailsTableView;
/**
* Creates an instance of the ContactsApp, initialising the
* main controller and the address book.
*/
public ContactsApp() {
super();
// Initialise the main controller
this.mainController = new MainController();
// Initialise the Address Book
this.addressBook = new AddressBook();
this.fillAddressBookWithDummies();
}
/**
* The main starting point of the application.
*
......@@ -68,6 +53,17 @@ public class ContactsApp extends Application {
launch(args);
}
@Override
public void init() throws Exception {
super.init();
// Initialise the main controller
this.mainController = new MainController();
// Initialise the Address Book from a file
this.addressBook = this.mainController.loadAddressBookFromFile();
}
@Override
public void start(Stage primaryStage) {
// Build the GUI of the main window
......@@ -108,6 +104,9 @@ public class ContactsApp extends Application {
*/
@Override
public void stop() {
// Save the address book to file
this.mainController.saveAddressBookToFile(this.addressBook);
// Exit the application
System.exit(0);
}
......@@ -247,11 +246,11 @@ public class ContactsApp extends Application {
Menu menuFile = new Menu("File");
MenuItem importFromCsv = new MenuItem("Import from .CSV...");
importFromCsv.setOnAction(event -> mainController.exportToCsv());
importFromCsv.setOnAction(event -> mainController.importFromCsv(this.addressBook, this));
menuFile.getItems().add(importFromCsv);
MenuItem exportToCsv = new MenuItem("Export to .CSV...");
exportToCsv.setOnAction(event -> mainController.exportToCsv());
exportToCsv.setOnAction(event -> mainController.exportToCsv(this.addressBook));
menuFile.getItems().add(exportToCsv);
// Add a separator line before Exit
......@@ -329,17 +328,4 @@ public class ContactsApp extends Application {