Added first version of server registry for online server discovery.
This commit is contained in:
parent
10eed8e8cd
commit
9c5cde199e
|
@ -2,4 +2,5 @@
|
|||
client/target/
|
||||
core/target/
|
||||
server/target/
|
||||
server-registry/target/
|
||||
/*.iml
|
1
pom.xml
1
pom.xml
|
@ -12,6 +12,7 @@
|
|||
<module>server</module>
|
||||
<module>client</module>
|
||||
<module>core</module>
|
||||
<module>server-registry</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
) {}
|
|
@ -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
|
||||
) {}
|
|
@ -0,0 +1,7 @@
|
|||
package nl.andrewlalis.aos_server_registry.servlet.dto;
|
||||
|
||||
public record ServerStatusUpdate (
|
||||
String name,
|
||||
String address,
|
||||
int currentPlayers
|
||||
) {}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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);
|
Loading…
Reference in New Issue