Our project uses external libraries and services from:
Additionally, some code was reused and modified as below by:
System.out
.Parser
class.The Architecture Diagram below explains the high level design of the app.
Given below is a quick overview of the main components and how they interact with one another.
Our program flow is managed by the Parking class.
User input is passed to the Command class, which then calls the Parser to parse the user input string as a command. Each command subclass handles its own execution.
Below are the main subcomponents that Parking and the command subclass delegate work to:
Ui
: Deals with user interaction, such as reading input and printing output.CommandResult
: Returns the results of the command instructed.Parser
: Takes in the user input string to determine what is the command the user has instructed the program to do.The model component consists of a CarparkList
(and CarparkFilteredList
) class that contains
instances of Carpark
, as well as the Favourite
class, under the package seedu.data
This component:
Favourite
class.Carpark
objects), contained in a CarparkList
object.CarparkFilteredList
).Carpark
object based on a unique code (for the find
command) as well
as filtering by a substring or set of substrings (filter
command).LotType
(Car, Motorcycle, Heavy Vehicle) and places them in a HashMap
for easy access. For example: Three Carpark
objects may have the same unique carpark code as they are the same
carpark, but contain available lot information for different types of lot. These three objects will be grouped under
one object with the HashMap allAvailableLots
containing a breakdown of lots by type.Note: The Carpark
class contain many getters, setters and annotations to be used with the jackson
module.
These may seem like they are unused in an IDE, but their presence is crucial for the parsing of the JSON files.
Please do not remove these unless absolutely necessary.
The model component consists of a Api
class that supports API call to the LTA DataMall Services, under the
package seedu.api
.
This component:
200 OK
and not empty.secret.txt
file once and stores the API as a variable (will only read from the file
if requested again).The API component is also able to:
The following sequence diagram shows how the API key is loaded.
The storage component consists of a FileReader
and FileWriter
class in the seedu.files
package.
FileReader
is an abstract class and only contains static methods, while FileWriter
can be instantiated and is used
in the API
and Model
components.
A LtaJsonWrapper
class is also present, for use with the Jackson JSON parser.
This component:
CarparkList
and
Carpark
classes).CarparkList
and
Carpark
classes).For more information on the process of loading files from JSON, please see the section 3.4 Updating CarparkList with JSON file from API.
The user-facing parts of the program are implemented with the Ui
class.
It implements the following main methods:
println()
- Prints either a String or an Ansi followed by a newline using System.out.println()
.print()
- Prints either a String or an Ansi using System.out.print()
.printGreen()
- Prints a String in green colour.printRed()
- Prints a String in red colour.printRenderedString()
- Prints a String to render.getLine()
- Asks user for input and returns input.getCommand()
- Asks user for a command input and returns input.showLogo()
- Prints ASCII art of a car and parKING logo.printResult()
- Shows the result of a command execution to the user. Includes additional formatting of the results of
different commands.printError()
- Print exception message.changeScanner()
- Changes the scanner for the Ui
object. To be used for JUnit testing.getSeparatorString()
- Returns a separator string.How the parsing works:
When called upon to parse a user command, the Parser
class creates an ABCCommandParser
(ABC
is a placeholder
for the specific command name e.g. FindCommandParser
) which uses the other classes to parse the user command and
create an ABCCommand
object (e.g. FindCommand
), which the Parser
returns back as a Command
object.
All ABCCommandParser
classes (e.g. FindCommandParser
, FilterCommandParser
etc) inherit from the Parser
interface so that they can be treated similarly where possible.
How the command works:
Parser
returns a Command
object, it runs Command.execute()
, which is a method to execute the command
and return the intended result. This method returns a CommandResult
, which is an object that takes in String
and
CarparkList
and returns what the user will be seeing on the terminal.ABCCommand
(ABC
is a placeholder for the specific command name e.g.
FindCommand
) as an extension of the Command
class.ABCCommand
classes will have an override execute()
method, which will override the execute
method in Command
and return the respective CommandResult
result of the Command.The Common component contain two auxiliary classes that just contain information that may be shared between or accessed by
multiple other components. Both CommonData
and CommonFiles
are abstract and cannot be instantiated, and all fields
within are static and final.
CommonFiles
contains information regarding files - file and directory paths.
CommonData
contains assorted information that is packed together for easy access. These fields are ones that may be
likely to need to be modified. Some examples are strings showing the correct format for using commands, used between
command classes whenever an invalid input is entered, and the strings containing URLs for the API.
This section describes some noteworthy details on how certain features are implemented.
favourite
and unfavourite
commandThe Favourite
class uses the FileReader
and FileStorage
classes to read and write carpark IDs to a favourite.txt
file so that user-favourited carparks can be saved locally and retrieved even after the user exits the application.
It contains the following attributes:
favouriteList
- An ArrayList of favourited carpark IDs.fileStorage
- A FileStorage object that helps to write to favourite.txt
.directory
- A String that contains the directory of favourite.txt
.file
- A String that contains the name of the file, in this case favourite.txt
.It implements the following operations:
updateFavouriteList()
- Reads in data from favourite.txt
file and saves it to this object.ensureValidity()
- Checks if all carpark IDs inside an ArrayList are valid carparks in a CarparkList.writeFavouriteList()
- Writes all favourite carpark IDs to favourite.txt
file.getFavouriteList()
- Static method that returns an ArrayList of favourited carpark IDs.getFavouriteListString()
- Returns a string of all favourited carpark IDs and their lot availability.The favourite feature is implemented using the FavouriteCommand
class.
It uses the Favourite
class to update and write to favouriteList
.
It implements the following operations:
execute()
- Main method to execute different favourite methods based on user input.setFavourite()
- Inserts a carpark into the favourite list.The unfavourite feature is implemented using the UnfavouriteCommand
class.
It uses the Favourite
class to update and remove favourited carparks from favouriteList
.
It implements the following operations:
execute()
- Main method to execute different unfavourite methods based on user input.setUnfavourite()
- Removes a carpark from the favourite list.Given below is an example of how the various classes are used to perform favourite / unfavourite operations:
Step 1. On startup, Favourite
is initialised. The main programs calls loadFavourite()
which then calls
updateFavouriteList()
and writeFavouriteList()
shortly after to populate and validate its ArrayList with the
carpark IDs in the file.
Step 2. The user executes favourite 1
to favourite the carpark with carpark ID 1
. The command is passed to the
Parser class which returns a FavouriteCommand
object to the main program.
The main program then calls executeCommand()
which calls execute()
from the FavouriteCommand
class.
It first checks if the second argument in the command is equal to list
. Since this is false, execute()
checks if
there exists a carpark with carpark ID 1
by calling isCarparkValid()
from the CarparkList
class. If the carpark
ID is valid, information about the carpark is retrieved from the findCarpark()
method and setFavourite()
is then
called to add the valid carpark ID into favouriteList
, which searches favouriteList
for any identical carpark IDs
and throws DuplicateCarparkException
if found, to prevent addition of duplicates. 1
is then added into
favouriteList
and setFavourite()
calls writeFavouriteList()
which overwrites data from favouriteList
to
favourite.txt
.
Note: If any exception is thrown,
1
will not be added intofavouriteList
, andwriteFavouriteList()
will not be called, hence preserving the validity of the carpark IDs.
Step 3. The user executes favourite 2 3 4
to favourite the carparks with carpark ID 2
, 3
and 4
. Execution is
similar to Step 2, except now execute()
will repeat the execution for each additional carpark ID provided, skipping
over the invalid carpark IDs.
Step 4. The user realises he/she made a mistake and wants to unfavourite the carpark with carpark ID 2
, and executes
unfavourite 2
. The command is passed to the Parser class which returns a UnfavouriteCommand
object.
The main program calls executeCommand()
which calls execute()
from the UnfavouriteCommand
class. execute()
checks if there exists a carpark with carpark ID 2
by calling isCarparkValid()
from theCarparkList
class. If the
carpark ID is valid, setUnfavourite()
is called, which first checks if favouriteList
contains an entry that matches
2
, and throws NoCarparkFoundException
if none is found. Next, 2
is removed from favouriteList
and
setUnfavourite()
calls writeFavouriteList()
which overwrites data from favouriteList
to favourite.txt
.
Step 5. The user wants to view all favourited carparks and executes favourite list
. The command is passed to the
Parser
class which returns a FavouriteCommand
object. The main program calls executeCommand()
which calls
execute()
from the FavouriteCommand
class. After checking if the second argument is equal to list
,
updateFavouriteList()
and writeFavouriteList()
are called to get and validate the most updated favourited carpark
IDs before getFavouriteListString()
is called. The carpark IDs and lot availability of all favourited carparks are
then passed back to the main program in a CommandResult
class.
The following sequence diagram shows how a favourite / unfavourite command works:
Aspect: Format of input after favourite
or unfavourite
command
filter
result
filter
commandThe filter
command is one of the more complicated functions of the program. The sequence diagram above shows the sequence of events when the filterByAddress()
method inside of carparkList
is called.
The private filterBySubstring()
method assigns a HashSet<Carpark>
object carparkListBuffer
, all carparks with development sentences that
match the substring. At the same time, the Word
object inside that Carpark
object’s developmentSentence that matches the substring is marked as bold, for use with the Jansi formatting later on, where words that match the query are marked with a different colour for easy viewing.
The Argument
class that the filter
command uses for error checking is an extension of the Sentence
class with additional methods to count dashed arguments. This is to ensure that the appropriate number of arguments and dashed arguments are in the command.
Note: Only prefixing substrings (substrings in the beginning of the word) will be counted as a match in address mode. For ID mode, any substrings or full matches will be included in the results of the command.
The filter
command can take in 1 or more arguments, which is then parsed into a Sentence
object as query.
This allows for more flexibility on the users’ end, allowing them to narrow down their search.
There is a discrepancy between how filter
is implemented depending on whether the command is in address or ID mode, using the
dashed argument -id
and -address
, or if no dashed argument is present (defaults to address mode).
In address mode, for a carpark to be included in the carparkFilteredList
, it must match all possible Words
in the search query as substrings.
For example, CLEMENTI AVE 3
can be matched with the user command filter clem ave 3
, but not filter clem ave 2
. Additionally, substrings must
be prefixing - found at the beginning of any word in the developmentSentence
field in any Carpark
object.
In id mode, any carpark’s ID only needs to match any word in the search query. Additionally, any substring at all, even in the middle of the sentence, will match.
For example, an ID 11
, 31
and A4
will all be matched with the user command filter -id 1 4
.
This discrepancy is due to the fact that a carpark ID is necessarily only ever one word - so it makes no sense to restrict
carparks further by making it match all words. Additionally, for additional flexibility,filter -id QUERY
also matches
not only prefixing substrings but any substring at all.
If users want a more restrictive filter
, they most likely already know what the carpark ID is and should then just use the
find
command.
Before going deep into how the data is fetched from the LTA API, we will run through how the Api
component of data fetching works.
Storage
, HttpClient
and Ui
class.Parking
class will call the loadApiKey()
method.
secret.txt
is found, file will be created.Api
instance (but not written to the local file).asyncExecuteRequest()
method to construct the HTTP request packet header and send the request
asynchronously.fetchData()
method to get response immediately from the API.
401 Unauthorised Access
(due to invalid API key), the method will try to fetch data
at most 5 times.Since each API call only returns 500 entries per fetch, we need to make multiple API calls. Thus, there is another method that handles the API calls all at once. The process is still similar.
syncFetchData()
method, which calls both the asyncExecuteRequest()
and fetchData()
five times (LTA has less
than 2500 Parking lot data)
Storage
component.The following sequence diagram shows how data is fetched using the update
command.
These are the reference sequence diagrams to supplement the above diagram.
Asynchronous request
We chose the LTA API service to provide us with the carpark availability due to
it having access to multiple agency’s databases. Thus, by using only one API we can get the most number of entries available.
Government API services usually utilise pagination (in this case the skip
parameter), such that
each query only gives a maximum of 500 entries at one time. As a result, multiple calls are needed to capture
the complete database that the API service can offer.
Thus, we utilise asynchronous calls to make all the HTTP requests first, and then fetch the response one by one. This means that the responses coming into our program will not be bottlenecked and will be faster than if we were to use a synchronous request.
After receiving all data from the responses, we need to concatenate them into one file such that the parser only needs to read from one file. This is done to minimise the possibility of read failure or exceptions being thrown.
Synchronous request capability
Although we are focusing more on the asynchronous part, the program right now also supports synchronous calls
for future development such as updating a specific value instead of the whole data. By calling the
asyncExecuteRequest()
and fetchData()
methods we can simulate synchronous HTTP requests.
Inconsistent dataset received
During the course of our development, we found out that sometimes the responses we receive will not be the full set of 500 entries per call. This led to some confusion at first but the behaviour seems random and definitely not caused by our program. Thus, we have to do multiple checks such as checking whether any data is being sent over (even if the response code is 200 OK) and tabulate the number of carparks that exist in the response.
Dataset does not adhere to LTA’s documentation
Coming to the end of our development where we placed more emphasis on unit testing, we found out
that LTA occasionally gives us invalid data. For example, in the documentation, only 3 types
of carpark lot type exists: C
(Car), H
(Heavy Vehicles) and Y
(Motorcycle). However, upon further inspection
we found out that other types also existed: M
and L
.
Another problem we found is that LTA does not do its own data validation. Thus, we found some carparks having a negative number of available lots. This is a big problem as from the user’s perspective it may be seen that our program is at fault.
Due to such bug discoveries, we made our CarparkList
parser much more robust, and it now does data
validation internally to ensure no such data is presented to the user.
The following is an overview of the sequence of what is called at initialization and with the use of the update
command:
The sequence can be summarized as follows:
carparkList
as a CarparkList
object.newCarparkList
, also a CarparkList
object.carparkList
with new availability information from newCarparkList
.carparkList
into a .txt file.NOTE: For ease of viewing the list of carparks inside the carparkList will be sorted upon generation by carpark ID.
More detailed information including exception handling will be in the sections below:
The program will attempt to read from a .txt file. This .txt file contains a save string of the CarparkList as last modified by the program. Additionally, the user can customise this by changing some fields (for example, renaming a carpark to “Near my house” for convenient viewing later), which will be loaded and stored by the program.
The sequence of events is as follows:
carparkList.txt
file).loadJson()
method is called instead
to regenerate from a backup. This backup is guaranteed to be valid, as explained in 3.4.2 Loading CarparkList from JSON fileDesign Considerations
1. Choice of “last resort” backup loading
The reason for reading from the backup JSON as a last resort is so that no loss of information is found: the LTA API on occasion
will only return 500 or a variable number of carparks. If the last resort is to parse the fresh JSON from the LTA API into
program, and the new CarparkList only has 500 objects, some objects in the Favourites
list may become invalid, and more than that,
a large portion of carpark information will not be available at all.
As such, we elected to use the backup JSON as a base as a verifiable comprehensive sampling of carparks, so no information
will be invalid and users can still use filter
and search for these carparks that might be unable if the freshly fetched API was used.
To allow users to work around this, we have a “Last Updated” field in the detail view to allow users to see if the carpark
has been just updated or was not updated by the API call.
2. Invalid format behaviour
Invalid formatting may result when users tamper with the file by adding or removing delimiters (||
), or entering data that will result
in an improperly formatted CarparkList
object. For example, changing the carparkId
field for a carpark such that
two carparks clash will result in the find
command not working properly as the HashMap will not be able to take
duplicate values.
It was decided for the same reason above that rather than just ignoring single rows and using new ones, any invalid formatting at all
will cause the entire text string to be ignored (and overwritten later). This is because a user causing a single row to be invalid may cause
that row to be skipped, and if the Favourites
list includes that particular row, it may cause a mismatch in expected information
between the user and the program.
The sequence of events from loading from a JSON file is as follows:
LtaResponse.json
.LtaResponse.json
file exists and is of a valid format, it is read by the Jackson parser, creating Carpark
objects, which will then be returned to the program.LtaResponse.json
doesn’t exist or has an invalid format, LtaResponseSample.json
is read instead: a backup
file to be used if there is no internet connection or an error with the LTA file.LtaResponse.json
and LtaResponseSample.json
file come with errors, the LtaResponseSample.json
is regenerated
by copying from within the .jar file. It is then read again, and the CarparkList
object is returned.Invalid format behaviour
In the UG, users are warned not to tamper with these two files as it may result in regeneration of the files due to invalid format. As with the
carparkList.txt
file, any errors at all will cause the entire file to be ignored and the backup to be used instead. The backup LtaResponseSample.json
should not be tampered with either, but if it is, it will always be regenerated from the .jar
, which should never fail.
The sequence of events for writing to a text file is as follows:
getSaveString()
method inside the carparkList
passed in as a parameter.If the file cannot be found or the appropriate directories are missing, the file structure and new text file will be regenerated and then written to.
The Ui
class makes use of the jansi.render()
method to render strings that were formatted earlier with codes to mark which text should be coloured.
The Jansi library does not work within the IDE (tested in IntelliJ), but works with consoles in Mac, Linux and Windows 10 and above. If the console environment is currently not supported, jansi.render()
strips all formatting and returns just the plain text, resulting in clean but uncoloured text.
Future plans will support more extensive colouring of the UI.
All custom exceptions within the program inherit from the abstract ParkingException
class. The majority of custom exceptions have a built-in message in the class itself rather than loading a message in via a constructor. This is to encourage specific exceptions that should only have a message for one problem, and to prevent rewriting the same error message if the Exception is thrown in multiple files. In some cases, this string requires a String.format()
method call to format properly - for example, with the InvalidCommandException
.
Exceptions are generally handled by using the Ui.printError()
method which takes one parameter of type ParkingException
. This method will print the string by calling getMessage()
in the exception and formatting it red so the user can recognise it is an error easily.
Some special cases that don’t follow the format of having the text string inside the exception itself is InvalidFormatException
and NoFileFoundException
. These exceptions, while being a specific
problem, are wide enough in scope for messages to vary wildly depending on the nature of the file exception or the invalid format.
The target user that we have in mind is a driver who needs to find an available parking lot and wants to easily find carpark information, so he knows where he can park. We want to make the data from LTA easier to fetch and read for users who are able to use a CLI. This would be a more efficient way for Singaporean drivers to be able to obtain information regarding their desired carpark and also for them to be able to filter out carparks based on their addresses. Most importantly, it does not require any internet connection to use if users want to search carpark information (based on what was last updated).
parKING is a desktop app that helps Singaporean drivers find and filter out carpark information easily, and is optimized for use via a Command Line Interface (CLI).
Version | As a … | I want to … | So that I can … |
---|---|---|---|
v1.0 | Driver | Search lot availability by a unique carpark ID for a specific carpark | Know where I can park |
v1.0 | User with no internet | Access a list of available carparks on the app offline | Estimate where I can park based on last updated information offline |
v2.0 | Driver | Be able to save favourites | Monitor carparks important to me |
v2.0 | Driver | Import my favourite carparks | Use my saved settings/preferences across different devices |
v2.0 | Driver | Filter the carparks based on their address | Search for carparks without knowing the carpark ID |
v2.1 | Driver | Search for carparks based on carpark ID | Search for carparks without knowing the carpark address |
v2.1 | Driver | Have my carpark information be colour coordinated | Look at carpark information with ease |
The following command(s) do not take any arguments:
list
help
exit
update
Providing arguments will result in an error message, for example:
Input:
help arg1
Output:
There were unrecognized arguments after the `help` command. Please try the
`help` command again by itself.
The following command(s) take only 1 argument:
find
auth
Providing no arguments will result in an error message, for example:
Input:
find
Output:
Empty argument. Valid command(s):
`find CARPARK_ID` or `fin CARPARK_ID` : Display information about the
specific queried carpark based on carpark ID.
Providing too many arguments will result in an error message, for example:
Input:
auth arg1 arg2
Output:
This command only takes exactly 1 argument(s). Valid command(s):
`auth API_KEY` or `a API_KEY` : To authenticate your personal API key.
`auth default` or `a default` : To authenticate using the default key
provided by parKING.
`auth status` or `a status` : To get the authentication status.
The following command(s) take in more than 1 argument:
filter
filter -id
filter -address
unfavourite
favourite
(excluding favourite list
)Providing no arguments will result in an error message, for example:
Input:
filter
Output:
Empty argument. Valid command(s):
`filter QUERY` or `fil QUERY` : Filter carparks based on Carpark information.
The following command does not take in any additional arguments:
favourite list
Providing extra arguments will result in an error message, for example:
Input:
favourite list arg1
Output:
This command with the `list` argument cannot take any additional
arguments. Valid commands(s):
`favourite list` or `fav list` : To get the list of favourited carparks.
`favourite CARPARK_ID` or `fav CARPARK_ID` : Favourite carpark by its ID.
Users can test both Favourite.txt and CarparkList.txt to modify the data being passed to the program.
Refer to the User Guide for accurate information regarding the valid and invalid inputs. Users can try giving carpark IDs or addresses that do not exist.