Implemented buy package function

~5h work, multiple failed ideas
This commit is contained in:
Benedikt Galbavy 2024-01-06 19:22:13 +01:00
parent 43a6e3596f
commit 2cc7a33c8c
14 changed files with 235 additions and 49 deletions

7
.idea/sqldialects.xml generated Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$APPLICATION_CONFIG_DIR$/consoles/db/720ef4bc-36c4-4861-8776-49b639600223/console.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/setup.sql" dialect="PostgreSQL" />
</component>
</project>

View File

@ -18,7 +18,7 @@ public class Main {
/* packages */ /* packages */
router.addRoute(HttpMethod.POST, "/packages", new PackagesService(), new int[]{}); 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 */ /* cards */
router.addRoute(HttpMethod.GET, "/cards", new CardsService(), new int[]{}); router.addRoute(HttpMethod.GET, "/cards", new CardsService(), new int[]{});

View File

@ -0,0 +1,4 @@
package at.nanopenguin.mtcg;
public record Pair<T, U>(T left, U right) {
}

View File

@ -1,14 +1,16 @@
package at.nanopenguin.mtcg.application; package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.Pair;
import at.nanopenguin.mtcg.application.service.schemas.Card; import at.nanopenguin.mtcg.application.service.schemas.Card;
import at.nanopenguin.mtcg.db.DbQuery; import at.nanopenguin.mtcg.db.DbQuery;
import at.nanopenguin.mtcg.db.SqlCommand; import at.nanopenguin.mtcg.db.SqlCommand;
import at.nanopenguin.mtcg.db.Table; import at.nanopenguin.mtcg.db.Table;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.val; import lombok.val;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.List; import java.util.*;
import java.util.UUID;
public class Package { public class Package {
public static boolean create(List<Card> cards) throws SQLException { public static boolean create(List<Card> cards) throws SQLException {
@ -42,25 +44,51 @@ public class Package {
return true; return true;
} }
public static boolean addToUser(UUID userUuid) throws SQLException { public synchronized static Pair<PurchaseStatus, List<Card>> addToUser(UUID userUuid) throws SQLException {
return DbQuery.builder() 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(""" .customSql("""
DO $$ UPDATE cards
DECLARE user_uuid uuid; SET owner = (?::uuid), package = null
DECLARE package_uuid uuid; WHERE package = (
DECLARE cost int; SELECT uuid
BEGIN FROM packages
cost = ?; ORDER BY created_at
user_uuid = ?::uuid; LIMIT 1
IF (SELECT coins FROM users WHERE uuid = user_uuid) >= cost THEN )
package_uuid = (SELECT uuid FROM packages ORDER BY created_at LIMIT 1); RETURNING uuid AS id, name, damage;
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
.value(userUuid) .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<List<Card>>() {}));
} }
} }

View File

@ -0,0 +1,7 @@
package at.nanopenguin.mtcg.application;
public enum PurchaseStatus {
SUCCESS,
NOT_ENOUGH_MONEY,
NO_PACKAGE_AVAILABLE
}

View File

@ -58,7 +58,7 @@ public final class SessionHandler {
return uuid; return uuid;
} }
public static UUID uuidFromHttpHeader(String headerValue) { public static UUID tokenFromHttpHeader(String headerValue) {
return headerValue == null ? null : UUID.fromString(headerValue.replaceFirst("^Bearer ", "")); return headerValue == null ? null : UUID.fromString(headerValue.replaceFirst("^Bearer ", ""));
} }
@ -85,4 +85,8 @@ public final class SessionHandler {
return TokenValidity.FORBIDDEN; return TokenValidity.FORBIDDEN;
} }
public UUID userUuidFromToken(UUID token) {
return this.sessions.containsKey(token) ? this.sessions.get(token).id() : null;
}
} }

View File

