diff --git a/build.gradle b/build.gradle index c1e7768..dc9ef05 100644 --- a/build.gradle +++ b/build.gradle @@ -137,4 +137,4 @@ file("$rootProject.projectDir/src/main/resources/telraam/testConfig.properties") task migrateTestingDatabase(type: FlywayMigrateTask) { url = testProps.getProperty("DB_URL") baselineOnMigrate = true -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6c7b331 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.logging.level=info diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1ecd3a1..5028f28 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Sun Nov 03 23:02:12 CET 2019 -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew.bat b/gradlew.bat index 9618d8d..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,100 +1,100 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/main/java/telraam/beacon/Beacon.java b/src/main/java/telraam/beacon/Beacon.java new file mode 100644 index 0000000..faac2bc --- /dev/null +++ b/src/main/java/telraam/beacon/Beacon.java @@ -0,0 +1,57 @@ +package telraam.beacon; + +import java.io.IOException; +import java.io.InputStream; +import java.io.EOFException; +import java.net.Socket; + +/** +* Beacon is socket wrapper that listens to the sockets +* and emits BeaconMessages when enough bytes are read. +* +* Beacons are closed at the first Exception encountered. +* This could be changed if need be. +* +* @author Arthur Vercruysse +*/ +public class Beacon extends EventGenerator implements Runnable { + private Socket s; + private int messageSize = BeaconMessage.MESSAGESIZE; + + public Beacon(Socket socket, Callback> h) { + super(h); + + this.s = socket; + + new Thread(this).start(); + } + + public void run() { + this.connect(); + + byte[] buf = new byte[messageSize]; + int at = 0; + InputStream is; + + try { + is = s.getInputStream(); + } catch (IOException e) { + error(e); + return; + } + + try { + while (true) { + int c = is.read(buf, at, messageSize - at); + if (c < 0) throw new EOFException(); + at += c; + if (at == messageSize) { + this.data(new BeaconMessage(buf)); + at = 0; + } + } + } catch (IOException e) { + exit(); + } + } +} diff --git a/src/main/java/telraam/beacon/BeaconAggregator.java b/src/main/java/telraam/beacon/BeaconAggregator.java new file mode 100644 index 0000000..224d73a --- /dev/null +++ b/src/main/java/telraam/beacon/BeaconAggregator.java @@ -0,0 +1,31 @@ +package telraam.beacon; + +import java.io.IOException; + +/** +* BeaconAggregator is the main class to handle aggregate BeaconMessages. +* Register listeners to data, errors, connects and disconnects. +* +* If port is negative no server is started (should only be used in tests). +* +* @author Arthur Vercruysse +*/ +public class BeaconAggregator extends TCPFactory implements Callback> { + + public BeaconAggregator(int port) throws IOException { + // Does not work, java can't handle cool code + // super((s) -> new Beacon(s, this), port); + super(port); + super.creator = (s) -> { + new Beacon(s, this); + return null; + }; + } + + public Void handle(Event event) { + // this is the handler for event. + // Sending the data to the correct handlers set by TCPFactory + event.handle(this); + return null; + } +} diff --git a/src/main/java/telraam/beacon/BeaconMessage.java b/src/main/java/telraam/beacon/BeaconMessage.java new file mode 100644 index 0000000..558edc2 --- /dev/null +++ b/src/main/java/telraam/beacon/BeaconMessage.java @@ -0,0 +1,18 @@ +package telraam.beacon; + +/** +* BeaconMessage is the representation of what is received from a beacon. +* This should parse the incoming byte[]. +* +* @author Arthur Vercruysse +*/ +public class BeaconMessage { + public static final int MESSAGESIZE = 10; + + public byte[] data; + + // DO NOT STORE THIS DATA, IT WILL BE OVERWRITTEN + public BeaconMessage(byte[] data) { + this.data = data; + } +} diff --git a/src/main/java/telraam/beacon/Callback.java b/src/main/java/telraam/beacon/Callback.java new file mode 100644 index 0000000..d6e9a53 --- /dev/null +++ b/src/main/java/telraam/beacon/Callback.java @@ -0,0 +1,10 @@ +package telraam.beacon; + +/** +* Stupid interface for callbacks. You mind if I request Callback? +* +* @author Arthur Vercruysse +*/ +public interface Callback { + public Output handle(Input value); +} diff --git a/src/main/java/telraam/beacon/Event.java b/src/main/java/telraam/beacon/Event.java new file mode 100644 index 0000000..887089c --- /dev/null +++ b/src/main/java/telraam/beacon/Event.java @@ -0,0 +1,63 @@ +package telraam.beacon; + +/** +* Event is an 'enum' class with data attached. +* An events can be handled with an EventHandler like a TCPFactory. +* `event.handle(this);` +* +* @author Arthur Vercruysse +*/ +public abstract class Event { + abstract void handle(EventHandler h); + + public static class Data extends Event { + public B inner; + + public Data(B data) { + inner = data; + } + + void handle(EventHandler h) { + h.data(inner); + } + } + + public static class Error extends Event { + public Exception inner; + + public Error(Exception e) { + inner = e; + } + + void handle(EventHandler h) { + h.error(inner); + } + } + + public static class Connect extends Event { + public Connect() { + } + + void handle(EventHandler h) { + h.connect(); + } + } + + public static class Exit extends Event { + public Exit() { + } + + void handle(EventHandler h) { + h.exit(); + } + } + + public interface EventHandler { + void exit(); + void connect(); + + void error(Exception e); + + void data(B b); + } +} diff --git a/src/main/java/telraam/beacon/EventGenerator.java b/src/main/java/telraam/beacon/EventGenerator.java new file mode 100644 index 0000000..5db062a --- /dev/null +++ b/src/main/java/telraam/beacon/EventGenerator.java @@ -0,0 +1,35 @@ +package telraam.beacon; + +/** +* Callback> wrapper in disguise. +* Exposing simpler methods to wrap in the right Event. +* +* @author Arthur Vercruysse +*/ +public abstract class EventGenerator { + protected Callback> handler; + + public EventGenerator(Callback> handler) { + this.handler = handler; + } + + protected void connect() { + handle(new Event.Connect<>()); + } + + protected void data(B data) { + handle(new Event.Data<>(data)); + } + + protected void error(Exception e) { + handle(new Event.Error<>(e)); + } + + protected void exit() { + handle(new Event.Exit<>()); + } + + protected void handle(Event event) { + handler.handle(event); + } +} diff --git a/src/main/java/telraam/beacon/README.md b/src/main/java/telraam/beacon/README.md new file mode 100644 index 0000000..a3651f6 --- /dev/null +++ b/src/main/java/telraam/beacon/README.md @@ -0,0 +1,11 @@ +# BEACONS + +Yes I really need a README to explain my shitty code. + +The main class in this package is the BeaconAggregator, which is basically an `Event` generator, B being the thing built from the data, here `BeaconMessages`. + +`BeaconAggregator` extends `TCPFactory` that spawns `Beacon`'s that actually do the generating. On TCPFactories you can subscribe handlers for basic events, like Data (with `onData`), errors (with `onError`), connects (with `onConnect`) and disconnects (with `onDisconnect`). + +So beacons generate events, which is a nice way to hide IO exceptions etc that is wrapped in `Event`. These get handled by `Event.EventHandlers` like the `TCPFactory`. + +TODO: Function scoping, eg the functions `exit` `error` and `data` should not be public, but interfaces bla bla bla. diff --git a/src/main/java/telraam/beacon/TCPFactory.java b/src/main/java/telraam/beacon/TCPFactory.java new file mode 100644 index 0000000..bc74ddc --- /dev/null +++ b/src/main/java/telraam/beacon/TCPFactory.java @@ -0,0 +1,87 @@ +package telraam.beacon; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.List; +import java.util.ArrayList; + +import java.util.logging.Level; +import java.util.logging.Logger; + +/** +* The meat and potato's, but actually just spawning new connections with a creator, +* and exposing subscriber pattern. +* +* @author Arthur Vercruysse +*/ +public class TCPFactory implements Event.EventHandler, Runnable { + private static Logger logger = Logger.getLogger(TCPFactory.class.getName()); + + private ServerSocket socket; + protected Callback creator; + + protected List> handlers = new ArrayList<>(); + protected List> errorHandlers = new ArrayList<>(); + protected List> exitHandlers = new ArrayList<>(); + protected List> connectHandlers = new ArrayList<>(); + + public TCPFactory(Callback creator, int port) throws IOException { + this(port); + this.creator = creator; + } + + protected TCPFactory(int port) throws IOException { + if (port > 0) + this.socket = new ServerSocket(port); + logger.log(Level.INFO, "Starting tcp on port "+port); + } + + public void run() { + logger.log(Level.INFO, "Actually accepting connections"); + while (true) { + try { + Socket s = socket.accept(); + this.creator.handle(s); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + public TCPFactory onError(Callback handler) { + this.errorHandlers.add(handler); + return this; + } + + public TCPFactory onData(Callback handler) { + this.handlers.add(handler); + return this; + } + + public TCPFactory onDisconnect(Callback handler) { + this.exitHandlers.add(handler); + return this; + } + + public TCPFactory onConnect(Callback handler) { + this.connectHandlers.add(handler); + return this; + } + + public void exit() { + this.exitHandlers.forEach((eh) -> eh.handle(null)); + } + + public void connect() { + this.connectHandlers.forEach((th) -> th.handle(null)); + } + + public void error(Exception e) { + this.errorHandlers.forEach((eh) -> eh.handle(e)); + } + + public void data(B t) { + this.handlers.forEach((th) -> th.handle(t)); + } +} diff --git a/src/test/java/telraam/beacon/BeaconTest.java b/src/test/java/telraam/beacon/BeaconTest.java new file mode 100644 index 0000000..18dfcc7 --- /dev/null +++ b/src/test/java/telraam/beacon/BeaconTest.java @@ -0,0 +1,194 @@ +package telraam.beacon; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.lang.reflect.Field; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.BeforeAll; +import java.util.logging.Logger; +import java.util.logging.Level; + +/** +* Beacon integration test. +* Spoofing ServerSocket and Socket so you can write to it at will. +* TODO: Test socket exception, but I don't really know what could fail. +* +* @author Arthur Vercruysse +*/ +public class BeaconTest { + private static Logger logger = Logger.getLogger(BeaconTest.class.getName()); + + private static final Semaphore barrier = new Semaphore(8); + + static List connectedSockets = new ArrayList<>(); + + public static class OurSocket extends Socket { + private PipedInputStream pis; + private PipedOutputStream pos; + + public OurSocket() throws IOException { + super(); + barrier.acquireUninterruptibly(); + + pis = new PipedInputStream(); + pos = new PipedOutputStream(pis); + } + + public InputStream getInputStream() throws IOException { + return this.pis; + } + + public void write(byte[] buf, boolean acq) throws IOException { + if (acq) { + barrier.acquireUninterruptibly(); + } + + pos.write(buf); + pos.flush(); + } + + public void close() throws IOException { + barrier.acquireUninterruptibly(); + pos.close(); + pis.close(); + } + } + + public static class OurServerSocket extends ServerSocket { + private int connections; + + public OurServerSocket(int connections) throws IOException { + super(); + this.connections = connections; + } + + @Override + public Socket accept() throws IOException { + // Only spawn connections amount of sockets + if (connections < 1) { + barrier.release(); + while (true) { + try { + Thread.sleep(2000); + } catch (Exception e) { + } + } + } + connections--; + OurSocket s = new OurSocket(); + // super.implAccept(s); // This fails, and should not be called + connectedSockets.add(s); + return s; + } + } + + static BeaconAggregator ba; + static AtomicInteger data = new AtomicInteger(); + static AtomicInteger connects = new AtomicInteger(); + static AtomicInteger errors = new AtomicInteger(); + static AtomicInteger exits = new AtomicInteger(); + + @BeforeAll + public static void init() throws Exception { + ba = new BeaconAggregator(-1); + + Field socketField = ba.getClass().getSuperclass().getDeclaredField("socket"); + + socketField.setAccessible(true); + socketField.set(ba, new OurServerSocket(5)); + } + + @Test + public void testEverythingBeacon() throws Exception { + + ba.onConnect((_e) -> { + connects.incrementAndGet(); + barrier.release(); + return null; + }); + + ba.onData((_e) -> { + data.incrementAndGet(); + barrier.release(); + return null; + }); + + ba.onDisconnect((_e) -> { + exits.incrementAndGet(); + barrier.release(); + return null; + }); + + ba.onError((e) -> { + logger.log(Level.SEVERE, "error", e); + errors.incrementAndGet(); + return null; + }); + + barrier.acquire(); + new Thread(ba).start(); + + barrier.acquire(8); + barrier.release(8); + + // Check if all beacons are connected + assertEquals(5, connects.get()); + assertEquals(errors.get(), 0); + + // Check if they can disconnect at will + connectedSockets.remove(0).close(); + + barrier.acquire(8); + barrier.release(8); + + assertEquals(exits.get(), 1); + assertEquals(errors.get(), 0); + + // Check if no beacon messages are sent with incomplete data + // Aka do they buffer correctly? + for (OurSocket s: connectedSockets) { + s.write("hadeksfd".getBytes(), false); + } + + barrier.acquire(8); + barrier.release(8); + + assertEquals(data.get(), 0); + assertEquals(errors.get(), 0); + + // But not too much either + for (OurSocket s: connectedSockets) { + s.write("dsa".getBytes(), true); + } + + barrier.acquire(8); + barrier.release(8); + + assertEquals(data.get(), connectedSockets.size()); + assertEquals(errors.get(), 0); + + // Do they all close correctly + for (OurSocket s: connectedSockets) { + s.close(); + } + + barrier.acquire(8); + barrier.release(8); + + assertEquals(exits.get(), 5); + + // No errors received + assertEquals(errors.get(), 0); + } +}