From 9c5cde199eead8241d17c29ff945a2c5c13d7f35 Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Wed, 30 Jun 2021 12:03:38 +0200 Subject: [PATCH] Added first version of server registry for online server discovery. --- .gitignore | 1 + pom.xml | 1 + server-registry/pom.xml | 73 ++++ .../src/main/java/module-info.java | 14 + .../aos_server_registry/ServerRegistry.java | 46 +++ .../aos_server_registry/data/DataManager.java | 45 +++ .../data/ScriptRunner.java | 318 ++++++++++++++++++ .../data/ServerDataPruner.java | 31 ++ .../aos_server_registry/data/Transaction.java | 8 + .../aos_server_registry/servlet/Page.java | 45 +++ .../servlet/ServerInfoServlet.java | 160 +++++++++ .../servlet/dto/ServerInfoResponse.java | 11 + .../servlet/dto/ServerInfoUpdate.java | 10 + .../servlet/dto/ServerStatusUpdate.java | 7 + .../aos_server_registry/util/Requests.java | 35 ++ .../aos_server_registry/util/Responses.java | 47 +++ .../aos_server_registry/schema.sql | 18 + 17 files changed, 870 insertions(+) create mode 100644 server-registry/pom.xml create mode 100644 server-registry/src/main/java/module-info.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/DataManager.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ScriptRunner.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/Transaction.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/Page.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerStatusUpdate.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java create mode 100644 server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Responses.java create mode 100644 server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql diff --git a/.gitignore b/.gitignore index 4d76962..832142b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ client/target/ core/target/ server/target/ +server-registry/target/ /*.iml \ No newline at end of file diff --git a/pom.xml b/pom.xml index 187fae8..016bc2a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ server client core + server-registry diff --git a/server-registry/pom.xml b/server-registry/pom.xml new file mode 100644 index 0000000..0ae42d3 --- /dev/null +++ b/server-registry/pom.xml @@ -0,0 +1,73 @@ + + + + ace-of-shades + nl.andrewlalis + 4.0 + + 4.0.0 + + server-registry + + + 16 + 16 + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + + nl.andrewlalis.aos_server_registry.ServerRegistry + + + + jar-with-dependencies + + + + + make-assembly + package + + single + + + + + + + + + + io.undertow + undertow-core + 2.2.8.Final + + + io.undertow + undertow-servlet + 2.2.8.Final + + + + com.fasterxml.jackson.core + jackson-databind + 2.12.3 + + + + com.h2database + h2 + 1.4.200 + + + + \ No newline at end of file diff --git a/server-registry/src/main/java/module-info.java b/server-registry/src/main/java/module-info.java new file mode 100644 index 0000000..22a7631 --- /dev/null +++ b/server-registry/src/main/java/module-info.java @@ -0,0 +1,14 @@ +module aos_server_registry { + requires undertow.core; + requires undertow.servlet; + requires jdk.unsupported; // Needed for undertow support. + requires java.servlet; + requires com.fasterxml.jackson.databind; + requires com.h2database; + requires java.sql; + + opens nl.andrewlalis.aos_server_registry to com.fasterxml.jackson.databind; + opens nl.andrewlalis.aos_server_registry.servlet to com.fasterxml.jackson.databind; + exports nl.andrewlalis.aos_server_registry.servlet to undertow.servlet; + opens nl.andrewlalis.aos_server_registry.servlet.dto to com.fasterxml.jackson.databind; +} \ No newline at end of file diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java new file mode 100644 index 0000000..5c2171f --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/ServerRegistry.java @@ -0,0 +1,46 @@ +package nl.andrewlalis.aos_server_registry; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.undertow.Undertow; +import io.undertow.server.HttpHandler; +import io.undertow.servlet.Servlets; +import io.undertow.servlet.api.DeploymentInfo; +import io.undertow.servlet.api.DeploymentManager; +import nl.andrewlalis.aos_server_registry.data.ServerDataPruner; +import nl.andrewlalis.aos_server_registry.servlet.ServerInfoServlet; + +import javax.servlet.ServletException; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ServerRegistry { + public static final int PORT = 8567; + public static final ObjectMapper mapper = new ObjectMapper(); + + public static void main(String[] args) throws ServletException { + startServer(); + // Every few minutes, prune all stale servers from the registry. + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3); + scheduler.scheduleAtFixedRate(new ServerDataPruner(), 1, 1, TimeUnit.MINUTES); + } + + private static void startServer() throws ServletException { + DeploymentInfo servletBuilder = Servlets.deployment() + .setClassLoader(ServerRegistry.class.getClassLoader()) + .setContextPath("/") + .setDeploymentName("AOS Server Registry") + .addServlets( + Servlets.servlet("ServersInfoServlet", ServerInfoServlet.class) + .addMapping("/serverInfo") + ); + DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder); + manager.deploy(); + HttpHandler servletHandler = manager.start(); + Undertow server = Undertow.builder() + .addHttpListener(PORT, "localhost") + .setHandler(servletHandler) + .build(); + server.start(); + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/DataManager.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/DataManager.java new file mode 100644 index 0000000..10d7ff1 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/DataManager.java @@ -0,0 +1,45 @@ +package nl.andrewlalis.aos_server_registry.data; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class DataManager { + private static final String JDBC_URL = "jdbc:h2:mem:server_registry;MODE=MySQL"; + + private static DataManager instance; + + public static DataManager getInstance() throws SQLException { + if (instance == null) { + instance = new DataManager(); + instance.resetDatabase(); + } + return instance; + } + + private final Connection connection; + + private DataManager() throws SQLException { + this.connection = DriverManager.getConnection(JDBC_URL); + } + + public Connection getConnection() { + return this.connection; + } + + public void resetDatabase() throws SQLException { + var in = DataManager.class.getResourceAsStream("/nl/andrewlalis/aos_server_registry/schema.sql"); + if (in == null) throw new SQLException("Missing schema.sql. Cannot reset database."); + try { + ScriptRunner runner = new ScriptRunner(this.connection, false, true); + runner.setErrorLogWriter(new PrintWriter(System.err)); + runner.runScript(new InputStreamReader(in)); + System.out.println("Successfully reset database."); + } catch (IOException e) { + throw new SQLException(e); + } + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ScriptRunner.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ScriptRunner.java new file mode 100644 index 0000000..ae0ca26 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ScriptRunner.java @@ -0,0 +1,318 @@ +package nl.andrewlalis.aos_server_registry.data; +/* + * Slightly modified version of the com.ibatis.common.jdbc.ScriptRunner class + * from the iBATIS Apache project. Only removed dependency on Resource class + * and a constructor + * GPSHansl, 06.08.2015: regex for delimiter, rearrange comment/delimiter detection, remove some ide warnings. + */ + +/* + * Copyright 2004 Clinton Begin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import java.io.*; +import java.sql.*; +import java.text.SimpleDateFormat; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Tool to run database scripts + */ +public class ScriptRunner { + + private static final String DEFAULT_DELIMITER = ";"; + private static final Pattern SOURCE_COMMAND = Pattern.compile("^\\s*SOURCE\\s+(.*?)\\s*$", Pattern.CASE_INSENSITIVE); + + /** + * regex to detect delimiter. + * ignores spaces, allows delimiter in comment, allows an equals-sign + */ + public static final Pattern delimP = Pattern.compile("^\\s*(--)?\\s*delimiter\\s*=?\\s*([^\\s]+)+\\s*.*$", Pattern.CASE_INSENSITIVE); + + private final Connection connection; + + private final boolean stopOnError; + private final boolean autoCommit; + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + private PrintWriter logWriter = null; + @SuppressWarnings("UseOfSystemOutOrSystemErr") + private PrintWriter errorLogWriter = null; + + private String delimiter = DEFAULT_DELIMITER; + private boolean fullLineDelimiter = false; + + private String userDirectory = System.getProperty("user.dir"); + + /** + * Default constructor + */ + public ScriptRunner(Connection connection, boolean autoCommit, + boolean stopOnError) { + this.connection = connection; + this.autoCommit = autoCommit; + this.stopOnError = stopOnError; + File logFile = new File("create_db.log"); + File errorLogFile = new File("create_db_error.log"); + try { + if (logFile.exists()) { + logWriter = new PrintWriter(new FileWriter(logFile, true)); + } else { + logWriter = new PrintWriter(new FileWriter(logFile, false)); + } + } catch(IOException e){ + System.err.println("Unable to access or create the db_create log"); + } + try { + if (errorLogFile.exists()) { + errorLogWriter = new PrintWriter(new FileWriter(errorLogFile, true)); + } else { + errorLogWriter = new PrintWriter(new FileWriter(errorLogFile, false)); + } + } catch(IOException e){ + System.err.println("Unable to access or create the db_create error log"); + } + String timeStamp = new SimpleDateFormat("dd/mm/yyyy HH:mm:ss").format(new java.util.Date()); + println("\n-------\n" + timeStamp + "\n-------\n"); + printlnError("\n-------\n" + timeStamp + "\n-------\n"); + } + + public void setDelimiter(String delimiter, boolean fullLineDelimiter) { + this.delimiter = delimiter; + this.fullLineDelimiter = fullLineDelimiter; + } + + /** + * Setter for logWriter property + * + * @param logWriter - the new value of the logWriter property + */ + public void setLogWriter(PrintWriter logWriter) { + this.logWriter = logWriter; + } + + /** + * Setter for errorLogWriter property + * + * @param errorLogWriter - the new value of the errorLogWriter property + */ + public void setErrorLogWriter(PrintWriter errorLogWriter) { + this.errorLogWriter = errorLogWriter; + } + + /** + * Set the current working directory. Source commands will be relative to this. + */ + public void setUserDirectory(String userDirectory) { + this.userDirectory = userDirectory; + } + + /** + * Runs an SQL script (read in using the Reader parameter) + * + * @param filepath - the filepath of the script to run. May be relative to the userDirectory. + */ + public void runScript(String filepath) throws IOException, SQLException { + File file = new File(userDirectory, filepath); + this.runScript(new BufferedReader(new FileReader(file))); + } + + /** + * Runs an SQL script (read in using the Reader parameter) + * + * @param reader - the source of the script + */ + public void runScript(Reader reader) throws IOException, SQLException { + try { + boolean originalAutoCommit = connection.getAutoCommit(); + try { + if (originalAutoCommit != this.autoCommit) { + connection.setAutoCommit(this.autoCommit); + } + runScript(connection, reader); + } finally { + connection.setAutoCommit(originalAutoCommit); + } + } catch (IOException | SQLException e) { + throw e; + } catch (Exception e) { + throw new RuntimeException("Error running script. Cause: " + e, e); + } + } + + /** + * Runs an SQL script (read in using the Reader parameter) using the + * connection passed in + * + * @param conn - the connection to use for the script + * @param reader - the source of the script + * @throws SQLException if any SQL errors occur + * @throws IOException if there is an error reading from the Reader + */ + private void runScript(Connection conn, Reader reader) throws IOException, + SQLException { + StringBuffer command = null; + try { + LineNumberReader lineReader = new LineNumberReader(reader); + String line; + while ((line = lineReader.readLine()) != null) { + if (command == null) { + command = new StringBuffer(); + } + String trimmedLine = line.trim(); + final Matcher delimMatch = delimP.matcher(trimmedLine); + if (trimmedLine.length() < 1 + || trimmedLine.startsWith("//")) { + // Do nothing + } else if (delimMatch.matches()) { + setDelimiter(delimMatch.group(2), false); + } else if (trimmedLine.startsWith("--")) { + println(trimmedLine); + } else if (trimmedLine.length() < 1 + || trimmedLine.startsWith("--")) { + // Do nothing + } else if (!fullLineDelimiter + && trimmedLine.endsWith(getDelimiter()) + || fullLineDelimiter + && trimmedLine.equals(getDelimiter())) { + command.append(line.substring(0, line + .lastIndexOf(getDelimiter()))); + command.append(" "); + this.execCommand(conn, command, lineReader); + command = null; + } else { + command.append(line); + command.append("\n"); + } + } + if (command != null) { + this.execCommand(conn, command, lineReader); + } + if (!autoCommit) { + conn.commit(); + } + } + catch (IOException e) { + throw new IOException(String.format("Error executing '%s': %s", command, e.getMessage()), e); + } finally { + conn.rollback(); + flush(); + } + } + + private void execCommand(Connection conn, StringBuffer command, + LineNumberReader lineReader) throws IOException, SQLException { + + if (command.length() == 0) { + return; + } + + Matcher sourceCommandMatcher = SOURCE_COMMAND.matcher(command); + if (sourceCommandMatcher.matches()) { + this.runScriptFile(conn, sourceCommandMatcher.group(1)); + return; + } + + this.execSqlCommand(conn, command, lineReader); + } + + private void runScriptFile(Connection conn, String filepath) throws IOException, SQLException { + File file = new File(userDirectory, filepath); + this.runScript(conn, new BufferedReader(new FileReader(file))); + } + + private void execSqlCommand(Connection conn, StringBuffer command, + LineNumberReader lineReader) throws SQLException { + + Statement statement = conn.createStatement(); + + println(command); + + boolean hasResults = false; + try { + hasResults = statement.execute(command.toString()); + } catch (SQLException e) { + final String errText = String.format("Error executing '%s' (line %d): %s", + command, lineReader.getLineNumber(), e.getMessage()); + printlnError(errText); + System.err.println(errText); + if (stopOnError) { + throw new SQLException(errText, e); + } + } + + if (autoCommit && !conn.getAutoCommit()) { + conn.commit(); + } + + ResultSet rs = statement.getResultSet(); + if (hasResults && rs != null) { + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + for (int i = 1; i <= cols; i++) { + String name = md.getColumnLabel(i); + print(name + "\t"); + } + println(""); + while (rs.next()) { + for (int i = 1; i <= cols; i++) { + String value = rs.getString(i); + print(value + "\t"); + } + println(""); + } + } + + try { + statement.close(); + } catch (Exception e) { + // Ignore to workaround a bug in Jakarta DBCP + } + } + + private String getDelimiter() { + return delimiter; + } + + @SuppressWarnings("UseOfSystemOutOrSystemErr") + + private void print(Object o) { + if (logWriter != null) { + logWriter.print(o); + } + } + + private void println(Object o) { + if (logWriter != null) { + logWriter.println(o); + } + } + + private void printlnError(Object o) { + if (errorLogWriter != null) { + errorLogWriter.println(o); + } + } + + private void flush() { + if (logWriter != null) { + logWriter.flush(); + } + if (errorLogWriter != null) { + errorLogWriter.flush(); + } + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java new file mode 100644 index 0000000..8800a96 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/ServerDataPruner.java @@ -0,0 +1,31 @@ +package nl.andrewlalis.aos_server_registry.data; + +import java.sql.PreparedStatement; +import java.sql.SQLException; + +/** + * Scheduled task that runs once in a while and removes servers from the + * registry which have not been updated in a while. + */ +public class ServerDataPruner implements Runnable { + public static final int INTERVAL_MINUTES = 5; + @Override + public void run() { + try { + var con = DataManager.getInstance().getConnection(); + String sql = """ + DELETE FROM servers + WHERE DATEDIFF('MINUTE', servers.updated_at, CURRENT_TIMESTAMP(0)) > ? + """; + PreparedStatement stmt = con.prepareStatement(sql); + stmt.setInt(1, INTERVAL_MINUTES); + int rowCount = stmt.executeUpdate(); + stmt.close(); + if (rowCount > 0) { + System.out.println("Removed " + rowCount + " servers from registry due to inactivity."); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/Transaction.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/Transaction.java new file mode 100644 index 0000000..2c72773 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/data/Transaction.java @@ -0,0 +1,8 @@ +package nl.andrewlalis.aos_server_registry.data; + +import java.sql.Connection; +import java.sql.SQLException; + +public interface Transaction { + void execute(Connection con) throws SQLException; +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/Page.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/Page.java new file mode 100644 index 0000000..8337ad3 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/Page.java @@ -0,0 +1,45 @@ +package nl.andrewlalis.aos_server_registry.servlet; + +import java.util.List; + +public class Page { + private final List contents; + private final int elementCount; + private final int pageSize; + private final int currentPage; + private final boolean firstPage; + private final boolean lastPage; + + public Page(List contents, int currentPage, int pageSize) { + this.contents = contents; + this.elementCount = contents.size(); + this.pageSize = pageSize; + this.currentPage = currentPage; + this.firstPage = currentPage == 0; + this.lastPage = this.elementCount < this.pageSize; + } + + public List getContents() { + return contents; + } + + public int getElementCount() { + return elementCount; + } + + public int getPageSize() { + return pageSize; + } + + public int getCurrentPage() { + return currentPage; + } + + public boolean isFirstPage() { + return firstPage; + } + + public boolean isLastPage() { + return lastPage; + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java new file mode 100644 index 0000000..a4bd6a2 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/ServerInfoServlet.java @@ -0,0 +1,160 @@ +package nl.andrewlalis.aos_server_registry.servlet; + +import nl.andrewlalis.aos_server_registry.data.DataManager; +import nl.andrewlalis.aos_server_registry.servlet.dto.ServerInfoResponse; +import nl.andrewlalis.aos_server_registry.servlet.dto.ServerInfoUpdate; +import nl.andrewlalis.aos_server_registry.servlet.dto.ServerStatusUpdate; +import nl.andrewlalis.aos_server_registry.util.Requests; +import nl.andrewlalis.aos_server_registry.util.Responses; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ServerInfoServlet extends HttpServlet { + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + int page = Requests.getIntParam(req, "page", 0, i -> i >= 0); + int size = Requests.getIntParam(req, "size", 20, i -> i >= 5 && i <= 50); + String searchQuery = Requests.getStringParam(req, "q", null, s -> !s.isBlank()); + String order = Requests.getStringParam(req, "order", "name", s -> !s.isBlank() && ( + s.equalsIgnoreCase("name") || + s.equalsIgnoreCase("address") || + s.equalsIgnoreCase("location") || + s.equalsIgnoreCase("max_players") || + s.equalsIgnoreCase("current_players") + )); + String orderDir = Requests.getStringParam(req, "dir", "ASC", s -> s.equalsIgnoreCase("ASC") || s.equalsIgnoreCase("DESC")); + try { + var results = this.getData(size, page, searchQuery, order, orderDir); + Responses.ok(resp, new Page<>(results, page, size)); + } catch (SQLException t) { + t.printStackTrace(); + Responses.internalServerError(resp, "Database error."); + } + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + var info = Requests.getBody(req, ServerInfoUpdate.class); + try { + this.saveNewServer(info); + Responses.ok(resp, Map.of("message", "Server info saved.")); + } catch (SQLException e) { + e.printStackTrace(); + Responses.internalServerError(resp, "Database error."); + } + } + + @Override + protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + var status = Requests.getBody(req, ServerStatusUpdate.class); + try { + this.updateServerStatus(status); + Responses.ok(resp, Map.of("message", "Server status updated.")); + } catch (SQLException e) { + e.printStackTrace(); + Responses.internalServerError(resp, "Database error."); + } + } + + private List getData(int size, int page, String searchQuery, String order, String orderDir) throws SQLException { + final List results = new ArrayList<>(20); + var con = DataManager.getInstance().getConnection(); + String selectQuery = """ + SELECT name, address, updated_at, description, location, max_players, current_players + FROM servers + //CONDITIONS + ORDER BY name + LIMIT ? + OFFSET ? + """; + selectQuery = selectQuery.replace("ORDER BY name", "ORDER BY " + order + " " + orderDir); + if (searchQuery != null && !searchQuery.isBlank()) { + selectQuery = selectQuery.replace("//CONDITIONS", "WHERE UPPER(name) LIKE ?"); + } + PreparedStatement stmt = con.prepareStatement(selectQuery); + int index = 1; + if (searchQuery != null && !searchQuery.isBlank()) { + stmt.setString(index++, "%" + searchQuery.toUpperCase() + "%"); + } + stmt.setInt(index++, size); + stmt.setInt(index, page * size); + ResultSet rs = stmt.executeQuery(); + while (rs.next()) { + results.add(new ServerInfoResponse( + rs.getString(1), + rs.getString(2), + rs.getTimestamp(3).toInstant().atOffset(ZoneOffset.UTC).toString(), + rs.getString(4), + rs.getString(5), + rs.getInt(6), + rs.getInt(7) + )); + } + stmt.close(); + return results; + } + + private void saveNewServer(ServerInfoUpdate info) throws SQLException { + var con = DataManager.getInstance().getConnection(); + PreparedStatement stmt = con.prepareStatement("SELECT name, address FROM servers WHERE name = ? AND address = ?"); + stmt.setString(1, info.name()); + stmt.setString(2, info.address()); + ResultSet rs = stmt.executeQuery(); + boolean exists = rs.next(); + stmt.close(); + if (!exists) { + PreparedStatement createStmt = con.prepareStatement(""" + INSERT INTO servers (name, address, description, location, max_players, current_players) + VALUES (?, ?, ?, ?, ?, ?); + """); + createStmt.setString(1, info.name()); + createStmt.setString(2, info.address()); + createStmt.setString(3, info.description()); + createStmt.setString(4, info.location()); + createStmt.setInt(5, info.maxPlayers()); + createStmt.setInt(6, info.currentPlayers()); + int rowCount = createStmt.executeUpdate(); + createStmt.close(); + if (rowCount != 1) throw new SQLException("Could not insert new server."); + } else { + PreparedStatement updateStmt = con.prepareStatement(""" + UPDATE servers SET description = ?, location = ?, max_players = ?, current_players = ? + WHERE name = ? AND address = ?; + """); + updateStmt.setString(1, info.description()); + updateStmt.setString(2, info.location()); + updateStmt.setInt(3, info.maxPlayers()); + updateStmt.setInt(4, info.currentPlayers()); + updateStmt.setString(5, info.name()); + updateStmt.setString(6, info.address()); + int rowCount = updateStmt.executeUpdate(); + updateStmt.close(); + if (rowCount != 1) throw new SQLException("Could not update server."); + } + } + + private void updateServerStatus(ServerStatusUpdate status) throws SQLException { + var con = DataManager.getInstance().getConnection(); + PreparedStatement stmt = con.prepareStatement(""" + UPDATE servers SET current_players = ? + WHERE name = ? AND address = ? + """); + stmt.setInt(1, status.currentPlayers()); + stmt.setString(2, status.name()); + stmt.setString(3, status.address()); + int rowCount = stmt.executeUpdate(); + stmt.close(); + if (rowCount != 1) throw new SQLException("Could not update server status."); + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java new file mode 100644 index 0000000..eb27412 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoResponse.java @@ -0,0 +1,11 @@ +package nl.andrewlalis.aos_server_registry.servlet.dto; + +public record ServerInfoResponse( + String name, + String address, + String updatedAt, + String description, + String location, + int maxPlayers, + int currentPlayers +) {} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java new file mode 100644 index 0000000..7dce69f --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerInfoUpdate.java @@ -0,0 +1,10 @@ +package nl.andrewlalis.aos_server_registry.servlet.dto; + +public record ServerInfoUpdate ( + String name, + String address, + String description, + String location, + int maxPlayers, + int currentPlayers +) {} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerStatusUpdate.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerStatusUpdate.java new file mode 100644 index 0000000..41d3f6f --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/servlet/dto/ServerStatusUpdate.java @@ -0,0 +1,7 @@ +package nl.andrewlalis.aos_server_registry.servlet.dto; + +public record ServerStatusUpdate ( + String name, + String address, + int currentPlayers +) {} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java new file mode 100644 index 0000000..ea186b9 --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Requests.java @@ -0,0 +1,35 @@ +package nl.andrewlalis.aos_server_registry.util; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.function.Function; + +import static nl.andrewlalis.aos_server_registry.ServerRegistry.mapper; + +public class Requests { + public static T getBody(HttpServletRequest req, Class bodyClass) throws IOException { + return mapper.readValue(req.getInputStream(), bodyClass); + } + + public static int getIntParam(HttpServletRequest req, String name, int defaultValue, Function validator) { + return getParam(req, name, defaultValue, Integer::parseInt, validator); + } + + public static String getStringParam(HttpServletRequest req, String name, String defaultValue, Function validator) { + return getParam(req, name, defaultValue, s -> s, validator); + } + + private static T getParam(HttpServletRequest req, String name, T defaultValue, Function parser, Function validator) { + var values = req.getParameterValues(name); + if (values == null || values.length == 0) return defaultValue; + try { + T value = parser.apply(values[0]); + if (!validator.apply(value)) { + return defaultValue; + } + return value; + } catch (Exception e) { + return defaultValue; + } + } +} diff --git a/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Responses.java b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Responses.java new file mode 100644 index 0000000..cda999a --- /dev/null +++ b/server-registry/src/main/java/nl/andrewlalis/aos_server_registry/util/Responses.java @@ -0,0 +1,47 @@ +package nl.andrewlalis.aos_server_registry.util; + +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; + +import static nl.andrewlalis.aos_server_registry.ServerRegistry.mapper; + +/** + * Helper class which provides some convenience methods for returning simple + * JSON responses. + */ +public class Responses { + public static void ok(HttpServletResponse resp, Object body) throws IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + mapper.writeValue(resp.getOutputStream(), body); + } + + public static void badRequest(HttpServletResponse resp, String msg) throws IOException { + respond(resp, HttpServletResponse.SC_BAD_REQUEST, msg); + } + + public static void notFound(HttpServletResponse resp) throws IOException { + respond(resp, HttpServletResponse.SC_NOT_FOUND, "Not found."); + } + + public static void notFound(HttpServletResponse resp, String msg) throws IOException { + respond(resp, HttpServletResponse.SC_NOT_FOUND, msg); + } + + public static void internalServerError(HttpServletResponse resp) throws IOException { + respond(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error."); + } + + public static void internalServerError(HttpServletResponse resp, String msg) throws IOException { + respond(resp, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, msg); + } + + private static void respond(HttpServletResponse resp, int status, String msg) throws IOException { + resp.setStatus(status); + resp.setContentType("application/json"); + mapper.writeValue(resp.getOutputStream(), msg); + } + + private static record ResponseBody(String message) {} +} diff --git a/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql b/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql new file mode 100644 index 0000000..5406d57 --- /dev/null +++ b/server-registry/src/main/resources/nl/andrewlalis/aos_server_registry/schema.sql @@ -0,0 +1,18 @@ +SET MODE MySQL; + +CREATE TABLE servers ( + name VARCHAR(64) NOT NULL, + address VARCHAR(255) NOT NULL, + created_at TIMESTAMP(0) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP(0), + updated_at TIMESTAMP(0) WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0), + description VARCHAR(1024), + location VARCHAR(64), + + max_players INTEGER NOT NULL, + current_players INTEGER NOT NULL, + + PRIMARY KEY (name, address), + CHECK (max_players > 0 AND current_players >= 0) +); + +CREATE INDEX server_name_idx ON servers(name);