From 0213bc46066e15b9ca9bdf6da208351122728af5 Mon Sep 17 00:00:00 2001 From: Mehul Ahal <112100480+MehulAhal@users.noreply.github.com> Date: Wed, 11 Jan 2023 18:41:36 +0100 Subject: [PATCH 01/10] added DTOs and models --- .DS_Store | Bin 6148 -> 8196 bytes src/Main.java | 9 +++-- src/apis/DatabaseApi.java | 4 +-- src/data/dtos/QueryDTO.java | 4 +-- .../enums/CustomPreparedStatementsRead.java | 19 ++++++++++ .../enums/CustomReadPreparedStatements.java | 34 ------------------ src/data/models/QueryResultsModel.java | 11 ++++++ src/services/DatabaseService.java | 16 ++++----- 8 files changed, 46 insertions(+), 51 deletions(-) create mode 100644 src/data/enums/CustomPreparedStatementsRead.java delete mode 100644 src/data/enums/CustomReadPreparedStatements.java create mode 100644 src/data/models/QueryResultsModel.java diff --git a/.DS_Store b/.DS_Store index 913f2974db796c4005d47a91c9c4281f6531d131..f14155ede9815c452865117fbfff0aa758c77e28 100644 GIT binary patch delta 346 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aMAD7G? zP3B>ls>#Km$B@pD$xs5s=?r-c`3yx2sh&Cc$w@i+NgxA&csln(r2{oGPF}~h*_nkQ2PmGzP>gCU zqb$hS|6l-A%fP@4w7is|1Wi>pNY&&p_GETupglzl$w2#03W8b4Hf=1t&nUkbDb#gwt7a resultSets) { +} diff --git a/src/services/DatabaseService.java b/src/services/DatabaseService.java index 23d1a1d..c50c177 100644 --- a/src/services/DatabaseService.java +++ b/src/services/DatabaseService.java @@ -2,9 +2,9 @@ package services; import apis.DatabaseApi; import data.dtos.QueryDTO; -import data.enums.CustomReadPreparedStatements; +import data.enums.CustomPreparedStatementsRead; +import data.models.QueryResultsModel; -import javax.xml.transform.Result; import java.sql.*; import java.util.ArrayList; @@ -12,9 +12,9 @@ import java.util.ArrayList; * Stores the active connection between the app and the local SQL Server. */ 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; @@ -24,9 +24,9 @@ public class DatabaseService { databaseApi = new DatabaseApi(); } - public ResultSet[] executeReadReportsEndpoint(CustomReadPreparedStatements customPreparedStatement) { + public QueryResultsModel executeReadReportsEndpoint(CustomPreparedStatementsRead customPreparedStatement) { final QueryDTO queryDTO = new QueryDTO(customPreparedStatement, new Object[]{}); - final PreparedStatement resultStatement = databaseApi.performQuery(queryDTO, connection); + final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection); try { final ArrayList resultSets = new ArrayList<>(); do { @@ -34,7 +34,7 @@ public class DatabaseService { } while (resultStatement.getMoreResults()); - return resultSets.toArray(new ResultSet[0]); + return new QueryResultsModel(resultSets); } catch (SQLException e) { throw new RuntimeException(e); } From b35ffac6fa9f4ba43662d5500295243c61adf52a Mon Sep 17 00:00:00 2001 From: Mehul Ahal <112100480+MehulAhal@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:03:03 +0100 Subject: [PATCH 02/10] added SQL queries --- src/Main.java | 8 ++- src/apis/DatabaseApi.java | 19 ++++--- src/data/dtos/QueryDTO.java | 6 +- .../enums/CustomPreparedStatementsRead.java | 57 ++++++++++++++++++- src/services/DatabaseService.java | 9 +-- src/services/ReportGenerationService.java | 9 +++ 6 files changed, 91 insertions(+), 17 deletions(-) create mode 100644 src/services/ReportGenerationService.java diff --git a/src/Main.java b/src/Main.java index f45e1ee..c312694 100644 --- a/src/Main.java +++ b/src/Main.java @@ -1,8 +1,12 @@ +import data.dtos.QueryDTO; import data.enums.CustomPreparedStatementsRead; import data.models.QueryResultsModel; import services.AppConfigService; import services.DatabaseService; +import java.sql.ResultSet; +import java.sql.SQLException; + public class Main { public static void main(String[] args) { final App app = new App(); @@ -20,7 +24,7 @@ class App { final AppConfigService appConfigService; public void run() { - appConfigService.promptUserArguments(); - final QueryResultsModel resultSetsQuery1 = databaseService.executeReadReportsEndpoint(CustomPreparedStatementsRead.HighestAndLowest10Vaccination); + //appConfigService.promptUserArguments(); + final QueryResultsModel resultSetsQuery1 = databaseService.executeReadReportsEndpoint(new QueryDTO(CustomPreparedStatementsRead.HighestAndLowest10Vaccination)); } } \ No newline at end of file diff --git a/src/apis/DatabaseApi.java b/src/apis/DatabaseApi.java index fec0eae..55fe26b 100644 --- a/src/apis/DatabaseApi.java +++ b/src/apis/DatabaseApi.java @@ -5,13 +5,16 @@ import data.dtos.QueryDTO; import java.sql.*; public class DatabaseApi { - public PreparedStatement performReadQuery(QueryDTO queryDTO, Connection connection) { - try { - PreparedStatement statement = connection.prepareStatement(queryDTO.queryTemplate().statementTemplate); - statement.execute(); - return statement; - } catch (SQLException e) { - throw new RuntimeException(e); - } + public PreparedStatement performReadQuery(QueryDTO queryDTO, Connection connection) throws SQLException { + // Set template + PreparedStatement statement = connection.prepareStatement(queryDTO.statement().statementTemplate); + + // Add in arguments + for(int argIndex = 0; argIndex < queryDTO.templateArgs().length; argIndex++) + // parameter indexing starts at 1 + statement.setString(argIndex + 1, queryDTO.templateArgs()[argIndex].toString()); + + statement.execute(); + return statement; } } diff --git a/src/data/dtos/QueryDTO.java b/src/data/dtos/QueryDTO.java index 3d0747e..5f404f6 100644 --- a/src/data/dtos/QueryDTO.java +++ b/src/data/dtos/QueryDTO.java @@ -2,6 +2,8 @@ package data.dtos; import data.enums.CustomPreparedStatementsRead; -public record QueryDTO(CustomPreparedStatementsRead queryTemplate, Object[] placeholderReplacements) { - +public record QueryDTO(CustomPreparedStatementsRead statement, Object[] templateArgs) { + public QueryDTO(CustomPreparedStatementsRead customPreparedStatementsRead) { + this(customPreparedStatementsRead, new Object[]{}); + } } diff --git a/src/data/enums/CustomPreparedStatementsRead.java b/src/data/enums/CustomPreparedStatementsRead.java index 3df1874..49bff64 100644 --- a/src/data/enums/CustomPreparedStatementsRead.java +++ b/src/data/enums/CustomPreparedStatementsRead.java @@ -1,13 +1,68 @@ package data.enums; +/** + * Holds prepared data for Read-based SQL queries. + * + */ public enum CustomPreparedStatementsRead { + DailyInfectionsAndDeathAggregate(""" + WITH matched_countries AS ( + SELECT c.name, c.code\s + 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; + """ + ), + 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 + ; """ ), HighestInfectionsPer100K(""" - + SELECT c.name, c.population, SUM(cases.infections) as total_infections, (SUM(cases.infections) * 100000) / c.population as infections_per_100k + 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 + ; + """ + ), + + CountriesLaggingBehind(""" + SELECT c.name + FROM countries c + LEFT JOIN vaccinations v ON c.code = v.iso_country + GROUP BY c.name + HAVING SUM(v.daily_vaccinations) < c.population + ; """ ); diff --git a/src/services/DatabaseService.java b/src/services/DatabaseService.java index c50c177..f053207 100644 --- a/src/services/DatabaseService.java +++ b/src/services/DatabaseService.java @@ -24,12 +24,13 @@ public class DatabaseService { databaseApi = new DatabaseApi(); } - public QueryResultsModel executeReadReportsEndpoint(CustomPreparedStatementsRead customPreparedStatement) { - final QueryDTO queryDTO = new QueryDTO(customPreparedStatement, new Object[]{}); - final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection); + public QueryResultsModel executeReadReportsEndpoint(QueryDTO queryDTO) { try { + final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection); + final ArrayList resultSets = new ArrayList<>(); do { + // execute and persist results in a model resultSets.add(resultStatement.getResultSet()); } while (resultStatement.getMoreResults()); @@ -40,7 +41,7 @@ public class DatabaseService { } } - /** + /** * Connect to the PostgreSQL database * * @return a Connection object diff --git a/src/services/ReportGenerationService.java b/src/services/ReportGenerationService.java new file mode 100644 index 0000000..826fa6e --- /dev/null +++ b/src/services/ReportGenerationService.java @@ -0,0 +1,9 @@ +package services; + +import data.models.QueryResultsModel; + +public class ReportGenerationService { + public static void printResults(QueryResultsModel queryResultsModel) { + //queryResultsModel. + } +} From c7e9824208484dfa5700e0da7684ad44da35cf57 Mon Sep 17 00:00:00 2001 From: Mehul Ahal <112100480+MehulAhal@users.noreply.github.com> Date: Thu, 12 Jan 2023 10:54:33 +0100 Subject: [PATCH 03/10] results persistance --- src/Main.java | 31 +++++++++++--- src/apis/DatabaseApi.java | 32 ++++++++++++--- src/data/dtos/QueryDTO.java | 5 +++ .../enums/CustomPreparedStatementsRead.java | 21 ++++++---- src/data/models/PersistentResultModel.java | 41 +++++++++++++++++++ src/data/models/QueryResultsModel.java | 11 ----- src/data/repos/QueryResultsRepository.java | 28 +++++++++++++ src/services/AppConfigService.java | 4 +- src/services/DatabaseService.java | 41 ++++++++++--------- src/services/ReportGenerationService.java | 4 +- 10 files changed, 166 insertions(+), 52 deletions(-) create mode 100644 src/data/models/PersistentResultModel.java delete mode 100644 src/data/models/QueryResultsModel.java create mode 100644 src/data/repos/QueryResultsRepository.java diff --git a/src/Main.java b/src/Main.java index c312694..f942644 100644 --- a/src/Main.java +++ b/src/Main.java @@ -1,11 +1,12 @@ import data.dtos.QueryDTO; import data.enums.CustomPreparedStatementsRead; -import data.models.QueryResultsModel; +import data.repos.QueryResultsRepository; import services.AppConfigService; import services.DatabaseService; +import services.ReportGenerationService; -import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; public class Main { public static void main(String[] args) { @@ -18,13 +19,33 @@ 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; + + private static final CustomPreparedStatementsRead[] reportEndpoints = { + CustomPreparedStatementsRead.HighestAndLowest10Vaccination, + CustomPreparedStatementsRead.HighestInfectionsPer100K, + //CustomPreparedStatementsRead.CountriesLaggingBehind, + }; + + private static final CustomPreparedStatementsRead[] queryEndpoints = { + CustomPreparedStatementsRead.DailyInfectionsAndDeathAggregate, + }; public void run() { //appConfigService.promptUserArguments(); - final QueryResultsModel resultSetsQuery1 = databaseService.executeReadReportsEndpoint(new QueryDTO(CustomPreparedStatementsRead.HighestAndLowest10Vaccination)); + try { + QueryResultsRepository resultsRepository; + for (final CustomPreparedStatementsRead query : reportEndpoints) { + resultsRepository = databaseService.executeReadReportsEndpoint(new QueryDTO(query)); + reportGenerationService.reportResults(resultsRepository); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } } } \ No newline at end of file diff --git a/src/apis/DatabaseApi.java b/src/apis/DatabaseApi.java index 55fe26b..570c421 100644 --- a/src/apis/DatabaseApi.java +++ b/src/apis/DatabaseApi.java @@ -1,20 +1,42 @@ package apis; import data.dtos.QueryDTO; +import org.postgresql.util.PSQLException; import java.sql.*; +import java.util.Date; +import java.util.MissingFormatArgumentException; +/** + * Communicates with the database to perform final CRUD operations, returning any results. + * When there is a need for more than a single READ action, the Api can be abstracted, and implement into constituent action-specific APIs. + */ public class DatabaseApi { public PreparedStatement performReadQuery(QueryDTO queryDTO, Connection connection) throws SQLException { // Set template PreparedStatement statement = connection.prepareStatement(queryDTO.statement().statementTemplate); - // Add in arguments - for(int argIndex = 0; argIndex < queryDTO.templateArgs().length; argIndex++) - // parameter indexing starts at 1 - statement.setString(argIndex + 1, queryDTO.templateArgs()[argIndex].toString()); + try { + // 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(); + } 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."); + throw e; + } - statement.execute(); return statement; } + } diff --git a/src/data/dtos/QueryDTO.java b/src/data/dtos/QueryDTO.java index 5f404f6..e605d9e 100644 --- a/src/data/dtos/QueryDTO.java +++ b/src/data/dtos/QueryDTO.java @@ -2,6 +2,11 @@ package data.dtos; import data.enums.CustomPreparedStatementsRead; +/** + * Container for storing SQL statements to be executed, along with their aruments. + * @param statement + * @param templateArgs + */ public record QueryDTO(CustomPreparedStatementsRead statement, Object[] templateArgs) { public QueryDTO(CustomPreparedStatementsRead customPreparedStatementsRead) { this(customPreparedStatementsRead, new Object[]{}); diff --git a/src/data/enums/CustomPreparedStatementsRead.java b/src/data/enums/CustomPreparedStatementsRead.java index 49bff64..fcde257 100644 --- a/src/data/enums/CustomPreparedStatementsRead.java +++ b/src/data/enums/CustomPreparedStatementsRead.java @@ -7,15 +7,17 @@ package data.enums; public enum CustomPreparedStatementsRead { DailyInfectionsAndDeathAggregate(""" WITH matched_countries AS ( - SELECT c.name, c.code\s + 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; - """ + ORDER BY recorded_date + ; + """, + "10 countries with the highest vaccinations (with count), and countries with lowest number of vaccinations." ), HighestAndLowest10Vaccination(""" @@ -42,7 +44,8 @@ public enum CustomPreparedStatementsRead { ORDER BY total_vaccinations ASC LIMIT 10 ; - """ + """, + "10 countries with the highest infections per 100k inhabitants (with count)" ), HighestInfectionsPer100K(""" @@ -53,7 +56,8 @@ public enum CustomPreparedStatementsRead { ORDER BY infections_per_100k DESC LIMIT 10 ; - """ + """, + "" ), CountriesLaggingBehind(""" @@ -63,12 +67,15 @@ public enum CustomPreparedStatementsRead { GROUP BY c.name HAVING SUM(v.daily_vaccinations) < c.population ; - """ + """, + "" ); public final String statementTemplate; + public final String description; - private CustomPreparedStatementsRead(String statementTemplate) { + private CustomPreparedStatementsRead(String statementTemplate, String description) { this.statementTemplate = statementTemplate; + this.description = description; } } diff --git a/src/data/models/PersistentResultModel.java b/src/data/models/PersistentResultModel.java new file mode 100644 index 0000000..d2cce7d --- /dev/null +++ b/src/data/models/PersistentResultModel.java @@ -0,0 +1,41 @@ +package data.models; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Collectors; + +public class PersistentResultModel { + /** + * Factory constructor, that adapts a given ResultSet to PersistentResultModel. + * @param resultSet + * @throws SQLException + */ + public PersistentResultModel (ResultSet resultSet) throws SQLException { + final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); + final int columnCount = resultSetMetaData.getColumnCount(); + + final ArrayList grwingResultsGrid = new ArrayList<>(); + while (resultSet.next()) { + final Object[] currentRow = new Object[columnCount]; + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { + currentRow[columnIndex] = resultSet.getObject(columnIndex+1); + } + grwingResultsGrid.add(currentRow); + } + + resultColumns = new ResultColumnModel[columnCount]; + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { + int finalColumnIndex = columnIndex; + final var entries = grwingResultsGrid.stream().map(row -> row[finalColumnIndex]).toList().toArray(); + resultColumns[columnIndex] = new ResultColumnModel(resultSetMetaData.getColumnName(columnIndex+1), entries); + } + + } + + final public ResultColumnModel[] resultColumns; + public record ResultColumnModel(String columnName, Object[] entries){} +} diff --git a/src/data/models/QueryResultsModel.java b/src/data/models/QueryResultsModel.java deleted file mode 100644 index fdffa81..0000000 --- a/src/data/models/QueryResultsModel.java +++ /dev/null @@ -1,11 +0,0 @@ -package data.models; - -import java.sql.ResultSet; - -/** - * Stores the results obtained from executing an SQL query. - * Essentially a type-def, which is not natively supported in Java. - * @param resultSets Result of the query - */ -public record QueryResultsModel(java.util.ArrayList resultSets) { -} diff --git a/src/data/repos/QueryResultsRepository.java b/src/data/repos/QueryResultsRepository.java new file mode 100644 index 0000000..7453e5c --- /dev/null +++ b/src/data/repos/QueryResultsRepository.java @@ -0,0 +1,28 @@ +package data.repos; + + +import data.models.PersistentResultModel; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; + +/** + * Stores the results obtained from executing an SQL query. + */ +public class QueryResultsRepository { + /** + * Store for the results + */ + final private ArrayList persistentResults = new ArrayList<>(); + public PersistentResultModel[] getResults () {return persistentResults.toArray(new PersistentResultModel[]{});} + + /** + * Adds a ResultSet to the store to persist. + * @param resultSet + * @throws SQLException + */ + public void addResult(ResultSet resultSet) throws SQLException { + persistentResults.add(new PersistentResultModel(resultSet)); + } +} diff --git a/src/services/AppConfigService.java b/src/services/AppConfigService.java index b875254..19a801f 100644 --- a/src/services/AppConfigService.java +++ b/src/services/AppConfigService.java @@ -12,8 +12,8 @@ import java.util.Scanner; * Prompts and stores the configuration that the app needs to be run with. */ public class AppConfigService { - String countryName; - Date date; + public String countryName; + public Date date; public void promptUserArguments() { final Scanner scanner = new Scanner(System.in); diff --git a/src/services/DatabaseService.java b/src/services/DatabaseService.java index f053207..00b47dc 100644 --- a/src/services/DatabaseService.java +++ b/src/services/DatabaseService.java @@ -2,14 +2,12 @@ package services; import apis.DatabaseApi; import data.dtos.QueryDTO; -import data.enums.CustomPreparedStatementsRead; -import data.models.QueryResultsModel; +import data.repos.QueryResultsRepository; import java.sql.*; -import java.util.ArrayList; /** - * Stores the active connection between the app and the local SQL Server. + * Stores the active connection between the app and the SQL Server. */ public class DatabaseService { private static final String url = "jdbc:postgresql://localhost:5432/lunatech_covid"; @@ -20,25 +18,27 @@ public class DatabaseService { private final Connection connection; public DatabaseService() { - connection = connect(); + connection = getConnection(); databaseApi = new DatabaseApi(); } - public QueryResultsModel executeReadReportsEndpoint(QueryDTO queryDTO) { - try { - final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection); + /** + * Fetches read-query results and packages them to persist them (and so free the connection). + * + * @param queryDTO The query with its arguments. + * @return Persisting (non-lazy) results from executing the query. + */ + public QueryResultsRepository executeReadReportsEndpoint(QueryDTO queryDTO) throws SQLException { + final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection); - final ArrayList resultSets = new ArrayList<>(); - do { - // execute and persist results in a model - resultSets.add(resultStatement.getResultSet()); - } - while (resultStatement.getMoreResults()); - - return new QueryResultsModel(resultSets); - } catch (SQLException e) { - throw new RuntimeException(e); + final QueryResultsRepository resultSets = new QueryResultsRepository(); + do { + final ResultSet resultSet = resultStatement.getResultSet(); + resultSets.addResult(resultSet); } + while (resultStatement.getMoreResults()); + + return resultSets; } /** @@ -46,13 +46,14 @@ public class DatabaseService { * * @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."); } catch (SQLException e) { - System.out.println(e.getMessage()); + System.out.println("Unable to connect to PostgreSQL server."); + throw new RuntimeException(); } return connection; diff --git a/src/services/ReportGenerationService.java b/src/services/ReportGenerationService.java index 826fa6e..3341207 100644 --- a/src/services/ReportGenerationService.java +++ b/src/services/ReportGenerationService.java @@ -1,9 +1,9 @@ package services; -import data.models.QueryResultsModel; +import data.repos.QueryResultsRepository; public class ReportGenerationService { - public static void printResults(QueryResultsModel queryResultsModel) { + public void reportResults(QueryResultsRepository queryResultsRepository) { //queryResultsModel. } } From ffd4b3fa5b06f19b88e4b3f1ada1a05c18f876b4 Mon Sep 17 00:00:00 2001 From: Mehul Ahal <112100480+MehulAhal@users.noreply.github.com> Date: Thu, 12 Jan 2023 22:28:44 +0100 Subject: [PATCH 04/10] printing --- src/Main.java | 12 +++- .../enums/CustomPreparedStatementsRead.java | 57 ++++++++------- src/data/models/PersistentResultModel.java | 41 ----------- src/data/models/PersistentResultSetModel.java | 69 +++++++++++++++++++ src/data/repos/QueryResultsRepository.java | 23 +++++-- src/services/AppConfigService.java | 2 +- src/services/DatabaseService.java | 2 +- src/services/ReportGenerationService.java | 2 +- 8 files changed, 127 insertions(+), 81 deletions(-) delete mode 100644 src/data/models/PersistentResultModel.java create mode 100644 src/data/models/PersistentResultSetModel.java diff --git a/src/Main.java b/src/Main.java index f942644..630989a 100644 --- a/src/Main.java +++ b/src/Main.java @@ -6,7 +6,6 @@ import services.DatabaseService; import services.ReportGenerationService; import java.sql.SQLException; -import java.util.Arrays; public class Main { public static void main(String[] args) { @@ -29,7 +28,7 @@ class App { private static final CustomPreparedStatementsRead[] reportEndpoints = { CustomPreparedStatementsRead.HighestAndLowest10Vaccination, CustomPreparedStatementsRead.HighestInfectionsPer100K, - //CustomPreparedStatementsRead.CountriesLaggingBehind, + CustomPreparedStatementsRead.CountriesLaggingBehind, }; private static final CustomPreparedStatementsRead[] queryEndpoints = { @@ -37,13 +36,20 @@ class App { }; public void run() { - //appConfigService.promptUserArguments(); try { QueryResultsRepository resultsRepository; for (final CustomPreparedStatementsRead query : reportEndpoints) { resultsRepository = databaseService.executeReadReportsEndpoint(new QueryDTO(query)); reportGenerationService.reportResults(resultsRepository); } + + for(final CustomPreparedStatementsRead query: queryEndpoints) { + appConfigService.promptAndSetUserArguments(); + resultsRepository = databaseService.executeReadReportsEndpoint(new QueryDTO(query, new Object[]{appConfigService.countryName, appConfigService.date,})); + reportGenerationService.reportResults(resultsRepository); + } + + } catch (SQLException e) { throw new RuntimeException(e); } diff --git a/src/data/enums/CustomPreparedStatementsRead.java b/src/data/enums/CustomPreparedStatementsRead.java index fcde257..5af4977 100644 --- a/src/data/enums/CustomPreparedStatementsRead.java +++ b/src/data/enums/CustomPreparedStatementsRead.java @@ -7,24 +7,23 @@ package data.enums; public enum CustomPreparedStatementsRead { DailyInfectionsAndDeathAggregate(""" WITH matched_countries AS ( - SELECT c.name, c.code - FROM countries c - WHERE c.name LIKE CONCAT('%',?, '%') + 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 - ; """, - "10 countries with the highest vaccinations (with count), and countries with lowest number of vaccinations." + "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 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 @@ -34,41 +33,39 @@ public enum CustomPreparedStatementsRead { ; WITH vacc_count_per_country AS ( - SELECT iso_country, SUM(daily_vaccinations) as total_vaccinations - FROM vaccinations - GROUP BY iso_country + 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)" ), - HighestInfectionsPer100K(""" - SELECT c.name, c.population, SUM(cases.infections) as total_infections, (SUM(cases.infections) * 100000) / c.population as infections_per_100k - 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 - ; - """, - "" - ), - CountriesLaggingBehind(""" SELECT c.name - FROM countries c - LEFT JOIN vaccinations v ON c.code = v.iso_country - GROUP BY c.name - HAVING SUM(v.daily_vaccinations) < c.population - ; + 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; diff --git a/src/data/models/PersistentResultModel.java b/src/data/models/PersistentResultModel.java deleted file mode 100644 index d2cce7d..0000000 --- a/src/data/models/PersistentResultModel.java +++ /dev/null @@ -1,41 +0,0 @@ -package data.models; - -import java.sql.Array; -import java.sql.ResultSet; -import java.sql.ResultSetMetaData; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.stream.Collectors; - -public class PersistentResultModel { - /** - * Factory constructor, that adapts a given ResultSet to PersistentResultModel. - * @param resultSet - * @throws SQLException - */ - public PersistentResultModel (ResultSet resultSet) throws SQLException { - final ResultSetMetaData resultSetMetaData = resultSet.getMetaData(); - final int columnCount = resultSetMetaData.getColumnCount(); - - final ArrayList grwingResultsGrid = new ArrayList<>(); - while (resultSet.next()) { - final Object[] currentRow = new Object[columnCount]; - for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - currentRow[columnIndex] = resultSet.getObject(columnIndex+1); - } - grwingResultsGrid.add(currentRow); - } - - resultColumns = new ResultColumnModel[columnCount]; - for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { - int finalColumnIndex = columnIndex; - final var entries = grwingResultsGrid.stream().map(row -> row[finalColumnIndex]).toList().toArray(); - resultColumns[columnIndex] = new ResultColumnModel(resultSetMetaData.getColumnName(columnIndex+1), entries); - } - - } - - final public ResultColumnModel[] resultColumns; - public record ResultColumnModel(String columnName, Object[] entries){} -} diff --git a/src/data/models/PersistentResultSetModel.java b/src/data/models/PersistentResultSetModel.java new file mode 100644 index 0000000..b11df46 --- /dev/null +++ b/src/data/models/PersistentResultSetModel.java @@ -0,0 +1,69 @@ +package data.models; + +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +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 rows = new ArrayList<>(); + final Object[] rowElements = new Object[columnCount]; + while (resultSet.next()) { + for (int columnIndex = 0; columnIndex < rowElements.length; columnIndex++) + rowElements[columnIndex] = resultSet.getObject(columnIndex+1); + + rows.add(new ResultRowModel(rowElements.clone())); + } + + resultRowEntries = new ResultRowModel[rows.size()]; + rows.toArray(resultRowEntries); + + } + + final public ResultRowModel[] resultRowEntries; + final public String[] columnNames; + 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() { + 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; + } +} diff --git a/src/data/repos/QueryResultsRepository.java b/src/data/repos/QueryResultsRepository.java index 7453e5c..d22a64b 100644 --- a/src/data/repos/QueryResultsRepository.java +++ b/src/data/repos/QueryResultsRepository.java @@ -1,7 +1,7 @@ package data.repos; -import data.models.PersistentResultModel; +import data.models.PersistentResultSetModel; import java.sql.ResultSet; import java.sql.SQLException; @@ -11,11 +11,16 @@ import java.util.ArrayList; * Stores the results obtained from executing an SQL query. */ public class QueryResultsRepository { + public QueryResultsRepository(String queryDescription) { + this.queryDescription = queryDescription; + } + /** * Store for the results */ - final private ArrayList persistentResults = new ArrayList<>(); - public PersistentResultModel[] getResults () {return persistentResults.toArray(new PersistentResultModel[]{});} + final private ArrayList persistentResults = new ArrayList<>(); + + final private String queryDescription; /** * Adds a ResultSet to the store to persist. @@ -23,6 +28,16 @@ public class QueryResultsRepository { * @throws SQLException */ public void addResult(ResultSet resultSet) throws SQLException { - persistentResults.add(new PersistentResultModel(resultSet)); + persistentResults.add(new PersistentResultSetModel(resultSet)); + } + + @Override + public String toString() { + StringBuilder templateBuilder = new StringBuilder("\n\nDESCRIPTION: ").append(queryDescription); + for (final PersistentResultSetModel result : persistentResults) { + templateBuilder.append("\n").append(result).append("\n>"); + } + + return templateBuilder.deleteCharAt(templateBuilder.length()-1).append("\n\n").toString(); } } diff --git a/src/services/AppConfigService.java b/src/services/AppConfigService.java index 19a801f..083620b 100644 --- a/src/services/AppConfigService.java +++ b/src/services/AppConfigService.java @@ -15,7 +15,7 @@ public class AppConfigService { public String countryName; public Date date; - public void promptUserArguments() { + public void promptAndSetUserArguments() { final Scanner scanner = new Scanner(System.in); System.out.println(ConstValues.inputCountryName); countryName = scanner.nextLine(); diff --git a/src/services/DatabaseService.java b/src/services/DatabaseService.java index 00b47dc..6c3e698 100644 --- a/src/services/DatabaseService.java +++ b/src/services/DatabaseService.java @@ -31,7 +31,7 @@ public class DatabaseService { public QueryResultsRepository executeReadReportsEndpoint(QueryDTO queryDTO) throws SQLException { final PreparedStatement resultStatement = databaseApi.performReadQuery(queryDTO, connection); - final QueryResultsRepository resultSets = new QueryResultsRepository(); + final QueryResultsRepository resultSets = new QueryResultsRepository(queryDTO.statement().description); do { final ResultSet resultSet = resultStatement.getResultSet(); resultSets.addResult(resultSet); diff --git a/src/services/ReportGenerationService.java b/src/services/ReportGenerationService.java index 3341207..3f08c34 100644 --- a/src/services/ReportGenerationService.java +++ b/src/services/ReportGenerationService.java @@ -4,6 +4,6 @@ import data.repos.QueryResultsRepository; public class ReportGenerationService { public void reportResults(QueryResultsRepository queryResultsRepository) { - //queryResultsModel. + System.out.println(queryResultsRepository); } } From 792d293dd40f44da86b721a16b51bce56ffb04e4 Mon Sep 17 00:00:00 2001 From: Mehul Ahal <112100480+MehulAhal@users.noreply.github.com> Date: Thu, 12 Jan 2023 23:50:28 +0100 Subject: [PATCH 05/10] added docs --- README.md | 42 ++++++++++++++++++ docs/ClassUML.png | Bin 0 -> 850030 bytes src/Main.java | 21 ++++++++- src/apis/DatabaseApi.java | 16 ++++++- src/data/dtos/QueryDTO.java | 8 +++- .../enums/CustomPreparedStatementsRead.java | 1 - src/data/models/PersistentResultSetModel.java | 24 ++++++++++ src/data/repos/QueryResultsRepository.java | 11 ++++- src/services/AppConfigService.java | 8 +++- src/services/DatabaseService.java | 6 ++- src/services/ReportGenerationService.java | 14 +++++- .../constants => utils}/ConstFormatters.java | 2 +- .../constants => utils}/ConstValues.java | 2 +- 13 files changed, 139 insertions(+), 16 deletions(-) create mode 100644 docs/ClassUML.png rename src/{data/constants => utils}/ConstFormatters.java (87%) rename src/{data/constants => utils}/ConstValues.java (87%) diff --git a/README.md b/README.md index 8fcc6af..34609f8 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,45 @@ Please use this readme as your projects readme. You can find instructions for the assignment in the [`INSTRUCTIONS.md`](INSTRUCTIONS.md) file. + +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. + +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`. + +### 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. + +### data.enums + +`data.enums` package contains the `CustomPreparedStatementsRead` enum. This enum contains all the read-only prepared statements that are used throughout the application. + +### data.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. + +### data.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. diff --git a/docs/ClassUML.png b/docs/ClassUML.png new file mode 100644 index 0000000000000000000000000000000000000000..3535f4b8a241bd70e6b4398c9543623c4073a4c6 GIT binary patch literal 850030 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