diff --git a/.gitignore b/.gitignore index f69c89c677742989ebdbf3d0fcd673123d4c8a53..a89aff6d874a8d6b22b44e4d2a6e56251c793474 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ /doc/ /contacts.log /addressbook.dat +workspace (Arnes MacBook Pros kopi som er i konflikt 2020-05-04).xml diff --git a/.idea/checkstyle-idea.xml b/.idea/checkstyle-idea.xml index 02ee5c8128232ae086ee0e4606f9dfbfd3bdfe23..d59cf5ff058ababa9d6a19f1c7ad77c1b0c55ba7 100644 --- a/.idea/checkstyle-idea.xml +++ b/.idea/checkstyle-idea.xml @@ -7,6 +7,9 @@ <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="PROJECT_RELATIVE:$PROJECT_DIR$/idatx2001_checks.xml:IDATx2001 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/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index f23d524925f4de5330e1ecf85f911c42ea9db92b..4fe762afc3ca43ffc71a62dfd2d55d0d22411e6d 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ <component name="ProjectCodeStyleConfiguration"> <state> - <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default copy" /> + <option name="PREFERRED_PROJECT_CODE_STYLE" value="IDATx2001 Checks" /> </state> </component> \ No newline at end of file diff --git a/.idea/libraries/junit_junit_4_13.xml b/.idea/libraries/junit_junit_4_13.xml new file mode 100644 index 0000000000000000000000000000000000000000..cc71bc4016ac2c562e3f47674ae51368cc98973f --- /dev/null +++ b/.idea/libraries/junit_junit_4_13.xml @@ -0,0 +1,11 @@ +<component name="libraryTable"> + <library name="junit:junit:4.13" type="repository"> + <properties maven-id="junit:junit:4.13" /> + <CLASSES> + <root url="jar://$PROJECT_DIR$/lib/junit-4.13.jar!/" /> + <root url="jar://$PROJECT_DIR$/lib/hamcrest-core-1.3.jar!/" /> + </CLASSES> + <JAVADOC /> + <SOURCES /> + </library> +</component> \ No newline at end of file diff --git a/.idea/libraries/lib.xml b/.idea/libraries/lib.xml new file mode 100644 index 0000000000000000000000000000000000000000..fa8838a590c51fab5874c30c10510a0b093b73a0 --- /dev/null +++ b/.idea/libraries/lib.xml @@ -0,0 +1,10 @@ +<component name="libraryTable"> + <library name="lib"> + <CLASSES> + <root url="file://$PROJECT_DIR$/lib" /> + </CLASSES> + <JAVADOC /> + <SOURCES /> + <jarDirectory url="file://$PROJECT_DIR$/lib" recursive="false" /> + </library> +</component> \ No newline at end of file diff --git a/.idea/libraries/libs.xml b/.idea/libraries/libs.xml deleted file mode 100644 index 8faa3ad189cf3302cda728ae117e83c4f051f49c..0000000000000000000000000000000000000000 --- a/.idea/libraries/libs.xml +++ /dev/null @@ -1,10 +0,0 @@ -<component name="libraryTable"> - <library name="libs"> - <CLASSES> - <root url="file://$PROJECT_DIR$/libs" /> - </CLASSES> - <JAVADOC /> - <SOURCES /> - <jarDirectory url="file://$PROJECT_DIR$/libs" recursive="false" /> - </library> -</component> \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 20f7160ec525404523a01780a44c605ace44315a..666d3b15148c7bec959bec8c5adfd95c4af87928 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,7 +4,7 @@ <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"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK"> <output url="file://$PROJECT_DIR$/out" /> </component> </project> \ No newline at end of file diff --git a/Contacts.iml b/Contacts.iml index f132698282b598b97fddd8c7c9924e87857e4852..1cee3978516a21568ca2cb9278e888863081f44a 100644 --- a/Contacts.iml +++ b/Contacts.iml @@ -23,9 +23,11 @@ <exclude-output /> <content url="file://$MODULE_DIR$"> <sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" /> + <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" /> </content> <orderEntry type="inheritedJdk" /> <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" name="libs" level="project" /> + <orderEntry type="library" name="junit:junit:4.13" level="project" /> + <orderEntry type="library" name="lib" level="project" /> </component> </module> \ No newline at end of file diff --git a/README.md b/README.md index b76d6a400dd2484f58de45118369e364b732ec5f..3838d216aa03272ec5acdd05dc78aac1394001de 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,10 @@ The project was developed for use in teaching in the course "IDATx2001 Programme **Version** | **Description** --------|------------ +v0.6 | Added an alternative JavaFX-GUI based on FXML to give an example of how the project could be solved using FXML and CSS. To run the FXML-version, run the class **ContactsAppFXML**. This class uses the **ContactsMain.fxml**-file to build the GUI, together with the **ContactsAppFXML.css**-file. The controller used with the FXML-version of the GUI, is the **ContactsAppFXMLController**. <br>NOTE: Using "FXML" as part of the class-names is not a recommended practice, and is only done so in this project to make it clear which of the classes/files are linked to the use of the JavaFx FXML-solution. <br>Also, the existing non FXML-controller class **MainController** has been refactored to take the responsibility for the AddressBook instance (previously managed by the view class **ContactsApp**. <br>And there are also some other minor changes/modifications. v0.5.1 | Some minor adjustments: removed *synchronized* from the iterator()-method of ContactDetails (not needed). Updated the ContactDetails-constructor to make use of the set-methods of the class. 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.3 | 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. diff --git a/idatx2001_checks.xml b/idatx2001_checks.xml new file mode 100644 index 0000000000000000000000000000000000000000..03b283a33a04568b95060aedb7a48031db18d8da --- /dev/null +++ b/idatx2001_checks.xml @@ -0,0 +1,321 @@ +<?xml version="1.0"?> +<!DOCTYPE module PUBLIC + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> + +<!-- + Checkstyle configuration based on the Google coding convetions, but with some changes + for the IDATA2001 Programming 2 course at NTNU + + Checkstyle configuration that checks the Google coding conventions from Google Java Style + that can be found at https://google.github.io/styleguide/javaguide.html + Checkstyle is very configurable. Be sure to read the documentation at + http://checkstyle.org (or in your downloaded distribution). + To completely disable a check, just comment it out or delete it from the file. + To suppress certain violations please review suppression filters. + Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov. + --> + +<module name = "Checker"> + <property name="charset" value="UTF-8"/> + + <property name="severity" value="warning"/> + + <property name="fileExtensions" value="java, properties, xml"/> + <!-- Excludes all 'module-info.java' files --> + <!-- See https://checkstyle.org/config_filefilters.html --> + <module name="BeforeExecutionExclusionFileFilter"> + <property name="fileNamePattern" value="module\-info\.java$"/> + </module> + <!-- https://checkstyle.org/config_filters.html#SuppressionFilter --> + <module name="SuppressionFilter"> + <property name="file" value="${org.checkstyle.google.suppressionfilter.config}" + default="checkstyle-suppressions.xml" /> + <property name="optional" value="true"/> + </module> + + <!-- Checks for whitespace --> + <!-- See http://checkstyle.org/config_whitespace.html --> + <module name="FileTabCharacter"> + <property name="eachLine" value="true"/> + </module> + + <module name="LineLength"> + <property name="fileExtensions" value="java"/> + <property name="max" value="100"/> + <property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/> + </module> + + <module name="TreeWalker"> + <module name="OuterTypeFilename"/> + <module name="IllegalTokenText"> + <property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/> + <property name="format" + value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/> + <property name="message" + value="Consider using special escape sequence instead of octal value or Unicode escaped value."/> + </module> + <module name="AvoidEscapedUnicodeCharacters"> + <property name="allowEscapesForControlCharacters" value="true"/> + <property name="allowByTailComment" value="true"/> + <property name="allowNonPrintableEscapes" value="true"/> + </module> + <module name="AvoidStarImport"/> + <module name="OneTopLevelClass"/> + <module name="NoLineWrap"> + <property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/> + </module> + <module name="EmptyBlock"> + <property name="option" value="TEXT"/> + <property name="tokens" + value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/> + </module> + <module name="NeedBraces"> + <property name="tokens" + value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/> + </module> + <module name="LeftCurly"> + <property name="tokens" + value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF, + INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT, + LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, + LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF, + OBJBLOCK, STATIC_INIT"/> + </module> + <module name="RightCurly"> + <property name="id" value="RightCurlySame"/> + <property name="tokens" + value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, + LITERAL_DO"/> + </module> + <module name="RightCurly"> + <property name="id" value="RightCurlyAlone"/> + <property name="option" value="alone"/> + <property name="tokens" + value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT, + INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF"/> + </module> + <module name="WhitespaceAround"> + <property name="allowEmptyConstructors" value="true"/> + <property name="allowEmptyLambdas" value="true"/> + <property name="allowEmptyMethods" value="true"/> + <property name="allowEmptyTypes" value="true"/> + <property name="allowEmptyLoops" value="true"/> + <property name="tokens" + value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, + BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND, + LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, + LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED, + LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN, + NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR, + SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/> + <message key="ws.notFollowed" + value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/> + <message key="ws.notPreceded" + value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/> + </module> + <module name="OneStatementPerLine"/> + <module name="MultipleVariableDeclarations"/> + <module name="ArrayTypeStyle"/> + <module name="MissingSwitchDefault"/> + <module name="FallThrough"/> + <module name="UpperEll"/> + <module name="ModifierOrder"/> + <module name="EmptyLineSeparator"> + <property name="tokens" + value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF, + STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/> + <property name="allowNoEmptyLineBetweenFields" value="true"/> + </module> + <module name="SeparatorWrap"> + <property name="id" value="SeparatorWrapDot"/> + <property name="tokens" value="DOT"/> + <property name="option" value="nl"/> + </module> + <module name="SeparatorWrap"> + <property name="id" value="SeparatorWrapComma"/> + <property name="tokens" value="COMMA"/> + <property name="option" value="EOL"/> + </module> + <module name="SeparatorWrap"> + <!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 --> + <property name="id" value="SeparatorWrapEllipsis"/> + <property name="tokens" value="ELLIPSIS"/> + <property name="option" value="EOL"/> + </module> + <module name="SeparatorWrap"> + <!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 --> + <property name="id" value="SeparatorWrapArrayDeclarator"/> + <property name="tokens" value="ARRAY_DECLARATOR"/> + <property name="option" value="EOL"/> + </module> + <module name="SeparatorWrap"> + <property name="id" value="SeparatorWrapMethodRef"/> + <property name="tokens" value="METHOD_REF"/> + <property name="option" value="nl"/> + </module> + <module name="PackageName"> + <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/> + <message key="name.invalidPattern" + value="Package name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="TypeName"> + <property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF"/> + <message key="name.invalidPattern" + value="Type name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="MemberName"> + <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/> + <message key="name.invalidPattern" + value="Member name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="ParameterName"> + <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/> + <message key="name.invalidPattern" + value="Parameter name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="LambdaParameterName"> + <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/> + <message key="name.invalidPattern" + value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="CatchParameterName"> + <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/> + <message key="name.invalidPattern" + value="Catch parameter name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="LocalVariableName"> + <property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/> + <message key="name.invalidPattern" + value="Local variable name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="ClassTypeParameterName"> + <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/> + <message key="name.invalidPattern" + value="Class type name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="MethodTypeParameterName"> + <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/> + <message key="name.invalidPattern" + value="Method type name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="InterfaceTypeParameterName"> + <property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/> + <message key="name.invalidPattern" + value="Interface type name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="NoFinalizer"/> + <module name="GenericWhitespace"> + <message key="ws.followed" + value="GenericWhitespace ''{0}'' is followed by whitespace."/> + <message key="ws.preceded" + value="GenericWhitespace ''{0}'' is preceded with whitespace."/> + <message key="ws.illegalFollow" + value="GenericWhitespace ''{0}'' should followed by whitespace."/> + <message key="ws.notPreceded" + value="GenericWhitespace ''{0}'' is not preceded with whitespace."/> + </module> + <module name="Indentation"> + <property name="basicOffset" value="2"/> + <property name="braceAdjustment" value="0"/> + <property name="caseIndent" value="2"/> + <property name="throwsIndent" value="4"/> + <property name="lineWrappingIndentation" value="4"/> + <property name="arrayInitIndent" value="2"/> + </module> + <module name="AbbreviationAsWordInName"> + <property name="ignoreFinal" value="false"/> + <property name="allowedAbbreviationLength" value="1"/> + <property name="tokens" + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF, + PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF"/> + </module> + <module name="OverloadMethodsDeclarationOrder"/> + <module name="VariableDeclarationUsageDistance"/> + <module name="CustomImportOrder"> + <property name="sortImportsInGroupAlphabetically" value="true"/> + <property name="separateLineBetweenGroups" value="true"/> + <property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/> + <property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/> + </module> + <module name="MethodParamPad"> + <property name="tokens" + value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF, + SUPER_CTOR_CALL, ENUM_CONSTANT_DEF"/> + </module> + <module name="NoWhitespaceBefore"> + <property name="tokens" + value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/> + <property name="allowLineBreaks" value="true"/> + </module> + <module name="ParenPad"> + <property name="tokens" + value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF, + EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW, + LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL, + METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA"/> + </module> + <module name="OperatorWrap"> + <property name="option" value="NL"/> + <property name="tokens" + value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, + LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/> + </module> + <module name="AnnotationLocation"> + <property name="id" value="AnnotationLocationMostCases"/> + <property name="tokens" + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/> + </module> + <module name="AnnotationLocation"> + <property name="id" value="AnnotationLocationVariables"/> + <property name="tokens" value="VARIABLE_DEF"/> + <property name="allowSamelineMultipleAnnotations" value="true"/> + </module> + <module name="NonEmptyAtclauseDescription"/> + <module name="InvalidJavadocPosition"/> + <module name="JavadocTagContinuationIndentation"/> + <module name="SummaryJavadoc"> + <property name="forbiddenSummaryFragments" + value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/> + </module> + <module name="JavadocParagraph"/> + <module name="AtclauseOrder"> + <property name="tagOrder" value="@param, @return, @throws, @deprecated"/> + <property name="target" + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/> + </module> + <module name="JavadocMethod"> + <property name="scope" value="public"/> + <property name="allowMissingParamTags" value="false"/> + <property name="allowMissingReturnTag" value="false"/> + <property name="allowedAnnotations" value="Override, Test"/> + <property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/> + </module> + <module name="MissingJavadocMethod"> + <property name="scope" value="public"/> + <!--property name="minLineCount" value="2"/--> + <property name="allowedAnnotations" value="Override, Test"/> + <property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/> + </module> + <module name="MethodName"> + <property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/> + <message key="name.invalidPattern" + value="Method name ''{0}'' must match pattern ''{1}''."/> + </module> + <module name="SingleLineJavadoc"> + <property name="ignoreInlineTags" value="false"/> + </module> + <module name="EmptyCatchBlock"> + <property name="exceptionVariableName" value="expected"/> + </module> + <module name="CommentsIndentation"> + <property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/> + </module> + <!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter --> + <module name="SuppressionXpathFilter"> + <property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}" + default="checkstyle-xpath-suppressions.xml" /> + <property name="optional" value="true"/> + </module> + </module> +</module> diff --git a/libs/derby.jar b/lib/derby.jar similarity index 100% rename from libs/derby.jar rename to lib/derby.jar diff --git a/libs/derbyclient.jar b/lib/derbyclient.jar similarity index 100% rename from libs/derbyclient.jar rename to lib/derbyclient.jar diff --git a/libs/eclipselink.jar b/lib/eclipselink.jar similarity index 100% rename from libs/eclipselink.jar rename to lib/eclipselink.jar diff --git a/lib/hamcrest-core-1.3.jar b/lib/hamcrest-core-1.3.jar new file mode 100644 index 0000000000000000000000000000000000000000..9d5fe16e3dd37ebe79a36f61f5d0e1a69a653a8a Binary files /dev/null and b/lib/hamcrest-core-1.3.jar differ diff --git a/libs/jakarta.persistence_2.2.3.jar b/lib/jakarta.persistence_2.2.3.jar similarity index 100% rename from libs/jakarta.persistence_2.2.3.jar rename to lib/jakarta.persistence_2.2.3.jar diff --git a/lib/junit-4.13.jar b/lib/junit-4.13.jar new file mode 100644 index 0000000000000000000000000000000000000000..acc3c4320b580776193b875cd6e131d460ce2b7a Binary files /dev/null and b/lib/junit-4.13.jar differ diff --git a/libs/mysql-connector-java-8.0.19.jar b/lib/mysql-connector-java-8.0.19.jar similarity index 100% rename from libs/mysql-connector-java-8.0.19.jar rename to lib/mysql-connector-java-8.0.19.jar diff --git a/src/no/ntnu/idata2001/contacts/AppVersion.java b/src/no/ntnu/idata2001/contacts/AppVersion.java new file mode 100644 index 0000000000000000000000000000000000000000..d982d12b6336f107e01d23cb133bd15d387c699c --- /dev/null +++ b/src/no/ntnu/idata2001/contacts/AppVersion.java @@ -0,0 +1,17 @@ +package no.ntnu.idata2001.contacts; + +/** + * A simple class to hold the version of the application. + * + * <p>Using the "final" keyword on a class prevents the class to + * be subclassed (inherited from), which makes sence in this case. + * The field is also being set to final to indicate that there are to be + * no changes to the field (i.e. a constant). Setting the field to + * static creates a "class field", i.e. a field that exsists without having to + * create an instance of the class. + * To access the VERSION-constant, use: + * <code>AppVersion.VERSION</code> + */ +public final class AppVersion { + public static final String VERSION = "0.6"; +} diff --git a/src/no/ntnu/idata2001/contacts/controllers/ContactsAppFXMLController.java b/src/no/ntnu/idata2001/contacts/controllers/ContactsAppFXMLController.java new file mode 100644 index 0000000000000000000000000000000000000000..c334587ae0c2a2d6d08c9547f4bdc48fb0ca7054 --- /dev/null +++ b/src/no/ntnu/idata2001/contacts/controllers/ContactsAppFXMLController.java @@ -0,0 +1,342 @@ +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; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.stage.FileChooser; +import javafx.util.Pair; +import no.ntnu.idata2001.contacts.AppVersion; +import no.ntnu.idata2001.contacts.model.AddressBook; +import no.ntnu.idata2001.contacts.model.AddressBookDBHandler; +import no.ntnu.idata2001.contacts.model.AddressBookFileHandler; +import no.ntnu.idata2001.contacts.model.AddressBookPlain; +import no.ntnu.idata2001.contacts.model.ContactDetails; +import no.ntnu.idata2001.contacts.views.ContactDetailsDialog; + +/** + * The controller class mapped to the FXML main class. + * + * <p>A few words on the @FXML-annotation: + * The @FXML annotation is used to enable initialization of fields performed + * by the FXML-loader. For example, in the code below, there is a field + * for the TableView. The instance to be created, will be created by the FXML loader, + * hence you should NEVER create this instance here in the controller. + * + * <p>Now, if the field (like contactDetailsTableView) is public, you do not need to + * use the annotation @FXML on the field for it to be initialized. BUT if you set it + * to private (which you should, according to good practice), you MUST add the @FXML + * annotation for the field to be initialized by the FXML loader. + * See: https://stackoverflow.com/questions/30210170/is-fxml-needed-for-every-declaration + */ +public class ContactsAppFXMLController { + private final Logger logger = Logger.getLogger(getClass().toString()); + + @FXML + private TableView<ContactDetails> contactDetailsTableView; + + // The observable list to be used with the TableView + ObservableList<ContactDetails> observableContactsList; + // The address book to be used to store the contact details in. + private AddressBook addressBook; + + /** + * This method will be called by the FXML-loader after having loaded + * all the GUI-components and initialized them. + */ + @FXML + private void initialize() { + this.observableContactsList = FXCollections.observableArrayList(); + this.addressBook = new AddressBookDBHandler(); + //this.addressBook = new AddressBookPlain(); + this.observableContactsList.setAll(this.addressBook.getAllContacts()); + this.contactDetailsTableView.setItems(observableContactsList); + } + + /** + * Updates the ObservableArray wrapper with the current content in the + * Literature register. Call this method whenever changes are made to the + * underlying LiteratureRegister. + * + * @param addressBook the address book to use for updating the observable list + */ + public void updateObservableList(AddressBook addressBook) { + this.observableContactsList.setAll(addressBook.getAllContacts()); + } + + /** + * Display the input dialog to get input to create a new Contact. + * If the user confirms creating a new contact, a new instance + * of ContactDetails is created and added to the AddressBook provided. + * + * @param actionEvent the actionevent triggering this call + */ + public void addContact(ActionEvent actionEvent) { + ContactDetailsDialog contactsDialog = new ContactDetailsDialog(); + Optional<ContactDetails> result = contactsDialog.showAndWait(); + + if (result.isPresent()) { + ContactDetails newContactDetails = result.get(); + this.addressBook.addContact(newContactDetails); + this.updateObservableList(this.addressBook); + } + } + + /** + * Import contacts from a .CSV-file chosen by the user. + * + * @param actionEvent the actionevent triggering this call + */ + public void importFromCSV(ActionEvent actionEvent) { + 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(this.addressBook, file); + this.updateObservableList(this.addressBook); + } 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(); + } + } + } + + /** + * Export all contacts in the address book to a CSV-file specified by the user. + * + * @param actionEvent the actionevent triggering this call + */ + public void exportToCSV(ActionEvent actionEvent) { + 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(this.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(); + } + } + } + + /** + * Exit the application. Displays a confirmation dialog. + * + * @param actionEvent the actionevent triggering this call + */ + public void exitApplication(ActionEvent actionEvent) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Confirmation Dialog"); + alert.setHeaderText("Exit Application ?"); + alert.setContentText("Are you sure you want to exit this application?"); + + Optional<ButtonType> result = alert.showAndWait(); + + if (result.isPresent() && (result.get() == ButtonType.OK)) { + // ... user choose OK + Platform.exit(); + } + } + + /** + * Removes the Contact selected in the table. If no Contact is + * selected, nothing is deleted, and the user is informed that he/she must + * select which Contact to delete. + * + * @param actionEvent the actionevent triggering this call + */ + public void removeContact(ActionEvent actionEvent) { + ContactDetails selectedContact = this.contactDetailsTableView + .getSelectionModel() + .getSelectedItem(); + if (selectedContact == null) { + showPleaseSelectItemDialog(); + } else { + if (showDeleteConfirmationDialog()) { + this.addressBook.removeContact(selectedContact.getPhone()); + this.updateObservableList(this.addressBook); + } + } + } + + /** + * Edit the selected item. + * + * @param actionEvent the actionevent triggering this call + */ + public void editContact(ActionEvent actionEvent) { + ContactDetails selectedContact = this.contactDetailsTableView + .getSelectionModel() + .getSelectedItem(); + if (selectedContact == null) { + showPleaseSelectItemDialog(); + } else { + ContactDetailsDialog contactDialog = new ContactDetailsDialog(selectedContact, true); + contactDialog.showAndWait(); + + // The selectedContact now contains the updates. If the address book is + // stored in memory (implemented using one of the collection classes in the JDK + // or similar), we do not need to tell the address book that this instance + // has been changed. But if a DB is being used, the updated details + // must be stored/updeted in the DB too. Hence lets treat this change + // independent upon the address book being "in memory" or in a DB by + // calling the update()-method of the address book + this.addressBook.updateContact(selectedContact); + + this.updateObservableList(this.addressBook); + } + } + + + /** + * Displays an example of an alert (info) dialog. In this case an "about" + * type of dialog. + * + * @param actionEvent the actionevent triggering this call + */ + public void showAboutDialog(ActionEvent actionEvent) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Information Dialog - About"); + alert.setHeaderText("Contacts Register\nv" + AppVersion.VERSION); + alert.setContentText("A brilliant application created by\n" + + "(C)Arne Styve\n" + + "2020-03-16"); + + alert.showAndWait(); + } + + /** + * Displays a login dialog using a custom dialog. + * Just to demonstrate the {@link javafx.scene.control.PasswordField}-control. + * + * @param actionEvent the actionevent triggering this call + */ + public void showLoginDialog(ActionEvent actionEvent) { + // Create the custom dialog. + Dialog<Pair<String, String>> dialog = new Dialog<>(); + dialog.setTitle("Login Dialog"); + dialog.setHeaderText("Look, a Custom Login Dialog"); + + // Set the button types. + ButtonType loginButtonType = new ButtonType("Login", ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll(loginButtonType, ButtonType.CANCEL); + + // Create the username and password labels and fields. + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + TextField username = new TextField(); + username.setPromptText("Username"); + + PasswordField password = new PasswordField(); + password.setPromptText("Password"); + + grid.add(new Label("Username:"), 0, 0); + grid.add(username, 1, 0); + grid.add(new Label("Password:"), 0, 1); + grid.add(password, 1, 1); + + // Enable/Disable login button depending on whether a username was entered. + Node loginButton = dialog.getDialogPane().lookupButton(loginButtonType); + loginButton.setDisable(true); + + // Do some validation . + username.textProperty().addListener((observable, oldValue, newValue) -> + loginButton.setDisable(newValue.trim().isEmpty())); + + dialog.getDialogPane().setContent(grid); + + // Request focus on the username field by default. + Platform.runLater(username::requestFocus); + + // 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; + }); + + Optional<Pair<String, String>> result = dialog.showAndWait(); + + result.ifPresent( + usernamePassword -> logger.log(Level.INFO, () -> "Username=" + usernamePassword.getKey() + + ", Password=" + usernamePassword.getValue())); + } + + /** + * Displays a warning informing the user that an item must be selected from + * the table. + */ + public void showPleaseSelectItemDialog() { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Information"); + alert.setHeaderText("No items selected"); + alert.setContentText("No item is selected from the table.\n" + + "Please select an item from the table."); + + alert.showAndWait(); + } + + /** + * Displays a delete confirmation dialog. If the user confirms the delete, + * <code>true</code> is returned. + * + * @return <code>true</code> if the user confirms the delete + */ + public boolean showDeleteConfirmationDialog() { + boolean deleteConfirmed = false; + + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Delete confirmation"); + alert.setHeaderText("Delete confirmation"); + alert.setContentText("Are you sure you want to delete this item?"); + + Optional<ButtonType> result = alert.showAndWait(); + + if (result.isPresent()) { + deleteConfirmed = (result.get() == ButtonType.OK); + } + return deleteConfirmed; + } +} diff --git a/src/no/ntnu/idata2001/contacts/controllers/MainController.java b/src/no/ntnu/idata2001/contacts/controllers/MainController.java index dbd10f7226e5b956cbd90c10bf3f7c419b4b42af..48d1423b5b972388d9dcb955add8ed20777e0a05 100644 --- a/src/no/ntnu/idata2001/contacts/controllers/MainController.java +++ b/src/no/ntnu/idata2001/contacts/controllers/MainController.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Platform; +import javafx.event.ActionEvent; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.control.Alert; @@ -18,9 +19,11 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.stage.FileChooser; import javafx.util.Pair; +import no.ntnu.idata2001.contacts.AppVersion; import no.ntnu.idata2001.contacts.model.AddressBook; import no.ntnu.idata2001.contacts.model.AddressBookDBHandler; import no.ntnu.idata2001.contacts.model.AddressBookFileHandler; +import no.ntnu.idata2001.contacts.model.AddressBookPlain; import no.ntnu.idata2001.contacts.model.ContactDetails; import no.ntnu.idata2001.contacts.views.ContactDetailsDialog; import no.ntnu.idata2001.contacts.views.ContactsApp; @@ -38,6 +41,8 @@ import no.ntnu.idata2001.contacts.views.ContactsApp; */ public class MainController { private final Logger logger; + private AddressBook addressBook; + private ContactsApp parentView; // The parent View (GUI) class using this controller // File used for object serialization. Used by the methods // saveAddressBookToFile() and loadAddressBookFromFile() @@ -46,22 +51,41 @@ public class MainController { /** * Creates an instance of the MainController class, initialising * the logger. + * + * @param parentView the view object using this instance as the controller + * in the MVC-pattern. */ - public MainController() { + public MainController(ContactsApp parentView) { this.logger = Logger.getLogger(getClass().toString()); + this.parentView = parentView; } + /** + * Initializes the controller. Called by the GUI-component after the GUI + * has been created. + */ + public void initialize() { + // Use the AddressBookDBHandler as AddressBook implementation + this.addressBook = new AddressBookDBHandler(); + //this.addressBook = new AddressBookPlain(); + } + + /** + * Returns the address book. + * @return the address book. + */ + public AddressBook getAddressBook() { + return this.addressBook; + } /** * Display the input dialog to get input to create a new Contact. * If the user confirms creating a new contact, a new instance * 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 actionEvent the actionevent triggering this call */ - public void addContact(AddressBook addressBook, ContactsApp parent) { + public void addContact(ActionEvent actionEvent) { ContactDetailsDialog contactsDialog = new ContactDetailsDialog(); @@ -69,8 +93,8 @@ public class MainController { if (result.isPresent()) { ContactDetails newContactDetails = result.get(); - addressBook.addContact(newContactDetails); - parent.updateObservableList(); + this.addressBook.addContact(newContactDetails); + this.parentView.updateObservableList(this.addressBook); } } @@ -88,7 +112,16 @@ public class MainController { ContactDetailsDialog contactDialog = new ContactDetailsDialog(selectedContact, true); contactDialog.showAndWait(); - parent.updateObservableList(); + // The selectedContact now contains the updates. If the address book is + // stored in memory (implemented using one of the collection classes in the JDK + // or similar), we do not need to tell the address book that this instance + // has been changed. But if a DB is being used, the updated details + // must be stored/updeted in the DB too. Hence lets treat this change + // independent upon the address book being "in memory" or in a DB by + // calling the update()-method of the address book + this.addressBook.updateContact(selectedContact); + + parent.updateObservableList(this.addressBook); } } @@ -99,31 +132,22 @@ public class MainController { * * @param selectedContact the Contact to delete. If no Contact has been selected, * 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. */ - public void deleteContact(ContactDetails selectedContact, - AddressBook addressBook, - ContactsApp parent) { + public void deleteContact(ContactDetails selectedContact) { if (selectedContact == null) { showPleaseSelectItemDialog(); } else { if (showDeleteConfirmationDialog()) { - addressBook.removeContact(selectedContact.getPhone()); - parent.updateObservableList(); + this.addressBook.removeContact(selectedContact.getPhone()); + this.parentView.updateObservableList(this.addressBook); } } } - /** * 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) { + public void importFromCsv() { FileChooser fileChooser = new FileChooser(); // Set extension filter for .csv-file @@ -135,8 +159,8 @@ public class MainController { File file = fileChooser.showOpenDialog(null); if (file != null) { try { - AddressBookFileHandler.importFromCsv(addressBook, file); - parent.updateObservableList(); + AddressBookFileHandler.importFromCsv(this.addressBook, file); + this.parentView.updateObservableList(this.addressBook); } catch (IOException ioe) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("File Import Error"); @@ -149,10 +173,8 @@ public class MainController { /** * 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) { + public void exportToCsv() { FileChooser fileChooser = new FileChooser(); // Set extension filter for .csv-file @@ -164,7 +186,7 @@ public class MainController { File file = fileChooser.showSaveDialog(null); if (file != null) { try { - AddressBookFileHandler.exportToCsv(addressBook, file); + AddressBookFileHandler.exportToCsv(this.addressBook, file); } catch (IOException ioe) { Alert alert = new Alert(Alert.AlertType.ERROR); alert.setTitle("File Export Error"); @@ -206,15 +228,6 @@ public class MainController { return AddressBookFileHandler.loadFromFile(inFile); } - /** - * Loads an entire AddressBook from a database. - * - * @return an address book populated by contact details loaded from the database. - */ - public AddressBook loadAddressBookFromDB() { - return new AddressBookDBHandler(); - } - /** * Exit the application. Displays a confirmation dialog. */ @@ -236,13 +249,11 @@ public class MainController { /** * Displays an example of an alert (info) dialog. In this case an "about" * type of dialog. - * - * @param version the version of the application, to be displayed in the dialog. */ - public void showAboutDialog(String version) { + public void showAboutDialog() { Alert alert = new Alert(Alert.AlertType.INFORMATION); alert.setTitle("Information Dialog - About"); - alert.setHeaderText("Contacts Register\nv" + version); + alert.setHeaderText("Contacts Register\nv" + AppVersion.VERSION); alert.setContentText("A brilliant application created by\n" + "(C)Arne Styve\n" + "2020-03-16"); @@ -306,7 +317,7 @@ public class MainController { Optional<Pair<String, String>> result = dialog.showAndWait(); result.ifPresent( - usernamePassword -> logger.log(Level.INFO, "Username=" + usernamePassword.getKey() + usernamePassword -> logger.log(Level.INFO, () -> "Username=" + usernamePassword.getKey() + ", Password=" + usernamePassword.getValue())); } diff --git a/src/no/ntnu/idata2001/contacts/model/AddressBook.java b/src/no/ntnu/idata2001/contacts/model/AddressBook.java index b3dfc5c08f9d06d795f7abd369aee8a7fafe6d0e..8a79111921f247022734d83ba27fcc9cff20a28d 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBook.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBook.java @@ -25,6 +25,13 @@ public interface AddressBook extends Serializable, Iterable<ContactDetails> { */ void removeContact(String phoneNumber); + /** + * Update the contact info for the given contact. + * + * @param contactDetails the contact details to update. + */ + void updateContact(ContactDetails contactDetails); + /** * Returns all the contacts as a collection. * diff --git a/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java b/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java index 4cab41543369befcd606bf93304f602bc9e783f7..eed0e57d986a63529babcbb9cc40c14d150c4e4f 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBookDBHandler.java @@ -46,8 +46,8 @@ public class AddressBookDBHandler implements AddressBook { */ public AddressBookDBHandler() { this.efact = Persistence.createEntityManagerFactory("contacts-pu"); -// this.efact = Persistence.createEntityManagerFactory("contacts-mysql-pu"); -// this.efact = Persistence.createEntityManagerFactory("contacts-localdb-pu"); + // this.efact = Persistence.createEntityManagerFactory("contacts-mysql-pu"); + // this.efact = Persistence.createEntityManagerFactory("contacts-localdb-pu"); } @@ -58,6 +58,7 @@ 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 = " @@ -92,6 +93,25 @@ public class AddressBookDBHandler implements AddressBook { } + @Override + public void updateContact(ContactDetails contactDetails) { + EntityManager eman = this.efact.createEntityManager(); + eman.getTransaction().begin(); + + // First find the contact details matching the unique id of the + // given contact (cannot use the phone number, since that + // might have changed....) + ContactDetails contactToUpdate = eman.find(ContactDetails.class, contactDetails.getId()); + + if (contactToUpdate != null) { + // Use the updateFrom() method of the ContactDetails-class to + // update the entity in the DB with the data from the parameter contactDetails + contactToUpdate.updateFrom(contactDetails); + eman.getTransaction().commit(); + } + eman.close(); + } + @Override public Collection<ContactDetails> getAllContacts() { EntityManager eman = this.efact.createEntityManager(); @@ -104,7 +124,6 @@ public class AddressBookDBHandler implements AddressBook { 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/AddressBookFileHandler.java b/src/no/ntnu/idata2001/contacts/model/AddressBookFileHandler.java index 911541e0f2d0e87f414cffb5fe58ddbce2b19161..c7863022f163928c926af0d6d0ed97fda19ffce7 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBookFileHandler.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBookFileHandler.java @@ -103,7 +103,7 @@ public class AddressBookFileHandler { * @throws ContactDetailsFormatException if the string cannot be parsed as ContactDetails */ private static ContactDetails parseStringAsContactDetails(String line) { - ContactDetails contact = null; + ContactDetails contact; String[] subStrings = line.split(CSV_DELIMITER); if (subStrings.length == 3) { contact = new ContactDetails(subStrings[0], subStrings[1], subStrings[2]); @@ -152,7 +152,7 @@ public class AddressBookFileHandler { ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); return (AddressBook) objectInputStream.readObject(); } catch (IOException | ClassNotFoundException e) { - logger.log(Level.INFO, "Could not open file " + logger.log(Level.INFO, () -> "Could not open file " + inFile.getName() + ". An empty AddressBook was returned."); return new AddressBookPlain(); } diff --git a/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java b/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java index 02603d1b90273272f5674f4f10ab6ad48b24c670..19827b32d643d6f26743aaa835b31e07514dcb59 100644 --- a/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java +++ b/src/no/ntnu/idata2001/contacts/model/AddressBookPlain.java @@ -1,5 +1,6 @@ package no.ntnu.idata2001.contacts.model; +import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.TreeMap; @@ -9,7 +10,10 @@ import java.util.TreeMap; * Based on the example in the book "Objects first with Java" by David J. Barnes * and Michael Kölling. * - * <p>Each contact is stored in a TreeMap using the phone number as the key. + * <p>Each contact is stored in an ArrayList. We could have used + * a HasMap or a TreeMap using the phone number as the key, but since the phone + * number might change for a given contact, it is not advisable to use the + * phone number as a key, and hence no need to use a Map to store the contact details. * * @author David J. Barnes and Michael Kölling and Arne Styve * @version 2020.03.16 @@ -23,13 +27,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 final TreeMap<String, ContactDetails> book; + private final ArrayList<ContactDetails> book; /** * Creates an instance of the AddressBook, initialising the instance. */ public AddressBookPlain() { - this.book = new TreeMap<>(); + this.book = new ArrayList<>(); } /** @@ -39,18 +43,53 @@ public class AddressBookPlain implements AddressBook { */ public void addContact(ContactDetails contact) { if (contact != null) { - book.put(contact.getPhone(), contact); + book.add(contact); } } /** - * Remove the contact with the given phonenumber from the address book. + * Remove the contact with the given phone number from the address book. * The phone number should be one that is currently in use. * * @param phoneNumber The phone number to the contact to remove */ public void removeContact(String phoneNumber) { - this.book.remove(phoneNumber); + // First find the contact details with the given phone number + ContactDetails contactDetails = findContactByPhone(phoneNumber); + if (contactDetails != null) { + this.book.remove(contactDetails); + } + } + + /** + * Finds the contact details matching the given phone number. + * If no match is found, <code>null</code> is returned. + * @param phoneNumber the phone number to search for + * @return the contact details matching the phone number provided. + * If no match, <code>null</code> is returned. + */ + private ContactDetails findContactByPhone(String phoneNumber) { + ContactDetails foundContact = null; + for (ContactDetails contactDetails : this.book) { + if (contactDetails.getPhone().equals(phoneNumber)) { + foundContact = contactDetails; + } + } + return foundContact; + } + + @Override + public void updateContact(ContactDetails contactDetails) { + // There is really no need to do anything with regards + // to the ArrayList storing the contact details, as + // long as the contactDetaisl-object provided in the + // parameter is still within the ArrayList. + // But to be sure, we will verify that the object IS in the + // ArrayList, and if not, we will add it, just in case. + + if (!this.book.contains(contactDetails)) { + this.book.add(contactDetails); + } } /** @@ -59,7 +98,7 @@ public class AddressBookPlain implements AddressBook { * @return all the contacts as a collection. */ public Collection<ContactDetails> getAllContacts() { - return this.book.values(); + return this.book; } @Override @@ -69,6 +108,6 @@ public class AddressBookPlain implements AddressBook { @Override public Iterator<ContactDetails> iterator() { - return this.book.values().iterator(); + return this.book.iterator(); } } diff --git a/src/no/ntnu/idata2001/contacts/model/ContactDetails.java b/src/no/ntnu/idata2001/contacts/model/ContactDetails.java index 02689213a7ccc2d04b8839d572f60de81542ee5e..92e99ad8a1813796fbe22933fcf6654e73ad2403 100644 --- a/src/no/ntnu/idata2001/contacts/model/ContactDetails.java +++ b/src/no/ntnu/idata2001/contacts/model/ContactDetails.java @@ -31,7 +31,7 @@ public class ContactDetails implements Comparable<ContactDetails>, Serializable * @param address The address. * @throws IllegalArgumentException if any of the parameters are invalid. */ - public ContactDetails(String name, String phone, String address) throws IllegalArgumentException{ + public ContactDetails(String name, String phone, String address) { // A good recommended practice is to always use set-methods in the // constructors, and to make sure that the set-methods performs the // necessary validity-checks on the parameter. @@ -50,6 +50,24 @@ public class ContactDetails implements Comparable<ContactDetails>, Serializable // Intentionally left empty. } + /** + * Updates the contact details from another instance of contact details. + * @param contactDetails the contact details to use for updating this instance. + */ + public void updateFrom(ContactDetails contactDetails) { + this.setAddress(contactDetails.getAddress()); + this.setName(contactDetails.getName()); + this.setPhone(contactDetails.getPhone()); + } + + /** + * Returns the unique ID of the contact details instance. + * @return the unique ID of the contact details instance. + */ + public Integer getId() { + return this.id; + } + /** * Returns the name. * diff --git a/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java b/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java index a9c4df5ab4b66f7d713e4c59fb2c13538903cabf..fe9fd5278c79f66c09f5e8905a41fb07c0661266 100644 --- a/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java +++ b/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java @@ -117,7 +117,7 @@ public class ContactDetailsDialog extends Dialog<ContactDetails> { TextField phoneNumber = new TextField(); phoneNumber.setPromptText("Phone number"); - // Fill inn data from the provided Newspaper, if not null. + // Fill inn data from the provided contact details, if not null. if ((mode == Mode.EDIT) || (mode == Mode.INFO)) { name.setText(existingContact.getName()); address.setText(existingContact.getAddress()); diff --git a/src/no/ntnu/idata2001/contacts/views/ContactsApp.java b/src/no/ntnu/idata2001/contacts/views/ContactsApp.java index 451ea7319008d226eaeee50e3a1412b05e3080d7..2e3f8eea9fafee39cc87179bc522918c013a2237 100644 --- a/src/no/ntnu/idata2001/contacts/views/ContactsApp.java +++ b/src/no/ntnu/idata2001/contacts/views/ContactsApp.java @@ -28,6 +28,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.stage.Stage; +import no.ntnu.idata2001.contacts.AppVersion; import no.ntnu.idata2001.contacts.controllers.MainController; import no.ntnu.idata2001.contacts.model.AddressBook; import no.ntnu.idata2001.contacts.model.ContactDetails; @@ -36,11 +37,7 @@ import no.ntnu.idata2001.contacts.model.ContactDetails; * The main window/application of the contacts app. */ public class ContactsApp extends Application { - - private static final String VERSION = "0.5.1"; - private MainController mainController; - private AddressBook addressBook; // The JavaFX ObservableListWrapper used to connect tot he underlying AddressBook private ObservableList<ContactDetails> addressBookListWrapper; @@ -72,10 +69,8 @@ public class ContactsApp extends Application { super.init(); // Initialise the main controller - this.mainController = new MainController(); - - // Initialise the Address Book from a file - this.addressBook = this.mainController.loadAddressBookFromDB(); + this.mainController = new MainController(this); + this.mainController.initialize(); } @Override @@ -103,9 +98,12 @@ public class ContactsApp extends Application { Scene scene = new Scene(root, 600, 500); // Set title of the stage (window) and add the scene - primaryStage.setTitle("Contacts v" + VERSION); + primaryStage.setTitle("Contacts v" + AppVersion.VERSION); primaryStage.setScene(scene); + // And initialize the controller + this.mainController.initialize(); + // Finally, make the stage (window) visible primaryStage.show(); } @@ -171,9 +169,11 @@ public class ContactsApp extends Application { * Updates the ObservableArray wrapper with the current content in the * Literature register. Call this method whenever changes are made to the * underlying LiteratureRegister. + * + * @param addressBook the address book to use for updating the observable list */ - public void updateObservableList() { - this.addressBookListWrapper.setAll(this.addressBook.getAllContacts()); + public void updateObservableList(AddressBook addressBook) { + this.addressBookListWrapper.setAll(addressBook.getAllContacts()); } /** @@ -184,7 +184,7 @@ public class ContactsApp extends Application { private ObservableList<ContactDetails> getAddressBookListWrapper() { // Create an ObservableArrayList wrapping the LiteratureRegister addressBookListWrapper - = FXCollections.observableArrayList(this.addressBook.getAllContacts()); + = FXCollections.observableArrayList(this.mainController.getAddressBook().getAllContacts()); return addressBookListWrapper; } @@ -219,7 +219,7 @@ public class ContactsApp extends Application { new ImageView( new Image(getClass().getResource("./icons/add_contact@2x.png").toExternalForm()))); - addContactBtn.setOnAction(actionEvent -> mainController.addContact(this.addressBook, this)); + addContactBtn.setOnAction(actionEvent -> mainController.addContact(actionEvent)); // Add the edit contact-button in the toolbar Button editContactBtn = new Button(); @@ -237,8 +237,7 @@ public class ContactsApp extends Application { deleteContactBtn.setOnAction(event -> mainController.deleteContact( - this.contactDetailsTableView.getSelectionModel().getSelectedItem(), - this.addressBook, this)); + this.contactDetailsTableView.getSelectionModel().getSelectedItem())); //Add the Buttons to the ToolBar. @@ -258,11 +257,11 @@ public class ContactsApp extends Application { Menu menuFile = new Menu("File"); MenuItem importFromCsv = new MenuItem("Import from .CSV..."); - importFromCsv.setOnAction(event -> mainController.importFromCsv(this.addressBook, this)); + importFromCsv.setOnAction(event -> mainController.importFromCsv()); menuFile.getItems().add(importFromCsv); MenuItem exportToCsv = new MenuItem("Export to .CSV..."); - exportToCsv.setOnAction(event -> mainController.exportToCsv(this.addressBook)); + exportToCsv.setOnAction(event -> mainController.exportToCsv()); menuFile.getItems().add(exportToCsv); // Add a separator line before Exit @@ -275,7 +274,7 @@ public class ContactsApp extends Application { // ----- The Help-menu ------ Menu menuHelp = new Menu("Help"); MenuItem about = new MenuItem("About"); - about.setOnAction(event -> mainController.showAboutDialog(VERSION)); + about.setOnAction(event -> mainController.showAboutDialog()); menuHelp.getItems().add(about); // ----- The LogIn-menu ------ @@ -296,7 +295,7 @@ public class ContactsApp extends Application { addContactMenu.setGraphic(addContactView); addContactMenu.setAccelerator(new KeyCodeCombination(KeyCode.A)); - addContactMenu.setOnAction(event -> mainController.addContact(this.addressBook, this)); + addContactMenu.setOnAction(event -> mainController.addContact(event)); Menu menuEdit = new Menu("Edit"); menuEdit.getItems().add(addContactMenu); @@ -328,8 +327,9 @@ public class ContactsApp extends Application { removeContactMenu.setAccelerator(new KeyCodeCombination(KeyCode.DELETE)); removeContactMenu.setOnAction(event -> mainController.deleteContact( - this.contactDetailsTableView.getSelectionModel() - .getSelectedItem(), this.addressBook, this)); + this.contactDetailsTableView + .getSelectionModel() + .getSelectedItem())); menuEdit.getItems().add(removeContactMenu); diff --git a/src/no/ntnu/idata2001/contacts/views/ContactsAppFXML.css b/src/no/ntnu/idata2001/contacts/views/ContactsAppFXML.css new file mode 100644 index 0000000000000000000000000000000000000000..118615a0e40eb1ed8a0d4c1d73b9c57d39add38a --- /dev/null +++ b/src/no/ntnu/idata2001/contacts/views/ContactsAppFXML.css @@ -0,0 +1,37 @@ +/*Icons to the menu items.*/ +.addContact > .label { + -fx-graphic: url("./icons/add_contact.png"); +} +/*Avoid icon on the accelerator text*/ +.addContact > .accelerator-text{ + -fx-graphic: none; +} + +.editContact > .label { + -fx-graphic: url("./icons/edit_contact.png"); +} + +.editContact > .accelerator-text{ + -fx-graphic: none; +} + +.removeContact > .label { + -fx-graphic: url("./icons/remove_contact.png"); +} + +.removeContact > .accelerator-text{ + -fx-graphic: none; +} + +/*Icons to the button items.*/ +.addContactBtn { + -fx-graphic: url("./icons/add_contact.png"); +} + +.editContactBtn { + -fx-graphic: url("./icons/edit_contact.png"); +} + +.removeContactBtn { + -fx-graphic: url("./icons/remove_contact.png"); +} \ No newline at end of file diff --git a/src/no/ntnu/idata2001/contacts/views/ContactsAppFXML.java b/src/no/ntnu/idata2001/contacts/views/ContactsAppFXML.java new file mode 100644 index 0000000000000000000000000000000000000000..3ccc8f2e5872031589d55e99c744adde32938f27 --- /dev/null +++ b/src/no/ntnu/idata2001/contacts/views/ContactsAppFXML.java @@ -0,0 +1,34 @@ +package no.ntnu.idata2001.contacts.views; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.stage.Stage; + +public class ContactsAppFXML extends Application { + + /** + * The main entry point for the application, launching the + * JavaFX app. + * @param args command line arguments + */ + public static void main(String[] args) { + launch(args); + } + + @Override + public void start(Stage primaryStage) throws Exception { + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("ContactsMain.fxml")); + + Parent root = fxmlLoader.load(); + + primaryStage.setTitle("Hello World"); + Scene scene = new Scene(root, 300, 275); + scene.getStylesheets().add(ContactsAppFXML.class + .getResource("ContactsAppFXML.css") + .toExternalForm()); + primaryStage.setScene(scene); + primaryStage.show(); + } +} diff --git a/src/no/ntnu/idata2001/contacts/views/ContactsMain.fxml b/src/no/ntnu/idata2001/contacts/views/ContactsMain.fxml new file mode 100644 index 0000000000000000000000000000000000000000..6ccdd22bf7548608914381d84194272278ec3358 --- /dev/null +++ b/src/no/ntnu/idata2001/contacts/views/ContactsMain.fxml @@ -0,0 +1,94 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.control.Button?> +<?import javafx.scene.control.Menu?> +<?import javafx.scene.control.MenuBar?> +<?import javafx.scene.control.MenuItem?> +<?import javafx.scene.control.SeparatorMenuItem?> +<?import javafx.scene.control.TableColumn?> +<?import javafx.scene.control.TableView?> +<?import javafx.scene.control.ToolBar?> +<?import javafx.scene.control.cell.PropertyValueFactory?> +<?import javafx.scene.layout.BorderPane?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.layout.VBox?> +<?import javafx.scene.text.Text?> + +<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" + prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.111" + xmlns:fx="http://javafx.com/fxml/1" + fx:controller="no.ntnu.idata2001.contacts.controllers.ContactsAppFXMLController"> + <top> + <VBox> + <children> + <MenuBar> + <menus> + <Menu mnemonicParsing="false" text="File"> + <items> + <MenuItem mnemonicParsing="false" onAction="#importFromCSV" text="Import from .CSV..." /> + <MenuItem mnemonicParsing="false" onAction="#exportToCSV" text="Export to .CSV..." /> + <SeparatorMenuItem mnemonicParsing="false" /> + <MenuItem mnemonicParsing="false" onAction="#exitApplication" text="Exit" /> + </items> + </Menu> + <Menu mnemonicParsing="false" text="Edit"> + <items> + <MenuItem accelerator="A" mnemonicParsing="false" onAction="#addContact" styleClass="addContact" text="Add new Contact..." /> + <MenuItem accelerator="E" mnemonicParsing="false" onAction="#editContact" styleClass="editContact" text="Edit Selected Contact..." /> + <MenuItem accelerator="D" mnemonicParsing="false" onAction="#removeContact" styleClass="removeContact" text="Remove Selected Contact..." /> + </items> + </Menu> + <Menu mnemonicParsing="false" text="Help"> + <items> + <MenuItem mnemonicParsing="false" onAction="#showAboutDialog" text="About" /> + </items> + </Menu> + <Menu mnemonicParsing="false" text="Admin"> + <items> + <MenuItem mnemonicParsing="false" onAction="#showLoginDialog" text="Log In..." /> + </items> + </Menu> + </menus> + </MenuBar> + <ToolBar prefHeight="40.0" prefWidth="200.0"> + <items> + <Button mnemonicParsing="false" onAction="#addContact" styleClass="addContactBtn" /> + <Button mnemonicParsing="false" onAction="#editContact" styleClass="editContactBtn" /> + <Button mnemonicParsing="false" onAction="#removeContact" styleClass="removeContactBtn" /> + </items> + </ToolBar> + </children> + </VBox> + </top> + <bottom> + <HBox style="-fx-background-color: #999999;" BorderPane.alignment="CENTER"> + <children> + <Text text="Status: OK" /> + </children> + </HBox> + </bottom> + <center> + <TableView fx:id="contactDetailsTableView" > + <columns> + <TableColumn maxWidth="-1.0" minWidth="200.0" prefWidth="-1.0" text="Name"> + <cellValueFactory> + <PropertyValueFactory property="name" /> + </cellValueFactory> + </TableColumn> + <TableColumn maxWidth="-1.0" minWidth="200.0" prefWidth="-1.0" text="Address"> + <cellValueFactory> + <PropertyValueFactory property="address" /> + </cellValueFactory> + </TableColumn> + <TableColumn maxWidth="-1.0" minWidth="200.0" prefWidth="-1.0" text="Phone"> + <cellValueFactory> + <PropertyValueFactory property="phone" /> + </cellValueFactory> + </TableColumn> + </columns> + <columnResizePolicy> + <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" /> + </columnResizePolicy> + </TableView> + </center> +</BorderPane> diff --git a/test/no/ntnu/idata2001/contacts/model/ContactDetailsTest.java b/test/no/ntnu/idata2001/contacts/model/ContactDetailsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..1755c31debe63658577b52928be6a45852a13bff --- /dev/null +++ b/test/no/ntnu/idata2001/contacts/model/ContactDetailsTest.java @@ -0,0 +1,23 @@ +package no.ntnu.idata2001.contacts.model; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +public class ContactDetailsTest { + + @Before + public void setUp() throws Exception { + } + + @After + public void tearDown() throws Exception { + } + + @Test + public void setName() { + } +}