By: CS2113-T14-1 Since: Sept 2019 Licence: MIT
Welcome to the Dr. Duke developer guide! This document assumes a familiarity with the user guide.
In order to be a successful Dr. Duke developer, you need a general understanding of:
-
Dr. Duke's architecture and object model
-
The Java framework which Dr. Duke builds on
-
JDK
11or above -
IntelliJ IDE
NoteIntelliJ by default has Gradle and JavaFx plugins installed.
Do not disable them. If you have disabled them, go toFile>Settings>Pluginsto re-enable them.
-
Fork this repo, and clone the fork to your computer
-
Open IntelliJ (if you are not in the welcome screen, click
File>Close Projectto close the existing project dialog first) -
Set up the correct JDK version for Gradle
-
Click
Configure>Project Defaults>Project Structure -
Click
New…and find the directory of the JDK
-
-
Click
Import Project -
Locate the
build.gradlefile and select it. ClickOK -
Click
Open as Project -
Click
OKto accept the default settings -
Open a console and run the command
gradlew processResources(Mac/Linux:./gradlew processResources). It should finish with theBUILD SUCCESSFULmessage.
This will generate all resources required by the application and tests.
The Architecture Diagram shown above explains the high-level design of Dr. Duke.
This class diagram aptly describes the relationships between the various core classes in the UI component.
The UI component uses the JavaFX UI framework. The layout of these UI elements are defined in matching .fxml files that are in the src/main/resources/view folder. For example, the layout of the MainWindow is specified in MainWindow.fxml.
The UI component is exposed to external modules/components via the Ui interface. The UiManager implements this interface and acts as the manager of the UI component. Hence, UiManager holds a reference to the MainWindow (the primary UI window that houses the other UI elements that the application will use).
As mentioned, the UI is made up of a MainWindow that houses various UI elements such as CommandWindow, HomeWindow, PatientWindow, etc. These UI elements, including MainWindow extends from the abstract class UiElement. This abstract class serves as the parent class for all of the UI elements used in the application, and allows for their easy creation. In addtion, the MainWindow holds a reference of the UiContext that exposes the current Context of the application. This is required as the Context of the application determines what UI window is shown, i.e. HomeWindow for Home context, PatientWindow for Patient context.
This class diagram describes the relationships between the various core classes involved in parsing the user’s input into Commands. The first word (delimited by a space or newline) of the user’s input is the command name. All commands extend the Command abstract class, which provides enough functionality for basic commands consisting of a single word. The operation of the Command is specified in the execute method. The mapping from the command name to the Command should be created in the Commands class, which is loaded by the default Parser constructor, together with a reference to a Context enum (ordinarily, a reference to the context field in the DukeCore instance). A Parser can also be constructed with a subclass of Commands to specify a different set of commands.
The Commands class has a single function getCommand(), which takes, as arguments, a String that should uniquely identify the requested Command within a particular Context, and a Context enum representing the context from which the Command was called. It constructs and returns a new instance of the Command object thus identified.
If a Command has no arguments, the newly constructed instance is returned without further processing. If it takes any arguments, it extends ArgCommand. Each ArgCommand is associated with an ArgSpec singleton, whose private constructor sets the parameters of the ArgCommand: emptyArgMsg (the error message when no argument is supplied), cmdArgLevel (an ArgLevel enum indicating whether an argument for the command is necessary, optional, or forbidden) and the data structures switchMap and switchAliases, generated by the switchInit() function. The switchInit() function takes a vararg of Switch objects, which should specify the switches for the particular Command.
switchMap maps the full name of a switch to a Switch object, describing its properties, and switchAliases maps aliases to the full name of the switch they represent. An alias is a string that, when provided by the user as a switch, is recognised as a specific switch. For example, for the switch investigation (given as -i[nv(x|estigation)] in the User Guide) has the following aliases:
-
i -
in -
inv -
invx -
inve -
inves -
invest -
investi -
investig -
investiga -
investigat -
investigati -
investigatio -
investigation
As this would be very tedious to list manually, it is automatically generated by the switchInit() function, using the data in the Switch objects provided to it. Observe that almost all these aliases are prefixes of the word investigation, with the shortest being i. This follows from the requirement that the switch can be recognised as long as the user has input enough characters for it to be unambiguous. Let i in this example be the root, the shortest unambiguous part of the full name of the switch. Then, every prefix of the word investigation starting from the root is an alias of the switch investigation. All aliases of this form are generated by a loop in switchInit(), from the root and the full name in the Switch object. Any additional aliases can be supplied via the aliases vararg in the Switch constructor. Refer to the Javadoc of Switch for further details on its fields.
In summary, to define a new Command:
-
Define a subclass of
Command -
Specify its execution in
execute -
Update
Commandsto link the command name to theCommand
If this is an ArgCommand, in addition to doing the above for a subclass of ArgCommand:
-
Define a subclass of
ArgSpec(by convention,<name>Specis associated with<name>Command) -
Define the private static field
specand the public static methodgetSpec()to provide singleton behaviour -
Create a private constructor for the subclass
-
Define
cmdArgLevelandemptyArgMsg -
Construct the switches for the
ArgCommandand supply them as arguments toswitchInit()-
If there are no switches, call
switchInit()with no arguments
-
-
Switch values are accessed from the ArgCommand with the getSwitchVal() method, which takes the name of a switch, as a String, as an argument, and returns the String representing the argument supplied for the switch.
|
Note
|
If there is no argument given for a switch, getSwitchVal(<switch name>) returns null. However, if a switch is not given, getSwitchVal(<switch name>) also returns null. The former case can be distinguished by the fact that switchVals will contain <switch name> as a key.
|
The Parser object scans through a user-supplied string. The first word is extracted, and if the corresponding command is an ArgCommand, it uses several nested finite state machines (FSMs) which switch on the characters in the input. Switches are extracted, using the aliases in switchAliases to identify the full names of the corresponding switches. The switch arguments are then compared against the requirements of the ArgCommand, as stored in the switchMap.
The finite state machine for input parsing has the following states:
-
EMPTY: parsing whitespace, which has no semantic meaning aside from serving as a separator -
ARG: parsing an argument that is not quoted, which may be for a switch or for the command itself -
STRING: parsing an argument that is surrounded by double quotes -
SWITCH: parsing a switch name
The state transitions on encountering would not be clearly represented on a state diagram, but can be summarised as follows:
-
EMPTY-
EMPTY→EMPTY: <Newline> or <Space> -
EMPTY→SWITCH:- -
EMPTY→STRING:" -
EMPTY→ARG: <any other character>
-
-
SWITCH-
SWITCH→EMPTY: <Newline> or <Space> -
SWITCH→SWITCH(add current switch and begin processing a new switch):- -
SWITCH→STRING(add current switch and begin parsing a string as an argument):" -
SWITCH→SWITCH: <any other character>
-
-
STRING-
STRING→EMPTY:" -
STRING→STRING: <any other character>
-
-
ARG-
ARG→EMPTY: <Newline> or <Space> -
ARG→DukeException: Unescaped"or- -
ARG→ARG: <any other character>
-
Preceding any transition character with a backslash \ will escape it, allowing it to be treated as an ordinary character.
When transitioning from EMPTY to any other state, checkInputAllowed() is used to check if input is allowed at that point. While in the ARG, STRING or SWITCH states, each character that is read is added to a StringBuilder elementBuilder. When exiting the state, the string is processed as a switch via addSwitch(), or written to the Command being constructed as an argument by writeElement(). This can be an argument for the Command itself, or a switch argument. For more details on how switches are processed, see above on Command objects, and on the Switch Autocorrect feature.
When every character in the input has been consumed, cleanup will be performed based on the state that the Parser is in at that point:
-
EMPTY: nothing is done -
ARG: callwriteElement()to write a command or switch argument -
SWITCH: calladdSwitch()to process the switch name -
STRING: callwriteElement(), assuming the user simply forgot to close the string
The Class Diagram shown above describes the relationship among the different data classes used in Dr. Duke.
The statusArr stores the textual description of each numerical value for the status.
This class diagram describes the relationship between the Storage class, GsonStorage, the patient class, Patient, and the other classes used to describe and handle patient data.
The storage/load mechanism is facilitated by GsonStorage. GsonStorage uses the Google-developed Java Library Gson 2.8.6. Gson is a library that can be used to convert Java Objects into their JSON representation. It can also be used to convert JSON representations back to the equivalent Java` Object. For more information about Gson refer to the Gson User Guide at https://github.com/google/gson/blob/master/UserGuide.md.
The JSON representations of the patients are stored in a JSON file called patients.json.
GsonStorage implements the following operations:
-
HashMap<String, Patient> loadPatientHashMap()- Loads all the patients inpatients.jsonto the hashmappatientObservableMap -
void writeJsonFile(HashMap<String, Patient> patientMap)- Creates an array containing the patients inpatientObservableMapand writes the arraysJSONrepresentation topatients.json -
String getFilePath()- returns the filepath topatients.json -
PatientMap resetAllData()- Clearspatients.jsonand returns an empty hash map
When the user boots Dr.Duke a GsonStorage and a PatientMap object is created. The method loadPatientHashmap in GsonStorage is then executed which extracts all the JSON representations of the patients in patients.json as a string. The GSON method fromJson() is then executed on the JSON representation of the patients which creates the equivalent java array contaning Patient objects. The array is iterated through and every patient is loaded into the patientObservableMap attribute of the PatientMap object.
During runtime, every new patient that is created is stored in the patientObservableMap.
When the user shuts down Dr.Duke the patientObservableMap is sent back to the GsonStorage object by calling the writeJsonFile method on the GsonSotrage object. The writeJsonFile method iterates through the patientObservableMap and places every Patient object in a java array. When all the patients are in the array the arrays JSON representation is created using the Gson method toJson(). The context of the patient.son file is then cleared and the new JSON representation of the array containing all the patients is written to the patient.json file which concludes the storage circle.
As can be seen in the class diagram, every individual’s patient’s data in nested from the Patient object representing that patient. The diagram also displays that there are no circle references. For these two reasons, using Gson to store all the data about the different patients is very convenient and effective as everything can be stored by simply creating the JSON representations of each Patient object and the rest of the nesting will be parsed automatically by the Gson source code.
If further development of Dr.Duke requires the storage of other objects that are nested from the patient objects that will be done automatically by the existing storage mechanism as long as there are no circle references. If further development requires storage of objects that are not nested from patient objects the storage mechanism needs to be updated to include two or more arrays instead of one; one containing the JSON representations of the Patient objects and the other/s containing the JSON representation of the other object/s.
This section describes some noteworthy details on how certain features in Dr. Duke are implemented.
Dr. Duke aims to assist House Officers in quick, accurate, and efficient recording and retrieval of patient data required to provide efficient care. Therefore, one of its main goals is to speed up the process at which users enter their intended commands so users can get more things done faster. There are several benefits in implementing the autocomplete feature.
-
Reduce the time taken for the user to enter a complete and valid command.
-
Reduce the frequency at which the user refers to the User Guide or help section to view the syntax of a particular command.
The autocomplete mechanism is facilitated by two main classes, namely AutoCompleteTextField and AutoCompleteModel.
AutoCompleteTextField is an element of the UI component. It extends from JavaFX TextField, and it displays a contextual
menu whenever the user enters a key character/word in the text field. In our case, a key character/word is defined as such:
a command keyword or switch ("-"). It implements the following operations.
-
AutoCompleteTextField#updateMenu()- Populates/updates the contextual menu. -
AutoCompleteTextField#displayMenu()- Shows the contextual menu. -
AutoCompleteTextField#hideMenu()- Hides the contextual menu.
AutoCompleteModel is an element of the Model component. It updates the content of the contextual menu as the user
types in the AutoCompleteTextField. The content of the contextual menu is also determined by the current context of the application.
Given below is an example usage scenario and how the autocomplete mechanism behaves at each step.
Step 1: The user launches the application. The AutoCompleteTextField in the CommandWindow is blank, and the context is Home.
The user wishes to add a patient (a sample valid command syntax is new -name "John Doe" -bed 01 -allergies "paracetamol").
Step 2: The user keys in "n" in the text field. At this point, the contextual menu appears and shows the user a list of
available commands in the Home context that matches with "n", i.e. new.
Step 3: The user then use the arrow keys (up and down) to navigate through the contextual menu and the enter key to select the appropriate item presented in the menu. The text field is then updated accordingly, and the contextual menu is subsequently hidden.
Step 4: The user proceeds to key in the switches for the new command. As the user keys in "-", the contextual menu appears once again
to show the user a list of switches associated with the new command, i.e. "-name", "-bed", "-allergies", etc.
Step 5: The user will repeat Steps 3 and 4 until a valid command syntax has been fully entered. The user will then press the enter key to execute said command.
The following activity diagram summarizes what happens when a user types in the AutoCompleteTextField.
While rapidly adding different types of patient data, it is inevitable that typing mistakes will be made. While short forms of switches are accepted in order to minimise the amount of typing that needs to be done to organise information, and therefore the risk of mistakes being made, we still need to account for the cases where they occur. An automated means of correcting the text would allow these corrections to be made as quickly as possible and with minimal effort required from the user, reducing the disruption to his workflow caused by these mistakes.
If a user-supplied switch is not an alias for any switch, this triggers the disambiguation functions in CommandHelpers. We use a modified Levenshtein-Damerau distance which takes into account the taxicab distance between keys on a standard QWERTY keyboard in weighting the cost of substitutions. Pseudocode for the Levenshtein-Damerau distance computation can be found here and ideas for implementation of keyboard distance analysis are taken from here. This provides a realistic measure of the likelihood that a particular mistake was made, as the likelihood of accidentally pressing an incorrect key is dramatically decreased if the incorrect key in question is a keyboard’s length away from one’s intended key, which is a fact that the basic Levenshtein-Damerau distance algorithm fails to capture.
The distance of the ambiguous string to every alias whose length differs from the string’s by at most 2 is calculated. Basic pruning is implemented, terminating the distance estimation computation if it exceeds the minimum distance found so far.
If there is a switch with a unique lowest distance from the input string, that switch is automatically selected, with a warning shown to the user to indicate that his input was autocorrected. If not, the user is prompted with a screen listing the closest matches, as well as all valid switches for this command. The closest matches are numbered, and the user may select one by entering its corresponding number, or he may enter another valid switch in its full form.
Taxicab distance is used as opposed to Euclidean in order to avoid computing square roots, and only the substitution cost is affected by the keyboard distance, as having missed or accidentally added a character, or typing the characters out of sequence, is not dependent on the distance between two keys.
This function is called by the parser finite state machine whenever a complete switch that does not match any alias is processed, instead of presenting all combinations of possible corrections after the whole input is parsed. This allows mistyped switches to be individually and unambiguously corrected, instead of creating a confusing combinatorical explosion of possible switches if the user makes several mistakes in a complex query, some of which may have more than two close matches for a switch if the user had used their shortened forms.
Dr. Duke aims to assist House Officers in quick, accurate recording and retrieval of patient data required to provide efficient care. Therefore, it makes sense to be able to view past history of a Patient. If the patient was previously admitted, there would be numerous benefits in implementing the find feature.
-
Reduce the time taken for the user to enter details of the Patient.
-
Understand the past medical history of a Patient better.
The search mechanism is facilitated by two main functions, namely toString and find.
toString is a method every component of the data model has. It is overridden when there is more information to be added for a particular
class. In our case, this facilitates searching for information by representing everything in String form.
find method is included in every class that store HashMaps or Observable Maps. It searches all string representation of the elements in the
HashMap by utilising the toString method.
Given below is an example usage scenario and how the search mechanism behaves at each step.
Step 1: The user launches the application and navigates to a particular patient context for example, John. The TextField in the
CommandWindow is blank, and the context is Patient:John. The user wishes to search John for a particular piece of information
e.g. Fever (a sample valid command syntax is find Fever).
Step 2: The find method will be called and all data related to the Patient will be searched for Fever, It will display the results in a new
Context containing all impressions where John had Fever in a seperate window
Step 3: The user can then select a particular impression and review the information or change the information if desired,
The following activity diagram summarizes what happens when a user types in the AutoCompleteTextField.
The discharge feature deletes a patient from Dr.Duke and creates a .txt report file where all data about the patient at the point of discharge is stored. These report files can be used to manually recreate a patient if a doctor wants to add a discharged patient back to Dr.Duke. This feature also prevents Dr.Duke from getting full as new patients come and go from the hospital using the same bed numbers. To be able to discharge a patient that is no longer at the hospital also enables quicker lookup of the patients that are at the hospital.
The discharge mechanism is facilitated by the ReportCommand and ReportSpec classes. ReportCommand extends the Command class and ReportSpec extends the ArgSpec class. Like every command, ReportCommand has an execute method. The execute method is called upon when the user enters a “discharge” command followed by a valid bed number. The “discharge” command has the optional switch -sum that enables the user to input a short discharge summary, for example, the reason why the patient is discharged and the date and time of the discharge. As the reports are stored in a text format the user can also add additional text to the report after the report has been created by simply writing new text to the report file with a text editor. The syntax of the “discharge” command is implemented in ReportSpec using the Switch class.
Given below is an example of what a discharge command with a discharge summary that follows the syntax could look like
-
discharge A12 -sum Patient left the hospital, 2019-03-03 08:00
The execute method in ReportCommand creates one report file for each discharged patient and places it in the “report” folder within the “data” folder. Every discharged patient file is named with the patient’s name and bed number separated by a -. For example, if a patient named “Alexander Smith” with the bed number "A300" was discharged the file name would be AlexanderSmith-A300.
The execute method uses the FileWriter class to write the report to the report file utilizing toReportString which is a method that every DukeObject implements. The toReportString returns a string representation of every attribute that is not a null value and some other strings that make the report more reader-friendly.
A future consideration is to store the reports in PDF files instead of text files. This would be beneficial as it would decrease the risk of the user to accidentally change the reports while reading it. Using PDFs could also make the reports more reader-friendly for the user. A drawback of using PDFs is that it makes it harder for the user to add text to the reports after they have been created. Another future consideration is to automatically include the date and time of when each discharge in the reports.
Target user profile:
House officers, who are typically freshly-graduated medical students, play a vital role in managing hospital patients. They are responsible, among many other things, for collating all information regarding each hospital patient and organising it to provide a clear picture of the patient’s situation, and for presenting that picture to senior doctors who can then make assessments and recommendations based on that picture. As much of this information needs to be exchanged at a rapid pace, Dr. Duke assists in quick, accurate and efficient recording and retrieval of the patient data required to provide effective care.
The house officers we are targeting with this app:
-
need to manage a significant number of patients
-
need to quickly input and organise patient data
-
prefer desktop apps over other types
-
prefer typing over mouse input
-
can type fast
Value proposition:
-
input, organise and access information about patients faster than with a typical mouse/GUI driven app
Priorities: High (must have) - * * *, Medium (nice to have) - * *, Low (unlikely to have) - *
| Priority | As a … | I want to … | So that I can… |
|---|---|---|---|
|
house officer |
check my patients' allergies |
issue them with the appropriate medicine |
|
house officer who has to manage a lot of information |
flag and view the critical issues to follow up for each patient |
complete the follow-up(s) as soon as possible |
|
house officer who has to manage many patients |
view the previous medical history of my patients |
understand what has been done to manage/treat their conditions |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
be able to make typing errors but still have my input recognised |
avoid having to waste time to retype my command |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
confirm my input type and modify it quickly if it is incorrect |
avoid having to retype or tediously transfer entries that were input in the wrong place |
|
house officer who needs to upload records into the hospital’s health system |
generate unified reports that are fully compatible with the system |
avoid having to manually input those records |
|
house officer keeping track of information for my consultant |
keep track of whether or not I’ve checked for the results of certain investigations |
make sure the consultant is kept up-to-date |
|
house officer who has to manage a lot of information |
easily link new information and follow-up items to particular conditions |
have a clearer picture of each condition and its corresponding management plan |
|
house officer with a consultant that talks too fast |
differentiate the types of input with just a single control character |
avoid having to waste time switching between windows |
|
house officer who has to manage a lot of information |
easily view and navigate through data associated with particular conditions that particular patients have |
have a clearer view of what that particular condition is |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
undo my previous commands |
quickly rectify mistakes made when inputting data |
|
house officer who has to manage a lot of information |
search through all of the records of a patient |
find all the details relevant to a particular aspect of his/her care plan |
|
house officer who has to manage many patients |
easily view all critical issues all my patients are facing by level of importance |
address them as soon as possible |
|
house officer who needs to input a lot of data quickly and is prone to mistyping |
have my input automatically checked to ensure it is of the right format |
always be assured that I am inputting the right commands. |
(For all use cases, the System is Dr. Duke and the Actor is the user, unless specified otherwise)
MSS
-
User requests to add a patient.
-
Dr. Duke requests for details of the patient.
-
User enters the requested details.
-
Dr. Duke creates a new profile for the patient according to the specified details.
Use case ends.
Extensions
-
3a. Dr. Duke detects an error in the entered details.
-
3a1. Dr. Duke prompts the user with an error message and requests for the correct details.
-
3a2. User enters correct details.
-
Steps 3a1 and 3a2 are repeated until the given details are valid.
-
Use case resumes from Step 4.
-
MSS
-
User searches for the patient (UC-3).
-
Dr. Duke requests for new details of the patient.
-
User enters new details of the patient.
-
Dr. Duke updates the profile for the patient.
Use case ends.
Extensions
-
3a. Dr. Duke detects an error in the entered details.
-
3a1. Dr. Duke prompts the user with an error message and requests for the correct details.
-
3a2. User enters correct details.
-
Steps 3a1 and 3a2 are repeated until the given details are valid.
-
Use case resumes from Step 4.
-
MSS
-
User enters the patient’s name.
-
Dr. Duke returns list of all relevant results.
-
User selects the target patient in the list.
Use case ends.
Extensions
-
2a. The returned list is empty.
Use case ends.
MSS
-
User searches for the patient (UC-3).
-
Dr. Duke shows the detailed records of the patient.
Use case ends.
MSS
-
User searches for the patient (UC-3) and requests to discharge him/her.
-
Dr. Duke shows the details of the patient and requests for a confirmation.
-
User confirms that the patient may be discharged.
-
Dr. Duke generates a discharge report for the patient and delete his/her record from the system.
Use case ends.
Extensions
-
a. At any time, User chooses to cancel the discharge operation.
-
a1. Dr. Duke requests to confirm the cancellation.
-
a2. User confirms the cancellation.
Use case ends.
-
MSS
-
User searches for the patient (UC-3) and requests to generate a report on his/her current health condition.
-
Dr. Duke generates a detailed report for the patient.
Use case ends.
Preconditions: At least 1 command in the command history.
MSS
-
User requests to undo previous command(s).
-
Dr. Duke shows the list of command(s) to be reverted and requests for a confirmation.
-
User reviews the command(s) and confirms the undo operation.
-
Dr. Duke performs the undo operation and returns the system to an older state.
Use case ends.
-
The software should be portable, i.e. work on any mainstream OS as long as the OS has Java
11or above installed. -
The software should be able to hold up to 500 patients without a noticeable reduction in performance for typical usage.
-
The software should work without internet access.
-
The software should have good user documentation, which details all aspects of the software to assist new users on how to use this software.
-
The software should have good developer documentation to allow developers to understand the design of the software easily so that they can further develop and enhance the software.
-
The software should be easily testable.
-
A user with an above average typing speed for regular English text should be able to accomplish most of his/her intended tasks faster using commands than using the mouse.
-
All data transactions should be atomic - either they succeed and the persistent data storage is immediately updated, or they fail and the user is notified of that event, with the data being unchanged.







