code improvments
code imprs code improvements code improvs
This commit is contained in:
parent
7053085344
commit
d5730455e6
12 changed files with 132 additions and 82 deletions
|
@ -44,10 +44,13 @@ The application's main class is `Main` class, it runs the entire application. It
|
|||
### 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.
|
||||
+ 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 again, too much 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.
|
||||
|
|
|
@ -53,16 +53,44 @@ class App {
|
|||
* generates reports using the `ReportGenerationService` class, and prompts the user for input using the `AppConfigService` class.
|
||||
*/
|
||||
public void run() {
|
||||
try {
|
||||
QueryResultsRepository resultsRepository;
|
||||
for (final CustomPreparedStatementsRead query : reportEndpoints) {
|
||||
resultsRepository = databaseService.executeReadReportsEndpoint(new QueryDTO(query));
|
||||
reportGenerationService.reportBaseResults(resultsRepository);
|
||||
}
|
||||
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.executeReadReportsEndpoint(new QueryDTO(query, new Object[]{appConfigService.countryName, appConfigService.date,}));
|
||||
resultsRepository = databaseService.getReadReportsEndpoint(new QueryDTO(query, new Object[]{appConfigService.getCountryName(), appConfigService.getDate(),}));
|
||||
reportGenerationService.reportBaseResults(resultsRepository);
|
||||
}
|
||||
|
||||
|
|
|
@ -5,24 +5,22 @@ import org.postgresql.util.PSQLException;
|
|||
|
||||
import java.sql.*;
|
||||
import java.util.Date;
|
||||
import java.util.MissingFormatArgumentException;
|
||||
|
||||
/**
|
||||
* The `DatabaseApi` class provides a way to communicate with the database to perform read operations and return the results.
|
||||
* 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 `MissingFormatArgumentException`
|
||||
* 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 {
|
||||
/**
|
||||
* Creates and executes a PreparedStatement, given the QueryDTO.
|
||||
* @param queryDTO
|
||||
* @param connection
|
||||
* @return
|
||||
* @throws MissingFormatArgumentException in case of any missing argument or extra argument.
|
||||
* @throws SQLException
|
||||
* @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
|
||||
|
@ -43,12 +41,15 @@ public class DatabaseApi {
|
|||
|
||||
statement.execute();
|
||||
} catch (PSQLException e) {
|
||||
if (e.getMessage().startsWith("No value specified for parameter")) throw new MissingFormatArgumentException("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 MissingFormatArgumentException("No replacement variables needed, but were they were still supplied.");
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -2,6 +2,6 @@ package constants;
|
|||
|
||||
import java.text.SimpleDateFormat;
|
||||
|
||||
public class ConstFormatters{
|
||||
public class Formatters {
|
||||
final public static SimpleDateFormat APPLICATION_DATE_FORMATTER = new SimpleDateFormat("dd-MM-yyyy");
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ 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 templateArgsn an array of `Object` representing the arguments to be used in the statement template.
|
||||
* @param templateArgs an array of `Object` representing the arguments to be used in the statement template.
|
||||
*/
|
||||
public record QueryDTO(CustomPreparedStatementsRead statement, Object[] templateArgs) {
|
||||
/**
|
||||
|
|
|
@ -70,7 +70,7 @@ public enum CustomPreparedStatementsRead {
|
|||
public final String statementTemplate;
|
||||
public final String description;
|
||||
|
||||
private CustomPreparedStatementsRead(String statementTemplate, String description) {
|
||||
CustomPreparedStatementsRead(String statementTemplate, String description) {
|
||||
this.statementTemplate = statementTemplate;
|
||||
this.description = description;
|
||||
}
|
||||
|
|
|
@ -30,17 +30,16 @@ public class PersistentResultSetModel {
|
|||
|
||||
// Retrieving rest of the results
|
||||
final List<ResultRowModel> rows = new ArrayList<>();
|
||||
final Object[] rowElements = new Object[columnCount];
|
||||
while (resultSet.next()) {
|
||||
for (int columnIndex = 0; columnIndex < rowElements.length; columnIndex++)
|
||||
final Object[] rowElements = new Object[columnCount];
|
||||
for (int columnIndex = 0; columnIndex < columnCount; columnIndex++)
|
||||
rowElements[columnIndex] = resultSet.getObject(columnIndex+1);
|
||||
|
||||
rows.add(new ResultRowModel(rowElements.clone()));
|
||||
rows.add(new ResultRowModel(rowElements));
|
||||
}
|
||||
|
||||
resultRowEntries = new ResultRowModel[rows.size()];
|
||||
rows.toArray(resultRowEntries);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,9 +50,6 @@ public class PersistentResultSetModel {
|
|||
* Represents column names that appear in the result set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* It returns the string representation of the result set in tabular format, showing the column names and rowElements.
|
||||
*/
|
||||
final public String[] columnNames;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,38 +1,40 @@
|
|||
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 an `ArrayList` of `PersistentResultSetModel` 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.
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store for the results
|
||||
*/
|
||||
final private ArrayList<PersistentResultSetModel> persistentResults = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Represents a description of the query that was executed.
|
||||
*/
|
||||
final private String queryDescription;
|
||||
|
||||
/**
|
||||
* Creates a PersistentResultSetModel instance from given ResultSet, and adds it to the persistentResults list
|
||||
* @param resultSet
|
||||
* @throws SQLException
|
||||
*
|
||||
* @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));
|
||||
|
@ -40,11 +42,11 @@ public class QueryResultsRepository {
|
|||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder templateBuilder = new StringBuilder("\n\nDESCRIPTION: ").append(queryDescription);
|
||||
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();
|
||||
return templateBuilder.deleteCharAt(templateBuilder.length() - 1).append("\n\n").toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package services;
|
||||
|
||||
import utils.Formatters;
|
||||
import utils.ConstValues;
|
||||
import utils.ConstText;
|
||||
|
||||
import java.text.ParseException;
|
||||
|
||||
|
@ -12,37 +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;}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
final Scanner scanner = new Scanner(System.in);
|
||||
System.out.println(ConstValues.inputCountryName);
|
||||
countryName = scanner.nextLine();
|
||||
System.out.println();
|
||||
// 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 = 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("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", Formatters.APPLICATION_DATE_FORMATTER.toPattern());
|
||||
}
|
||||
}
|
||||
|
||||
scanner.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package services;
|
|||
import apis.DatabaseApi;
|
||||
import data.dtos.QueryDTO;
|
||||
import data.repos.QueryResultsRepository;
|
||||
import utils.ConstText;
|
||||
|
||||
import java.sql.*;
|
||||
|
||||
|
@ -30,7 +31,7 @@ public class DatabaseService {
|
|||
* @param queryDTO The query with its arguments.
|
||||
* @return Persisting (non-lazy) results from executing the query.
|
||||
*/
|
||||
public QueryResultsRepository executeReadReportsEndpoint(QueryDTO queryDTO) throws SQLException {
|
||||
public QueryResultsRepository getReadReportsEndpoint(QueryDTO queryDTO) throws SQLException {
|
||||
final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection);
|
||||
|
||||
final QueryResultsRepository resultSets = new QueryResultsRepository(queryDTO.statement().description);
|
||||
|
@ -52,9 +53,9 @@ public class DatabaseService {
|
|||
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("Unable to connect to PostgreSQL server.");
|
||||
System.out.println(ConstText.failureServerConnection);
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
|
|
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 +0,0 @@
|
|||
package utils;
|
||||
|
||||
public class ConstValues {
|
||||
//I
|
||||
final public static String inputCountryName = "Enter country: ";
|
||||
final public static String inputDate = "Enter date: ";
|
||||
}
|
Loading…
Reference in a new issue