Some rewrites + packages

~4h work
This commit is contained in:
Benedikt Galbavy 2024-01-05 16:32:46 +01:00
parent 872172cf76
commit 756d299e2c
10 changed files with 248 additions and 146 deletions

View File

@ -1,5 +1,4 @@
import at.nanopenguin.mtcg.application.service.TestService;
import at.nanopenguin.mtcg.application.service.UserService;
import at.nanopenguin.mtcg.application.service.*;
import at.nanopenguin.mtcg.http.HttpMethod;
import at.nanopenguin.mtcg.http.Router;
import at.nanopenguin.mtcg.http.Server;
@ -18,24 +17,24 @@ public class Main {
router.addRoute(HttpMethod.POST, "/sessions", new UserService(), new int[]{});
/* packages */
router.addRoute(HttpMethod.POST, "/packages", new TestService(), new int[]{});
router.addRoute(HttpMethod.POST, "/transaction/packages", new TestService(), new int[]{});
router.addRoute(HttpMethod.POST, "/packages", new PackagesService(), new int[]{});
router.addRoute(HttpMethod.POST, "/transaction/packages", new PackagesService(), new int[]{});
/* cards */
router.addRoute(HttpMethod.GET, "/cards", new TestService(), new int[]{});
router.addRoute(HttpMethod.GET, "/deck", new TestService(), new int[]{});
router.addRoute(HttpMethod.PUT, "/deck", new TestService(), new int[]{});
router.addRoute(HttpMethod.GET, "/cards", new CardsService(), new int[]{});
router.addRoute(HttpMethod.GET, "/deck", new CardsService(), new int[]{});
router.addRoute(HttpMethod.PUT, "/deck", new CardsService(), new int[]{});
/* game */
router.addRoute(HttpMethod.GET, "/stats", new TestService(), new int[]{});
router.addRoute(HttpMethod.GET, "/scoreboard", new TestService(), new int[]{});
router.addRoute(HttpMethod.POST, "/battles", new TestService(), new int[]{});
router.addRoute(HttpMethod.GET, "/stats", new GameService(), new int[]{});
router.addRoute(HttpMethod.GET, "/scoreboard", new GameService(), new int[]{});
router.addRoute(HttpMethod.POST, "/battles", new GameService(), new int[]{});
/* trading */
router.addRoute(HttpMethod.GET, "/tradings", new TestService(), new int[]{});
router.addRoute(HttpMethod.POST, "/tradings", new TestService(), new int[]{});
router.addRoute(HttpMethod.DELETE, "/tradings/{tradingDealId}", new TestService(), new int[]{2});
router.addRoute(HttpMethod.POST, "/tradings/{tradingDealId}", new TestService(), new int[]{2});
router.addRoute(HttpMethod.GET, "/tradings", new TradingService(), new int[]{});
router.addRoute(HttpMethod.POST, "/tradings", new TradingService(), new int[]{});
router.addRoute(HttpMethod.DELETE, "/tradings/{tradingDealId}", new TradingService(), new int[]{2});
router.addRoute(HttpMethod.POST, "/tradings/{tradingDealId}", new TradingService(), new int[]{2});
Server server = new Server(10001, 10, router);
server.start();

View File

@ -0,0 +1,44 @@
package at.nanopenguin.mtcg.application;
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 lombok.val;
import java.sql.SQLException;
import java.util.List;
import java.util.UUID;
public class Package {
public static boolean create(List<Card> cards) throws SQLException {
// TODO: assert package size == 5
val result = DbQuery.builder()
.command(SqlCommand.INSERT)
.table(Table.PACKAGES)
.parameter("cost", 5)
.returnColumn("uuid")
.executeQuery();
if (result.isEmpty()) throw new SQLException(); // maybe change to different exception
if (!result.get(0).containsKey("uuid")) throw new SQLException();
UUID packageUuid = (UUID) result.get(0).get("uuid");
for (val card : cards) {
val query = DbQuery.builder()
.command(SqlCommand.INSERT)
.table(Table.CARDS);
if (card.id() != null) query.parameter("uuid", card.id());
if (query.parameter("damage", card.damage())
.parameter("name", card.name())
.parameter("package", packageUuid)
.executeUpdate() != 1){
return false;
}
}
return true;
}
}

View File

@ -52,12 +52,16 @@ public final class SessionHandler {
return uuid;
}
public static UUID uuidFromHttpHeader(String headerValue) {
return headerValue == null ? null : UUID.fromString(headerValue.replaceFirst("^Bearer ", ""));
}
public boolean verifyUUID(UUID uuid) {
return verifyUUID(uuid, false);
}
public boolean verifyUUID(UUID uuid, boolean requireAdmin) {
return Sessions.containsKey(uuid) && (!requireAdmin || Sessions.get(uuid).admin());
return uuid != null && Sessions.containsKey(uuid) && (!requireAdmin || Sessions.get(uuid).admin());
}
public boolean verifyUUID(UUID uuid, String username) {
@ -65,6 +69,6 @@ public final class SessionHandler {
}
public boolean verifyUUID(UUID uuid, String username, boolean allowAdmin) {
return Sessions.containsKey(uuid) && (username.equals(Sessions.get(uuid).username()) || (allowAdmin && Sessions.get(uuid).admin()));
return uuid != null && Sessions.containsKey(uuid) && (username.equals(Sessions.get(uuid).username()) || (allowAdmin && Sessions.get(uuid).admin()));
}
}

View File

@ -1,20 +1,46 @@
package at.nanopenguin.mtcg.application;
import at.nanopenguin.mtcg.application.service.schemas.UserCredentials;
import at.nanopenguin.mtcg.application.service.schemas.UserData;
import at.nanopenguin.mtcg.db.DbQuery;
import at.nanopenguin.mtcg.db.SqlCommand;
import at.nanopenguin.mtcg.db.Table;
import at.nanopenguin.mtcg.http.HttpStatus;
import at.nanopenguin.mtcg.http.Response;
import lombok.val;
import java.sql.SQLException;
public class User {
public static int create(UserCredentials userCredentials) throws SQLException {
public static boolean create(UserCredentials userCredentials) throws SQLException {
return DbQuery.builder()
.command(SqlCommand.INSERT)
.table(Table.USERS)
.parameter("username", userCredentials.username())
.parameter("password", userCredentials.password())
.executeUpdate();
.executeUpdate() == 1;
}
public static boolean update(String username, UserData userData) throws SQLException {
return DbQuery.builder()
.command(SqlCommand.UPDATE)
.table(Table.USERS)
.parameter("name", userData.name())
.parameter("bio", userData.bio())
.parameter("image", userData.image())
.condition("username", username)
.executeUpdate() == 1;
}
public static UserData retrieve(String username) throws SQLException {
val result = DbQuery.builder()
.command(SqlCommand.SELECT)
.table(Table.USERS)
.condition("username", username)
.executeQuery();
if (result.isEmpty()) return null;
val row1 = result.get(0);
return new UserData((String) row1.get("name"), (String) row1.get("bio"), (String) row1.get("image"));
}
}

View File

@ -1,15 +0,0 @@
package at.nanopenguin.mtcg.application.service;
import at.nanopenguin.mtcg.application.service.Service;
import at.nanopenguin.mtcg.http.HttpStatus;
import at.nanopenguin.mtcg.http.HttpRequest;
import at.nanopenguin.mtcg.http.Response;
public class InternalErrorService implements Service {
/* For error in http server */
@Override
public Response handleRequest(HttpRequest request) {
return new Response(HttpStatus.INTERNAL, "application/json", "");
}
}

View File

@ -1,12 +1,22 @@
package at.nanopenguin.mtcg.application.service;
import at.nanopenguin.mtcg.application.Package;
import at.nanopenguin.mtcg.application.SessionHandler;
import at.nanopenguin.mtcg.application.service.schemas.Card;
import at.nanopenguin.mtcg.http.HttpMethod;
import at.nanopenguin.mtcg.http.HttpRequest;
import at.nanopenguin.mtcg.http.HttpStatus;
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.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
public class PackagesService implements Service {
@ -14,11 +24,21 @@ public class PackagesService implements Service {
public Response handleRequest(HttpRequest request) throws JsonProcessingException {
try {
if (request.getPath().split("/")[1].equals("packages") && request.getMethod() == HttpMethod.POST) {
return new Response(HttpStatus.NOT_IMPLEMENTED); // new ObjectMapper().readValue(request.getBody(), Card.class);
UUID uuid;
if ((uuid = SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization"))) == null)
return new Response(HttpStatus.UNAUTHORIZED); // TODO: unauthorized for invalid token
if (!SessionHandler.getInstance().verifyUUID(uuid, true))
return new Response(HttpStatus.FORBIDDEN);
return new Response(Package.create(new ObjectMapper().readValue(request.getBody(), new TypeReference<List<Card>>() {})) ?
HttpStatus.CREATED :
HttpStatus.CONFLICT);
}
if (String.join("/", Arrays.copyOfRange(request.getPath().split("/"), 1, 2)).equals("transactions/packages") && request.getMethod() == HttpMethod.POST) {
return new Response(HttpStatus.NOT_IMPLEMENTED);
return new Response(!SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization"))) ?
HttpStatus.UNAUTHORIZED :
HttpStatus.NOT_IMPLEMENTED);
}
return new Response(HttpStatus.NOT_FOUND);
@ -26,5 +46,9 @@ public class PackagesService implements Service {
catch (ArrayIndexOutOfBoundsException e) {
return new Response(HttpStatus.BAD_REQUEST);
}
catch (SQLException e) {
System.out.println(e.getMessage());
return new Response(HttpStatus.INTERNAL);
}
}
}

View File

@ -35,38 +35,23 @@ public class UserService implements Service {
return switch (request.getMethod()) {
case GET -> {
String username = request.getPath().split("/")[2];
if (request.getHttpHeader("Authorization") == null || !SessionHandler.getInstance().verifyUUID(UUID.fromString(request.getHttpHeader("Authorization").replaceFirst("^Bearer ", "")), username, true))
if (!SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), username, true))
yield new Response(HttpStatus.UNAUTHORIZED);
val result = DbQuery.builder()
.command(SqlCommand.SELECT)
.table(Table.USERS)
.condition("username", username)
.executeQuery();
if (result.isEmpty()) yield new Response(HttpStatus.NOT_FOUND);
val row1 = result.get(0);
UserData userData = new UserData((String) row1.get("name"), (String) row1.get("bio"), (String) row1.get("image"));
yield new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(userData));
val userData = User.retrieve(username);
yield userData != null ?
new Response(HttpStatus.OK, "application/json", new ObjectMapper().writeValueAsString(userData)) :
new Response(HttpStatus.NOT_FOUND);
}
case POST -> { // register new user
int success = User.create(new ObjectMapper().readValue(request.getBody(), UserCredentials.class));
yield new Response(success > 0 ? HttpStatus.CREATED : HttpStatus.CONFLICT);
UserCredentials userCredentials = new ObjectMapper().readValue(request.getBody(), UserCredentials.class);
yield new Response(User.create(userCredentials) ? HttpStatus.CREATED : HttpStatus.CONFLICT);
}
case PUT -> {
String username = request.getPath().split("/")[2];
UserData userData = new ObjectMapper().readValue(request.getBody(), UserData.class);
if (request.getHttpHeader("Authorization") == null || !SessionHandler.getInstance().verifyUUID(UUID.fromString(request.getHttpHeader("Authorization").replaceFirst("^Bearer ", "")), username, true))
if (!SessionHandler.getInstance().verifyUUID(SessionHandler.uuidFromHttpHeader(request.getHttpHeader("Authorization")), username, true))
yield new Response(HttpStatus.UNAUTHORIZED);
if (DbQuery.builder()
.command(SqlCommand.UPDATE)
.table(Table.USERS)
.parameter("name", userData.name())
.parameter("bio", userData.bio())
.parameter("image", userData.image())
.condition("username", username)
.executeUpdate() == 1) {
yield new Response(HttpStatus.OK);
}
yield new Response(HttpStatus.NOT_FOUND);
yield User.update(username, userData) ? new Response(HttpStatus.OK) : new Response(HttpStatus.NOT_FOUND);
}
default -> new Response(HttpStatus.NOT_FOUND);
};

View File

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

View File

@ -8,6 +8,7 @@ import lombok.val;
import java.sql.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Builder
public final class DbQuery {
@ -23,15 +24,24 @@ public final class DbQuery {
private SortedMap<String, Object> parameters;
@Singular
private SortedMap<String, Object> conditions;
private String returnColumn;
public static class DbQueryBuilder {
public List<Map<String, Object>> executeQuery() throws SQLException {
DbQuery dbQuery = this.build();
if (dbQuery.command != SqlCommand.SELECT) throw new SQLException();
return dbQuery.read();
if (dbQuery.command != SqlCommand.SELECT && dbQuery.returnColumn == null) throw new SQLException();
return switch (dbQuery.command) {
case INSERT -> dbQuery.create(true);
case SELECT -> dbQuery.read();
case UPDATE -> dbQuery.update(true);
default -> throw new SQLException();
};
}
public int executeUpdate() throws SQLException {
if (this.returnColumn != null) throw new SQLException();
DbQuery dbQuery = this.build();
return switch (dbQuery.command) {
case INSERT -> dbQuery.create();
@ -47,49 +57,31 @@ public final class DbQuery {
}
private String buildParameterizedString(@NonNull SortedMap<String, Object> parameters) {
if (!parameters.isEmpty()) {
return String.join(" = ?, ", parameters.keySet()) + " = ?";
}
if (parameters.isEmpty()) {
return "";
}
return String.join(" = ?, ", parameters.keySet()) + " = ?";
}
private int create() throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException("No parameters provided for INSERT statement.");
private static int executeUpdate(String sql, List<Object> parameterValues) throws SQLException {
try (Connection connection = connect()) {
String columns = this.parameters.keySet().stream()
.filter(columnName -> columnName.matches("[a-zA-Z0-9_]+"))
.collect(Collectors.joining(", "));
String sql = String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING;", table.table, columns, String.join(", ", Collections.nCopies(this.parameters.size(), "?")));
// on conflict return int equals 0
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1;
for (val entry : this.parameters.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
for (val value : parameterValues) {
preparedStatement.setObject(i++, value);
}
return preparedStatement.executeUpdate();
}
}
}
private List<Map<String, Object>> read() throws SQLException {
private static List<Map<String, Object>> executeQuery(String sql, List<Object> parameterValues) throws SQLException {
try (Connection connection = connect()) {
StringJoiner columnJoiner = new StringJoiner(", ");
if (this.columns.isEmpty()) {
columnJoiner.add("*");
}
this.columns.forEach(columnJoiner::add);
String sql = String.format("SELECT %s FROM %s%s;", columnJoiner, table.table,
this.conditions.isEmpty() ? "" : " WHERE (" + this.buildParameterizedString(this.conditions) + ")");
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1;
for (val entry : this.conditions.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
for (val value : parameterValues) {
preparedStatement.setObject(i++, value);
}
ResultSet resultSet = preparedStatement.executeQuery();
@ -106,45 +98,84 @@ public final class DbQuery {
return result;
}
}
private String getInsertStatement(boolean hasReturn) throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException("No parameters provided for INSERT statement.");
String columns = this.parameters.keySet().stream()
.filter(columnName -> columnName.matches("[a-zA-Z0-9_]+"))
.collect(Collectors.joining(", "));
return String.format("INSERT INTO %s (%s) VALUES (%s) ON CONFLICT DO NOTHING%s;",
table.table,
columns,
String.join(", ", Collections.nCopies(this.parameters.size(), "?")),
hasReturn ? " RETURNING " + this.returnColumn : "");
}
private int update() throws SQLException {
private int create() throws SQLException {
return executeUpdate(getInsertStatement(false), new ArrayList<>(this.parameters.values()));
}
private List<Map<String, Object>> create(boolean hasReturn) throws SQLException {
if (!hasReturn) {
// throw dev-is-stupid error
}
return executeQuery(getInsertStatement(true), new ArrayList<>(this.parameters.values()));
}
private List<Map<String, Object>> read() throws SQLException {
StringJoiner columnJoiner = new StringJoiner(", ");
if (this.columns.isEmpty()) {
columnJoiner.add("*");
}
this.columns.forEach(columnJoiner::add);
String sql = String.format("SELECT %s FROM %s%s;",
columnJoiner,
table.table,
this.conditions.isEmpty() ? "" : " WHERE (" + this.buildParameterizedString(this.conditions) + ")");
return executeQuery(sql, new ArrayList<>(this.conditions.values()));
}
private String getUpdateStatement(boolean hasReturn) throws SQLException {
if (this.parameters.isEmpty()) throw new SQLException();
if (this.conditions.isEmpty()) throw new SQLException();
try (Connection connection = connect()) {
String sql = String.format("UPDATE %s SET %s WHERE (%s);", table.table,
return String.format("UPDATE %s SET %s WHERE (%s)%s;",
table.table,
this.buildParameterizedString(this.parameters),
this.buildParameterizedString(this.conditions));
PreparedStatement preparedStatement = connection.prepareStatement(sql);
int i = 1;
for (val entry : this.parameters.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
}
for (val entry : this.conditions.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
this.buildParameterizedString(this.conditions),
hasReturn ? " RETURNING " + this.returnColumn : "");
}
return preparedStatement.executeUpdate();
private int update() throws SQLException {
return executeUpdate(getUpdateStatement(false), new ArrayList<>(
Stream.concat(
this.parameters.values().stream(),
this.conditions.values().stream()
).collect(Collectors.toList())));
}
private List<Map<String, Object>> update(boolean hasReturn) throws SQLException {
if (!hasReturn) {
// throw dev-is-stupid error
}
return executeQuery(getUpdateStatement(true), new ArrayList<>(
Stream.concat(
this.parameters.values().stream(),
this.conditions.values().stream()
).collect(Collectors.toList())));
}
private int delete() throws SQLException {
if (this.conditions.isEmpty()) throw new SQLException();
try (Connection connection = connect()) {
String sql = String.format("DELETE FROM %s WHERE (%s);", table.table, this.buildParameterizedString(this.conditions));
String sql = String.format("DELETE FROM %s WHERE (%s);",
table.table,
this.buildParameterizedString(this.conditions));
try (PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
int i = 1;
for (val entry : this.conditions.entrySet()) {
preparedStatement.setObject(i++, entry.getValue());
}
return preparedStatement.executeUpdate();
}
}
return executeUpdate(sql, new ArrayList<>(this.conditions.values()));
}
}

View File

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