Added first version of server registry for online server discovery.

This commit is contained in:
Andrew Lalis 2021-06-30 12:03:38 +02:00
parent 10eed8e8cd
commit 9c5cde199e
17 changed files with 870 additions and 0 deletions

1
.gitignore vendored
View File

@ -2,4 +2,5 @@
client/target/
core/target/
server/target/
server-registry/target/
/*.iml

View File

@ -12,6 +12,7 @@
<module>server</module>
<module>client</module>
<module>core</module>
<module>server-registry</module>
</modules>
<properties>

73
server-registry/pom.xml Normal file
View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ace-of-shades</artifactId>
<groupId>nl.andrewlalis</groupId>
<version>4.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>server-registry</artifactId>
<properties>
<maven.compiler.source>16</maven.compiler.source>
<maven.compiler.target>16</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>nl.andrewlalis.aos_server_registry.ServerRegistry</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<version>2.2.8.Final</version>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<version>2.2.8.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.h2database/h2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
</dependencies>
</project>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,45 @@
package nl.andrewlalis.aos_server_registry.servlet;
import java.util.List;
public class Page<T> {
private final List<T> contents;
private final int elementCount;
private final int pageSize;
private final int currentPage;
private final boolean firstPage;
private final boolean lastPage;
public Page(List<T> 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<T> 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;
}
}

View File

@ -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<ServerInfoResponse> getData(int size, int page, String searchQuery, String order, String orderDir) throws SQLException {
final List<ServerInfoResponse> 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.");
}
}

View File

@ -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
) {}

View File

@ -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
) {}

View File

@ -0,0 +1,7 @@
package nl.andrewlalis.aos_server_registry.servlet.dto;
public record ServerStatusUpdate (
String name,
String address,
int currentPlayers
) {}

View File

@ -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> T getBody(HttpServletRequest req, Class<T> bodyClass) throws IOException {
return mapper.readValue(req.getInputStream(), bodyClass);
}
public static int getIntParam(HttpServletRequest req, String name, int defaultValue, Function<Integer, Boolean> validator) {
return getParam(req, name, defaultValue, Integer::parseInt, validator);
}
public static String getStringParam(HttpServletRequest req, String name, String defaultValue, Function<String, Boolean> validator) {
return getParam(req, name, defaultValue, s -> s, validator);
}
private static <T> T getParam(HttpServletRequest req, String name, T defaultValue, Function<String, T> parser, Function<T, Boolean> 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;
}
}
}

View File

@ -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) {}
}

View File

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