From 2cc7a33c8cba6562376b27f8fc6015a1238a0b63 Mon Sep 17 00:00:00 2001 From: Benedikt Galbavy Date: Sat, 6 Jan 2024 19:22:13 +0100 Subject: [PATCH] Implemented buy package function ~5h work, multiple failed ideas --- .idea/sqldialects.xml | 7 ++ src/Main.java | 2 +- src/at/nanopenguin/mtcg/Pair.java | 4 + .../nanopenguin/mtcg/application/Package.java | 68 ++++++++++----- .../mtcg/application/PurchaseStatus.java | 7 ++ .../mtcg/application/SessionHandler.java | 6 +- .../application/service/PackagesService.java | 21 +++-- .../mtcg/application/service/UserService.java | 4 +- .../application/service/schemas/Card.java | 3 +- src/at/nanopenguin/mtcg/db/DbQuery.java | 83 +++++++++++++++---- src/at/nanopenguin/mtcg/db/SqlCommand.java | 1 + .../mtcg/db/StatementExecutor.java | 70 ++++++++++++++++ src/at/nanopenguin/mtcg/db/Table.java | 3 +- .../nanopenguin/mtcg/http/RequestHandler.java | 5 ++ 14 files changed, 235 insertions(+), 49 deletions(-) create mode 100644 .idea/sqldialects.xml create mode 100644 src/at/nanopenguin/mtcg/Pair.java create mode 100644 src/at/nanopenguin/mtcg/application/PurchaseStatus.java create mode 100644 src/at/nanopenguin/mtcg/db/StatementExecutor.java diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..546db64 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Main.java b/src/Main.java index c04b4ff..9d2967a 100644 --- a/src/Main.java +++ b/src/Main.java @@ -18,7 +18,7 @@ public class Main { /* packages */ router.addRoute(HttpMethod.POST, "/packages", new PackagesService(), new int[]{}); - router.addRoute(HttpMethod.POST, "/transaction/packages", new PackagesService(), new int[]{}); + router.addRoute(HttpMethod.POST, "/transactions/packages", new PackagesService(), new int[]{}); /* cards */ router.addRoute(HttpMethod.GET, "/cards", new CardsService(), new int[]{}); diff --git a/src/at/nanopenguin/mtcg/Pair.java b/src/at/nanopenguin/mtcg/Pair.java new file mode 100644 index 0000000..b5a0d97 --- /dev/null +++ b/src/at/nanopenguin/mtcg/Pair.java @@ -0,0 +1,4 @@ +package at.nanopenguin.mtcg; + +public record Pair(T left, U right) { +} diff --git a/src/at/nanopenguin/mtcg/application/Package.java b/src/at/nanopenguin/mtcg/application/Package.java index 21c75c1..8cc4dd5 100644 --- a/src/at/nanopenguin/mtcg/application/Package.java +++ b/src/at/nanopenguin/mtcg/application/Package.java @@ -1,14 +1,16 @@ package at.nanopenguin.mtcg.application; +import at.nanopenguin.mtcg.Pair; import at.nanopenguin.mtcg.application.service.schemas.Card; import at.nanopenguin.mtcg.db.DbQuery; import at.nanopenguin.mtcg.db.SqlCommand; import at.nanopenguin.mtcg.db.Table; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.val; import java.sql.SQLException; -import java.util.List; -import java.util.UUID; +import java.util.*; public class Package { public static boolean create(List cards) throws SQLException { @@ -42,25 +44,51 @@ public class Package { return true; } - public static boolean addToUser(UUID userUuid) throws SQLException { - return DbQuery.builder() + public synchronized static Pair> addToUser(UUID userUuid) throws SQLException { + int coins = (int) DbQuery.builder() + .command(SqlCommand.SELECT) + .table(Table.USERS) + .column("coins") + .condition("uuid", userUuid) + .executeQuery() + .get(0) + .get("coins"); + if (coins < 5) { + return new Pair<>(PurchaseStatus.NOT_ENOUGH_MONEY, null); + } + + if ((long) DbQuery.builder() + .command(SqlCommand.SELECT) + .table(Table.PACKAGES) + .column("COUNT(*)") + .executeQuery() + .get(0) + .get("count") == 0) { + return new Pair<>(PurchaseStatus.NO_PACKAGE_AVAILABLE, null); + } + + val result = DbQuery.builder() .customSql(""" - DO $$ - DECLARE user_uuid uuid; - DECLARE package_uuid uuid; - DECLARE cost int; - BEGIN - cost = ?; - user_uuid = ?::uuid; - IF (SELECT coins FROM users WHERE uuid = user_uuid) >= cost THEN - package_uuid = (SELECT uuid FROM packages ORDER BY created_at LIMIT 1); - UPDATE cards SET owner = user_uuid WHERE package = package_uuid; - UPDATE users SET coins = coins - cost WHERE uuid = user_uuid; - DELETE FROM packages WHERE uuid = package_uuid; - END IF; - END $$;""") - .value(5) // TODO: don't hardcode cost + UPDATE cards + SET owner = (?::uuid), package = null + WHERE package = ( + SELECT uuid + FROM packages + ORDER BY created_at + LIMIT 1 + ) + RETURNING uuid AS id, name, damage; + """) .value(userUuid) - .executeUpdate() > 0; + .executeQuery(); + + DbQuery.builder() + .command(SqlCommand.UPDATE) + .table(Table.USERS) + .parameter("coins", coins - 5) + .condition("uuid", userUuid) + .executeUpdate(); + + return new Pair<>(PurchaseStatus.SUCCESS, new ObjectMapper().convertValue(result, new TypeReference>() {})); } } diff --git a/src/at/nanopenguin/mtcg/application/PurchaseStatus.java b/src/at/nanopenguin/mtcg/application/PurchaseStatus.java new file mode 100644 index 0000000..2fb7b0d --- /dev/null +++ b/src/at/nanopenguin/mtcg/application/PurchaseStatus.java @@ -0,0 +1,7 @@ +package at.nanopenguin.mtcg.application; + +public enum PurchaseStatus { + SUCCESS, + NOT_ENOUGH_MONEY, + NO_PACKAGE_AVAILABLE +} diff --git a/src/at/nanopenguin/mtcg/application/SessionHandler.java b/src/at/nanopenguin/mtcg/application/SessionHandler.java index 435d149..ebbf2be 100644 --- a/src/at/nanopenguin/mtcg/application/SessionHandler.java +++ b/src/at/nanopenguin/mtcg/application/SessionHandler.java @@ -58,7 +58,7 @@ public final class SessionHandler { return uuid; } - public static UUID uuidFromHttpHeader(String headerValue) { + public static UUID tokenFromHttpHeader(String headerValue) { return headerValue == null ? null : UUID.fromString(headerValue.replaceFirst("^Bearer ", "")); } @@ -85,4 +85,8 @@ public final class SessionHandler { return TokenValidity.FORBIDDEN; } + + public UUID userUuidFromToken(UUID token) { + return this.sessions.containsKey(token) ? this.sessions.get(token).id() : null; + } } \ No newline at end of file diff --git a/src/at/nanopenguin/mtcg/application/service/PackagesService.java b/src/at/nanopenguin/mtcg/application/service/PackagesService.java index 337d978..ba56586 100644 --- a/src/at/nanopenguin/mtcg/application/service/PackagesService.java +++ b/src/at/nanopenguin/mtcg/application/service/PackagesService.java @@ -1,6 +1,7 @@ package at.nanopenguin.mtcg.application.service; import at.nanopenguin.mtcg.application.Package; +import at.nanopenguin.mtcg.application.PurchaseStatus; import at.nanopenguin.mtcg.application.SessionHandler; import at.nanopenguin.mtcg.application.TokenValidity; import at.nanopenguin.mtcg.application.service.schemas.Card; @@ -11,6 +12,7 @@ import at.nanopenguin.mtcg.http.Response; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.val; import java.sql.SQLException; import java.util.Arrays; @@ -21,10 +23,10 @@ public class PackagesService implements Service { @Override public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException { + UUID token = SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")); if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) { - UUID uuid; - return switch (SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), true)) { + return switch (SessionHandler.getInstance().verifyUUID(token, true)) { case MISSING, INVALID -> new Response(HttpStatus.UNAUTHORIZED); case FORBIDDEN -> new Response(HttpStatus.FORBIDDEN); case VALID -> new Response( @@ -35,10 +37,17 @@ public class PackagesService implements Service { }; } - if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 2)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) { - return new Response(SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization"))) != TokenValidity.VALID ? - HttpStatus.UNAUTHORIZED : - HttpStatus.NOT_IMPLEMENTED); // Package.addToUser(); + if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 3)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) { + if (SessionHandler.getInstance().verifyUUID(token) != TokenValidity.VALID) return new Response(HttpStatus.UNAUTHORIZED); + val result = Package.addToUser(SessionHandler.getInstance().userUuidFromToken(token)); + if (result.left() == PurchaseStatus.SUCCESS) { + return new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(result.right())); + } + return new Response(switch (Package.addToUser(SessionHandler.getInstance().userUuidFromToken(token)).left()) { + case NO_PACKAGE_AVAILABLE -> HttpStatus.NOT_FOUND; + case NOT_ENOUGH_MONEY -> HttpStatus.FORBIDDEN; + default -> HttpStatus.INTERNAL; + }); } return new Response(HttpStatus.NOT_FOUND); diff --git a/src/at/nanopenguin/mtcg/application/service/UserService.java b/src/at/nanopenguin/mtcg/application/service/UserService.java index 929ce94..cf2f390 100644 --- a/src/at/nanopenguin/mtcg/application/service/UserService.java +++ b/src/at/nanopenguin/mtcg/application/service/UserService.java @@ -32,7 +32,7 @@ public class UserService implements Service { return switch (request.getMethod()) { case GET -> { String username = request.getPath().split("/")[2]; - if (SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID) + if (SessionHandler.getInstance().verifyUUID(SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID) yield new Response(HttpStatus.UNAUTHORIZED); val userData = User.retrieve(username); yield userData != null ? @@ -46,7 +46,7 @@ public class UserService implements Service { case PUT -> { String username = request.getPath().split("/")[2]; UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class); - if (SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.FORBIDDEN) + if (SessionHandler.getInstance().verifyUUID(SessionHandler.tokenFromHttpHeader(request.getHttpHeader("Authorization")), username, true) != TokenValidity.VALID) yield new Response(HttpStatus.UNAUTHORIZED); yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND); } diff --git a/src/at/nanopenguin/mtcg/application/service/schemas/Card.java b/src/at/nanopenguin/mtcg/application/service/schemas/Card.java index c34bc0e..ff93860 100644 --- a/src/at/nanopenguin/mtcg/application/service/schemas/Card.java +++ b/src/at/nanopenguin/mtcg/application/service/schemas/Card.java @@ -1,9 +1,10 @@ package at.nanopenguin.mtcg.application.service.schemas; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import java.util.UUID; @JsonIgnoreProperties(ignoreUnknown = true) -public record Card(UUID id, String name, Float damage) { +public record Card(@JsonProperty("id") UUID id, String name, Float damage) { } diff --git a/src/at/nanopenguin/mtcg/db/DbQuery.java b/src/at/nanopenguin/mtcg/db/DbQuery.java index fb02017..c017f7d 100644 --- a/src/at/nanopenguin/mtcg/db/DbQuery.java +++ b/src/at/nanopenguin/mtcg/db/DbQuery.java @@ -28,14 +28,15 @@ public final class DbQuery { private String customSql; @Singular private List values; + private boolean isScript; public static class DbQueryBuilder { public List> executeQuery() throws SQLException { DbQuery dbQuery = this.build(); + if (dbQuery.customSql != null) return DbQuery.executeQuery(dbQuery.customSql, dbQuery.values, dbQuery.isScript); if (dbQuery.command != SqlCommand.SELECT && dbQuery.returnColumn == null) throw new SQLException(); - if (dbQuery.customSql != null) return DbQuery.executeQuery(dbQuery.customSql, dbQuery.values); return switch (dbQuery.command) { case INSERT -> dbQuery.create(true); case SELECT -> dbQuery.read(); @@ -45,9 +46,9 @@ public final class DbQuery { } public int executeUpdate() throws SQLException { - if (this.returnColumn != null) throw new SQLException(); DbQuery dbQuery = this.build(); - if (dbQuery.customSql != null) return DbQuery.executeUpdate(dbQuery.customSql, dbQuery.values); + if (dbQuery.customSql != null) return DbQuery.executeUpdate(dbQuery.customSql, dbQuery.values, dbQuery.isScript); + if (this.returnColumn != null) throw new SQLException(); return switch (dbQuery.command) { case INSERT -> dbQuery.create(); case UPDATE -> dbQuery.update(); @@ -55,6 +56,24 @@ public final class DbQuery { default -> throw new SQLException(); }; } + + public boolean execute() throws SQLException { + DbQuery dbQuery = this.build(); + if (dbQuery.customSql == null) throw new SQLException(); + return DbQuery.execute(dbQuery.customSql, dbQuery.values, dbQuery.isScript); + } + + public DbQuery.DbQueryBuilder customSql(String customSql) { + this.customSql = customSql; + this.command = SqlCommand.CUSTOM; + this.table = Table.NAN; + return this; + } + + public DbQuery.DbQueryBuilder isScript() { + this.isScript = true; + return this; + } } private static Connection connect() throws SQLException { @@ -69,38 +88,68 @@ public final class DbQuery { } private static int executeUpdate(String sql, List parameterValues) throws SQLException { + return executeUpdate(sql, parameterValues, false); + } + + private static int executeUpdate(String sql, List parameterValues, boolean isScript) throws SQLException { try (Connection connection = connect()) { - PreparedStatement preparedStatement = connection.prepareStatement(sql); + StatementExecutor statementExecutor = isScript ? + new CallableStatementExecutor(connection.prepareCall(sql)) : + new PreparedStatementExecutor(connection.prepareStatement(sql)); int i = 1; for (val value : parameterValues) { - preparedStatement.setObject(i++, value); + statementExecutor.setObject(i++, value); } - return preparedStatement.executeUpdate(); + return statementExecutor.executeUpdate(); + } + } + + private static boolean execute(String sql, List parameterValues) throws SQLException { + return execute(sql, parameterValues, false); + } + + private static boolean execute(String sql, List parameterValues, boolean isScript) throws SQLException { + try (Connection connection = connect()) { + StatementExecutor statementExecutor = isScript ? + new CallableStatementExecutor(connection.prepareCall(sql)) : + new PreparedStatementExecutor(connection.prepareStatement(sql)); + int i = 1; + for (val value : parameterValues) { + statementExecutor.setObject(i++, value); + } + + return statementExecutor.execute(); } } private static List> executeQuery(String sql, List parameterValues) throws SQLException { - try (Connection connection = connect()) { + return executeQuery(sql, parameterValues, false); + } - PreparedStatement preparedStatement = connection.prepareStatement(sql); + private static List> executeQuery(String sql, List parameterValues, boolean isScript) throws SQLException { + try (Connection connection = connect()) { + StatementExecutor statementExecutor = isScript ? + new CallableStatementExecutor(connection.prepareCall(sql)) : + new PreparedStatementExecutor(connection.prepareStatement(sql)); int i = 1; for (val value : parameterValues) { - preparedStatement.setObject(i++, value); + statementExecutor.setObject(i++, value); } - ResultSet resultSet = preparedStatement.executeQuery(); + try (ResultSet resultSet = statementExecutor.executeQuery()) { - List> result = new ArrayList<>(); - while (resultSet.next()) { - Map row = new HashMap<>(); - for (i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) { - row.put(resultSet.getMetaData().getColumnName(i), resultSet.getObject(i)); + List> result = new ArrayList<>(); + while (resultSet.next()) { + Map row = new HashMap<>(); + for (i = 1; i <= resultSet.getMetaData().getColumnCount(); i++) { + row.put(resultSet.getMetaData().getColumnName(i), resultSet.getObject(i)); + } + result.add(row); } - result.add(row); - } return result; + } } } diff --git a/src/at/nanopenguin/mtcg/db/SqlCommand.java b/src/at/nanopenguin/mtcg/db/SqlCommand.java index 515927b..001b541 100644 --- a/src/at/nanopenguin/mtcg/db/SqlCommand.java +++ b/src/at/nanopenguin/mtcg/db/SqlCommand.java @@ -5,4 +5,5 @@ public enum SqlCommand { SELECT, UPDATE, DELETE, + CUSTOM, } diff --git a/src/at/nanopenguin/mtcg/db/StatementExecutor.java b/src/at/nanopenguin/mtcg/db/StatementExecutor.java new file mode 100644 index 0000000..13056f3 --- /dev/null +++ b/src/at/nanopenguin/mtcg/db/StatementExecutor.java @@ -0,0 +1,70 @@ +package at.nanopenguin.mtcg.db; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +interface StatementExecutor { + int executeUpdate() throws SQLException; + ResultSet executeQuery() throws SQLException; + void setObject(int i, Object o) throws SQLException; + + boolean execute() throws SQLException; +} + +class PreparedStatementExecutor implements StatementExecutor { + private final PreparedStatement preparedStatement; + + public PreparedStatementExecutor(PreparedStatement preparedStatement) { + this.preparedStatement = preparedStatement; + } + + @Override + public int executeUpdate() throws SQLException { + return preparedStatement.executeUpdate(); + } + + @Override + public ResultSet executeQuery() throws SQLException { + return preparedStatement.executeQuery(); + } + + @Override + public void setObject(int i, Object o) throws SQLException { + this.preparedStatement.setObject(i, o); + } + + @Override + public boolean execute() throws SQLException { + return this.preparedStatement.execute(); + } +} + +class CallableStatementExecutor implements StatementExecutor { + private final CallableStatement callableStatement; + + public CallableStatementExecutor(CallableStatement callableStatement) { + this.callableStatement = callableStatement; + } + + @Override + public int executeUpdate() throws SQLException { + return callableStatement.executeUpdate(); + } + + @Override + public ResultSet executeQuery() throws SQLException { + return this.callableStatement.executeQuery(); + } + + @Override + public void setObject(int i, Object o) throws SQLException { + this.callableStatement.setObject(i, o); + } + + @Override + public boolean execute() throws SQLException { + return this.callableStatement.execute(); + } +} diff --git a/src/at/nanopenguin/mtcg/db/Table.java b/src/at/nanopenguin/mtcg/db/Table.java index bc57c5b..17e3244 100644 --- a/src/at/nanopenguin/mtcg/db/Table.java +++ b/src/at/nanopenguin/mtcg/db/Table.java @@ -6,7 +6,8 @@ import lombok.RequiredArgsConstructor; public enum Table { USERS("users"), CARDS("cards"), - PACKAGES("packages"); + PACKAGES("packages"), + NAN(null); public final String table; } diff --git a/src/at/nanopenguin/mtcg/http/RequestHandler.java b/src/at/nanopenguin/mtcg/http/RequestHandler.java index f8b6f3a..53160a1 100644 --- a/src/at/nanopenguin/mtcg/http/RequestHandler.java +++ b/src/at/nanopenguin/mtcg/http/RequestHandler.java @@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor; import java.io.*; import java.net.Socket; +import java.util.Arrays; @RequiredArgsConstructor public class RequestHandler implements Runnable { @@ -44,6 +45,10 @@ public class RequestHandler implements Runnable { } catch (Exception e) { System.out.println(e.getMessage()); // TODO: more info + System.out.println(Arrays.toString(e.getStackTrace()) + .replace("[", "\t") + .replace(", ", System.lineSeparator() + "\t") + .replace("]", "")); response = new Response(HttpStatus.INTERNAL); } }