@ -1,6 +1,7 @@
package at.nanopenguin.mtcg.application.service; package at.nanopenguin.mtcg.application.service;
import at.nanopenguin.mtcg.application.Package; import at.nanopenguin.mtcg.application.Package;
import at.nanopenguin.mtcg.application.PurchaseStatus;
import at.nanopenguin.mtcg.application.SessionHandler; import at.nanopenguin.mtcg.application.SessionHandler;
import at.nanopenguin.mtcg.application.TokenValidity; import at.nanopenguin.mtcg.application.TokenValidity;
import at.nanopenguin.mtcg.application.service.schemas.Card; 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.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.val;
import java.sql.SQLException; import java.sql.SQLException;
import java.util.Arrays; import java.util.Arrays;
@ -21,10 +23,10 @@ public class PackagesService implements Service {
@Override @Override
public Response handleRequest(HttpRequest request) throws JsonProcessingException, SQLException, ArrayIndexOutOfBoundsException { 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) { if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) {
UUID uuid; return switch (SessionHandler.getInstance().verifyUUID(token, true)) {
return switch (SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), true)) {
case MISSING, INVALID -> new Response(HttpStatus.UNAUTHORIZED); case MISSING, INVALID -> new Response(HttpStatus.UNAUTHORIZED);
case FORBIDDEN -> new Response(HttpStatus.FORBIDDEN); case FORBIDDEN -> new Response(HttpStatus.FORBIDDEN);
case VALID -> new Response( 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) { if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 3)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) {
return new Response(SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization"))) != TokenValidity.VALID ? if (SessionHandler.getInstance().verifyUUID(token) != TokenValidity.VALID) return new Response(HttpStatus.UNAUTHORIZED);
HttpStatus.UNAUTHORIZED : val result = Package.addToUser(SessionHandler.getInstance().userUuidFromToken(token));
HttpStatus.NOT_IMPLEMENTED); // Package.addToUser(); 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); return new Response(HttpStatus.NOT_FOUND);

View File

@ -32,7 +32,7 @@ public class UserService implements Service {
return switch (request.getMethod()) { return switch (request.getMethod()) {
case GET -> { case GET -> {
String username = request.getPath().split("/")[2]; 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); yield new Response(HttpStatus.UNAUTHORIZED);
val userData = User.retrieve(username); val userData = User.retrieve(username);
yield userData != null ? yield userData != null ?
@ -46,7 +46,7 @@ public class UserService implements Service {
case PUT -> { case PUT -> {
String username = request.getPath().split("/")[2]; String username = request.getPath().split("/")[2];
UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class); 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 new Response(HttpStatus.UNAUTHORIZED);
yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND); yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND);
} }

View File

@ -1,9 +1,10 @@
package at.nanopenguin.mtcg.application.service.schemas; package at.nanopenguin.mtcg.application.service.schemas;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.UUID; import java.util.UUID;
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public record Card(UUID id, String name, Float damage) { public record Card(@JsonProperty("id") UUID id, String name, Float damage) {
} }

View File

@ -28,14 +28,15 @@ public final class DbQuery {
private String customSql; private String customSql;
@Singular @Singular
private List<Object> values; private List<Object> values;
private boolean isScript;
public static class DbQueryBuilder { public static class DbQueryBuilder {
public List<Map<String, Object>> executeQuery() throws SQLException { public List<Map<String, Object>> executeQuery() throws SQLException {
DbQuery dbQuery = this.build(); 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.command != SqlCommand.SELECT && dbQuery.returnColumn == null) throw new SQLException();
if (dbQuery.customSql != null) return DbQuery.executeQuery(dbQuery.customSql, dbQuery.values);
return switch (dbQuery.command) { return switch (dbQuery.command) {
case INSERT -> dbQuery.create(true); case INSERT -> dbQuery.create(true);
case SELECT -> dbQuery.read(); case SELECT -> dbQuery.read();
@ -45,9 +46,9 @@ public final class DbQuery {
} }
public int executeUpdate() throws SQLException { public int executeUpdate() throws SQLException {
if (this.returnColumn != null) throw new SQLException();
DbQuery dbQuery = this.build(); 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) { return switch (dbQuery.command) {
case INSERT -> dbQuery.create(); case INSERT -> dbQuery.create();
case UPDATE -> dbQuery.update(); case UPDATE -> dbQuery.update();
@ -55,6 +56,24 @@ public final class DbQuery {
default -> throw new SQLException(); 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 { private static Connection connect() throws SQLException {
@ -69,27 +88,56 @@ public final class DbQuery {
} }
private static int executeUpdate(String sql, List<Object> parameterValues) throws SQLException { private static int executeUpdate(String sql, List<Object> parameterValues) throws SQLException {
try (Connection connection = connect()) { return executeUpdate(sql, parameterValues, false);
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1;
for (val value : parameterValues) {
preparedStatement.setObject(i++, value);
} }
return preparedStatement.executeUpdate(); private static int executeUpdate(String sql, List<Object> 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.executeUpdate();
}
}
private static boolean execute(String sql, List<Object> parameterValues) throws SQLException {
return execute(sql, parameterValues, false);
}
private static boolean execute(String sql, List<Object> 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<Map<String, Object>> executeQuery(String sql, List<Object> parameterValues) throws SQLException { private static List<Map<String, Object>> executeQuery(String sql, List<Object> parameterValues) throws SQLException {
try (Connection connection = connect()) { return executeQuery(sql, parameterValues, false);
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1;
for (val value : parameterValues) {
preparedStatement.setObject(i++, value);
} }
ResultSet resultSet = preparedStatement.executeQuery(); private static List<Map<String, Object>> executeQuery(String sql, List<Object> 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);
}
try (ResultSet resultSet = statementExecutor.executeQuery()) {
List<Map<String, Object>> result = new ArrayList<>(); List<Map<String, Object>> result = new ArrayList<>();
while (resultSet.next()) { while (resultSet.next()) {
@ -103,6 +151,7 @@ public final class DbQuery {
return result; return result;
} }
} }
}
private String getInsertStatement(boolean hasReturn) throws SQLException { private String getInsertStatement(boolean hasReturn) throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException("No parameters provided for INSERT statement."); if (this.parameters.isEmpty()) throw new SQLException("No parameters provided for INSERT statement.");

View File

@ -5,4 +5,5 @@ public enum SqlCommand {
SELECT, SELECT,
UPDATE, UPDATE,
DELETE, DELETE,
CUSTOM,
} }

View File

@ -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();
}
}

View File

@ -6,7 +6,8 @@ import lombok.RequiredArgsConstructor;
public enum Table { public enum Table {
USERS("users"), USERS("users"),
CARDS("cards"), CARDS("cards"),
PACKAGES("packages"); PACKAGES("packages"),
NAN(null);
public final String table; public final String table;
} }

View File

@ -6,6 +6,7 @@ import lombok.RequiredArgsConstructor;
import java.io.*; import java.io.*;
import java.net.Socket; import java.net.Socket;
import java.util.Arrays;
@RequiredArgsConstructor @RequiredArgsConstructor
public class RequestHandler implements Runnable { public class RequestHandler implements Runnable {
@ -44,6 +45,10 @@ public class RequestHandler implements Runnable {
} }
catch (Exception e) { catch (Exception e) {
System.out.println(e.getMessage()); // TODO: more info 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); response = new Response(HttpStatus.INTERNAL);
} }
} }