diff --git a/.gitignore b/.gitignore index 2ee2b4d..6367d07 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ gradle-app.setting __MACOSX .DS_Store .vscode -krusty.sqlite3 \ No newline at end of file +krusty.sqlite3 + +*.sqlite3 +*.db diff --git a/app/Migrations/0010-tables.sql b/app/Migrations/0010-tables.sql deleted file mode 100644 index 23f64bd..0000000 --- a/app/Migrations/0010-tables.sql +++ /dev/null @@ -1,88 +0,0 @@ -CREATE TABLE IF NOT EXISTS Customers ( - CustomerID int PRIMARY KEY, - Name varchar(100), - Address varchar(255) -); - -CREATE TABLE IF NOT EXISTS Products ( - ProductID int PRIMARY KEY, - Name varchar(100) -); - -CREATE TABLE IF NOT EXISTS Recipes ( - RecipeName varchat(100), - RecipeYear Year, - ingrediences int, - ProductID int, - PRIMARY KEY (RecipeName, RecipeYear), - FOREIGN KEY (ingrediences) REFERENCES ingredience(IngredienceID), - FOREIGN KEY (ProductID) REFERENCES Products(ProductID) -); - -CREATE TABLE IF NOT EXISTS ingredience ( - IngredienceID int PRIMARY KEY, - RawMaterialName varchar(100), - amount int, - unit varchar(50), - FOREIGN KEY (RawMaterialName) REFERENCES RawMaterials(RawMaterialName) -); - -CREATE TABLE IF NOT EXISTS RawMaterials ( - RawMaterialName varchar(100) PRIMARY KEY, - Quantity int, - LastDeliveryDateTime datetime -); - -CREATE TABLE IF NOT EXISTS PalletsProduced ( - PalletID int PRIMARY KEY, - ProductID int, - ProductionDateTime datetime, - FOREIGN KEY (ProductID) REFERENCES Products (ProductID) -); - -CREATE TABLE IF NOT EXISTS PalletsDelivered ( - DeliveredID int PRIMARY KEY, - PalletID int, - DeliveryDateTime datetime, - FOREIGN KEY (PalletID) REFERENCES PalletsProduced (PalletID), - FOREIGN KEY (DeliveredID) REFERENCES Truck (Pallet) -); - -CREATE TABLE IF NOT EXISTS Truck ( - truckId int PRIMARY KEY, - capacity int, - Pallet int -); - -CREATE TABLE IF NOT EXISTS loadingBill ( - LoadingbillID int PRIMARY KEY, - adress varchar(100), - customer varchar(100), - truckID int, - FOREIGN KEY (truckID) REFERENCES Truck (truckId) -); - -CREATE TABLE IF NOT EXISTS Orders ( - OrderID int PRIMARY KEY, - CustomerID int, - ProductID int, - Quantity int, - OrderDateTime datetime, - FOREIGN KEY (CustomerID) REFERENCES Customers (CustomerID), - FOREIGN KEY (ProductID) REFERENCES Products (ProductID) -); - -CREATE TABLE IF NOT EXISTS BlockedProducts ( - BlockedProductID int PRIMARY KEY, - ProductID int, - BlockedDateTime datetime, - FOREIGN KEY (ProductID) REFERENCES Products (ProductID) -); - -CREATE TABLE IF NOT EXISTS PalletTraceability ( - TraceID int PRIMARY KEY, - location varchar(100), - locationdate datetime, - PalletID int, - FOREIGN KEY (PalletID) REFERENCES PalletsProduced (PalletID) -); diff --git a/app/Migrations/0020-data.sql b/app/Migrations/0020-data.sql deleted file mode 100644 index 2cac31f..0000000 --- a/app/Migrations/0020-data.sql +++ /dev/null @@ -1,4 +0,0 @@ --- Inserts here -INSERT INTO Customers (CustomerID, Name, Address) VALUES - (1, 'John Doe', '123 Main St'), - (2, 'Jane Smith', '456 Elm St'); \ No newline at end of file diff --git a/app/Migrations/create-schema.sql b/app/Migrations/create-schema.sql new file mode 100644 index 0000000..483694f --- /dev/null +++ b/app/Migrations/create-schema.sql @@ -0,0 +1,83 @@ +-------------------------------------------- +-- Recipe/Cookie related tables +-------------------------------------------- + +-- Our known customers, may need more fields +CREATE TABLE IF NOT EXISTS customers ( + customer_id int PRIMARY KEY, + customer_name varchar(100), + customer_address varchar(255) +); + +-- Orders from customers. +-- Keep in mind that the delivery_date may be NULL +CREATE TABLE IF NOT EXISTS orders ( + order_id int PRIMARY KEY, + customer_id int, + order_date date DEFAULT NOW, + delivery_date date, -- Set when the order hits the truck + FOREIGN KEY (customer_id) REFERENCES customers(customer_id) +); + +-------------------------------------------- +-- Recipe/Cookie related tables +-------------------------------------------- + +-- Recipes for all the cookies (essentially a list of cookies) +CREATE TABLE IF NOT EXISTS recipes ( + recipe_id int PRIMARY KEY, + recipe_name varchar(100) -- Cookie name +); + +-- "The company has a raw materials warehouse in which +-- all ingredients used in their production are stored." + +-- Describes ingredients and stock. +-- Each ingredient has 'amount' of 'unit' in stock +CREATE TABLE IF NOT EXISTS ingredients ( + ingredient_id int PRIMARY KEY, + ingredient_name varchar(100), + amount int, + unit varchar(50) +); + +-- Describes what ingredients goes into what recipe +-- Each recipe requires 'amount' of 'ingredient' +CREATE TABLE IF NOT EXISTS recipe_contents ( + recipe_id int, + ingredient_id int, + amount int, + PRIMARY KEY (recipe_id, ingredient_id) +); + +-------------------------------------------- +-- Pallet related tables +-------------------------------------------- + +-- Pallets are used to store cookies for delivery +CREATE TABLE IF NOT EXISTS pallets ( + pallet_id int PRIMARY KEY, + recipe_id int, + order_id int, + FOREIGN KEY (recipe_id) REFERENCES recipes(recipe_id), + FOREIGN KEY (order_id) REFERENCES Orders(order_id) +); + +-- What does the pallet contain? +CREATE TABLE IF NOT EXISTS pallet_contents ( + pallet_id int, + ingredient_id int, + amount int, + PRIMARY KEY (pallet_id, ingredient_id), + FOREIGN KEY (pallet_id) REFERENCES pallets(pallet_id), + FOREIGN KEY (ingredient_id) REFERENCES ingredients(ingredient_id) +); + +-- Has an order been delivered? +-- When the truck is loaded, a delivery is considered done +CREATE TABLE IF NOT EXISTS delivery_bill ( + delivery_id int PRIMARY KEY, + order_id int, + delivery_date date DEFAULT NOW, + FOREIGN KEY (order_id) REFERENCES Orders(order_id) +); \ No newline at end of file diff --git a/app/Migrations/initial-data.sql b/app/Migrations/initial-data.sql new file mode 100644 index 0000000..4dbbd05 --- /dev/null +++ b/app/Migrations/initial-data.sql @@ -0,0 +1,22 @@ +-- Inserts here +INSERT OR IGNORE INTO + customers (customer_id, customer_name, customer_address) +VALUES + (1, 'Bjudkakor AB', 'Ystad'), + (2, 'Finkakor AB', 'Helsingborg'), + (3, 'Gästkakor AB', 'Hässleholm'), + (4, 'Kaffebröd AB', 'Landskrona'), + (5, 'Kalaskakor AB', 'Trelleborg'), + (6, 'Partykakor AB', 'Kristianstad'), + (7, 'Skånekakor AB', 'Perstorp'), + (8, 'Småbröd AB', 'Malmö'); + +INSERT INTO + recipes (recipe_name) +VALUES + ('Nut ring'), + ('Nut cookie'), + ('Amneris'), + ('Tango'), + ('Almond delight'), + ('Berliner'); \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c8a22dc..807df65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,8 +18,8 @@ repositories { dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.10.2") - // testImplementation("org.skyscreamer:jsonassert:1.5.0") // For JSON assertions in tests. - // testImplementation("com.mashape.unirest:unirest-java:1.4.9") // For HTTP requests in tests. + testImplementation("org.skyscreamer:jsonassert:1.5.0") // For JSON assertions in tests. + testImplementation("com.mashape.unirest:unirest-java:1.4.9") // For HTTP requests in tests. testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.10.2") // implementation("com.google.guava:guava:33.1.0-jre") // Currently not used. diff --git a/app/src/main/java/krusty/Database.java b/app/src/main/java/krusty/Database.java index e673436..26ef811 100644 --- a/app/src/main/java/krusty/Database.java +++ b/app/src/main/java/krusty/Database.java @@ -8,21 +8,29 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.sql.Statement; -import java.io.BufferedReader; // Likely dependencies for general operations import java.io.IOException; -import java.io.FileReader; +import java.sql.ResultSet; +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:.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::memory:"; + private static final String jdbcString = "jdbc:sqlite:krusty.db"; private Connection conn = null; public String getCustomers(Request req, Response res) { - return "{}"; + 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) { @@ -30,7 +38,8 @@ public class Database { } public String getCookies(Request req, Response res) { - return "{\"cookies\":[]}"; + String result = selectQuery("recipes", "cookies", "recipe_name"); + return result; } public String getRecipes(Request req, Response res) { @@ -60,31 +69,65 @@ public class Database { } } + /** + * 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 (BufferedReader reader = new BufferedReader(new FileReader(filename))) { - StringBuilder scriptBuilder = new StringBuilder(); - String line; + try (Stream lines = Files.lines(Paths.get(filename))) { - // Read the script file line by line - while ((line = reader.readLine()) != null) { - scriptBuilder.append(line).append("\n"); - } - String script = scriptBuilder.toString().trim(); + // 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(";"); - // Execute the script - try (Statement statement = conn.createStatement()) { - statement.execute(script); + 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) { - System.err.println("Error executing script: " + e.getMessage()); + String prepend = String.format("Error executing script: %s", filename); + System.err.println(prepend + e.getMessage()); throw e; } } -} +} \ No newline at end of file diff --git a/app/src/main/java/krusty/DefaultRecipes.java b/app/src/main/java/krusty/DefaultRecipes.java new file mode 100644 index 0000000..87d8324 --- /dev/null +++ b/app/src/main/java/krusty/DefaultRecipes.java @@ -0,0 +1,57 @@ +package krusty; + +import java.util.Arrays; +import java.util.List; + +public class DefaultRecipes { + public static List recipes = Arrays.asList( + new Recipe("Nut ring", + new Ingredient[] { + new Ingredient("Flour", 450, "g"), + new Ingredient("Butter", 450, "g"), + new Ingredient("Icing sugar", 190, "g"), + new Ingredient("Roasted, chopped nuts", 225, "g") + }), + new Recipe("Nut cookie", + new Ingredient[] { + new Ingredient("Fine-ground nuts", 750, "g"), + new Ingredient("Ground, roasted nuts", 625, "g"), + new Ingredient("Bread crumbs", 125, "g"), + new Ingredient("Sugar", 375, "g"), + new Ingredient("Egg Whites", 350, "ml"), + new Ingredient("Chocolate", 50, "g") + }), + new Recipe("Amneris", + new Ingredient[] { + new Ingredient("Marzipan", 750, "g"), + new Ingredient("Butter", 250, "g"), + new Ingredient("Eggs", 250, "g"), + new Ingredient("Potato starch", 25, "g"), + new Ingredient("Wheat flour", 25, "g") + }), + new Recipe("Tango", + new Ingredient[] { + new Ingredient("Butter", 200, "g"), + new Ingredient("Sugar", 250, "g"), + new Ingredient("Flour", 300, "g"), + new Ingredient("Sodium bicarbonate", 4, "g"), + new Ingredient("Vanilla", 2, "g") + }), + new Recipe("Almond delight", + new Ingredient[] { + new Ingredient("Butter", 400, "g"), + new Ingredient("Sugar", 270, "g"), + new Ingredient("Chopped almonds", 279, "g"), + new Ingredient("Flour", 400, "g"), + new Ingredient("Cinnamon", 10, "g") + }), + new Recipe("Berliner", + new Ingredient[] { + new Ingredient("Flour", 350, "g"), + new Ingredient("Butter", 250, "g"), + new Ingredient("Icing sugar", 100, "g"), + new Ingredient("Eggs", 50, "g"), + new Ingredient("Vanilla sugar", 5, "g"), + new Ingredient("Chocolate", 50, "g") + })); +} \ No newline at end of file diff --git a/app/src/main/java/krusty/Ingredient.java b/app/src/main/java/krusty/Ingredient.java new file mode 100644 index 0000000..04be1ca --- /dev/null +++ b/app/src/main/java/krusty/Ingredient.java @@ -0,0 +1,16 @@ +package krusty; + +public class Ingredient { + public String name, unit; + public int amount; + + public Ingredient(String name, int amount, String unit) { + this.name = name; + this.amount = amount; + this.unit = unit; + } + + public String toString() { + return String.format("%s: %d %s", name, amount, unit); + } +} \ No newline at end of file diff --git a/app/src/main/java/krusty/Recipe.java b/app/src/main/java/krusty/Recipe.java new file mode 100644 index 0000000..864e39a --- /dev/null +++ b/app/src/main/java/krusty/Recipe.java @@ -0,0 +1,15 @@ +package krusty; + +public class Recipe { + public String name; + public Ingredient ingredients[]; + + public Recipe(String name, Ingredient[] ingredients) { + this.name = name; + this.ingredients = ingredients; + } + + public String toString() { + return name; + } +} \ No newline at end of file diff --git a/app/src/main/java/krusty/ServerMain.java b/app/src/main/java/krusty/ServerMain.java index d753727..8e7c2ce 100644 --- a/app/src/main/java/krusty/ServerMain.java +++ b/app/src/main/java/krusty/ServerMain.java @@ -20,8 +20,8 @@ public class ServerMain { // Here, we can migrate an arbitrary number of SQL scripts. try { - db.migrateScript("Migrations/0010-tables.sql"); - db.migrateScript("Migrations/0020-data.sql"); + db.migrateScript("Migrations/create-schema.sql"); + db.migrateScript("Migrations/initial-data.sql"); } catch (Exception e) { throw new IOError(e); } diff --git a/makefile b/makefile index 5c904f3..dec224d 100644 --- a/makefile +++ b/makefile @@ -10,4 +10,11 @@ clean: test: ./gradlew test -.PHONY: run clean test build \ No newline at end of file +dbdump: + sqlite3 app/krusty.db .dump + +migrate: + sqlite3 app/krusty.db < app/Migrations/create-schema.sql + sqlite3 app/krusty.db < app/Migrations/initial-data.sql + +.PHONY: run clean test build