Compare commits
10 commits
cf3fa3f566
...
a8687095c2
Author | SHA1 | Date | |
---|---|---|---|
a8687095c2 | |||
d5730455e6 | |||
|
7053085344 | ||
|
b7cac335ec | ||
6ee2c5a715 | |||
|
792d293dd4 | ||
|
ffd4b3fa5b | ||
|
c7e9824208 | ||
|
b35ffac6fa | ||
|
0213bc4606 |
21 changed files with 566 additions and 119 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -104,7 +104,7 @@ fabric.properties
|
|||
|
||||
# Package Files #
|
||||
*.jar
|
||||
!libs/postgresql-42.5.1.jar
|
||||
!libs/
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
|
|
6
.idea/misc.xml
Normal file
6
.idea/misc.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_19" project-jdk-name="19" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/covid_10012023_MehulAhal_covid-reports.iml" filepath="$PROJECT_DIR$/covid_10012023_MehulAhal_covid-reports.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
79
README.md
79
README.md
|
@ -2,3 +2,82 @@
|
|||
|
||||
Please use this readme as your projects readme. You can find instructions for
|
||||
the assignment in the [`INSTRUCTIONS.md`](INSTRUCTIONS.md) file.
|
||||
|
||||
# Table of contents
|
||||
- [Setup instructions](#setup-instructions)
|
||||
- [Overview](#overview)
|
||||
* [Classes and packages](#classes-and-packages)
|
||||
+ [Services](#services)
|
||||
- [AppConfigService](#appconfigservice)
|
||||
- [DatabaseService](#databaseservice)
|
||||
- [ReportGenerationService](#reportgenerationservice)
|
||||
+ [Data](#data)
|
||||
- [DTOs](#dtos)
|
||||
- [Enums](#enums)
|
||||
- [Repos](#repos)
|
||||
- [Models](#models)
|
||||
+ [APIs](#apis)
|
||||
- [Improvements](#improvements)
|
||||
|
||||
# Setup instructions
|
||||
1. Add the [postgresql-42.5.1.jar](libs/postgresql-42.5.1.jar) as a library dependency in your IDE.
|
||||
+ Instructions for specific IDEs: [IntelliJ](https://stackoverflow.com/questions/1051640/correct-way-to-add-external-jars-lib-jar-to-an-intellij-idea-project), [Eclipse](https://www.edureka.co/community/4028/how-to-import-a-jar-file-in-eclipse)
|
||||
+ Per build-tool, for different version etc. [Maven](https://mvnrepository.com/artifact/mysql/mysql-connector-java), [Gradle](https://stackoverflow.com/a/50484781/11981966), [Ant](https://ant.apache.org/manual/api/org/apache/tools/ant/taskdefs/JDBCTask.html).
|
||||
2. Follow the Docker-setup instruction in [INSTRUCTIONS.md](INSTRUCTIONS.md#setup).
|
||||
|
||||
Overview
|
||||
========
|
||||
This project is a simple command-line application that communicates with a PostgreSQL database to retrieve data and generate reports based on that data. It's composed of several classes and packages that work together to accomplish this task.
|
||||
|
||||
![Class UML](docs/ClassUML.png)
|
||||
|
||||
Classes and packages
|
||||
--------------------
|
||||
|
||||
The application's main class is `Main` class, it runs the entire application. It's composed of three services: `AppConfigService`, `DatabaseService`, and `ReportGenerationService`.
|
||||
### Services
|
||||
#### AppConfigService
|
||||
|
||||
`AppConfigService` is a service class that prompts and stores the configuration that the app needs to be run with. It prompts the user for input for the country name and date, and stores them as class members.
|
||||
|
||||
#### DatabaseService
|
||||
|
||||
`DatabaseService` class is responsible for maintaining the active connection between the app and the SQL Server. It also communicates with the `DatabaseApi` class to perform final CRUD operations and return any results.
|
||||
|
||||
#### ReportGenerationService
|
||||
|
||||
`ReportGenerationService` generates a report from results. Since only a std-out is the only strategy, composition is preferred. It provides a single method `reportBaseResults(QueryResultsRepository queryResultsRepository)` that takes a `QueryResultsRepository` object as input and prints the results to the console. The method prints the `toString()` representation of the `QueryResultsRepository` object. It provides a basic printer and more members can be added in the future to use different strategies for printing out the received results.
|
||||
|
||||
### Data
|
||||
#### DTOs
|
||||
`data.dtos` package contains the `QueryDTO` class. This class serves as a container for storing SQL statements to be executed, along with their arguments.
|
||||
|
||||
#### Enums
|
||||
`data.enums` package contains the `CustomPreparedStatementsRead` enum. This enum contains all the read-only prepared statements that are used throughout the application.
|
||||
|
||||
#### Repos
|
||||
`data.repos` package contains the `QueryResultsRepository` class. This class stores the results of the executed queries, and can be used to generate a report.
|
||||
|
||||
#### Models
|
||||
`data.models` package contains the `PersistentResultSetModel` class. This class adapts a given `ResultSet` to a `PersistentResultModel`.
|
||||
|
||||
### APIs
|
||||
`apis` package contains the `DatabaseApi` class. This class communicates with the database to perform final CRUD operations, returning any results. This class is responsible for executing SQL statements, which are passed to it in the form of a QueryDTO class. It receives a QueryDTO object and a JDBC connection, then it processes the query and returns a PreparedStatement object. This class is designed to handle any exception that may occur during the execution of the query and it can be extended in the future to handle other types of queries if needed.
|
||||
|
||||
# Improvements
|
||||
Here are some points that I could have improved upon, but did not go through with it because of either time constraints, or because it might be too much for the scope of this assignment.
|
||||
|
||||
+ Ideally all the services, and inner services and APIs being used, should be injected in by dependency injection. In Dart, [get_it](https://pub.dev/packages/get_it), is makes this very easy with a Locator file. But for Java, I was not able to find something that was as easy.
|
||||
+ Ideally the AppConfigService should be more generalized to acecpt N number of user inputs, for the scalability.
|
||||
+ That would have also helped with making the pipeline in `run()` better, as the list of config vars could be supplied directly, instead of manually specifying.
|
||||
+ The DatabaseService class is tightly coupling the DatabaseApi class. It's better to make the DatabaseApi class a singleton and inject it into the DatabaseService class.
|
||||
+ The DatabaseApi class is doing more than just communicating with the database, it's also handling error handling. It would be better to extract the error handling logic into a separate class.
|
||||
+ Could be rectified by using lots of custom Exceptions in general, but overkill for this scope.
|
||||
+ The QueryResultsRepository class is tightly coupled to the PersistentResultSetModel class. It would be better to extract the PersistentResultSetModel class into a separate package, and use a factory pattern to create instances of it.
|
||||
+ The App class is doing too much. It would be better to extract the logic for each endpoint into separate classes, and use a factory pattern to create instances of them.
|
||||
+ But then till it is needed, composition was preferred over inheritance.
|
||||
+ The Main class should be refactored to use dependency injection, so that the dependencies can be easily swapped out for testing.
|
||||
+ The AppConfigService class should be refactored to use a more robust input validation library, to handle more edge cases.
|
||||
+ The ReportGenerationService class should be refactored to support more output formats, such as HTML, CSV and PDF.
|
||||
+ The Main class is using exceptions for flow control, it would be better to use specific exception classes for different error conditions, and handle them accordingly.
|
||||
+ The Main class is catching a general SQLException and rethrowing it as a runtime exception, but it would be better to handle specific SQLException subclasses and provide more informative error messages.
|
||||
|
|
|
@ -7,15 +7,6 @@
|
|||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module-library" exported="">
|
||||
<library>
|
||||
<CLASSES>
|
||||
<root url="file://$MODULE_DIR$/libs" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
<jarDirectory url="file://$MODULE_DIR$/libs" recursive="false" />
|
||||
</library>
|
||||
</orderEntry>
|
||||
<orderEntry type="library" name="postgresql-42.5.1" level="project" />
|
||||
</component>
|
||||
</module>
|
BIN
docs/ClassUML.png
Normal file
BIN
docs/ClassUML.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 826 KiB |
|
@ -1,9 +1,15 @@
|
|||
import data.enums.CustomReadPreparedStatements;
|
||||
import data.dtos.QueryDTO;
|
||||
import data.enums.CustomPreparedStatementsRead;
|
||||
import data.repos.QueryResultsRepository;
|
||||
import services.AppConfigService;
|
||||
import services.DatabaseService;
|
||||
import services.ReportGenerationService;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* Entry point of the application
|
||||
*/
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
final App app = new App();
|
||||
|
@ -11,17 +17,84 @@ public class Main {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Responsible for the overall flow of the application. It creates and manages instances Services.
|
||||
*/
|
||||
class App {
|
||||
public App() {
|
||||
databaseService = new DatabaseService();
|
||||
appConfigService = new AppConfigService();
|
||||
reportGenerationService = new ReportGenerationService();
|
||||
}
|
||||
|
||||
final DatabaseService databaseService;
|
||||
final AppConfigService appConfigService;
|
||||
final private DatabaseService databaseService;
|
||||
final private AppConfigService appConfigService;
|
||||
final private ReportGenerationService reportGenerationService;
|
||||
|
||||
/**
|
||||
* Endpoints mentioned under the `Report` section
|
||||
*/
|
||||
private static final CustomPreparedStatementsRead[] reportEndpoints = {
|
||||
CustomPreparedStatementsRead.HighestAndLowest10Vaccination,
|
||||
CustomPreparedStatementsRead.HighestInfectionsPer100K,
|
||||
CustomPreparedStatementsRead.CountriesLaggingBehind,
|
||||
};
|
||||
|
||||
/**
|
||||
* Endpoints mentioned under the `Query` section
|
||||
*/
|
||||
private static final CustomPreparedStatementsRead[] queryEndpoints = {
|
||||
CustomPreparedStatementsRead.DailyInfectionsAndDeathAggregate,
|
||||
};
|
||||
|
||||
/**
|
||||
* Responsible for the overall flow of the application.
|
||||
* It retrieves data from the database using the `DatabaseService` class,
|
||||
* generates reports using the `ReportGenerationService` class, and prompts the user for input using the `AppConfigService` class.
|
||||
*/
|
||||
public void run() {
|
||||
//appConfigService.promptUserArguments();
|
||||
final ResultSet[] resultSetsQuery1 = databaseService.executeReadReportsEndpoint(CustomReadPreparedStatements.HighestAndLowest10Vaccination);
|
||||
executeNoInteractionEndpointPipeline(reportEndpoints);
|
||||
executeInteractiveEndpointPipeline(queryEndpoints);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is responsible for executing a pipeline of read-only queries, using a provided set of
|
||||
* {@link CustomPreparedStatementsRead} queryEndpoints, without any user interaction.
|
||||
* The method will iterate over the provided queryEndpoints, and for each query it will:
|
||||
* Execute the read query using {@link DatabaseService#getReadReportsEndpoint(QueryDTO)}
|
||||
* Store the results in a new {@link QueryResultsRepository}
|
||||
* Generate a report for the stored results using {@link ReportGenerationService#reportBaseResults(QueryResultsRepository)}
|
||||
* @param queryEndpoints an array of {@link CustomPreparedStatementsRead} containing the queries to be executed.
|
||||
* @throws RuntimeException when there is a SQLException thrown by {@link DatabaseService#getReadReportsEndpoint(QueryDTO)}
|
||||
*/
|
||||
private void executeNoInteractionEndpointPipeline(CustomPreparedStatementsRead[] queryEndpoints) {
|
||||
QueryResultsRepository resultsRepository;
|
||||
for (final CustomPreparedStatementsRead query : queryEndpoints) {
|
||||
try {
|
||||
resultsRepository = databaseService.getReadReportsEndpoint(new QueryDTO(query));
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
reportGenerationService.reportBaseResults(resultsRepository);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a series of read queries specified in the {@code queryEndpoints} parameter, and prompts the user for input of country name and date.
|
||||
* The results of the queries are passed to reportGenerationService to be reported.
|
||||
* @param queryEndpoints the endpoints to execute in the pipeline
|
||||
* @throws RuntimeException if a SQLException occurs when getting the read reports endpoint
|
||||
*/
|
||||
private void executeInteractiveEndpointPipeline(CustomPreparedStatementsRead[] queryEndpoints) {
|
||||
QueryResultsRepository resultsRepository;
|
||||
try {
|
||||
for(final CustomPreparedStatementsRead query: queryEndpoints) {
|
||||
appConfigService.promptAndSetUserArguments();
|
||||
resultsRepository = databaseService.getReadReportsEndpoint(new QueryDTO(query, new Object[]{appConfigService.getCountryName(), appConfigService.getDate(),}));
|
||||
reportGenerationService.reportBaseResults(resultsRepository);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +1,55 @@
|
|||
package apis;
|
||||
|
||||
import data.dtos.QueryDTO;
|
||||
import org.postgresql.util.PSQLException;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* The `DatabaseApi` class is a DAO that provides a way to communicate with the database to perform read operations and return the results.
|
||||
* It contains a single method `performReadQuery()` that takes a `QueryDTO` and a `Connection` object as parameters and
|
||||
* returns a `PreparedStatement` object. It throws `SQLException` in case of any SQL error or `IllegalArgumentException`
|
||||
* in case of any missing argument or extra argument.
|
||||
* <p>
|
||||
* The class can be abstracted and implemented into constituent action-specific APIs when there is a need for more than a single READ action.
|
||||
*/
|
||||
public class DatabaseApi {
|
||||
public PreparedStatement performQuery(QueryDTO queryDTO, Connection connection) {
|
||||
/**
|
||||
* Creates and executes a PreparedStatement, given the QueryDTO.
|
||||
* @param queryDTO The query in the form of the queryDTO
|
||||
* @param connection SQL server connection
|
||||
* @return The executed prepared statement, containing the results of the query
|
||||
* @throws IllegalArgumentException in case of any missing argument or extra argument.
|
||||
*/
|
||||
public PreparedStatement performReadQuery(QueryDTO queryDTO, Connection connection) throws SQLException {
|
||||
// Set template
|
||||
PreparedStatement statement = connection.prepareStatement(queryDTO.statement().statementTemplate);
|
||||
|
||||
try {
|
||||
PreparedStatement statement = connection.prepareStatement(queryDTO.queryTemplate().statementData);
|
||||
// Add in arguments
|
||||
for(int argIndex = 0; argIndex < queryDTO.templateArgs().length; argIndex++) {
|
||||
final Object curentArg = queryDTO.templateArgs()[argIndex];
|
||||
|
||||
// parameter indexing starts at 1
|
||||
if (curentArg instanceof Date)
|
||||
statement.setDate(argIndex + 1, new java.sql.Date(((Date) curentArg).getTime()));
|
||||
else
|
||||
statement.setString(argIndex + 1, curentArg.toString());
|
||||
|
||||
}
|
||||
|
||||
statement.execute();
|
||||
return statement;
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (PSQLException e) {
|
||||
if (e.getMessage().startsWith("No value specified for parameter"))
|
||||
throw new IllegalArgumentException("The templates present in the query, were not correctly mapped to their replacement variables.");
|
||||
else if (e.getMessage().startsWith("The column index is out of range:")) {
|
||||
throw new IllegalArgumentException("Are you trying to supply replacement variables, when they are not needed?");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
return statement;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package data.constants;
|
||||
|
||||
public class ConstValues {
|
||||
//I
|
||||
final public static String inputCountryName = "Enter country: ";
|
||||
final public static String inputDate = "Enter date: ";
|
||||
}
|
|
@ -1,7 +1,18 @@
|
|||
package data.dtos;
|
||||
|
||||
import data.enums.CustomReadPreparedStatements;
|
||||
|
||||
public record QueryDTO(CustomReadPreparedStatements queryTemplate, Object[] placeholderReplacements) {
|
||||
import data.enums.CustomPreparedStatementsRead;
|
||||
|
||||
/**
|
||||
* Container for storing SQL statements to be executed, along with their aruments.
|
||||
* @param statement an instance of `CustomPreparedStatementsRead` enum which contains the statement template to be executed.
|
||||
* @param templateArgs an array of `Object` representing the arguments to be used in the statement template.
|
||||
*/
|
||||
public record QueryDTO(CustomPreparedStatementsRead statement, Object[] templateArgs) {
|
||||
/**
|
||||
* overloaded constructor that takes only a `CustomPreparedStatementsRead` object as parameter.
|
||||
* @param customPreparedStatementsRead
|
||||
*/
|
||||
public QueryDTO(CustomPreparedStatementsRead customPreparedStatementsRead) {
|
||||
this(customPreparedStatementsRead, new Object[]{});
|
||||
}
|
||||
}
|
||||
|
|
77
src/data/enums/CustomPreparedStatementsRead.java
Normal file
77
src/data/enums/CustomPreparedStatementsRead.java
Normal file
|
@ -0,0 +1,77 @@
|
|||
package data.enums;
|
||||
|
||||
/**
|
||||
* Holds prepared data for Read-based SQL queries.
|
||||
*/
|
||||
public enum CustomPreparedStatementsRead {
|
||||
DailyInfectionsAndDeathAggregate("""
|
||||
WITH matched_countries AS (
|
||||
SELECT c.name, c.code
|
||||
FROM countries c
|
||||
WHERE c.name LIKE CONCAT('%',?, '%')
|
||||
)
|
||||
SELECT recorded_date, infections, deaths
|
||||
FROM cases
|
||||
WHERE iso_country IN (SELECT code FROM matched_countries) AND recorded_date <= ?
|
||||
ORDER BY recorded_date
|
||||
""",
|
||||
"Daily infections and deaths up until, inside a range (incl. fuzzy search)"
|
||||
),
|
||||
|
||||
HighestAndLowest10Vaccination("""
|
||||
WITH vacc_count_per_country AS (
|
||||
SELECT iso_country, SUM(daily_vaccinations) as total_vaccinations
|
||||
FROM vaccinations
|
||||
GROUP BY iso_country
|
||||
)
|
||||
SELECT name, total_vaccinations
|
||||
FROM countries JOIN vacc_count_per_country
|
||||
ON countries.code = vacc_count_per_country.iso_country
|
||||
ORDER BY total_vaccinations DESC
|
||||
LIMIT 10
|
||||
;
|
||||
|
||||
WITH vacc_count_per_country AS (
|
||||
SELECT iso_country, SUM(daily_vaccinations) as total_vaccinations
|
||||
FROM vaccinations
|
||||
GROUP BY iso_country
|
||||
)
|
||||
SELECT name, total_vaccinations
|
||||
FROM countries JOIN vacc_count_per_country
|
||||
ON countries.code = vacc_count_per_country.iso_country
|
||||
ORDER BY total_vaccinations ASC
|
||||
LIMIT 10
|
||||
""",
|
||||
"10 countries with the highest vaccinations (with count), and countries with lowest number of vaccinations."
|
||||
),
|
||||
|
||||
HighestInfectionsPer100K("""
|
||||
SELECT c.name, ((SUM(cases.infections) * 100000) / c.population) as infections_per_100k, c.population, SUM(cases.infections) as total_infections
|
||||
FROM cases INNER JOIN countries c ON cases.iso_country = c.code
|
||||
GROUP BY c.name, c.population
|
||||
ORDER BY infections_per_100k DESC
|
||||
LIMIT 10
|
||||
""",
|
||||
"10 countries with the highest infections per 100k inhabitants (with count)"
|
||||
),
|
||||
|
||||
CountriesLaggingBehind("""
|
||||
SELECT c.name
|
||||
FROM countries c INNER JOIN (
|
||||
SELECT iso_country, SUM(daily_vaccinations) as total_vaccinations
|
||||
FROM vaccinations
|
||||
GROUP BY iso_country
|
||||
) v ON c.code = v.iso_country
|
||||
WHERE v.total_vaccinations < c.population
|
||||
""",
|
||||
"Countries that haven't vaccinated their whole population with at least one shot or jab."
|
||||
);
|
||||
|
||||
public final String statementTemplate;
|
||||
public final String description;
|
||||
|
||||
CustomPreparedStatementsRead(String statementTemplate, String description) {
|
||||
this.statementTemplate = statementTemplate;
|
||||
this.description = description;
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package data.enums;
|
||||
|
||||
public enum CustomReadPreparedStatements {
|
||||
HighestAndLowest10Vaccination("""
|
||||
WITH vacc_count_per_country AS (
|
||||
SELECT iso_country, SUM(daily_vaccinations) as total_vaccinations
|
||||
FROM vaccinations
|
||||
GROUP BY iso_country
|
||||
)
|
||||
SELECT name, total_vaccinations
|
||||
FROM countries JOIN vacc_count_per_country
|
||||
ON countries.code = vacc_count_per_country.iso_country
|
||||
ORDER BY total_vaccinations DESC
|
||||
LIMIT 10;
|
||||
|
||||
WITH vacc_count_per_country AS (
|
||||
SELECT iso_country, SUM(daily_vaccinations) as total_vaccinations
|
||||
FROM vaccinations
|
||||
GROUP BY iso_country
|
||||
)
|
||||
SELECT name, total_vaccinations
|
||||
FROM countries JOIN vacc_count_per_country
|
||||
ON countries.code = vacc_count_per_country.iso_country
|
||||
ORDER BY total_vaccinations ASC
|
||||
LIMIT 10;
|
||||
"""
|
||||
);
|
||||
|
||||
public final String statementData;
|
||||
|
||||
private CustomReadPreparedStatements(String statementData) {
|
||||
this.statementData = statementData;
|
||||
}
|
||||
}
|
91
src/data/models/PersistentResultSetModel.java
Normal file
91
src/data/models/PersistentResultSetModel.java
Normal file
|
@ -0,0 +1,91 @@
|
|||
package data.models;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.ResultSetMetaData;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The class `PersistentResultSetModel` adapts a given `ResultSet` to a
|
||||
* PersistentResultModel. The class provides two main fields, an array of `ResultRowModel` called
|
||||
* `resultRowEntries` and an array of `String` called `columnNames`.
|
||||
* The factory constructor takes a `ResultSet` as a parameter, and adapts it to a `PersistentResultModel`.
|
||||
* The class also provides a toString method that returns the string representation of the result set.
|
||||
*/
|
||||
public class PersistentResultSetModel {
|
||||
/**
|
||||
* Factory constructor, that adapts a given ResultSet to PersistentResultModel.
|
||||
* @param resultSet
|
||||
* @throws SQLException
|
||||
*/
|
||||
public PersistentResultSetModel(ResultSet resultSet) throws SQLException {
|
||||
final ResultSetMetaData resultSetMetaData = resultSet.getMetaData();
|
||||
final int columnCount = resultSetMetaData.getColumnCount();
|
||||
|
||||
// Retrieving column names
|
||||
columnNames = new String[columnCount];
|
||||
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++)
|
||||
columnNames[columnIndex] = resultSetMetaData.getColumnName(columnIndex+1);
|
||||
|
||||
// Retrieving rest of the results
|
||||
final List<ResultRowModel> rows = new ArrayList<>();
|
||||
while (resultSet.next()) {
|
||||
final Object[] rowElements = new Object[columnCount];
|
||||
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++)
|
||||
rowElements[columnIndex] = resultSet.getObject(columnIndex+1);
|
||||
|
||||
rows.add(new ResultRowModel(rowElements));
|
||||
}
|
||||
|
||||
resultRowEntries = new ResultRowModel[rows.size()];
|
||||
rows.toArray(resultRowEntries);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents all the rows of the result set.
|
||||
*/
|
||||
final public ResultRowModel[] resultRowEntries;
|
||||
/**
|
||||
* Represents column names that appear in the result set.
|
||||
*/
|
||||
|
||||
final public String[] columnNames;
|
||||
|
||||
/**
|
||||
* The `ResultRowModel` class represents a single row in the result set. It contains an array of
|
||||
* `Object` called `rowElements`. The class provides an implementation of the `toString()` method
|
||||
* that returns the string representation of the row.
|
||||
* @param rowElements The elements of the row
|
||||
*/
|
||||
public record ResultRowModel(Object[] rowElements){
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder templateBuilder = new StringBuilder("|");
|
||||
for (final Object rowElement : rowElements) {
|
||||
templateBuilder.append("\t\t\t").append(rowElement).append("\t\t\t").append('|');
|
||||
}
|
||||
|
||||
return templateBuilder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (resultRowEntries.length == 0) return "--------No results--------";
|
||||
|
||||
final int rowLengthEstimate = resultRowEntries[0].toString().replaceAll("\t", " ").length();
|
||||
final String outerHorizontalLine = new String(new char[rowLengthEstimate]).replace("\0", "_");
|
||||
final String innerHorizontalLine = new String(new char[rowLengthEstimate]).replace("\0", "-");
|
||||
|
||||
StringBuilder templateBuilder = new StringBuilder(outerHorizontalLine).append('\n');
|
||||
for (final String columnName : columnNames)
|
||||
templateBuilder.append("|\t\t\t").append(columnName.toUpperCase()).append("\t\t\t");
|
||||
templateBuilder.append("|\n").append(outerHorizontalLine);
|
||||
|
||||
for (final ResultRowModel row: resultRowEntries)
|
||||
templateBuilder.append('\n').append(row).append('\n').append(innerHorizontalLine);
|
||||
|
||||
return templateBuilder + "\n" + outerHorizontalLine;
|
||||
}
|
||||
}
|
52
src/data/repos/QueryResultsRepository.java
Normal file
52
src/data/repos/QueryResultsRepository.java
Normal file
|
@ -0,0 +1,52 @@
|
|||
package data.repos;
|
||||
|
||||
import data.models.PersistentResultSetModel;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides a way to store and persist the results of read queries.
|
||||
* It contains a list of `PersistentResultSetModel` instances called `persistentResults` and a `String` called `queryDescription`.
|
||||
* The class provides a constructor that takes a `queryDescription` as a parameter and an `addResult()` method
|
||||
* that takes a `ResultSet` as a parameter and adds it to the `persistentResults` list.
|
||||
* The class also provides a `toString()` method that returns the string representation of the stored results and their description.
|
||||
*/
|
||||
public class QueryResultsRepository {
|
||||
|
||||
/**
|
||||
* Store for the results
|
||||
*/
|
||||
private final List<PersistentResultSetModel> persistentResults = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Represents a description of the query that was executed.
|
||||
*/
|
||||
private final String queryDescription;
|
||||
|
||||
public QueryResultsRepository(String queryDescription) {
|
||||
this.queryDescription = queryDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PersistentResultSetModel instance from given ResultSet, and adds it to the persistentResults list
|
||||
*
|
||||
* @param resultSet the resultSet to be added to the persistentResults list
|
||||
* @throws SQLException if an error occurs while creating the PersistentResultSetModel instance
|
||||
*/
|
||||
public void addResult(ResultSet resultSet) throws SQLException {
|
||||
persistentResults.add(new PersistentResultSetModel(resultSet));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder templateBuilder = new StringBuilder("\n\nQUERY DESCRIPTION: ").append(queryDescription);
|
||||
for (final PersistentResultSetModel result : persistentResults) {
|
||||
templateBuilder.append("\n").append(result).append("\n>");
|
||||
}
|
||||
|
||||
return templateBuilder.deleteCharAt(templateBuilder.length() - 1).append("\n\n").toString();
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package services;
|
||||
|
||||
import data.constants.ConstFormatters;
|
||||
import data.constants.ConstValues;
|
||||
import utils.Formatters;
|
||||
import utils.ConstText;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
|
@ -12,33 +12,39 @@ import java.util.Scanner;
|
|||
* Prompts and stores the configuration that the app needs to be run with.
|
||||
*/
|
||||
public class AppConfigService {
|
||||
String countryName;
|
||||
Date date;
|
||||
private String countryName;
|
||||
public String getCountryName() {return countryName;}
|
||||
private Date date;
|
||||
public Date getDate() {return date;}
|
||||
|
||||
public void promptUserArguments() {
|
||||
final Scanner scanner = new Scanner(System.in);
|
||||
System.out.println(ConstValues.inputCountryName);
|
||||
countryName = scanner.nextLine();
|
||||
System.out.println();
|
||||
/**
|
||||
* Prompts the user to input the country name and date, and stores the input.
|
||||
* It will keep prompting the user until the date is in the correct format.
|
||||
*/
|
||||
public void promptAndSetUserArguments() {
|
||||
// Closing even on an uncaught exception
|
||||
try (Scanner scanner = new Scanner(System.in)) {
|
||||
System.out.println(ConstText.inputCountryName);
|
||||
countryName = scanner.nextLine();
|
||||
System.out.println();
|
||||
|
||||
System.out.println(ConstValues.inputDate);
|
||||
while (true) {
|
||||
try {
|
||||
date = ConstFormatters.APPLICATION_DATE_FORMATTER.parse(scanner.nextLine());
|
||||
// Always using exact current date. Otherwise, different day possible at the start of the run.
|
||||
if (date.after(new Date())) {
|
||||
System.out.println("There is no data available for the future yet... Try again:");
|
||||
continue;
|
||||
System.out.println(ConstText.inputDate);
|
||||
while (true) {
|
||||
try {
|
||||
date = Formatters.APPLICATION_DATE_FORMATTER.parse(scanner.nextLine());
|
||||
// Always using exact current date. Otherwise, different day possible at the start of the run.
|
||||
if (date.after(new Date())) {
|
||||
System.out.println(ConstText.dateInFuture);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not doing anymore month/day validations.
|
||||
|
||||
break;
|
||||
} catch (ParseException e) {
|
||||
System.out.printf(ConstText.unableParseDate, Formatters.APPLICATION_DATE_FORMATTER.toPattern());
|
||||
}
|
||||
|
||||
// Not doing anymore month/day validations.
|
||||
|
||||
break;
|
||||
} catch (ParseException e) {
|
||||
System.out.printf("Unable to parse date. Should be in format '%s'.\nTry again:%n", ConstFormatters.APPLICATION_DATE_FORMATTER.toPattern());
|
||||
}
|
||||
}
|
||||
|
||||
scanner.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,58 +2,63 @@ package services;
|
|||
|
||||
import apis.DatabaseApi;
|
||||
import data.dtos.QueryDTO;
|
||||
import data.enums.CustomReadPreparedStatements;
|
||||
import data.repos.QueryResultsRepository;
|
||||
import utils.ConstText;
|
||||
|
||||
import javax.xml.transform.Result;
|
||||
import java.sql.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Stores the active connection between the app and the local SQL Server.
|
||||
* Responsible for storing the active connection between the application and the SQL server.
|
||||
* It creates the connection to the PostgreSQL server and provides a way to execute read queries and persist their results.
|
||||
* It uses DatabaseApi class for performing read queries.
|
||||
*/
|
||||
public class DatabaseService {
|
||||
private final String url = "jdbc:postgresql://localhost:5432/lunatech_covid";
|
||||
private final String user = "postgres";
|
||||
private final String password = "postgres";
|
||||
private static final String url = "jdbc:postgresql://localhost:5432/lunatech_covid";
|
||||
private static final String user = "postgres";
|
||||
private static final String password = "postgres";
|
||||
|
||||
private final DatabaseApi databaseApi;
|
||||
private final Connection connection;
|
||||
|
||||
public DatabaseService() {
|
||||
connection = connect();
|
||||
connection = getConnection();
|
||||
databaseApi = new DatabaseApi();
|
||||
}
|
||||
|
||||
public ResultSet[] executeReadReportsEndpoint(CustomReadPreparedStatements customPreparedStatement) {
|
||||
final QueryDTO queryDTO = new QueryDTO(customPreparedStatement, new Object[]{});
|
||||
final PreparedStatement resultStatement = databaseApi.performQuery(queryDTO, connection);
|
||||
try {
|
||||
final ArrayList<ResultSet> resultSets = new ArrayList<>();
|
||||
do {
|
||||
resultSets.add(resultStatement.getResultSet());
|
||||
}
|
||||
while (resultStatement.getMoreResults());
|
||||
/**
|
||||
* Fetches read-query results and packages them to persist them (and so frees the connection).
|
||||
*
|
||||
* @param queryDTO The query with its arguments.
|
||||
* @return Persisting (non-lazy) results from executing the query.
|
||||
*/
|
||||
public QueryResultsRepository getReadReportsEndpoint(QueryDTO queryDTO) throws SQLException {
|
||||
final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection);
|
||||
|
||||
return resultSets.toArray(new ResultSet[0]);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
final QueryResultsRepository resultSets = new QueryResultsRepository(queryDTO.statement().description);
|
||||
do {
|
||||
final ResultSet resultSet = resultStatement.getResultSet();
|
||||
resultSets.addResult(resultSet);
|
||||
}
|
||||
while (resultStatement.getMoreResults());
|
||||
|
||||
return resultSets;
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Connect to the PostgreSQL database
|
||||
*
|
||||
* @return a Connection object
|
||||
*/
|
||||
private Connection connect() {
|
||||
private Connection getConnection() {
|
||||
Connection connection = null;
|
||||
try {
|
||||
connection = DriverManager.getConnection(url, user, password);
|
||||
System.out.println("Connected to the PostgreSQL server successfully.");
|
||||
System.out.println(ConstText.successServerConnection);
|
||||
} catch (SQLException e) {
|
||||
System.out.println(e.getMessage());
|
||||
System.out.println(ConstText.failureServerConnection);
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
src/services/ReportGenerationService.java
Normal file
21
src/services/ReportGenerationService.java
Normal file
|
@ -0,0 +1,21 @@
|
|||
package services;
|
||||
|
||||
import data.repos.QueryResultsRepository;
|
||||
|
||||
/**
|
||||
* Generates a report from results
|
||||
*
|
||||
* Since only a std-out is the only strategy, composition is preferred.
|
||||
* If more strategies are needed in the future, just add in a ReporterApi that delegates the task
|
||||
* to any (strategy) class that implements it.
|
||||
* A basic printer is provided, more members can be added to use print out the received results differently.
|
||||
*/
|
||||
public class ReportGenerationService {
|
||||
/**
|
||||
* Prints results to the console.
|
||||
* @param queryResultsRepository
|
||||
*/
|
||||
public void reportBaseResults(QueryResultsRepository queryResultsRepository) {
|
||||
System.out.println(queryResultsRepository);
|
||||
}
|
||||
}
|
24
src/utils/ConstText.java
Normal file
24
src/utils/ConstText.java
Normal file
|
@ -0,0 +1,24 @@
|
|||
package utils;
|
||||
|
||||
/**
|
||||
* Contains string constants used in the application for the purpose of user I/O.
|
||||
* <p>
|
||||
* Effectively a simpler standalone for l10n localization.
|
||||
*/
|
||||
public class ConstText {
|
||||
//D
|
||||
public static final String dateInFuture = "There is no data available for the future yet... Try again:";
|
||||
|
||||
//F
|
||||
public static final String failureServerConnection = "Unable to connect to PostgreSQL server.";
|
||||
|
||||
//I
|
||||
public static final String inputCountryName = "Enter country: ";
|
||||
public static final String inputDate = "Enter date: ";
|
||||
|
||||
//S
|
||||
public static final String successServerConnection = "Connected to the PostgreSQL server successfully.";
|
||||
|
||||
//U
|
||||
public static final String unableParseDate = "Unable to parse date. Should be in format '%s'.\nTry again:%n";
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package data.constants;
|
||||
package utils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class ConstFormatters{
|
||||
public class Formatters {
|
||||
final public static SimpleDateFormat APPLICATION_DATE_FORMATTER = new SimpleDateFormat("dd-MM-yyyy");
|
||||
}
|
Loading…
Reference in a new issue