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/
|
client/target/
|
||||||
core/target/
|
core/target/
|
||||||
server/target/
|
server/target/
|
||||||
|
server-registry/target/
|
||||||
/*.iml
|
/*.iml
|
1
pom.xml
1
pom.xml
|
@ -12,6 +12,7 @@
|
||||||
<module>server</module>
|
<module>server</module>
|
||||||
<module>client</module>
|
<module>client</module>
|
||||||
<module>core</module>
|
<module>core</module>
|
||||||
|
<module>server-registry</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<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