379 lines
No EOL
12 KiB
Java
379 lines
No EOL
12 KiB
Java
package krusty;
|
|
|
|
import spark.Request;
|
|
import spark.Response;
|
|
|
|
// Likely dependencies for db operations
|
|
import java.sql.Connection;
|
|
import java.sql.DriverManager;
|
|
import java.sql.PreparedStatement;
|
|
import java.sql.SQLException;
|
|
import java.sql.Statement;
|
|
import java.text.SimpleDateFormat;
|
|
// Likely dependencies for general operations
|
|
import java.io.IOException;
|
|
import java.sql.ResultSet;
|
|
import java.util.Date;
|
|
import java.util.Optional;
|
|
import java.util.StringJoiner;
|
|
import java.util.stream.Collectors;
|
|
import java.nio.file.Files;
|
|
import java.nio.file.Paths;
|
|
import java.util.stream.Stream;
|
|
|
|
public class Database {
|
|
|
|
// Here, we use an in-memory database. This string could be changed to
|
|
// "jdbc:sqlite:<filename>.sqlite3" to use a file-based database instead.
|
|
// Nore that ":memory:" is an **SQLite specific** magic string that tells the
|
|
// underlying SQLite engine to store the database in memory.
|
|
private static final String jdbcString = "jdbc:sqlite::memory:";
|
|
// private static final String jdbcString = "jdbc:sqlite:krusty.db";
|
|
|
|
// Hold a single connection to the database. Note that this is
|
|
// not a pool, so this is not thread-safe nor efficient.
|
|
private Connection conn = null;
|
|
|
|
public String getCustomers(Request req, Response res) {
|
|
String result = selectQuery("customers", "customers", "customer_name", "customer_address");
|
|
result = result.replaceAll("customer_name", "name");
|
|
result = result.replaceAll("customer_address", "address");
|
|
return result;
|
|
}
|
|
|
|
public String getRawMaterials(Request req, Response res) {
|
|
String result = selectQuery("raw_materials", "raw-materials", "ingredient_name", "ingredient_quantity", "unit");
|
|
result = result.replaceAll("ingredient_name", "name");
|
|
result = result.replaceAll("ingredient_quantity", "amount");
|
|
return result;
|
|
}
|
|
|
|
public String getCookies(Request req, Response res) {
|
|
String result = selectQuery("cookies", "cookies", "cookie_name");
|
|
result = result.replaceAll("cookie_name", "name");
|
|
return result;
|
|
}
|
|
|
|
public String getRecipes(Request req, Response res) {
|
|
// Essentially serialize DefaultRecipes to json
|
|
return "{}";
|
|
}
|
|
|
|
public String getPallets(Request req, Response res) {
|
|
// These queries look like:
|
|
// http://localhost:8888/api/v1/pallets?cookie=Nut+cookie&from=2024-05-23&to=2024-05-30&blocked=yes
|
|
// They may contain any combination of the parameters, or none at all.
|
|
|
|
Optional<Recipe> r = Optional.empty(); // Holds the ecipe if we have a cookie
|
|
Optional<Boolean> blocked = Optional.empty(); // Holds the blocked status
|
|
Optional<Date> from = Optional.empty(); // Holds the from date
|
|
Optional<Date> to = Optional.empty(); // Holds the to date
|
|
|
|
// First we need the cookie parameter
|
|
String cookie = req.queryParams("cookie");
|
|
|
|
// And the blocked parameter
|
|
String blocked_str = req.queryParams("blocked");
|
|
|
|
// Then we need the date parameters
|
|
String from_str = req.queryParams("from");
|
|
String to_str = req.queryParams("to");
|
|
|
|
// Fancy functional one-liner to get the recipe if the cookie is present
|
|
if (cookie != null) {
|
|
r = Optional.ofNullable(DefaultRecipes.recipes.stream()
|
|
.filter(recipe -> recipe.name.equals(cookie))
|
|
.findFirst().orElse(null));
|
|
}
|
|
|
|
if (blocked_str != null) {
|
|
blocked = switch (blocked_str) {
|
|
case "yes" -> Optional.of(true);
|
|
case "no" -> Optional.of(false);
|
|
default -> Optional.empty();
|
|
};
|
|
}
|
|
|
|
if(from_str != null) {
|
|
try {
|
|
from = Optional.of(new SimpleDateFormat("yyyy-MM-dd").parse(from_str));
|
|
} catch (Exception e) {
|
|
from = Optional.empty();
|
|
}
|
|
}
|
|
|
|
if(to_str != null) {
|
|
try {
|
|
to = Optional.of(new SimpleDateFormat("yyyy-MM-dd").parse(to_str));
|
|
} catch (Exception e) {
|
|
to = Optional.empty();
|
|
}
|
|
}
|
|
|
|
// If the interval is negative, reset the dates
|
|
if(from.isPresent() && to.isPresent() && from.get().after(to.get())) {
|
|
from = Optional.empty();
|
|
to = Optional.empty();
|
|
}
|
|
|
|
// This type of code is unreadable, error prone and hard to maintain.
|
|
// The fact that im responsible for this code makes my soul hurt.
|
|
// This part almost made me write a simple query factory to handle this.
|
|
//
|
|
// SqlBuilder exists to 'take the pain out of generating SQL queries',
|
|
// but it's not in the standard library.
|
|
//
|
|
// Helmets, seatbelts and safety goggles on; we need to execute a query.
|
|
try {
|
|
Statement stmt = conn.createStatement();
|
|
StringBuilder query = new StringBuilder(
|
|
"SELECT cookie_name, status FROM pallets JOIN cookies ON pallets.cookie_id = cookies.cookie_id");
|
|
|
|
// r is validated here
|
|
if (r.isPresent()) {
|
|
query.append(" WHERE cookie_name = '" + r.get().name + "'");
|
|
}
|
|
|
|
// If both from and to are present
|
|
if (from.isPresent()) {
|
|
String query_from = new SimpleDateFormat("yyyy-MM-dd").format(from.get());
|
|
|
|
// Super hacky, low quality code
|
|
String clause = query.toString().contains("WHERE") ? " AND " : " WHERE ";
|
|
|
|
query.append(clause + "production_date >= '" + query_from + "'");
|
|
}
|
|
|
|
if(to.isPresent()) {
|
|
String query_to = new SimpleDateFormat("yyyy-MM-dd").format(to.get());
|
|
|
|
// Super hacky, low quality code
|
|
String clause = query.toString().contains("WHERE") ? " AND " : " WHERE ";
|
|
|
|
query.append(clause + "production_date <= '" + query_to + "'");
|
|
}
|
|
|
|
// if (from.isPresent() && to.isPresent()) {
|
|
// String query_from = new SimpleDateFormat("yyyy-MM-dd").format(from.get());
|
|
// String query_to = new SimpleDateFormat("yyyy-MM-dd").format(to.get());
|
|
|
|
// // Super hacky, low quality code
|
|
// String clause = query.toString().contains("WHERE") ? " AND " : " WHERE ";
|
|
|
|
// query.append(clause + "production_date BETWEEN '" + query_from + "' AND '" + query_to + "'");
|
|
// }
|
|
|
|
if(blocked.isPresent()) {
|
|
// This again
|
|
String clause = query.toString().contains("WHERE") ? " AND " : " WHERE ";
|
|
query.append(clause);
|
|
|
|
// TODO: WARNING This logic is flawed. WARNING
|
|
// Remember, status can be 'freezer', 'delivered' or 'blocked'
|
|
query.append("status = " + (blocked.get() ? "'blocked'" : "'freezer'"));
|
|
}
|
|
|
|
query.append(";");
|
|
|
|
System.out.println(query.toString());
|
|
|
|
ResultSet result = stmt.executeQuery(query.toString());
|
|
|
|
// Rename the columns
|
|
String jsonResult = Jsonizer.toJson(result, "pallets");
|
|
|
|
// Some carmack level code, as usual
|
|
jsonResult = jsonResult.replaceAll("cookie_name", "cookie");
|
|
jsonResult = jsonResult.replaceAll("freezer", "no");
|
|
jsonResult = jsonResult.replaceAll("delivered", "no");
|
|
jsonResult = jsonResult.replaceAll("blocked", "yes");
|
|
jsonResult = jsonResult.replaceAll("status", "blocked");
|
|
|
|
return jsonResult;
|
|
} catch (SQLException e) {
|
|
System.out.printf("Error executing query: \n%s", e);
|
|
}
|
|
|
|
// Statue 500, to give the client a
|
|
// chance to figure out that something went wrong
|
|
res.status(500);
|
|
return "{\"pallets\":[]}";
|
|
}
|
|
|
|
public String reset(Request req, Response res) {
|
|
try {
|
|
this.migrateScript("Migrations/create-schema.sql");
|
|
this.migrateScript("Migrations/initial-data.sql");
|
|
} catch (Exception e) {
|
|
System.out.printf("Error resetting database: \n%s", e);
|
|
res.status(500);
|
|
return "{}";
|
|
}
|
|
return "{}";
|
|
}
|
|
|
|
public String createPallet(Request req, Response res) {
|
|
// This on only has one query param and looks like:
|
|
// http://localhost:8888/api/v1/pallets?cookie=Amneris
|
|
|
|
Optional<Recipe> r = Optional.empty();
|
|
String cookie = req.queryParams("cookie");
|
|
|
|
if (cookie != null) {
|
|
r = Optional.ofNullable(DefaultRecipes.recipes.stream()
|
|
.filter(recipe -> recipe.name.equals(cookie))
|
|
.findFirst().orElse(null));
|
|
}
|
|
|
|
if (r.isEmpty()) {
|
|
// Return 404
|
|
res.status(404);
|
|
return "{}";
|
|
}
|
|
|
|
// System.out.println(r.get());
|
|
|
|
try (PreparedStatement getRawMaterials = conn
|
|
.prepareStatement("SELECT * FROM raw_materials WHERE ingredient_name = ?");
|
|
PreparedStatement decrementRawMaterials = conn.prepareStatement(
|
|
"UPDATE raw_materials SET ingredient_quantity = ingredient_quantity - ? WHERE ingredient_name = ?");
|
|
PreparedStatement insertPallet = conn.prepareStatement(
|
|
"INSERT INTO pallets (cookie_id, production_date, status) VALUES (?, ?, ?)");
|
|
PreparedStatement getCookieId = conn
|
|
.prepareStatement("SELECT cookie_id FROM cookies WHERE cookie_name = ?")) {
|
|
// Start transaction
|
|
conn.setAutoCommit(false);
|
|
|
|
for (Ingredient i : r.get().ingredients) {
|
|
getRawMaterials.setString(1, i.name);
|
|
ResultSet result = getRawMaterials.executeQuery();
|
|
if (!result.next()) {
|
|
// Rollback transaction
|
|
conn.rollback();
|
|
// Return 500
|
|
res.status(500);
|
|
return "{}";
|
|
}
|
|
|
|
// Check if we have enough raw materials
|
|
if (result.getInt("ingredient_quantity") < i.amount) {
|
|
// Rollback transaction
|
|
conn.rollback();
|
|
// Return 500
|
|
res.status(500);
|
|
return "{}";
|
|
}
|
|
|
|
decrementRawMaterials.setInt(1, i.amount * 54); // 5400 / 100
|
|
decrementRawMaterials.setString(2, i.name);
|
|
decrementRawMaterials.executeUpdate();
|
|
}
|
|
|
|
// Fish out the cookie id
|
|
getCookieId.setString(1, cookie);
|
|
ResultSet cookie_rs = getCookieId.executeQuery();
|
|
if (!cookie_rs.next()) {
|
|
// Rollback transaction
|
|
conn.rollback();
|
|
// Return 500
|
|
res.status(500);
|
|
return "{}";
|
|
}
|
|
|
|
int cookie_id = cookie_rs.getInt("cookie_id");
|
|
|
|
insertPallet.setInt(1, cookie_id);
|
|
insertPallet.setString(2, new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
|
|
insertPallet.setString(3, "freezer");
|
|
|
|
insertPallet.executeUpdate();
|
|
conn.commit();
|
|
} catch (SQLException e) {
|
|
System.out.printf("Error starting transaction: \n%s", e);
|
|
}
|
|
|
|
// TODO: NOT DONE
|
|
|
|
// 1. Get query param
|
|
// 2. Check if cookie exists (is in DefaultRecipes)
|
|
// 3. Start transaction
|
|
// 3. Check with db if raw materials are available -> decrement if so
|
|
// 4. Insert into pallets
|
|
// 5. Commit transaction
|
|
// 6. Return pallet id
|
|
return "{}";
|
|
}
|
|
|
|
/** Connects to the database using the configured jdbcString. */
|
|
public void connect() {
|
|
try {
|
|
conn = DriverManager.getConnection(jdbcString);
|
|
} catch (Exception e) {
|
|
System.out.println(e.getMessage());
|
|
} finally {
|
|
System.out.println("Connection to SQLite has been established.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Selects columns from a table and returns the result as a JSON string.
|
|
* Does _absolutely no_ query sanitization, so be careful with user input.
|
|
*/
|
|
private String selectQuery(String table, String jsonName, String... columns) {
|
|
String jsonResult = "{}"; // Valid json to return if fail
|
|
|
|
try {
|
|
Statement stmt = this.conn.createStatement();
|
|
StringBuilder query = new StringBuilder("SELECT ");
|
|
|
|
StringJoiner args = new StringJoiner(", ");
|
|
for (String column : columns) {
|
|
args.add(column);
|
|
}
|
|
|
|
query.append(args.toString());
|
|
query.append(" FROM " + table + ";");
|
|
|
|
/* Sanitization is for cowards */
|
|
|
|
ResultSet result = stmt.executeQuery(query.toString());
|
|
jsonResult = Jsonizer.toJson(result, jsonName);
|
|
} catch (SQLException e) {
|
|
System.out.printf("Error executing query: \n%s", e);
|
|
}
|
|
|
|
return jsonResult;
|
|
}
|
|
|
|
// The script location is relative to the gradle
|
|
// build script ("build.gradle.kts", in this case).
|
|
/** Reads an sql script into the database */
|
|
public void migrateScript(String filename) throws IOException, SQLException {
|
|
try (Stream<String> lines = Files.lines(Paths.get(filename))) {
|
|
|
|
// Combine into one big string, with all comments and empty lines removed.
|
|
String[] statements = lines.filter(line -> !line.startsWith("--") && !line.isBlank())
|
|
.map(line -> line.replaceAll("--.*", "").replaceAll("\\s+", " ").trim())
|
|
.collect(Collectors.joining("\n")).split(";");
|
|
|
|
for (String query : statements) {
|
|
try (Statement statement = conn.createStatement()) {
|
|
statement.execute(query);
|
|
statement.close();
|
|
} catch (SQLException e) {
|
|
System.err.println("Error executing script: " + e.getMessage());
|
|
throw e;
|
|
}
|
|
}
|
|
System.out.println(String.format("Executed script %s", filename));
|
|
|
|
} catch (IOException e) {
|
|
System.err.println("Error reading script file: " + e.getMessage());
|
|
throw e;
|
|
} catch (SQLException e) {
|
|
String prepend = String.format("Error executing script: %s", filename);
|
|
System.err.println(prepend + e.getMessage());
|
|
throw e;
|
|
}
|
|
}
|
|
} |