diff --git a/.gitignore b/.gitignore index d98ce8d7baf840ad4bb159adda2cd99370f23f1e..f69c89c677742989ebdbf3d0fcd673123d4c8a53 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,13 @@ +# Default ignored files +/workspace.xml /out/ -addressbook.dat -.idea/sonarlint +/.idea/sonarlint +/dataSources/ +/dataSources.local.xml /vpproject/ /Contacts.vpdm/ /derby.log -/contactsdb/ \ No newline at end of file +/contactsdb/ +/doc/ +/contacts.log +/addressbook.dat diff --git a/.idea/.gitignore b/.idea/.gitignore index 5c98b428844d9f7d529e2b6fb918d15bf072f3df..7a472128d31948069a46dca8c8f6008ddf3897c6 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,2 +1,5 @@ # Default ignored files -/workspace.xml \ No newline at end of file +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 71d66c0f3ec305658aaae0ec147790e0909f3362..02ee5c8128232ae086ee0e4606f9dfbfd3bdfe23 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -7,9 +7,6 @@ <entry key="copy-libs" value="false" /> <entry key="location-0" value="BUNDLED:(bundled):Sun Checks" /> <entry key="location-1" value="BUNDLED:(bundled):Google Checks" /> - <entry key="location-2" value="LOCAL_FILE:/Users/Shared/Dropbox/NTNU/Undervisning/IDATA2001 Programmering 2/CheckStyle files/idata2001_checks.xml:IDATA2001 Checks" /> - <entry key="property-2.org.checkstyle.google.suppressionfilter.config" value="" /> - <entry key="property-2.org.checkstyle.google.suppressionxpathfilter.config" value="" /> <entry key="scan-before-checkin" value="false" /> <entry key="scanscope" value="JavaOnly" /> <entry key="suppress-errors" value="false" /> diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000000000000000000000000000000000000..f549a2366a0c00fce7547580aac8c0c5546f822a --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> + <data-source source="LOCAL" name="Apache Derby (Embedded) -" uuid="6f00763e-3a69-4367-8591-27393851cd60"> + <driver-ref>derby.embedded</driver-ref> + <synchronize>true</synchronize> + <jdbc-driver>org.apache.derby.jdbc.EmbeddedDriver</jdbc-driver> + </data-source> + <data-source source="LOCAL" name="Apache Derby (Remote) - jdbc:derby://localhost:1527/contactsdb" uuid="519b1631-2014-4273-9a73-dc67a189ce14"> + <driver-ref>derby.remote</driver-ref> + <synchronize>true</synchronize> + <jdbc-driver>org.apache.derby.jdbc.ClientDriver</jdbc-driver> + <jdbc-url>jdbc:derby://localhost:1527/contactsdb</jdbc-url> + </data-source> + <data-source source="LOCAL" name="MySQL@IDI" uuid="01d24ffe-dde5-4cf7-80ed-2a4f158faf6c"> + <driver-ref>mysql.8</driver-ref> + <synchronize>true</synchronize> + <jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver> + <jdbc-url>jdbc:mysql://mysql-ait.stud.idi.ntnu.no/asty</jdbc-url> + </data-source> + </component> +</project> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index f878aa18a3cb0174a3da0ba22d06802156d54a69..20f7160ec525404523a01780a44c605ace44315a 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> <project version="4"> <component name="FrameworkDetectionExcludesConfiguration" detection-enabled="false" /> + <component name="JavadocGenerationManager"> + <option name="OUTPUT_DIRECTORY" value="$PROJECT_DIR$/doc" /> + </component> <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8.0_201" project-jdk-type="JavaSDK"> <output url="file://$PROJECT_DIR$/out" /> </component> diff --git a/Contacts.iml b/Contacts.iml index b0904ac98ed93f89be372b8bac4438581a240443..2e3d390c42ea0c8f816c17c6686e791f512aff34 100644 --- a/Contacts.iml +++ b/Contacts.iml @@ -5,12 +5,24 @@ <map /> </option> </component> + <component name="FacetManager"> + <facet type="jpa" name="JPA"> + <configuration> + <setting name="validation-enabled" value="true" /> + <datasource-mapping> + <factory-entry name="contacts-pu" /> + </datasource-mapping> + <naming-strategy-map /> + <deploymentDescriptor name="persistence.xml" url="file://$MODULE_DIR$/src/META-INF/persistence.xml" /> + </configuration> + </facet> + </component> <component name="NewModuleRootManager" inherit-compiler-output="true"> <exclude-output /> <content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> </content> - <orderEntry type="jdk" jdkName="1.8.0_201" jdkType="JavaSDK" /> + <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="library" name="libs" level="project" /> </component> diff --git a/README.md b/README.md index d9de2d4136c7c94c03c5379451973702f44cffce..ac271b454ad712a060655f24c9fdd6ffae4b87c6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ The project was developed for use in teaching in the course "IDATx2001 Programme **Version** | **Description** --------|------------ +v0.5 | Added support for MySQL-DB at IDI (https://mysql-ait.stud.idi.ntnu.no/phpmyadmin/) through a separate *persistence unit (PU)* in the persistence.xml-file. NOTE: You must set your own **username**, **password** and **database name**. Also support has been added for a locally installed Apache Derby Server. v0.4.1 | Renamed the class AddressBookDAO to AddressBookDBHandler, since DAO is a general term that also could be used for the AddressBookFileHandler. Also altered slightly the use of EntityManager. v0.4 | Added Relational Database support, using the embedded Apache Derby server. For details of the changes made, se below. v0.3 | Adds object serialization of the entire address book. @@ -39,8 +40,7 @@ is the same folder that the **MANIFEST.MF** should be. ## 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. +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 SceneBuilder, but builds the GUI from within the Java code. #### JavaFX concepts demonstrated in the project diff --git a/libs/derbyclient.jar b/libs/derbyclient.jar new file mode 100644 index 0000000000000000000000000000000000000000..dde69cd349fdc424cd8929952a1219ea68554850 Binary files /dev/null and b/libs/derbyclient.jar differ diff --git a/libs/mysql-connector-java-8.0.19.jar b/libs/mysql-connector-java-8.0.19.jar new file mode 100644 index 0000000000000000000000000000000000000000..77505177899b42c18b9a7fb2e5f43d033d23ce5e Binary files /dev/null and b/libs/mysql-connector-java-8.0.19.jar differ diff --git a/src/META-INF/persistence.xml b/src/META-INF/persistence.xml index 27b9cb69f88dc65f2d936d42ad291903e4516188..ed0a450d9cb4cef8fe21ceb8fe8e017366480066 100644 --- a/src/META-INF/persistence.xml +++ b/src/META-INF/persistence.xml @@ -2,6 +2,14 @@ <persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> + + <!-- + The persistence unit (pu) to be used to connect to a Derby Embedded database. En embedded database + is really just a file-system, not a running server, but you interact with the embedded database + in the same way you would a RDB-server. + Note that using an embedded database should only be used when there never is more than one + application (client) accessing the database. + --> <persistence-unit name="contacts-pu" transaction-type="RESOURCE_LOCAL"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <exclude-unlisted-classes>false</exclude-unlisted-classes> @@ -17,7 +25,7 @@ <!-- Alternatives: create-tables, drop-and-create-tables--> <property name="eclipselink.ddl-generation" value="create-tables"/> <!-- Alternatives: FINE (logs all SQL), ALL, CONFIG, INFO, WARNING..., OFF --> - <property name="eclipselink.logging.level" value="FINE"/> + <property name="eclipselink.logging.level" value="OFF"/> <!-- The Database can be pre-filled with entries during startup. This would be very useful during testing @@ -30,4 +38,62 @@ </properties> </persistence-unit> + <!-- + This persistence unit connects to a locally installed Derby Database server. To use this, we need to add + the derbyclient.jar to the path (the libs-folder). + Also, the DB-server must have been started (from a terminal window using "startNetworkServer"). + --> + <persistence-unit name="contacts-localdb-pu" transaction-type="RESOURCE_LOCAL"> + <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> + <exclude-unlisted-classes>false</exclude-unlisted-classes> + + <properties> + <property name="eclipselink.target-database" value="Derby"/> + + <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.ClientDriver"/> + <property name="javax.persistence.jdbc.url" value="jdbc:derby://localhost:1527/contactsdb;create=true"/> + <property name="javax.persistence.jdbc.user" value="app"/> + <property name="javax.persistence.jdbc.password" value="app"/> + + <!-- Alternatives: create-tables, drop-and-create-tables--> + <property name="eclipselink.ddl-generation" value="create-tables"/> + <!-- Alternatives: FINE (logs all SQL), ALL, CONFIG, INFO, WARNING..., OFF --> + <property name="eclipselink.logging.level" value="INFO"/> + + <!-- + The Database can be pre-filled with entries during startup. This would be very useful during testing + of the application, used in conjunction with "drop-and-create-tables". The SQL-statements for inserting + entries in the DB-table can be stored in a text-file. The line below, when un-commented, will + read SQL-statements from the file "META-INF/sql/data.sql" during application startup. + --> + <!--property name="javax.persistence.sql-load-script-source" value="META-INF/sql/data.sql"/--> + + </properties> + </persistence-unit> + + + + <!-- + This persistence-unit connect to the MySQL-server installed at IDI in Trondheim. Prior to connecting, + you must contact stab-tk@idi.ntnu.no to have a user account and a database setup for you. + The URL of the server is: mysql-ait.stud.idi.ntnu.no + --> + <persistence-unit name="contacts-mysql-pu" transaction-type="RESOURCE_LOCAL"> + <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> + <class>no.ntnu.idata2001.contacts.model.ContactDetails</class> + <properties> + <property name="javax.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver" /> + <property name="javax.persistence.jdbc.url" value="jdbc:mysql://mysql-ait.stud.idi.ntnu.no/asty" /> + <property name="javax.persistence.jdbc.user" value="asty" /> + <property name="javax.persistence.jdbc.password" value="SOME_PASSWORD" /> + + <property name="eclipselink.target-database" value="MySQL"/> + + <property name="eclipselink.ddl-generation" value="create-tables" /> + <property name="eclipselink.ddl-generation.output-mode" value="database" /> + + <property name="eclipselink.logging.level" value="INFO"/> + </properties> + </persistence-unit> + </persistence> \ No newline at end of file diff --git a/src/no/ntnu/idata2001/contacts/model/AddressBook.java b/src/no/ntnu/idata2001/contacts/model/AddressBook.java index f38b10792730f0e241e378f5d8d0fb34393f20e6..b3dfc5c08f9d06d795f7abd369aee8a7fafe6d0e 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBook.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBook.java @@ -4,6 +4,10 @@ import java.io.Serializable; import java.util.Collection; import java.util.Iterator; +/** + * Defines the interface to an address book. Provides methods for adding contacts, + * deleting contacts and iterating over contacts. + */ public interface AddressBook extends Serializable, Iterable<ContactDetails> { /** diff --git a/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java b/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java index 9bf3415aeb2de08b5d9070f982e4cce5f107a918..3f9b778c782d58d9968a8bb19fcf56dc1e7f53a5 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java @@ -3,6 +3,8 @@ package no.ntnu.idata2001.contacts.model; import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; @@ -10,22 +12,22 @@ import javax.persistence.Query; /** - * An addressbook holding contacts, stored in a relational database, implemented using + * <p>An addressbook holding contacts, stored in a relational database, implemented using * the JPA implementation EclipseLink. * Using the JPA standard for Object Relational Mapping (ORM) removes database specifics * from the Java code, and hides the underlying RDB solution, making it easy to change between * different suppliers of Relational Database management Systems (RDBMS), and between locally * embedded database and server based database systems. * JPA makes use of <b>EntityManager</b> (EMF) and <b>EntityManagerFactory</b> (EM) classes for - * communication with the underlying RDBMS. Creating an EntityManagerFactory instance is costly: - * </p> - * (<a href="https://docs.jboss.org/hibernate/entitymanager/3.5/reference/en/html_single/#d0e980"> - * "An entity manager factory is typically create at application initialization time and closed at - * * application end. It's creation is an expensive process."</a> - * </p> - * Hence the EMF is implemented as a field in this class. + * communication with the underlying RDBMS. Creating an EntityManagerFactory instance is costly:</p> * - * <b>NOTE:</b> While the EMF is <b>thread safe</b>, the EM is <b>not thread safe</b>. + * <p>(<a href="https://docs.jboss.org/hibernate/entitymanager/3.5/reference/en/html_single/#d0e980">) + * "An entity manager factory is typically create at application initialization time and closed at + * * application end. It's creation is an expensive process."</a></p> + * <p> + * Hence the EMF is implemented as a field in this class. + * </p> + * <p><b>NOTE:</b> While the EMF is <b>thread safe</b>, the EM is <b>not thread safe</b>.</p> */ public class AddressBookDBHandler implements AddressBook { @@ -36,11 +38,17 @@ public class AddressBookDBHandler implements AddressBook { // communicate with the database, and should hence not be serialized. private final transient EntityManagerFactory efact; + // Create a logger to be used within this class + private final transient Logger logger = Logger.getLogger(this.getClass().getName()); + /** * Creates an instance of the AddressBookDBHandler. */ public AddressBookDBHandler() { this.efact = Persistence.createEntityManagerFactory("contacts-pu"); +// this.efact = Persistence.createEntityManagerFactory("contacts-mysql-pu"); +// this.efact = Persistence.createEntityManagerFactory("contacts-localdb-pu"); + } @Override @@ -50,26 +58,55 @@ public class AddressBookDBHandler implements AddressBook { eman.persist(contact); eman.getTransaction().commit(); eman.close(); + this.logger.log(Level.INFO, () -> + "A contact was added to the DB. Name = " + + contact.getName() + " Phone = " + + contact.getPhone()); } @Override public void removeContact(String phoneNumber) { - //TODO: To be implemented later... + EntityManager eman = this.efact.createEntityManager(); + + eman.getTransaction().begin(); + + String sql = "DELETE FROM ContactDetails c WHERE c.phone LIKE :contactPhone"; + + Query query = eman.createQuery(sql).setParameter("contactPhone", phoneNumber); + int n = query.executeUpdate(); + + eman.getTransaction().commit(); + + // A quick note on this way of logging a message where we need to use + // string concatenation: String concatenation is "expensive". Hence we should + // avoid having to build the string if it is not going to be used, i.e. if the + // logging level is set to lower than INFO, this log-message will never be logged + // even though the string concatenation always will been performed. + // One solution, is to use lambda like shown below. This way we ensure that the lambda + // expression will not be executed unless the logging level is set to INFO or higher. + // Another alternative is to use a formatter. + this.logger.log(Level.INFO, () -> "Removed contact with phone = " + + phoneNumber + ". EM returned " + n + " object deleted."); + + eman.close(); + } @Override public Collection<ContactDetails> getAllContacts() { EntityManager eman = this.efact.createEntityManager(); - List<ContactDetails> contactsList = null; - String sql = "SELECT c FROM ContactDetails c"; Query query = eman.createQuery(sql); - contactsList = query.getResultList(); + + List<ContactDetails> contactsList = query.getResultList(); eman.close(); + + this.logger.log(Level.INFO, () -> + "Read all contacts from the DB, a total of " + contactsList.size()); return contactsList; } diff --git a/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java b/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java index 813707b44a5a6248989c0628e579505e1f55e8b0..02603d1b90273272f5674f4f10ab6ad48b24c670 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java @@ -7,11 +7,11 @@ import java.util.TreeMap; /** * Represents an Address book containing contacts with contact details. * Based on the example in the book "Objects first with Java" by David J. Barnes - * and Michael Kölling. + * and Michael Kölling. * * <p>Each contact is stored in a TreeMap using the phone number as the key. * - * @author David J. Barnes and Michael Kölling and Arne Styve + * @author David J. Barnes and Michael Kölling and Arne Styve * @version 2020.03.16 */ public class AddressBookPlain implements AddressBook { @@ -23,13 +23,13 @@ public class AddressBookPlain implements AddressBook { // TreeMap is a bit less efficient than a HashMap in terms of searching, du to the // sorted order. For more details on the difference: // https://javatutorial.net/difference-between-hashmap-and-treemap-in-java - private TreeMap<String, ContactDetails> book; + private final TreeMap<String, ContactDetails> book; /** * Creates an instance of the AddressBook, initialising the instance. */ public AddressBookPlain() { - book = new TreeMap<>(); + this.book = new TreeMap<>(); } /** @@ -64,7 +64,7 @@ public class AddressBookPlain implements AddressBook { @Override public void close() { - + // Left empty intentionally. Nothing to close in this class. } @Override diff --git a/src/no/ntnu/idata2001/contacts/model/ContactDetails.java b/src/no/ntnu/idata2001/contacts/model/ContactDetails.java index 957485ef90d9f8b535a199f6f32662cc0f873176..58db2f43705e852c26ff2a98b4d75c4b75575518 100644 --- a/src/no/ntnu/idata2001/contacts/model/ContactDetails.java +++ b/src/no/ntnu/idata2001/contacts/model/ContactDetails.java @@ -8,9 +8,9 @@ import javax.persistence.Id; /** * Holds details about a contact, like name, address and phone number. * Based on the example in the book "Objects first with Java" by David J. Barnes - * and Michael Kölling. + * and Michael Kölling. * - * @author David J. Barnes and Michael Kölling and Arne Styve + * @author David J. Barnes and Michael Kölling and Arne Styve * @version 2020.03.16 */ @Entity diff --git a/src/no/ntnu/idata2001/contacts/views/ContactsApp.java b/src/no/ntnu/idata2001/contacts/views/ContactsApp.java index ab0575d9c07218455f89d4a4b8f9cfc19e62a446..f353dcb4846f710531ef2af5e6e3ecc4aa4eb7cc 100644 --- a/src/no/ntnu/idata2001/contacts/views/ContactsApp.java +++ b/src/no/ntnu/idata2001/contacts/views/ContactsApp.java @@ -1,5 +1,10 @@ package no.ntnu.idata2001.contacts.views; +import java.io.IOException; +import java.util.logging.FileHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.Logger; import javafx.application.Application; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -32,7 +37,7 @@ import no.ntnu.idata2001.contacts.model.ContactDetails; */ public class ContactsApp extends Application { - private static final String VERSION = "0.4.2"; + private static final String VERSION = "0.5"; private MainController mainController; private AddressBook addressBook; @@ -50,6 +55,15 @@ public class ContactsApp extends Application { * @param args command line arguments provided during startup. Not used in this app. */ public static void main(String[] args) { + // Setup logging to file, and the level to FINEST + try { + Handler fh = new FileHandler("./contacts.log"); + Logger.getLogger("no.ntnu.idata2001.contacts").addHandler(fh); + Logger.getLogger("no.ntnu.idata2001.contacts").setLevel(Level.FINEST); + } catch (IOException e) { + Logger.getLogger(ContactsApp.class.getName()).log(Level.SEVERE, + "Could not create a log-file."); + } launch(args); }